1use std::collections::HashMap;
32
33use ogeom_core::{OgeomResult, ogeom_bail};
34use ogeom_math::Transform;
35use ogeom_topo::{Location, Model, NodeData, Shape, ShapeType, TShapeId};
36
37use crate::history::{Built, History};
38
39pub mod roles {
41 use ogeom_core::Role;
42
43 pub const COPY: Role = Role::op_defined(30);
45}
46
47pub fn transformed(model: &mut Model, shape: &Shape, transform: Transform) -> OgeomResult<Built> {
66 if model.node(shape).is_none() {
67 ogeom_bail!(Dangling, "shape refers to a node not in this model");
68 }
69 model.begin_operation();
70 let datum = model.add_datum(transform);
71 let moved = shape.moved(&Location::of(datum));
72
73 let mut history = History::new();
74 history.modify(shape, moved.clone());
75 Ok(Built::new(moved, history))
76}
77
78pub fn copied(model: &mut Model, shape: &Shape) -> OgeomResult<Built> {
87 if model.node(shape).is_none() {
88 ogeom_bail!(Dangling, "shape refers to a node not in this model");
89 }
90 model.begin_operation();
91
92 let mut done: HashMap<TShapeId, Shape> = HashMap::new();
93 let mut history = History::new();
94 let bare = duplicate(model, shape, &mut done, &mut history)?;
95 let root = bare.moved(shape.location()).composed(shape.orientation());
97 Ok(Built::new(root, history))
98}
99
100fn duplicate(
121 model: &mut Model,
122 shape: &Shape,
123 done: &mut HashMap<TShapeId, Shape>,
124 history: &mut History,
125) -> OgeomResult<Shape> {
126 if let Some(existing) = done.get(&shape.node()) {
127 return Ok(existing.clone());
128 }
129
130 let Some(node) = model.node(shape) else {
131 ogeom_bail!(Dangling, "shape refers to a node not in this model");
132 };
133 let kind = node.kind();
134 let data = node.data().clone();
135 let stored = node.children().to_vec();
136
137 let mut children = Vec::new();
140 for raw in &stored {
141 let world = raw.moved(shape.location()).composed(shape.orientation());
144 let bare = duplicate(model, &world, done, history)?;
145 children.push(Shape::new(
151 bare.node(),
152 raw.location().clone(),
153 raw.orientation(),
154 ));
155 }
156
157 let fresh = match (kind, data) {
158 (ShapeType::Vertex, NodeData::Vertex(v)) => model.add_vertex(v),
159 (ShapeType::Edge, NodeData::Edge(e)) => model.add_edge(*e, &children)?,
160 (ShapeType::Wire, _) => model.add_wire(&children)?,
161 (ShapeType::Face, NodeData::Face(f)) => model.add_face(*f, &children)?,
162 (ShapeType::Shell, _) => model.add_shell(&children)?,
163 (ShapeType::Solid, _) => model.add_solid(&children)?,
164 (ShapeType::CompSolid, _) => model.add_compsolid(&children)?,
165 (ShapeType::Compound, _) => model.add_compound(&children)?,
166 (other, _) => ogeom_bail!(
167 Construction,
168 "a {other:?} node does not hold the data its kind requires, so it \
169 cannot be copied"
170 ),
171 };
172
173 let bare = Shape::of(fresh.node());
176 model.set_derived(&bare, std::slice::from_ref(shape), roles::COPY)?;
177 history.modify(shape, bare.clone());
178 done.insert(shape.node(), bare.clone());
179
180 Ok(bare)
181}
182
183#[cfg(test)]
184#[allow(clippy::unwrap_used, clippy::expect_used)]
185mod tests {
186 use super::*;
187 use crate::check::check;
188 use crate::mass::volume_properties;
189 use crate::{make_box, make_cylinder};
190 use approx::assert_relative_eq;
191 use ogeom_core::Tolerances;
192 use ogeom_math::{Axis, Direction, Frame, Point, Vector};
193 use ogeom_mesh::Deflection;
194 use ogeom_topo::explore_unique;
195
196 const T: Tolerances = Tolerances::millimetres();
197
198 fn deflection() -> Deflection {
199 Deflection {
200 chord: 0.01,
201 ..Deflection::default()
202 }
203 }
204
205 #[test]
206 fn a_rigid_move_creates_no_topology_at_all() {
207 let mut model = Model::new();
210 let solid = make_box(&mut model, Frame::WORLD, (1.0, 2.0, 3.0), T)
211 .unwrap()
212 .shape;
213 let before = model.node_count();
214
215 let moved = transformed(&mut model, &solid, Transform::translation(Vector::X * 10.0))
216 .unwrap()
217 .shape;
218
219 assert_eq!(model.node_count(), before, "a placement copied something");
220 assert!(moved.is_partner(&solid), "the same topology, elsewhere");
221 assert!(!moved.is_same(&solid), "but at a different placement");
222 }
223
224 #[test]
225 fn a_moved_shape_measures_the_same_and_sits_elsewhere() {
226 let mut model = Model::new();
227 let solid = make_box(&mut model, Frame::WORLD, (1.0, 2.0, 3.0), T)
228 .unwrap()
229 .shape;
230 let offset = Vector::new(10.0, -20.0, 30.0);
231 let moved = transformed(&mut model, &solid, Transform::translation(offset))
232 .unwrap()
233 .shape;
234
235 let here = volume_properties(&model, &solid, deflection(), T).unwrap();
236 let there = volume_properties(&model, &moved, deflection(), T).unwrap();
237 assert_relative_eq!(here.mass, there.mass, epsilon = 1e-9);
238 assert!(there.centre.distance(here.centre + offset) < 1e-9);
239
240 assert!(check(&model, &moved, T).unwrap().is_valid());
241 }
242
243 #[test]
244 fn a_rotation_is_a_placement_and_a_reflection_still_is() {
245 let mut model = Model::new();
246 let solid = make_box(&mut model, Frame::WORLD, (1.0, 2.0, 3.0), T)
247 .unwrap()
248 .shape;
249 for transform in [
250 Transform::rotation(Axis::Z, 0.7),
251 Transform::scaling(Point::ORIGIN, 2.0, T).unwrap(),
252 Transform::plane_mirror(Point::ORIGIN, Direction::Z),
253 ] {
254 assert!(
255 transformed(&mut model, &solid, transform).is_ok(),
256 "a rigid or uniformly scaled motion should be a placement"
257 );
258 }
259 }
260
261 #[test]
262 fn a_uniform_scale_scales_the_volume_by_its_cube() {
263 let mut model = Model::new();
264 let solid = make_box(&mut model, Frame::WORLD, (1.0, 1.0, 1.0), T)
265 .unwrap()
266 .shape;
267 let bigger = transformed(
268 &mut model,
269 &solid,
270 Transform::scaling(Point::ORIGIN, 3.0, T).unwrap(),
271 )
272 .unwrap()
273 .shape;
274 let props = volume_properties(&model, &bigger, deflection(), T).unwrap();
275 assert_relative_eq!(props.mass, 27.0, epsilon = 1e-9);
276 }
277
278 #[test]
279 fn a_copy_has_its_own_topology_and_the_same_geometry() {
280 let mut model = Model::new();
281 let solid = make_box(&mut model, Frame::WORLD, (2.0, 2.0, 2.0), T)
282 .unwrap()
283 .shape;
284 let (curves, pcurves, surfaces) = model.geometry().counts();
285
286 let copy = copied(&mut model, &solid).unwrap().shape;
287 assert!(!copy.is_partner(&solid), "a copy is not the same topology");
288 assert_eq!(
289 model.geometry().counts(),
290 (curves, pcurves, surfaces),
291 "geometry is immutable and shared; copying it buys nothing"
292 );
293
294 assert!(check(&model, ©, T).unwrap().is_valid());
296 let props = volume_properties(&model, ©, deflection(), T).unwrap();
297 assert_relative_eq!(props.mass, 8.0, epsilon = 1e-9);
298 }
299
300 #[test]
301 fn a_copy_keeps_shared_edges_shared() {
302 let mut model = Model::new();
306 let solid = make_cylinder(&mut model, Frame::WORLD, 2.0, 3.0, T)
307 .unwrap()
308 .shape;
309 let copy = copied(&mut model, &solid).unwrap().shape;
310
311 for kind in [
312 ShapeType::Face,
313 ShapeType::Edge,
314 ShapeType::Vertex,
315 ShapeType::Wire,
316 ] {
317 assert_eq!(
318 explore_unique(&model, ©, kind).unwrap().len(),
319 explore_unique(&model, &solid, kind).unwrap().len(),
320 "the copy has a different number of {kind:?}"
321 );
322 }
323 assert!(check(&model, ©, T).unwrap().is_valid());
324 }
325
326 #[test]
327 fn a_copy_of_a_prism_with_instanced_caps_is_still_a_closed_solid() {
328 let mut model = Model::new();
335 let solid = prism_of_a_square(&mut model);
336
337 let copy = copied(&mut model, &solid).unwrap().shape;
338
339 let diagnosis = check(&model, ©, T).unwrap();
340 assert!(diagnosis.is_valid(), "{:?}", diagnosis.problems);
341 let props = volume_properties(&model, ©, deflection(), T).unwrap();
342 assert_relative_eq!(props.mass, 500.0, epsilon = 1e-9);
343
344 for kind in [
346 ShapeType::Face,
347 ShapeType::Edge,
348 ShapeType::Vertex,
349 ShapeType::Wire,
350 ] {
351 assert_eq!(
352 explore_unique(&model, ©, kind).unwrap().len(),
353 explore_unique(&model, &solid, kind).unwrap().len(),
354 "the copy has a different number of {kind:?}"
355 );
356 }
357 }
358
359 #[test]
360 fn a_copy_of_a_prism_bakes_cleanly() {
361 let mut model = Model::new();
366 let solid = prism_of_a_square(&mut model);
367 let copy = copied(&mut model, &solid).unwrap().shape;
368
369 let baked = crate::convert::baked_shape(&mut model, ©, T)
370 .unwrap()
371 .shape;
372
373 let diagnosis = check(&model, &baked, T).unwrap();
374 assert!(diagnosis.is_valid(), "{:?}", diagnosis.problems);
375 let props = volume_properties(&model, &baked, deflection(), T).unwrap();
376 assert_relative_eq!(props.mass, 500.0, epsilon = 1e-6);
377 }
378
379 fn prism_of_a_square(model: &mut Model) -> Shape {
381 use crate::build::{make_face_with_pcurves, make_polygon};
382 use crate::sweep::make_prism;
383 use ogeom_geom::{PlaneSurface, SurfaceGeometry};
384 use ogeom_math::Plane;
385
386 let pts = [
387 Point::new(0.0, 0.0, 0.0),
388 Point::new(10.0, 0.0, 0.0),
389 Point::new(10.0, 10.0, 0.0),
390 Point::new(0.0, 10.0, 0.0),
391 ];
392 let wire = make_polygon(model, &pts, true, T).unwrap().shape;
393 let surface = PlaneSurface::over(Plane::XY, (-1.0, 11.0), (-1.0, 11.0)).unwrap();
394 let edges = model.children_of(&wire).unwrap();
395 let face = make_face_with_pcurves(model, SurfaceGeometry::Plane(surface), &[edges], T)
396 .unwrap()
397 .shape;
398 make_prism(model, &face, Vector::new(0.0, 0.0, 5.0), T)
399 .unwrap()
400 .shape
401 }
402
403 #[test]
404 fn a_copy_names_what_it_came_from() {
405 let mut model = Model::new();
406 let solid = make_box(&mut model, Frame::WORLD, (1.0, 1.0, 1.0), T)
407 .unwrap()
408 .shape;
409 let built = copied(&mut model, &solid).unwrap();
410
411 assert_eq!(
412 model
413 .provenance_of(&built.shape)
414 .and_then(ogeom_core::Provenance::role),
415 Some(roles::COPY)
416 );
417 assert_eq!(
420 built.history.modified(&solid),
421 std::slice::from_ref(&built.shape)
422 );
423 assert!(!built.history.is_deleted(&solid));
424 }
425
426 #[test]
427 fn placing_or_copying_a_stranger_is_an_error() {
428 let mut other = Model::new();
429 for _ in 0..4 {
430 other.add_vertex(ogeom_topo::VertexData::new(Point::ORIGIN));
431 }
432 let beyond = other.add_vertex(ogeom_topo::VertexData::new(Point::ORIGIN));
433
434 let mut empty = Model::new();
435 assert!(transformed(&mut empty, &beyond, Transform::IDENTITY).is_err());
436 assert!(copied(&mut empty, &beyond).is_err());
437 }
438}