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

+1 -1
View File
@@ -837,7 +837,7 @@ mod tests {
solidcraft_doc::FeatureKind::Move { bodies, .. } => Some(bodies.clone()),
_ => None,
});
assert_eq!(moved, Some(vec!["Lid".to_string()]), "the move follows the rename");
assert_eq!(moved, Some(vec![solidcraft_doc::BodyName::from("Lid")]), "the move follows the rename");
assert!(s.model.results.iter().all(|r| r.error.is_none()));
}
+6 -5
View File
@@ -1,4 +1,5 @@
//! Independent copies of a component definition graph, assembled and evaluated atomically.
use solidcraft_doc::BodyName;
use std::collections::{BTreeMap, BTreeSet};
use crate::params::bad;
@@ -327,7 +328,7 @@ pub(super) fn paste(
let entry = feats.iter().find(|f| {
!f.suppressed
&& matches!(&f.kind, FeatureKind::BodyTransfer { bodies, from, to, .. }
if !selected.contains(from) && bodies.contains(&body.name)
if !selected.contains(from) && bodies.iter().any(|b| *b == body.name)
&& s.model.result(f.id).is_some_and(|result| !result.skipped && result.error.is_none()
&& result.state.body(&body.name).is_some_and(|entered| source.model_body_component(entered) == *to)))
});
@@ -426,7 +427,7 @@ pub(super) fn paste(
if let FeatureKind::Emboss { face: Some(face), .. } = &mut f.kind
&& !face.path.is_empty()
&& let FeatureKind::Emboss { face: Some(old_face), .. } = &old.kind
&& bodies.contains_key(&old_face.body)
&& bodies.contains_key(old_face.body.as_ref())
{
face.path = cloned_path(&source, &old_face.path, component, &clone_path, &occurrences)?;
}
@@ -483,7 +484,7 @@ pub(super) fn paste(
.collect();
if !moved_out.is_empty() {
let name = unique("Assigned-out bodies", &mut used);
let id = scratch.doc_mut().add_feature(FeatureKind::Remove { bodies: moved_out }, Some(&name))?;
let id = scratch.doc_mut().add_feature(FeatureKind::Remove { bodies: moved_out.into_iter().map(BodyName).collect() }, Some(&name))?;
if let Some(f) = scratch.doc_mut().feature_mut(id) {
f.component = root;
}
@@ -526,7 +527,7 @@ pub(super) fn paste(
for feature in &feats {
if let FeatureKind::Emboss { face: Some(face), .. } = &feature.kind
&& !face.path.is_empty()
&& !bodies.contains_key(&face.body)
&& !bodies.contains_key(face.body.as_ref())
{
notices.push(format!("{} retains external selected occurrence path {:?}", feature.name, face.path));
}
@@ -588,7 +589,7 @@ pub(super) fn paste(
}
if let FeatureKind::Pattern { features: references, .. } | FeatureKind::Mirror { features: references, .. } = &feature.kind {
for name in references {
if !names.contains_key(name) {
if !names.contains_key(name.as_ref()) {
notices.push(format!("{} retains external feature reference {name}", feature.name));
}
}
@@ -376,7 +376,7 @@ fn generated_copy_parameters_cannot_heal_an_unrelated_missing_reference() {
let feature = s.doc_mut().feature_mut(broken).unwrap();
feature.component = 0;
if let solidcraft_doc::FeatureKind::Box { width, .. } = &mut feature.kind {
*width = missing;
*width = missing.into();
}
s.refresh();
let original_error = s.model.result(broken).unwrap().error.clone();
+2 -1
View File
@@ -1,5 +1,6 @@
//! Atomic timeline ownership changes, preserving the selected world geometry.
use super::*;
use solidcraft_doc::BodyName;
use std::collections::{BTreeMap, BTreeSet};
type UniqueFrames = BTreeMap<u64, Option<(Vec<u64>, Mat)>>;
@@ -83,7 +84,7 @@ fn transfer(s: &mut Session, name: &str, to: u64, frames: &UniqueFrames, cmd: &s
if let Some(look) = inherited {
s.doc_mut().appearances.bodies.insert(name.to_string(), look);
}
let id = s.doc_mut().add_feature(FeatureKind::BodyTransfer { bodies: vec![name.to_string()], from, to, transform }, None)?;
let id = s.doc_mut().add_feature(FeatureKind::BodyTransfer { bodies: vec![BodyName::from(name)], from, to, transform }, None)?;
if let Some(feature) = s.doc_mut().feature_mut(id) {
feature.component = to;
}
+2 -2
View File
@@ -135,8 +135,8 @@ fn geo(s: &Session, v: Option<&Value>, cmd: &str, what: &str) -> Result<GeoRef>
Ok(g)
}
fn t_param(s: &Session, p: &Value, cmd: &str) -> Result<String> {
let t = if p.get("t").is_some() { req_expr(cmd, p, "t")? } else { "0.5".to_string() };
fn t_param(s: &Session, p: &Value, cmd: &str) -> Result<solidcraft_doc::Expr> {
let t = if p.get("t").is_some() { req_expr(cmd, p, "t")? } else { "0.5".into() };
check_expr(s, &t, solidcraft_doc::expr::Kind::Unitless, cmd, "t")?;
Ok(t)
}
+6 -6
View File
@@ -767,7 +767,7 @@ fn edit_feature(s: &mut Session, p: &Value) -> Result<Value> {
return Err(bad(cmd, unknown_field_message(&field, &before, f.kind.type_name())));
}
if let FeatureKind::BodyTransfer { bodies, from, to, transform } = &nf.kind {
solidcraft_doc::validate_body_transfer(&s.doc, bodies, *from, *to, transform)?;
solidcraft_doc::validate_body_transfer(&s.doc, &solidcraft_doc::plain(bodies), *from, *to, transform)?;
nf.component = *to;
}
if let (FeatureKind::Sketch { sketch: a, .. }, FeatureKind::Sketch { sketch: b, .. }) = (&nf.kind, &f.kind)
@@ -783,22 +783,22 @@ fn edit_feature(s: &mut Session, p: &Value) -> Result<Value> {
&& a != b
{
let before = s.model.state_before(id);
nf.edge_refs = solidcraft_doc::edge_refs(&before, body, a);
nf.edge_names = solidcraft_doc::edge_names_for(&before, body, a);
nf.edge_refs = solidcraft_doc::edge_refs(&before, body, &solidcraft_doc::plain(a));
nf.edge_names = solidcraft_doc::edge_names_for(&before, body, &solidcraft_doc::plain(a));
}
// New or reordered flange edges: their names now, so each edge's own angle and height stay
// with the edge they were given for.
if let (FeatureKind::SheetFlange { edges: a, .. }, FeatureKind::SheetFlange { edges: b, .. }) = (&nf.kind, &f.kind) {
let points = |e: &[solidcraft_doc::FlangeEdge]| e.iter().map(|x| x.point).collect::<Vec<_>>();
if points(a) != points(b) {
nf.edge_names = solidcraft_doc::sheet::sheet_edge_names(&s.model.state_before(id), &points(a));
nf.edge_names = solidcraft_doc::sheet::sheet_edge_names(&s.model.state_before(id), &solidcraft_doc::plain(&points(a)));
}
}
// New bend edges for a conversion: the names of the body's edges there now.
if let (FeatureKind::SheetConvert { bends: a, body, .. }, FeatureKind::SheetConvert { bends: b, body: was, .. }) = (&nf.kind, &f.kind)
&& (a != b || body != was)
{
nf.edge_names = solidcraft_doc::sheet::bend_edge_names(&s.model.state_before(id), body, a);
nf.edge_names = solidcraft_doc::sheet::bend_edge_names(&s.model.state_before(id), body, &solidcraft_doc::plain(a));
}
// Profiles picked by index or point are kept by their curves, as when the feature was made.
if nf.kind.clone().profile_sels_mut() != f.kind.clone().profile_sels_mut() {
@@ -1045,7 +1045,7 @@ fn redefine(s: &mut Session, p: &Value) -> Result<Value> {
&& before.body == after.body
&& before.path == after.path
&& before.author_path == after.author_path
&& before.at.dist(after.at) < 1e-8
&& before.at.dist(after.at.0) < 1e-8
{
*after = before.clone();
}
+3 -3
View File
@@ -35,7 +35,7 @@ fn generated_face_profiles_keep_exact_links_live_after_edit_and_reopen() {
let result = run(&mut s, "solid.extrude", json!({"face":[5,5,20],"distance":10,"operation":"new","name":"Pad"}));
let pad = s.doc.feature(result["feature"].as_u64().unwrap()).unwrap();
let FeatureKind::Extrude { sketch, .. } = pad.kind else { panic!("extrude") };
let generated = s.doc.feature(sketch).unwrap();
let generated = s.doc.feature(sketch.0).unwrap();
let FeatureKind::Sketch { plane, sketch: profile } = &generated.kind else { panic!("sketch") };
let PlaneRef::Face { anchor: Some(anchor), .. } = plane else { panic!("the original Box cap remains exact after a hole cut") };
assert_eq!(anchor.target.origin_feature, base);
@@ -50,8 +50,8 @@ fn generated_face_profiles_keep_exact_links_live_after_edit_and_reopen() {
let edited = s.model.state();
assert!(edited.bodies.iter().any(|body| body.body.contains(Vec3::new(49.0, 5.0, 34.0))));
assert!(!edited.bodies.iter().any(|body| body.body.contains(Vec3::new(51.0, 5.0, 34.0))));
assert!(edited.sketch(sketch).unwrap().sketch.links.iter().all(|link| link.auto_face && !link.lost));
let solved = edited.sketch(sketch).unwrap().plane;
assert!(edited.sketch(sketch.0).unwrap().sketch.links.iter().all(|link| link.auto_face && !link.lost));
let solved = edited.sketch(sketch.0).unwrap().plane;
assert!((solved.origin.z - 25.0).abs() < 1e-6);
let before = s.model.state();
let before_volume: f64 = before.bodies.iter().map(|body| solidcraft_kernel::measure(&body.body).unwrap().volume).sum();
+178 -70
View File
@@ -2,8 +2,10 @@
use serde_json::{Value, json};
use solidcraft_doc::{
AxisRef, Direction, Extent, FeatureKind, Operation, PointExpr, ProfileSel, SweepOrientation, ThinSide, ThinWall, expr::Kind, point_expr,
AxisRef, Direction, Expr, Extent, FeatureKind, Operation, PointExpr, ProfileSel, SweepOrientation, ThinSide, ThinWall, expr::Kind, point_expr,
};
use solidcraft_doc::{BodyName, FeatureName, SketchId};
use solidcraft_doc::{Dir, FacePick, Position};
use solidcraft_geom::Vec3;
use super::{CommandSpec, not_in_sketch};
@@ -393,7 +395,7 @@ fn add_feature_in(s: &mut Session, p: &Value, kind: FeatureKind, world: bool) ->
if s.doc.units.trim() != "mm" {
let doc = s.doc.clone();
for (_, e, k) in kind.inputs_mut() {
*e = doc.with_design_unit(e, k);
*e = doc.with_design_unit(e, k).into();
}
}
// Profiles picked by index are kept by the curves around them: a recompute can renumber a
@@ -403,17 +405,20 @@ fn add_feature_in(s: &mut Session, p: &Value, kind: FeatureKind, world: bool) ->
let (refs, names) = match &kind {
FeatureKind::Fillet { edges, body, .. } | FeatureKind::Chamfer { edges, body, .. } => {
let st = s.model.state();
(solidcraft_doc::edge_refs(&st, body, edges), solidcraft_doc::edge_names_for(&st, body, edges))
(
solidcraft_doc::edge_refs(&st, body, &solidcraft_doc::plain(edges)),
solidcraft_doc::edge_names_for(&st, body, &solidcraft_doc::plain(edges)),
)
}
// Sheet metal edges are named by the sheet model.
FeatureKind::SheetFlange { edges, .. } => {
let points: Vec<_> = edges.iter().map(|e| e.point).collect();
(Vec::new(), solidcraft_doc::sheet::sheet_edge_names(&s.model.state(), &points))
(Vec::new(), solidcraft_doc::sheet::sheet_edge_names(&s.model.state(), &solidcraft_doc::plain(&points)))
}
FeatureKind::SheetHem { edges, .. } => (Vec::new(), solidcraft_doc::sheet::sheet_edge_names(&s.model.state(), edges)),
FeatureKind::SheetHem { edges, .. } => (Vec::new(), solidcraft_doc::sheet::sheet_edge_names(&s.model.state(), &solidcraft_doc::plain(edges))),
// A conversion's bend edges are edges of the body it converts.
FeatureKind::SheetConvert { body, bends, .. } if !bends.is_empty() => {
(Vec::new(), solidcraft_doc::sheet::bend_edge_names(&s.model.state(), body, bends))
(Vec::new(), solidcraft_doc::sheet::bend_edge_names(&s.model.state(), body, &solidcraft_doc::plain(bends)))
}
_ => (Vec::new(), Vec::new()),
};
@@ -424,7 +429,7 @@ fn add_feature_in(s: &mut Session, p: &Value, kind: FeatureKind, world: bool) ->
let body = state.body(&face.body).ok_or_else(|| EngineError::Other("selected cylinder body no longer exists".into()))?;
let frame = face.body_frame(&s.doc, body).map_err(|e| EngineError::Other(e.to_string()))?;
let inverse = solidcraft_doc::mat_inverse(&frame).ok_or_else(|| EngineError::Other("invalid cylinder placement".into()))?;
let at = solidcraft_doc::apply_point(&solidcraft_doc::mat_mul(&inverse, &s.active_component_frame()), face.at);
let at = solidcraft_doc::apply_point(&solidcraft_doc::mat_mul(&inverse, &s.active_component_frame()), face.at.0);
let mut scoped = solidcraft_doc::ModelState::default();
scoped.bodies.push(body.clone());
let names = solidcraft_doc::naming::face_names_at(&scoped, &[at]);
@@ -561,7 +566,7 @@ fn extrude(s: &mut Session, p: &Value) -> Result<Value> {
targets.extend(face_body.or_else(|| sketch_face_body(s, sketch)));
}
let kind = FeatureKind::Extrude {
sketch,
sketch: SketchId(sketch),
profiles: match face_profile {
Some(f) => f,
None if thin.is_some() => ProfileSel::All,
@@ -569,7 +574,7 @@ fn extrude(s: &mut Session, p: &Value) -> Result<Value> {
},
extent,
operation,
targets,
targets: targets.into_iter().map(BodyName).collect(),
thin,
};
add_feature(s, p, kind)
@@ -685,9 +690,9 @@ pub(super) fn extent_param(s: &Session, p: &Value, cmd: &str) -> Result<Extent>
start_offset,
through_all,
taper,
to: p.get("to").and_then(vec3),
to: p.get("to").and_then(vec3).map(FacePick),
to_offset: expr(p, "to_offset"),
from: p.get("from").and_then(vec3),
from: p.get("from").and_then(vec3).map(FacePick),
})
}
@@ -724,16 +729,16 @@ fn revolve(s: &mut Session, p: &Value) -> Result<Value> {
Some(value) => serde_json::from_value(value.clone()).map_err(|_| bad(cmd, "`convention` must be sketch_normal or legacy_axis_start"))?,
};
let kind = FeatureKind::Revolve {
sketch,
sketch: SketchId(sketch),
profiles,
axis,
angle,
operation: operation(p, cmd)?,
targets: string_list(p, "targets"),
targets: string_list(p, "targets").into_iter().map(BodyName).collect(),
angle2,
symmetric,
convention,
to,
to: to.map(FacePick),
};
add_feature(s, p, kind)
}
@@ -744,7 +749,7 @@ pub(super) fn axis_param(p: &Value, cmd: &str) -> Result<AxisRef> {
if let Some(o) = p.get("axis").filter(|v| v.is_object()) {
let origin = o.get("origin").and_then(vec3).unwrap_or(Vec3::ZERO);
let dir = o.get("dir").and_then(vec3).ok_or_else(|| bad(cmd, "axis needs `dir`"))?;
return Ok(AxisRef::Line { origin, dir });
return Ok(AxisRef::Line { origin: Position(origin), dir: Dir(dir) });
}
match str_(p, "axis") {
Some(a @ ("X" | "Y" | "Z")) => Ok(AxisRef::World { axis: a.into() }),
@@ -754,12 +759,12 @@ pub(super) fn axis_param(p: &Value, cmd: &str) -> Result<AxisRef> {
}
}
fn radius_expr(s: &Session, p: &Value, cmd: &str) -> Result<String> {
fn radius_expr(s: &Session, p: &Value, cmd: &str) -> Result<Expr> {
match (expr(p, "radius"), expr(p, "diameter")) {
(Some(r), _) => Ok(r),
(None, Some(d)) => {
let [d] = length_operands(s, [(&d, &typed_length_unit(s))]);
Ok(format!("({d}) / 2"))
Ok(format!("({d}) / 2").into())
}
_ => Err(bad(cmd, "needs `radius` or `diameter`")),
}
@@ -833,11 +838,13 @@ fn prim_box(s: &mut Session, p: &Value) -> Result<Value> {
(None, Some([cx, cy, cz])) => {
let unit = typed_length_unit(s);
let mm_per_unit = solidcraft_doc::expr::unit_info(&unit).map_or(1.0, |(_, k)| k);
let back = |c: String, size: &String| match (c.parse::<f64>(), size.parse::<f64>()) {
(Ok(a), Ok(b)) => format!("{}", a - b * mm_per_unit / 2.0),
_ => {
let [c, size] = length_operands(s, [(&c, "mm"), (size, &unit)]);
format!("({c}) - ({size}) / 2")
let back = |c: Expr, size: &Expr| -> Expr {
match (c.parse::<f64>(), size.parse::<f64>()) {
(Ok(a), Ok(b)) => format!("{}", a - b * mm_per_unit / 2.0).into(),
_ => {
let [c, size] = length_operands(s, [(&c, "mm"), (size, &unit)]);
format!("({c}) - ({size}) / 2").into()
}
}
};
[back(cx, &l), back(cy, &w), back(cz, &h)]
@@ -855,7 +862,7 @@ fn prim_cylinder(s: &mut Session, p: &Value) -> Result<Value> {
check_expr(s, &height, Kind::Length, cmd, "height")?;
let base = point_param(s, p, "base", cmd)?.unwrap_or_else(|| point_expr(Vec3::ZERO));
let axis = axis_dir_param(p, cmd)?;
add_feature(s, p, FeatureKind::Cylinder { base, axis, radius, height, operation: operation(p, cmd)? })
add_feature(s, p, FeatureKind::Cylinder { base, axis: Dir(axis), radius, height, operation: operation(p, cmd)? })
}
fn prim_sphere(s: &mut Session, p: &Value) -> Result<Value> {
@@ -968,8 +975,8 @@ fn press_pull(s: &mut Session, p: &Value) -> Result<Value> {
// Pushed in: the extrude cuts the other way by as much (a bare number stays one, so it keeps
// being read as a bare number typed into the extrude).
let flipped = match distance.trim().parse::<f64>() {
Ok(v) if solidcraft_doc::expr::is_bare_number(&distance) => format!("{}", -v),
_ => format!("-({distance})"),
Ok(v) if solidcraft_doc::expr::is_bare_number(&distance) => Expr::new(format!("{}", -v)),
_ => Expr::new(format!("-({distance})")),
};
let (dist, dir, op) = if d > 0.0 { (distance, "positive", "join") } else { (flipped, "negative", "cut") };
extrude(s, &json!({"face": face, "distance": dist, "direction": dir, "operation": op}))
@@ -1003,10 +1010,10 @@ fn fillet(s: &mut Session, p: &Value) -> Result<Value> {
_ => return Err(bad(cmd, "a `mid` radius is a number or an expression")),
};
check_expr(s, &r, Kind::Length, cmd, "mid radius")?;
mid.push((t, r));
mid.push((t, r.into()));
}
let smooth = p.get("smooth").and_then(Value::as_bool).unwrap_or(false);
solidcraft_doc::FilletStyle::Variable { radius2, start, mid, smooth }
solidcraft_doc::FilletStyle::Variable { radius2, start: Position(start), mid, smooth }
}
Some("spline") => {
let start = p
@@ -1022,7 +1029,7 @@ fn fillet(s: &mut Session, p: &Value) -> Result<Value> {
_ => return Err(bad(cmd, "a control radius is a number or an expression")),
};
check_expr(s, &r, Kind::Length, cmd, "control radius")?;
radii.push(r);
radii.push(Expr::from(r));
}
// All control radii (`radius` and `radii`) plus the degree (1…3) plus one.
if radii.is_empty() || !(radii.len() + 3..=radii.len() + 5).contains(&knots.len()) {
@@ -1031,7 +1038,7 @@ fn fillet(s: &mut Session, p: &Value) -> Result<Value> {
"a spline fillet needs `radii` (the control radii after `radius`) and `knots` (as many as all control radii, the degree 1…3 and one more)",
));
}
solidcraft_doc::FilletStyle::Spline { start, knots, radii }
solidcraft_doc::FilletStyle::Spline { start: Position(start), knots, radii }
}
Some(o) => return Err(bad(cmd, format!("unknown fillet type `{o}` (constant, chord, variable, spline or curvature)"))),
};
@@ -1044,7 +1051,11 @@ fn fillet(s: &mut Session, p: &Value) -> Result<Value> {
if !fit.is_exact() && !style.is_constant() {
return Err(bad(cmd, "`fit: reduce` and `fit: taper` take a constant radius"));
}
let mut out = add_feature(s, p, FeatureKind::Fillet { edges, radius, body: str_(p, "body").map(str::to_string), style, fit })?;
let mut out = add_feature(
s,
p,
FeatureKind::Fillet { edges: edges.into_iter().map(Position).collect(), radius, body: str_(p, "body").map(BodyName::from), style, fit },
)?;
// The radii the fillet used, when `fit: reduce` reduced it.
if let Some(id) = out.get("feature").and_then(Value::as_u64)
&& let Some(state) = s.model.result(id).map(|res| &res.state)
@@ -1074,7 +1085,18 @@ fn chamfer(s: &mut Session, p: &Value) -> Result<Value> {
return Err(bad(cmd, "give `distance2` or `angle`, not both"));
}
let flip = bool_(p, "flip").unwrap_or(false);
add_feature(s, p, FeatureKind::Chamfer { edges, distance, distance2, angle, flip, body: str_(p, "body").map(str::to_string) })
add_feature(
s,
p,
FeatureKind::Chamfer {
edges: edges.into_iter().map(Position).collect(),
distance,
distance2,
angle,
flip,
body: str_(p, "body").map(BodyName::from),
},
)
}
fn combine(s: &mut Session, p: &Value) -> Result<Value> {
@@ -1091,7 +1113,12 @@ fn combine(s: &mut Session, p: &Value) -> Result<Value> {
let mut out = add_feature(
s,
p,
FeatureKind::Combine { target: target.clone(), tools, operation: op, keep_tools: bool_(p, "keep_tools").unwrap_or(false) },
FeatureKind::Combine {
target: target.clone().into(),
tools: tools.into_iter().map(BodyName).collect(),
operation: op,
keep_tools: bool_(p, "keep_tools").unwrap_or(false),
},
)?;
// New Component: the result goes into a component of its own.
if bool_(p, "new_component").unwrap_or(false) {
@@ -1111,7 +1138,7 @@ fn move_bodies(s: &mut Session, p: &Value) -> Result<Value> {
}
let t = match p.get("translate") {
Some(Value::Array(a)) if a.len() == 3 => {
let mut out: [String; 3] = Default::default();
let mut out: [Expr; 3] = Default::default();
for (i, v) in a.iter().enumerate() {
let e = expr(&json!({"v": v}), "v").ok_or_else(|| bad(cmd, "translate entries must be numbers or expressions"))?;
check_expr(s, &e, Kind::Length, cmd, "translate")?;
@@ -1131,7 +1158,13 @@ fn move_bodies(s: &mut Session, p: &Value) -> Result<Value> {
add_feature(
s,
p,
FeatureKind::Move { bodies, translate: t, rotate_axis: p.get("axis").and_then(vec3), angle, copy: bool_(p, "copy").unwrap_or(false) },
FeatureKind::Move {
bodies: bodies.into_iter().map(BodyName).collect(),
translate: t,
rotate_axis: p.get("axis").and_then(vec3).map(Dir),
angle,
copy: bool_(p, "copy").unwrap_or(false),
},
)
}
@@ -1174,7 +1207,15 @@ fn axis_line(p: &Value, cmd: &str) -> Result<(Vec3, Vec3)> {
fn finish_pattern(s: &mut Session, p: &Value, features: Vec<String>, pattern: solidcraft_doc::PatternKind, bodies: Vec<String>) -> Result<Value> {
let comps = string_list(p, "components");
if comps.is_empty() {
return add_feature(s, p, FeatureKind::Pattern { features, pattern, bodies });
return add_feature(
s,
p,
FeatureKind::Pattern {
features: features.into_iter().map(FeatureName).collect(),
pattern,
bodies: bodies.into_iter().map(BodyName).collect(),
},
);
}
let cmd = "pattern (components)";
let mats = solidcraft_doc::pattern_matrices(&s.doc, &s.model.state(), &pattern)?;
@@ -1222,7 +1263,7 @@ fn pattern_rect(s: &mut Session, p: &Value) -> Result<Value> {
if let Some(c) = &spacing2 {
check_expr(s, c, Kind::Length, cmd, "spacing2")?;
}
let pattern = solidcraft_doc::PatternKind::Rectangular { dir1, count1, spacing1, dir2, count2, spacing2 };
let pattern = solidcraft_doc::PatternKind::Rectangular { dir1: Dir(dir1), count1, spacing1, dir2: dir2.map(Dir), count2, spacing2 };
finish_pattern(s, p, features, pattern, bodies)
}
@@ -1234,7 +1275,7 @@ fn pattern_circ(s: &mut Session, p: &Value) -> Result<Value> {
check_expr(s, &count, Kind::Unitless, cmd, "count")?;
let angle = expr(p, "angle").unwrap_or_else(|| "360 deg".into());
check_expr(s, &angle, Kind::Angle, cmd, "angle")?;
let pattern = solidcraft_doc::PatternKind::Circular { origin, axis, count, angle };
let pattern = solidcraft_doc::PatternKind::Circular { origin: Position(origin), axis: Dir(axis), count, angle };
finish_pattern(s, p, features, pattern, bodies)
}
@@ -1260,7 +1301,7 @@ fn pattern_path(s: &mut Session, p: &Value) -> Result<Value> {
Some(o) => return Err(bad(cmd, format!("unknown orientation `{o}` (identical or path)"))),
};
let flip = bool_(p, "flip").unwrap_or(false);
let pattern = solidcraft_doc::PatternKind::Path { path_sketch, path, count, spacing, extent, flip, orient };
let pattern = solidcraft_doc::PatternKind::Path { path_sketch: SketchId(path_sketch), path, count, spacing, extent, flip, orient };
finish_pattern(s, p, features, pattern, bodies)
}
@@ -1270,7 +1311,16 @@ fn mirror(s: &mut Session, p: &Value) -> Result<Value> {
let features = if bodies.is_empty() { source_features(s, p, cmd)? } else { Vec::new() };
let plane = plane_param(s, p.get("plane"), cmd)?;
let combine = bool_(p, "combine").unwrap_or(false);
add_feature(s, p, FeatureKind::Mirror { features, plane, bodies, combine })
add_feature(
s,
p,
FeatureKind::Mirror {
features: features.into_iter().map(FeatureName).collect(),
plane,
bodies: bodies.into_iter().map(BodyName).collect(),
combine,
},
)
}
/// The other name of an origin plane (`Top` for `XY`, `XY` for `Top`...), if `name` is one.
@@ -1306,7 +1356,7 @@ pub(super) fn plane_param(s: &Session, v: Option<&Value>, cmd: &str) -> Result<s
use solidcraft_doc::PlaneRef;
const WANT: &str = "XY, XZ, YZ, a construction plane name or id, or {origin, normal} (origin coordinates may be length expressions)";
let construction = |r: &str| match s.doc.find_feature(r) {
Some(f) if matches!(f.kind, FeatureKind::ConstructionPlane { .. }) => Ok(PlaneRef::Construction { name: f.name.clone() }),
Some(f) if matches!(f.kind, FeatureKind::ConstructionPlane { .. }) => Ok(PlaneRef::Construction { name: f.name.clone().into() }),
Some(f) => Err(bad(cmd, format!("`{}` is not a construction plane (a plane is {WANT})", f.name))),
None => Err(bad(cmd, format!("no plane `{r}` (a plane is {WANT})"))),
};
@@ -1339,7 +1389,7 @@ pub(super) fn plane_param(s: &Session, v: Option<&Value>, cmd: &str) -> Result<s
None => point_expr(Vec3::ZERO),
Some(_) => point_param(s, o, "origin", cmd)?.unwrap_or_else(|| point_expr(Vec3::ZERO)),
};
let at = |e: &String| s.doc.eval(e, Kind::Length).unwrap_or(0.0);
let at = |e: &str| s.doc.eval(e, Kind::Length).unwrap_or(0.0);
let here = Vec3::new(at(&origin[0]), at(&origin[1]), at(&origin[2]));
let pl = match (o.get("x_dir").and_then(vec3), o.get("y_dir").and_then(vec3), o.get("normal").and_then(vec3)) {
(Some(x), Some(y), _) => solidcraft_geom::Plane::new(here, x, y),
@@ -1393,13 +1443,25 @@ fn split_face(s: &mut Session, p: &Value) -> Result<Value> {
let kind = if let Some(t) = p.get("tool").filter(|t| !t.is_null()) {
let tb = t.get("body").and_then(Value::as_str).ok_or_else(|| bad(cmd, "`tool.body` must name a body"))?.to_string();
let point = t.get("point").and_then(crate::params::vec3).ok_or_else(|| bad(cmd, "`tool.point` must be [x, y, z] on the tool face"))?;
FeatureKind::SplitFace { faces, body, plane: xy(), tool: Some(solidcraft_doc::FaceAt { body: tb, point }), sketch: None }
FeatureKind::SplitFace {
faces: faces.into_iter().map(FacePick).collect(),
body: body.map(BodyName),
plane: xy(),
tool: Some(solidcraft_doc::FaceAt { body: BodyName(tb), point: Position(point) }),
sketch: None,
}
} else if p.get("sketch").is_some_and(|v| !v.is_null()) {
let sketch = sketch_id(s, p.get("sketch"), cmd, "sketch")?;
FeatureKind::SplitFace { faces, body, plane: xy(), tool: None, sketch: Some(sketch) }
FeatureKind::SplitFace {
faces: faces.into_iter().map(FacePick).collect(),
body: body.map(BodyName),
plane: xy(),
tool: None,
sketch: Some(SketchId(sketch)),
}
} else {
let plane = plane_param(s, p.get("plane"), cmd)?;
FeatureKind::SplitFace { faces, body, plane, tool: None, sketch: None }
FeatureKind::SplitFace { faces: faces.into_iter().map(FacePick).collect(), body: body.map(BodyName), plane, tool: None, sketch: None }
};
add_feature(s, p, kind)
}
@@ -1423,14 +1485,14 @@ fn split_body(s: &mut Session, p: &Value) -> Result<Value> {
s,
p,
FeatureKind::Split {
body,
body: BodyName(body),
plane: solidcraft_doc::PlaneRef::Origin { name: "XY".into() },
tool: Some(solidcraft_doc::FaceAt { body: tb, point }),
tool: Some(solidcraft_doc::FaceAt { body: BodyName(tb), point: Position(point) }),
},
);
}
let plane = plane_param(s, p.get("plane"), cmd)?;
add_feature(s, p, FeatureKind::Split { body, plane, tool: None })
add_feature(s, p, FeatureKind::Split { body: BodyName(body), plane, tool: None })
}
/// ISO 273 clearance hole diameters (close, normal, loose fit) for metric bolts.
@@ -1492,7 +1554,7 @@ fn hole(s: &mut Session, p: &Value) -> Result<Value> {
let first =
ids.first().and_then(|id| ss.sketch.points.iter().find(|q| &q.id == id)).map(|q| ss.plane.to_world(q.pos)).unwrap_or_default();
let flip = p.get("flip").and_then(Value::as_bool).unwrap_or(false);
Some((solidcraft_doc::SketchPoints { sketch, ids, flip }, first, -n))
Some((solidcraft_doc::SketchPoints { sketch: SketchId(sketch), ids, flip }, first, -n))
}
_ => None,
};
@@ -1526,11 +1588,11 @@ fn hole(s: &mut Session, p: &Value) -> Result<Value> {
(None, _, Some(c)) => {
let fit = str_(p, "fit").unwrap_or("normal").to_ascii_lowercase();
let d = clearance_diameter(c, &fit).ok_or_else(|| bad(cmd, format!("no clearance hole for `{c}` ({fit} fit): M1.6 to M64")))?;
format!("{d} mm")
Expr::new(format!("{d} mm"))
}
(None, Some(t), None) => {
let (major, pitch) = solidcraft_doc::parse_metric_thread(t).ok_or_else(|| bad(cmd, "thread"))?;
format!("{} mm", ((major - pitch) * 1000.0).round() / 1000.0)
Expr::new(format!("{} mm", ((major - pitch) * 1000.0).round() / 1000.0))
}
(None, None, None) => return Err(bad(cmd, "needs `diameter`, `thread` (a tapped hole) or `clearance` (a bolt size)")),
};
@@ -1539,7 +1601,7 @@ fn hole(s: &mut Session, p: &Value) -> Result<Value> {
if let Some(d) = &depth {
check_expr(s, d, Kind::Length, cmd, "depth")?;
}
let get = |k: &str| -> Result<String> {
let get = |k: &str| -> Result<Expr> {
let e = req_expr(cmd, p, k)?;
Ok(e)
};
@@ -1570,7 +1632,20 @@ fn hole(s: &mut Session, p: &Value) -> Result<Value> {
{
p["participants"] = json!([body]);
}
add_feature(s, &p, FeatureKind::Hole { position, direction, diameter, depth, hole: kind, points: points.map(|x| x.0), thread, to })
add_feature(
s,
&p,
FeatureKind::Hole {
position: FacePick(position),
direction: Dir(direction),
diameter,
depth,
hole: kind,
points: points.map(|x| x.0),
thread,
to: to.map(FacePick),
},
)
}
/// The body whose face is nearest `p` (within its pick tolerance), when it belongs to another
@@ -1627,9 +1702,9 @@ fn shell(s: &mut Session, p: &Value) -> Result<Value> {
s,
p,
FeatureKind::Shell {
faces,
faces: faces.into_iter().map(FacePick).collect(),
thickness,
body: str_(p, "body").map(str::to_string),
body: str_(p, "body").map(BodyName::from),
direction: if direction == "inside" { String::new() } else { direction },
tangent_chain: bool_(p, "tangent_chain").unwrap_or(false),
},
@@ -1645,7 +1720,17 @@ fn draft(s: &mut Session, p: &Value) -> Result<Value> {
let (vals, _) = s.doc.param_values();
let pl = s.doc.resolve_plane_in(&vals, &neutral, 0, Some(&s.model.state()))?;
let pull = p.get("pull").and_then(vec3).unwrap_or_else(|| pl.normal());
add_feature(s, p, FeatureKind::Draft { faces, angle, neutral, pull, body: str_(p, "body").map(str::to_string) })
add_feature(
s,
p,
FeatureKind::Draft {
faces: faces.into_iter().map(FacePick).collect(),
angle,
neutral,
pull: Dir(pull),
body: str_(p, "body").map(BodyName::from),
},
)
}
pub(super) fn sketch_id(s: &Session, v: Option<&Value>, cmd: &str, what: &str) -> Result<u64> {
@@ -1673,22 +1758,22 @@ fn sweep(s: &mut Session, p: &Value) -> Result<Value> {
s,
p,
FeatureKind::Sweep {
sketch,
sketch: SketchId(sketch),
profiles: profiles(p, cmd)?,
path_sketch,
path_sketch: SketchId(path_sketch),
path,
orientation,
twist,
scale,
operation: operation(p, cmd)?,
targets: string_list(p, "targets"),
targets: string_list(p, "targets").into_iter().map(BodyName).collect(),
},
)
}
/// A sweep's `orientation`, `twist` and `scale` (solid.sweep and surface.sweep), checked: an
/// orientation the sweep knows, a twist within four turns, an end scale within 0.01…100.
pub(super) fn sweep_options(s: &Session, p: &Value, cmd: &str) -> Result<(SweepOrientation, Option<String>, Option<String>)> {
pub(super) fn sweep_options(s: &Session, p: &Value, cmd: &str) -> Result<(SweepOrientation, Option<Expr>, Option<Expr>)> {
let orientation = match p.get("orientation") {
None | Some(Value::Null) => SweepOrientation::Perpendicular,
Some(Value::String(o)) if o.eq_ignore_ascii_case("perpendicular") => SweepOrientation::Perpendicular,
@@ -1700,7 +1785,7 @@ pub(super) fn sweep_options(s: &Session, p: &Value, cmd: &str) -> Result<(SweepO
if !(d.len() > 1e-9) {
return Err(bad(cmd, "the `orientation` direction must not be zero"));
}
SweepOrientation::Direction(d)
SweepOrientation::Direction(Dir(d))
}
};
let twist = match p.get("twist") {
@@ -1744,12 +1829,20 @@ fn loft(s: &mut Session, p: &Value) -> Result<Value> {
if !st.sketch(sketch).is_some_and(|ss| ss.sketch.points.iter().any(|q| q.id == pt)) {
return Err(bad(cmd, format!("no point `{pt}` in the section's sketch")));
}
sections.push(solidcraft_doc::LoftSection { sketch, profiles: solidcraft_doc::ProfileSel::All, point: Some(pt.to_string()) });
sections.push(solidcraft_doc::LoftSection {
sketch: SketchId(sketch),
profiles: solidcraft_doc::ProfileSel::All,
point: Some(pt.to_string()),
});
continue;
}
sections.push(solidcraft_doc::LoftSection { sketch, profiles: profiles(sec, cmd)?, point: None });
sections.push(solidcraft_doc::LoftSection { sketch: SketchId(sketch), profiles: profiles(sec, cmd)?, point: None });
}
add_feature(s, p, FeatureKind::Loft { sections, operation: operation(p, cmd)?, targets: string_list(p, "targets") })
add_feature(
s,
p,
FeatureKind::Loft { sections, operation: operation(p, cmd)?, targets: string_list(p, "targets").into_iter().map(BodyName).collect() },
)
}
fn thread(s: &mut Session, p: &Value) -> Result<Value> {
@@ -1769,7 +1862,7 @@ fn thread(s: &mut Session, p: &Value) -> Result<Value> {
check_expr(s, l, Kind::Length, cmd, "length")?;
}
let modeled = p.get("modeled").and_then(Value::as_bool).unwrap_or(false);
let v = add_feature(s, p, FeatureKind::Thread { face, designation, length, modeled })?;
let v = add_feature(s, p, FeatureKind::Thread { face: FacePick(face), designation, length, modeled })?;
let st = s.model.state();
let t = v.get("feature").and_then(Value::as_u64).and_then(|id| st.threads.iter().find(|t| t.feature == id).cloned());
Ok(json!({"feature": v.get("feature"), "name": v.get("name"), "thread": t}))
@@ -1788,14 +1881,25 @@ fn pipe(s: &mut Session, p: &Value) -> Result<Value> {
if let Some(w) = &wall {
check_expr(s, w, Kind::Length, cmd, "wall")?;
}
add_feature(s, p, FeatureKind::Pipe { path_sketch, path, diameter, wall, operation: operation(p, cmd)?, targets: string_list(p, "targets") })
add_feature(
s,
p,
FeatureKind::Pipe {
path_sketch: SketchId(path_sketch),
path,
diameter,
wall,
operation: operation(p, cmd)?,
targets: string_list(p, "targets").into_iter().map(BodyName).collect(),
},
)
}
fn bounding_solid(s: &mut Session, p: &Value) -> Result<Value> {
let cmd = "solid.bounding_solid";
let margin = expr(p, "margin").unwrap_or_else(|| "0".into());
check_expr(s, &margin, Kind::Length, cmd, "margin")?;
add_feature(s, p, FeatureKind::BoundingSolid { bodies: string_list(p, "bodies"), margin })
add_feature(s, p, FeatureKind::BoundingSolid { bodies: string_list(p, "bodies").into_iter().map(BodyName).collect(), margin })
}
fn scale(s: &mut Session, p: &Value) -> Result<Value> {
@@ -1810,7 +1914,7 @@ fn scale(s: &mut Session, p: &Value) -> Result<Value> {
for x in &e {
check_expr(s, x, Kind::Unitless, cmd, "factors")?;
}
Some([e[0].clone(), e[1].clone(), e[2].clone()])
Some([e[0].clone().into(), e[1].clone().into(), e[2].clone().into()])
}
Some(_) => return Err(bad(cmd, "`factors` must have three values")),
None => None,
@@ -1822,13 +1926,13 @@ fn scale(s: &mut Session, p: &Value) -> Result<Value> {
};
check_expr(s, &factor, Kind::Unitless, cmd, "factor")?;
let origin = p.get("origin").and_then(vec3).unwrap_or(Vec3::ZERO);
add_feature(s, p, FeatureKind::Scale { bodies, origin, factor, factors })
add_feature(s, p, FeatureKind::Scale { bodies: bodies.into_iter().map(BodyName).collect(), origin: Position(origin), factor, factors })
}
fn delete_face(s: &mut Session, p: &Value) -> Result<Value> {
let faces = face_points(s, p, "face.delete")?;
let heal = bool_(p, "heal").unwrap_or(true);
add_feature(s, p, FeatureKind::DeleteFace { faces, body: str_(p, "body").map(str::to_string), heal })
add_feature(s, p, FeatureKind::DeleteFace { faces: faces.into_iter().map(FacePick).collect(), body: str_(p, "body").map(BodyName::from), heal })
}
fn offset_face(s: &mut Session, p: &Value) -> Result<Value> {
@@ -1836,7 +1940,11 @@ fn offset_face(s: &mut Session, p: &Value) -> Result<Value> {
let faces = face_points(s, p, cmd)?;
let distance = req_expr(cmd, p, "distance")?;
check_expr(s, &distance, Kind::Length, cmd, "distance")?;
add_feature(s, p, FeatureKind::OffsetFace { faces, distance, body: str_(p, "body").map(str::to_string) })
add_feature(
s,
p,
FeatureKind::OffsetFace { faces: faces.into_iter().map(FacePick).collect(), distance, body: str_(p, "body").map(BodyName::from) },
)
}
#[cfg(test)]
+39 -13
View File
@@ -3,6 +3,8 @@
use serde_json::Value;
use solidcraft_doc::FeatureKind;
use solidcraft_doc::expr::Kind;
use solidcraft_doc::{BodyName, SketchId};
use solidcraft_doc::{Dir, FacePick, Position};
use solidcraft_geom::Vec3;
use super::CommandSpec;
@@ -57,11 +59,11 @@ fn coil(s: &mut Session, p: &Value) -> Result<Value> {
(Some(n), _, Some(pt)) => (n, pt),
(Some(n), Some(h), None) => {
let [h] = length_operands(s, [(&h, &unit)]);
(n.clone(), format!("({h}) / ({n})"))
(n.clone(), format!("({h}) / ({n})").into())
}
(None, Some(h), Some(pt)) => {
let [h, by] = length_operands(s, [(&h, &unit), (&pt, &unit)]);
(format!("({h}) / ({by})"), pt)
(format!("({h}) / ({by})").into(), pt)
}
_ => return Err(bad(cmd, "give two of `revolutions`, `height` and `pitch`")),
};
@@ -91,7 +93,7 @@ fn coil(s: &mut Session, p: &Value) -> Result<Value> {
p,
FeatureKind::Coil {
base,
axis,
axis: Dir(axis),
diameter,
pitch,
turns,
@@ -101,7 +103,7 @@ fn coil(s: &mut Session, p: &Value) -> Result<Value> {
start_angle,
clockwise,
operation,
targets: string_list(p, "targets"),
targets: string_list(p, "targets").into_iter().map(BodyName).collect(),
},
)
}
@@ -131,7 +133,7 @@ fn rib_like(s: &mut Session, p: &Value, cmd: &str, web: bool) -> Result<Value> {
check_expr(s, d, Kind::Length, cmd, "depth")?;
}
let flip = bool_(p, "flip").unwrap_or(false);
add_feature(s, p, FeatureKind::Rib { sketch, curves, thickness, depth, flip, web })
add_feature(s, p, FeatureKind::Rib { sketch: SketchId(sketch), curves, thickness, depth, flip, web })
}
fn rib(s: &mut Session, p: &Value) -> Result<Value> {
@@ -212,11 +214,15 @@ fn emboss(s: &mut Session, p: &Value) -> Result<Value> {
} else {
let inferred = infer_emboss_selection(s, sketch, &profiles, &depth, deboss, &targets)?;
if let Some(face) = &inferred {
targets = vec![face.body.clone()];
targets = vec![face.body.0.clone()];
}
inferred
};
add_feature(s, p, FeatureKind::Emboss { sketch, profiles, depth, deboss, targets, face })
add_feature(
s,
p,
FeatureKind::Emboss { sketch: SketchId(sketch), profiles, depth, deboss, targets: targets.into_iter().map(BodyName).collect(), face },
)
}
fn emboss_selection(s: &Session, selected: &solidcraft_doc::ModelBody, at: Vec3) -> Result<solidcraft_doc::EmbossFace> {
@@ -240,7 +246,7 @@ fn emboss_selection(s: &Session, selected: &solidcraft_doc::ModelBody, at: Vec3)
return Err(bad(cmd, "cylinder placement is too deep"));
}
let inverse = solidcraft_doc::mat_inverse(&s.active_component_frame()).ok_or_else(|| bad(cmd, "invalid active frame"))?;
Ok(solidcraft_doc::EmbossFace { body: name.to_owned(), at: solidcraft_doc::apply_point(&inverse, at), path, author_path })
Ok(solidcraft_doc::EmbossFace { body: name.into(), at: FacePick(solidcraft_doc::apply_point(&inverse, at)), path, author_path })
}
fn check_emboss_positions(s: &Session, sketch: u64, body: &solidcraft_doc::ModelBody, face: &solidcraft_doc::EmbossFace) -> Result<()> {
@@ -307,7 +313,7 @@ fn infer_emboss_selection(
face.sketch_frame(&placed, s.active_component, sketch_owner, raw).map_err(|e| bad(cmd, e.to_string()))?;
check_emboss_positions(s, sketch, selected, &face)?;
let author_inverse = solidcraft_doc::mat_inverse(&s.active_component_frame()).ok_or_else(|| bad(cmd, "invalid author frame"))?;
face.at = solidcraft_doc::apply_point(&solidcraft_doc::mat_mul(&author_inverse, &body_frame), at);
face.at = FacePick(solidcraft_doc::apply_point(&solidcraft_doc::mat_mul(&author_inverse, &body_frame), at));
if chosen.is_some() {
return Err(bad(cmd, "multiple tangent cylinders match the selected profiles; select a target face or body"));
}
@@ -341,7 +347,7 @@ fn replace_face(s: &mut Session, p: &Value) -> Result<Value> {
None => plane_param(s, p.get("target"), cmd)?,
};
let body = str_(p, "body").map(str::to_string);
add_feature(s, p, FeatureKind::ReplaceFace { faces, target, body })
add_feature(s, p, FeatureKind::ReplaceFace { faces: faces.into_iter().map(FacePick).collect(), target, body: body.map(BodyName) })
}
fn align(s: &mut Session, p: &Value) -> Result<Value> {
@@ -376,7 +382,18 @@ fn align(s: &mut Session, p: &Value) -> Result<Value> {
_ => (None, None),
};
let flip = bool_(p, "flip").unwrap_or(false);
add_feature(s, p, FeatureKind::Align { bodies, from, to, from_normal, to_normal, flip })
add_feature(
s,
p,
FeatureKind::Align {
bodies: bodies.into_iter().map(BodyName).collect(),
from: Position(from),
to: Position(to),
from_normal: from_normal.map(Dir),
to_normal: to_normal.map(Dir),
flip,
},
)
}
fn remove(s: &mut Session, p: &Value) -> Result<Value> {
@@ -389,7 +406,7 @@ fn remove(s: &mut Session, p: &Value) -> Result<Value> {
if let Some(b) = bodies.iter().find(|b| st.body(b).is_none()) {
return Err(bad(cmd, format!("no body `{b}`")));
}
add_feature(s, p, FeatureKind::Remove { bodies })
add_feature(s, p, FeatureKind::Remove { bodies: bodies.into_iter().map(BodyName).collect() })
}
#[cfg(test)]
@@ -443,5 +460,14 @@ fn boundary_fill(s: &mut Session, p: &Value) -> Result<Value> {
return Err(bad(cmd, "`cells` must list 1…1000 points [x, y, z], one inside each cell"));
}
let operation = super::features::operation(p, cmd)?;
add_feature(s, p, FeatureKind::BoundaryFill { tools, cells, operation, remove_tools: bool_(p, "remove_tools").unwrap_or(false) })
add_feature(
s,
p,
FeatureKind::BoundaryFill {
tools: tools.into_iter().map(BodyName).collect(),
cells: cells.into_iter().map(Position).collect(),
operation,
remove_tools: bool_(p, "remove_tools").unwrap_or(false),
},
)
}
+18 -16
View File
@@ -1,9 +1,11 @@
//! Plastic (enclosure) features: Boss, Lip / Groove, Snap Fit and Rest, and plastic rules.
use serde_json::{Value, json};
use solidcraft_doc::BodyName;
use solidcraft_doc::FeatureKind;
use solidcraft_doc::expr::Kind;
use solidcraft_doc::plastic::PlasticRule;
use solidcraft_doc::{Dir, FacePick, Position};
use solidcraft_geom::Vec3;
use super::CommandSpec;
@@ -87,7 +89,7 @@ fn manage_rules(s: &mut Session, p: &Value) -> Result<Value> {
let probe = e.replace("Thickness", "(1 mm)");
check_expr(s, &probe, kind, cmd, k)?;
if let Some(slot) = rule.field_mut(k) {
*slot = e;
*slot = e.into();
}
}
}
@@ -159,20 +161,20 @@ fn rest(s: &mut Session, p: &Value) -> Result<Value> {
None => None,
};
let kind = FeatureKind::Rest {
position,
direction,
along,
position: FacePick(position),
direction: Dir(direction),
along: along.map(Dir),
width: required(s, p, "width", Kind::Length, cmd)?,
length: checked(s, p, "length", Kind::Length, cmd)?,
height: required(s, p, "height", Kind::Length, cmd)?,
draft: checked(s, p, "draft", Kind::Angle, cmd)?,
thickness: checked(s, p, "thickness", Kind::Length, cmd)?,
body: str_(p, "body").map(str::to_string),
body: str_(p, "body").map(BodyName::from),
};
add_feature(s, p, kind)
}
fn checked(s: &Session, p: &Value, k: &str, kind: Kind, cmd: &str) -> Result<Option<String>> {
fn checked(s: &Session, p: &Value, k: &str, kind: Kind, cmd: &str) -> Result<Option<solidcraft_doc::Expr>> {
match expr(p, k) {
Some(e) => {
check_expr(s, &e, kind, cmd, k)?;
@@ -182,7 +184,7 @@ fn checked(s: &Session, p: &Value, k: &str, kind: Kind, cmd: &str) -> Result<Opt
}
}
fn required(s: &Session, p: &Value, k: &str, kind: Kind, cmd: &str) -> Result<String> {
fn required(s: &Session, p: &Value, k: &str, kind: Kind, cmd: &str) -> Result<solidcraft_doc::Expr> {
let e = req_expr(cmd, p, k)?;
check_expr(s, &e, kind, cmd, k)?;
Ok(e)
@@ -202,8 +204,8 @@ fn boss(s: &mut Session, p: &Value) -> Result<Value> {
let cmd = "plastic.boss";
let (position, direction) = placed(s, p, cmd)?;
let kind = FeatureKind::Boss {
position,
direction,
position: FacePick(position),
direction: Dir(direction),
diameter: required(s, p, "diameter", Kind::Length, cmd)?,
height: required(s, p, "height", Kind::Length, cmd)?,
hole_diameter: checked(s, p, "hole_diameter", Kind::Length, cmd)?,
@@ -214,7 +216,7 @@ fn boss(s: &mut Session, p: &Value) -> Result<Value> {
rib_thickness: checked(s, p, "rib_thickness", Kind::Length, cmd)?,
rib_length: checked(s, p, "rib_length", Kind::Length, cmd)?,
rib_offset: checked(s, p, "rib_offset", Kind::Length, cmd)?,
body: str_(p, "body").map(str::to_string),
body: str_(p, "body").map(BodyName::from),
};
add_feature(s, p, kind)
}
@@ -233,13 +235,13 @@ fn lip(s: &mut Session, p: &Value) -> Result<Value> {
Some(o) => return Err(bad(cmd, format!("unknown side `{o}` (inside or outside)"))),
};
let kind = FeatureKind::Lip {
face,
face: Position(face),
width: required(s, p, "width", Kind::Length, cmd)?,
height: required(s, p, "height", Kind::Length, cmd)?,
groove,
gap: checked(s, p, "gap", Kind::Length, cmd)?,
outside: outside || bool_(p, "outside").unwrap_or(false),
body: str_(p, "body").map(str::to_string),
body: str_(p, "body").map(BodyName::from),
};
add_feature(s, p, kind)
}
@@ -252,15 +254,15 @@ fn snap_fit(s: &mut Session, p: &Value) -> Result<Value> {
return Err(bad(cmd, "`hook` must point across the arm"));
}
let kind = FeatureKind::SnapFit {
position,
direction,
hook,
position: FacePick(position),
direction: Dir(direction),
hook: Dir(hook),
length: required(s, p, "length", Kind::Length, cmd)?,
thickness: required(s, p, "thickness", Kind::Length, cmd)?,
width: required(s, p, "width", Kind::Length, cmd)?,
catch_depth: required(s, p, "catch_depth", Kind::Length, cmd)?,
catch_length: required(s, p, "catch_length", Kind::Length, cmd)?,
body: str_(p, "body").map(str::to_string),
body: str_(p, "body").map(BodyName::from),
};
add_feature(s, p, kind)
}
+39 -12
View File
@@ -4,6 +4,8 @@
use serde_json::{Value, json};
use solidcraft_doc::expr::Kind;
use solidcraft_doc::sheet::{self, SheetBody, SheetRule};
use solidcraft_doc::{BodyName, SketchId};
use solidcraft_doc::{FacePick, Position};
use solidcraft_doc::{FeatureKind, FlangeEdge};
use solidcraft_geom::{Vec2, Vec3};
@@ -110,7 +112,7 @@ fn flange_edges(s: &Session, p: &Value, cmd: &str) -> Result<Vec<FlangeEdge>> {
return Err(bad(cmd, format!("edges[{i}]: unknown key `{k}` (an edge takes point, angle and height)")));
}
let point = o.get("point").and_then(vec3).ok_or_else(|| bad(cmd, format!("edges[{i}]: `point` must be [x, y, z]")))?;
let own = |k: &str, kind: Kind| -> Result<Option<String>> {
let own = |k: &str, kind: Kind| -> Result<Option<solidcraft_doc::Expr>> {
if o.get(k).is_none_or(Value::is_null) {
return Ok(None);
}
@@ -118,7 +120,7 @@ fn flange_edges(s: &Session, p: &Value, cmd: &str) -> Result<Vec<FlangeEdge>> {
check_expr(s, &e, kind, cmd, &format!("edges[{i}].{k}"))?;
Ok(Some(e))
};
edges.push(FlangeEdge { point, angle: own("angle", Kind::Angle)?, height: own("height", Kind::Length)? });
edges.push(FlangeEdge { point: Position(point), angle: own("angle", Kind::Angle)?, height: own("height", Kind::Length)? });
}
Ok(edges)
}
@@ -162,7 +164,11 @@ fn flange(s: &mut Session, p: &Value) -> Result<Value> {
"base" => {
let sketch = feature_sketch(s, p, cmd)?;
let rule = rule_param(s, p, cmd)?;
add_feature(s, p, FeatureKind::SheetBase { sketch, profiles: profiles(p, cmd)?, rule, flip: bool_(p, "flip").unwrap_or(false) })
add_feature(
s,
p,
FeatureKind::SheetBase { sketch: SketchId(sketch), profiles: profiles(p, cmd)?, rule, flip: bool_(p, "flip").unwrap_or(false) },
)
}
"contour" => {
let sketch = sketch_id(s, p.get("sketch"), cmd, "sketch")?;
@@ -177,7 +183,7 @@ fn flange(s: &mut Session, p: &Value) -> Result<Value> {
s,
p,
FeatureKind::SheetContour {
sketch,
sketch: SketchId(sketch),
curves,
distance,
rule,
@@ -225,7 +231,7 @@ fn flange(s: &mut Session, p: &Value) -> Result<Value> {
radius,
position,
flip: bool_(p, "flip").unwrap_or(false),
body,
body: body.map(BodyName),
corner_relief_shape,
corner_relief,
},
@@ -249,7 +255,7 @@ fn perforate(s: &mut Session, p: &Value) -> Result<Value> {
if pattern.as_deref().is_some_and(|t| t != "staggered" && t != "square") {
return Err(bad(cmd, "`pattern` is \"staggered\" (60°, default) or \"square\""));
}
add_feature(s, p, FeatureKind::SheetPerforate { diameter, pitch, margin, pattern, body: str_(p, "body").map(str::to_string) })
add_feature(s, p, FeatureKind::SheetPerforate { diameter, pitch, margin, pattern, body: str_(p, "body").map(BodyName::from) })
}
fn hem(s: &mut Session, p: &Value) -> Result<Value> {
@@ -264,7 +270,13 @@ fn hem(s: &mut Session, p: &Value) -> Result<Value> {
add_feature(
s,
p,
FeatureKind::SheetHem { edges, length, gap, flip: bool_(p, "flip").unwrap_or(false), body: str_(p, "body").map(str::to_string) },
FeatureKind::SheetHem {
edges: edges.into_iter().map(Position).collect(),
length,
gap,
flip: bool_(p, "flip").unwrap_or(false),
body: str_(p, "body").map(BodyName::from),
},
)
}
@@ -311,7 +323,18 @@ fn fold(s: &mut Session, p: &Value) -> Result<Value> {
let r = add_feature(
s,
p,
FeatureKind::SheetFold { sketch, curve, a, b, angle, radius, position, flip: bool_(p, "flip").unwrap_or(false), fixed, body },
FeatureKind::SheetFold {
sketch: sketch.map(SketchId),
curve,
a: Position(a),
b: Position(b),
angle,
radius,
position,
flip: bool_(p, "flip").unwrap_or(false),
fixed: fixed.map(Position),
body: body.map(BodyName),
},
)?;
// A recompute keeps a stored fold whose fixed point no longer touches the sheet (with a
// warning); a new fold refuses one, since it picks no side (#263).
@@ -330,11 +353,11 @@ fn fold(s: &mut Session, p: &Value) -> Result<Value> {
}
fn unfold(s: &mut Session, p: &Value) -> Result<Value> {
add_feature(s, p, FeatureKind::SheetUnfold { body: str_(p, "body").map(str::to_string), refold: false })
add_feature(s, p, FeatureKind::SheetUnfold { body: str_(p, "body").map(BodyName::from), refold: false })
}
fn refold(s: &mut Session, p: &Value) -> Result<Value> {
add_feature(s, p, FeatureKind::SheetUnfold { body: str_(p, "body").map(str::to_string), refold: true })
add_feature(s, p, FeatureKind::SheetUnfold { body: str_(p, "body").map(BodyName::from), refold: true })
}
fn convert(s: &mut Session, p: &Value) -> Result<Value> {
@@ -355,7 +378,11 @@ fn convert(s: &mut Session, p: &Value) -> Result<Value> {
return Err(bad(cmd, format!("`{key}` is not a sheet.convert parameter: the sheet metal rule sets it. Pass a rule's name as `rule`")));
}
let rule = rule_param(s, p, cmd)?;
add_feature(s, p, FeatureKind::SheetConvert { body, face, rule, bends })
add_feature(
s,
p,
FeatureKind::SheetConvert { body: BodyName(body), face: FacePick(face), rule, bends: bends.into_iter().map(Position).collect() },
)
}
fn rules(s: &mut Session, p: &Value) -> Result<Value> {
@@ -386,7 +413,7 @@ fn rules(s: &mut Session, p: &Value) -> Result<Value> {
("gap", &mut rule.gap),
] {
if let Some(e) = expr(p, k) {
*slot = e;
*slot = e.into();
}
}
if let Some(shape) = p.get("corner_relief_shape").filter(|v| !v.is_null()) {
+1 -1
View File
@@ -746,7 +746,7 @@ fn tangent_circle(s: &mut Session, p: &Value, cmd: &str, n: usize, radius: Optio
fn circle_tan_tan(s: &mut Session, p: &Value) -> Result<Value> {
let cmd = "sketch.circle.two_tangent";
let r = match (crate::params::expr(p, "radius"), num(p, "diameter")) {
(Some(r), _) => r,
(Some(r), _) => r.into_string(),
(None, Some(d)) => format!("{}", d / 2.0),
_ => return Err(bad(cmd, "needs `radius` or `diameter`")),
};
+1 -1
View File
@@ -1193,7 +1193,7 @@ fn offset_length(s: &Session, p: &Value, key: &str, positive: bool) -> Result<(f
let cmd = "sketch.offset";
let expression = match num(p, key) {
Some(v) => format!("{v} mm"),
None => expr(p, key).ok_or_else(|| bad(cmd, format!("`{key}` must be a length expression")))?,
None => expr(p, key).ok_or_else(|| bad(cmd, format!("`{key}` must be a length expression")))?.into_string(),
};
let expression = s.doc.with_design_unit(&expression, solidcraft_doc::expr::Kind::Length);
let value = s.doc.eval(&expression, solidcraft_doc::expr::Kind::Length)?;
+60 -23
View File
@@ -5,6 +5,8 @@ use serde_json::Value;
use solidcraft_doc::{
BlendSide, EdgeContinuity, ExtendMode, ExtendTo, FaceAt, FeatureKind, PatchContinuity, PlaneRef, ProfileSel, RuledCorners, RuledMode, expr::Kind,
};
use solidcraft_doc::{BodyName, SketchId};
use solidcraft_doc::{Dir, FacePick, Position};
use solidcraft_kernel::Continuity;
use super::CommandSpec;
@@ -87,14 +89,26 @@ fn patch(s: &mut Session, p: &Value) -> Result<Value> {
let cmd = "surface.patch";
let crown = crown_param(p, cmd)?;
let continuity = patch_continuity(p, cmd)?;
let points = patch_points(p, cmd)?;
let points: Vec<Position> = patch_points(p, cmd)?.into_iter().map(Position).collect();
let edges: Vec<solidcraft_geom::Vec3> = p.get("edges").and_then(Value::as_array).map(|a| a.iter().filter_map(vec3).collect()).unwrap_or_default();
let mut v = if !edges.is_empty() {
if edges.len() > 10_000 {
return Err(bad(cmd, "too many edges"));
}
let body = str_(p, "body").map(str::to_string);
add_feature(s, p, FeatureKind::Patch { sketch: None, profiles: ProfileSel::All, body, edges, crown, continuity: continuity.clone(), points })?
add_feature(
s,
p,
FeatureKind::Patch {
sketch: None,
profiles: ProfileSel::All,
body: body.map(BodyName),
edges: edges.into_iter().map(Position).collect(),
crown,
continuity: continuity.clone(),
points,
},
)?
} else {
if !continuity.is_g0() || !points.is_empty() {
return Err(bad(cmd, "`continuity` and `points` are for a patch on a loop of edges"));
@@ -102,7 +116,7 @@ fn patch(s: &mut Session, p: &Value) -> Result<Value> {
let sketch = feature_sketch(s, p, cmd)?;
let profiles = profiles(p, cmd)?;
let kind = FeatureKind::Patch {
sketch: Some(sketch),
sketch: Some(SketchId(sketch)),
profiles,
body: None,
edges: Vec::new(),
@@ -138,7 +152,7 @@ fn patch_continuity(p: &Value, cmd: &str) -> Result<PatchContinuity> {
list.iter()
.map(|e| {
let edge = e.get("edge").and_then(vec3).ok_or_else(|| bad(cmd, "a `continuity` entry's `edge` must be a point [x,y,z]"))?;
Ok(EdgeContinuity { edge, g: patch_order(e.get("g"), cmd)? })
Ok(EdgeContinuity { edge: Position(edge), g: patch_order(e.get("g"), cmd)? })
})
.collect()
};
@@ -192,7 +206,7 @@ fn blend_side(p: &Value, cmd: &str, key: &str) -> Result<BlendSide> {
if edges.is_empty() || edges.len() > 10_000 {
return Err(bad(cmd, format!("`{key}.edges` must list 1 to 10000 points on the edges of a chain")));
}
Ok(BlendSide { body: body.to_string(), edges })
Ok(BlendSide { body: BodyName(body.to_string()), edges: edges.into_iter().map(Position).collect() })
}
/// `surface.blend`: a blend surface bridging two picked edge chains of two bodies.
@@ -229,7 +243,7 @@ fn blend(s: &mut Session, p: &Value) -> Result<Value> {
let point = |x: Option<&Value>| -> Result<solidcraft_geom::Vec3> {
x.and_then(vec3).ok_or_else(|| bad(cmd, "each `align` entry must be a point [x,y,z]"))
};
Some([point(list.first())?, point(list.get(1))?])
Some([Position(point(list.first())?), Position(point(list.get(1))?)])
}
};
let stitch = bool_(p, "stitch").unwrap_or(false);
@@ -244,7 +258,7 @@ fn surface_extrude(s: &mut Session, p: &Value) -> Result<Value> {
if extent.through_all || extent.taper.is_some() || extent.to.is_some() || extent.from.is_some() {
return Err(bad(cmd, "not supported yet: through all, to or from an object, or a taper"));
}
add_feature(s, p, FeatureKind::SurfaceExtrude { sketch, profiles, curves, extent })
add_feature(s, p, FeatureKind::SurfaceExtrude { sketch: SketchId(sketch), profiles, curves, extent })
}
/// A body by name, and points on some of its faces (none: all of them).
@@ -262,12 +276,12 @@ fn body_faces(s: &Session, p: &Value, cmd: &str) -> Result<(String, Vec<solidcra
fn unstitch(s: &mut Session, p: &Value) -> Result<Value> {
let (body, faces) = body_faces(s, p, "surface.unstitch")?;
add_feature(s, p, FeatureKind::Unstitch { body, faces })
add_feature(s, p, FeatureKind::Unstitch { body: BodyName(body), faces: faces.into_iter().map(Position).collect() })
}
fn reverse_normal(s: &mut Session, p: &Value) -> Result<Value> {
let (body, faces) = body_faces(s, p, "surface.reverse_normal")?;
add_feature(s, p, FeatureKind::ReverseNormal { body, faces })
add_feature(s, p, FeatureKind::ReverseNormal { body: BodyName(body), faces: faces.into_iter().map(Position).collect() })
}
fn untrim(s: &mut Session, p: &Value) -> Result<Value> {
@@ -294,7 +308,12 @@ fn untrim(s: &mut Session, p: &Value) -> Result<Value> {
if !outer && holes.as_ref().is_some_and(Vec::is_empty) {
return Err(bad(cmd, "nothing to untrim: pick holes to remove or restore the outer boundary"));
}
let kind = FeatureKind::SurfaceUntrim { face: FaceAt { body, point }, holes, outer, in_place: bool_(p, "in_place").unwrap_or(false) };
let kind = FeatureKind::SurfaceUntrim {
face: FaceAt { body: BodyName(body), point: Position(point) },
holes: holes.map(|h| h.into_iter().map(Position).collect()),
outer,
in_place: bool_(p, "in_place").unwrap_or(false),
};
add_feature(s, p, kind)
}
@@ -321,7 +340,7 @@ fn surface_revolve(s: &mut Session, p: &Value) -> Result<Value> {
check_expr(s, a, Kind::Angle, cmd, "angle2")?;
}
let symmetric = bool_(p, "symmetric").unwrap_or(false) || str_(p, "direction").is_some_and(|d| d.eq_ignore_ascii_case("symmetric"));
add_feature(s, p, FeatureKind::SurfaceRevolve { sketch, profiles, curves, axis, angle, angle2, symmetric })
add_feature(s, p, FeatureKind::SurfaceRevolve { sketch: SketchId(sketch), profiles, curves, axis, angle, angle2, symmetric })
}
fn surface_sweep(s: &mut Session, p: &Value) -> Result<Value> {
@@ -334,7 +353,11 @@ fn surface_sweep(s: &mut Session, p: &Value) -> Result<Value> {
return Err(bad(cmd, "`path` must list 1…1000 curve ids"));
}
let (orientation, twist, scale) = super::features::sweep_options(s, p, cmd)?;
add_feature(s, p, FeatureKind::SurfaceSweep { sketch, profiles, curves, path_sketch, path, orientation, twist, scale })
add_feature(
s,
p,
FeatureKind::SurfaceSweep { sketch: SketchId(sketch), profiles, curves, path_sketch: SketchId(path_sketch), path, orientation, twist, scale },
)
}
fn surface_loft(s: &mut Session, p: &Value) -> Result<Value> {
@@ -351,7 +374,7 @@ fn surface_loft(s: &mut Session, p: &Value) -> Result<Value> {
return Err(bad(cmd, "at most 1000 curves a section"));
}
let profiles = if sec.get("profiles").is_some() { Some(profiles(sec, cmd)?) } else { None };
sections.push(solidcraft_doc::SheetSection { sketch, profiles, curves });
sections.push(solidcraft_doc::SheetSection { sketch: SketchId(sketch), profiles, curves });
}
add_feature(s, p, FeatureKind::SurfaceLoft { sections })
}
@@ -369,8 +392,8 @@ fn offset(s: &mut Session, p: &Value) -> Result<Value> {
check_expr(s, t, Kind::Length, cmd, "tolerance")?;
}
let kind = FeatureKind::SurfaceOffset {
faces,
body: str_(p, "body").map(str::to_string),
faces: faces.into_iter().map(FacePick).collect(),
body: str_(p, "body").map(BodyName::from),
distance,
chain: bool_(p, "chain").unwrap_or(false),
tolerance,
@@ -422,7 +445,17 @@ fn ruled(s: &mut Session, p: &Value) -> Result<Value> {
Some(v) => Some(vec3(v).ok_or_else(|| bad(cmd, "`face` must be [x, y, z] on a face"))?),
None => None,
};
let kind = FeatureKind::SurfaceRuled { body, edges, distance, mode, angle, direction, flip: bool_(p, "flip").unwrap_or(false), corners, face };
let kind = FeatureKind::SurfaceRuled {
body: BodyName(body),
edges: edges.into_iter().map(Position).collect(),
distance,
mode,
angle,
direction: direction.map(Dir),
flip: bool_(p, "flip").unwrap_or(false),
corners,
face: face.map(FacePick),
};
add_feature(s, p, kind)
}
@@ -443,7 +476,7 @@ fn stitch(s: &mut Session, p: &Value) -> Result<Value> {
let bodies = bodies_param(s, p, cmd)?;
let tolerance = expr(p, "tolerance").unwrap_or_else(|| "0".into());
check_expr(s, &tolerance, Kind::Length, cmd, "tolerance")?;
add_feature(s, p, FeatureKind::Stitch { bodies, tolerance })
add_feature(s, p, FeatureKind::Stitch { bodies: bodies.into_iter().map(BodyName).collect(), tolerance })
}
fn thicken(s: &mut Session, p: &Value) -> Result<Value> {
@@ -452,11 +485,11 @@ fn thicken(s: &mut Session, p: &Value) -> Result<Value> {
let thickness = expr(p, "thickness").ok_or_else(|| bad(cmd, "`thickness` is required"))?;
check_expr(s, &thickness, Kind::Length, cmd, "thickness")?;
let kind = FeatureKind::Thicken {
bodies,
bodies: bodies.into_iter().map(BodyName).collect(),
thickness,
symmetric: bool_(p, "symmetric").unwrap_or(false),
operation: operation(p, cmd)?,
targets: string_list(p, "targets"),
targets: string_list(p, "targets").into_iter().map(BodyName).collect(),
};
add_feature(s, p, kind)
}
@@ -486,7 +519,7 @@ fn extend(s: &mut Session, p: &Value) -> Result<Value> {
return Err(bad(cmd, format!("no body `{tb}`")));
}
let point = t.get("point").and_then(vec3).ok_or_else(|| bad(cmd, "`up_to.point` must be [x, y, z] on the target face"))?;
Some(ExtendTo::Face(FaceAt { body: tb, point }))
Some(ExtendTo::Face(FaceAt { body: BodyName(tb), point: Position(point) }))
}
Some(v) => Some(ExtendTo::Plane(plane_param(s, Some(v), cmd)?)),
};
@@ -500,7 +533,11 @@ fn extend(s: &mut Session, p: &Value) -> Result<Value> {
};
check_expr(s, &distance, Kind::Length, cmd, "distance")?;
let neighbours = bool_(p, "neighbours").unwrap_or(true);
add_feature(s, p, FeatureKind::SurfaceExtend { body, edges, distance, mode, up_to, neighbours })
add_feature(
s,
p,
FeatureKind::SurfaceExtend { body: BodyName(body), edges: edges.into_iter().map(Position).collect(), distance, mode, up_to, neighbours },
)
}
fn trim(s: &mut Session, p: &Value) -> Result<Value> {
@@ -514,11 +551,11 @@ fn trim(s: &mut Session, p: &Value) -> Result<Value> {
Some(t) => {
let tb = t.get("body").and_then(Value::as_str).ok_or_else(|| bad(cmd, "`tool.body` must name a body"))?.to_string();
let point = t.get("point").and_then(vec3).ok_or_else(|| bad(cmd, "`tool.point` must be [x, y, z] on the tool face"))?;
(PlaneRef::Origin { name: "XY".into() }, Some(FaceAt { body: tb, point }))
(PlaneRef::Origin { name: "XY".into() }, Some(FaceAt { body: BodyName(tb), point: Position(point) }))
}
None => (plane_param(s, p.get("plane"), cmd)?, None),
};
add_feature(s, p, FeatureKind::SurfaceTrim { body, plane, tool, keep })
add_feature(s, p, FeatureKind::SurfaceTrim { body: BodyName(body), plane, tool, keep: Position(keep) })
}
#[cfg(test)]