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

+2 -2
View File
@@ -176,7 +176,7 @@ fn map_geo_ref(m: &Mat, g: &mut crate::construct::GeoRef) {
/// Map a point given as expressions by `m`: numbers stay numbers; a coordinate that is an
/// expression stays one (`m · (x, y, z) + t`, written out), so it still follows its parameters.
pub fn map_point_expr(m: &Mat, p: &mut crate::PointExpr) {
let num = |e: &String| e.trim().parse::<f64>().ok().filter(|v| v.is_finite());
let num = |e: &crate::Expr| e.trim().parse::<f64>().ok().filter(|v| v.is_finite());
if let [Some(x), Some(y), Some(z)] = [num(&p[0]), num(&p[1]), num(&p[2])] {
*p = crate::point_expr(apply_point(m, Vec3::new(x, y, z)));
return;
@@ -202,7 +202,7 @@ pub fn map_point_expr(m: &Mat, p: &mut crate::PointExpr) {
} else if constant.abs() >= 1e-12 {
e = format!("{e} {} {} mm", if constant < 0.0 { '-' } else { '+' }, constant.abs());
}
*out = e;
*out = e.into();
}
}
+7 -1
View File
@@ -78,7 +78,13 @@ mod tests {
(vec!["bad\nname".into()], 0, crate::IDENTITY),
(vec!["Block".into()], 0, scaled),
] {
assert!(doc.add_feature(crate::FeatureKind::BodyTransfer { bodies, from, to: 0, transform }, None).is_err());
assert!(
doc.add_feature(
crate::FeatureKind::BodyTransfer { bodies: bodies.into_iter().map(crate::BodyName).collect(), from, to: 0, transform },
None
)
.is_err()
);
assert_eq!(doc.to_json(), before);
}
}
+3 -3
View File
@@ -114,7 +114,7 @@ fn remap_plane(
}
match p {
PlaneRef::Offset { base, .. } | PlaneRef::AtAngle { base, .. } => remap_plane(base, ids, names, bodies, depth + 1)?,
PlaneRef::Construction { name } => mapped_name(name, names),
PlaneRef::Construction { name } => mapped_name(&mut name.0, names),
PlaneRef::Face { name, anchor, .. } => {
if let Some(name) = name {
*name = remap_topology(name, ids, bodies);
@@ -221,10 +221,10 @@ impl crate::Feature {
*n = remap_topology(n, ids, bodies);
}
for (_, e, _) in self.kind.inputs_mut() {
*e = clone_expression(e, parameters);
*e = clone_expression(e, parameters).into();
}
for e in self.kind.placement_exprs_mut() {
*e = clone_expression(e, parameters);
*e = clone_expression(e, parameters).into();
}
self.kind.remap_bodies(bodies);
match &mut self.kind {
+6 -6
View File
@@ -17,7 +17,7 @@ fn shell_document() -> (Document, u64, u64) {
let shell = doc
.add_feature(
FeatureKind::Shell {
faces: vec![Vec3::new(5., 6., 10.)],
faces: vec![crate::FacePick(Vec3::new(5., 6., 10.))],
thickness: "wall".into(),
body: Some("Case".into()),
direction: "inside".into(),
@@ -119,7 +119,7 @@ fn shell_planar_anchor_rebuild_cold_geometry_and_missing_source_errors() {
.add_feature(
FeatureKind::Extrude {
thin: None,
sketch: sk,
sketch: crate::SketchId(sk),
profiles: crate::ProfileSel::All,
extent: serde_json::from_value(json!({"distance":"2"})).unwrap(),
operation: crate::Operation::NewBody,
@@ -282,7 +282,7 @@ fn saved_former_strip_projection_reports_lost_without_expanding_healthy_material
.add_feature(
FeatureKind::Extrude {
thin: None,
sketch: sk,
sketch: crate::SketchId(sk),
profiles: crate::ProfileSel::All,
extent: serde_json::from_value(json!({"distance":"1"})).unwrap(),
operation: crate::Operation::NewBody,
@@ -344,7 +344,7 @@ fn saved_curved_strip_projection_is_lost_after_exact_wall_coalescence() {
let shell = doc
.add_feature(
FeatureKind::Shell {
faces: vec![Vec3::new(0., 0., 30.)],
faces: vec![crate::FacePick(Vec3::new(0., 0., 30.))],
thickness: "wall".into(),
body: Some("Case".into()),
direction: "inside".into(),
@@ -432,7 +432,7 @@ fn saved_curved_strip_projection_is_lost_after_exact_wall_coalescence() {
.add_feature(
FeatureKind::Extrude {
thin: None,
sketch: sk,
sketch: crate::SketchId(sk),
profiles: crate::ProfileSel::All,
extent: serde_json::from_value(json!({"distance":"1"})).unwrap(),
operation: crate::Operation::NewBody,
@@ -557,7 +557,7 @@ fn saved_stepped_tab_cut_piece_projection_is_lost_without_expanding_material() {
.add_feature(
FeatureKind::Extrude {
thin: None,
sketch: sk,
sketch: crate::SketchId(sk),
profiles: crate::ProfileSel::All,
extent: serde_json::from_value(json!({"distance":"1"})).unwrap(),
operation: crate::Operation::NewBody,
+1 -1
View File
@@ -82,7 +82,7 @@ pub enum PointDef {
/// Where an edge (line) crosses a plane.
EdgeAndPlane { edge: GeoRef, plane: GeoRef },
/// At a fraction `t` (0…1) of the way along an edge or sketch curve.
AlongPath { path: GeoRef, t: String },
AlongPath { path: GeoRef, t: crate::Expr },
}
/// Resolved construction geometry, kept in the model state by feature.
+270 -406
View File
File diff suppressed because it is too large. Load diff
+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,
+52 -51
View File
@@ -10,7 +10,7 @@ use solidcraft_geom::{Loop2, Seg2};
pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st: &mut ModelState) -> Result<()> {
match &f.kind {
FeatureKind::Emboss { sketch, profiles, depth, deboss, targets, face } => {
let ss = st.sketch(*sketch).ok_or_else(|| DocError::Unknown(format!("sketch {sketch}")))?;
let ss = st.sketch(sketch.0).ok_or_else(|| DocError::Unknown(format!("sketch {sketch}")))?;
let regions = select_profiles(ss, profiles)?;
let d = val(vals, depth, Kind::Length)?;
if !(d > 0.0) {
@@ -35,7 +35,7 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
crate::apply_vector(&sketch_transform, ss.plane.y),
)
.ok_or_else(|| DocError::Invalid("invalid cylinder sketch frame".into()))?;
let mut at = crate::apply_point(&crate::mat_mul(&inverse, &author_frame), face.at);
let mut at = crate::apply_point(&crate::mat_mul(&inverse, &author_frame), face.at.0);
let name = f
.face_names
.first()
@@ -67,10 +67,10 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
if st.bodies.is_empty() {
return Err(DocError::Invalid("there is no body to emboss".into()));
}
apply_op(st, f, tools, if *deboss { Operation::Cut } else { Operation::Join }, targets)
apply_op(st, f, tools, if *deboss { Operation::Cut } else { Operation::Join }, &crate::plain(targets))
}
FeatureKind::Rib { sketch, curves, thickness, depth, flip, web } => {
let ss = st.sketch(*sketch).ok_or_else(|| DocError::Unknown(format!("sketch {sketch}")))?.clone();
let ss = st.sketch(sketch.0).ok_or_else(|| DocError::Unknown(format!("sketch {sketch}")))?.clone();
let t = val(vals, thickness, Kind::Length)?;
if !(t > 0.0) {
return Err(DocError::Invalid("the rib thickness must be positive".into()));
@@ -95,17 +95,18 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
FeatureKind::ReplaceFace { faces, target, body } => {
let tp = doc.resolve_plane_in(vals, target, 0, Some(st))?;
let tn = tp.normal();
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 b = mb.body.clone();
for p in faces {
let n = planar_normal_at(&b, *p).ok_or_else(|| DocError::Invalid("not supported yet: replacing a face that is not planar".into()))?;
let n =
planar_normal_at(&b, p.0).ok_or_else(|| DocError::Invalid("not supported yet: replacing a face that is not planar".into()))?;
if n.dot(tn).abs() < 1.0 - 1e-9 {
return Err(DocError::Invalid("not supported yet: a target that is not parallel to the face".into()));
}
let d = (tp.origin - *p).dot(n);
let d = (tp.origin - p.0).dot(n);
if d.abs() > 1e-9 {
b = kernel::offset_faces(&b, &[*p], d)?;
b = kernel::offset_faces(&b, &[p.0], d)?;
}
}
if let Some(slot) = st.bodies.get_mut(i) {
@@ -115,17 +116,17 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
}
FeatureKind::Align { bodies, from, to, from_normal, to_normal, flip } => {
let mut m = translation(Vec3::ZERO);
if let (Some(a), Some(b)) = (from_normal.and_then(Vec3::normalized), to_normal.and_then(Vec3::normalized)) {
if let (Some(a), Some(b)) = (from_normal.and_then(|n| n.normalized()), to_normal.and_then(|n| n.normalized())) {
// Turn `a` onto −b (faces meet) or b (flipped), about the `from` point.
let want = if *flip { b } else { b * -1.0 };
let axis = a.cross(want);
let ang = axis.len().atan2(a.dot(want));
let axis = axis.normalized().unwrap_or_else(|| a.any_perp());
if ang.abs() > 1e-12 {
m = rotation(*from, axis, ang);
m = rotation(from.0, axis, ang);
}
}
let d = *to - *from;
let d = to.0 - from.0;
m[3][0] += d.x;
m[3][1] += d.y;
m[3][2] += d.z;
@@ -150,7 +151,7 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
return Err(DocError::Unknown(format!("body `{n}`")));
}
}
st.bodies.retain(|b| !bodies.contains(&b.name));
st.bodies.retain(|b| !bodies.iter().any(|t| *t == b.name));
Ok(())
}
_ => Err(DocError::Invalid(format!("{} is not evaluated here", f.name))),
@@ -331,7 +332,7 @@ fn rebuild_sheet(st: &mut ModelState, idx: usize) -> Result<()> {
}
/// The sheet a feature works on: by body name, else the one with an edge nearest the pick.
fn sheet_for(st: &ModelState, body: &Option<String>, pick: Option<Vec3>) -> Result<usize> {
fn sheet_for(st: &ModelState, body: &Option<crate::BodyName>, pick: Option<Vec3>) -> Result<usize> {
if let Some(n) = body {
return st.sheets.iter().position(|s| &s.body == n).ok_or_else(|| DocError::Invalid(format!("`{n}` is not a sheet metal body")));
}
@@ -391,7 +392,7 @@ pub(super) fn sheet_eval(
use crate::sheet;
match &f.kind {
FeatureKind::SheetBase { sketch, profiles, rule, flip } => {
let ss = st.sketch(*sketch).ok_or_else(|| DocError::Unknown(format!("sketch {sketch}")))?.clone();
let ss = st.sketch(sketch.0).ok_or_else(|| DocError::Unknown(format!("sketch {sketch}")))?.clone();
let regions = solidcraft_sketch::merge_regions(&select_profiles(&ss, profiles)?);
let r = doc.sheet_rule(rule.as_deref());
let rv = doc.rule_values(vals, &r)?;
@@ -425,7 +426,7 @@ pub(super) fn sheet_eval(
Ok(())
}
FeatureKind::SheetContour { sketch, curves, distance, rule, flip, reverse } => {
let ss = st.sketch(*sketch).ok_or_else(|| DocError::Unknown(format!("sketch {sketch}")))?.clone();
let ss = st.sketch(sketch.0).ok_or_else(|| DocError::Unknown(format!("sketch {sketch}")))?.clone();
for id in curves {
let ci = ss.sketch.curve_index(id).ok_or_else(|| DocError::Unknown(format!("curve `{id}`")))?;
if !ss.sketch.segs(ci).iter().all(|s| matches!(s, Seg2::Line { .. })) {
@@ -450,7 +451,7 @@ pub(super) fn sheet_eval(
Ok(())
}
FeatureKind::SheetFlange { edges, height, angle, radius, position, flip, body, corner_relief_shape, corner_relief } => {
let i = sheet_for(st, body, edges.first().map(|e| e.point))?;
let i = sheet_for(st, body, edges.first().map(|e| e.point.0))?;
let (h, a) = (val(vals, height, Kind::Length)?, val(vals, angle, Kind::Angle)?);
// Each edge's own height and angle, else the feature's.
let mut own = Vec::with_capacity(edges.len());
@@ -490,7 +491,7 @@ pub(super) fn sheet_eval(
if sh.flat {
return Err(DocError::Invalid("refold the sheet before adding flanges".into()));
}
let points: Vec<Vec3> = edges.iter().map(|e| e.point).collect();
let points: Vec<Vec3> = edges.iter().map(|e| e.point.0).collect();
let each: Vec<sheet::EdgeFlange> = sheet_picks(sh, f, &points, warning)
.into_iter()
.zip(own)
@@ -535,7 +536,7 @@ pub(super) fn sheet_eval(
Ok(())
}
FeatureKind::SheetHem { edges, length, gap, flip, body } => {
let i = sheet_for(st, body, edges.first().copied())?;
let i = sheet_for(st, body, edges.first().copied().map(Vec3::from))?;
let l = val(vals, length, Kind::Length)?;
let g = match gap {
Some(e) => val(vals, e, Kind::Length)?,
@@ -545,7 +546,7 @@ pub(super) fn sheet_eval(
}
};
let sh = st.sheets.get_mut(i).ok_or_else(|| DocError::Invalid("sheet".into()))?;
let picks = sheet_picks(sh, f, edges, warning);
let picks = sheet_picks(sh, f, &crate::plain(edges), warning);
let n0 = sh.flanges.len();
for e in &picks {
sh.add_hem(*e, l, g, *flip)?;
@@ -557,14 +558,14 @@ pub(super) fn sheet_eval(
// The sketch line, where it is now.
let (a, b) = match (sketch, curve) {
(Some(sk), Some(c)) => {
let ss = st.sketch(*sk).ok_or_else(|| DocError::Unknown(format!("sketch {sk}")))?;
let ss = st.sketch(sk.0).ok_or_else(|| DocError::Unknown(format!("sketch {sk}")))?;
let ci = ss.sketch.curve_index(c).ok_or_else(|| DocError::Unknown(format!("curve `{c}`")))?;
match ss.sketch.segs(ci).as_slice() {
[Seg2::Line { a, b }] => (ss.plane.to_world(*a), ss.plane.to_world(*b)),
_ => return Err(DocError::Invalid("the fold line must be a sketch line".into())),
}
}
_ => (*a, *b),
_ => (a.0, b.0),
};
let i = sheet_for(st, body, Some((a + b) * 0.5))?;
let ang = val(vals, angle, Kind::Angle)?;
@@ -578,7 +579,7 @@ pub(super) fn sheet_eval(
// A fixed point that touches no material at all (e.g. far above the sheet) still picks
// the side it lies on, so a stored fold keeps recomputing; it says so (#371).
if let Some(p) = fixed
&& !on_sheet_material(st, i, *p)
&& !on_sheet_material(st, i, p.0)
{
*warning = Some(format!(
"{} at ({:.3}, {:.3}, {:.3}); the side of the fold line it lies on stays",
@@ -593,7 +594,7 @@ pub(super) fn sheet_eval(
return Err(DocError::Invalid("refold the sheet before folding it".into()));
}
let n0 = sh.flanges.len();
sh.add_fold(a, b, ang, rr, position, *flip, *fixed, warning)?;
sh.add_fold(a, b, ang, rr, position, *flip, fixed.map(Vec3::from), warning)?;
tag_flanges(sh, n0, f.id);
rebuild_sheet(st, i)
}
@@ -618,7 +619,7 @@ pub(super) fn sheet_eval(
let mut picks = Vec::with_capacity(bends.len());
for (i, p) in bends.iter().enumerate() {
let name = f.edge_names.get(i).filter(|n| !n.is_empty());
picks.push(match name.map(|n| (n, crate::naming::locate_edge(&mb, n, *p))) {
picks.push(match name.map(|n| (n, crate::naming::locate_edge(&mb, n, p.0))) {
Some((n, Ok(hit))) => {
if let Some(note) = hit.note(n) {
append_warning(warning, note);
@@ -627,12 +628,12 @@ pub(super) fn sheet_eval(
}
Some((n, Err(_))) => {
append_warning(warning, format!("edge `{n}` no longer exists; using the edge at the picked point"));
*p
p.0
}
None => *p,
None => p.0,
});
}
let (mut sh, notes) = sheet::convert_body(&mb, &r.name, &rv, *face, &picks)?;
let (mut sh, notes) = sheet::convert_body(&mb, &r.name, &rv, face.0, &picks)?;
for note in notes {
append_warning(warning, note);
}
@@ -641,10 +642,10 @@ pub(super) fn sheet_eval(
}
FeatureKind::SheetConvert { body, face, rule, .. } => {
let mb = st.body(body).cloned().ok_or_else(|| DocError::Unknown(format!("body `{body}`")))?;
let n = planar_normal_at(&mb.body, *face).ok_or_else(|| DocError::Invalid("pick a planar face of the plate".into()))?;
let n = planar_normal_at(&mb.body, face.0).ok_or_else(|| DocError::Invalid("pick a planar face of the plate".into()))?;
// Thickness: the body's extent along the face normal.
let extent = |mesh: &solidcraft_geom::Mesh| {
mesh.positions.iter().map(|p| (*p - *face).dot(n)).fold((f64::MAX, f64::MIN), |(lo, hi), d| (lo.min(d), hi.max(d)))
mesh.positions.iter().map(|p| (*p - face.0).dot(n)).fold((f64::MAX, f64::MIN), |(lo, hi), d| (lo.min(d), hi.max(d)))
};
// The tolerance (the mesh's chords, and how far off the face's plane a point of it may
// be) is at most a tenth of the thickness, not only a thousandth of the size. On a long
@@ -662,13 +663,13 @@ pub(super) fn sheet_eval(
// The face outline: boundary edges of the triangles on that face's plane, where it runs
// along a round wall an arc of the wall's circle (not chords: the plate check below
// compares its area with the exact volume).
let plane = Plane::from_normal(*face + n * lo, n * -1.0).ok_or_else(|| DocError::Invalid("face".into()))?;
let plane = Plane::from_normal(face.0 + n * lo, n * -1.0).ok_or_else(|| DocError::Invalid("face".into()))?;
// Recesses in one face (an engraved label) become pockets; the outline is the other face's.
let recesses = plate_recesses(&mb.body, &mesh, &plane, *face, n, (lo, hi), tol)?;
let recesses = plate_recesses(&mb.body, &mesh, &plane, face.0, n, (lo, hi), tol)?;
let mut walls = std::collections::HashMap::new();
let mut wall = |f: u32| *walls.entry(f).or_insert_with(|| sheet::round_wall(&mb.body, &plane, n, f));
let mut loops: Vec<Loop2> =
face_outline(&mesh, &plane, *face, n, recesses.level, tol).iter().map(|l| sheet::outline_loop(l, &mut wall, tol * 1e-2)).collect();
face_outline(&mesh, &plane, face.0, n, recesses.level, tol).iter().map(|l| sheet::outline_loop(l, &mut wall, tol * 1e-2)).collect();
loops.sort_by(|a, b| b.signed_area().abs().total_cmp(&a.signed_area().abs()));
let mut it = loops.into_iter();
let outer = it.next().ok_or_else(|| DocError::Invalid("no outline on that face".into()))?.ccw();
@@ -784,7 +785,7 @@ pub(super) fn sheet_cut(
if extent.taper.is_some() || !any_eligible_sheet(f, st, targets) {
return Ok(Vec::new());
}
let Some(ss) = st.sketch(*sketch).cloned() else { return Ok(Vec::new()) };
let Some(ss) = st.sketch(sketch.0).cloned() else { return Ok(Vec::new()) };
let regions = solidcraft_sketch::merge_regions(&select_profiles(&ss, profiles)?);
// The outlines of the sketch's text: the sheet remembers where its letters went.
let text: Vec<Seg2> = ss
@@ -1479,7 +1480,7 @@ fn plate_recesses(body: &Body, mesh: &Mesh, plane: &Plane, at: Vec3, n: Vec3, (l
}
/// The body a placed plastic feature joins: by name, else the one under the point.
fn body_under(st: &ModelState, body: &Option<String>, p: Vec3) -> Result<String> {
fn body_under(st: &ModelState, body: &Option<crate::BodyName>, p: Vec3) -> Result<String> {
let i = body_at(st, body, &[p])?;
let b = st.bodies.get(i).ok_or_else(|| DocError::Invalid("there is no body".into()))?;
let tolerance = (b.body.size() * 1e-5).max(1e-4);
@@ -1541,7 +1542,7 @@ pub(super) fn plastic_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &F
rib_offset,
body,
} => {
let target = body_under(st, body, *position)?;
let target = body_under(st, body, position.0)?;
let (r0, h) = (len(diameter)? / 2.0, len(height)?);
if !(r0 > 0.0 && h > 0.0) {
return Err(DocError::Invalid("the boss diameter and height must be positive".into()));
@@ -1559,9 +1560,9 @@ pub(super) fn plastic_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &F
return Err(DocError::Invalid("the draft closes the boss before its top".into()));
}
let delta = (h * 0.05).clamp(1e-3, 0.5);
let (x, _, z) = local_frame(*direction, None)?;
let (x, _, z) = local_frame(direction.0, None)?;
// The post: a (tapered) cylinder from a little inside the face.
let base = Plane::new(*position - z * delta, x, z.cross(x)).ok_or_else(|| DocError::Invalid("boss frame".into()))?;
let base = Plane::new(position.0 - z * delta, x, z.cross(x)).ok_or_else(|| DocError::Invalid("boss frame".into()))?;
let r_in = r0 + delta * dr.tan();
let disc = Region2 { outer: Loop2::circle(Vec2::ZERO, r_in), holes: Vec::new() };
let post = if dr.abs() > 1e-12 {
@@ -1594,7 +1595,7 @@ pub(super) fn plastic_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &F
for k in 0..n as usize {
let a = std::f64::consts::TAU * k as f64 / n;
let (u, v) = (x * a.cos() + z.cross(x) * a.sin(), z.cross(x) * a.cos() - x * a.sin());
let rp = Plane::new(*position, u, v).ok_or_else(|| DocError::Invalid("rib frame".into()))?;
let rp = Plane::new(position.0, u, v).ok_or_else(|| DocError::Invalid("rib frame".into()))?;
let rect = Region2 {
outer: Loop2::polygon(&[
Vec2::new(r0 * 0.5, -t / 2.0),
@@ -1613,7 +1614,7 @@ pub(super) fn plastic_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &F
let i = st.bodies.iter().position(|b| b.name == target).ok_or_else(|| DocError::Invalid("boss body".into()))?;
let Some(mb) = st.bodies.get(i).cloned() else { return Err(DocError::Invalid("boss body".into())) };
let mut warn = None;
let edges = resolve_edges(&mb.body, &[*position + x * r0], &[], &mut warn)?;
let edges = resolve_edges(&mb.body, &[position.0 + x * r0], &[], &mut warn)?;
let nb = mb.traced().then(kernel::fillet(&mb.body, &edges, rf)?);
if let Some(slot) = st.bodies.get_mut(i) {
*slot = ModelBody::traced_new(mb.name, nb, mb.feature);
@@ -1628,7 +1629,7 @@ pub(super) fn plastic_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &F
if !(rh > 0.0 && rh < r_top && depth > 0.0) {
return Err(DocError::Invalid("the boss hole must fit inside the boss".into()));
}
let hp = Plane::new(*position, x, z.cross(x)).ok_or_else(|| DocError::Invalid("hole frame".into()))?;
let hp = Plane::new(position.0, x, z.cross(x)).ok_or_else(|| DocError::Invalid("hole frame".into()))?;
let tool = kernel::extrude(&hp, &[Region2 { outer: Loop2::circle(Vec2::ZERO, rh), holes: Vec::new() }], h - depth, h + delta)?;
let tool = tool.into_iter().map(|b| crate::lineage::part_tool(b, "hole")).collect();
apply_traced(st, f, tool, Operation::Cut, std::slice::from_ref(&target))?;
@@ -1636,9 +1637,9 @@ pub(super) fn plastic_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &F
Ok(())
}
FeatureKind::Lip { face, width, height, groove, gap, outside, body } => {
let target = body_under(st, body, *face)?;
let target = body_under(st, body, face.0)?;
let mb = st.body(&target).cloned().ok_or_else(|| DocError::Invalid("body".into()))?;
let n = planar_normal_at(&mb.body, *face).ok_or_else(|| DocError::Invalid("pick the planar rim face".into()))?;
let n = planar_normal_at(&mb.body, face.0).ok_or_else(|| DocError::Invalid("pick the planar rim face".into()))?;
let (w, h) = (len(width)?, len(height)?);
let g = match gap {
Some(e) => len(e)?,
@@ -1648,10 +1649,10 @@ pub(super) fn plastic_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &F
if !(w > 0.0 && h > 0.0 && g >= 0.0) {
return Err(DocError::Invalid("the lip width and height must be positive".into()));
}
let plane = Plane::from_normal(*face, n).ok_or_else(|| DocError::Invalid("rim plane".into()))?;
let plane = Plane::from_normal(face.0, n).ok_or_else(|| DocError::Invalid("rim plane".into()))?;
let tol = (mb.body.size() * 1e-3).max(1e-3);
let mesh = mb.body.tessellate(tol)?;
let mut loops = face_loops(&mesh, &plane, *face, n, 0.0, tol);
let mut loops = face_loops(&mesh, &plane, face.0, n, 0.0, tol);
loops.sort_by(|a, b| poly_area(b).abs().total_cmp(&poly_area(a).abs()));
let ccw = |l: &[Vec2]| if poly_area(l) < 0.0 { l.iter().rev().copied().collect::<Vec<_>>() } else { l.to_vec() };
// The inner edge is an opening (the cavity under a rim): not the foot of a boss or a
@@ -1685,7 +1686,7 @@ pub(super) fn plastic_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &F
// A lip: the band raised from the rim's own faces (its walls continue the walls
// below, which a join can't do: they would coincide).
if !*groove
&& let Ok(b) = kernel::raise_band(&mb.body, *face, w, h, *outside)
&& let Ok(b) = kernel::raise_band(&mb.body, face.0, w, h, *outside)
&& let Some(slot) = st.bodies.iter_mut().find(|x| x.name == target)
{
*slot = ModelBody::traced_new(target.clone(), Traced::step(b, &[&mb.traced()]), slot.feature);
@@ -1715,17 +1716,17 @@ pub(super) fn plastic_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &F
}
}
FeatureKind::SnapFit { position, direction, hook, length, thickness, width, catch_depth, catch_length, body } => {
let target = body_under(st, body, *position)?;
let target = body_under(st, body, position.0)?;
let (l, t, w, cd, cl) = (len(length)?, len(thickness)?, len(width)?, len(catch_depth)?, len(catch_length)?);
if !(l > 0.0 && t > 0.0 && w > 0.0 && cd > 0.0 && cl > 0.0 && cl < l) {
return Err(DocError::Invalid("the snap fit's sizes must be positive and the catch shorter than the arm".into()));
}
let (x, y, z) = local_frame(*direction, Some(*hook))?;
let (x, y, z) = local_frame(direction.0, Some(hook.0))?;
let delta = (l * 0.05).clamp(1e-3, 0.5);
// Side profile (x = toward the catch, y = up the arm), extruded across the width.
let pts =
[Vec2::new(-t, -delta), Vec2::new(0.0, -delta), Vec2::new(0.0, l - cl), Vec2::new(cd, l - cl), Vec2::new(0.0, l), Vec2::new(-t, l)];
let o = *position - y * (w / 2.0);
let o = position.0 - y * (w / 2.0);
let pl = Plane::new(o, x, z).ok_or_else(|| DocError::Invalid("snap fit frame".into()))?;
let mut lp = Loop2::polygon(&pts);
if lp.signed_area() < 0.0 {
@@ -1738,7 +1739,7 @@ pub(super) fn plastic_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &F
apply_traced(st, f, tool, Operation::Join, std::slice::from_ref(&target))
}
FeatureKind::Rest { position, direction, along, width, length, height, draft, thickness, body } => {
let target = body_under(st, body, *position)?;
let target = body_under(st, body, position.0)?;
let (w, h) = (len(width)?, len(height)?);
let l = length.as_deref().map(len).transpose()?;
let dr = match draft {
@@ -1761,7 +1762,7 @@ pub(super) fn plastic_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &F
return Err(DocError::Invalid("the draft closes the rest before its top".into()));
}
let delta = (h * 0.05).clamp(1e-3, 0.5);
let (x, y, z) = local_frame(*direction, *along)?;
let (x, y, z) = local_frame(direction.0, along.map(Vec3::from))?;
// Sized at the face: the tool starts a little inside, grown by the draft over that.
let e = delta * dr.tan();
let shape = |grow: f64| match l {
@@ -1778,7 +1779,7 @@ pub(super) fn plastic_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &F
None => Vec::new(),
};
let region = Region2 { outer: shape(e), holes };
let base = Plane::new(*position - z * delta, x, y).ok_or_else(|| DocError::Invalid("rest frame".into()))?;
let base = Plane::new(position.0 - z * delta, x, y).ok_or_else(|| DocError::Invalid("rest frame".into()))?;
let tool = if dr.abs() > 1e-12 {
vec![kernel::extrude_tapered(&base, &region, h + delta, 1.0, -dr)?]
} else {
+43 -31
View File
@@ -11,22 +11,22 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
FeatureKind::Patch { sketch, profiles, body, edges, crown, continuity, points } => {
let mut made = Vec::new();
if let Some(sk) = sketch {
let ss = st.sketch(*sk).ok_or_else(|| DocError::Unknown(format!("sketch {sk} (it must come earlier in the timeline)")))?;
let ss = st.sketch(sk.0).ok_or_else(|| DocError::Unknown(format!("sketch {sk} (it must come earlier in the timeline)")))?;
let regions = solidcraft_sketch::merge_regions(&select_profiles(ss, profiles)?);
for r in &regions {
made.push(kernel::patch_region(&ss.plane, r)?);
}
}
if !edges.is_empty() {
let i = body_at(st, body, edges)?;
let i = body_at(st, body, &crate::plain(edges))?;
let b = st.bodies.get(i).ok_or_else(|| DocError::Invalid("body".into()))?;
let spec = kernel::PatchSpec {
continuity: continuity.all,
edges: continuity.edges.iter().map(|e| (e.edge, e.g)).collect(),
points: points.clone(),
edges: continuity.edges.iter().map(|e| (e.edge.0, e.g)).collect(),
points: crate::plain(points),
crown: *crown,
};
made.push(kernel::patch_edges_with(&b.body, edges, &spec)?);
made.push(kernel::patch_edges_with(&b.body, &crate::plain(edges), &spec)?);
}
if made.is_empty() {
return Err(DocError::Invalid("a patch needs a sketch region or a loop of edges".into()));
@@ -65,7 +65,7 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
if tools.is_empty() {
return Err(DocError::Invalid("pick the surfaces to thicken".into()));
}
apply_op(st, f, tools, *operation, targets)
apply_op(st, f, tools, *operation, &crate::plain(targets))
}
FeatureKind::SurfaceTrim { body, plane, tool, keep } => {
let i = st.bodies.iter().position(|b| &b.name == body).ok_or_else(|| DocError::Unknown(format!("body `{body}`")))?;
@@ -73,11 +73,11 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
let trimmed = match tool {
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::trim(&mb.body, &kernel::SplitTool::Face { body: &tb.body, face }, *keep)?
kernel::trim(&mb.body, &kernel::SplitTool::Face { body: &tb.body, face }, keep.0)?
}
None => kernel::trim(&mb.body, &kernel::SplitTool::Plane(doc.resolve_plane_in(vals, plane, 0, Some(st))?), *keep)?,
None => kernel::trim(&mb.body, &kernel::SplitTool::Plane(doc.resolve_plane_in(vals, plane, 0, Some(st))?), keep.0)?,
};
if let Some(slot) = st.bodies.get_mut(i) {
*slot = ModelBody::new(mb.name, trimmed, mb.feature);
@@ -94,18 +94,20 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
};
let at = |limit| kernel::ExtendOptions { mode, limit, neighbours: *neighbours };
let out = match up_to {
None => kernel::extend_surface(&mb.body, edges, &at(kernel::ExtendLimit::Distance(val(vals, distance, Kind::Length)?)))?,
Some(crate::ExtendTo::Bounds) => kernel::extend_surface(&mb.body, edges, &at(kernel::ExtendLimit::Bounds))?,
None => {
kernel::extend_surface(&mb.body, &crate::plain(edges), &at(kernel::ExtendLimit::Distance(val(vals, distance, Kind::Length)?)))?
}
Some(crate::ExtendTo::Bounds) => kernel::extend_surface(&mb.body, &crate::plain(edges), &at(kernel::ExtendLimit::Bounds))?,
Some(crate::ExtendTo::Plane(plane)) => {
let tool = kernel::SplitTool::Plane(doc.resolve_plane_in(vals, plane, 0, Some(st))?);
kernel::extend_surface(&mb.body, edges, &at(kernel::ExtendLimit::UpTo(&tool)))?
kernel::extend_surface(&mb.body, &crate::plain(edges), &at(kernel::ExtendLimit::UpTo(&tool)))?
}
Some(crate::ExtendTo::Face(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 target point", t.body)))?;
let tool = kernel::SplitTool::Face { body: &tb.body, face };
kernel::extend_surface(&mb.body, edges, &at(kernel::ExtendLimit::UpTo(&tool)))?
kernel::extend_surface(&mb.body, &crate::plain(edges), &at(kernel::ExtendLimit::UpTo(&tool)))?
}
};
if let Some(slot) = st.bodies.get_mut(i) {
@@ -119,7 +121,17 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
if from.edges.is_empty() || to.edges.is_empty() {
return Err(DocError::Invalid("a blend needs a chain of edges at each end".into()));
}
let (made, gap) = kernel::blend_between(&a, &from.edges, &b, &to.edges, *continuity, *bulge, *align, BLEND_STATIONS, *stitch)?;
let (made, gap) = kernel::blend_between(
&a,
&crate::plain(&from.edges),
&b,
&crate::plain(&to.edges),
*continuity,
*bulge,
align.map(|[p, q]| [p.0, q.0]),
BLEND_STATIONS,
*stitch,
)?;
if gap > 1e-9 {
*warning = Some(format!("the blend's boundary lies up to {gap:.4} mm from the edges it was built on"));
}
@@ -127,7 +139,7 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
// A stitched blend takes the place of both bodies it joins; otherwise it is a new
// surface body of its own.
if *stitch {
let gone = [from.body.clone(), to.body.clone()];
let gone = [from.body.0.clone(), to.body.0.clone()];
st.bodies.retain(|x| !gone.contains(&x.name));
}
st.bodies.push(ModelBody::new(name, made, f.id));
@@ -136,7 +148,7 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
FeatureKind::SurfaceExtrude { sketch, profiles, curves, extent } => {
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,
)?;
if extent.through_all || extent.taper.is_some() || extent.to.is_some() || extent.from.is_some() {
@@ -153,7 +165,7 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
FeatureKind::SurfaceRevolve { sketch, profiles, curves, axis, angle, angle2, symmetric } => {
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 (o, d) = revolve_axis(&ss, axis)?;
@@ -174,7 +186,7 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
FeatureKind::Unstitch { body, faces } => {
let i = st.bodies.iter().position(|b| &b.name == body).ok_or_else(|| DocError::Unknown(format!("body `{body}`")))?;
let Some(mb) = st.bodies.get(i).cloned() else { return Err(DocError::Invalid("body".into())) };
let (pieces, rest) = kernel::unstitch(&mb.body, faces)?;
let (pieces, rest) = kernel::unstitch(&mb.body, &crate::plain(faces))?;
let source = mb.traced();
// The faces left keep the body's name; each face taken out is a new surface body.
match rest {
@@ -196,7 +208,7 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
FeatureKind::ReverseNormal { body, faces } => {
let i = st.bodies.iter().position(|b| &b.name == body).ok_or_else(|| DocError::Unknown(format!("body `{body}`")))?;
let Some(mb) = st.bodies.get(i).cloned() else { return Err(DocError::Invalid("body".into())) };
let turned = mb.traced().then(kernel::reverse_faces(&mb.body, faces)?);
let turned = mb.traced().then(kernel::reverse_faces(&mb.body, &crate::plain(faces))?);
if let Some(slot) = st.bodies.get_mut(i) {
*slot = ModelBody::traced_new(mb.name, turned, mb.feature);
}
@@ -204,9 +216,9 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
}
FeatureKind::SurfaceSweep { sketch, profiles, curves, path_sketch, path, orientation, twist, scale } => {
let ctl = crate::eval::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)")))?;
// As Sweep: a 3D path sketch is followed in the feature's frame, the curves with it.
let profile;
let ss = if !ps.sketch.wires.is_empty() {
@@ -235,7 +247,7 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
for sec in sections {
let ss = sketch_in_feature_frame(
doc,
st.sketch(sec.sketch)
st.sketch(sec.sketch.0)
.ok_or_else(|| DocError::Unknown(format!("sketch {} (it must come earlier in the timeline)", sec.sketch)))?,
f.component,
)?;
@@ -265,9 +277,9 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
Some(t) => val(vals, t, Kind::Length)?,
None => 0.01,
};
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 picks = if *chain { smooth_chain(&mb, faces) } else { faces.clone() };
let picks = if *chain { smooth_chain(&mb, &crate::plain(faces)) } else { crate::plain(faces) };
let mut picked: Vec<usize> = Vec::new();
for p in &picks {
let fi =
@@ -288,10 +300,10 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
RuledMode::Tangent => kernel::RuledMode::Tangent,
RuledMode::Angle => {
let a = angle.as_ref().ok_or_else(|| DocError::Invalid("a ruled surface at an angle needs `angle`".into()))?;
kernel::RuledMode::Angle { angle: val(vals, a, Kind::Angle)?, reference: *direction }
kernel::RuledMode::Angle { angle: val(vals, a, Kind::Angle)?, reference: direction.map(Vec3::from) }
}
RuledMode::Direction => kernel::RuledMode::Direction(
direction.ok_or_else(|| DocError::Invalid("a ruled surface along a direction needs `direction`".into()))?,
direction.map(Vec3::from).ok_or_else(|| DocError::Invalid("a ruled surface along a direction needs `direction`".into()))?,
),
};
let corners = match corners {
@@ -299,8 +311,8 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
RuledCorners::Round => kernel::RuledCorners::Round,
};
let mb = st.body(body).cloned().ok_or_else(|| DocError::Unknown(format!("body `{body}`")))?;
let r = kernel::Ruled { distance: d, mode, flip: *flip, corners, face: *face };
let made = Traced::step(kernel::ruled_surface(&mb.body, edges, &r)?, &[&mb.traced()]);
let r = kernel::Ruled { distance: d, mode, flip: *flip, corners, face: face.map(Vec3::from) };
let made = Traced::step(kernel::ruled_surface(&mb.body, &crate::plain(edges), &r)?, &[&mb.traced()]);
let name = new_name(st, f, 0);
st.bodies.push(ModelBody::traced_new(name, made, f.id));
Ok(())
@@ -308,11 +320,11 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
FeatureKind::SurfaceUntrim { face, holes, outer, in_place } => {
let i = st.bodies.iter().position(|b| b.name == face.body).ok_or_else(|| DocError::Unknown(format!("body `{}`", face.body)))?;
let Some(mb) = st.bodies.get(i).cloned() else { return Err(DocError::Invalid("body".into())) };
let fi = crate::appearance::face_index_at(&mb, face.point)
let fi = crate::appearance::face_index_at(&mb, face.point.0)
.ok_or_else(|| DocError::Invalid(format!("Untrim: no face of {} at {:?}", mb.name, face.point)))?;
let which = match holes {
None => kernel::UntrimHoles::All,
Some(points) => kernel::UntrimHoles::At(points),
Some(points) => kernel::UntrimHoles::At(&crate::plain(points)),
};
// A surface body's face is untrimmed where it is when asked; a solid's is copied out.
let replace = *in_place && mb.body.is_surface();
+88
View File
@@ -15,6 +15,94 @@ const MAX_LEN: usize = 4096;
const MAX_DEPTH: usize = 64;
const MAX_ARGS: usize = 64;
/// An expression a feature holds, as the user wrote it (`2 * width + 5 mm`): kept as text and
/// evaluated with the design's parameters. It is saved as a plain string, and reads as one
/// (`&expr` is a `&str`); it changes only as a whole (`*expr = new.into()`).
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
#[serde(transparent)]
pub struct Expr(String);
impl Expr {
pub fn new(s: impl Into<String>) -> Self {
Expr(s.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn into_string(self) -> String {
self.0
}
}
impl std::ops::Deref for Expr {
type Target = str;
fn deref(&self) -> &str {
&self.0
}
}
impl AsRef<str> for Expr {
fn as_ref(&self) -> &str {
&self.0
}
}
impl From<String> for Expr {
fn from(s: String) -> Self {
Expr(s)
}
}
impl From<&str> for Expr {
fn from(s: &str) -> Self {
Expr(s.to_string())
}
}
impl From<&String> for Expr {
fn from(s: &String) -> Self {
Expr(s.clone())
}
}
impl From<Expr> for String {
fn from(e: Expr) -> Self {
e.0
}
}
impl std::fmt::Display for Expr {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl PartialEq<str> for Expr {
fn eq(&self, other: &str) -> bool {
self.0 == other
}
}
impl PartialEq<&str> for Expr {
fn eq(&self, other: &&str) -> bool {
self.0 == *other
}
}
impl PartialEq<Expr> for String {
fn eq(&self, other: &Expr) -> bool {
*self == other.0
}
}
impl PartialEq<String> for Expr {
fn eq(&self, other: &String) -> bool {
&self.0 == other
}
}
/// What an expression must evaluate to.
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
+8 -4
View File
@@ -69,8 +69,12 @@ impl FaceAnchor {
}
let inverse = crate::mat_inverse(&self.author_frame(doc, author)?)
.ok_or_else(|| DocError::Invalid("attached sketch author frame is invalid".into()))?;
let source =
EmbossFace { body: self.target.body.clone(), at: self.target.point, path: self.path.clone(), author_path: self.author_path.clone() };
let source = EmbossFace {
body: self.target.body.clone().into(),
at: crate::FacePick(self.target.point),
path: self.path.clone(),
author_path: self.author_path.clone(),
};
Ok(crate::mat_mul(&inverse, &source.body_frame(doc, body)?))
}
}
@@ -447,7 +451,7 @@ mod tests {
.add_feature(
FeatureKind::Extrude {
thin: None,
sketch: source,
sketch: crate::SketchId(source),
profiles: crate::ProfileSel::Indices { indices: vec![0, 1] },
extent,
operation: crate::Operation::NewBody,
@@ -558,7 +562,7 @@ mod tests {
.add_feature(
FeatureKind::Extrude {
thin: None,
sketch: sk,
sketch: crate::SketchId(sk),
profiles: crate::ProfileSel::All,
extent,
operation: crate::Operation::NewBody,
+12 -13
View File
@@ -5,15 +5,14 @@
use std::collections::BTreeMap;
use crate::FeatureKind;
use crate::walk::Walk;
use crate::{BodyName, FeatureKind, FeatureName, SketchId};
/// A reference a feature holds: `S` is `&u64` or `&mut u64` (a sketch id), `N` is `&String` or
/// `&mut String` (a feature or body name).
pub(crate) enum Ref<S, N> {
/// A reference a feature holds: `S` is `&SketchId` or `&mut SketchId`, and so on.
pub(crate) enum Ref<S, F, B> {
Sketch(S),
Feature(N),
Body(N),
Feature(F),
Body(B),
}
impl FeatureKind {
@@ -23,7 +22,7 @@ impl FeatureKind {
let mut v = Vec::new();
self.walk_refs(&mut |r| {
if let Ref::Sketch(id) = r {
v.push(*id);
v.push(id.0);
}
});
v
@@ -34,13 +33,13 @@ impl FeatureKind {
pub fn remap_refs(&mut self, sketches: &BTreeMap<u64, u64>, names: &BTreeMap<String, String>) {
self.walk_refs_mut(&mut |r| match r {
Ref::Sketch(id) => {
if let Some(n) = sketches.get(id) {
*id = *n;
if let Some(n) = sketches.get(&id.0) {
*id = SketchId(*n);
}
}
Ref::Feature(name) => {
if let Some(n) = names.get(name) {
*name = n.clone();
if let Some(n) = names.get(name.as_ref()) {
*name = FeatureName(n.clone());
}
}
Ref::Body(_) => {}
@@ -51,9 +50,9 @@ impl FeatureKind {
pub fn remap_bodies(&mut self, bodies: &BTreeMap<String, String>) {
self.walk_refs_mut(&mut |r| {
if let Ref::Body(name) = r
&& let Some(n) = bodies.get(name)
&& let Some(n) = bodies.get(name.as_ref())
{
*name = n.clone();
*name = BodyName(n.clone());
}
});
}
+6 -6
View File
@@ -73,9 +73,9 @@ fn ref_record() -> String {
out
}
/// A variant with a field that refers to a sketch (a field named `*sketch*` holding an id, sketch
/// 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
/// A variant with a field that refers to a sketch (a `SketchId`, sketch 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
@@ -107,7 +107,7 @@ fn geometry_record() -> String {
out += &format!(" profiles: {}\n", profiles.join(", "));
}
if let Some(p) = kind.point_expr() {
out += &format!(" placement: {}\n", p.join(", "));
out += &format!(" placement: {}\n", p.iter().map(|e| e.as_str()).collect::<Vec<_>>().join(", "));
}
}
out
@@ -151,8 +151,8 @@ fn input_sketches_lists_every_variant_with_a_sketch_field() {
variant = t.split(|c: char| !c.is_alphanumeric()).next().unwrap_or("").to_lowercase();
continue;
}
let Some((name, ty)) = t.strip_prefix("pub ").unwrap_or(t).split_once(':') else { continue };
let refers = (name.contains("sketch") && ty.contains("u64")) || ty.contains("SketchPoints") || ty.contains("LoftSection");
let Some((_, ty)) = t.strip_prefix("pub ").unwrap_or(t).split_once(':') else { continue };
let refers = ty.contains("SketchId") || ty.contains("SketchPoints") || ty.contains("LoftSection");
if refers && !t.starts_with("//") && !with_sketch.contains(&variant) {
with_sketch.push(variant.clone());
}
+4 -4
View File
@@ -1435,7 +1435,7 @@ mod tests {
.add_feature(
FeatureKind::Extrude {
thin: None,
sketch,
sketch: crate::SketchId(sketch),
profiles: ProfileSel::All,
extent: Extent {
convention: crate::ExtrudeConvention::LegacyPerSide,
@@ -1445,9 +1445,9 @@ mod tests {
start_offset: None,
through_all: false,
taper: Some("0 deg".into()),
to: Some(solidcraft_geom::Vec3::new(0.0, 0.0, 10.0)),
to: Some(crate::FacePick(solidcraft_geom::Vec3::new(0.0, 0.0, 10.0))),
to_offset: Some("offset + 1 mm".into()),
from: Some(solidcraft_geom::Vec3::new(0.0, 0.0, 5.0)),
from: Some(crate::FacePick(solidcraft_geom::Vec3::new(0.0, 0.0, 5.0))),
},
operation: Operation::NewBody,
targets: vec![],
@@ -2077,7 +2077,7 @@ mod tests {
"orientation": {"direction": [0.0, 1.0, 0.0]}, "twist": "90 deg", "scale": "0.5"});
let k: FeatureKind = serde_json::from_value(new.clone()).unwrap();
let FeatureKind::SurfaceSweep { orientation, .. } = &k else { panic!("{k:?}") };
assert_eq!(*orientation, SweepOrientation::Direction(solidcraft_geom::Vec3::Y));
assert_eq!(*orientation, SweepOrientation::Direction(crate::Dir(solidcraft_geom::Vec3::Y)));
assert_eq!(serde_json::to_value(&k).unwrap(), new);
let parallel: SweepOrientation = serde_json::from_value(serde_json::json!("parallel")).unwrap();
assert_eq!(parallel, SweepOrientation::Parallel);
+1 -1
View File
@@ -424,7 +424,7 @@ mod tests {
.add_feature(
FeatureKind::Extrude {
thin: None,
sketch: if missing_sketch { 999_999 } else { id },
sketch: crate::SketchId(if missing_sketch { 999_999 } else { id }),
profiles: ProfileSel::OffsetBoundaries { associations: vec!["deleted-o1".into()] },
extent: crate::Extent {
convention: crate::ExtrudeConvention::LegacyPerSide,
+5
View File
@@ -34,10 +34,12 @@ pub mod metadata;
pub use metadata::Metadata;
pub mod motion;
mod name_lookup;
mod names;
pub mod naming;
mod params;
pub mod physical_material;
mod piece_names;
mod points;
mod profile_refs;
pub mod progress;
mod walk;
@@ -60,7 +62,10 @@ pub use eval::{
revolve_extent, revolve_extent_with_convention, select_profiles, sketch_hole_direction, state_in_frame, state_in_frame_at, through_all_distance,
world_state, world_state_reusing,
};
pub use expr::Expr;
pub use names::{BodyName, FeatureName, SketchId};
pub use params::{ParamRow, ParamUser, cycles};
pub use points::{Dir, FacePick, Position, plain};
pub use solidcraft_kernel as kernel;
pub use solidcraft_sketch as sketch;
+146
View File
@@ -0,0 +1,146 @@
//! What a feature refers to: bodies and features by name, sketches by id. Each is saved as the
//! plain name or id it wraps, but has its own type, so a body name can't be passed where a
//! feature name or an expression is meant.
use std::ops::Deref;
use serde::{Deserialize, Serialize};
macro_rules! name_type {
($(#[$doc:meta])* $name:ident) => {
$(#[$doc])*
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(transparent)]
pub struct $name(pub String);
impl Deref for $name {
type Target = str;
fn deref(&self) -> &str {
&self.0
}
}
impl AsRef<str> for $name {
fn as_ref(&self) -> &str {
&self.0
}
}
impl std::borrow::Borrow<str> for $name {
fn borrow(&self) -> &str {
&self.0
}
}
impl From<String> for $name {
fn from(s: String) -> Self {
$name(s)
}
}
impl From<&str> for $name {
fn from(s: &str) -> Self {
$name(s.to_string())
}
}
impl From<&String> for $name {
fn from(s: &String) -> Self {
$name(s.clone())
}
}
impl From<$name> for String {
fn from(n: $name) -> String {
n.0
}
}
impl std::fmt::Display for $name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl PartialEq<str> for $name {
fn eq(&self, other: &str) -> bool {
self.0 == other
}
}
impl PartialEq<&str> for $name {
fn eq(&self, other: &&str) -> bool {
self.0 == *other
}
}
impl PartialEq<String> for $name {
fn eq(&self, other: &String) -> bool {
&self.0 == other
}
}
impl PartialEq<$name> for String {
fn eq(&self, other: &$name) -> bool {
*self == other.0
}
}
impl PartialEq<$name> for str {
fn eq(&self, other: &$name) -> bool {
self == other.0
}
}
};
}
name_type!(
/// A body, by name.
BodyName
);
name_type!(
/// A feature, by name.
FeatureName
);
/// A sketch, by its feature id.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(transparent)]
pub struct SketchId(pub u64);
impl Deref for SketchId {
type Target = u64;
fn deref(&self) -> &u64 {
&self.0
}
}
impl From<u64> for SketchId {
fn from(id: u64) -> Self {
SketchId(id)
}
}
impl From<SketchId> for u64 {
fn from(id: SketchId) -> u64 {
id.0
}
}
impl PartialEq<u64> for SketchId {
fn eq(&self, other: &u64) -> bool {
self.0 == *other
}
}
impl PartialEq<SketchId> for u64 {
fn eq(&self, other: &SketchId) -> bool {
*self == other.0
}
}
impl std::fmt::Display for SketchId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
+5 -5
View File
@@ -540,7 +540,7 @@ fn role_name(f: &Feature, before: &ModelState, ps: &[Vec3], ns: &[Vec3], fi: usi
let c = ps.iter().fold(Vec3::ZERO, |a, p| a + *p) * (1.0 / ps.len().max(1) as f64);
match &f.kind {
FeatureKind::Extrude { sketch, .. } => {
let Some(ss) = before.sketch(*sketch) else { return format!("F{fid}:face:{fi}") };
let Some(ss) = before.sketch(sketch.0) else { return format!("F{fid}:face:{fi}") };
let n = ss.plane.normal();
// A face across the extrusion is a cap; others sweep a sketch curve.
let flat = !ns.is_empty() && ns.iter().all(|m| m.cross(n).len() < 1e-6);
@@ -552,7 +552,7 @@ fn role_name(f: &Feature, before: &ModelState, ps: &[Vec3], ns: &[Vec3], fi: usi
nearest_curve(ss, Vec2::new(q.x, q.y)).map(|id| format!("F{fid}:side:{id}")).unwrap_or_else(|| format!("F{fid}:face:{fi}"))
}
FeatureKind::Revolve { sketch, axis, .. } => {
let Some(ss) = before.sketch(*sketch) else { return format!("F{fid}:face:{fi}") };
let Some(ss) = before.sketch(sketch.0) else { return format!("F{fid}:face:{fi}") };
let Ok((o2, d2)) = crate::eval::revolve_axis(ss, axis) else { return format!("F{fid}:face:{fi}") };
let (o, d) = (ss.plane.to_world(o2), (ss.plane.to_world(o2 + d2) - ss.plane.to_world(o2)).normalized().unwrap_or(Vec3::Z));
// A flat face holding the axis is an angle cap: on the sketch plane, or the far one.
@@ -582,7 +582,7 @@ fn role_name(f: &Feature, before: &ModelState, ps: &[Vec3], ns: &[Vec3], fi: usi
FeatureKind::Loft { sections, .. } => sections.first().map(|x| x.sketch),
_ => None,
};
let Some(ss) = sk.and_then(|k| before.sketch(k)) else { return format!("F{fid}:face:{fi}") };
let Some(ss) = sk.and_then(|k| before.sketch(k.0)) else { return format!("F{fid}:face:{fi}") };
let n = ss.plane.normal();
let h = |p: &Vec3| (*p - ss.plane.origin).dot(n);
let flat = !ns.is_empty() && ns.windows(2).all(|w| w[0].cross(w[1]).len() < 1e-6);
@@ -613,10 +613,10 @@ fn role_name(f: &Feature, before: &ModelState, ps: &[Vec3], ns: &[Vec3], fi: usi
format!("F{fid}:{role}")
}
FeatureKind::Cylinder { axis, .. } => {
let flat = !ns.is_empty() && ns.iter().all(|m| m.cross(*axis).len() < 1e-6);
let flat = !ns.is_empty() && ns.iter().all(|m| m.cross(axis.0).len() < 1e-6);
let role = if !flat {
"side"
} else if ns.first().is_some_and(|m| m.dot(*axis) > 0.0) {
} else if ns.first().is_some_and(|m| m.dot(axis.0) > 0.0) {
"top"
} else {
"bottom"
+8 -8
View File
@@ -8,7 +8,7 @@ use std::collections::{BTreeMap, BTreeSet};
use serde::{Deserialize, Serialize};
use crate::expr::{self, Kind, ParamDef, Value};
use crate::expr::{self, Expr, Kind, ParamDef, Value};
use crate::walk::Walk;
use crate::{DocError, Document, FeatureKind, MAX_PARAMS, Result};
@@ -23,7 +23,7 @@ pub(crate) enum ExprRole {
impl FeatureKind {
/// The feature's numeric inputs: (label, expression, kind), in a fixed order.
pub fn inputs_mut(&mut self) -> Vec<(&'static str, &mut String, Kind)> {
pub fn inputs_mut(&mut self) -> Vec<(&'static str, &mut Expr, Kind)> {
let mut v = Vec::new();
self.walk_exprs_mut(&mut |(label, e, kind, role)| {
if role == ExprRole::Input {
@@ -38,7 +38,7 @@ impl FeatureKind {
let mut v = Vec::new();
self.walk_exprs(&mut |(label, e, kind, role)| {
if role == ExprRole::Input {
v.push((label, e.clone(), kind));
v.push((label, e.to_string(), kind));
}
});
v
@@ -47,7 +47,7 @@ impl FeatureKind {
/// 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> {
pub fn placement_exprs_mut(&mut self) -> Vec<&mut Expr> {
let mut v = Vec::new();
self.walk_exprs_mut(&mut |(_, e, _, role)| {
if role == ExprRole::Placement {
@@ -260,7 +260,7 @@ impl Document {
if let Some(f) = next.features.get_mut(i)
&& let Some((_, e, _)) = f.kind.inputs_mut().into_iter().nth(k)
{
*e = expr_s.clone();
*e = expr_s.clone().into();
}
if let Some(c) = comment {
next.param_comments.insert(name.to_string(), c.to_string());
@@ -308,7 +308,7 @@ impl Document {
if *target != normalized {
changed.insert(name.clone());
}
*target = normalized;
*target = normalized.into();
}
let (_, errors) = next.param_values();
for (name, error) in errors {
@@ -673,14 +673,14 @@ impl Document {
for e in f.kind.placement_exprs_mut() {
let r = expr::rename_reference(e, old, new);
if r != *e {
*e = r;
*e = r.into();
n += 1;
}
}
for (_, e, _) in f.kind.inputs_mut() {
let r = expr::rename_reference(e, old, new);
if r != *e {
*e = r;
*e = r.into();
n += 1;
}
}
+61
View File
@@ -0,0 +1,61 @@
//! The points a feature holds, by what they are: a point that picks a face, any other
//! position, a direction. Each is a `Vec3` (and is saved as one), but a different type, so a
//! direction can't be moved like a position, and a pick can't be mistaken for either.
use std::ops::{Deref, DerefMut};
use serde::{Deserialize, Serialize};
use solidcraft_geom::Vec3;
macro_rules! point_type {
($(#[$doc:meta])* $name:ident) => {
$(#[$doc])*
#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct $name(pub Vec3);
impl Deref for $name {
type Target = Vec3;
fn deref(&self) -> &Vec3 {
&self.0
}
}
impl DerefMut for $name {
fn deref_mut(&mut self) -> &mut Vec3 {
&mut self.0
}
}
impl From<Vec3> for $name {
fn from(v: Vec3) -> Self {
$name(v)
}
}
impl From<$name> for Vec3 {
fn from(p: $name) -> Vec3 {
p.0
}
}
};
}
point_type!(
/// A point that picks a face: it moves with the feature, and names the face it picks.
FacePick
);
point_type!(
/// A position (an edge pick, an axis origin, a fold line): it moves with the feature.
Position
);
point_type!(
/// A direction: it turns with the feature, and never moves.
Dir
);
/// The plain values of a list of picks, positions, directions or names (for code that takes
/// `Vec3`s or `String`s).
pub fn plain<T, P: Clone + Into<T>>(ps: &[P]) -> Vec<T> {
ps.iter().map(|p| p.clone().into()).collect()
}
+2 -2
View File
@@ -309,7 +309,7 @@ mod tests {
.add_feature(
crate::FeatureKind::Extrude {
thin: None,
sketch: feature,
sketch: crate::SketchId(feature),
profiles: selection.clone(),
extent: crate::Extent {
convention: crate::ExtrudeConvention::LegacyPerSide,
@@ -436,7 +436,7 @@ mod tests {
.add_feature(
crate::FeatureKind::Extrude {
thin: None,
sketch: feature,
sketch: crate::SketchId(feature),
profiles: selection.clone(),
extent: crate::Extent {
convention: crate::ExtrudeConvention::LegacyPerSide,
+5 -2
View File
@@ -261,8 +261,11 @@ fn resolve_uncached(
// Its path is a curve of its own sketch: set with the sketch (`Sketch::text_source_geometry`).
LinkSource::Text { .. } => return Err(DocError::Invalid("a text on a path is set with its sketch".into())),
LinkSource::Plane { name } => {
let pr =
if Plane::named(name).is_some() { PlaneRef::Origin { name: name.clone() } } else { PlaneRef::Construction { name: name.clone() } };
let pr = if Plane::named(name).is_some() {
PlaneRef::Origin { name: name.clone() }
} else {
PlaneRef::Construction { name: name.clone().into() }
};
let other = doc.resolve_plane_in(vals, &pr, 0, Some(st)).map_err(|_| lost(&format!("plane {name}")))?;
plane_trace(plane, &other).into_iter().collect()
}
+6 -6
View File
@@ -30,7 +30,7 @@ fn plate_doc() -> (Document, u64) {
doc.add_feature(
FeatureKind::Extrude {
thin: None,
sketch: s,
sketch: crate::SketchId(s),
profiles: ProfileSel::All,
extent: Extent {
convention: crate::ExtrudeConvention::LegacyPerSide,
@@ -108,7 +108,7 @@ fn box_fillet_cut_timeline() {
let (mut doc, _) = plate_doc();
doc.add_feature(
FeatureKind::Fillet {
edges: vec![Vec3::new(0.0, 0.0, 10.0)],
edges: vec![Position(Vec3::new(0.0, 0.0, 10.0))],
radius: "3".into(),
body: None,
style: Default::default(),
@@ -123,7 +123,7 @@ fn box_fillet_cut_timeline() {
doc.add_feature(
FeatureKind::Extrude {
thin: None,
sketch: s2,
sketch: crate::SketchId(s2),
profiles: ProfileSel::Points { points: vec![Vec2::new(20.0, 15.0)] },
extent: Extent {
convention: crate::ExtrudeConvention::LegacyPerSide,
@@ -162,7 +162,7 @@ fn errors_are_reported_not_fatal() {
let (mut doc, s) = plate_doc();
doc.add_feature(
FeatureKind::Revolve {
sketch: s,
sketch: crate::SketchId(s),
profiles: ProfileSel::All,
axis: AxisRef::SketchAxis { axis: "y".into() },
angle: "90 deg".into(),
@@ -178,7 +178,7 @@ fn errors_are_reported_not_fatal() {
.unwrap();
doc.add_feature(
FeatureKind::Fillet {
edges: vec![Vec3::new(500.0, 0.0, 0.0)],
edges: vec![Position(Vec3::new(500.0, 0.0, 0.0))],
radius: "1".into(),
body: None,
style: Default::default(),
@@ -244,7 +244,7 @@ fn primitives_and_combine() {
doc.add_feature(
FeatureKind::Cylinder {
base: point_expr(Vec3::new(5.0, 5.0, 0.0)),
axis: Vec3::Z,
axis: Dir(Vec3::Z),
radius: "2".into(),
height: "20".into(),
operation: Operation::NewBody,
+63 -17
View File
@@ -13,13 +13,13 @@ pub(crate) use crate::expr::Kind;
pub(crate) use crate::feature_geometry::Geo;
pub(crate) use crate::feature_refs::Ref;
pub(crate) use crate::params::ExprRole;
use crate::{PlaneRef, PointExpr};
use crate::{BodyName, Dir, Expr, FacePick, FeatureName, PlaneRef, PointExpr, Position, SketchId};
/// An expression: its label, the expression, its kind and its role.
pub(crate) type ExprItem<'a> = (&'static str, &'a String, Kind, ExprRole);
pub(crate) type ExprItemMut<'a> = (&'static str, &'a mut String, Kind, ExprRole);
pub(crate) type RefItem<'a> = Ref<&'a u64, &'a String>;
pub(crate) type RefItemMut<'a> = Ref<&'a mut u64, &'a mut String>;
pub(crate) type ExprItem<'a> = (&'static str, &'a Expr, Kind, ExprRole);
pub(crate) type ExprItemMut<'a> = (&'static str, &'a mut Expr, Kind, ExprRole);
pub(crate) type RefItem<'a> = Ref<&'a SketchId, &'a FeatureName, &'a BodyName>;
pub(crate) type RefItemMut<'a> = Ref<&'a mut SketchId, &'a mut FeatureName, &'a mut BodyName>;
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>;
@@ -56,6 +56,52 @@ impl Walk for crate::document::RuledMode {}
impl Walk for crate::document::RuledCorners {}
impl Walk for crate::document::ExtrudeConvention {}
/// A reference walks itself: a sketch, a feature or a body.
macro_rules! reference {
($t:ty, $role:ident) => {
impl Walk for $t {
fn walk_refs<'a>(&'a self, f: &mut dyn FnMut(RefItem<'a>)) {
f(Ref::$role(self))
}
fn walk_refs_mut<'a>(&'a mut self, f: &mut dyn FnMut(RefItemMut<'a>)) {
f(Ref::$role(self))
}
}
};
}
reference!(SketchId, Sketch);
reference!(FeatureName, Feature);
reference!(BodyName, Body);
/// A point walks itself with its role: a face pick, a position or a direction. As a plain
/// `Vec3` it also serves a field whose attribute gives it another role (`pinned`, `pick_if`).
macro_rules! point {
($t:ty, $role:ident) => {
impl Walk for $t {
fn walk_geometry<'a>(&'a self, f: &mut dyn FnMut(GeoItem<'a>)) {
f(Geo::$role(&self.0))
}
fn walk_geometry_mut<'a>(&'a mut self, f: &mut dyn FnMut(GeoItemMut<'a>)) {
f(Geo::$role(&mut self.0))
}
}
impl Each<Vec3> for $t {
fn each<'a>(&'a self, f: &mut dyn FnMut(&'a Vec3)) {
f(&self.0)
}
fn each_mut<'a>(&'a mut self, f: &mut dyn FnMut(&'a mut Vec3)) {
f(&mut self.0)
}
}
};
}
point!(FacePick, Pick);
point!(Position, Pos);
point!(Dir, Dir);
/// Containers walk what they hold, in order.
macro_rules! walk_each {
($([$($g:tt)*] $t:ty),*) => {$(
@@ -182,36 +228,36 @@ impl<T> Each<T> for Option<Vec<T>> {
/// The expressions of an expression field, each with its index and their count (for its label).
pub(crate) trait ExprField {
fn each<'a>(&'a self, f: &mut dyn FnMut(usize, usize, &'a String));
fn each_mut<'a>(&'a mut self, f: &mut dyn FnMut(usize, usize, &'a mut String));
fn each<'a>(&'a self, f: &mut dyn FnMut(usize, usize, &'a Expr));
fn each_mut<'a>(&'a mut self, f: &mut dyn FnMut(usize, usize, &'a mut Expr));
}
impl ExprField for String {
fn each<'a>(&'a self, f: &mut dyn FnMut(usize, usize, &'a String)) {
impl ExprField for Expr {
fn each<'a>(&'a self, f: &mut dyn FnMut(usize, usize, &'a Expr)) {
f(0, 1, self)
}
fn each_mut<'a>(&'a mut self, f: &mut dyn FnMut(usize, usize, &'a mut String)) {
fn each_mut<'a>(&'a mut self, f: &mut dyn FnMut(usize, usize, &'a mut Expr)) {
f(0, 1, self)
}
}
/// A position along an edge with its radius (a variable fillet's mid radii).
impl ExprField for (f64, String) {
fn each<'a>(&'a self, f: &mut dyn FnMut(usize, usize, &'a String)) {
impl ExprField for (f64, Expr) {
fn each<'a>(&'a self, f: &mut dyn FnMut(usize, usize, &'a Expr)) {
f(0, 1, &self.1)
}
fn each_mut<'a>(&'a mut self, f: &mut dyn FnMut(usize, usize, &'a mut String)) {
fn each_mut<'a>(&'a mut self, f: &mut dyn FnMut(usize, usize, &'a mut Expr)) {
f(0, 1, &mut self.1)
}
}
impl<T: ExprField> ExprField for Option<T> {
fn each<'a>(&'a self, f: &mut dyn FnMut(usize, usize, &'a String)) {
fn each<'a>(&'a self, f: &mut dyn FnMut(usize, usize, &'a Expr)) {
if let Some(t) = self {
t.each(f)
}
}
fn each_mut<'a>(&'a mut self, f: &mut dyn FnMut(usize, usize, &'a mut String)) {
fn each_mut<'a>(&'a mut self, f: &mut dyn FnMut(usize, usize, &'a mut Expr)) {
if let Some(t) = self {
t.each_mut(f)
}
@@ -222,13 +268,13 @@ impl<T: ExprField> ExprField for Option<T> {
macro_rules! expr_list {
($([$($g:tt)*] $t:ty),*) => {$(
impl<$($g)*> ExprField for $t {
fn each<'a>(&'a self, f: &mut dyn FnMut(usize, usize, &'a String)) {
fn each<'a>(&'a self, f: &mut dyn FnMut(usize, usize, &'a Expr)) {
let n = self.len();
for (i, t) in self.iter().enumerate() {
t.each(&mut |_, _, e| f(i, n, e));
}
}
fn each_mut<'a>(&'a mut self, f: &mut dyn FnMut(usize, usize, &'a mut String)) {
fn each_mut<'a>(&'a mut self, f: &mut dyn FnMut(usize, usize, &'a mut Expr)) {
let n = self.len();
for (i, t) in self.iter_mut().enumerate() {
t.each_mut(&mut |_, _, e| f(i, n, e));