Feature fields: a type for each role (Expr, BodyName, FacePick…) (#1273)

Thanks @jijinbei.
This commit is contained in:
JIJINBEI authored and GitHub committed 2026-10-16 00:28:12 -04:00
1 parent 3333f8a62b
commit 44968dbfe6
59 files changed
+1505 -1043

No files matched your search

+96 -92
View File
@@ -951,15 +951,15 @@ fn placement_digest(doc: &Document, f: &Feature) -> u64 {
_ => None,
};
if let Some(path) = path
&& doc.sketch(path).is_ok_and(|sketch| !sketch.wires.is_empty())
&& doc.sketch(path.0).is_ok_and(|sketch| !sketch.wires.is_empty())
{
let mut hash = std::collections::hash_map::DefaultHasher::new();
let owner = doc.feature(path).map(|feature| feature.component).unwrap_or(0);
let owner = doc.feature(path.0).map(|feature| feature.component).unwrap_or(0);
for frame in [doc.component_transform(f.component), doc.component_transform(owner)] {
frame.iter().flatten().for_each(|value| value.to_bits().hash(&mut hash));
}
if let FeatureKind::Sweep { sketch, .. } | FeatureKind::SurfaceSweep { sketch, .. } = &f.kind {
let owner = doc.feature(*sketch).map(|feature| feature.component).unwrap_or(0);
let owner = doc.feature(sketch.0).map(|feature| feature.component).unwrap_or(0);
doc.component_transform(owner).iter().flatten().for_each(|value| value.to_bits().hash(&mut hash));
}
return hash.finish();
@@ -1951,7 +1951,7 @@ fn face_level_at(st: &ModelState, plane: &Plane, h: f64, targets: &[String]) ->
/// The body a face-based feature applies to: by name, or the one whose surface is nearest the
/// first point.
fn body_at(st: &ModelState, body: &Option<String>, points: &[Vec3]) -> Result<usize> {
fn body_at(st: &ModelState, body: &Option<crate::BodyName>, points: &[Vec3]) -> Result<usize> {
if let Some(n) = body {
return st.bodies.iter().position(|b| &b.name == n).ok_or_else(|| DocError::Unknown(format!("body `{n}`")));
}
@@ -2085,7 +2085,7 @@ fn edge_dir(e: &kernel::EdgeInfo) -> Vec3 {
}
/// References for edges picked at `pts` on the body a blend would go on (the state before it).
pub fn edge_refs(st: &ModelState, body: &Option<String>, pts: &[Vec3]) -> Vec<EdgeRef> {
pub fn edge_refs(st: &ModelState, body: &Option<crate::BodyName>, pts: &[Vec3]) -> Vec<EdgeRef> {
let Ok(ti) = blend_target(st, body, pts) else { return Vec::new() };
let Some(mb) = st.bodies.get(ti) else { return Vec::new() };
let size = mb.body.size();
@@ -2103,7 +2103,7 @@ pub fn edge_refs(st: &ModelState, body: &Option<String>, pts: &[Vec3]) -> Vec<Ed
}
/// Persistent names of the edges picked at `pts` on the body a blend would go on.
pub fn edge_names_for(st: &ModelState, body: &Option<String>, pts: &[Vec3]) -> Vec<String> {
pub fn edge_names_for(st: &ModelState, body: &Option<crate::BodyName>, pts: &[Vec3]) -> Vec<String> {
match blend_target(st, body, pts).ok().and_then(|i| st.bodies.get(i)) {
Some(b) => crate::naming::edge_names_at(b, pts),
None => Vec::new(),
@@ -2190,7 +2190,7 @@ fn resolve_edges(b: &Body, pts: &[Vec3], refs: &[EdgeRef], warning: &mut Option<
}
/// The body a fillet/chamfer applies to: by name, or the one closest to the first edge point.
fn blend_target(state: &ModelState, body: &Option<String>, edges: &[Vec3]) -> Result<usize> {
fn blend_target(state: &ModelState, body: &Option<crate::BodyName>, edges: &[Vec3]) -> Result<usize> {
if let Some(n) = body {
return state.bodies.iter().position(|b| &b.name == n).ok_or_else(|| DocError::Unknown(format!("body `{n}`")));
}
@@ -2246,10 +2246,10 @@ pub(crate) fn revolve_axis(ss: &SolvedSketch, axis: &AxisRef) -> Result<(Vec2, V
AxisRef::Line { origin, dir } => {
let pl = &ss.plane;
let d = dir.normalized().ok_or_else(|| DocError::Invalid("revolve axis has no direction".into()))?;
if d.dot(pl.normal()).abs() > 1e-9 || pl.height(*origin).abs() > 1e-6 {
if d.dot(pl.normal()).abs() > 1e-9 || pl.height(origin.0).abs() > 1e-6 {
return Err(DocError::Invalid("the revolve axis does not lie in the sketch plane".into()));
}
Ok((pl.to_local(*origin), Vec2::new(d.dot(pl.x), d.dot(pl.y))))
Ok((pl.to_local(origin.0), Vec2::new(d.dot(pl.x), d.dot(pl.y))))
}
}
}
@@ -2303,7 +2303,8 @@ fn hole_spots(st: &ModelState, f: &Feature) -> Result<(Vec<Vec3>, Vec3)> {
let FeatureKind::Hole { position, direction, points, .. } = &f.kind else { return Err(DocError::Invalid("not a hole".into())) };
let (spots, dir) = match points {
Some(sp) => {
let ss = st.sketch(sp.sketch).ok_or_else(|| DocError::Unknown(format!("sketch {} (it must come earlier in the timeline)", sp.sketch)))?;
let ss =
st.sketch(sp.sketch.0).ok_or_else(|| DocError::Unknown(format!("sketch {} (it must come earlier in the timeline)", sp.sketch)))?;
let mut v = Vec::new();
for id in &sp.ids {
let p = ss.sketch.points.iter().find(|p| &p.id == id).ok_or_else(|| DocError::Unknown(format!("sketch point `{id}`")))?;
@@ -2315,7 +2316,7 @@ fn hole_spots(st: &ModelState, f: &Feature) -> Result<(Vec<Vec3>, Vec3)> {
}
None => {
let dir = direction.normalized().ok_or_else(|| DocError::Invalid("hole direction".into()))?;
let at = hole_on_surface(st, *position, dir);
let at = hole_on_surface(st, position.0, dir);
let dir = hole_dir_into_face(st, at, dir);
return Ok((vec![hole_on_surface(st, at, dir)], dir));
}
@@ -2328,7 +2329,7 @@ fn hole_spots(st: &ModelState, f: &Feature) -> Result<(Vec<Vec3>, Vec3)> {
/// A hole's depth: down to the To Object (where the hole's axis meets the face there, else the
/// point's depth along the axis), else as given.
fn hole_depth_to(st: &ModelState, at: &Vec3, dir: Vec3, to: &Option<Vec3>, depth: &Option<String>) -> Result<Option<String>> {
fn hole_depth_to(st: &ModelState, at: &Vec3, dir: Vec3, to: &Option<Vec3>, depth: &Option<crate::Expr>) -> Result<Option<crate::Expr>> {
let Some(t) = to else { return Ok(depth.clone()) };
let mut d = (*t - *at).dot(dir);
// The face at the To point, met along the axis.
@@ -2345,7 +2346,7 @@ fn hole_depth_to(st: &ModelState, at: &Vec3, dir: Vec3, to: &Option<Vec3>, depth
if !(d > 1e-6) {
return Err(DocError::Invalid("the hole's To object is not ahead of the hole".into()));
}
Ok(Some(format!("{d} mm")))
Ok(Some(format!("{d} mm").into()))
}
/// A hole's spots as given (not yet moved onto the surface) and its direction.
@@ -2353,7 +2354,7 @@ fn hole_spots_raw(st: &ModelState, f: &Feature) -> Result<(Vec<Vec3>, Vec3)> {
let FeatureKind::Hole { position, direction, points, .. } = &f.kind else { return Err(DocError::Invalid("not a hole".into())) };
match points {
Some(sp) => {
let ss = st.sketch(sp.sketch).ok_or_else(|| DocError::Unknown(format!("sketch {}", sp.sketch)))?;
let ss = st.sketch(sp.sketch.0).ok_or_else(|| DocError::Unknown(format!("sketch {}", sp.sketch)))?;
let v: Vec<Vec3> =
sp.ids.iter().filter_map(|id| ss.sketch.points.iter().find(|p| &p.id == id)).map(|p| ss.plane.to_world(p.pos)).take(10_000).collect();
let dir = sketch_hole_dir(st, &v, ss.plane.normal());
@@ -2362,7 +2363,7 @@ fn hole_spots_raw(st: &ModelState, f: &Feature) -> Result<(Vec<Vec3>, Vec3)> {
}
None => {
let dir = direction.normalized().ok_or_else(|| DocError::Invalid("hole direction".into()))?;
Ok((vec![*position], hole_dir_into_face(st, hole_on_surface(st, *position, dir), dir)))
Ok((vec![position.0], hole_dir_into_face(st, hole_on_surface(st, position.0, dir), dir)))
}
}
}
@@ -2377,7 +2378,7 @@ fn hole_feature_tools(vals: &BTreeMap<String, Value>, st: &ModelState, f: &Featu
}
let mut tools = Vec::new();
for at in spots {
let depth = hole_depth_to(st, &at, dir, to, depth)?;
let depth = hole_depth_to(st, &at, dir, &to.map(Vec3::from), depth)?;
tools.extend(hole_tools_and_prisms(vals, st, &at, dir, diameter, &depth, hole)?);
}
Ok(tools)
@@ -2454,7 +2455,7 @@ fn hole_tools(
position: &Vec3,
dir: Vec3,
diameter: &str,
depth: &Option<String>,
depth: &Option<crate::Expr>,
hole: &HoleKind,
) -> Result<Vec<Traced>> {
Ok(hole_tools_and_prisms(vals, st, position, dir, diameter, depth, hole)?.into_iter().map(|(tool, _)| tool).collect())
@@ -2471,7 +2472,7 @@ fn hole_tools_and_prisms(
position: &Vec3,
dir: Vec3,
diameter: &str,
depth: &Option<String>,
depth: &Option<crate::Expr>,
hole: &HoleKind,
) -> Result<Vec<(Traced, Option<more::PrimitivePrism>)>> {
let r = val(vals, diameter, Kind::Length)? / 2.0;
@@ -2746,7 +2747,7 @@ fn feature_tools_with_constructor(
FeatureKind::Extrude { sketch, profiles, extent, operation, targets, thin } => {
let ss = sketch_in_feature_frame(
doc,
st.sketch(*sketch).ok_or_else(|| DocError::Unknown(format!("sketch {sketch} (it must come earlier in the timeline)")))?,
st.sketch(sketch.0).ok_or_else(|| DocError::Unknown(format!("sketch {sketch} (it must come earlier in the timeline)")))?,
f.component,
)?;
let regions = match thin {
@@ -2762,14 +2763,14 @@ fn feature_tools_with_constructor(
// The face names follow `face_points`: To, then From.
let name = |k: usize| f.face_names.get(k).map(String::as_str);
let (to_name, from_name) = if extent.to.is_some() { (name(0), name(1)) } else { (None, name(0)) };
let to_face = extent.to.and_then(|p| object_face(&tst, to_name, p));
let to_face = extent.to.and_then(|p| object_face(&tst, to_name, p.0));
let centre = {
let pts: Vec<Vec2> = regions.iter().flat_map(|r| r.outer.polyline(1e-2)).collect();
let c = pts.iter().fold(Vec2::default(), |a, p| a + *p) * (1.0 / pts.len().max(1) as f64);
ss.plane.to_world(c)
};
let from = match extent.from {
Some(p) => height_at(&tst, object_face(&tst, from_name, p), &ss.plane, centre, p),
Some(p) => height_at(&tst, object_face(&tst, from_name, p.0), &ss.plane, centre, p.0),
None => 0.0,
};
let d = match extent.to {
@@ -2778,7 +2779,7 @@ fn feature_tools_with_constructor(
Some(e) => val(vals, e, Kind::Length)?,
None => 0.0,
};
let h = height_at(&tst, to_face, &ss.plane, centre, p);
let h = height_at(&tst, to_face, &ss.plane, centre, p.0);
let start = from + extent.start_offset.as_deref().map(|e| val(vals, e, Kind::Length)).transpose()?.unwrap_or(0.0);
// Past the face by the offset, in the direction travelled.
let to = h + if h >= start { off } else { -off };
@@ -2787,7 +2788,7 @@ fn feature_tools_with_constructor(
}
to - start
}
None if extent.through_all => through_all_distance(st, &ss.plane, targets)?,
None if extent.through_all => through_all_distance(st, &ss.plane, &crate::plain(targets))?,
None => val(vals, &extent.distance, Kind::Length)?,
};
if let Some(tp) = &extent.taper {
@@ -2805,7 +2806,7 @@ fn feature_tools_with_constructor(
// How far a tool grows past its profile at either end, at most.
let mut growth: f64 = 0.0;
// A face of the targets in the profile's plane (the same for every profile).
let on_face = matches!(operation, Operation::Join | Operation::Cut) && face_level_at(st, &ss.plane, from, targets);
let on_face = matches!(operation, Operation::Join | Operation::Cut) && face_level_at(st, &ss.plane, from, &crate::plain(targets));
let tools: Vec<Traced> = regions
.iter()
.map(|r| {
@@ -2814,7 +2815,7 @@ fn feature_tools_with_constructor(
// and the boolean finds no result there. The sides keep their slope, so
// the profile stays the section at the start.
let (lo, hi) = if sign < 0.0 { (from - len, from) } else { (from, from + len) };
let (l2, h2) = extend_for_coplanar(st, &ss.plane, r, lo, hi, *operation, targets);
let (l2, h2) = extend_for_coplanar(st, &ss.plane, r, lo, hi, *operation, &crate::plain(targets));
let (a, b) = if sign < 0.0 { (from - h2, from - l2) } else { (l2 - from, h2 - from) };
// A start that can't run on (the profile hangs over the face's edge):
// the sides stand straight for a hair first, so the boolean can push the
@@ -2882,7 +2883,7 @@ fn feature_tools_with_constructor(
if exact.iter().all(|tool| {
st.bodies
.iter()
.filter(|b| targets.is_empty() || targets.contains(&b.name))
.filter(|b| targets.is_empty() || targets.iter().any(|t| *t == b.name))
.any(|b| kernel::can_join_touching_faces(&b.body, &tool.body))
}) {
// Preserve a whole touching face: extending into a chamfer replaces
@@ -2893,11 +2894,12 @@ fn feature_tools_with_constructor(
continue;
}
}
let (l2, h2) = extend_for_coplanar(st, &ss.plane, r, lo + off, hi + off, *operation, targets);
let (l2, h2) = extend_for_coplanar(st, &ss.plane, r, lo + off, hi + off, *operation, &crate::plain(targets));
let probe = |p: Vec2| (1..4).map(|k| ss.plane.to_world(p) + n * (lo + off + (hi - lo) * k as f64 / 4.0)).collect::<Vec<_>>();
let original = r;
let r = grow_profile_for_coplanar(st, r, *operation, targets, &probe);
let r = fill_void_holes(st, &r, *operation, targets, &ss.plane, (l2, h2));
let plain_targets = crate::plain(targets);
let r = grow_profile_for_coplanar(st, r, *operation, &plain_targets, &probe);
let r = fill_void_holes(st, &r, *operation, &plain_targets, &ss.plane, (l2, h2));
untrimmed &= l2 == lo + off && h2 == hi + off && &r == original;
tools.extend(centered_extrude(&ss.plane, &r, l2, h2, Some(&ss))?);
footprints.push(r);
@@ -2973,7 +2975,7 @@ fn feature_tools_with_constructor(
}
}
// Certificates set face sources only: the faces, and their lineage, stay.
let body = body.with_extrude_certificates(f.id, *sketch, &curves)?.with_extrude_cap_certificates(f.id, *sketch, &caps)?;
let body = body.with_extrude_certificates(f.id, sketch.0, &curves)?.with_extrude_cap_certificates(f.id, sketch.0, &caps)?;
Ok(Traced { body, lineage })
})
.collect::<Result<Vec<_>>>()?;
@@ -2983,7 +2985,7 @@ fn feature_tools_with_constructor(
FeatureKind::Revolve { sketch, profiles, axis, angle, angle2, symmetric, convention, to, .. } => {
let ss = sketch_in_feature_frame(
doc,
st.sketch(*sketch).ok_or_else(|| DocError::Unknown(format!("sketch {sketch} (it must come earlier in the timeline)")))?,
st.sketch(sketch.0).ok_or_else(|| DocError::Unknown(format!("sketch {sketch} (it must come earlier in the timeline)")))?,
f.component,
)?;
let regions = solidcraft_sketch::merge_regions(&select_profiles(&ss, profiles)?);
@@ -3002,7 +3004,7 @@ fn feature_tools_with_constructor(
(ss.plane.to_world(o), ss.plane.dir_to_world(d)),
angles,
*symmetric,
*to,
to.map(Vec3::from),
*convention,
)?;
let (start, ang) = (extent.start, extent.sweep);
@@ -3017,7 +3019,7 @@ fn feature_tools_with_constructor(
(ss.plane.to_world(o), ss.plane.dir_to_world(d)),
angles,
*symmetric,
*to,
to.map(Vec3::from),
*convention,
)
} else {
@@ -3052,7 +3054,7 @@ fn feature_tools_with_constructor(
.collect::<Result<Vec<_>>>()?;
// To a curved face: turn past the whole target body, then cut back to its surface
// (the piece that starts at the profile).
let curved = curved_face(&tst, to.and_then(|p| object_face(&tst, f.face_names.first().map(String::as_str), p)), None);
let curved = curved_face(&tst, to.and_then(|p| object_face(&tst, f.face_names.first().map(String::as_str), p.0)), None);
let curved_body = curved.and_then(|bi| tst.bodies.get(bi));
// Each region's body: its sides named by the sketch curves, its caps start and end.
let tool = |r: &Region2, body: Body, built: Option<&Body>| crate::lineage::ProfileRoles::swept(Some(&ss), Some(r)).tool(body, built);
@@ -3114,11 +3116,11 @@ fn feature_tools_with_constructor(
// A profile lofted to a sketch point (an apex).
let [a, b] = &sections[..] else { return Err(DocError::Invalid("not supported yet: a point section in a loft of more than two".into())) };
let (prof, pt) = if a.point.is_some() { (b, a) } else { (a, b) };
let ss = st.sketch(prof.sketch).ok_or_else(|| DocError::Unknown(format!("sketch {}", prof.sketch)))?;
let ss = st.sketch(prof.sketch.0).ok_or_else(|| DocError::Unknown(format!("sketch {}", prof.sketch)))?;
let regions = solidcraft_sketch::merge_regions(&select_profiles(ss, &prof.profiles)?);
let [r] = &regions[..] else { return Err(DocError::Invalid("each loft section must be one profile".into())) };
refuse_loft_holes(r)?;
let ps = st.sketch(pt.sketch).ok_or_else(|| DocError::Unknown(format!("sketch {}", pt.sketch)))?;
let ps = st.sketch(pt.sketch.0).ok_or_else(|| DocError::Unknown(format!("sketch {}", pt.sketch)))?;
let id = pt.point.as_deref().unwrap_or_default();
let q = ps.sketch.points.iter().find(|p| p.id == id).ok_or_else(|| DocError::Unknown(format!("point `{id}` in {}", ps.name)))?;
let body = kernel::loft_to_point(&ss.plane, &r.outer, ps.plane.to_world(q.pos))?;
@@ -3169,7 +3171,7 @@ fn feature_tools_with_constructor(
let mut sketches = Vec::new();
for sec in sections {
let ss = st
.sketch(sec.sketch)
.sketch(sec.sketch.0)
.ok_or_else(|| DocError::Unknown(format!("sketch {} (it must come earlier in the timeline)", sec.sketch)))?;
let regions = solidcraft_sketch::merge_regions(&select_profiles(ss, &sec.profiles)?);
let [r] = &regions[..] else { return Err(DocError::Invalid("each loft section must be one profile".into())) };
@@ -3185,9 +3187,9 @@ fn feature_tools_with_constructor(
}
FeatureKind::Sweep { sketch, profiles, path_sketch, path, orientation, twist, scale, .. } => {
let ctl = sweep_control(vals, orientation, twist, scale)?;
let ss = st.sketch(*sketch).ok_or_else(|| DocError::Unknown(format!("sketch {sketch} (it must come earlier in the timeline)")))?;
let ss = st.sketch(sketch.0).ok_or_else(|| DocError::Unknown(format!("sketch {sketch} (it must come earlier in the timeline)")))?;
let ps =
st.sketch(*path_sketch).ok_or_else(|| DocError::Unknown(format!("sketch {path_sketch} (it must come earlier in the timeline)")))?;
st.sketch(path_sketch.0).ok_or_else(|| DocError::Unknown(format!("sketch {path_sketch} (it must come earlier in the timeline)")))?;
let profile;
let ss = if !ps.sketch.wires.is_empty() {
profile = profile_in_frame(doc, ss, f.component)?;
@@ -3209,7 +3211,7 @@ fn feature_tools_with_constructor(
}
FeatureKind::Pipe { path_sketch, path, diameter, wall, .. } => {
let ps =
st.sketch(*path_sketch).ok_or_else(|| DocError::Unknown(format!("sketch {path_sketch} (it must come earlier in the timeline)")))?;
st.sketch(path_sketch.0).ok_or_else(|| DocError::Unknown(format!("sketch {path_sketch} (it must come earlier in the timeline)")))?;
let mapped = path_in_frame(doc, ps, f.component)?;
let ps = mapped.as_ref();
let r = val(vals, diameter, Kind::Length)? / 2.0;
@@ -3239,7 +3241,7 @@ fn feature_tools_with_constructor(
}
FeatureKind::Cylinder { base, axis, radius, height, .. } => Ok(vec![crate::lineage::cylinder_tool(kernel::cylinder(
point(vals, base)?,
*axis,
axis.0,
val(vals, radius, Kind::Length)?,
val(vals, height, Kind::Length)?,
)?)]),
@@ -3258,13 +3260,13 @@ fn feature_tools_with_constructor(
pub(crate) fn sweep_control(
vals: &BTreeMap<String, Value>,
orientation: &crate::SweepOrientation,
twist: &Option<String>,
scale: &Option<String>,
twist: &Option<crate::Expr>,
scale: &Option<crate::Expr>,
) -> Result<kernel::SweepControl> {
let orientation = match *orientation {
crate::SweepOrientation::Perpendicular => kernel::SweepOrientation::Perpendicular,
crate::SweepOrientation::Parallel => kernel::SweepOrientation::Parallel,
crate::SweepOrientation::Direction(d) => kernel::SweepOrientation::Direction(d),
crate::SweepOrientation::Direction(d) => kernel::SweepOrientation::Direction(d.0),
};
let twist = match twist {
Some(t) => val(vals, t, Kind::Angle)?,
@@ -3280,7 +3282,7 @@ pub(crate) fn sweep_control(
}
/// A pipe's section: a circle of radius `r`, hollow when it has a wall.
fn pipe_region(vals: &BTreeMap<String, Value>, r: f64, wall: &Option<String>) -> Result<Region2> {
fn pipe_region(vals: &BTreeMap<String, Value>, r: f64, wall: &Option<crate::Expr>) -> Result<Region2> {
let holes = match wall {
Some(w) => {
let wt = val(vals, w, Kind::Length)?;
@@ -3485,15 +3487,15 @@ pub(crate) fn extent_sides(vals: &BTreeMap<String, Value>, extent: &crate::Exten
/// Operation and targets of a feature that makes tools.
fn feature_op(f: &Feature) -> (Operation, Vec<String>) {
match &f.kind {
FeatureKind::Extrude { operation, targets, .. } | FeatureKind::Revolve { operation, targets, .. } => (*operation, targets.clone()),
FeatureKind::Extrude { operation, targets, .. } | FeatureKind::Revolve { operation, targets, .. } => (*operation, crate::plain(targets)),
FeatureKind::Box { operation, .. }
| FeatureKind::Cylinder { operation, .. }
| FeatureKind::Sphere { operation, .. }
| FeatureKind::Torus { operation, .. } => (*operation, Vec::new()),
FeatureKind::Hole { .. } => (Operation::Cut, Vec::new()),
FeatureKind::Pipe { operation, targets, .. } => (*operation, targets.clone()),
FeatureKind::Loft { operation, targets, .. } | FeatureKind::Sweep { operation, targets, .. } => (*operation, targets.clone()),
FeatureKind::Coil { operation, targets, .. } => (*operation, targets.clone()),
FeatureKind::Pipe { operation, targets, .. } => (*operation, crate::plain(targets)),
FeatureKind::Loft { operation, targets, .. } | FeatureKind::Sweep { operation, targets, .. } => (*operation, crate::plain(targets)),
FeatureKind::Coil { operation, targets, .. } => (*operation, crate::plain(targets)),
_ => (Operation::NewBody, Vec::new()),
}
}
@@ -3583,7 +3585,7 @@ fn pattern_transforms(doc: &Document, vals: &BTreeMap<String, Value>, st: &Model
crate::PatternKind::Path { path_sketch, path, count: c, spacing, extent, flip, orient } => {
let n = count(c)?;
let d = val(vals, spacing, Kind::Length)?;
let ps = st.sketch(*path_sketch).ok_or_else(|| DocError::Unknown(format!("path sketch {path_sketch}")))?;
let ps = st.sketch(path_sketch.0).ok_or_else(|| DocError::Unknown(format!("path sketch {path_sketch}")))?;
let mapped = path_in_frame(doc, ps, component)?;
let ps = mapped.as_ref();
let mut segs = path_segments(ps, path, Vec3::new(f64::NAN, f64::NAN, f64::NAN))?;
@@ -3625,7 +3627,7 @@ fn pattern_transforms(doc: &Document, vals: &BTreeMap<String, Value>, st: &Model
0.0
};
for i in 1..n {
out.push(rotation(*origin, *axis, step * i as f64));
out.push(rotation(origin.0, axis.0, step * i as f64));
}
}
}
@@ -3722,7 +3724,7 @@ fn replay(
let r = val(vals, radius, Kind::Length)?;
// The edges the feature blended in this evaluation (found by name), falling back to
// its picked points.
let found = st.blend_edges.get(&src.id).cloned().unwrap_or_else(|| edges.clone());
let found = st.blend_edges.get(&src.id).cloned().unwrap_or_else(|| crate::plain(edges));
for m in mats {
let moved: Vec<Vec3> = found.iter().map(|p| crate::apply_point(m, *p)).collect();
let ti = blend_target(st, &None, &moved)?;
@@ -3757,7 +3759,7 @@ fn replay(
let d = crate::apply_vector(m, dir).normalized().ok_or_else(|| DocError::Invalid("hole direction".into()))?;
for p in &spots {
let at = hole_on_surface(st, crate::apply_point(m, *p), d);
let to = to.map(|t| crate::apply_point(m, t));
let to = to.map(|t| crate::apply_point(m, t.0));
let depth = hole_depth_to(st, &at, d, &to, depth)?;
// Each copy's faces are named as the hole's, with the instance.
copies.extend(
@@ -4085,8 +4087,8 @@ fn chamfer_with(
b: &kernel::Body,
edges: &[Vec3],
d: f64,
distance2: &Option<String>,
angle: &Option<String>,
distance2: &Option<crate::Expr>,
angle: &Option<crate::Expr>,
flip: bool,
) -> Result<kernel::Body> {
let side = match (distance2, angle) {
@@ -4102,7 +4104,9 @@ fn chamfer_with(
fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st: &mut ModelState, warning: &mut Option<String>) -> Result<()> {
match &f.kind {
FeatureKind::BodyTransfer { bodies, from, to, transform } => crate::body_transfer::evaluate(doc, st, bodies, *from, *to, *transform),
FeatureKind::BodyTransfer { bodies, from, to, transform } => {
crate::body_transfer::evaluate(doc, st, &crate::plain(bodies), *from, *to, *transform)
}
FeatureKind::SheetBase { .. }
| FeatureKind::SheetContour { .. }
| FeatureKind::SheetFlange { .. }
@@ -4189,12 +4193,12 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
// Cuts through sheet metal become cut-outs of its flat pattern.
if matches!(operation, Operation::Cut | Operation::Intersect) && matches!(f.kind, FeatureKind::Extrude { .. }) && !st.sheets.is_empty() {
let bodies: Vec<Body> = tools.iter().map(|t| t.body.clone()).collect();
let handled = more::sheet_cut(f, st, &bodies, targets, constructor.as_ref(), warning)?;
let handled = more::sheet_cut(f, st, &bodies, &crate::plain(targets), constructor.as_ref(), warning)?;
if !handled.is_empty() {
return apply_beside_sheet_cut(st, f, tools, *operation, targets, &handled);
return apply_beside_sheet_cut(st, f, tools, *operation, &crate::plain(targets), &handled);
}
}
apply_traced(st, f, tools, *operation, targets)
apply_traced(st, f, tools, *operation, &crate::plain(targets))
}
FeatureKind::Hole { thread, diameter, .. } => {
let (spots, dir) = hole_spots(st, f)?;
@@ -4222,14 +4226,14 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
Some(e) => Some(val(vals, e, Kind::Length)?),
None => None,
};
let t = thread_on(st, f.id, *face, designation.as_deref(), l)?;
let t = thread_on(st, f.id, face.0, designation.as_deref(), l)?;
if !*modeled {
st.threads.push(t);
return Ok(());
}
let i = st.bodies.iter().position(|b| b.name == t.body).ok_or_else(|| DocError::Invalid("threaded body".into()))?;
let Some(mb) = st.bodies.get(i).cloned() else { return Err(DocError::Invalid("body".into())) };
let cyl = kernel::cylinder_face_at(&mb.body, *face).ok_or_else(|| DocError::Invalid("no cylindrical face there to thread".into()))?;
let cyl = kernel::cylinder_face_at(&mb.body, face.0).ok_or_else(|| DocError::Invalid("no cylindrical face there to thread".into()))?;
let tol = t.pitch * 1e-3;
let groove = thread_groove(&t, cyl.radius, ((t.start - cyl.start).abs() < tol, (t.end - cyl.end).abs() < tol))?;
// The B-rep boolean can't take helical walls in reasonable time: the body is cut
@@ -4247,9 +4251,9 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
}
FeatureKind::Shell { faces, thickness, body, direction, tangent_chain } => {
let t = val(vals, thickness, Kind::Length)?;
let i = body_at(st, body, faces)?;
let i = body_at(st, body, &crate::plain(faces))?;
let Some(mb) = st.bodies.get(i).cloned() else { return Err(DocError::Invalid("body".into())) };
let open = if *tangent_chain { smooth_chain(&mb, faces) } else { faces.clone() };
let open = if *tangent_chain { smooth_chain(&mb, &crate::plain(faces)) } else { crate::plain(faces) };
let input = mb.traced();
// The shell, and the operands of the operation that made it.
let (nb, operands) = match direction.as_str() {
@@ -4286,9 +4290,9 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
FeatureKind::Draft { faces, angle, neutral, pull, body } => {
let a = val(vals, angle, Kind::Angle)?;
let pl = doc.resolve_plane_in(vals, neutral, 0, Some(st))?;
let i = body_at(st, body, faces)?;
let i = body_at(st, body, &crate::plain(faces))?;
let Some(mb) = st.bodies.get(i).cloned() else { return Err(DocError::Invalid("body".into())) };
let nb = Traced::step(kernel::draft(&mb.body, faces, &pl, *pull, a)?, &[&mb.traced()]);
let nb = Traced::step(kernel::draft(&mb.body, &crate::plain(faces), &pl, pull.0, a)?, &[&mb.traced()]);
if let Some(slot) = st.bodies.get_mut(i) {
*slot = ModelBody::traced_new(mb.name, nb, mb.feature);
}
@@ -4332,7 +4336,7 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
let mut made = Vec::new();
for p in cells {
// The cell: inside the tools that hold the point, outside the others.
let inside: Vec<bool> = tb.iter().map(|b| b.mesh().contains(*p)).collect();
let inside: Vec<bool> = tb.iter().map(|b| b.mesh().contains(p.0)).collect();
let mut acc: Option<Traced> = None;
for (b, _) in tb.iter().zip(&inside).filter(|(_, i)| **i) {
acc = match acc {
@@ -4350,16 +4354,16 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
made.push(cell);
}
if *remove_tools {
st.bodies.retain(|b| !tools.contains(&b.name));
st.bodies.retain(|b| !tools.iter().any(|t| *t == b.name));
}
apply_traced(st, f, made, *operation, &[])
}
FeatureKind::SplitFace { faces, body, plane, tool, sketch } => {
let i = body_at(st, body, faces)?;
let i = body_at(st, body, &crate::plain(faces))?;
let Some(mb) = st.bodies.get(i).cloned() else { return Err(DocError::Invalid("body".into())) };
let mut picked: Vec<usize> = Vec::new();
for p in faces {
let fi = crate::appearance::face_index_at(&mb, *p)
let fi = crate::appearance::face_index_at(&mb, p.0)
.ok_or_else(|| DocError::Invalid(format!("Split Face: no face of {} at {p:?}", mb.name)))?;
if !picked.contains(&fi) {
picked.push(fi);
@@ -4368,12 +4372,12 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
let nb = match (tool, sketch) {
(Some(t), _) => {
let tb = st.body(&t.body).ok_or_else(|| DocError::Unknown(format!("body `{}`", t.body)))?;
let face = crate::appearance::face_index_at(tb, t.point)
let face = crate::appearance::face_index_at(tb, t.point.0)
.ok_or_else(|| DocError::Invalid(format!("no face of `{}` at the tool point", t.body)))?;
kernel::split_faces(&mb.body, &picked, &kernel::SplitTool::Face { body: &tb.body, face })?
}
(None, Some(sid)) => {
let ss = st.sketch(*sid).ok_or_else(|| DocError::Unknown(format!("sketch {sid}")))?;
let ss = st.sketch(sid.0).ok_or_else(|| DocError::Unknown(format!("sketch {sid}")))?;
// Each curve's exact segments (lines and arcs split by their own plane and cylinder).
let curves: Vec<Vec<solidcraft_geom::Seg2>> = (0..ss.sketch.curves.len())
.filter(|ci| ss.sketch.curves.get(*ci).is_some_and(|c| !c.construction))
@@ -4403,7 +4407,7 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
// A face of a body (its surface extended).
Some(t) => {
let tb = st.body(&t.body).ok_or_else(|| DocError::Unknown(format!("body `{}`", t.body)))?;
let face = crate::appearance::face_index_at(tb, t.point)
let face = crate::appearance::face_index_at(tb, t.point.0)
.ok_or_else(|| DocError::Invalid(format!("no face of `{}` at the tool point", t.body)))?;
kernel::split_body(&mb.body, &kernel::SplitTool::Face { body: &tb.body, face })?
}
@@ -4432,7 +4436,7 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
FeatureKind::Pattern { features, pattern, bodies } => {
let mats = pattern_transforms(doc, vals, st, f.component, pattern)?;
if bodies.is_empty() {
return replay(doc, vals, f, st, features, &mats, warning);
return replay(doc, vals, f, st, &crate::plain(features), &mats, warning);
}
// Bodies: each copy is a new body named after its source.
let mut output = 0;
@@ -4450,7 +4454,7 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
FeatureKind::Mirror { features, plane, bodies, combine } => {
let pl = doc.resolve_plane_in(vals, plane, 0, Some(st))?;
if bodies.is_empty() {
return replay(doc, vals, f, st, features, &[mirror_matrix(&pl)], warning);
return replay(doc, vals, f, st, &crate::plain(features), &[mirror_matrix(&pl)], warning);
}
let m = mirror_matrix(&pl);
let mut output = 0;
@@ -4485,13 +4489,13 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
}
FeatureKind::Fillet { edges, radius, body, .. } | FeatureKind::Chamfer { edges, distance: radius, body, .. } => {
let r = val(vals, radius, Kind::Length)?;
let ti = blend_target(st, body, edges)?;
let ti = blend_target(st, body, &crate::plain(edges))?;
let Some(mb) = st.bodies.get(ti) else { return Err(DocError::Invalid("body".into())) };
let edges = if f.edge_names.iter().any(|n| !n.is_empty()) {
// By name first; a name that is gone falls back to the point, with a warning.
let (mut out, mut notes) = (Vec::new(), Vec::new());
for (i, p) in edges.iter().enumerate() {
match f.edge_names.get(i).filter(|n| !n.is_empty()).map(|n| (n, crate::naming::locate_edge(mb, n, *p))) {
match f.edge_names.get(i).filter(|n| !n.is_empty()).map(|n| (n, crate::naming::locate_edge(mb, n, p.0))) {
Some((n, Ok(hit))) => {
if let Some(note) = hit.note(n) {
notes.push(note);
@@ -4506,9 +4510,9 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
// The name is gone. A pick on the axis of a round must not fall
// through to the tangency seam (#775). A sharp edge that moved
// still resolves from the point, which an edit relies on.
if pick_lies_on_a_round_axis(&mb.body, *p) {
if pick_lies_on_a_round_axis(&mb.body, p.0) {
notes.push(skipped);
} else if let Ok(q) = resolve_edges(&mb.body, &[*p], &refs, &mut w) {
} else if let Ok(q) = resolve_edges(&mb.body, &[p.0], &refs, &mut w) {
out.extend(q);
notes.push(match f.edge_names.get(i) {
Some(n) if !n.is_empty() => format!("edge `{n}` no longer exists; using the edge at the picked point"),
@@ -4529,7 +4533,7 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
}
out
} else {
resolve_edges(&mb.body, edges, &f.edge_refs, warning)?
resolve_edges(&mb.body, &crate::plain(edges), &f.edge_refs, warning)?
};
st.blend_edges.insert(f.id, edges.clone());
let input = mb.traced();
@@ -4543,7 +4547,7 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
for (t, e) in mid {
pts.push((*t, val(vals, e, Kind::Length)?));
}
one_by_one(input, &edges, |b, p| kernel::fillet_variable_points(b, p, *start, &pts, *smooth))?
one_by_one(input, &edges, |b, p| kernel::fillet_variable_points(b, p, start.0, &pts, *smooth))?
}
FeatureKind::Fillet { style: crate::FilletStyle::Spline { start, knots, radii }, .. } => {
let mut values = vec![r];
@@ -4551,7 +4555,7 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
values.push(val(vals, e, Kind::Length)?);
}
let law = crate::FilletStyle::spline(knots, values);
one_by_one(input, &edges, |b, p| kernel::fillet_variable_spline(b, p, *start, &law))?
one_by_one(input, &edges, |b, p| kernel::fillet_variable_spline(b, p, start.0, &law))?
}
FeatureKind::Fillet { fit: crate::FilletFit::Taper, .. } => {
let (b, tapers) = kernel::fillet_tapered(&mb.body, &edges, r)?;
@@ -4598,9 +4602,9 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
}
FeatureKind::Cylinder { base, axis, radius, height, operation } => {
let (base, radius, height) = (point(vals, base)?, val(vals, radius, Kind::Length)?, val(vals, height, Kind::Length)?);
let b = kernel::cylinder(base, *axis, radius, height)?;
let b = kernel::cylinder(base, axis.0, radius, height)?;
let b = if matches!(operation, Operation::NewBody | Operation::Join) { b.with_cylinder_certificate(f.id)? } else { b };
let prism = more::PrimitivePrism::Cylinder { base, axis: *axis, radius, height };
let prism = more::PrimitivePrism::Cylinder { base, axis: axis.0, radius, height };
let handled = more::sheet_primitive_cut(f, st, &b, prism, *operation, false, warning)?;
let tools = vec![crate::lineage::cylinder_tool(b)];
if !handled.is_empty() {
@@ -4628,9 +4632,9 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
return Err(DocError::Unknown(format!("body `{target}`")));
}
let op = if *operation == Operation::NewBody { Operation::Join } else { *operation };
apply_traced(st, f, tb, op, std::slice::from_ref(target))?;
apply_traced(st, f, tb, op, std::slice::from_ref(&target.0))?;
if !keep_tools {
st.bodies.retain(|b| !tools.contains(&b.name));
st.bodies.retain(|b| !tools.iter().any(|t| *t == b.name));
}
Ok(())
}
@@ -4664,20 +4668,20 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
}
FeatureKind::OffsetFace { faces, distance, body } => {
let d = val(vals, distance, Kind::Length)?;
let i = body_at(st, body, faces)?;
let i = body_at(st, body, &crate::plain(faces))?;
let Some(mb) = st.bodies.get(i).cloned() else { return Err(DocError::Invalid("body".into())) };
let nb = Traced::step(kernel::offset_faces(&mb.body, faces, d)?, &[&mb.traced()]);
let nb = Traced::step(kernel::offset_faces(&mb.body, &crate::plain(faces), d)?, &[&mb.traced()]);
if let Some(slot) = st.bodies.get_mut(i) {
*slot = ModelBody::traced_new(mb.name, nb, mb.feature);
}
Ok(())
}
FeatureKind::DeleteFace { faces, body, heal } => {
let i = body_at(st, body, faces)?;
let i = body_at(st, body, &crate::plain(faces))?;
let Some(mb) = st.bodies.get(i).cloned() else { return Err(DocError::Invalid("body".into())) };
let mut picked: Vec<usize> = Vec::new();
for p in faces {
let fi = crate::appearance::face_index_at(&mb, *p)
let fi = crate::appearance::face_index_at(&mb, p.0)
.ok_or_else(|| DocError::Invalid(format!("Delete Face: no face of {} at {p:?}", mb.name)))?;
if !picked.contains(&fi) {
picked.push(fi);
@@ -4694,7 +4698,7 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
FeatureKind::BoundingSolid { bodies, margin } => {
let m = val(vals, margin, Kind::Length)?;
let mut bb = solidcraft_geom::Aabb3::EMPTY;
for b in st.bodies.iter().filter(|b| bodies.is_empty() || bodies.contains(&b.name)) {
for b in st.bodies.iter().filter(|b| bodies.is_empty() || bodies.iter().any(|t| *t == b.name)) {
bb = bb.union(&b.mesh().bounds());
}
if bb.is_empty() {
@@ -4713,7 +4717,7 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
Some(a) => val(vals, a, Kind::Angle)?,
None => 0.0,
};
let axis = rotate_axis.unwrap_or(Vec3::Z);
let axis = rotate_axis.map(Vec3::from).unwrap_or(Vec3::Z);
// With `copy` set, the source body is kept in place and the transform lands on a new
// body pushed beside it instead of replacing it.
let mut output = 0;
@@ -5215,7 +5219,7 @@ mod chain_tests {
.unwrap();
doc.add_feature(
FeatureKind::Fillet {
edges: vec![Vec3::new(20.0, 0.0, 20.0)],
edges: vec![crate::Position(Vec3::new(20.0, 0.0, 20.0))],
radius: "4".into(),
body: None,
style: crate::FilletStyle::Constant,