Offsets picked faces along their normals into a new surface body: planes and cylinders move exactly, other faces get a fitted B-spline within tolerance, and offsets that would fold over are refused. Adds the SurfaceOffset feature (noted under format 18), the surface.offset engine command and an Offset Surface dialog. Closes #758. Co-authored-by: jyviko <2501648+jyviko@users.noreply.github.com>
499 lines
19 KiB
Rust
499 lines
19 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, PatternKind, PlaneRef};
|
|
|
|
fn plane_names(p: &mut PlaneRef, names: &BTreeMap<String, String>) {
|
|
let mut p = p;
|
|
for _ in 0..64 {
|
|
match p {
|
|
PlaneRef::Offset { base, .. } | PlaneRef::AtAngle { base, .. } => p = base,
|
|
PlaneRef::Construction { name } => {
|
|
if let Some(n) = names.get(name) {
|
|
*name = n.clone();
|
|
}
|
|
return;
|
|
}
|
|
_ => return,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl FeatureKind {
|
|
/// Point references to sketches (by id) and features (by name) at their copies.
|
|
pub fn remap_refs(&mut self, sketches: &BTreeMap<u64, u64>, names: &BTreeMap<String, String>) {
|
|
let sk = |id: &mut u64| {
|
|
if let Some(n) = sketches.get(id) {
|
|
*id = *n;
|
|
}
|
|
};
|
|
let feats = |list: &mut Vec<String>| {
|
|
for x in list.iter_mut() {
|
|
if let Some(n) = names.get(x) {
|
|
*x = n.clone();
|
|
}
|
|
}
|
|
};
|
|
match self {
|
|
FeatureKind::Sketch { plane, .. } | FeatureKind::ConstructionPlane { plane } | FeatureKind::Split { plane, .. } => {
|
|
plane_names(plane, names)
|
|
}
|
|
FeatureKind::SplitFace { plane, sketch, .. } => {
|
|
plane_names(plane, names);
|
|
if let Some(s) = sketch {
|
|
sk(s);
|
|
}
|
|
}
|
|
FeatureKind::Extrude { sketch, .. }
|
|
| FeatureKind::SurfaceExtrude { sketch, .. }
|
|
| FeatureKind::SurfaceRevolve { sketch, .. }
|
|
| FeatureKind::Revolve { sketch, .. }
|
|
| FeatureKind::Emboss { sketch, .. }
|
|
| FeatureKind::SheetBase { sketch, .. }
|
|
| FeatureKind::SheetContour { sketch, .. }
|
|
| FeatureKind::Rib { sketch, .. } => sk(sketch),
|
|
FeatureKind::Patch { sketch: Some(s), .. } => sk(s),
|
|
FeatureKind::Sweep { sketch, path_sketch, .. } | FeatureKind::SurfaceSweep { sketch, path_sketch, .. } => {
|
|
sk(sketch);
|
|
sk(path_sketch);
|
|
}
|
|
FeatureKind::Pipe { path_sketch, .. } => sk(path_sketch),
|
|
FeatureKind::SheetFold { sketch: Some(s), .. } => sk(s),
|
|
FeatureKind::Loft { sections, .. } => sections.iter_mut().for_each(|s| sk(&mut s.sketch)),
|
|
FeatureKind::SurfaceLoft { sections } => sections.iter_mut().for_each(|s| sk(&mut s.sketch)),
|
|
FeatureKind::Hole { points: Some(p), .. } => sk(&mut p.sketch),
|
|
FeatureKind::Pattern { features, pattern, .. } => {
|
|
feats(features);
|
|
if let PatternKind::Path { path_sketch, .. } = pattern {
|
|
sk(path_sketch);
|
|
}
|
|
}
|
|
FeatureKind::Mirror { features, plane, .. } => {
|
|
feats(features);
|
|
plane_names(plane, names);
|
|
}
|
|
FeatureKind::Draft { neutral, .. } => plane_names(neutral, names),
|
|
FeatureKind::ReplaceFace { target, .. } => plane_names(target, names),
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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 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<String, String>) {
|
|
use FeatureKind as K;
|
|
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 self {
|
|
K::Extrude { targets, .. }
|
|
| K::Revolve { targets, .. }
|
|
| K::Loft { targets, .. }
|
|
| K::Sweep { targets, .. }
|
|
| K::Pipe { targets, .. }
|
|
| K::Emboss { targets, .. } => {
|
|
for n in targets {
|
|
mapped_name(n, bodies);
|
|
}
|
|
}
|
|
K::Combine { target, tools, .. } => {
|
|
mapped_name(target, bodies);
|
|
for n in tools {
|
|
mapped_name(n, bodies);
|
|
}
|
|
}
|
|
K::Pattern { bodies: list, .. }
|
|
| K::Mirror { bodies: list, .. }
|
|
| K::Scale { bodies: list, .. }
|
|
| K::BoundingSolid { bodies: list, .. }
|
|
| K::Align { bodies: list, .. }
|
|
| K::Remove { bodies: list }
|
|
| K::Stitch { bodies: list, .. }
|
|
| K::Move { bodies: list, .. }
|
|
| K::BodyTransfer { bodies: list, .. } => {
|
|
for n in list {
|
|
mapped_name(n, bodies);
|
|
}
|
|
}
|
|
K::Thicken { bodies: list, targets, .. } => {
|
|
for n in list.iter_mut().chain(targets) {
|
|
mapped_name(n, bodies);
|
|
}
|
|
}
|
|
K::BoundaryFill { tools, .. } => {
|
|
for n in tools {
|
|
mapped_name(n, bodies);
|
|
}
|
|
}
|
|
K::Fillet { body, .. }
|
|
| K::Chamfer { body, .. }
|
|
| K::Shell { body, .. }
|
|
| K::Draft { body, .. }
|
|
| K::SplitFace { body, .. }
|
|
| K::OffsetFace { body, .. }
|
|
| K::DeleteFace { body, .. }
|
|
| K::SurfaceOffset { body, .. }
|
|
| K::ReplaceFace { body, .. }
|
|
| K::SheetFlange { body, .. }
|
|
| K::SheetHem { body, .. }
|
|
| K::SheetFold { body, .. }
|
|
| K::SheetUnfold { body, .. }
|
|
| K::Boss { body, .. }
|
|
| K::Lip { body, .. }
|
|
| K::SnapFit { body, .. }
|
|
| K::Rest { body, .. }
|
|
| K::Patch { body, .. } => {
|
|
if let Some(n) = body {
|
|
mapped_name(n, bodies);
|
|
}
|
|
}
|
|
K::Split { body, .. }
|
|
| K::SheetConvert { body, .. }
|
|
| K::SurfaceExtend { body, .. }
|
|
| K::SurfaceTrim { body, .. }
|
|
| K::Unstitch { body, .. }
|
|
| K::ReverseNormal { body, .. }
|
|
| 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<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::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)?;
|
|
for c in &mut sketch.constraints {
|
|
if let Some(n) = &mut c.param {
|
|
mapped_name(n, 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::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");
|
|
}
|
|
}
|