One walk over the geometry a feature holds (3/3, #876) (#1028)

Moves the per-feature geometry handling (transforms, face points, profile selections, point expressions) into one geometry walk in feature_geometry.rs, with a golden record of what each feature kind moves. sheet.perforate holds no world geometry and joins the no-op arm.
This commit is contained in:
JIJINBEI authored and GitHub committed 2026-10-14 13:29:31 -04:00
1 parent f79dfe41b6
commit 1a456d2d1f
9 files changed
+709 -258

No files matched your search

+11 -155
View File
@@ -11,7 +11,7 @@
use serde::{Deserialize, Serialize};
use solidcraft_geom::{Plane, Vec3};
use crate::{AxisRef, DocError, Document, FeatureKind, PatternKind, PlaneRef, Result};
use crate::{DocError, Document, FeatureKind, PlaneRef, Result};
/// Column-major 4×4 affine transform (`m[col][row]`).
pub type Mat = [[f64; 4]; 4];
@@ -204,160 +204,16 @@ pub fn map_point_expr(m: &Mat, p: &mut crate::PointExpr) {
impl FeatureKind {
/// Map the feature's world geometry (points, directions, explicit planes) by `m`.
pub fn transform_geometry(&mut self, m: &Mat) {
let pt = |p: &mut Vec3| *p = apply_point(m, *p);
let dir = |v: &mut Vec3| *v = apply_vector(m, *v);
match self {
FeatureKind::Sketch { plane, .. } | FeatureKind::ConstructionPlane { plane } | FeatureKind::Mirror { plane, .. } => {
map_plane_ref(m, plane)
}
FeatureKind::ConstructionAxis { def } => def.refs_mut().into_iter().for_each(|r| map_geo_ref(m, r)),
FeatureKind::ConstructionPoint { def } => def.refs_mut().into_iter().for_each(|r| map_geo_ref(m, r)),
FeatureKind::Split { plane, tool, .. } => {
map_plane_ref(m, plane);
if let Some(t) = tool {
pt(&mut t.point);
}
}
FeatureKind::SplitFace { faces, plane, tool, .. } => {
faces.iter_mut().for_each(pt);
map_plane_ref(m, plane);
if let Some(t) = tool {
pt(&mut t.point);
}
}
FeatureKind::Revolve { axis: AxisRef::Line { origin, dir: d }, .. }
| FeatureKind::SurfaceRevolve { axis: AxisRef::Line { origin, dir: d }, .. } => {
pt(origin);
dir(d);
}
FeatureKind::Fillet { edges, style, .. } => {
edges.iter_mut().for_each(pt);
if let crate::FilletStyle::Variable { start, .. } | crate::FilletStyle::Spline { start, .. } = style {
pt(start);
}
}
FeatureKind::Chamfer { edges, .. } => edges.iter_mut().for_each(pt),
FeatureKind::Shell { faces, .. }
| FeatureKind::OffsetFace { faces, .. }
| FeatureKind::DeleteFace { faces, .. }
| FeatureKind::SurfaceOffset { faces, .. } => faces.iter_mut().for_each(pt),
FeatureKind::Draft { faces, neutral, pull, .. } => {
faces.iter_mut().for_each(pt);
map_plane_ref(m, neutral);
dir(pull);
}
FeatureKind::Box { corner, .. } => map_point_expr(m, corner),
FeatureKind::Cylinder { base, axis, .. } => {
map_point_expr(m, base);
dir(axis);
}
FeatureKind::Sphere { center, .. } | FeatureKind::Torus { center, .. } => map_point_expr(m, center),
FeatureKind::Pattern { pattern: PatternKind::Rectangular { dir1, dir2, .. }, .. } => {
dir(dir1);
if let Some(d) = dir2 {
dir(d);
}
}
FeatureKind::Pattern { pattern: PatternKind::Circular { origin, axis, .. }, .. } => {
pt(origin);
dir(axis);
}
// Follows its sketch.
FeatureKind::Pattern { pattern: PatternKind::Path { .. }, .. } => {}
FeatureKind::Hole { position, direction, .. } => {
pt(position);
dir(direction);
}
FeatureKind::Thread { face, .. } => pt(face),
FeatureKind::SheetFlange { edges, .. } => edges.iter_mut().for_each(|e| pt(&mut e.point)),
FeatureKind::SheetHem { edges, .. } => edges.iter_mut().for_each(pt),
FeatureKind::SheetConvert { face, bends, .. } => {
pt(face);
bends.iter_mut().for_each(pt);
}
FeatureKind::SheetFold { a, b, fixed, .. } => {
pt(a);
pt(b);
if let Some(f) = fixed {
pt(f);
}
}
FeatureKind::Boss { position, direction, .. } => {
pt(position);
dir(direction);
}
FeatureKind::Lip { face, .. } => pt(face),
FeatureKind::BoundaryFill { cells, .. } => cells.iter_mut().for_each(pt),
FeatureKind::Rest { position, direction, along, .. } => {
pt(position);
dir(direction);
if let Some(a) = along {
dir(a);
}
}
FeatureKind::SnapFit { position, direction, hook, .. } => {
pt(position);
dir(direction);
dir(hook);
}
FeatureKind::SheetBase { .. }
| FeatureKind::SheetContour { .. }
| FeatureKind::SheetUnfold { .. }
| FeatureKind::SheetPerforate { .. } => {}
FeatureKind::Coil { base, axis, .. } => {
map_point_expr(m, base);
dir(axis);
}
FeatureKind::ReplaceFace { faces, target, .. } => {
faces.iter_mut().for_each(pt);
map_plane_ref(m, target);
}
FeatureKind::Align { from, to, from_normal, to_normal, .. } => {
pt(from);
pt(to);
if let Some(n) = from_normal {
dir(n);
}
if let Some(n) = to_normal {
dir(n);
}
}
FeatureKind::Scale { origin, .. } => pt(origin),
FeatureKind::Move { rotate_axis, .. } => {
if let Some(a) = rotate_axis {
dir(a);
}
}
FeatureKind::Revolve { .. }
| FeatureKind::Extrude { .. }
| FeatureKind::SurfaceExtrude { .. }
| FeatureKind::SurfaceRevolve { .. }
| FeatureKind::SurfaceSweep { .. }
| FeatureKind::SurfaceLoft { .. }
| FeatureKind::Combine { .. }
| FeatureKind::Loft { .. }
| FeatureKind::Sweep { .. }
| FeatureKind::BoundingSolid { .. }
| FeatureKind::Pipe { .. }
| FeatureKind::Emboss { .. }
| FeatureKind::Rib { .. }
| FeatureKind::Remove { .. }
| FeatureKind::Stitch { .. }
| FeatureKind::Thicken { .. }
| FeatureKind::Import { .. }
| FeatureKind::MeshImport { .. }
| FeatureKind::MeshConvert { .. }
| FeatureKind::BodyTransfer { .. } => {}
FeatureKind::Patch { edges, .. } | FeatureKind::SurfaceExtend { edges, .. } => edges.iter_mut().for_each(pt),
FeatureKind::Unstitch { faces, .. } | FeatureKind::ReverseNormal { faces, .. } => faces.iter_mut().for_each(pt),
FeatureKind::SurfaceTrim { plane, tool, keep, .. } => {
map_plane_ref(m, plane);
if let Some(t) = tool {
pt(&mut t.point);
}
pt(keep);
}
}
use crate::feature_geometry::Geo;
self.walk_geometry_mut(&mut |g| match g {
Geo::Pick(p) | Geo::Pos(p) => *p = apply_point(m, *p),
Geo::Dir(v) => *v = apply_vector(m, *v),
Geo::Plane(p) => map_plane_ref(m, p),
Geo::Construct(r) => map_geo_ref(m, r),
Geo::Placement(e) => map_point_expr(m, e),
// An Emboss cylinder is picked in its component's frame (`EmbossFace`).
Geo::Pinned(_) | Geo::Profiles(..) => {}
});
}
}
-75
View File
@@ -1523,81 +1523,6 @@ impl FeatureKind {
FeatureKind::MeshConvert { .. } => "ConvertMesh",
}
}
/// Every expression the feature uses.
/// Points on faces the feature refers to (shell, draft, offset and replace faces, a hole's
/// placement), for persistent face names.
/// The feature's profile selections, each with its sketch.
pub fn profile_sels_mut(&mut self) -> Vec<(u64, &mut ProfileSel)> {
match self {
// A thin extrude follows curves; its profiles are not used.
FeatureKind::Extrude { thin: Some(_), .. } => Vec::new(),
FeatureKind::Extrude { sketch, profiles, .. }
| FeatureKind::Revolve { sketch, profiles, .. }
| FeatureKind::Sweep { sketch, profiles, .. }
| FeatureKind::Emboss { sketch, profiles, .. }
| FeatureKind::SheetBase { sketch, profiles, .. } => vec![(*sketch, profiles)],
FeatureKind::Patch { sketch: Some(sk), profiles, .. }
| FeatureKind::SurfaceExtrude { sketch: sk, profiles: Some(profiles), .. }
| FeatureKind::SurfaceRevolve { sketch: sk, profiles: Some(profiles), .. }
| FeatureKind::SurfaceSweep { sketch: sk, profiles: Some(profiles), .. } => {
vec![(*sk, profiles)]
}
FeatureKind::Loft { sections, .. } => sections.iter_mut().filter(|s| s.point.is_none()).map(|s| (s.sketch, &mut s.profiles)).collect(),
FeatureKind::SurfaceLoft { sections } => sections.iter_mut().filter_map(|s| Some((s.sketch, s.profiles.as_mut()?))).collect(),
_ => Vec::new(),
}
}
/// Profile selections and their source sketches without cloning feature data.
pub fn profile_sels(&self) -> Vec<(u64, &ProfileSel)> {
match self {
// A thin extrude follows curves; its profiles are not used.
FeatureKind::Extrude { thin: Some(_), .. } => Vec::new(),
FeatureKind::Extrude { sketch, profiles, .. }
| FeatureKind::Revolve { sketch, profiles, .. }
| FeatureKind::Sweep { sketch, profiles, .. }
| FeatureKind::Emboss { sketch, profiles, .. }
| FeatureKind::SheetBase { sketch, profiles, .. } => vec![(*sketch, profiles)],
FeatureKind::Patch { sketch: Some(sketch), profiles, .. }
| FeatureKind::SurfaceExtrude { sketch, profiles: Some(profiles), .. }
| FeatureKind::SurfaceRevolve { sketch, profiles: Some(profiles), .. }
| FeatureKind::SurfaceSweep { sketch, profiles: Some(profiles), .. } => {
vec![(*sketch, profiles)]
}
FeatureKind::Loft { sections, .. } => {
sections.iter().filter(|section| section.point.is_none()).map(|section| (section.sketch, &section.profiles)).collect()
}
FeatureKind::SurfaceLoft { sections } => sections.iter().filter_map(|s| Some((s.sketch, s.profiles.as_ref()?))).collect(),
_ => Vec::new(),
}
}
pub fn face_points_mut(&mut self) -> Vec<&mut Vec3> {
match self {
FeatureKind::Shell { faces, .. }
| FeatureKind::Draft { faces, .. }
| FeatureKind::OffsetFace { faces, .. }
| FeatureKind::DeleteFace { faces, .. }
| FeatureKind::SurfaceOffset { faces, .. }
| FeatureKind::SplitFace { faces, .. }
| FeatureKind::ReplaceFace { faces, .. } => faces.iter_mut().collect(),
FeatureKind::Hole { position, points: None, to, .. } => std::iter::once(position).chain(to.iter_mut()).collect(),
FeatureKind::Hole { to: Some(t), .. } => vec![t],
FeatureKind::Extrude { extent, .. } => extent.to.iter_mut().chain(extent.from.iter_mut()).collect(),
FeatureKind::Revolve { to: Some(p), .. } => vec![p],
FeatureKind::Thread { face, .. } => vec![face],
FeatureKind::Emboss { face: Some(face), .. } => vec![&mut face.at],
FeatureKind::Boss { position, .. } | FeatureKind::SnapFit { position, .. } | FeatureKind::Rest { position, .. } => vec![position],
// With bend edges the fixed face is found by name (a plate keeps its point).
FeatureKind::SheetConvert { face, bends, .. } if !bends.is_empty() => vec![face],
_ => Vec::new(),
}
}
pub fn face_points(&self) -> Vec<Vec3> {
let mut k = self.clone();
k.face_points_mut().into_iter().map(|p| *p).collect()
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+381
View File
@@ -0,0 +1,381 @@
//! The geometry a feature holds, each piece with its role: the points that pick faces (moved
//! with the feature, and the source of its persistent face names), other positions, directions,
//! planes, construction references, a primitive's placement point and its profile selections.
//! One walk, written once for `&` and `&mut`, behind `transform_geometry`, `face_points`,
//! `profile_sels` and `point_expr`. Every variant is listed, so a new one has to say what each of
//! its points is.
use solidcraft_geom::Vec3;
use crate::construct::GeoRef;
use crate::{AxisRef, FeatureKind, PatternKind, PlaneRef, PointExpr, ProfileSel};
/// A piece of a feature's geometry: `V` is `&Vec3` or `&mut Vec3`, and so on for the others.
pub(crate) enum Geo<V, P, G, E, S> {
/// A point that picks a face: it moves with the feature and names the face it picks.
Pick(V),
/// A point that picks a face but stays where it is when the feature is moved (an Emboss
/// cylinder, chosen in its component's frame).
Pinned(V),
/// Any other position (an edge pick, an axis origin, a fold line): it moves with the feature.
Pos(V),
/// A direction: it turns with the feature.
Dir(V),
Plane(P),
/// A construction axis's or point's reference to the model.
Construct(G),
/// A primitive's placement point (length expressions).
Placement(E),
/// A profile selection, with its sketch.
Profiles(u64, S),
}
pub(crate) type GeoItem<'a> = Geo<&'a Vec3, &'a PlaneRef, &'a GeoRef, &'a PointExpr, &'a ProfileSel>;
pub(crate) type GeoItemMut<'a> = Geo<&'a mut Vec3, &'a mut PlaneRef, &'a mut GeoRef, &'a mut PointExpr, &'a mut ProfileSel>;
/// Every piece of a feature's geometry, written once for `&` and `&mut`: `$item` is `GeoItem` or
/// `GeoItemMut`, `$refs` the matching `refs` / `refs_mut`, `$m` is `mut` or nothing. Face picks
/// come in the order the feature's face names follow.
macro_rules! geometry_walk {
($name:ident, $item:ident, $refs:ident $(, $m:tt)?) => {
pub(crate) fn $name<'a>(&'a $($m)? self, f: &mut impl FnMut($item<'a>)) {
use Geo::{Construct, Dir, Pick, Pinned, Placement, Plane, Pos, Profiles};
match self {
FeatureKind::Sketch { plane, .. } | FeatureKind::ConstructionPlane { plane } | FeatureKind::Mirror { plane, .. } => f(Plane(plane)),
FeatureKind::ConstructionAxis { def } => {
for r in def.$refs() {
f(Construct(r));
}
}
FeatureKind::ConstructionPoint { def } => {
for r in def.$refs() {
f(Construct(r));
}
}
FeatureKind::Extrude { sketch, profiles, extent, thin, .. } => {
// A thin extrude follows curves; its profiles are not used.
if thin.is_none() {
f(Profiles(*sketch, profiles));
}
if let Some(p) = & $($m)? extent.to {
f(Pick(p));
}
if let Some(p) = & $($m)? extent.from {
f(Pick(p));
}
}
// Surface Extrude refuses `to` and `from` for now; they would pick faces as for
// Extrude.
FeatureKind::SurfaceExtrude { sketch, profiles, extent, .. } => {
if let Some(s) = profiles {
f(Profiles(*sketch, s));
}
if let Some(p) = & $($m)? extent.to {
f(Pick(p));
}
if let Some(p) = & $($m)? extent.from {
f(Pick(p));
}
}
FeatureKind::Revolve { sketch, profiles, axis, to, .. } => {
f(Profiles(*sketch, profiles));
if let AxisRef::Line { origin, dir } = axis {
f(Pos(origin));
f(Dir(dir));
}
if let Some(p) = to {
f(Pick(p));
}
}
FeatureKind::SurfaceRevolve { sketch, profiles, axis, .. } => {
if let Some(s) = profiles {
f(Profiles(*sketch, s));
}
if let AxisRef::Line { origin, dir } = axis {
f(Pos(origin));
f(Dir(dir));
}
}
FeatureKind::Sweep { sketch, profiles, .. } | FeatureKind::SheetBase { sketch, profiles, .. } => f(Profiles(*sketch, profiles)),
FeatureKind::SurfaceSweep { sketch, profiles, .. } => {
if let Some(s) = profiles {
f(Profiles(*sketch, s));
}
}
FeatureKind::Emboss { sketch, profiles, face, .. } => {
f(Profiles(*sketch, profiles));
if let Some(face) = face {
f(Pinned(& $($m)? face.at));
}
}
FeatureKind::Loft { sections, .. } => {
// A section that is a point has no profile.
for s in sections {
if s.point.is_none() {
f(Profiles(s.sketch, & $($m)? s.profiles));
}
}
}
FeatureKind::SurfaceLoft { sections } => {
for s in sections {
if let Some(p) = & $($m)? s.profiles {
f(Profiles(s.sketch, p));
}
}
}
FeatureKind::Patch { sketch, profiles, edges, .. } => {
if let Some(sketch) = sketch {
f(Profiles(*sketch, profiles));
}
for e in edges {
f(Pos(e));
}
}
FeatureKind::Fillet { edges, style, .. } => {
for e in edges {
f(Pos(e));
}
if let crate::FilletStyle::Variable { start, .. } | crate::FilletStyle::Spline { start, .. } = style {
f(Pos(start));
}
}
FeatureKind::Chamfer { edges, .. } | FeatureKind::SheetHem { edges, .. } | FeatureKind::SurfaceExtend { edges, .. } => {
for e in edges {
f(Pos(e));
}
}
FeatureKind::Shell { faces, .. }
| FeatureKind::OffsetFace { faces, .. }
| FeatureKind::DeleteFace { faces, .. }
| FeatureKind::SurfaceOffset { faces, .. } => {
for p in faces {
f(Pick(p));
}
}
FeatureKind::Unstitch { faces, .. } | FeatureKind::ReverseNormal { faces, .. } => {
for p in faces {
f(Pos(p));
}
}
FeatureKind::BoundaryFill { cells, .. } => {
for p in cells {
f(Pos(p));
}
}
FeatureKind::Draft { faces, neutral, pull, .. } => {
for p in faces {
f(Pick(p));
}
f(Plane(neutral));
f(Dir(pull));
}
FeatureKind::ReplaceFace { faces, target, .. } => {
for p in faces {
f(Pick(p));
}
f(Plane(target));
}
FeatureKind::SplitFace { faces, plane, tool, .. } => {
for p in faces {
f(Pick(p));
}
f(Plane(plane));
if let Some(t) = tool {
f(Pos(& $($m)? t.point));
}
}
FeatureKind::Split { plane, tool, .. } => {
f(Plane(plane));
if let Some(t) = tool {
f(Pos(& $($m)? t.point));
}
}
FeatureKind::SurfaceTrim { plane, tool, keep, .. } => {
f(Plane(plane));
if let Some(t) = tool {
f(Pos(& $($m)? t.point));
}
f(Pos(keep));
}
FeatureKind::Box { corner: p, .. } | FeatureKind::Sphere { center: p, .. } | FeatureKind::Torus { center: p, .. } => {
f(Placement(p))
}
FeatureKind::Cylinder { base, axis, .. } | FeatureKind::Coil { base, axis, .. } => {
f(Placement(base));
f(Dir(axis));
}
FeatureKind::Pattern { pattern, .. } => match pattern {
PatternKind::Rectangular { dir1, dir2, .. } => {
f(Dir(dir1));
if let Some(d) = dir2 {
f(Dir(d));
}
}
PatternKind::Circular { origin, axis, .. } => {
f(Pos(origin));
f(Dir(axis));
}
// Follows its path sketch.
PatternKind::Path { .. } => {}
},
FeatureKind::Hole { position, direction, points, to, .. } => {
// Placed at sketch points, the hole names no face by its position.
if points.is_none() {
f(Pick(position));
} else {
f(Pos(position));
}
f(Dir(direction));
if let Some(p) = to {
f(Pick(p));
}
}
FeatureKind::Thread { face, .. } => f(Pick(face)),
FeatureKind::SheetFlange { edges, .. } => {
for e in edges {
f(Pos(& $($m)? e.point));
}
}
FeatureKind::SheetFold { a, b, fixed, .. } => {
f(Pos(a));
f(Pos(b));
if let Some(p) = fixed {
f(Pos(p));
}
}
FeatureKind::SheetConvert { face, bends, .. } => {
// With bend edges the fixed face is found by name (a plate keeps its point).
if bends.is_empty() {
f(Pos(face));
} else {
f(Pick(face));
}
for p in bends {
f(Pos(p));
}
}
FeatureKind::Boss { position, direction, .. } => {
f(Pick(position));
f(Dir(direction));
}
FeatureKind::SnapFit { position, direction, hook, .. } => {
f(Pick(position));
f(Dir(direction));
f(Dir(hook));
}
FeatureKind::Rest { position, direction, along, .. } => {
f(Pick(position));
f(Dir(direction));
if let Some(a) = along {
f(Dir(a));
}
}
FeatureKind::Lip { face, .. } => f(Pos(face)),
FeatureKind::Align { from, to, from_normal, to_normal, .. } => {
f(Pos(from));
f(Pos(to));
if let Some(n) = from_normal {
f(Dir(n));
}
if let Some(n) = to_normal {
f(Dir(n));
}
}
FeatureKind::Scale { origin, .. } => f(Pos(origin)),
FeatureKind::Move { rotate_axis, .. } => {
if let Some(a) = rotate_axis {
f(Dir(a));
}
}
FeatureKind::SheetContour { .. }
| FeatureKind::SheetPerforate { .. }
| FeatureKind::SheetUnfold { .. }
| FeatureKind::Combine { .. }
| FeatureKind::BoundingSolid { .. }
| FeatureKind::Pipe { .. }
| FeatureKind::Rib { .. }
| FeatureKind::Remove { .. }
| FeatureKind::Stitch { .. }
| FeatureKind::Thicken { .. }
| FeatureKind::Import { .. }
| FeatureKind::MeshImport { .. }
| FeatureKind::MeshConvert { .. }
| FeatureKind::BodyTransfer { .. } => {}
}
}
};
}
impl FeatureKind {
geometry_walk!(walk_geometry, GeoItem, refs);
geometry_walk!(walk_geometry_mut, GeoItemMut, refs_mut, mut);
/// The points that pick faces (shell, draft, offset and replace faces, a hole's placement and
/// `to`, an extrude's `to` and `from`…), in the order of the feature's persistent face names.
pub fn face_points_mut(&mut self) -> Vec<&mut Vec3> {
let mut v = Vec::new();
self.walk_geometry_mut(&mut |g| {
if let Geo::Pick(p) | Geo::Pinned(p) = g {
v.push(p);
}
});
v
}
/// Read-only view of [`FeatureKind::face_points_mut`].
pub fn face_points(&self) -> Vec<Vec3> {
let mut v = Vec::new();
self.walk_geometry(&mut |g| {
if let Geo::Pick(p) | Geo::Pinned(p) = g {
v.push(*p);
}
});
v
}
/// The feature's profile selections, each with its sketch.
pub fn profile_sels_mut(&mut self) -> Vec<(u64, &mut ProfileSel)> {
let mut v = Vec::new();
self.walk_geometry_mut(&mut |g| {
if let Geo::Profiles(sketch, sel) = g {
v.push((sketch, sel));
}
});
v
}
/// Read-only view of [`FeatureKind::profile_sels_mut`].
pub fn profile_sels(&self) -> Vec<(u64, &ProfileSel)> {
let mut v = Vec::new();
self.walk_geometry(&mut |g| {
if let Geo::Profiles(sketch, sel) = g {
v.push((sketch, sel));
}
});
v
}
/// A primitive's placement point (length expressions). Its coordinates are not inputs:
/// they get no parameter names and keep bare numbers in mm whatever the design's units, as
/// every other position does; renames and users still find them.
pub fn point_expr_mut(&mut self) -> Option<&mut PointExpr> {
let mut found = None;
self.walk_geometry_mut(&mut |g| {
if let Geo::Placement(p) = g {
found = Some(p);
}
});
found
}
/// Read-only view of [`FeatureKind::point_expr_mut`].
pub fn point_expr(&self) -> Option<&PointExpr> {
let mut found = None;
self.walk_geometry(&mut |g| {
if let Geo::Placement(p) = g {
found = Some(p);
}
});
found
}
}
+49 -2
View File
@@ -4,7 +4,9 @@
//! parameters) and `expressions` (every expression, placement points included). Each
//! expression field holds its own name, so a moved, missing or relabelled input shows;
//! - references: the sketches a feature is built from, and the fields `remap_refs` and
//! `remap_bodies` rewrite when everything is copied under new ids and names.
//! `remap_bodies` rewrite when everything is copied under new ids and names;
//! - geometry: the fields `transform_geometry` moves, the face points, the profile selections and
//! the placement point.
//!
//! The records live in `feature_walk_tests/*.txt`; to record an intended change, run the tests
//! with `FEATURE_WALK_BLESS=1`.
@@ -13,7 +15,7 @@ mod fixtures;
use std::collections::BTreeMap;
use fixtures::{changed, fixtures, with_refs};
use fixtures::{changed, fixtures, with_points, with_refs};
use crate::FeatureKind;
@@ -75,6 +77,42 @@ fn ref_record() -> String {
/// points or loft sections) must report it from `input_sketches`, or deleting the sketch leaves
/// the feature broken. New variants are caught by the walk's exhaustive match; this catches a
/// sketch field added to an existing variant (its fixture has to set it).
/// Per fixture (with every reference and position set): the fields `transform_geometry` changes
/// under a turn and a shift (every point, direction and plane it maps), the face points that name
/// faces, the profile selections with their sketches, and a primitive's placement point.
fn geometry_record() -> String {
// x → y → z → x, then (1, 2, 3): every point and every direction of the fixtures changes.
let m = [[0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0], [1.0, 0.0, 0.0, 0.0], [1.0, 2.0, 3.0, 1.0]];
let mut out = String::new();
for (name, v) in fixtures() {
let kind: FeatureKind = serde_json::from_value(with_points(name, with_refs(name, v))).unwrap_or_else(|e| panic!("{name}: {e}"));
let mut moved = kind.clone();
moved.transform_geometry(&m);
let mut paths = Vec::new();
changed(&serde_json::to_value(&kind).unwrap(), &serde_json::to_value(&moved).unwrap(), "", &mut paths);
out += &format!("{name}\n");
if !paths.is_empty() {
out += &format!(" moves: {}\n", paths.join(", "));
}
let faces: Vec<String> = kind.face_points().iter().map(|p| format!("[{}, {}, {}]", p.x, p.y, p.z)).collect();
if !faces.is_empty() {
out += &format!(" face points: {}\n", faces.join(", "));
}
let profiles: Vec<String> = kind
.profile_sels()
.iter()
.map(|(sketch, sel)| format!("{sketch} {}", serde_json::to_value(sel).unwrap()["type"].as_str().unwrap_or("?")))
.collect();
if !profiles.is_empty() {
out += &format!(" profiles: {}\n", profiles.join(", "));
}
if let Some(p) = kind.point_expr() {
out += &format!(" placement: {}\n", p.join(", "));
}
}
out
}
/// Compares `got` with the golden file `name` in `feature_walk_tests/`, line by line. With
/// `FEATURE_WALK_BLESS` set it rewrites the file instead, for an intended change (say why in the
/// pull request).
@@ -93,6 +131,10 @@ fn check_golden(name: &str, want: &str, got: &str) {
/// Recorded from the hand-written `inputs_mut` and `expressions` the walk replaced.
const GOLDEN_EXPRS: &str = include_str!("feature_walk_tests/golden_exprs.txt");
/// Recorded from the hand-written `transform_geometry`, `face_points`, `profile_sels` and
/// `point_expr` the walk replaced.
const GOLDEN_GEOMETRY: &str = include_str!("feature_walk_tests/golden_geometry.txt");
/// Recorded from the hand-written `input_sketches`, `remap_refs` and `remap_bodies` the walk
/// replaced, with two fixes: a coil's `targets` and a surface trim's plane are remapped too.
const GOLDEN_REFS: &str = include_str!("feature_walk_tests/golden_refs.txt");
@@ -125,6 +167,11 @@ fn input_sketches_lists_every_variant_with_a_sketch_field() {
}
}
#[test]
fn geometry_matches_the_golden_record() {
check_golden("golden_geometry.txt", GOLDEN_GEOMETRY, &geometry_record());
}
#[test]
fn references_match_the_golden_record() {
check_golden("golden_refs.txt", GOLDEN_REFS, &ref_record());
@@ -191,6 +191,41 @@ pub(super) fn with_refs(name: &str, mut v: Value) -> Value {
v
}
/// The positions and directions a fixture leaves out (they don't change its expressions or
/// references), set here so every point, direction and axis the geometry walks read is present:
/// `to` / `from` picks, line axes, normals and origins. `hole_simple` places its hole at
/// `position` (no sketch points), the other holes at sketch points.
pub(super) fn with_points(name: &str, mut v: Value) -> Value {
let p = |i: f64| json!([i, i + 0.5, i + 0.25]);
let line = json!({"type": "line", "origin": p(9.0), "dir": [0.0, 0.6, 0.8]});
let extra = match name {
"extrude" | "surface_extrude" => {
if let Some(e) = v.get_mut("extent").and_then(Value::as_object_mut) {
e.insert("to".into(), p(1.0));
e.insert("from".into(), p(2.0));
}
json!({})
}
"revolve" => json!({"to": p(3.0), "axis": line}),
"surface_revolve" => json!({"axis": line}),
"hole_simple" => json!({"points": null, "to": p(4.0)}),
"hole_drilled" | "hole_counterbore" | "hole_countersink" => json!({"to": p(4.0)}),
"emboss" => json!({"face": {"body": "B", "at": p(5.0)}}),
"sheet_convert" => json!({"bends": [p(6.0)]}),
"sheet_fold" => json!({"fixed": p(7.0)}),
"rest" => json!({"along": [0.6, 0.8, 0.0]}),
"align" => json!({"from_normal": [0.0, 0.0, 1.0], "to_normal": [1.0, 0.0, 0.0]}),
"scale" => json!({"origin": p(8.0)}),
"move" => json!({"rotate_axis": [0.0, 1.0, 0.0]}),
"unstitch" | "reverse_normal" => json!({"faces": [p(10.0)]}),
_ => json!({}),
};
if let (Some(o), Some(e)) = (v.as_object_mut(), extra.as_object()) {
o.extend(e.clone());
}
v
}
/// The paths of the leaves that differ between two JSON values.
pub(super) fn changed(a: &Value, b: &Value, path: &str, out: &mut Vec<String>) {
match (a, b) {
@@ -0,0 +1,161 @@
body_transfer
sketch
moves: plane.axis_dir[0], plane.axis_dir[1], plane.axis_origin[0], plane.axis_origin[1], plane.axis_origin[2]
sketch_along_path
moves: plane.path.at[0], plane.path.at[1], plane.path.at[2]
extrude
moves: extent.from[0], extent.from[1], extent.from[2], extent.to[0], extent.to[1], extent.to[2]
face points: [1, 1.5, 1.25], [2, 2.5, 2.25]
profiles: 1 all
extrude_thin
revolve
moves: axis.dir[0], axis.dir[1], axis.dir[2], axis.origin[0], axis.origin[1], axis.origin[2], to[0], to[1], to[2]
face points: [3, 3.5, 3.25]
profiles: 1 all
fillet_constant
moves: edges[0][0], edges[0][1], edges[0][2]
fillet_chord
moves: edges[0][0], edges[0][1], edges[0][2]
fillet_curvature
moves: edges[0][0], edges[0][1], edges[0][2]
fillet_variable
moves: edges[0][0], edges[0][1], edges[0][2], style.start[0], style.start[1], style.start[2]
fillet_spline
moves: edges[0][0], edges[0][1], edges[0][2], style.start[0], style.start[1], style.start[2]
chamfer
moves: edges[0][0], edges[0][1], edges[0][2]
box
moves: corner[0], corner[1], corner[2]
placement: box.x, box.y, box.z
cylinder
moves: axis[0], axis[2], base[0], base[1], base[2]
placement: cyl.x, cyl.y, cyl.z
sphere
moves: center[0], center[1], center[2]
placement: sph.x, sph.y, sph.z
torus
moves: center[0], center[1], center[2]
placement: tor.x, tor.y, tor.z
combine
pattern_rectangular
moves: pattern.dir1[0], pattern.dir1[1], pattern.dir2[1], pattern.dir2[2]
pattern_circular
moves: pattern.axis[0], pattern.axis[2], pattern.origin[0], pattern.origin[1], pattern.origin[2]
pattern_path
mirror
moves: plane.axis_dir[0], plane.axis_dir[1], plane.axis_origin[0], plane.axis_origin[1], plane.axis_origin[2]
hole_simple
moves: direction[0], direction[2], position[0], position[1], position[2], to[0], to[1], to[2]
face points: [0, 0, 0], [4, 4.5, 4.25]
hole_drilled
moves: direction[0], direction[2], position[0], position[1], position[2], to[0], to[1], to[2]
face points: [4, 4.5, 4.25]
hole_counterbore
moves: direction[0], direction[2], position[0], position[1], position[2], to[0], to[1], to[2]
face points: [4, 4.5, 4.25]
hole_countersink
moves: direction[0], direction[2], position[0], position[1], position[2], to[0], to[1], to[2]
face points: [4, 4.5, 4.25]
thread
moves: face[0], face[1], face[2]
face points: [0, 0, 0]
loft
profiles: 4 all, 5 all
sweep
profiles: 1 all
shell
moves: faces[0][0], faces[0][1], faces[0][2]
face points: [0, 0, 0]
draft
moves: faces[0][0], faces[0][1], faces[0][2], neutral.axis_dir[0], neutral.axis_dir[1], neutral.axis_origin[0], neutral.axis_origin[1], neutral.axis_origin[2], pull[0], pull[2]
face points: [0, 0, 0]
construction_plane
moves: plane.axis_dir[0], plane.axis_dir[1], plane.axis_origin[0], plane.axis_origin[1], plane.axis_origin[2]
split
moves: plane.axis_dir[0], plane.axis_dir[1], plane.axis_origin[0], plane.axis_origin[1], plane.axis_origin[2], tool.point[0], tool.point[1], tool.point[2]
construction_axis
moves: def.a.plane.axis_dir[0], def.a.plane.axis_dir[1], def.a.plane.axis_origin[0], def.a.plane.axis_origin[1], def.a.plane.axis_origin[2], def.b.plane.axis_dir[0], def.b.plane.axis_dir[1], def.b.plane.axis_origin[0], def.b.plane.axis_origin[1], def.b.plane.axis_origin[2]
construction_point_planes
moves: def.a.plane.axis_dir[0], def.a.plane.axis_dir[1], def.a.plane.axis_origin[0], def.a.plane.axis_origin[1], def.a.plane.axis_origin[2], def.b.plane.axis_dir[0], def.b.plane.axis_dir[1], def.b.plane.axis_origin[0], def.b.plane.axis_origin[1], def.b.plane.axis_origin[2], def.c.plane.axis_dir[0], def.c.plane.axis_dir[1], def.c.plane.axis_origin[0], def.c.plane.axis_origin[1], def.c.plane.axis_origin[2]
construction_point_along
moves: def.path.at[0], def.path.at[1], def.path.at[2]
split_face
moves: faces[0][0], faces[0][1], faces[0][2], plane.axis_dir[0], plane.axis_dir[1], plane.axis_origin[0], plane.axis_origin[1], plane.axis_origin[2], tool.point[0], tool.point[1], tool.point[2]
face points: [0, 0, 0]
scale
moves: origin[0], origin[1], origin[2]
offset_face
moves: faces[0][0], faces[0][1], faces[0][2]
face points: [0, 0, 0]
delete_face
moves: faces[0][0], faces[0][1], faces[0][2]
face points: [0, 0, 0]
bounding_solid
pipe
emboss
face points: [5, 5.5, 5.25]
profiles: 1 all
rib
replace_face
moves: faces[0][0], faces[0][1], faces[0][2], target.axis_dir[0], target.axis_dir[1], target.axis_origin[0], target.axis_origin[1], target.axis_origin[2]
face points: [0, 0, 0]
align
moves: from[0], from[1], from[2], from_normal[0], from_normal[2], to[0], to[1], to[2], to_normal[0], to_normal[1]
coil
moves: axis[0], axis[2], base[0], base[1], base[2]
placement: coil.x, coil.y, coil.z
sheet_base
profiles: 1 all
sheet_contour
sheet_flange
moves: edges[0].point[0], edges[0].point[1], edges[0].point[2], edges[1].point[0], edges[1].point[1], edges[1].point[2]
sheet_hem
moves: edges[0][0], edges[0][1], edges[0][2]
sheet_perforate
sheet_fold
moves: a[0], a[1], a[2], b[1], b[2], fixed[0], fixed[1], fixed[2]
sheet_unfold
sheet_convert
moves: bends[0][0], bends[0][1], bends[0][2], face[0], face[1], face[2]
face points: [0, 0, 0]
boss
moves: direction[0], direction[2], position[0], position[1], position[2]
face points: [0, 0, 0]
lip
moves: face[0], face[1], face[2]
snap_fit
moves: direction[0], direction[2], hook[0], hook[1], position[0], position[1], position[2]
face points: [0, 0, 0]
rest
moves: along[0], along[1], along[2], direction[0], direction[2], position[0], position[1], position[2]
face points: [0, 0, 0]
boundary_fill
moves: cells[0][0], cells[0][1], cells[0][2]
remove
patch
profiles: 8 all
stitch
thicken
surface_extrude
moves: extent.from[0], extent.from[1], extent.from[2], extent.to[0], extent.to[1], extent.to[2]
face points: [1, 1.5, 1.25], [2, 2.5, 2.25]
surface_revolve
moves: axis.dir[0], axis.dir[1], axis.dir[2], axis.origin[0], axis.origin[1], axis.origin[2]
surface_sweep
surface_loft
surface_offset
moves: faces[0][0], faces[0][1], faces[0][2]
face points: [0, 0, 0]
unstitch
moves: faces[0][0], faces[0][1], faces[0][2]
reverse_normal
moves: faces[0][0], faces[0][1], faces[0][2]
surface_extend
moves: edges[0][0], edges[0][1], edges[0][2]
surface_trim
moves: keep[0], keep[1], keep[2], plane.axis_dir[0], plane.axis_dir[1], plane.axis_origin[0], plane.axis_origin[1], plane.axis_origin[2], tool.point[0], tool.point[1], tool.point[2]
move
moves: rotate_axis[1], rotate_axis[2]
import
mesh_convert
mesh_import
+1
View File
@@ -19,6 +19,7 @@ mod document;
mod eval;
pub mod expr;
pub mod face_anchor;
mod feature_geometry;
mod feature_refs;
pub use face_anchor::{FaceAnchor, FaceAttachmentStatus};
pub mod format;
-26
View File
@@ -414,32 +414,6 @@ impl FeatureKind {
self.walk_exprs(&mut |_, e, _, _| v.push(e.as_str()));
v
}
/// A primitive's placement point (length expressions). Its coordinates are not inputs:
/// they get no parameter names and keep bare numbers in mm whatever the design's units, as
/// every other position does; renames and users still find them.
pub fn point_expr_mut(&mut self) -> Option<&mut crate::PointExpr> {
match self {
FeatureKind::Box { corner: p, .. }
| FeatureKind::Cylinder { base: p, .. }
| FeatureKind::Sphere { center: p, .. }
| FeatureKind::Torus { center: p, .. }
| FeatureKind::Coil { base: p, .. } => Some(p),
_ => None,
}
}
/// Read-only view of [`FeatureKind::point_expr_mut`].
pub fn point_expr(&self) -> Option<&crate::PointExpr> {
match self {
FeatureKind::Box { corner: p, .. }
| FeatureKind::Cylinder { base: p, .. }
| FeatureKind::Sphere { center: p, .. }
| FeatureKind::Torus { center: p, .. }
| FeatureKind::Coil { base: p, .. } => Some(p),
_ => None,
}
}
}
/// One row of the parameters table.
+71
View File
@@ -433,6 +433,77 @@ mod tests {
assert!((volume(&mut s) - 2.0 * one).abs() < 1e-6, "both blocks are rounded: {m}");
}
/// One body's measures (bounding box, volume).
fn measures(s: &mut Session, body: &str) -> serde_json::Value {
run(s, "inspect.measure", json!({"bodies": [body]}))["bodies"][0].clone()
}
/// Pasting with a translate moves an extrude's `to` and `from` picks with it: the pasted posts
/// run to the pasted roof and from the pasted shelf, not to and from the originals.
#[test]
fn a_pasted_extrude_runs_to_and_from_the_pasted_faces() {
let mut s = Session::default();
run(&mut s, "solid.box", json!({"length": 40, "width": 40, "height": 2, "corner": [0, 0, 20], "name": "Roof", "body_name": "Roof"}));
run(&mut s, "solid.box", json!({"length": 40, "width": 40, "height": 2, "corner": [0, 0, 10], "name": "Shelf", "body_name": "Shelf"}));
run(&mut s, "sketch.create", json!({"plane": "XY", "name": "S"}));
run(&mut s, "sketch.rectangle.two_point", json!({"p0": [5, 5], "p1": [10, 10]}));
run(&mut s, "sketch.finish", json!({}));
run(&mut s, "solid.extrude", json!({"sketch": "S", "distance": 5, "to": [7, 7, 20], "operation": "new", "name": "Up", "body_name": "Up"}));
run(&mut s, "sketch.create", json!({"plane": "XY", "name": "T"}));
run(&mut s, "sketch.rectangle.two_point", json!({"p0": [25, 25], "p1": [30, 30]}));
run(&mut s, "sketch.finish", json!({}));
run(
&mut s,
"solid.extrude",
json!({"sketch": "T", "distance": 5, "from": [27, 27, 12], "operation": "new", "name": "On", "body_name": "On"}),
);
run(&mut s, "timeline.copy", json!({"features": ["Roof", "Shelf", "S", "Up", "T", "On"]}));
let r = run(&mut s, "timeline.paste", json!({"translate": [100, 0, 10]}));
assert_eq!(r["errors"].as_array().map(Vec::len), Some(0), "{r}");
let up = measures(&mut s, "Up (copy)");
assert!((up["bbox"]["min"][2].as_f64().unwrap_or(0.0) - 10.0).abs() < 1e-6, "{up}");
assert!((up["bbox"]["max"][2].as_f64().unwrap_or(0.0) - 30.0).abs() < 1e-6, "it ends on the pasted roof: {up}");
let on = measures(&mut s, "On (copy)");
assert!((on["bbox"]["min"][2].as_f64().unwrap_or(0.0) - 22.0).abs() < 1e-6, "it starts on the pasted shelf: {on}");
assert!((on["bbox"]["max"][2].as_f64().unwrap_or(0.0) - 27.0).abs() < 1e-6, "{on}");
}
/// Pasting with a translate moves a hole's `to` pick with it: the pasted through hole still goes
/// through, rather than stopping at the original's depth.
#[test]
fn a_pasted_hole_drills_to_the_moved_point() {
let mut s = Session::default();
run(&mut s, "solid.box", json!({"length": 40, "width": 40, "height": 10, "name": "Block", "body_name": "Block"}));
run(&mut s, "solid.hole", json!({"position": [20, 20, 10], "direction": [0, 0, -1], "diameter": 10, "to": [30, 30, -2], "name": "Drill"}));
run(&mut s, "timeline.copy", json!({"features": ["Block", "Drill"]}));
let r = run(&mut s, "timeline.paste", json!({"translate": [60, 0, -6]}));
assert_eq!(r["errors"].as_array().map(Vec::len), Some(0), "{r}");
let (block, copy) = (measures(&mut s, "Block"), measures(&mut s, "Block (copy)"));
let v = |m: &serde_json::Value| m["volume_mm3"].as_f64().unwrap_or(f64::NAN);
assert!((v(&block) - (16000.0 - std::f64::consts::PI * 25.0 * 10.0)).abs() < 1.0, "a through hole: {block}");
assert!((v(&copy) - v(&block)).abs() < 1e-6, "the pasted hole goes through too: {copy}");
}
/// Pasting with a translate moves a revolve's `to` pick with it: the pasted turn ends at the same
/// angle about its moved axis.
#[test]
fn a_pasted_revolve_turns_to_the_moved_point() {
let mut s = Session::default();
run(&mut s, "sketch.create", json!({"plane": "XZ", "name": "S"}));
run(&mut s, "sketch.line", json!({"points": [[10, 0], [15, 0], [15, 5], [10, 5]], "closed": true}));
run(&mut s, "sketch.finish", json!({}));
run(&mut s, "solid.revolve", json!({"sketch": "S", "axis": "y", "to": [0, 12, 2], "name": "Turn", "body_name": "Arc"}));
run(&mut s, "timeline.copy", json!({"features": ["S", "Turn"]}));
let r = run(&mut s, "timeline.paste", json!({"translate": [50, 0, 0]}));
assert_eq!(r["errors"].as_array().map(Vec::len), Some(0), "{r}");
let (arc, copy) = (measures(&mut s, "Arc"), measures(&mut s, "Arc (copy)"));
let v = |m: &serde_json::Value| m["volume_mm3"].as_f64().unwrap_or(f64::NAN);
// A quarter turn of a 5 × 5 section whose centre is 12.5 from the axis.
let quarter = 25.0 * std::f64::consts::FRAC_PI_2 * 12.5;
assert!((v(&arc) - quarter).abs() < quarter * 1e-3, "{arc}");
assert!((v(&copy) - v(&arc)).abs() < 1e-6, "the pasted arc turns a quarter too: {copy}");
}
/// A pasted coil that joins a body made by an earlier copy joins that copy, not the original.
#[test]
fn a_pasted_coil_joins_the_pasted_body() {