diff --git a/crates/doc/src/clipboard.rs b/crates/doc/src/clipboard.rs index 2e0f7d0f..0b425e64 100644 --- a/crates/doc/src/clipboard.rs +++ b/crates/doc/src/clipboard.rs @@ -243,69 +243,19 @@ fn remap_link( } } -impl crate::Feature { - /// Clone identities through typed reference fields. Sketch-local entity IDs stay local. - pub fn remap_clone( - &mut self, - ids: &BTreeMap, - names: &BTreeMap, - bodies: &BTreeMap, - parameters: &BTreeMap, - ) -> crate::Result<()> { +impl FeatureKind { + /// Point the body names a feature acts on (targets, tools, the body it rounds…) at the + /// copies in `bodies`; names not in the map stay as they are. + pub fn remap_bodies(&mut self, bodies: &BTreeMap) { use FeatureKind as K; - match &self.kind { - K::Sketch { plane, sketch } => { - checked_plane(plane, 0)?; - for link in &sketch.links { - checked_link(&link.source, 0)?; - } - } - K::ConstructionPlane { plane } - | K::Split { plane, .. } - | K::SplitFace { plane, .. } - | K::Mirror { plane, .. } - | K::SurfaceTrim { plane, .. } => checked_plane(plane, 0)?, - K::Draft { neutral, .. } => checked_plane(neutral, 0)?, - K::ReplaceFace { target, .. } => checked_plane(target, 0)?, - K::ConstructionAxis { def } => { - for r in def.refs() { - checked_geo(r, 0)?; - } - } - K::ConstructionPoint { def } => { - for r in def.refs() { - checked_geo(r, 0)?; - } - } - _ => {} - } - self.kind.remap_refs(ids, names); - for n in &mut self.body_names { - mapped_name(n, bodies); - } - for n in &mut self.participants { - mapped_name(n, bodies); - } - for n in &mut self.param_names { - mapped_name(n, parameters); - } - for n in self.face_names.iter_mut().chain(&mut self.edge_names) { - *n = remap_topology(n, ids, bodies); - } - for (_, e, _) in self.kind.inputs_mut() { - *e = clone_expression(e, parameters); - } - for e in self.kind.point_expr_mut().into_iter().flatten() { - *e = clone_expression(e, parameters); - } - match &mut self.kind { + match self { K::Emboss { face: Some(face), .. } => mapped_name(&mut face.body, bodies), K::Split { tool: Some(tool), .. } | K::SplitFace { tool: Some(tool), .. } | K::SurfaceTrim { tool: Some(tool), .. } => { mapped_name(&mut tool.body, bodies) } _ => {} } - match &mut self.kind { + match self { K::Extrude { targets, .. } | K::Revolve { targets, .. } | K::Loft { targets, .. } @@ -373,6 +323,65 @@ impl crate::Feature { | K::MeshConvert { body, .. } => mapped_name(body, bodies), _ => {} } + } +} + +impl crate::Feature { + /// Clone identities through typed reference fields. Sketch-local entity IDs stay local. + pub fn remap_clone( + &mut self, + ids: &BTreeMap, + names: &BTreeMap, + bodies: &BTreeMap, + parameters: &BTreeMap, + ) -> crate::Result<()> { + use FeatureKind as K; + match &self.kind { + K::Sketch { plane, sketch } => { + checked_plane(plane, 0)?; + for link in &sketch.links { + checked_link(&link.source, 0)?; + } + } + K::ConstructionPlane { plane } + | K::Split { plane, .. } + | K::SplitFace { plane, .. } + | K::Mirror { plane, .. } + | K::SurfaceTrim { plane, .. } => checked_plane(plane, 0)?, + K::Draft { neutral, .. } => checked_plane(neutral, 0)?, + K::ReplaceFace { target, .. } => checked_plane(target, 0)?, + K::ConstructionAxis { def } => { + for r in def.refs() { + checked_geo(r, 0)?; + } + } + K::ConstructionPoint { def } => { + for r in def.refs() { + checked_geo(r, 0)?; + } + } + _ => {} + } + self.kind.remap_refs(ids, names); + for n in &mut self.body_names { + mapped_name(n, bodies); + } + for n in &mut self.participants { + mapped_name(n, bodies); + } + for n in &mut self.param_names { + mapped_name(n, parameters); + } + for n in self.face_names.iter_mut().chain(&mut self.edge_names) { + *n = remap_topology(n, ids, bodies); + } + for (_, e, _) in self.kind.inputs_mut() { + *e = clone_expression(e, parameters); + } + for e in self.kind.point_expr_mut().into_iter().flatten() { + *e = clone_expression(e, parameters); + } + self.kind.remap_bodies(bodies); match &mut self.kind { K::Sketch { plane, sketch } => { remap_plane(plane, ids, names, bodies, 0)?; diff --git a/crates/engine/src/cmd/clipboard.rs b/crates/engine/src/cmd/clipboard.rs index 238abb7c..7a7355e6 100644 --- a/crates/engine/src/cmd/clipboard.rs +++ b/crates/engine/src/cmd/clipboard.rs @@ -72,6 +72,8 @@ fn paste(s: &mut Session, p: &Value) -> Result { for f in feats { let mut kind = f.kind.clone(); kind.remap_refs(&sketches, &names); + // A body made by an earlier copy is the one this copy acts on (#613). + kind.remap_bodies(&bodies); if let FeatureKind::Sketch { plane, .. } = &mut kind && let Some(anchor) = super::sketch::face::anchor_mut(plane) { @@ -208,4 +210,25 @@ mod tests { let mut t = Session::default(); assert!(t.execute("timeline.paste", &json!({})).is_err()); } + + #[test] + fn pasted_fillet_rounds_the_copied_body_it_names() { + let mut s = Session::default(); + run(&mut s, "sketch.create", json!({"plane": "XY", "name": "S"})); + run(&mut s, "sketch.rectangle.two_point", json!({"p0": [0, 0], "p1": [40, 20]})); + run(&mut s, "sketch.finish", json!({})); + run(&mut s, "solid.extrude", json!({"sketch": "S", "distance": 10, "operation": "new", "body_name": "Block", "name": "Block"})); + run(&mut s, "solid.fillet", json!({"body": "Block", "radius": 2, "edges": [[20, 0, 10], [20, 20, 10]], "name": "Round"})); + let one = volume(&mut s); + assert!(one < 8000.0 - 1.0, "the fillet removes material: {one}"); + run(&mut s, "timeline.copy", json!({"features": ["S", "Block", "Round"]})); + let r = run(&mut s, "timeline.paste", json!({"translate": [0, 40, 0]})); + assert_eq!(r["errors"].as_array().map(Vec::len), Some(0), "{r}"); + let round = r["pasted"][2]["name"].as_str().unwrap_or_default().to_string(); + let def = run(&mut s, "timeline.inspect", json!({"feature": round})); + assert_eq!(def["definition"]["body"], "Block (copy)", "{def}"); + let m = run(&mut s, "inspect.measure", json!({})); + assert_eq!(m["body_count"], 2, "{m}"); + assert!((volume(&mut s) - 2.0 * one).abs() < 1e-6, "both blocks are rounded: {m}"); + } }