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:
1 parent
f79dfe41b6
commit
1a456d2d1f
9 files changed
+709
-258
No files matched your search
+11
-155
@@ -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(..) => {}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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, §ion.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)]
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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(©) - 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(©) - 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() {
|
||||
|
||||
Reference in new issue
Block a user