Files
solidcraft-upstream/crates/doc/src/clipboard.rs
T

342 lines
13 KiB
Rust

//! 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<u64, u64>, bodies: &BTreeMap<String, String>) -> 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:<body>:<panel>:<side>`, 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::<u64>()
&& 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<String, String>) {
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, String>) -> 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<u64, u64>,
names: &BTreeMap<String, String>,
bodies: &BTreeMap<String, String>,
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<u64, u64>,
names: &BTreeMap<String, String>,
bodies: &BTreeMap<String, String>,
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<u64, u64>,
names: &BTreeMap<String, String>,
bodies: &BTreeMap<String, String>,
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<u64, u64>,
names: &BTreeMap<String, String>,
bodies: &BTreeMap<String, String>,
parameters: &BTreeMap<String, String>,
) -> 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:[email protected]", &ids, &bodies), "F2:+x|F20:[email protected]");
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");
}
}