1use ogeom_core::FastMap;
32
33use ogeom_core::{OgeomResult, ogeom_bail};
34use ogeom_math::Transform;
35use ogeom_topo::{Location, Model, NodeData, Shape, ShapeType, TShapeId, explore_unique};
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> {
73 if model.node(shape).is_none() {
74 ogeom_bail!(Dangling, "shape refers to a node not in this model");
75 }
76 model.begin_operation();
77 let datum = model.add_datum(transform);
78 let by = Location::of(datum);
79 let moved = shape.moved(&by);
80
81 let mut history = History::new();
82 history.modify(shape, moved.clone());
83 for kind in [
84 ShapeType::CompSolid,
85 ShapeType::Solid,
86 ShapeType::Shell,
87 ShapeType::Face,
88 ShapeType::Wire,
89 ShapeType::Edge,
90 ShapeType::Vertex,
91 ] {
92 for part in explore_unique(model, shape, kind)? {
93 if !part.is_same(shape) {
94 let image = part.moved(&by);
95 history.modify(&part, image);
96 }
97 }
98 }
99 Ok(Built::new(moved, history))
100}
101
102pub fn copied(model: &mut Model, shape: &Shape) -> OgeomResult<Built> {
111 if model.node(shape).is_none() {
112 ogeom_bail!(Dangling, "shape refers to a node not in this model");
113 }
114 model.begin_operation();
115
116 let mut done: FastMap<TShapeId, Shape> = FastMap::default();
117 let mut history = History::new();
118 let bare = duplicate(model, shape, &mut done, &mut history)?;
119 let root = bare.moved(shape.location()).composed(shape.orientation());
121 Ok(Built::new(root, history))
122}
123
124fn duplicate(
145 model: &mut Model,
146 shape: &Shape,
147 done: &mut FastMap<TShapeId, Shape>,
148 history: &mut History,
149) -> OgeomResult<Shape> {
150 if let Some(existing) = done.get(&shape.node()) {
151 return Ok(existing.clone());
152 }
153
154 let Some(node) = model.node(shape) else {
155 ogeom_bail!(Dangling, "shape refers to a node not in this model");
156 };
157 let kind = node.kind();
158 let data = node.data().clone();
159 let stored = node.children().to_vec();
160
161 let mut children = Vec::new();
164 for raw in &stored {
165 let world = raw.moved(shape.location()).composed(shape.orientation());
168 let bare = duplicate(model, &world, done, history)?;
169 children.push(Shape::new(
175 bare.node(),
176 raw.location().clone(),
177 raw.orientation(),
178 ));
179 }
180
181 let fresh = match (kind, data) {
182 (ShapeType::Vertex, NodeData::Vertex(v)) => model.add_vertex(v),
183 (ShapeType::Edge, NodeData::Edge(e)) => model.add_edge(*e, &children)?,
184 (ShapeType::Wire, _) => model.add_wire(&children)?,
185 (ShapeType::Face, NodeData::Face(f)) => model.add_face(*f, &children)?,
186 (ShapeType::Shell, _) => model.add_shell(&children)?,
187 (ShapeType::Solid, _) => model.add_solid(&children)?,
188 (ShapeType::CompSolid, _) => model.add_compsolid(&children)?,
189 (ShapeType::Compound, _) => model.add_compound(&children)?,
190 (other, _) => ogeom_bail!(
191 Construction,
192 "a {other:?} node does not hold the data its kind requires, so it \
193 cannot be copied"
194 ),
195 };
196
197 let bare = Shape::of(fresh.node());
200 model.set_derived(&bare, std::slice::from_ref(shape), roles::COPY)?;
201 history.modify(shape, bare.clone());
202 done.insert(shape.node(), bare.clone());
203
204 Ok(bare)
205}
206
207#[cfg(test)]
208#[allow(clippy::unwrap_used, clippy::expect_used)]
209mod tests {
210 use super::*;
211 use crate::check::check;
212 use crate::mass::volume_properties;
213 use crate::{make_box, make_cylinder};
214 use approx::assert_relative_eq;
215 use ogeom_core::Tolerances;
216 use ogeom_math::{Axis, Direction, Frame, Point, Vector};
217 use ogeom_mesh::Deflection;
218
219 const T: Tolerances = Tolerances::millimetres();
220
221 fn deflection() -> Deflection {
222 Deflection {
223 chord: 0.01,
224 ..Deflection::default()
225 }
226 }
227
228 #[test]
229 fn a_rigid_move_creates_no_topology_at_all() {
230 let mut model = Model::new();
233 let solid = make_box(&mut model, Frame::WORLD, (1.0, 2.0, 3.0), T)
234 .unwrap()
235 .shape;
236 let before = model.node_count();
237
238 let moved = transformed(&mut model, &solid, Transform::translation(Vector::X * 10.0))
239 .unwrap()
240 .shape;
241
242 assert_eq!(model.node_count(), before, "a placement copied something");
243 assert!(moved.is_partner(&solid), "the same topology, elsewhere");
244 assert!(!moved.is_same(&solid), "but at a different placement");
245 }
246
247 #[test]
251 fn every_sub_shape_traces_to_its_occurrence_in_the_moved_shape() {
252 let mut model = Model::new();
253 let cube = make_box(&mut model, Frame::WORLD, (10.0, 10.0, 10.0), T)
254 .unwrap()
255 .shape;
256 for transform in [
257 Transform::translation(Vector::new(100.0, 0.0, 0.0)),
258 Transform::plane_mirror(Point::ORIGIN, Direction::X),
259 Transform::scaling(Point::ORIGIN, 2.0, T).unwrap(),
260 ] {
261 let moved = transformed(&mut model, &cube, transform).unwrap();
262 assert!(moved.history.trace(&cube)[0].is_same(&moved.shape));
263 for (kind, count) in [
264 (ShapeType::Face, 6),
265 (ShapeType::Edge, 12),
266 (ShapeType::Vertex, 8),
267 ] {
268 let result = explore_unique(&model, &moved.shape, kind).unwrap();
269 let parts = explore_unique(&model, &cube, kind).unwrap();
270 assert_eq!(parts.len(), count);
271 for part in &parts {
272 assert!(moved.history.is_affected(part));
273 let found: Vec<&Shape> = result
274 .iter()
275 .filter(|r| moved.history.trace(part).iter().any(|t| t.is_same(r)))
276 .collect();
277 assert_eq!(found.len(), 1, "{kind:?}");
278 assert_eq!(found[0].node(), part.node());
279 assert_eq!(model.provenance_of(found[0]), model.provenance_of(part));
280 }
281 }
282 }
283 }
284
285 #[test]
286 fn a_moved_shape_measures_the_same_and_sits_elsewhere() {
287 let mut model = Model::new();
288 let solid = make_box(&mut model, Frame::WORLD, (1.0, 2.0, 3.0), T)
289 .unwrap()
290 .shape;
291 let offset = Vector::new(10.0, -20.0, 30.0);
292 let moved = transformed(&mut model, &solid, Transform::translation(offset))
293 .unwrap()
294 .shape;
295
296 let here = volume_properties(&model, &solid, deflection(), T).unwrap();
297 let there = volume_properties(&model, &moved, deflection(), T).unwrap();
298 assert_relative_eq!(here.mass, there.mass, epsilon = 1e-9);
299 assert!(there.centre.distance(here.centre + offset) < 1e-9);
300
301 assert!(check(&model, &moved, T).unwrap().is_valid());
302 }
303
304 #[test]
305 fn a_rotation_is_a_placement_and_a_reflection_still_is() {
306 let mut model = Model::new();
307 let solid = make_box(&mut model, Frame::WORLD, (1.0, 2.0, 3.0), T)
308 .unwrap()
309 .shape;
310 for transform in [
311 Transform::rotation(Axis::Z, 0.7),
312 Transform::scaling(Point::ORIGIN, 2.0, T).unwrap(),
313 Transform::plane_mirror(Point::ORIGIN, Direction::Z),
314 ] {
315 assert!(
316 transformed(&mut model, &solid, transform).is_ok(),
317 "a rigid or uniformly scaled motion should be a placement"
318 );
319 }
320 }
321
322 #[test]
323 fn a_uniform_scale_scales_the_volume_by_its_cube() {
324 let mut model = Model::new();
325 let solid = make_box(&mut model, Frame::WORLD, (1.0, 1.0, 1.0), T)
326 .unwrap()
327 .shape;
328 let bigger = transformed(
329 &mut model,
330 &solid,
331 Transform::scaling(Point::ORIGIN, 3.0, T).unwrap(),
332 )
333 .unwrap()
334 .shape;
335 let props = volume_properties(&model, &bigger, deflection(), T).unwrap();
336 assert_relative_eq!(props.mass, 27.0, epsilon = 1e-9);
337 }
338
339 #[test]
340 fn a_copy_has_its_own_topology_and_the_same_geometry() {
341 let mut model = Model::new();
342 let solid = make_box(&mut model, Frame::WORLD, (2.0, 2.0, 2.0), T)
343 .unwrap()
344 .shape;
345 let (curves, pcurves, surfaces) = model.geometry().counts();
346
347 let copy = copied(&mut model, &solid).unwrap().shape;
348 assert!(!copy.is_partner(&solid), "a copy is not the same topology");
349 assert_eq!(
350 model.geometry().counts(),
351 (curves, pcurves, surfaces),
352 "geometry is immutable and shared; copying it buys nothing"
353 );
354
355 assert!(check(&model, ©, T).unwrap().is_valid());
357 let props = volume_properties(&model, ©, deflection(), T).unwrap();
358 assert_relative_eq!(props.mass, 8.0, epsilon = 1e-9);
359 }
360
361 #[test]
362 fn a_copy_keeps_shared_edges_shared() {
363 let mut model = Model::new();
367 let solid = make_cylinder(&mut model, Frame::WORLD, 2.0, 3.0, T)
368 .unwrap()
369 .shape;
370 let copy = copied(&mut model, &solid).unwrap().shape;
371
372 for kind in [
373 ShapeType::Face,
374 ShapeType::Edge,
375 ShapeType::Vertex,
376 ShapeType::Wire,
377 ] {
378 assert_eq!(
379 explore_unique(&model, ©, kind).unwrap().len(),
380 explore_unique(&model, &solid, kind).unwrap().len(),
381 "the copy has a different number of {kind:?}"
382 );
383 }
384 assert!(check(&model, ©, T).unwrap().is_valid());
385 }
386
387 #[test]
388 fn a_copy_of_a_prism_with_instanced_caps_is_still_a_closed_solid() {
389 let mut model = Model::new();
396 let solid = prism_of_a_square(&mut model);
397
398 let copy = copied(&mut model, &solid).unwrap().shape;
399
400 let diagnosis = check(&model, ©, T).unwrap();
401 assert!(diagnosis.is_valid(), "{:?}", diagnosis.problems);
402 let props = volume_properties(&model, ©, deflection(), T).unwrap();
403 assert_relative_eq!(props.mass, 500.0, epsilon = 1e-9);
404
405 for kind in [
407 ShapeType::Face,
408 ShapeType::Edge,
409 ShapeType::Vertex,
410 ShapeType::Wire,
411 ] {
412 assert_eq!(
413 explore_unique(&model, ©, kind).unwrap().len(),
414 explore_unique(&model, &solid, kind).unwrap().len(),
415 "the copy has a different number of {kind:?}"
416 );
417 }
418 }
419
420 #[test]
421 fn a_copy_of_a_prism_bakes_cleanly() {
422 let mut model = Model::new();
427 let solid = prism_of_a_square(&mut model);
428 let copy = copied(&mut model, &solid).unwrap().shape;
429
430 let baked = crate::convert::baked_shape(&mut model, ©, T)
431 .unwrap()
432 .shape;
433
434 let diagnosis = check(&model, &baked, T).unwrap();
435 assert!(diagnosis.is_valid(), "{:?}", diagnosis.problems);
436 let props = volume_properties(&model, &baked, deflection(), T).unwrap();
437 assert_relative_eq!(props.mass, 500.0, epsilon = 1e-6);
438 }
439
440 fn prism_of_a_square(model: &mut Model) -> Shape {
442 use crate::build::{make_face_with_pcurves, make_polygon};
443 use crate::sweep::make_prism;
444 use ogeom_geom::{PlaneSurface, SurfaceGeometry};
445 use ogeom_math::Plane;
446
447 let pts = [
448 Point::new(0.0, 0.0, 0.0),
449 Point::new(10.0, 0.0, 0.0),
450 Point::new(10.0, 10.0, 0.0),
451 Point::new(0.0, 10.0, 0.0),
452 ];
453 let wire = make_polygon(model, &pts, true, T).unwrap().shape;
454 let surface = PlaneSurface::over(Plane::XY, (-1.0, 11.0), (-1.0, 11.0)).unwrap();
455 let edges = model.children_of(&wire).unwrap();
456 let face = make_face_with_pcurves(model, SurfaceGeometry::Plane(surface), &[edges], T)
457 .unwrap()
458 .shape;
459 make_prism(model, &face, Vector::new(0.0, 0.0, 5.0), T)
460 .unwrap()
461 .shape
462 }
463
464 #[test]
465 fn a_copy_names_what_it_came_from() {
466 let mut model = Model::new();
467 let solid = make_box(&mut model, Frame::WORLD, (1.0, 1.0, 1.0), T)
468 .unwrap()
469 .shape;
470 let built = copied(&mut model, &solid).unwrap();
471
472 assert_eq!(
473 model
474 .provenance_of(&built.shape)
475 .and_then(ogeom_core::Provenance::role),
476 Some(roles::COPY)
477 );
478 assert_eq!(
481 built.history.modified(&solid),
482 std::slice::from_ref(&built.shape)
483 );
484 assert!(!built.history.is_deleted(&solid));
485 }
486
487 #[test]
488 fn placing_or_copying_a_stranger_is_an_error() {
489 let mut other = Model::new();
490 for _ in 0..4 {
491 other.add_vertex(ogeom_topo::VertexData::new(Point::ORIGIN));
492 }
493 let beyond = other.add_vertex(ogeom_topo::VertexData::new(Point::ORIGIN));
494
495 let mut empty = Model::new();
496 assert!(transformed(&mut empty, &beyond, Transform::IDENTITY).is_err());
497 assert!(copied(&mut empty, &beyond).is_err());
498 }
499}