Feature fields: a type for each role (Expr, BodyName, FacePick…) (#1273)
Thanks @jijinbei.
This commit is contained in:
1 parent
3333f8a62b
commit
44968dbfe6
59 files changed
+1505
-1043
No files matched your search
@@ -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)]
|
||||
|
||||
Reference in new issue
Block a user