//! Copies of features (timeline Copy / Paste): references to sketches and features inside the //! copied set are moved to the copies. use std::collections::BTreeMap; use crate::{FeatureKind, PlaneRef}; /// Remap persistent topology tokens without rewriting arbitrary user expressions or labels. pub fn remap_topology(name: &str, ids: &BTreeMap, bodies: &BTreeMap) -> String { // An older design's name keeps its mark; what follows it is remapped as any name. if let Some(old) = crate::naming::legacy_name(name) { return format!("{}{}", crate::naming::LEGACY_MARK, remap_topology(old, ids, bodies)); } // A flat face of a sheet: `sheet:::`, the panel a flange's feature token. if let Some((body, face)) = crate::sheet::split_sheet_face_name(name) { return crate::sheet::sheet_face_name(bodies.get(body).map_or(body, String::as_str), &remap_topology(face, ids, bodies)); } // Imported names belong to the complete canonical body prefix, even when that prefix // happens to contain text resembling a feature token. if let Some((body, suffix)) = name.rsplit_once(":face:") && let Some(new) = bodies.get(body) { return format!("{new}:face:{suffix}"); } let mut out = String::new(); let mut at = 0; while at < name.len() { let rest = &name[at..]; // A feature token starts a name, or one inside it: after `@`, `:` or `|`, or in a // piece's list of the faces around it (`[`, `,`). if rest.starts_with('F') && (at == 0 || matches!(name.as_bytes().get(at - 1), Some(b'@' | b':' | b'|' | b'[' | b','))) { let end = rest.as_bytes().iter().skip(1).take_while(|b| b.is_ascii_digit()).count() + 1; if end > 1 && matches!(rest.as_bytes().get(end), Some(b':') | Some(b'.')) && let Ok(id) = rest[1..end].parse::() && let Some(new) = ids.get(&id) { out.push_str(&format!("F{new}")); at += end; continue; } } let Some(ch) = rest.chars().next() else { break }; out.push(ch); at += ch.len_utf8(); } // Import faces have a canonical body prefix rather than a feature token. if let Some((body, suffix)) = out.rsplit_once(":face:") && let Some(new) = bodies.get(body) { return format!("{new}:face:{suffix}"); } out } fn mapped_name(n: &mut String, names: &BTreeMap) { if let Some(new) = names.get(n) { *n = new.clone(); } } /// Simultaneous generated-parameter substitution: map values may themselves be old keys. pub fn clone_expression(source: &str, parameters: &BTreeMap) -> String { let mut result = source.to_string(); let mut temporary = Vec::new(); for (index, (old, new)) in parameters.iter().enumerate() { let mut token = format!("__clone_reference_{index}"); while source.contains(&token) || parameters.values().any(|n| n == &token) { token.push('_'); } result = crate::expr::rename_reference(&result, old, &token); temporary.push((token, new)); } for (token, new) in temporary { result = crate::expr::rename_reference(&result, &token, new); } result } fn remap_geo( g: &mut crate::construct::GeoRef, ids: &BTreeMap, names: &BTreeMap, bodies: &BTreeMap, depth: usize, ) -> crate::Result<()> { use crate::construct::GeoRef; if depth > 64 { return Err(crate::DocError::Invalid("clone plane reference depth exceeded".into())); } match g { GeoRef::SketchPoint { sketch, .. } | GeoRef::SketchCurve { sketch, .. } => { if let Some(new) = ids.get(sketch) { *sketch = *new; } } GeoRef::Face { name: Some(n), .. } | GeoRef::Edge { name: Some(n), .. } => *n = remap_topology(n, ids, bodies), GeoRef::ConstructPoint { name } | GeoRef::Axis { name } => mapped_name(name, names), GeoRef::Plane { plane } => remap_plane(plane, ids, names, bodies, depth + 1)?, _ => {} } Ok(()) } fn remap_plane( p: &mut PlaneRef, ids: &BTreeMap, names: &BTreeMap, bodies: &BTreeMap, depth: usize, ) -> crate::Result<()> { if depth > 64 { return Err(crate::DocError::Invalid("clone plane reference depth exceeded".into())); } match p { PlaneRef::Offset { base, .. } | PlaneRef::AtAngle { base, .. } => remap_plane(base, ids, names, bodies, depth + 1)?, PlaneRef::Construction { name } => mapped_name(name, names), PlaneRef::Face { name, anchor, .. } => { if let Some(name) = name { *name = remap_topology(name, ids, bodies); } if let Some(anchor) = anchor { anchor.remap_target(ids, bodies)?; } } _ => { for r in p.refs_mut() { remap_geo(r, ids, names, bodies, depth + 1)?; } } } Ok(()) } fn remap_link( source: &mut solidcraft_sketch::LinkSource, ids: &BTreeMap, names: &BTreeMap, bodies: &BTreeMap, depth: usize, ) { use solidcraft_sketch::LinkSource; if depth > 64 { return; } match source { LinkSource::Edge { body, .. } | LinkSource::Face { body, .. } | LinkSource::Body { body } | LinkSource::Vertex { body, .. } | LinkSource::Iso { body, .. } | LinkSource::Spun { body, .. } => mapped_name(body, bodies), LinkSource::SketchCurve { sketch, .. } | LinkSource::SketchPoint { sketch, .. } => { if let Some(new) = ids.get(sketch) { *sketch = *new; } } LinkSource::OnSurface { sketch, body, .. } => { if let Some(new) = ids.get(sketch) { *sketch = *new; } mapped_name(body, bodies); } LinkSource::Intersection { a, b } => { remap_link(a, ids, names, bodies, depth + 1); remap_link(b, ids, names, bodies, depth + 1); } LinkSource::Plane { name } => mapped_name(name, names), _ => {} } } 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::SurfaceExtend { up_to: Some(crate::ExtendTo::Plane(plane)), .. } => checked_plane(plane, 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.placement_exprs_mut() { *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)?; for c in &mut sketch.constraints { if let Some(n) = &mut c.param { mapped_name(n, parameters); } } for e in sketch.text_exprs_mut() { *e = clone_expression(e, parameters); } for link in &mut sketch.links { remap_link(&mut link.source, ids, names, bodies, 0); for n in link.face_name.iter_mut().chain(&mut link.edge_name) { *n = remap_topology(n, ids, bodies); } } } K::ConstructionPlane { plane } | K::Split { plane, .. } | K::SplitFace { plane, .. } | K::Mirror { plane, .. } | K::SurfaceTrim { plane, .. } => remap_plane(plane, ids, names, bodies, 0)?, K::Draft { neutral, .. } => remap_plane(neutral, ids, names, bodies, 0)?, K::ReplaceFace { target, .. } => remap_plane(target, ids, names, bodies, 0)?, K::SurfaceExtend { up_to: Some(crate::ExtendTo::Plane(plane)), .. } => remap_plane(plane, ids, names, bodies, 0)?, K::ConstructionAxis { def } => { for r in def.refs_mut() { remap_geo(r, ids, names, bodies, 0)?; } } K::ConstructionPoint { def } => { for r in def.refs_mut() { remap_geo(r, ids, names, bodies, 0)?; } } _ => {} } Ok(()) } } fn checked_geo(g: &crate::construct::GeoRef, depth: usize) -> crate::Result<()> { if depth > 64 { return Err(crate::DocError::Invalid("copy reference nesting exceeds 64".into())); } if let crate::construct::GeoRef::Plane { plane } = g { checked_plane(plane, depth + 1)?; } Ok(()) } fn checked_plane(p: &PlaneRef, depth: usize) -> crate::Result<()> { if depth > 64 { return Err(crate::DocError::Invalid("copy reference nesting exceeds 64".into())); } match p { PlaneRef::Offset { base, .. } | PlaneRef::AtAngle { base, .. } => checked_plane(base, depth + 1)?, PlaneRef::Midplane { a, b } | PlaneRef::TwoEdges { a, b } => { checked_geo(a, depth + 1)?; checked_geo(b, depth + 1)?; } PlaneRef::Perpendicular { plane, line } => { checked_geo(plane, depth + 1)?; checked_geo(line, depth + 1)?; } PlaneRef::ThreePoints { a, b, c } => { checked_geo(a, depth + 1)?; checked_geo(b, depth + 1)?; checked_geo(c, depth + 1)?; } PlaneRef::Tangent { face, .. } => checked_geo(face, depth + 1)?, PlaneRef::AlongPath { path, .. } => checked_geo(path, depth + 1)?, _ => {} } Ok(()) } fn checked_link(source: &solidcraft_sketch::LinkSource, depth: usize) -> crate::Result<()> { if depth > 64 { return Err(crate::DocError::Invalid("copy link nesting exceeds 64".into())); } if let solidcraft_sketch::LinkSource::Intersection { a, b } = source { checked_link(a, depth + 1)?; checked_link(b, depth + 1)?; } Ok(()) } #[cfg(test)] mod clone_tests { use super::*; #[test] fn imported_names_and_topology_tokens_have_separate_identity() { let ids = BTreeMap::from([(1, 2), (10, 20)]); let bodies = BTreeMap::from([("F1:Housing".into(), "F1:Housing (copy)".into())]); assert_eq!(remap_topology("F1:Housing:face:0", &ids, &bodies), "F1:Housing (copy):face:0"); assert_eq!(remap_topology("F1:+x|F10:end@F1.2", &ids, &bodies), "F2:+x|F20:end@F2.2"); assert_eq!(remap_topology("UserF1:label", &ids, &bodies), "UserF1:label"); } #[test] fn a_sheet_face_follows_its_body_and_its_flange() { let ids = BTreeMap::from([(1, 2), (10, 20)]); let bodies = BTreeMap::from([("Body1".into(), "Body1 (copy)".into())]); assert_eq!(remap_topology("sheet:Body1:F1.0:top", &ids, &bodies), "sheet:Body1 (copy):F2.0:top"); assert_eq!(remap_topology("sheet:Body1:base:bottom", &ids, &bodies), "sheet:Body1 (copy):base:bottom"); assert_eq!(remap_topology("sheet:Other:F10.1:top", &ids, &bodies), "sheet:Other:F20.1:top"); } #[test] fn parameter_remap_is_simultaneous_and_token_scoped() { let map = BTreeMap::from([("d1".into(), "d2".into()), ("d2".into(), "d3".into())]); assert_eq!(clone_expression("d1 + d2 + d10 + 2 mm", &map), "d2 + d3 + d10 + 2 mm"); } }