solid.mirror: a plane's {origin} takes length expressions and follows them (#1492) (#1501)

Thanks @erkexzcx.
This commit is contained in:
Erikas authored and GitHub committed 2026-10-15 22:37:08 -04:00
1 parent daf618c344
commit a422fb1a4b
10 files changed
+120 -14

No files matched your search

+5
View File
@@ -136,6 +136,11 @@ fn map_plane(m: &Mat, p: &Plane) -> Plane {
fn map_plane_ref(m: &Mat, p: &mut PlaneRef) {
match p {
PlaneRef::Custom { plane } => *plane = map_plane(m, plane),
PlaneRef::Placed { origin, x_dir, y_dir } => {
map_point_expr(m, origin);
*x_dir = apply_vector(m, *x_dir);
*y_dir = apply_vector(m, *y_dir);
}
PlaneRef::Face { plane, at, .. } => {
*plane = map_plane(m, plane);
*at = apply_point(m, *at);
+1 -1
View File
@@ -223,7 +223,7 @@ impl crate::Feature {
for (_, e, _) in self.kind.inputs_mut() {
*e = clone_expression(e, parameters);
}
for e in self.kind.point_expr_mut().into_iter().flatten() {
for e in self.kind.placement_exprs_mut() {
*e = clone_expression(e, parameters);
}
self.kind.remap_bodies(bodies);
+17
View File
@@ -42,6 +42,14 @@ pub enum PlaneRef {
Origin { name: String },
/// An explicit plane (e.g. picked from a planar face).
Custom { plane: Plane },
/// An explicit plane whose origin is given as length expressions
/// (`{origin: ["half", 0, 0], normal: [1, 0, 0]}`), so it follows parameter changes.
Placed {
#[serde(with = "point_expr")]
origin: PointExpr,
x_dir: Vec3,
y_dir: Vec3,
},
/// Another plane offset along its normal by an expression.
Offset { base: Box<PlaneRef>, distance: String },
/// Another plane rotated about a line (in world coordinates) by an angle expression.
@@ -2938,6 +2946,15 @@ impl Document {
PlaneRef::Custom { plane } | PlaneRef::Face { plane, .. } => {
Plane::new(plane.origin, plane.x, plane.y).ok_or_else(|| DocError::Invalid("degenerate plane".into()))
}
PlaneRef::Placed { origin, x_dir, y_dir } => {
let [x, y, z] = origin;
let o =
Vec3::new(Self::eval_in(vals, x, Kind::Length)?, Self::eval_in(vals, y, Kind::Length)?, Self::eval_in(vals, z, Kind::Length)?);
if ![o.x, o.y, o.z].iter().all(|c| c.is_finite() && c.abs() < 1e9) {
return Err(DocError::Invalid("the plane's origin is out of range".into()));
}
Plane::new(o, *x_dir, *y_dir).ok_or_else(|| DocError::Invalid("degenerate plane".into()))
}
PlaneRef::Offset { base, distance } => {
let b = self.resolve_plane_in(vals, base, depth + 1, st)?;
Ok(b.offset(Self::eval_in(vals, distance, Kind::Length)?))
@@ -0,0 +1,4 @@
//! An explicit plane (a mirror's, or any plane reference) can be `placed`: an `origin` of three
//! length expressions with `x_dir` and `y_dir`, so it follows the design's parameters (#1492). A
//! plane whose origin is plain numbers is still saved as `custom`. Older designs need no
//! transformation; 0.13.x asks for an update instead of failing to read the plane.
+26 -2
View File
@@ -44,6 +44,30 @@ impl FeatureKind {
v
}
/// The coordinates of the points that place the feature: a primitive's position, an
/// explicit plane's origin. They are not inputs (no parameter names), but follow
/// parameters as inputs do.
pub fn placement_exprs_mut(&mut self) -> Vec<&mut String> {
let mut v = Vec::new();
self.walk_exprs_mut(&mut |(_, e, _, role)| {
if role == ExprRole::Placement {
v.push(e);
}
});
v
}
/// Read-only view of [`FeatureKind::placement_exprs_mut`].
pub fn placement_exprs(&self) -> Vec<&str> {
let mut v = Vec::new();
self.walk_exprs(&mut |(_, e, _, role)| {
if role == ExprRole::Placement {
v.push(e.as_str());
}
});
v
}
/// Every expression the feature uses: its inputs, then a primitive's placement coordinates.
pub fn expressions(&self) -> Vec<&str> {
let (mut inputs, mut placement) = (Vec::new(), Vec::new());
@@ -575,7 +599,7 @@ impl Document {
}
}
}
if f.kind.point_expr().is_some_and(|p| p.iter().any(|e| expr::references(e).iter().any(|r| r == name))) {
if f.kind.placement_exprs().iter().any(|e| expr::references(e).iter().any(|r| r == name)) {
out.push(ParamUser::Feature { feature: f.id, name: f.name.clone(), input: "Position".into() });
}
if let FeatureKind::Sketch { sketch, .. } = &f.kind {
@@ -646,7 +670,7 @@ impl Document {
*x = new.to_string();
}
}
for e in f.kind.point_expr_mut().into_iter().flatten() {
for e in f.kind.placement_exprs_mut() {
let r = expr::rename_reference(e, old, new);
if r != *e {
*e = r;
+15 -2
View File
@@ -264,8 +264,9 @@ impl<F: Fn(usize, usize) -> &'static str> Label for F {
}
}
/// The offsets and angles of a plane, outermost first (nesting is capped like every walk over a
/// plane), the construction plane it is built on, and the plane itself.
/// The offsets and angles of a plane, outermost first, and an explicit origin's coordinates
/// (nesting is capped like every walk over a plane), the construction plane it is built on, and
/// the plane itself.
impl Walk for PlaneRef {
fn walk_exprs<'a>(&'a self, f: &mut dyn FnMut(ExprItem<'a>)) {
let mut p = self;
@@ -280,6 +281,12 @@ impl Walk for PlaneRef {
p = base;
}
PlaneRef::AlongPath { t, .. } => return f(("Distance (0 to 1)", t, Kind::Unitless, ExprRole::Input)),
PlaneRef::Placed { origin, .. } => {
for (label, e) in ["X", "Y", "Z"].into_iter().zip(origin) {
f((label, e, Kind::Length, ExprRole::Placement));
}
return;
}
_ => return,
}
}
@@ -297,6 +304,12 @@ impl Walk for PlaneRef {
p = base;
}
PlaneRef::AlongPath { t, .. } => return f(("Distance (0 to 1)", t, Kind::Unitless, ExprRole::Input)),
PlaneRef::Placed { origin, .. } => {
for (label, e) in ["X", "Y", "Z"].into_iter().zip(origin) {
f((label, e, Kind::Length, ExprRole::Placement));
}
return;
}
_ => return,
}
}