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

+12 -26
View File
@@ -10,16 +10,17 @@
//!
//! What a field is, is written once, on the field, with `#[walk(...)]`:
//!
//! - no attribute: the field's own type walks itself (`impl Walk`): a plane, a nested struct, a
//! list of them, or nothing for names, flags and numbers. A bare `Vec3` has no walk, so a point
//! that does not say its role does not compile;
//! - `length = label`, `angle = label`, `unitless = label`: an expression input (a `String`, an
//! optional one, a list or an array of them, or `(f64, String)` pairs). The label is any
//! - no attribute: the field's own type walks itself (`impl Walk`): a reference (`SketchId`,
//! `BodyName`, `FeatureName`), a point with its role (`FacePick`, `Position`, `Dir`), a plane, a
//! nested struct, a list of them, or nothing for names, flags and numbers. An `Expr` or a bare
//! `Vec3` has no walk, so an expression or a point that does not say what it is does not
//! compile;
//! - `length = label`, `angle = label`, `unitless = label`: an expression input (an `Expr`, an
//! optional one, a list or an array of them, or `(f64, Expr)` pairs). The label is any
//! expression: a `&'static str`, an array of them (one per element), or a closure
//! `|i: usize, n: usize| -> &'static str`; it may read the other fields;
//! - `sketch`, `feature`, `body`: a reference (an id, or a name, optional or a list);
//! - `pick`, `pinned`, `pos`, `dir`: a point (`Vec3`, optional or a list) with that role;
//! `pick_if = cond`: a pick when `cond` holds, else a position;
//! - `pinned`: a face pick that stays where it is when the feature moves;
//! `pick_if = cond`: a face pick when `cond` holds, else a position;
//! - `placement`: a primitive's placement point (its X, Y and Z expressions are not inputs);
//! - `profiles_of = sketch_field`, optionally with `when = cond`: a profile selection;
//! - `after = field`: visit this field right after `field` instead of in declaration order (where
@@ -60,8 +61,6 @@ struct Roles {
geo: Option<Geo>,
/// The kind (`Length`, `Angle`, `Unitless`) and the label.
expr: Option<(&'static str, Expr)>,
/// `Sketch`, `Feature` or `Body`.
reference: Option<&'static str>,
after: Option<Ident>,
/// The positions inside this (nested) value pick faces here.
picks_faces: bool,
@@ -70,7 +69,7 @@ struct Roles {
impl Roles {
/// No role given: the field's type walks itself.
fn nested(&self) -> bool {
self.geo.is_none() && self.expr.is_none() && self.reference.is_none()
self.geo.is_none() && self.expr.is_none()
}
}
@@ -82,10 +81,7 @@ fn roles(field: &syn::Field) -> syn::Result<Roles> {
attr.parse_nested_meta(|meta| {
let name = meta.path.get_ident().map(Ident::to_string).unwrap_or_default();
match name.as_str() {
"pick" => r.geo = Some(Geo::Points("Pick")),
"pinned" => r.geo = Some(Geo::Points("Pinned")),
"pos" => r.geo = Some(Geo::Points("Pos")),
"dir" => r.geo = Some(Geo::Points("Dir")),
"placement" => r.geo = Some(Geo::Placement),
"pick_if" => r.geo = Some(Geo::PickIf(meta.value()?.parse()?)),
"profiles_of" => sketch = Some(meta.value()?.parse::<Ident>()?),
@@ -93,9 +89,6 @@ fn roles(field: &syn::Field) -> syn::Result<Roles> {
"length" => r.expr = Some(("Length", meta.value()?.parse()?)),
"angle" => r.expr = Some(("Angle", meta.value()?.parse()?)),
"unitless" => r.expr = Some(("Unitless", meta.value()?.parse()?)),
"sketch" => r.reference = Some("Sketch"),
"feature" => r.reference = Some("Feature"),
"body" => r.reference = Some("Body"),
"after" => r.after = Some(meta.value()?.parse()?),
"picks_faces" => r.picks_faces = true,
_ => return Err(meta.error("unknown walk role")),
@@ -157,14 +150,7 @@ fn walk_field(b: &Ident, r: &Roles, which: Which, mutable: bool) -> Tokens {
_ => quote!(),
}
}
Which::Refs => match r.reference {
Some(kind) => {
let t = if kind == "Sketch" { quote!(u64) } else { quote!(String) };
let kind = Ident::new(kind, Span::call_site());
quote! { #w::Each::<#t>::#each(#b, &mut |x| f(#w::Ref::#kind(x))); }
}
None => quote!(),
},
Which::Refs => quote!(),
Which::Geometry => match &r.geo {
Some(Geo::Points(v)) => {
let v = Ident::new(v, Span::call_site());
@@ -209,7 +195,7 @@ fn pre_values(b: &Ident, r: &Roles, which: Which) -> Tokens {
quote! {
let #wh: bool = #when;
let mut #id: Option<u64> = None;
#w::Each::<u64>::each(&*#sketch, &mut |s| #id = Some(*s));
#w::Each::<crate::SketchId>::each(&*#sketch, &mut |s| #id = Some(s.0));
}
}
_ => quote!(),
+2 -2
View File
@@ -176,7 +176,7 @@ fn map_geo_ref(m: &Mat, g: &mut crate::construct::GeoRef) {
/// Map a point given as expressions by `m`: numbers stay numbers; a coordinate that is an
/// expression stays one (`m · (x, y, z) + t`, written out), so it still follows its parameters.
pub fn map_point_expr(m: &Mat, p: &mut crate::PointExpr) {
let num = |e: &String| e.trim().parse::<f64>().ok().filter(|v| v.is_finite());
let num = |e: &crate::Expr| e.trim().parse::<f64>().ok().filter(|v| v.is_finite());
if let [Some(x), Some(y), Some(z)] = [num(&p[0]), num(&p[1]), num(&p[2])] {
*p = crate::point_expr(apply_point(m, Vec3::new(x, y, z)));
return;
@@ -202,7 +202,7 @@ pub fn map_point_expr(m: &Mat, p: &mut crate::PointExpr) {
} else if constant.abs() >= 1e-12 {
e = format!("{e} {} {} mm", if constant < 0.0 { '-' } else { '+' }, constant.abs());
}
*out = e;
*out = e.into();
}
}
+7 -1
View File
@@ -78,7 +78,13 @@ mod tests {
(vec!["bad\nname".into()], 0, crate::IDENTITY),
(vec!["Block".into()], 0, scaled),
] {
assert!(doc.add_feature(crate::FeatureKind::BodyTransfer { bodies, from, to: 0, transform }, None).is_err());
assert!(
doc.add_feature(
crate::FeatureKind::BodyTransfer { bodies: bodies.into_iter().map(crate::BodyName).collect(), from, to: 0, transform },
None
)
.is_err()
);
assert_eq!(doc.to_json(), before);
}
}
+3 -3
View File
@@ -114,7 +114,7 @@ fn remap_plane(
}
match p {
PlaneRef::Offset { base, .. } | PlaneRef::AtAngle { base, .. } => remap_plane(base, ids, names, bodies, depth + 1)?,
PlaneRef::Construction { name } => mapped_name(name, names),
PlaneRef::Construction { name } => mapped_name(&mut name.0, names),
PlaneRef::Face { name, anchor, .. } => {
if let Some(name) = name {
*name = remap_topology(name, ids, bodies);
@@ -221,10 +221,10 @@ impl crate::Feature {
*n = remap_topology(n, ids, bodies);
}
for (_, e, _) in self.kind.inputs_mut() {
*e = clone_expression(e, parameters);
*e = clone_expression(e, parameters).into();
}
for e in self.kind.placement_exprs_mut() {
*e = clone_expression(e, parameters);
*e = clone_expression(e, parameters).into();
}
self.kind.remap_bodies(bodies);
match &mut self.kind {
+6 -6
View File
@@ -17,7 +17,7 @@ fn shell_document() -> (Document, u64, u64) {
let shell = doc
.add_feature(
FeatureKind::Shell {
faces: vec![Vec3::new(5., 6., 10.)],
faces: vec![crate::FacePick(Vec3::new(5., 6., 10.))],
thickness: "wall".into(),
body: Some("Case".into()),
direction: "inside".into(),
@@ -119,7 +119,7 @@ fn shell_planar_anchor_rebuild_cold_geometry_and_missing_source_errors() {
.add_feature(
FeatureKind::Extrude {
thin: None,
sketch: sk,
sketch: crate::SketchId(sk),
profiles: crate::ProfileSel::All,
extent: serde_json::from_value(json!({"distance":"2"})).unwrap(),
operation: crate::Operation::NewBody,
@@ -282,7 +282,7 @@ fn saved_former_strip_projection_reports_lost_without_expanding_healthy_material
.add_feature(
FeatureKind::Extrude {
thin: None,
sketch: sk,
sketch: crate::SketchId(sk),
profiles: crate::ProfileSel::All,
extent: serde_json::from_value(json!({"distance":"1"})).unwrap(),
operation: crate::Operation::NewBody,
@@ -344,7 +344,7 @@ fn saved_curved_strip_projection_is_lost_after_exact_wall_coalescence() {
let shell = doc
.add_feature(
FeatureKind::Shell {
faces: vec![Vec3::new(0., 0., 30.)],
faces: vec![crate::FacePick(Vec3::new(0., 0., 30.))],
thickness: "wall".into(),
body: Some("Case".into()),
direction: "inside".into(),
@@ -432,7 +432,7 @@ fn saved_curved_strip_projection_is_lost_after_exact_wall_coalescence() {
.add_feature(
FeatureKind::Extrude {
thin: None,
sketch: sk,
sketch: crate::SketchId(sk),
profiles: crate::ProfileSel::All,
extent: serde_json::from_value(json!({"distance":"1"})).unwrap(),
operation: crate::Operation::NewBody,
@@ -557,7 +557,7 @@ fn saved_stepped_tab_cut_piece_projection_is_lost_without_expanding_material() {
.add_feature(
FeatureKind::Extrude {
thin: None,
sketch: sk,
sketch: crate::SketchId(sk),
profiles: crate::ProfileSel::All,
extent: serde_json::from_value(json!({"distance":"1"})).unwrap(),
operation: crate::Operation::NewBody,
+1 -1
View File
@@ -82,7 +82,7 @@ pub enum PointDef {
/// Where an edge (line) crosses a plane.
EdgeAndPlane { edge: GeoRef, plane: GeoRef },
/// At a fraction `t` (0…1) of the way along an edge or sketch curve.
AlongPath { path: GeoRef, t: String },
AlongPath { path: GeoRef, t: crate::Expr },
}
/// Resolved construction geometry, kept in the model state by feature.
+270 -406
View File
File diff suppressed because it is too large. Load diff
+96 -92
View File
@@ -951,15 +951,15 @@ fn placement_digest(doc: &Document, f: &Feature) -> u64 {
_ => None,
};
if let Some(path) = path
&& doc.sketch(path).is_ok_and(|sketch| !sketch.wires.is_empty())
&& doc.sketch(path.0).is_ok_and(|sketch| !sketch.wires.is_empty())
{
let mut hash = std::collections::hash_map::DefaultHasher::new();
let owner = doc.feature(path).map(|feature| feature.component).unwrap_or(0);
let owner = doc.feature(path.0).map(|feature| feature.component).unwrap_or(0);
for frame in [doc.component_transform(f.component), doc.component_transform(owner)] {
frame.iter().flatten().for_each(|value| value.to_bits().hash(&mut hash));
}
if let FeatureKind::Sweep { sketch, .. } | FeatureKind::SurfaceSweep { sketch, .. } = &f.kind {
let owner = doc.feature(*sketch).map(|feature| feature.component).unwrap_or(0);
let owner = doc.feature(sketch.0).map(|feature| feature.component).unwrap_or(0);
doc.component_transform(owner).iter().flatten().for_each(|value| value.to_bits().hash(&mut hash));
}
return hash.finish();
@@ -1951,7 +1951,7 @@ fn face_level_at(st: &ModelState, plane: &Plane, h: f64, targets: &[String]) ->
/// The body a face-based feature applies to: by name, or the one whose surface is nearest the
/// first point.
fn body_at(st: &ModelState, body: &Option<String>, points: &[Vec3]) -> Result<usize> {
fn body_at(st: &ModelState, body: &Option<crate::BodyName>, points: &[Vec3]) -> Result<usize> {
if let Some(n) = body {
return st.bodies.iter().position(|b| &b.name == n).ok_or_else(|| DocError::Unknown(format!("body `{n}`")));
}
@@ -2085,7 +2085,7 @@ fn edge_dir(e: &kernel::EdgeInfo) -> Vec3 {
}
/// References for edges picked at `pts` on the body a blend would go on (the state before it).
pub fn edge_refs(st: &ModelState, body: &Option<String>, pts: &[Vec3]) -> Vec<EdgeRef> {
pub fn edge_refs(st: &ModelState, body: &Option<crate::BodyName>, pts: &[Vec3]) -> Vec<EdgeRef> {
let Ok(ti) = blend_target(st, body, pts) else { return Vec::new() };
let Some(mb) = st.bodies.get(ti) else { return Vec::new() };
let size = mb.body.size();
@@ -2103,7 +2103,7 @@ pub fn edge_refs(st: &ModelState, body: &Option<String>, pts: &[Vec3]) -> Vec<Ed
}
/// Persistent names of the edges picked at `pts` on the body a blend would go on.
pub fn edge_names_for(st: &ModelState, body: &Option<String>, pts: &[Vec3]) -> Vec<String> {
pub fn edge_names_for(st: &ModelState, body: &Option<crate::BodyName>, pts: &[Vec3]) -> Vec<String> {
match blend_target(st, body, pts).ok().and_then(|i| st.bodies.get(i)) {
Some(b) => crate::naming::edge_names_at(b, pts),
None => Vec::new(),
@@ -2190,7 +2190,7 @@ fn resolve_edges(b: &Body, pts: &[Vec3], refs: &[EdgeRef], warning: &mut Option<
}
/// The body a fillet/chamfer applies to: by name, or the one closest to the first edge point.
fn blend_target(state: &ModelState, body: &Option<String>, edges: &[Vec3]) -> Result<usize> {
fn blend_target(state: &ModelState, body: &Option<crate::BodyName>, edges: &[Vec3]) -> Result<usize> {
if let Some(n) = body {
return state.bodies.iter().position(|b| &b.name == n).ok_or_else(|| DocError::Unknown(format!("body `{n}`")));
}
@@ -2246,10 +2246,10 @@ pub(crate) fn revolve_axis(ss: &SolvedSketch, axis: &AxisRef) -> Result<(Vec2, V
AxisRef::Line { origin, dir } => {
let pl = &ss.plane;
let d = dir.normalized().ok_or_else(|| DocError::Invalid("revolve axis has no direction".into()))?;
if d.dot(pl.normal()).abs() > 1e-9 || pl.height(*origin).abs() > 1e-6 {
if d.dot(pl.normal()).abs() > 1e-9 || pl.height(origin.0).abs() > 1e-6 {
return Err(DocError::Invalid("the revolve axis does not lie in the sketch plane".into()));
}
Ok((pl.to_local(*origin), Vec2::new(d.dot(pl.x), d.dot(pl.y))))
Ok((pl.to_local(origin.0), Vec2::new(d.dot(pl.x), d.dot(pl.y))))
}
}
}
@@ -2303,7 +2303,8 @@ fn hole_spots(st: &ModelState, f: &Feature) -> Result<(Vec<Vec3>, Vec3)> {
let FeatureKind::Hole { position, direction, points, .. } = &f.kind else { return Err(DocError::Invalid("not a hole".into())) };
let (spots, dir) = match points {
Some(sp) => {
let ss = st.sketch(sp.sketch).ok_or_else(|| DocError::Unknown(format!("sketch {} (it must come earlier in the timeline)", sp.sketch)))?;
let ss =
st.sketch(sp.sketch.0).ok_or_else(|| DocError::Unknown(format!("sketch {} (it must come earlier in the timeline)", sp.sketch)))?;
let mut v = Vec::new();
for id in &sp.ids {
let p = ss.sketch.points.iter().find(|p| &p.id == id).ok_or_else(|| DocError::Unknown(format!("sketch point `{id}`")))?;
@@ -2315,7 +2316,7 @@ fn hole_spots(st: &ModelState, f: &Feature) -> Result<(Vec<Vec3>, Vec3)> {
}
None => {
let dir = direction.normalized().ok_or_else(|| DocError::Invalid("hole direction".into()))?;
let at = hole_on_surface(st, *position, dir);
let at = hole_on_surface(st, position.0, dir);
let dir = hole_dir_into_face(st, at, dir);
return Ok((vec![hole_on_surface(st, at, dir)], dir));
}
@@ -2328,7 +2329,7 @@ fn hole_spots(st: &ModelState, f: &Feature) -> Result<(Vec<Vec3>, Vec3)> {
/// A hole's depth: down to the To Object (where the hole's axis meets the face there, else the
/// point's depth along the axis), else as given.
fn hole_depth_to(st: &ModelState, at: &Vec3, dir: Vec3, to: &Option<Vec3>, depth: &Option<String>) -> Result<Option<String>> {
fn hole_depth_to(st: &ModelState, at: &Vec3, dir: Vec3, to: &Option<Vec3>, depth: &Option<crate::Expr>) -> Result<Option<crate::Expr>> {
let Some(t) = to else { return Ok(depth.clone()) };
let mut d = (*t - *at).dot(dir);
// The face at the To point, met along the axis.
@@ -2345,7 +2346,7 @@ fn hole_depth_to(st: &ModelState, at: &Vec3, dir: Vec3, to: &Option<Vec3>, depth
if !(d > 1e-6) {
return Err(DocError::Invalid("the hole's To object is not ahead of the hole".into()));
}
Ok(Some(format!("{d} mm")))
Ok(Some(format!("{d} mm").into()))
}
/// A hole's spots as given (not yet moved onto the surface) and its direction.
@@ -2353,7 +2354,7 @@ fn hole_spots_raw(st: &ModelState, f: &Feature) -> Result<(Vec<Vec3>, Vec3)> {
let FeatureKind::Hole { position, direction, points, .. } = &f.kind else { return Err(DocError::Invalid("not a hole".into())) };
match points {
Some(sp) => {
let ss = st.sketch(sp.sketch).ok_or_else(|| DocError::Unknown(format!("sketch {}", sp.sketch)))?;
let ss = st.sketch(sp.sketch.0).ok_or_else(|| DocError::Unknown(format!("sketch {}", sp.sketch)))?;
let v: Vec<Vec3> =
sp.ids.iter().filter_map(|id| ss.sketch.points.iter().find(|p| &p.id == id)).map(|p| ss.plane.to_world(p.pos)).take(10_000).collect();
let dir = sketch_hole_dir(st, &v, ss.plane.normal());
@@ -2362,7 +2363,7 @@ fn hole_spots_raw(st: &ModelState, f: &Feature) -> Result<(Vec<Vec3>, Vec3)> {
}
None => {
let dir = direction.normalized().ok_or_else(|| DocError::Invalid("hole direction".into()))?;
Ok((vec![*position], hole_dir_into_face(st, hole_on_surface(st, *position, dir), dir)))
Ok((vec![position.0], hole_dir_into_face(st, hole_on_surface(st, position.0, dir), dir)))
}
}
}
@@ -2377,7 +2378,7 @@ fn hole_feature_tools(vals: &BTreeMap<String, Value>, st: &ModelState, f: &Featu
}
let mut tools = Vec::new();
for at in spots {
let depth = hole_depth_to(st, &at, dir, to, depth)?;
let depth = hole_depth_to(st, &at, dir, &to.map(Vec3::from), depth)?;
tools.extend(hole_tools_and_prisms(vals, st, &at, dir, diameter, &depth, hole)?);
}
Ok(tools)
@@ -2454,7 +2455,7 @@ fn hole_tools(
position: &Vec3,
dir: Vec3,
diameter: &str,
depth: &Option<String>,
depth: &Option<crate::Expr>,
hole: &HoleKind,
) -> Result<Vec<Traced>> {
Ok(hole_tools_and_prisms(vals, st, position, dir, diameter, depth, hole)?.into_iter().map(|(tool, _)| tool).collect())
@@ -2471,7 +2472,7 @@ fn hole_tools_and_prisms(
position: &Vec3,
dir: Vec3,
diameter: &str,
depth: &Option<String>,
depth: &Option<crate::Expr>,
hole: &HoleKind,
) -> Result<Vec<(Traced, Option<more::PrimitivePrism>)>> {
let r = val(vals, diameter, Kind::Length)? / 2.0;
@@ -2746,7 +2747,7 @@ fn feature_tools_with_constructor(
FeatureKind::Extrude { sketch, profiles, extent, operation, targets, thin } => {
let ss = sketch_in_feature_frame(
doc,
st.sketch(*sketch).ok_or_else(|| DocError::Unknown(format!("sketch {sketch} (it must come earlier in the timeline)")))?,
st.sketch(sketch.0).ok_or_else(|| DocError::Unknown(format!("sketch {sketch} (it must come earlier in the timeline)")))?,
f.component,
)?;
let regions = match thin {
@@ -2762,14 +2763,14 @@ fn feature_tools_with_constructor(
// The face names follow `face_points`: To, then From.
let name = |k: usize| f.face_names.get(k).map(String::as_str);
let (to_name, from_name) = if extent.to.is_some() { (name(0), name(1)) } else { (None, name(0)) };
let to_face = extent.to.and_then(|p| object_face(&tst, to_name, p));
let to_face = extent.to.and_then(|p| object_face(&tst, to_name, p.0));
let centre = {
let pts: Vec<Vec2> = regions.iter().flat_map(|r| r.outer.polyline(1e-2)).collect();
let c = pts.iter().fold(Vec2::default(), |a, p| a + *p) * (1.0 / pts.len().max(1) as f64);
ss.plane.to_world(c)
};
let from = match extent.from {
Some(p) => height_at(&tst, object_face(&tst, from_name, p), &ss.plane, centre, p),
Some(p) => height_at(&tst, object_face(&tst, from_name, p.0), &ss.plane, centre, p.0),
None => 0.0,
};
let d = match extent.to {
@@ -2778,7 +2779,7 @@ fn feature_tools_with_constructor(
Some(e) => val(vals, e, Kind::Length)?,
None => 0.0,
};
let h = height_at(&tst, to_face, &ss.plane, centre, p);
let h = height_at(&tst, to_face, &ss.plane, centre, p.0);
let start = from + extent.start_offset.as_deref().map(|e| val(vals, e, Kind::Length)).transpose()?.unwrap_or(0.0);
// Past the face by the offset, in the direction travelled.
let to = h + if h >= start { off } else { -off };
@@ -2787,7 +2788,7 @@ fn feature_tools_with_constructor(
}
to - start
}
None if extent.through_all => through_all_distance(st, &ss.plane, targets)?,
None if extent.through_all => through_all_distance(st, &ss.plane, &crate::plain(targets))?,
None => val(vals, &extent.distance, Kind::Length)?,
};
if let Some(tp) = &extent.taper {
@@ -2805,7 +2806,7 @@ fn feature_tools_with_constructor(
// How far a tool grows past its profile at either end, at most.
let mut growth: f64 = 0.0;
// A face of the targets in the profile's plane (the same for every profile).
let on_face = matches!(operation, Operation::Join | Operation::Cut) && face_level_at(st, &ss.plane, from, targets);
let on_face = matches!(operation, Operation::Join | Operation::Cut) && face_level_at(st, &ss.plane, from, &crate::plain(targets));
let tools: Vec<Traced> = regions
.iter()
.map(|r| {
@@ -2814,7 +2815,7 @@ fn feature_tools_with_constructor(
// and the boolean finds no result there. The sides keep their slope, so
// the profile stays the section at the start.
let (lo, hi) = if sign < 0.0 { (from - len, from) } else { (from, from + len) };
let (l2, h2) = extend_for_coplanar(st, &ss.plane, r, lo, hi, *operation, targets);
let (l2, h2) = extend_for_coplanar(st, &ss.plane, r, lo, hi, *operation, &crate::plain(targets));
let (a, b) = if sign < 0.0 { (from - h2, from - l2) } else { (l2 - from, h2 - from) };
// A start that can't run on (the profile hangs over the face's edge):
// the sides stand straight for a hair first, so the boolean can push the
@@ -2882,7 +2883,7 @@ fn feature_tools_with_constructor(
if exact.iter().all(|tool| {
st.bodies
.iter()
.filter(|b| targets.is_empty() || targets.contains(&b.name))
.filter(|b| targets.is_empty() || targets.iter().any(|t| *t == b.name))
.any(|b| kernel::can_join_touching_faces(&b.body, &tool.body))
}) {
// Preserve a whole touching face: extending into a chamfer replaces
@@ -2893,11 +2894,12 @@ fn feature_tools_with_constructor(
continue;
}
}
let (l2, h2) = extend_for_coplanar(st, &ss.plane, r, lo + off, hi + off, *operation, targets);
let (l2, h2) = extend_for_coplanar(st, &ss.plane, r, lo + off, hi + off, *operation, &crate::plain(targets));
let probe = |p: Vec2| (1..4).map(|k| ss.plane.to_world(p) + n * (lo + off + (hi - lo) * k as f64 / 4.0)).collect::<Vec<_>>();
let original = r;
let r = grow_profile_for_coplanar(st, r, *operation, targets, &probe);
let r = fill_void_holes(st, &r, *operation, targets, &ss.plane, (l2, h2));
let plain_targets = crate::plain(targets);
let r = grow_profile_for_coplanar(st, r, *operation, &plain_targets, &probe);
let r = fill_void_holes(st, &r, *operation, &plain_targets, &ss.plane, (l2, h2));
untrimmed &= l2 == lo + off && h2 == hi + off && &r == original;
tools.extend(centered_extrude(&ss.plane, &r, l2, h2, Some(&ss))?);
footprints.push(r);
@@ -2973,7 +2975,7 @@ fn feature_tools_with_constructor(
}
}
// Certificates set face sources only: the faces, and their lineage, stay.
let body = body.with_extrude_certificates(f.id, *sketch, &curves)?.with_extrude_cap_certificates(f.id, *sketch, &caps)?;
let body = body.with_extrude_certificates(f.id, sketch.0, &curves)?.with_extrude_cap_certificates(f.id, sketch.0, &caps)?;
Ok(Traced { body, lineage })
})
.collect::<Result<Vec<_>>>()?;
@@ -2983,7 +2985,7 @@ fn feature_tools_with_constructor(
FeatureKind::Revolve { sketch, profiles, axis, angle, angle2, symmetric, convention, to, .. } => {
let ss = sketch_in_feature_frame(
doc,
st.sketch(*sketch).ok_or_else(|| DocError::Unknown(format!("sketch {sketch} (it must come earlier in the timeline)")))?,
st.sketch(sketch.0).ok_or_else(|| DocError::Unknown(format!("sketch {sketch} (it must come earlier in the timeline)")))?,
f.component,
)?;
let regions = solidcraft_sketch::merge_regions(&select_profiles(&ss, profiles)?);
@@ -3002,7 +3004,7 @@ fn feature_tools_with_constructor(
(ss.plane.to_world(o), ss.plane.dir_to_world(d)),
angles,
*symmetric,
*to,
to.map(Vec3::from),
*convention,
)?;
let (start, ang) = (extent.start, extent.sweep);
@@ -3017,7 +3019,7 @@ fn feature_tools_with_constructor(
(ss.plane.to_world(o), ss.plane.dir_to_world(d)),
angles,
*symmetric,
*to,
to.map(Vec3::from),
*convention,
)
} else {
@@ -3052,7 +3054,7 @@ fn feature_tools_with_constructor(
.collect::<Result<Vec<_>>>()?;
// To a curved face: turn past the whole target body, then cut back to its surface
// (the piece that starts at the profile).
let curved = curved_face(&tst, to.and_then(|p| object_face(&tst, f.face_names.first().map(String::as_str), p)), None);
let curved = curved_face(&tst, to.and_then(|p| object_face(&tst, f.face_names.first().map(String::as_str), p.0)), None);
let curved_body = curved.and_then(|bi| tst.bodies.get(bi));
// Each region's body: its sides named by the sketch curves, its caps start and end.
let tool = |r: &Region2, body: Body, built: Option<&Body>| crate::lineage::ProfileRoles::swept(Some(&ss), Some(r)).tool(body, built);
@@ -3114,11 +3116,11 @@ fn feature_tools_with_constructor(
// A profile lofted to a sketch point (an apex).
let [a, b] = &sections[..] else { return Err(DocError::Invalid("not supported yet: a point section in a loft of more than two".into())) };
let (prof, pt) = if a.point.is_some() { (b, a) } else { (a, b) };
let ss = st.sketch(prof.sketch).ok_or_else(|| DocError::Unknown(format!("sketch {}", prof.sketch)))?;
let ss = st.sketch(prof.sketch.0).ok_or_else(|| DocError::Unknown(format!("sketch {}", prof.sketch)))?;
let regions = solidcraft_sketch::merge_regions(&select_profiles(ss, &prof.profiles)?);
let [r] = &regions[..] else { return Err(DocError::Invalid("each loft section must be one profile".into())) };
refuse_loft_holes(r)?;
let ps = st.sketch(pt.sketch).ok_or_else(|| DocError::Unknown(format!("sketch {}", pt.sketch)))?;
let ps = st.sketch(pt.sketch.0).ok_or_else(|| DocError::Unknown(format!("sketch {}", pt.sketch)))?;
let id = pt.point.as_deref().unwrap_or_default();
let q = ps.sketch.points.iter().find(|p| p.id == id).ok_or_else(|| DocError::Unknown(format!("point `{id}` in {}", ps.name)))?;
let body = kernel::loft_to_point(&ss.plane, &r.outer, ps.plane.to_world(q.pos))?;
@@ -3169,7 +3171,7 @@ fn feature_tools_with_constructor(
let mut sketches = Vec::new();
for sec in sections {
let ss = st
.sketch(sec.sketch)
.sketch(sec.sketch.0)
.ok_or_else(|| DocError::Unknown(format!("sketch {} (it must come earlier in the timeline)", sec.sketch)))?;
let regions = solidcraft_sketch::merge_regions(&select_profiles(ss, &sec.profiles)?);
let [r] = &regions[..] else { return Err(DocError::Invalid("each loft section must be one profile".into())) };
@@ -3185,9 +3187,9 @@ fn feature_tools_with_constructor(
}
FeatureKind::Sweep { sketch, profiles, path_sketch, path, orientation, twist, scale, .. } => {
let ctl = sweep_control(vals, orientation, twist, scale)?;
let ss = st.sketch(*sketch).ok_or_else(|| DocError::Unknown(format!("sketch {sketch} (it must come earlier in the timeline)")))?;
let ss = st.sketch(sketch.0).ok_or_else(|| DocError::Unknown(format!("sketch {sketch} (it must come earlier in the timeline)")))?;
let ps =
st.sketch(*path_sketch).ok_or_else(|| DocError::Unknown(format!("sketch {path_sketch} (it must come earlier in the timeline)")))?;
st.sketch(path_sketch.0).ok_or_else(|| DocError::Unknown(format!("sketch {path_sketch} (it must come earlier in the timeline)")))?;
let profile;
let ss = if !ps.sketch.wires.is_empty() {
profile = profile_in_frame(doc, ss, f.component)?;
@@ -3209,7 +3211,7 @@ fn feature_tools_with_constructor(
}
FeatureKind::Pipe { path_sketch, path, diameter, wall, .. } => {
let ps =
st.sketch(*path_sketch).ok_or_else(|| DocError::Unknown(format!("sketch {path_sketch} (it must come earlier in the timeline)")))?;
st.sketch(path_sketch.0).ok_or_else(|| DocError::Unknown(format!("sketch {path_sketch} (it must come earlier in the timeline)")))?;
let mapped = path_in_frame(doc, ps, f.component)?;
let ps = mapped.as_ref();
let r = val(vals, diameter, Kind::Length)? / 2.0;
@@ -3239,7 +3241,7 @@ fn feature_tools_with_constructor(
}
FeatureKind::Cylinder { base, axis, radius, height, .. } => Ok(vec![crate::lineage::cylinder_tool(kernel::cylinder(
point(vals, base)?,
*axis,
axis.0,
val(vals, radius, Kind::Length)?,
val(vals, height, Kind::Length)?,
)?)]),
@@ -3258,13 +3260,13 @@ fn feature_tools_with_constructor(
pub(crate) fn sweep_control(
vals: &BTreeMap<String, Value>,
orientation: &crate::SweepOrientation,
twist: &Option<String>,
scale: &Option<String>,
twist: &Option<crate::Expr>,
scale: &Option<crate::Expr>,
) -> Result<kernel::SweepControl> {
let orientation = match *orientation {
crate::SweepOrientation::Perpendicular => kernel::SweepOrientation::Perpendicular,
crate::SweepOrientation::Parallel => kernel::SweepOrientation::Parallel,
crate::SweepOrientation::Direction(d) => kernel::SweepOrientation::Direction(d),
crate::SweepOrientation::Direction(d) => kernel::SweepOrientation::Direction(d.0),
};
let twist = match twist {
Some(t) => val(vals, t, Kind::Angle)?,
@@ -3280,7 +3282,7 @@ pub(crate) fn sweep_control(
}
/// A pipe's section: a circle of radius `r`, hollow when it has a wall.
fn pipe_region(vals: &BTreeMap<String, Value>, r: f64, wall: &Option<String>) -> Result<Region2> {
fn pipe_region(vals: &BTreeMap<String, Value>, r: f64, wall: &Option<crate::Expr>) -> Result<Region2> {
let holes = match wall {
Some(w) => {
let wt = val(vals, w, Kind::Length)?;
@@ -3485,15 +3487,15 @@ pub(crate) fn extent_sides(vals: &BTreeMap<String, Value>, extent: &crate::Exten
/// Operation and targets of a feature that makes tools.
fn feature_op(f: &Feature) -> (Operation, Vec<String>) {
match &f.kind {
FeatureKind::Extrude { operation, targets, .. } | FeatureKind::Revolve { operation, targets, .. } => (*operation, targets.clone()),
FeatureKind::Extrude { operation, targets, .. } | FeatureKind::Revolve { operation, targets, .. } => (*operation, crate::plain(targets)),
FeatureKind::Box { operation, .. }
| FeatureKind::Cylinder { operation, .. }
| FeatureKind::Sphere { operation, .. }
| FeatureKind::Torus { operation, .. } => (*operation, Vec::new()),
FeatureKind::Hole { .. } => (Operation::Cut, Vec::new()),
FeatureKind::Pipe { operation, targets, .. } => (*operation, targets.clone()),
FeatureKind::Loft { operation, targets, .. } | FeatureKind::Sweep { operation, targets, .. } => (*operation, targets.clone()),
FeatureKind::Coil { operation, targets, .. } => (*operation, targets.clone()),
FeatureKind::Pipe { operation, targets, .. } => (*operation, crate::plain(targets)),
FeatureKind::Loft { operation, targets, .. } | FeatureKind::Sweep { operation, targets, .. } => (*operation, crate::plain(targets)),
FeatureKind::Coil { operation, targets, .. } => (*operation, crate::plain(targets)),
_ => (Operation::NewBody, Vec::new()),
}
}
@@ -3583,7 +3585,7 @@ fn pattern_transforms(doc: &Document, vals: &BTreeMap<String, Value>, st: &Model
crate::PatternKind::Path { path_sketch, path, count: c, spacing, extent, flip, orient } => {
let n = count(c)?;
let d = val(vals, spacing, Kind::Length)?;
let ps = st.sketch(*path_sketch).ok_or_else(|| DocError::Unknown(format!("path sketch {path_sketch}")))?;
let ps = st.sketch(path_sketch.0).ok_or_else(|| DocError::Unknown(format!("path sketch {path_sketch}")))?;
let mapped = path_in_frame(doc, ps, component)?;
let ps = mapped.as_ref();
let mut segs = path_segments(ps, path, Vec3::new(f64::NAN, f64::NAN, f64::NAN))?;
@@ -3625,7 +3627,7 @@ fn pattern_transforms(doc: &Document, vals: &BTreeMap<String, Value>, st: &Model
0.0
};
for i in 1..n {
out.push(rotation(*origin, *axis, step * i as f64));
out.push(rotation(origin.0, axis.0, step * i as f64));
}
}
}
@@ -3722,7 +3724,7 @@ fn replay(
let r = val(vals, radius, Kind::Length)?;
// The edges the feature blended in this evaluation (found by name), falling back to
// its picked points.
let found = st.blend_edges.get(&src.id).cloned().unwrap_or_else(|| edges.clone());
let found = st.blend_edges.get(&src.id).cloned().unwrap_or_else(|| crate::plain(edges));
for m in mats {
let moved: Vec<Vec3> = found.iter().map(|p| crate::apply_point(m, *p)).collect();
let ti = blend_target(st, &None, &moved)?;
@@ -3757,7 +3759,7 @@ fn replay(
let d = crate::apply_vector(m, dir).normalized().ok_or_else(|| DocError::Invalid("hole direction".into()))?;
for p in &spots {
let at = hole_on_surface(st, crate::apply_point(m, *p), d);
let to = to.map(|t| crate::apply_point(m, t));
let to = to.map(|t| crate::apply_point(m, t.0));
let depth = hole_depth_to(st, &at, d, &to, depth)?;
// Each copy's faces are named as the hole's, with the instance.
copies.extend(
@@ -4085,8 +4087,8 @@ fn chamfer_with(
b: &kernel::Body,
edges: &[Vec3],
d: f64,
distance2: &Option<String>,
angle: &Option<String>,
distance2: &Option<crate::Expr>,
angle: &Option<crate::Expr>,
flip: bool,
) -> Result<kernel::Body> {
let side = match (distance2, angle) {
@@ -4102,7 +4104,9 @@ fn chamfer_with(
fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st: &mut ModelState, warning: &mut Option<String>) -> Result<()> {
match &f.kind {
FeatureKind::BodyTransfer { bodies, from, to, transform } => crate::body_transfer::evaluate(doc, st, bodies, *from, *to, *transform),
FeatureKind::BodyTransfer { bodies, from, to, transform } => {
crate::body_transfer::evaluate(doc, st, &crate::plain(bodies), *from, *to, *transform)
}
FeatureKind::SheetBase { .. }
| FeatureKind::SheetContour { .. }
| FeatureKind::SheetFlange { .. }
@@ -4189,12 +4193,12 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
// Cuts through sheet metal become cut-outs of its flat pattern.
if matches!(operation, Operation::Cut | Operation::Intersect) && matches!(f.kind, FeatureKind::Extrude { .. }) && !st.sheets.is_empty() {
let bodies: Vec<Body> = tools.iter().map(|t| t.body.clone()).collect();
let handled = more::sheet_cut(f, st, &bodies, targets, constructor.as_ref(), warning)?;
let handled = more::sheet_cut(f, st, &bodies, &crate::plain(targets), constructor.as_ref(), warning)?;
if !handled.is_empty() {
return apply_beside_sheet_cut(st, f, tools, *operation, targets, &handled);
return apply_beside_sheet_cut(st, f, tools, *operation, &crate::plain(targets), &handled);
}
}
apply_traced(st, f, tools, *operation, targets)
apply_traced(st, f, tools, *operation, &crate::plain(targets))
}
FeatureKind::Hole { thread, diameter, .. } => {
let (spots, dir) = hole_spots(st, f)?;
@@ -4222,14 +4226,14 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
Some(e) => Some(val(vals, e, Kind::Length)?),
None => None,
};
let t = thread_on(st, f.id, *face, designation.as_deref(), l)?;
let t = thread_on(st, f.id, face.0, designation.as_deref(), l)?;
if !*modeled {
st.threads.push(t);
return Ok(());
}
let i = st.bodies.iter().position(|b| b.name == t.body).ok_or_else(|| DocError::Invalid("threaded body".into()))?;
let Some(mb) = st.bodies.get(i).cloned() else { return Err(DocError::Invalid("body".into())) };
let cyl = kernel::cylinder_face_at(&mb.body, *face).ok_or_else(|| DocError::Invalid("no cylindrical face there to thread".into()))?;
let cyl = kernel::cylinder_face_at(&mb.body, face.0).ok_or_else(|| DocError::Invalid("no cylindrical face there to thread".into()))?;
let tol = t.pitch * 1e-3;
let groove = thread_groove(&t, cyl.radius, ((t.start - cyl.start).abs() < tol, (t.end - cyl.end).abs() < tol))?;
// The B-rep boolean can't take helical walls in reasonable time: the body is cut
@@ -4247,9 +4251,9 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
}
FeatureKind::Shell { faces, thickness, body, direction, tangent_chain } => {
let t = val(vals, thickness, Kind::Length)?;
let i = body_at(st, body, faces)?;
let i = body_at(st, body, &crate::plain(faces))?;
let Some(mb) = st.bodies.get(i).cloned() else { return Err(DocError::Invalid("body".into())) };
let open = if *tangent_chain { smooth_chain(&mb, faces) } else { faces.clone() };
let open = if *tangent_chain { smooth_chain(&mb, &crate::plain(faces)) } else { crate::plain(faces) };
let input = mb.traced();
// The shell, and the operands of the operation that made it.
let (nb, operands) = match direction.as_str() {
@@ -4286,9 +4290,9 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
FeatureKind::Draft { faces, angle, neutral, pull, body } => {
let a = val(vals, angle, Kind::Angle)?;
let pl = doc.resolve_plane_in(vals, neutral, 0, Some(st))?;
let i = body_at(st, body, faces)?;
let i = body_at(st, body, &crate::plain(faces))?;
let Some(mb) = st.bodies.get(i).cloned() else { return Err(DocError::Invalid("body".into())) };
let nb = Traced::step(kernel::draft(&mb.body, faces, &pl, *pull, a)?, &[&mb.traced()]);
let nb = Traced::step(kernel::draft(&mb.body, &crate::plain(faces), &pl, pull.0, a)?, &[&mb.traced()]);
if let Some(slot) = st.bodies.get_mut(i) {
*slot = ModelBody::traced_new(mb.name, nb, mb.feature);
}
@@ -4332,7 +4336,7 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
let mut made = Vec::new();
for p in cells {
// The cell: inside the tools that hold the point, outside the others.
let inside: Vec<bool> = tb.iter().map(|b| b.mesh().contains(*p)).collect();
let inside: Vec<bool> = tb.iter().map(|b| b.mesh().contains(p.0)).collect();
let mut acc: Option<Traced> = None;
for (b, _) in tb.iter().zip(&inside).filter(|(_, i)| **i) {
acc = match acc {
@@ -4350,16 +4354,16 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
made.push(cell);
}
if *remove_tools {
st.bodies.retain(|b| !tools.contains(&b.name));
st.bodies.retain(|b| !tools.iter().any(|t| *t == b.name));
}
apply_traced(st, f, made, *operation, &[])
}
FeatureKind::SplitFace { faces, body, plane, tool, sketch } => {
let i = body_at(st, body, faces)?;
let i = body_at(st, body, &crate::plain(faces))?;
let Some(mb) = st.bodies.get(i).cloned() else { return Err(DocError::Invalid("body".into())) };
let mut picked: Vec<usize> = Vec::new();
for p in faces {
let fi = crate::appearance::face_index_at(&mb, *p)
let fi = crate::appearance::face_index_at(&mb, p.0)
.ok_or_else(|| DocError::Invalid(format!("Split Face: no face of {} at {p:?}", mb.name)))?;
if !picked.contains(&fi) {
picked.push(fi);
@@ -4368,12 +4372,12 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
let nb = match (tool, sketch) {
(Some(t), _) => {
let tb = st.body(&t.body).ok_or_else(|| DocError::Unknown(format!("body `{}`", t.body)))?;
let face = crate::appearance::face_index_at(tb, t.point)
let face = crate::appearance::face_index_at(tb, t.point.0)
.ok_or_else(|| DocError::Invalid(format!("no face of `{}` at the tool point", t.body)))?;
kernel::split_faces(&mb.body, &picked, &kernel::SplitTool::Face { body: &tb.body, face })?
}
(None, Some(sid)) => {
let ss = st.sketch(*sid).ok_or_else(|| DocError::Unknown(format!("sketch {sid}")))?;
let ss = st.sketch(sid.0).ok_or_else(|| DocError::Unknown(format!("sketch {sid}")))?;
// Each curve's exact segments (lines and arcs split by their own plane and cylinder).
let curves: Vec<Vec<solidcraft_geom::Seg2>> = (0..ss.sketch.curves.len())
.filter(|ci| ss.sketch.curves.get(*ci).is_some_and(|c| !c.construction))
@@ -4403,7 +4407,7 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
// A face of a body (its surface extended).
Some(t) => {
let tb = st.body(&t.body).ok_or_else(|| DocError::Unknown(format!("body `{}`", t.body)))?;
let face = crate::appearance::face_index_at(tb, t.point)
let face = crate::appearance::face_index_at(tb, t.point.0)
.ok_or_else(|| DocError::Invalid(format!("no face of `{}` at the tool point", t.body)))?;
kernel::split_body(&mb.body, &kernel::SplitTool::Face { body: &tb.body, face })?
}
@@ -4432,7 +4436,7 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
FeatureKind::Pattern { features, pattern, bodies } => {
let mats = pattern_transforms(doc, vals, st, f.component, pattern)?;
if bodies.is_empty() {
return replay(doc, vals, f, st, features, &mats, warning);
return replay(doc, vals, f, st, &crate::plain(features), &mats, warning);
}
// Bodies: each copy is a new body named after its source.
let mut output = 0;
@@ -4450,7 +4454,7 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
FeatureKind::Mirror { features, plane, bodies, combine } => {
let pl = doc.resolve_plane_in(vals, plane, 0, Some(st))?;
if bodies.is_empty() {
return replay(doc, vals, f, st, features, &[mirror_matrix(&pl)], warning);
return replay(doc, vals, f, st, &crate::plain(features), &[mirror_matrix(&pl)], warning);
}
let m = mirror_matrix(&pl);
let mut output = 0;
@@ -4485,13 +4489,13 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
}
FeatureKind::Fillet { edges, radius, body, .. } | FeatureKind::Chamfer { edges, distance: radius, body, .. } => {
let r = val(vals, radius, Kind::Length)?;
let ti = blend_target(st, body, edges)?;
let ti = blend_target(st, body, &crate::plain(edges))?;
let Some(mb) = st.bodies.get(ti) else { return Err(DocError::Invalid("body".into())) };
let edges = if f.edge_names.iter().any(|n| !n.is_empty()) {
// By name first; a name that is gone falls back to the point, with a warning.
let (mut out, mut notes) = (Vec::new(), Vec::new());
for (i, p) in edges.iter().enumerate() {
match f.edge_names.get(i).filter(|n| !n.is_empty()).map(|n| (n, crate::naming::locate_edge(mb, n, *p))) {
match f.edge_names.get(i).filter(|n| !n.is_empty()).map(|n| (n, crate::naming::locate_edge(mb, n, p.0))) {
Some((n, Ok(hit))) => {
if let Some(note) = hit.note(n) {
notes.push(note);
@@ -4506,9 +4510,9 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
// The name is gone. A pick on the axis of a round must not fall
// through to the tangency seam (#775). A sharp edge that moved
// still resolves from the point, which an edit relies on.
if pick_lies_on_a_round_axis(&mb.body, *p) {
if pick_lies_on_a_round_axis(&mb.body, p.0) {
notes.push(skipped);
} else if let Ok(q) = resolve_edges(&mb.body, &[*p], &refs, &mut w) {
} else if let Ok(q) = resolve_edges(&mb.body, &[p.0], &refs, &mut w) {
out.extend(q);
notes.push(match f.edge_names.get(i) {
Some(n) if !n.is_empty() => format!("edge `{n}` no longer exists; using the edge at the picked point"),
@@ -4529,7 +4533,7 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
}
out
} else {
resolve_edges(&mb.body, edges, &f.edge_refs, warning)?
resolve_edges(&mb.body, &crate::plain(edges), &f.edge_refs, warning)?
};
st.blend_edges.insert(f.id, edges.clone());
let input = mb.traced();
@@ -4543,7 +4547,7 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
for (t, e) in mid {
pts.push((*t, val(vals, e, Kind::Length)?));
}
one_by_one(input, &edges, |b, p| kernel::fillet_variable_points(b, p, *start, &pts, *smooth))?
one_by_one(input, &edges, |b, p| kernel::fillet_variable_points(b, p, start.0, &pts, *smooth))?
}
FeatureKind::Fillet { style: crate::FilletStyle::Spline { start, knots, radii }, .. } => {
let mut values = vec![r];
@@ -4551,7 +4555,7 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
values.push(val(vals, e, Kind::Length)?);
}
let law = crate::FilletStyle::spline(knots, values);
one_by_one(input, &edges, |b, p| kernel::fillet_variable_spline(b, p, *start, &law))?
one_by_one(input, &edges, |b, p| kernel::fillet_variable_spline(b, p, start.0, &law))?
}
FeatureKind::Fillet { fit: crate::FilletFit::Taper, .. } => {
let (b, tapers) = kernel::fillet_tapered(&mb.body, &edges, r)?;
@@ -4598,9 +4602,9 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
}
FeatureKind::Cylinder { base, axis, radius, height, operation } => {
let (base, radius, height) = (point(vals, base)?, val(vals, radius, Kind::Length)?, val(vals, height, Kind::Length)?);
let b = kernel::cylinder(base, *axis, radius, height)?;
let b = kernel::cylinder(base, axis.0, radius, height)?;
let b = if matches!(operation, Operation::NewBody | Operation::Join) { b.with_cylinder_certificate(f.id)? } else { b };
let prism = more::PrimitivePrism::Cylinder { base, axis: *axis, radius, height };
let prism = more::PrimitivePrism::Cylinder { base, axis: axis.0, radius, height };
let handled = more::sheet_primitive_cut(f, st, &b, prism, *operation, false, warning)?;
let tools = vec![crate::lineage::cylinder_tool(b)];
if !handled.is_empty() {
@@ -4628,9 +4632,9 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
return Err(DocError::Unknown(format!("body `{target}`")));
}
let op = if *operation == Operation::NewBody { Operation::Join } else { *operation };
apply_traced(st, f, tb, op, std::slice::from_ref(target))?;
apply_traced(st, f, tb, op, std::slice::from_ref(&target.0))?;
if !keep_tools {
st.bodies.retain(|b| !tools.contains(&b.name));
st.bodies.retain(|b| !tools.iter().any(|t| *t == b.name));
}
Ok(())
}
@@ -4664,20 +4668,20 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
}
FeatureKind::OffsetFace { faces, distance, body } => {
let d = val(vals, distance, Kind::Length)?;
let i = body_at(st, body, faces)?;
let i = body_at(st, body, &crate::plain(faces))?;
let Some(mb) = st.bodies.get(i).cloned() else { return Err(DocError::Invalid("body".into())) };
let nb = Traced::step(kernel::offset_faces(&mb.body, faces, d)?, &[&mb.traced()]);
let nb = Traced::step(kernel::offset_faces(&mb.body, &crate::plain(faces), d)?, &[&mb.traced()]);
if let Some(slot) = st.bodies.get_mut(i) {
*slot = ModelBody::traced_new(mb.name, nb, mb.feature);
}
Ok(())
}
FeatureKind::DeleteFace { faces, body, heal } => {
let i = body_at(st, body, faces)?;
let i = body_at(st, body, &crate::plain(faces))?;
let Some(mb) = st.bodies.get(i).cloned() else { return Err(DocError::Invalid("body".into())) };
let mut picked: Vec<usize> = Vec::new();
for p in faces {
let fi = crate::appearance::face_index_at(&mb, *p)
let fi = crate::appearance::face_index_at(&mb, p.0)
.ok_or_else(|| DocError::Invalid(format!("Delete Face: no face of {} at {p:?}", mb.name)))?;
if !picked.contains(&fi) {
picked.push(fi);
@@ -4694,7 +4698,7 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
FeatureKind::BoundingSolid { bodies, margin } => {
let m = val(vals, margin, Kind::Length)?;
let mut bb = solidcraft_geom::Aabb3::EMPTY;
for b in st.bodies.iter().filter(|b| bodies.is_empty() || bodies.contains(&b.name)) {
for b in st.bodies.iter().filter(|b| bodies.is_empty() || bodies.iter().any(|t| *t == b.name)) {
bb = bb.union(&b.mesh().bounds());
}
if bb.is_empty() {
@@ -4713,7 +4717,7 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
Some(a) => val(vals, a, Kind::Angle)?,
None => 0.0,
};
let axis = rotate_axis.unwrap_or(Vec3::Z);
let axis = rotate_axis.map(Vec3::from).unwrap_or(Vec3::Z);
// With `copy` set, the source body is kept in place and the transform lands on a new
// body pushed beside it instead of replacing it.
let mut output = 0;
@@ -5215,7 +5219,7 @@ mod chain_tests {
.unwrap();
doc.add_feature(
FeatureKind::Fillet {
edges: vec![Vec3::new(20.0, 0.0, 20.0)],
edges: vec![crate::Position(Vec3::new(20.0, 0.0, 20.0))],
radius: "4".into(),
body: None,
style: crate::FilletStyle::Constant,
+52 -51
View File
@@ -10,7 +10,7 @@ use solidcraft_geom::{Loop2, Seg2};
pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st: &mut ModelState) -> Result<()> {
match &f.kind {
FeatureKind::Emboss { sketch, profiles, depth, deboss, targets, face } => {
let ss = st.sketch(*sketch).ok_or_else(|| DocError::Unknown(format!("sketch {sketch}")))?;
let ss = st.sketch(sketch.0).ok_or_else(|| DocError::Unknown(format!("sketch {sketch}")))?;
let regions = select_profiles(ss, profiles)?;
let d = val(vals, depth, Kind::Length)?;
if !(d > 0.0) {
@@ -35,7 +35,7 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
crate::apply_vector(&sketch_transform, ss.plane.y),
)
.ok_or_else(|| DocError::Invalid("invalid cylinder sketch frame".into()))?;
let mut at = crate::apply_point(&crate::mat_mul(&inverse, &author_frame), face.at);
let mut at = crate::apply_point(&crate::mat_mul(&inverse, &author_frame), face.at.0);
let name = f
.face_names
.first()
@@ -67,10 +67,10 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
if st.bodies.is_empty() {
return Err(DocError::Invalid("there is no body to emboss".into()));
}
apply_op(st, f, tools, if *deboss { Operation::Cut } else { Operation::Join }, targets)
apply_op(st, f, tools, if *deboss { Operation::Cut } else { Operation::Join }, &crate::plain(targets))
}
FeatureKind::Rib { sketch, curves, thickness, depth, flip, web } => {
let ss = st.sketch(*sketch).ok_or_else(|| DocError::Unknown(format!("sketch {sketch}")))?.clone();
let ss = st.sketch(sketch.0).ok_or_else(|| DocError::Unknown(format!("sketch {sketch}")))?.clone();
let t = val(vals, thickness, Kind::Length)?;
if !(t > 0.0) {
return Err(DocError::Invalid("the rib thickness must be positive".into()));
@@ -95,17 +95,18 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
FeatureKind::ReplaceFace { faces, target, body } => {
let tp = doc.resolve_plane_in(vals, target, 0, Some(st))?;
let tn = tp.normal();
let i = body_at(st, body, faces)?;
let i = body_at(st, body, &crate::plain(faces))?;
let Some(mb) = st.bodies.get(i).cloned() else { return Err(DocError::Invalid("body".into())) };
let mut b = mb.body.clone();
for p in faces {
let n = planar_normal_at(&b, *p).ok_or_else(|| DocError::Invalid("not supported yet: replacing a face that is not planar".into()))?;
let n =
planar_normal_at(&b, p.0).ok_or_else(|| DocError::Invalid("not supported yet: replacing a face that is not planar".into()))?;
if n.dot(tn).abs() < 1.0 - 1e-9 {
return Err(DocError::Invalid("not supported yet: a target that is not parallel to the face".into()));
}
let d = (tp.origin - *p).dot(n);
let d = (tp.origin - p.0).dot(n);
if d.abs() > 1e-9 {
b = kernel::offset_faces(&b, &[*p], d)?;
b = kernel::offset_faces(&b, &[p.0], d)?;
}
}
if let Some(slot) = st.bodies.get_mut(i) {
@@ -115,17 +116,17 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
}
FeatureKind::Align { bodies, from, to, from_normal, to_normal, flip } => {
let mut m = translation(Vec3::ZERO);
if let (Some(a), Some(b)) = (from_normal.and_then(Vec3::normalized), to_normal.and_then(Vec3::normalized)) {
if let (Some(a), Some(b)) = (from_normal.and_then(|n| n.normalized()), to_normal.and_then(|n| n.normalized())) {
// Turn `a` onto −b (faces meet) or b (flipped), about the `from` point.
let want = if *flip { b } else { b * -1.0 };
let axis = a.cross(want);
let ang = axis.len().atan2(a.dot(want));
let axis = axis.normalized().unwrap_or_else(|| a.any_perp());
if ang.abs() > 1e-12 {
m = rotation(*from, axis, ang);
m = rotation(from.0, axis, ang);
}
}
let d = *to - *from;
let d = to.0 - from.0;
m[3][0] += d.x;
m[3][1] += d.y;
m[3][2] += d.z;
@@ -150,7 +151,7 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
return Err(DocError::Unknown(format!("body `{n}`")));
}
}
st.bodies.retain(|b| !bodies.contains(&b.name));
st.bodies.retain(|b| !bodies.iter().any(|t| *t == b.name));
Ok(())
}
_ => Err(DocError::Invalid(format!("{} is not evaluated here", f.name))),
@@ -331,7 +332,7 @@ fn rebuild_sheet(st: &mut ModelState, idx: usize) -> Result<()> {
}
/// The sheet a feature works on: by body name, else the one with an edge nearest the pick.
fn sheet_for(st: &ModelState, body: &Option<String>, pick: Option<Vec3>) -> Result<usize> {
fn sheet_for(st: &ModelState, body: &Option<crate::BodyName>, pick: Option<Vec3>) -> Result<usize> {
if let Some(n) = body {
return st.sheets.iter().position(|s| &s.body == n).ok_or_else(|| DocError::Invalid(format!("`{n}` is not a sheet metal body")));
}
@@ -391,7 +392,7 @@ pub(super) fn sheet_eval(
use crate::sheet;
match &f.kind {
FeatureKind::SheetBase { sketch, profiles, rule, flip } => {
let ss = st.sketch(*sketch).ok_or_else(|| DocError::Unknown(format!("sketch {sketch}")))?.clone();
let ss = st.sketch(sketch.0).ok_or_else(|| DocError::Unknown(format!("sketch {sketch}")))?.clone();
let regions = solidcraft_sketch::merge_regions(&select_profiles(&ss, profiles)?);
let r = doc.sheet_rule(rule.as_deref());
let rv = doc.rule_values(vals, &r)?;
@@ -425,7 +426,7 @@ pub(super) fn sheet_eval(
Ok(())
}
FeatureKind::SheetContour { sketch, curves, distance, rule, flip, reverse } => {
let ss = st.sketch(*sketch).ok_or_else(|| DocError::Unknown(format!("sketch {sketch}")))?.clone();
let ss = st.sketch(sketch.0).ok_or_else(|| DocError::Unknown(format!("sketch {sketch}")))?.clone();
for id in curves {
let ci = ss.sketch.curve_index(id).ok_or_else(|| DocError::Unknown(format!("curve `{id}`")))?;
if !ss.sketch.segs(ci).iter().all(|s| matches!(s, Seg2::Line { .. })) {
@@ -450,7 +451,7 @@ pub(super) fn sheet_eval(
Ok(())
}
FeatureKind::SheetFlange { edges, height, angle, radius, position, flip, body, corner_relief_shape, corner_relief } => {
let i = sheet_for(st, body, edges.first().map(|e| e.point))?;
let i = sheet_for(st, body, edges.first().map(|e| e.point.0))?;
let (h, a) = (val(vals, height, Kind::Length)?, val(vals, angle, Kind::Angle)?);
// Each edge's own height and angle, else the feature's.
let mut own = Vec::with_capacity(edges.len());
@@ -490,7 +491,7 @@ pub(super) fn sheet_eval(
if sh.flat {
return Err(DocError::Invalid("refold the sheet before adding flanges".into()));
}
let points: Vec<Vec3> = edges.iter().map(|e| e.point).collect();
let points: Vec<Vec3> = edges.iter().map(|e| e.point.0).collect();
let each: Vec<sheet::EdgeFlange> = sheet_picks(sh, f, &points, warning)
.into_iter()
.zip(own)
@@ -535,7 +536,7 @@ pub(super) fn sheet_eval(
Ok(())
}
FeatureKind::SheetHem { edges, length, gap, flip, body } => {
let i = sheet_for(st, body, edges.first().copied())?;
let i = sheet_for(st, body, edges.first().copied().map(Vec3::from))?;
let l = val(vals, length, Kind::Length)?;
let g = match gap {
Some(e) => val(vals, e, Kind::Length)?,
@@ -545,7 +546,7 @@ pub(super) fn sheet_eval(
}
};
let sh = st.sheets.get_mut(i).ok_or_else(|| DocError::Invalid("sheet".into()))?;
let picks = sheet_picks(sh, f, edges, warning);
let picks = sheet_picks(sh, f, &crate::plain(edges), warning);
let n0 = sh.flanges.len();
for e in &picks {
sh.add_hem(*e, l, g, *flip)?;
@@ -557,14 +558,14 @@ pub(super) fn sheet_eval(
// The sketch line, where it is now.
let (a, b) = match (sketch, curve) {
(Some(sk), Some(c)) => {
let ss = st.sketch(*sk).ok_or_else(|| DocError::Unknown(format!("sketch {sk}")))?;
let ss = st.sketch(sk.0).ok_or_else(|| DocError::Unknown(format!("sketch {sk}")))?;
let ci = ss.sketch.curve_index(c).ok_or_else(|| DocError::Unknown(format!("curve `{c}`")))?;
match ss.sketch.segs(ci).as_slice() {
[Seg2::Line { a, b }] => (ss.plane.to_world(*a), ss.plane.to_world(*b)),
_ => return Err(DocError::Invalid("the fold line must be a sketch line".into())),
}
}
_ => (*a, *b),
_ => (a.0, b.0),
};
let i = sheet_for(st, body, Some((a + b) * 0.5))?;
let ang = val(vals, angle, Kind::Angle)?;
@@ -578,7 +579,7 @@ pub(super) fn sheet_eval(
// A fixed point that touches no material at all (e.g. far above the sheet) still picks
// the side it lies on, so a stored fold keeps recomputing; it says so (#371).
if let Some(p) = fixed
&& !on_sheet_material(st, i, *p)
&& !on_sheet_material(st, i, p.0)
{
*warning = Some(format!(
"{} at ({:.3}, {:.3}, {:.3}); the side of the fold line it lies on stays",
@@ -593,7 +594,7 @@ pub(super) fn sheet_eval(
return Err(DocError::Invalid("refold the sheet before folding it".into()));
}
let n0 = sh.flanges.len();
sh.add_fold(a, b, ang, rr, position, *flip, *fixed, warning)?;
sh.add_fold(a, b, ang, rr, position, *flip, fixed.map(Vec3::from), warning)?;
tag_flanges(sh, n0, f.id);
rebuild_sheet(st, i)
}
@@ -618,7 +619,7 @@ pub(super) fn sheet_eval(
let mut picks = Vec::with_capacity(bends.len());
for (i, p) in bends.iter().enumerate() {
let name = f.edge_names.get(i).filter(|n| !n.is_empty());
picks.push(match name.map(|n| (n, crate::naming::locate_edge(&mb, n, *p))) {
picks.push(match name.map(|n| (n, crate::naming::locate_edge(&mb, n, p.0))) {
Some((n, Ok(hit))) => {
if let Some(note) = hit.note(n) {
append_warning(warning, note);
@@ -627,12 +628,12 @@ pub(super) fn sheet_eval(
}
Some((n, Err(_))) => {
append_warning(warning, format!("edge `{n}` no longer exists; using the edge at the picked point"));
*p
p.0
}
None => *p,
None => p.0,
});
}
let (mut sh, notes) = sheet::convert_body(&mb, &r.name, &rv, *face, &picks)?;
let (mut sh, notes) = sheet::convert_body(&mb, &r.name, &rv, face.0, &picks)?;
for note in notes {
append_warning(warning, note);
}
@@ -641,10 +642,10 @@ pub(super) fn sheet_eval(
}
FeatureKind::SheetConvert { body, face, rule, .. } => {
let mb = st.body(body).cloned().ok_or_else(|| DocError::Unknown(format!("body `{body}`")))?;
let n = planar_normal_at(&mb.body, *face).ok_or_else(|| DocError::Invalid("pick a planar face of the plate".into()))?;
let n = planar_normal_at(&mb.body, face.0).ok_or_else(|| DocError::Invalid("pick a planar face of the plate".into()))?;
// Thickness: the body's extent along the face normal.
let extent = |mesh: &solidcraft_geom::Mesh| {
mesh.positions.iter().map(|p| (*p - *face).dot(n)).fold((f64::MAX, f64::MIN), |(lo, hi), d| (lo.min(d), hi.max(d)))
mesh.positions.iter().map(|p| (*p - face.0).dot(n)).fold((f64::MAX, f64::MIN), |(lo, hi), d| (lo.min(d), hi.max(d)))
};
// The tolerance (the mesh's chords, and how far off the face's plane a point of it may
// be) is at most a tenth of the thickness, not only a thousandth of the size. On a long
@@ -662,13 +663,13 @@ pub(super) fn sheet_eval(
// The face outline: boundary edges of the triangles on that face's plane, where it runs
// along a round wall an arc of the wall's circle (not chords: the plate check below
// compares its area with the exact volume).
let plane = Plane::from_normal(*face + n * lo, n * -1.0).ok_or_else(|| DocError::Invalid("face".into()))?;
let plane = Plane::from_normal(face.0 + n * lo, n * -1.0).ok_or_else(|| DocError::Invalid("face".into()))?;
// Recesses in one face (an engraved label) become pockets; the outline is the other face's.
let recesses = plate_recesses(&mb.body, &mesh, &plane, *face, n, (lo, hi), tol)?;
let recesses = plate_recesses(&mb.body, &mesh, &plane, face.0, n, (lo, hi), tol)?;
let mut walls = std::collections::HashMap::new();
let mut wall = |f: u32| *walls.entry(f).or_insert_with(|| sheet::round_wall(&mb.body, &plane, n, f));
let mut loops: Vec<Loop2> =
face_outline(&mesh, &plane, *face, n, recesses.level, tol).iter().map(|l| sheet::outline_loop(l, &mut wall, tol * 1e-2)).collect();
face_outline(&mesh, &plane, face.0, n, recesses.level, tol).iter().map(|l| sheet::outline_loop(l, &mut wall, tol * 1e-2)).collect();
loops.sort_by(|a, b| b.signed_area().abs().total_cmp(&a.signed_area().abs()));
let mut it = loops.into_iter();
let outer = it.next().ok_or_else(|| DocError::Invalid("no outline on that face".into()))?.ccw();
@@ -784,7 +785,7 @@ pub(super) fn sheet_cut(
if extent.taper.is_some() || !any_eligible_sheet(f, st, targets) {
return Ok(Vec::new());
}
let Some(ss) = st.sketch(*sketch).cloned() else { return Ok(Vec::new()) };
let Some(ss) = st.sketch(sketch.0).cloned() else { return Ok(Vec::new()) };
let regions = solidcraft_sketch::merge_regions(&select_profiles(&ss, profiles)?);
// The outlines of the sketch's text: the sheet remembers where its letters went.
let text: Vec<Seg2> = ss
@@ -1479,7 +1480,7 @@ fn plate_recesses(body: &Body, mesh: &Mesh, plane: &Plane, at: Vec3, n: Vec3, (l
}
/// The body a placed plastic feature joins: by name, else the one under the point.
fn body_under(st: &ModelState, body: &Option<String>, p: Vec3) -> Result<String> {
fn body_under(st: &ModelState, body: &Option<crate::BodyName>, p: Vec3) -> Result<String> {
let i = body_at(st, body, &[p])?;
let b = st.bodies.get(i).ok_or_else(|| DocError::Invalid("there is no body".into()))?;
let tolerance = (b.body.size() * 1e-5).max(1e-4);
@@ -1541,7 +1542,7 @@ pub(super) fn plastic_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &F
rib_offset,
body,
} => {
let target = body_under(st, body, *position)?;
let target = body_under(st, body, position.0)?;
let (r0, h) = (len(diameter)? / 2.0, len(height)?);
if !(r0 > 0.0 && h > 0.0) {
return Err(DocError::Invalid("the boss diameter and height must be positive".into()));
@@ -1559,9 +1560,9 @@ pub(super) fn plastic_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &F
return Err(DocError::Invalid("the draft closes the boss before its top".into()));
}
let delta = (h * 0.05).clamp(1e-3, 0.5);
let (x, _, z) = local_frame(*direction, None)?;
let (x, _, z) = local_frame(direction.0, None)?;
// The post: a (tapered) cylinder from a little inside the face.
let base = Plane::new(*position - z * delta, x, z.cross(x)).ok_or_else(|| DocError::Invalid("boss frame".into()))?;
let base = Plane::new(position.0 - z * delta, x, z.cross(x)).ok_or_else(|| DocError::Invalid("boss frame".into()))?;
let r_in = r0 + delta * dr.tan();
let disc = Region2 { outer: Loop2::circle(Vec2::ZERO, r_in), holes: Vec::new() };
let post = if dr.abs() > 1e-12 {
@@ -1594,7 +1595,7 @@ pub(super) fn plastic_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &F
for k in 0..n as usize {
let a = std::f64::consts::TAU * k as f64 / n;
let (u, v) = (x * a.cos() + z.cross(x) * a.sin(), z.cross(x) * a.cos() - x * a.sin());
let rp = Plane::new(*position, u, v).ok_or_else(|| DocError::Invalid("rib frame".into()))?;
let rp = Plane::new(position.0, u, v).ok_or_else(|| DocError::Invalid("rib frame".into()))?;
let rect = Region2 {
outer: Loop2::polygon(&[
Vec2::new(r0 * 0.5, -t / 2.0),
@@ -1613,7 +1614,7 @@ pub(super) fn plastic_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &F
let i = st.bodies.iter().position(|b| b.name == target).ok_or_else(|| DocError::Invalid("boss body".into()))?;
let Some(mb) = st.bodies.get(i).cloned() else { return Err(DocError::Invalid("boss body".into())) };
let mut warn = None;
let edges = resolve_edges(&mb.body, &[*position + x * r0], &[], &mut warn)?;
let edges = resolve_edges(&mb.body, &[position.0 + x * r0], &[], &mut warn)?;
let nb = mb.traced().then(kernel::fillet(&mb.body, &edges, rf)?);
if let Some(slot) = st.bodies.get_mut(i) {
*slot = ModelBody::traced_new(mb.name, nb, mb.feature);
@@ -1628,7 +1629,7 @@ pub(super) fn plastic_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &F
if !(rh > 0.0 && rh < r_top && depth > 0.0) {
return Err(DocError::Invalid("the boss hole must fit inside the boss".into()));
}
let hp = Plane::new(*position, x, z.cross(x)).ok_or_else(|| DocError::Invalid("hole frame".into()))?;
let hp = Plane::new(position.0, x, z.cross(x)).ok_or_else(|| DocError::Invalid("hole frame".into()))?;
let tool = kernel::extrude(&hp, &[Region2 { outer: Loop2::circle(Vec2::ZERO, rh), holes: Vec::new() }], h - depth, h + delta)?;
let tool = tool.into_iter().map(|b| crate::lineage::part_tool(b, "hole")).collect();
apply_traced(st, f, tool, Operation::Cut, std::slice::from_ref(&target))?;
@@ -1636,9 +1637,9 @@ pub(super) fn plastic_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &F
Ok(())
}
FeatureKind::Lip { face, width, height, groove, gap, outside, body } => {
let target = body_under(st, body, *face)?;
let target = body_under(st, body, face.0)?;
let mb = st.body(&target).cloned().ok_or_else(|| DocError::Invalid("body".into()))?;
let n = planar_normal_at(&mb.body, *face).ok_or_else(|| DocError::Invalid("pick the planar rim face".into()))?;
let n = planar_normal_at(&mb.body, face.0).ok_or_else(|| DocError::Invalid("pick the planar rim face".into()))?;
let (w, h) = (len(width)?, len(height)?);
let g = match gap {
Some(e) => len(e)?,
@@ -1648,10 +1649,10 @@ pub(super) fn plastic_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &F
if !(w > 0.0 && h > 0.0 && g >= 0.0) {
return Err(DocError::Invalid("the lip width and height must be positive".into()));
}
let plane = Plane::from_normal(*face, n).ok_or_else(|| DocError::Invalid("rim plane".into()))?;
let plane = Plane::from_normal(face.0, n).ok_or_else(|| DocError::Invalid("rim plane".into()))?;
let tol = (mb.body.size() * 1e-3).max(1e-3);
let mesh = mb.body.tessellate(tol)?;
let mut loops = face_loops(&mesh, &plane, *face, n, 0.0, tol);
let mut loops = face_loops(&mesh, &plane, face.0, n, 0.0, tol);
loops.sort_by(|a, b| poly_area(b).abs().total_cmp(&poly_area(a).abs()));
let ccw = |l: &[Vec2]| if poly_area(l) < 0.0 { l.iter().rev().copied().collect::<Vec<_>>() } else { l.to_vec() };
// The inner edge is an opening (the cavity under a rim): not the foot of a boss or a
@@ -1685,7 +1686,7 @@ pub(super) fn plastic_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &F
// A lip: the band raised from the rim's own faces (its walls continue the walls
// below, which a join can't do: they would coincide).
if !*groove
&& let Ok(b) = kernel::raise_band(&mb.body, *face, w, h, *outside)
&& let Ok(b) = kernel::raise_band(&mb.body, face.0, w, h, *outside)
&& let Some(slot) = st.bodies.iter_mut().find(|x| x.name == target)
{
*slot = ModelBody::traced_new(target.clone(), Traced::step(b, &[&mb.traced()]), slot.feature);
@@ -1715,17 +1716,17 @@ pub(super) fn plastic_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &F
}
}
FeatureKind::SnapFit { position, direction, hook, length, thickness, width, catch_depth, catch_length, body } => {
let target = body_under(st, body, *position)?;
let target = body_under(st, body, position.0)?;
let (l, t, w, cd, cl) = (len(length)?, len(thickness)?, len(width)?, len(catch_depth)?, len(catch_length)?);
if !(l > 0.0 && t > 0.0 && w > 0.0 && cd > 0.0 && cl > 0.0 && cl < l) {
return Err(DocError::Invalid("the snap fit's sizes must be positive and the catch shorter than the arm".into()));
}
let (x, y, z) = local_frame(*direction, Some(*hook))?;
let (x, y, z) = local_frame(direction.0, Some(hook.0))?;
let delta = (l * 0.05).clamp(1e-3, 0.5);
// Side profile (x = toward the catch, y = up the arm), extruded across the width.
let pts =
[Vec2::new(-t, -delta), Vec2::new(0.0, -delta), Vec2::new(0.0, l - cl), Vec2::new(cd, l - cl), Vec2::new(0.0, l), Vec2::new(-t, l)];
let o = *position - y * (w / 2.0);
let o = position.0 - y * (w / 2.0);
let pl = Plane::new(o, x, z).ok_or_else(|| DocError::Invalid("snap fit frame".into()))?;
let mut lp = Loop2::polygon(&pts);
if lp.signed_area() < 0.0 {
@@ -1738,7 +1739,7 @@ pub(super) fn plastic_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &F
apply_traced(st, f, tool, Operation::Join, std::slice::from_ref(&target))
}
FeatureKind::Rest { position, direction, along, width, length, height, draft, thickness, body } => {
let target = body_under(st, body, *position)?;
let target = body_under(st, body, position.0)?;
let (w, h) = (len(width)?, len(height)?);
let l = length.as_deref().map(len).transpose()?;
let dr = match draft {
@@ -1761,7 +1762,7 @@ pub(super) fn plastic_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &F
return Err(DocError::Invalid("the draft closes the rest before its top".into()));
}
let delta = (h * 0.05).clamp(1e-3, 0.5);
let (x, y, z) = local_frame(*direction, *along)?;
let (x, y, z) = local_frame(direction.0, along.map(Vec3::from))?;
// Sized at the face: the tool starts a little inside, grown by the draft over that.
let e = delta * dr.tan();
let shape = |grow: f64| match l {
@@ -1778,7 +1779,7 @@ pub(super) fn plastic_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &F
None => Vec::new(),
};
let region = Region2 { outer: shape(e), holes };
let base = Plane::new(*position - z * delta, x, y).ok_or_else(|| DocError::Invalid("rest frame".into()))?;
let base = Plane::new(position.0 - z * delta, x, y).ok_or_else(|| DocError::Invalid("rest frame".into()))?;
let tool = if dr.abs() > 1e-12 {
vec![kernel::extrude_tapered(&base, &region, h + delta, 1.0, -dr)?]
} else {
+43 -31
View File
@@ -11,22 +11,22 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
FeatureKind::Patch { sketch, profiles, body, edges, crown, continuity, points } => {
let mut made = Vec::new();
if let Some(sk) = sketch {
let ss = st.sketch(*sk).ok_or_else(|| DocError::Unknown(format!("sketch {sk} (it must come earlier in the timeline)")))?;
let ss = st.sketch(sk.0).ok_or_else(|| DocError::Unknown(format!("sketch {sk} (it must come earlier in the timeline)")))?;
let regions = solidcraft_sketch::merge_regions(&select_profiles(ss, profiles)?);
for r in &regions {
made.push(kernel::patch_region(&ss.plane, r)?);
}
}
if !edges.is_empty() {
let i = body_at(st, body, edges)?;
let i = body_at(st, body, &crate::plain(edges))?;
let b = st.bodies.get(i).ok_or_else(|| DocError::Invalid("body".into()))?;
let spec = kernel::PatchSpec {
continuity: continuity.all,
edges: continuity.edges.iter().map(|e| (e.edge, e.g)).collect(),
points: points.clone(),
edges: continuity.edges.iter().map(|e| (e.edge.0, e.g)).collect(),
points: crate::plain(points),
crown: *crown,
};
made.push(kernel::patch_edges_with(&b.body, edges, &spec)?);
made.push(kernel::patch_edges_with(&b.body, &crate::plain(edges), &spec)?);
}
if made.is_empty() {
return Err(DocError::Invalid("a patch needs a sketch region or a loop of edges".into()));
@@ -65,7 +65,7 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
if tools.is_empty() {
return Err(DocError::Invalid("pick the surfaces to thicken".into()));
}
apply_op(st, f, tools, *operation, targets)
apply_op(st, f, tools, *operation, &crate::plain(targets))
}
FeatureKind::SurfaceTrim { body, plane, tool, keep } => {
let i = st.bodies.iter().position(|b| &b.name == body).ok_or_else(|| DocError::Unknown(format!("body `{body}`")))?;
@@ -73,11 +73,11 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
let trimmed = match tool {
Some(t) => {
let tb = st.body(&t.body).ok_or_else(|| DocError::Unknown(format!("body `{}`", t.body)))?;
let face = crate::appearance::face_index_at(tb, t.point)
let face = crate::appearance::face_index_at(tb, t.point.0)
.ok_or_else(|| DocError::Invalid(format!("no face of `{}` at the tool point", t.body)))?;
kernel::trim(&mb.body, &kernel::SplitTool::Face { body: &tb.body, face }, *keep)?
kernel::trim(&mb.body, &kernel::SplitTool::Face { body: &tb.body, face }, keep.0)?
}
None => kernel::trim(&mb.body, &kernel::SplitTool::Plane(doc.resolve_plane_in(vals, plane, 0, Some(st))?), *keep)?,
None => kernel::trim(&mb.body, &kernel::SplitTool::Plane(doc.resolve_plane_in(vals, plane, 0, Some(st))?), keep.0)?,
};
if let Some(slot) = st.bodies.get_mut(i) {
*slot = ModelBody::new(mb.name, trimmed, mb.feature);
@@ -94,18 +94,20 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
};
let at = |limit| kernel::ExtendOptions { mode, limit, neighbours: *neighbours };
let out = match up_to {
None => kernel::extend_surface(&mb.body, edges, &at(kernel::ExtendLimit::Distance(val(vals, distance, Kind::Length)?)))?,
Some(crate::ExtendTo::Bounds) => kernel::extend_surface(&mb.body, edges, &at(kernel::ExtendLimit::Bounds))?,
None => {
kernel::extend_surface(&mb.body, &crate::plain(edges), &at(kernel::ExtendLimit::Distance(val(vals, distance, Kind::Length)?)))?
}
Some(crate::ExtendTo::Bounds) => kernel::extend_surface(&mb.body, &crate::plain(edges), &at(kernel::ExtendLimit::Bounds))?,
Some(crate::ExtendTo::Plane(plane)) => {
let tool = kernel::SplitTool::Plane(doc.resolve_plane_in(vals, plane, 0, Some(st))?);
kernel::extend_surface(&mb.body, edges, &at(kernel::ExtendLimit::UpTo(&tool)))?
kernel::extend_surface(&mb.body, &crate::plain(edges), &at(kernel::ExtendLimit::UpTo(&tool)))?
}
Some(crate::ExtendTo::Face(t)) => {
let tb = st.body(&t.body).ok_or_else(|| DocError::Unknown(format!("body `{}`", t.body)))?;
let face = crate::appearance::face_index_at(tb, t.point)
let face = crate::appearance::face_index_at(tb, t.point.0)
.ok_or_else(|| DocError::Invalid(format!("no face of `{}` at the target point", t.body)))?;
let tool = kernel::SplitTool::Face { body: &tb.body, face };
kernel::extend_surface(&mb.body, edges, &at(kernel::ExtendLimit::UpTo(&tool)))?
kernel::extend_surface(&mb.body, &crate::plain(edges), &at(kernel::ExtendLimit::UpTo(&tool)))?
}
};
if let Some(slot) = st.bodies.get_mut(i) {
@@ -119,7 +121,17 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
if from.edges.is_empty() || to.edges.is_empty() {
return Err(DocError::Invalid("a blend needs a chain of edges at each end".into()));
}
let (made, gap) = kernel::blend_between(&a, &from.edges, &b, &to.edges, *continuity, *bulge, *align, BLEND_STATIONS, *stitch)?;
let (made, gap) = kernel::blend_between(
&a,
&crate::plain(&from.edges),
&b,
&crate::plain(&to.edges),
*continuity,
*bulge,
align.map(|[p, q]| [p.0, q.0]),
BLEND_STATIONS,
*stitch,
)?;
if gap > 1e-9 {
*warning = Some(format!("the blend's boundary lies up to {gap:.4} mm from the edges it was built on"));
}
@@ -127,7 +139,7 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
// A stitched blend takes the place of both bodies it joins; otherwise it is a new
// surface body of its own.
if *stitch {
let gone = [from.body.clone(), to.body.clone()];
let gone = [from.body.0.clone(), to.body.0.clone()];
st.bodies.retain(|x| !gone.contains(&x.name));
}
st.bodies.push(ModelBody::new(name, made, f.id));
@@ -136,7 +148,7 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
FeatureKind::SurfaceExtrude { sketch, profiles, curves, extent } => {
let ss = sketch_in_feature_frame(
doc,
st.sketch(*sketch).ok_or_else(|| DocError::Unknown(format!("sketch {sketch} (it must come earlier in the timeline)")))?,
st.sketch(sketch.0).ok_or_else(|| DocError::Unknown(format!("sketch {sketch} (it must come earlier in the timeline)")))?,
f.component,
)?;
if extent.through_all || extent.taper.is_some() || extent.to.is_some() || extent.from.is_some() {
@@ -153,7 +165,7 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
FeatureKind::SurfaceRevolve { sketch, profiles, curves, axis, angle, angle2, symmetric } => {
let ss = sketch_in_feature_frame(
doc,
st.sketch(*sketch).ok_or_else(|| DocError::Unknown(format!("sketch {sketch} (it must come earlier in the timeline)")))?,
st.sketch(sketch.0).ok_or_else(|| DocError::Unknown(format!("sketch {sketch} (it must come earlier in the timeline)")))?,
f.component,
)?;
let (o, d) = revolve_axis(&ss, axis)?;
@@ -174,7 +186,7 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
FeatureKind::Unstitch { body, faces } => {
let i = st.bodies.iter().position(|b| &b.name == body).ok_or_else(|| DocError::Unknown(format!("body `{body}`")))?;
let Some(mb) = st.bodies.get(i).cloned() else { return Err(DocError::Invalid("body".into())) };
let (pieces, rest) = kernel::unstitch(&mb.body, faces)?;
let (pieces, rest) = kernel::unstitch(&mb.body, &crate::plain(faces))?;
let source = mb.traced();
// The faces left keep the body's name; each face taken out is a new surface body.
match rest {
@@ -196,7 +208,7 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
FeatureKind::ReverseNormal { body, faces } => {
let i = st.bodies.iter().position(|b| &b.name == body).ok_or_else(|| DocError::Unknown(format!("body `{body}`")))?;
let Some(mb) = st.bodies.get(i).cloned() else { return Err(DocError::Invalid("body".into())) };
let turned = mb.traced().then(kernel::reverse_faces(&mb.body, faces)?);
let turned = mb.traced().then(kernel::reverse_faces(&mb.body, &crate::plain(faces))?);
if let Some(slot) = st.bodies.get_mut(i) {
*slot = ModelBody::traced_new(mb.name, turned, mb.feature);
}
@@ -204,9 +216,9 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
}
FeatureKind::SurfaceSweep { sketch, profiles, curves, path_sketch, path, orientation, twist, scale } => {
let ctl = crate::eval::sweep_control(vals, orientation, twist, scale)?;
let ss = st.sketch(*sketch).ok_or_else(|| DocError::Unknown(format!("sketch {sketch} (it must come earlier in the timeline)")))?;
let ss = st.sketch(sketch.0).ok_or_else(|| DocError::Unknown(format!("sketch {sketch} (it must come earlier in the timeline)")))?;
let ps =
st.sketch(*path_sketch).ok_or_else(|| DocError::Unknown(format!("sketch {path_sketch} (it must come earlier in the timeline)")))?;
st.sketch(path_sketch.0).ok_or_else(|| DocError::Unknown(format!("sketch {path_sketch} (it must come earlier in the timeline)")))?;
// As Sweep: a 3D path sketch is followed in the feature's frame, the curves with it.
let profile;
let ss = if !ps.sketch.wires.is_empty() {
@@ -235,7 +247,7 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
for sec in sections {
let ss = sketch_in_feature_frame(
doc,
st.sketch(sec.sketch)
st.sketch(sec.sketch.0)
.ok_or_else(|| DocError::Unknown(format!("sketch {} (it must come earlier in the timeline)", sec.sketch)))?,
f.component,
)?;
@@ -265,9 +277,9 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
Some(t) => val(vals, t, Kind::Length)?,
None => 0.01,
};
let i = body_at(st, body, faces)?;
let i = body_at(st, body, &crate::plain(faces))?;
let Some(mb) = st.bodies.get(i).cloned() else { return Err(DocError::Invalid("body".into())) };
let picks = if *chain { smooth_chain(&mb, faces) } else { faces.clone() };
let picks = if *chain { smooth_chain(&mb, &crate::plain(faces)) } else { crate::plain(faces) };
let mut picked: Vec<usize> = Vec::new();
for p in &picks {
let fi =
@@ -288,10 +300,10 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
RuledMode::Tangent => kernel::RuledMode::Tangent,
RuledMode::Angle => {
let a = angle.as_ref().ok_or_else(|| DocError::Invalid("a ruled surface at an angle needs `angle`".into()))?;
kernel::RuledMode::Angle { angle: val(vals, a, Kind::Angle)?, reference: *direction }
kernel::RuledMode::Angle { angle: val(vals, a, Kind::Angle)?, reference: direction.map(Vec3::from) }
}
RuledMode::Direction => kernel::RuledMode::Direction(
direction.ok_or_else(|| DocError::Invalid("a ruled surface along a direction needs `direction`".into()))?,
direction.map(Vec3::from).ok_or_else(|| DocError::Invalid("a ruled surface along a direction needs `direction`".into()))?,
),
};
let corners = match corners {
@@ -299,8 +311,8 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
RuledCorners::Round => kernel::RuledCorners::Round,
};
let mb = st.body(body).cloned().ok_or_else(|| DocError::Unknown(format!("body `{body}`")))?;
let r = kernel::Ruled { distance: d, mode, flip: *flip, corners, face: *face };
let made = Traced::step(kernel::ruled_surface(&mb.body, edges, &r)?, &[&mb.traced()]);
let r = kernel::Ruled { distance: d, mode, flip: *flip, corners, face: face.map(Vec3::from) };
let made = Traced::step(kernel::ruled_surface(&mb.body, &crate::plain(edges), &r)?, &[&mb.traced()]);
let name = new_name(st, f, 0);
st.bodies.push(ModelBody::traced_new(name, made, f.id));
Ok(())
@@ -308,11 +320,11 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
FeatureKind::SurfaceUntrim { face, holes, outer, in_place } => {
let i = st.bodies.iter().position(|b| b.name == face.body).ok_or_else(|| DocError::Unknown(format!("body `{}`", face.body)))?;
let Some(mb) = st.bodies.get(i).cloned() else { return Err(DocError::Invalid("body".into())) };
let fi = crate::appearance::face_index_at(&mb, face.point)
let fi = crate::appearance::face_index_at(&mb, face.point.0)
.ok_or_else(|| DocError::Invalid(format!("Untrim: no face of {} at {:?}", mb.name, face.point)))?;
let which = match holes {
None => kernel::UntrimHoles::All,
Some(points) => kernel::UntrimHoles::At(points),
Some(points) => kernel::UntrimHoles::At(&crate::plain(points)),
};
// A surface body's face is untrimmed where it is when asked; a solid's is copied out.
let replace = *in_place && mb.body.is_surface();
+88
View File
@@ -15,6 +15,94 @@ const MAX_LEN: usize = 4096;
const MAX_DEPTH: usize = 64;
const MAX_ARGS: usize = 64;
/// An expression a feature holds, as the user wrote it (`2 * width + 5 mm`): kept as text and
/// evaluated with the design's parameters. It is saved as a plain string, and reads as one
/// (`&expr` is a `&str`); it changes only as a whole (`*expr = new.into()`).
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
#[serde(transparent)]
pub struct Expr(String);
impl Expr {
pub fn new(s: impl Into<String>) -> Self {
Expr(s.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn into_string(self) -> String {
self.0
}
}
impl std::ops::Deref for Expr {
type Target = str;
fn deref(&self) -> &str {
&self.0
}
}
impl AsRef<str> for Expr {
fn as_ref(&self) -> &str {
&self.0
}
}
impl From<String> for Expr {
fn from(s: String) -> Self {
Expr(s)
}
}
impl From<&str> for Expr {
fn from(s: &str) -> Self {
Expr(s.to_string())
}
}
impl From<&String> for Expr {
fn from(s: &String) -> Self {
Expr(s.clone())
}
}
impl From<Expr> for String {
fn from(e: Expr) -> Self {
e.0
}
}
impl std::fmt::Display for Expr {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl PartialEq<str> for Expr {
fn eq(&self, other: &str) -> bool {
self.0 == other
}
}
impl PartialEq<&str> for Expr {
fn eq(&self, other: &&str) -> bool {
self.0 == *other
}
}
impl PartialEq<Expr> for String {
fn eq(&self, other: &Expr) -> bool {
*self == other.0
}
}
impl PartialEq<String> for Expr {
fn eq(&self, other: &String) -> bool {
&self.0 == other
}
}
/// What an expression must evaluate to.
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
+8 -4
View File
@@ -69,8 +69,12 @@ impl FaceAnchor {
}
let inverse = crate::mat_inverse(&self.author_frame(doc, author)?)
.ok_or_else(|| DocError::Invalid("attached sketch author frame is invalid".into()))?;
let source =
EmbossFace { body: self.target.body.clone(), at: self.target.point, path: self.path.clone(), author_path: self.author_path.clone() };
let source = EmbossFace {
body: self.target.body.clone().into(),
at: crate::FacePick(self.target.point),
path: self.path.clone(),
author_path: self.author_path.clone(),
};
Ok(crate::mat_mul(&inverse, &source.body_frame(doc, body)?))
}
}
@@ -447,7 +451,7 @@ mod tests {
.add_feature(
FeatureKind::Extrude {
thin: None,
sketch: source,
sketch: crate::SketchId(source),
profiles: crate::ProfileSel::Indices { indices: vec![0, 1] },
extent,
operation: crate::Operation::NewBody,
@@ -558,7 +562,7 @@ mod tests {
.add_feature(
FeatureKind::Extrude {
thin: None,
sketch: sk,
sketch: crate::SketchId(sk),
profiles: crate::ProfileSel::All,
extent,
operation: crate::Operation::NewBody,
+12 -13
View File
@@ -5,15 +5,14 @@
use std::collections::BTreeMap;
use crate::FeatureKind;
use crate::walk::Walk;
use crate::{BodyName, FeatureKind, FeatureName, SketchId};
/// A reference a feature holds: `S` is `&u64` or `&mut u64` (a sketch id), `N` is `&String` or
/// `&mut String` (a feature or body name).
pub(crate) enum Ref<S, N> {
/// A reference a feature holds: `S` is `&SketchId` or `&mut SketchId`, and so on.
pub(crate) enum Ref<S, F, B> {
Sketch(S),
Feature(N),
Body(N),
Feature(F),
Body(B),
}
impl FeatureKind {
@@ -23,7 +22,7 @@ impl FeatureKind {
let mut v = Vec::new();
self.walk_refs(&mut |r| {
if let Ref::Sketch(id) = r {
v.push(*id);
v.push(id.0);
}
});
v
@@ -34,13 +33,13 @@ impl FeatureKind {
pub fn remap_refs(&mut self, sketches: &BTreeMap<u64, u64>, names: &BTreeMap<String, String>) {
self.walk_refs_mut(&mut |r| match r {
Ref::Sketch(id) => {
if let Some(n) = sketches.get(id) {
*id = *n;
if let Some(n) = sketches.get(&id.0) {
*id = SketchId(*n);
}
}
Ref::Feature(name) => {
if let Some(n) = names.get(name) {
*name = n.clone();
if let Some(n) = names.get(name.as_ref()) {
*name = FeatureName(n.clone());
}
}
Ref::Body(_) => {}
@@ -51,9 +50,9 @@ impl FeatureKind {
pub fn remap_bodies(&mut self, bodies: &BTreeMap<String, String>) {
self.walk_refs_mut(&mut |r| {
if let Ref::Body(name) = r
&& let Some(n) = bodies.get(name)
&& let Some(n) = bodies.get(name.as_ref())
{
*name = n.clone();
*name = BodyName(n.clone());
}
});
}
+6 -6
View File
@@ -73,9 +73,9 @@ fn ref_record() -> String {
out
}
/// A variant with a field that refers to a sketch (a field named `*sketch*` holding an id, sketch
/// points or loft sections) must report it from `input_sketches`, or deleting the sketch leaves
/// the feature broken. New variants are caught by the walk's exhaustive match; this catches a
/// A variant with a field that refers to a sketch (a `SketchId`, sketch points or loft
/// sections) must report it from `input_sketches`, or deleting the sketch leaves the feature
/// broken. New variants are caught by the walk's exhaustive match; this catches a
/// sketch field added to an existing variant (its fixture has to set it).
/// Per fixture (with every reference and position set): the fields `transform_geometry` changes
/// under a turn and a shift (every point, direction and plane it maps), the face points that name
@@ -107,7 +107,7 @@ fn geometry_record() -> String {
out += &format!(" profiles: {}\n", profiles.join(", "));
}
if let Some(p) = kind.point_expr() {
out += &format!(" placement: {}\n", p.join(", "));
out += &format!(" placement: {}\n", p.iter().map(|e| e.as_str()).collect::<Vec<_>>().join(", "));
}
}
out
@@ -151,8 +151,8 @@ fn input_sketches_lists_every_variant_with_a_sketch_field() {
variant = t.split(|c: char| !c.is_alphanumeric()).next().unwrap_or("").to_lowercase();
continue;
}
let Some((name, ty)) = t.strip_prefix("pub ").unwrap_or(t).split_once(':') else { continue };
let refers = (name.contains("sketch") && ty.contains("u64")) || ty.contains("SketchPoints") || ty.contains("LoftSection");
let Some((_, ty)) = t.strip_prefix("pub ").unwrap_or(t).split_once(':') else { continue };
let refers = ty.contains("SketchId") || ty.contains("SketchPoints") || ty.contains("LoftSection");
if refers && !t.starts_with("//") && !with_sketch.contains(&variant) {
with_sketch.push(variant.clone());
}
+4 -4
View File
@@ -1435,7 +1435,7 @@ mod tests {
.add_feature(
FeatureKind::Extrude {
thin: None,
sketch,
sketch: crate::SketchId(sketch),
profiles: ProfileSel::All,
extent: Extent {
convention: crate::ExtrudeConvention::LegacyPerSide,
@@ -1445,9 +1445,9 @@ mod tests {
start_offset: None,
through_all: false,
taper: Some("0 deg".into()),
to: Some(solidcraft_geom::Vec3::new(0.0, 0.0, 10.0)),
to: Some(crate::FacePick(solidcraft_geom::Vec3::new(0.0, 0.0, 10.0))),
to_offset: Some("offset + 1 mm".into()),
from: Some(solidcraft_geom::Vec3::new(0.0, 0.0, 5.0)),
from: Some(crate::FacePick(solidcraft_geom::Vec3::new(0.0, 0.0, 5.0))),
},
operation: Operation::NewBody,
targets: vec![],
@@ -2077,7 +2077,7 @@ mod tests {
"orientation": {"direction": [0.0, 1.0, 0.0]}, "twist": "90 deg", "scale": "0.5"});
let k: FeatureKind = serde_json::from_value(new.clone()).unwrap();
let FeatureKind::SurfaceSweep { orientation, .. } = &k else { panic!("{k:?}") };
assert_eq!(*orientation, SweepOrientation::Direction(solidcraft_geom::Vec3::Y));
assert_eq!(*orientation, SweepOrientation::Direction(crate::Dir(solidcraft_geom::Vec3::Y)));
assert_eq!(serde_json::to_value(&k).unwrap(), new);
let parallel: SweepOrientation = serde_json::from_value(serde_json::json!("parallel")).unwrap();
assert_eq!(parallel, SweepOrientation::Parallel);
+1 -1
View File
@@ -424,7 +424,7 @@ mod tests {
.add_feature(
FeatureKind::Extrude {
thin: None,
sketch: if missing_sketch { 999_999 } else { id },
sketch: crate::SketchId(if missing_sketch { 999_999 } else { id }),
profiles: ProfileSel::OffsetBoundaries { associations: vec!["deleted-o1".into()] },
extent: crate::Extent {
convention: crate::ExtrudeConvention::LegacyPerSide,
+5
View File
@@ -34,10 +34,12 @@ pub mod metadata;
pub use metadata::Metadata;
pub mod motion;
mod name_lookup;
mod names;
pub mod naming;
mod params;
pub mod physical_material;
mod piece_names;
mod points;
mod profile_refs;
pub mod progress;
mod walk;
@@ -60,7 +62,10 @@ pub use eval::{
revolve_extent, revolve_extent_with_convention, select_profiles, sketch_hole_direction, state_in_frame, state_in_frame_at, through_all_distance,
world_state, world_state_reusing,
};
pub use expr::Expr;
pub use names::{BodyName, FeatureName, SketchId};
pub use params::{ParamRow, ParamUser, cycles};
pub use points::{Dir, FacePick, Position, plain};
pub use solidcraft_kernel as kernel;
pub use solidcraft_sketch as sketch;
+146
View File
@@ -0,0 +1,146 @@
//! What a feature refers to: bodies and features by name, sketches by id. Each is saved as the
//! plain name or id it wraps, but has its own type, so a body name can't be passed where a
//! feature name or an expression is meant.
use std::ops::Deref;
use serde::{Deserialize, Serialize};
macro_rules! name_type {
($(#[$doc:meta])* $name:ident) => {
$(#[$doc])*
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(transparent)]
pub struct $name(pub String);
impl Deref for $name {
type Target = str;
fn deref(&self) -> &str {
&self.0
}
}
impl AsRef<str> for $name {
fn as_ref(&self) -> &str {
&self.0
}
}
impl std::borrow::Borrow<str> for $name {
fn borrow(&self) -> &str {
&self.0
}
}
impl From<String> for $name {
fn from(s: String) -> Self {
$name(s)
}
}
impl From<&str> for $name {
fn from(s: &str) -> Self {
$name(s.to_string())
}
}
impl From<&String> for $name {
fn from(s: &String) -> Self {
$name(s.clone())
}
}
impl From<$name> for String {
fn from(n: $name) -> String {
n.0
}
}
impl std::fmt::Display for $name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl PartialEq<str> for $name {
fn eq(&self, other: &str) -> bool {
self.0 == other
}
}
impl PartialEq<&str> for $name {
fn eq(&self, other: &&str) -> bool {
self.0 == *other
}
}
impl PartialEq<String> for $name {
fn eq(&self, other: &String) -> bool {
&self.0 == other
}
}
impl PartialEq<$name> for String {
fn eq(&self, other: &$name) -> bool {
*self == other.0
}
}
impl PartialEq<$name> for str {
fn eq(&self, other: &$name) -> bool {
self == other.0
}
}
};
}
name_type!(
/// A body, by name.
BodyName
);
name_type!(
/// A feature, by name.
FeatureName
);
/// A sketch, by its feature id.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(transparent)]
pub struct SketchId(pub u64);
impl Deref for SketchId {
type Target = u64;
fn deref(&self) -> &u64 {
&self.0
}
}
impl From<u64> for SketchId {
fn from(id: u64) -> Self {
SketchId(id)
}
}
impl From<SketchId> for u64 {
fn from(id: SketchId) -> u64 {
id.0
}
}
impl PartialEq<u64> for SketchId {
fn eq(&self, other: &u64) -> bool {
self.0 == *other
}
}
impl PartialEq<SketchId> for u64 {
fn eq(&self, other: &SketchId) -> bool {
*self == other.0
}
}
impl std::fmt::Display for SketchId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
+5 -5
View File
@@ -540,7 +540,7 @@ fn role_name(f: &Feature, before: &ModelState, ps: &[Vec3], ns: &[Vec3], fi: usi
let c = ps.iter().fold(Vec3::ZERO, |a, p| a + *p) * (1.0 / ps.len().max(1) as f64);
match &f.kind {
FeatureKind::Extrude { sketch, .. } => {
let Some(ss) = before.sketch(*sketch) else { return format!("F{fid}:face:{fi}") };
let Some(ss) = before.sketch(sketch.0) else { return format!("F{fid}:face:{fi}") };
let n = ss.plane.normal();
// A face across the extrusion is a cap; others sweep a sketch curve.
let flat = !ns.is_empty() && ns.iter().all(|m| m.cross(n).len() < 1e-6);
@@ -552,7 +552,7 @@ fn role_name(f: &Feature, before: &ModelState, ps: &[Vec3], ns: &[Vec3], fi: usi
nearest_curve(ss, Vec2::new(q.x, q.y)).map(|id| format!("F{fid}:side:{id}")).unwrap_or_else(|| format!("F{fid}:face:{fi}"))
}
FeatureKind::Revolve { sketch, axis, .. } => {
let Some(ss) = before.sketch(*sketch) else { return format!("F{fid}:face:{fi}") };
let Some(ss) = before.sketch(sketch.0) else { return format!("F{fid}:face:{fi}") };
let Ok((o2, d2)) = crate::eval::revolve_axis(ss, axis) else { return format!("F{fid}:face:{fi}") };
let (o, d) = (ss.plane.to_world(o2), (ss.plane.to_world(o2 + d2) - ss.plane.to_world(o2)).normalized().unwrap_or(Vec3::Z));
// A flat face holding the axis is an angle cap: on the sketch plane, or the far one.
@@ -582,7 +582,7 @@ fn role_name(f: &Feature, before: &ModelState, ps: &[Vec3], ns: &[Vec3], fi: usi
FeatureKind::Loft { sections, .. } => sections.first().map(|x| x.sketch),
_ => None,
};
let Some(ss) = sk.and_then(|k| before.sketch(k)) else { return format!("F{fid}:face:{fi}") };
let Some(ss) = sk.and_then(|k| before.sketch(k.0)) else { return format!("F{fid}:face:{fi}") };
let n = ss.plane.normal();
let h = |p: &Vec3| (*p - ss.plane.origin).dot(n);
let flat = !ns.is_empty() && ns.windows(2).all(|w| w[0].cross(w[1]).len() < 1e-6);
@@ -613,10 +613,10 @@ fn role_name(f: &Feature, before: &ModelState, ps: &[Vec3], ns: &[Vec3], fi: usi
format!("F{fid}:{role}")
}
FeatureKind::Cylinder { axis, .. } => {
let flat = !ns.is_empty() && ns.iter().all(|m| m.cross(*axis).len() < 1e-6);
let flat = !ns.is_empty() && ns.iter().all(|m| m.cross(axis.0).len() < 1e-6);
let role = if !flat {
"side"
} else if ns.first().is_some_and(|m| m.dot(*axis) > 0.0) {
} else if ns.first().is_some_and(|m| m.dot(axis.0) > 0.0) {
"top"
} else {
"bottom"
+8 -8
View File
@@ -8,7 +8,7 @@ use std::collections::{BTreeMap, BTreeSet};
use serde::{Deserialize, Serialize};
use crate::expr::{self, Kind, ParamDef, Value};
use crate::expr::{self, Expr, Kind, ParamDef, Value};
use crate::walk::Walk;
use crate::{DocError, Document, FeatureKind, MAX_PARAMS, Result};
@@ -23,7 +23,7 @@ pub(crate) enum ExprRole {
impl FeatureKind {
/// The feature's numeric inputs: (label, expression, kind), in a fixed order.
pub fn inputs_mut(&mut self) -> Vec<(&'static str, &mut String, Kind)> {
pub fn inputs_mut(&mut self) -> Vec<(&'static str, &mut Expr, Kind)> {
let mut v = Vec::new();
self.walk_exprs_mut(&mut |(label, e, kind, role)| {
if role == ExprRole::Input {
@@ -38,7 +38,7 @@ impl FeatureKind {
let mut v = Vec::new();
self.walk_exprs(&mut |(label, e, kind, role)| {
if role == ExprRole::Input {
v.push((label, e.clone(), kind));
v.push((label, e.to_string(), kind));
}
});
v
@@ -47,7 +47,7 @@ impl FeatureKind {
/// The coordinates of the points that place the feature: a primitive's position, an
/// explicit plane's origin. They are not inputs (no parameter names), but follow
/// parameters as inputs do.
pub fn placement_exprs_mut(&mut self) -> Vec<&mut String> {
pub fn placement_exprs_mut(&mut self) -> Vec<&mut Expr> {
let mut v = Vec::new();
self.walk_exprs_mut(&mut |(_, e, _, role)| {
if role == ExprRole::Placement {
@@ -260,7 +260,7 @@ impl Document {
if let Some(f) = next.features.get_mut(i)
&& let Some((_, e, _)) = f.kind.inputs_mut().into_iter().nth(k)
{
*e = expr_s.clone();
*e = expr_s.clone().into();
}
if let Some(c) = comment {
next.param_comments.insert(name.to_string(), c.to_string());
@@ -308,7 +308,7 @@ impl Document {
if *target != normalized {
changed.insert(name.clone());
}
*target = normalized;
*target = normalized.into();
}
let (_, errors) = next.param_values();
for (name, error) in errors {
@@ -673,14 +673,14 @@ impl Document {
for e in f.kind.placement_exprs_mut() {
let r = expr::rename_reference(e, old, new);
if r != *e {
*e = r;
*e = r.into();
n += 1;
}
}
for (_, e, _) in f.kind.inputs_mut() {
let r = expr::rename_reference(e, old, new);
if r != *e {
*e = r;
*e = r.into();
n += 1;
}
}
+61
View File
@@ -0,0 +1,61 @@
//! The points a feature holds, by what they are: a point that picks a face, any other
//! position, a direction. Each is a `Vec3` (and is saved as one), but a different type, so a
//! direction can't be moved like a position, and a pick can't be mistaken for either.
use std::ops::{Deref, DerefMut};
use serde::{Deserialize, Serialize};
use solidcraft_geom::Vec3;
macro_rules! point_type {
($(#[$doc:meta])* $name:ident) => {
$(#[$doc])*
#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct $name(pub Vec3);
impl Deref for $name {
type Target = Vec3;
fn deref(&self) -> &Vec3 {
&self.0
}
}
impl DerefMut for $name {
fn deref_mut(&mut self) -> &mut Vec3 {
&mut self.0
}
}
impl From<Vec3> for $name {
fn from(v: Vec3) -> Self {
$name(v)
}
}
impl From<$name> for Vec3 {
fn from(p: $name) -> Vec3 {
p.0
}
}
};
}
point_type!(
/// A point that picks a face: it moves with the feature, and names the face it picks.
FacePick
);
point_type!(
/// A position (an edge pick, an axis origin, a fold line): it moves with the feature.
Position
);
point_type!(
/// A direction: it turns with the feature, and never moves.
Dir
);
/// The plain values of a list of picks, positions, directions or names (for code that takes
/// `Vec3`s or `String`s).
pub fn plain<T, P: Clone + Into<T>>(ps: &[P]) -> Vec<T> {
ps.iter().map(|p| p.clone().into()).collect()
}
+2 -2
View File
@@ -309,7 +309,7 @@ mod tests {
.add_feature(
crate::FeatureKind::Extrude {
thin: None,
sketch: feature,
sketch: crate::SketchId(feature),
profiles: selection.clone(),
extent: crate::Extent {
convention: crate::ExtrudeConvention::LegacyPerSide,
@@ -436,7 +436,7 @@ mod tests {
.add_feature(
crate::FeatureKind::Extrude {
thin: None,
sketch: feature,
sketch: crate::SketchId(feature),
profiles: selection.clone(),
extent: crate::Extent {
convention: crate::ExtrudeConvention::LegacyPerSide,
+5 -2
View File
@@ -261,8 +261,11 @@ fn resolve_uncached(
// Its path is a curve of its own sketch: set with the sketch (`Sketch::text_source_geometry`).
LinkSource::Text { .. } => return Err(DocError::Invalid("a text on a path is set with its sketch".into())),
LinkSource::Plane { name } => {
let pr =
if Plane::named(name).is_some() { PlaneRef::Origin { name: name.clone() } } else { PlaneRef::Construction { name: name.clone() } };
let pr = if Plane::named(name).is_some() {
PlaneRef::Origin { name: name.clone() }
} else {
PlaneRef::Construction { name: name.clone().into() }
};
let other = doc.resolve_plane_in(vals, &pr, 0, Some(st)).map_err(|_| lost(&format!("plane {name}")))?;
plane_trace(plane, &other).into_iter().collect()
}
+6 -6
View File
@@ -30,7 +30,7 @@ fn plate_doc() -> (Document, u64) {
doc.add_feature(
FeatureKind::Extrude {
thin: None,
sketch: s,
sketch: crate::SketchId(s),
profiles: ProfileSel::All,
extent: Extent {
convention: crate::ExtrudeConvention::LegacyPerSide,
@@ -108,7 +108,7 @@ fn box_fillet_cut_timeline() {
let (mut doc, _) = plate_doc();
doc.add_feature(
FeatureKind::Fillet {
edges: vec![Vec3::new(0.0, 0.0, 10.0)],
edges: vec![Position(Vec3::new(0.0, 0.0, 10.0))],
radius: "3".into(),
body: None,
style: Default::default(),
@@ -123,7 +123,7 @@ fn box_fillet_cut_timeline() {
doc.add_feature(
FeatureKind::Extrude {
thin: None,
sketch: s2,
sketch: crate::SketchId(s2),
profiles: ProfileSel::Points { points: vec![Vec2::new(20.0, 15.0)] },
extent: Extent {
convention: crate::ExtrudeConvention::LegacyPerSide,
@@ -162,7 +162,7 @@ fn errors_are_reported_not_fatal() {
let (mut doc, s) = plate_doc();
doc.add_feature(
FeatureKind::Revolve {
sketch: s,
sketch: crate::SketchId(s),
profiles: ProfileSel::All,
axis: AxisRef::SketchAxis { axis: "y".into() },
angle: "90 deg".into(),
@@ -178,7 +178,7 @@ fn errors_are_reported_not_fatal() {
.unwrap();
doc.add_feature(
FeatureKind::Fillet {
edges: vec![Vec3::new(500.0, 0.0, 0.0)],
edges: vec![Position(Vec3::new(500.0, 0.0, 0.0))],
radius: "1".into(),
body: None,
style: Default::default(),
@@ -244,7 +244,7 @@ fn primitives_and_combine() {
doc.add_feature(
FeatureKind::Cylinder {
base: point_expr(Vec3::new(5.0, 5.0, 0.0)),
axis: Vec3::Z,
axis: Dir(Vec3::Z),
radius: "2".into(),
height: "20".into(),
operation: Operation::NewBody,
+63 -17
View File
@@ -13,13 +13,13 @@ pub(crate) use crate::expr::Kind;
pub(crate) use crate::feature_geometry::Geo;
pub(crate) use crate::feature_refs::Ref;
pub(crate) use crate::params::ExprRole;
use crate::{PlaneRef, PointExpr};
use crate::{BodyName, Dir, Expr, FacePick, FeatureName, PlaneRef, PointExpr, Position, SketchId};
/// An expression: its label, the expression, its kind and its role.
pub(crate) type ExprItem<'a> = (&'static str, &'a String, Kind, ExprRole);
pub(crate) type ExprItemMut<'a> = (&'static str, &'a mut String, Kind, ExprRole);
pub(crate) type RefItem<'a> = Ref<&'a u64, &'a String>;
pub(crate) type RefItemMut<'a> = Ref<&'a mut u64, &'a mut String>;
pub(crate) type ExprItem<'a> = (&'static str, &'a Expr, Kind, ExprRole);
pub(crate) type ExprItemMut<'a> = (&'static str, &'a mut Expr, Kind, ExprRole);
pub(crate) type RefItem<'a> = Ref<&'a SketchId, &'a FeatureName, &'a BodyName>;
pub(crate) type RefItemMut<'a> = Ref<&'a mut SketchId, &'a mut FeatureName, &'a mut BodyName>;
pub(crate) type GeoItem<'a> = Geo<&'a Vec3, &'a PlaneRef, &'a GeoRef, &'a PointExpr, &'a ProfileSel>;
pub(crate) type GeoItemMut<'a> = Geo<&'a mut Vec3, &'a mut PlaneRef, &'a mut GeoRef, &'a mut PointExpr, &'a mut ProfileSel>;
@@ -56,6 +56,52 @@ impl Walk for crate::document::RuledMode {}
impl Walk for crate::document::RuledCorners {}
impl Walk for crate::document::ExtrudeConvention {}
/// A reference walks itself: a sketch, a feature or a body.
macro_rules! reference {
($t:ty, $role:ident) => {
impl Walk for $t {
fn walk_refs<'a>(&'a self, f: &mut dyn FnMut(RefItem<'a>)) {
f(Ref::$role(self))
}
fn walk_refs_mut<'a>(&'a mut self, f: &mut dyn FnMut(RefItemMut<'a>)) {
f(Ref::$role(self))
}
}
};
}
reference!(SketchId, Sketch);
reference!(FeatureName, Feature);
reference!(BodyName, Body);
/// A point walks itself with its role: a face pick, a position or a direction. As a plain
/// `Vec3` it also serves a field whose attribute gives it another role (`pinned`, `pick_if`).
macro_rules! point {
($t:ty, $role:ident) => {
impl Walk for $t {
fn walk_geometry<'a>(&'a self, f: &mut dyn FnMut(GeoItem<'a>)) {
f(Geo::$role(&self.0))
}
fn walk_geometry_mut<'a>(&'a mut self, f: &mut dyn FnMut(GeoItemMut<'a>)) {
f(Geo::$role(&mut self.0))
}
}
impl Each<Vec3> for $t {
fn each<'a>(&'a self, f: &mut dyn FnMut(&'a Vec3)) {
f(&self.0)
}
fn each_mut<'a>(&'a mut self, f: &mut dyn FnMut(&'a mut Vec3)) {
f(&mut self.0)
}
}
};
}
point!(FacePick, Pick);
point!(Position, Pos);
point!(Dir, Dir);
/// Containers walk what they hold, in order.
macro_rules! walk_each {
($([$($g:tt)*] $t:ty),*) => {$(
@@ -182,36 +228,36 @@ impl<T> Each<T> for Option<Vec<T>> {
/// The expressions of an expression field, each with its index and their count (for its label).
pub(crate) trait ExprField {
fn each<'a>(&'a self, f: &mut dyn FnMut(usize, usize, &'a String));
fn each_mut<'a>(&'a mut self, f: &mut dyn FnMut(usize, usize, &'a mut String));
fn each<'a>(&'a self, f: &mut dyn FnMut(usize, usize, &'a Expr));
fn each_mut<'a>(&'a mut self, f: &mut dyn FnMut(usize, usize, &'a mut Expr));
}
impl ExprField for String {
fn each<'a>(&'a self, f: &mut dyn FnMut(usize, usize, &'a String)) {
impl ExprField for Expr {
fn each<'a>(&'a self, f: &mut dyn FnMut(usize, usize, &'a Expr)) {
f(0, 1, self)
}
fn each_mut<'a>(&'a mut self, f: &mut dyn FnMut(usize, usize, &'a mut String)) {
fn each_mut<'a>(&'a mut self, f: &mut dyn FnMut(usize, usize, &'a mut Expr)) {
f(0, 1, self)
}
}
/// A position along an edge with its radius (a variable fillet's mid radii).
impl ExprField for (f64, String) {
fn each<'a>(&'a self, f: &mut dyn FnMut(usize, usize, &'a String)) {
impl ExprField for (f64, Expr) {
fn each<'a>(&'a self, f: &mut dyn FnMut(usize, usize, &'a Expr)) {
f(0, 1, &self.1)
}
fn each_mut<'a>(&'a mut self, f: &mut dyn FnMut(usize, usize, &'a mut String)) {
fn each_mut<'a>(&'a mut self, f: &mut dyn FnMut(usize, usize, &'a mut Expr)) {
f(0, 1, &mut self.1)
}
}
impl<T: ExprField> ExprField for Option<T> {
fn each<'a>(&'a self, f: &mut dyn FnMut(usize, usize, &'a String)) {
fn each<'a>(&'a self, f: &mut dyn FnMut(usize, usize, &'a Expr)) {
if let Some(t) = self {
t.each(f)
}
}
fn each_mut<'a>(&'a mut self, f: &mut dyn FnMut(usize, usize, &'a mut String)) {
fn each_mut<'a>(&'a mut self, f: &mut dyn FnMut(usize, usize, &'a mut Expr)) {
if let Some(t) = self {
t.each_mut(f)
}
@@ -222,13 +268,13 @@ impl<T: ExprField> ExprField for Option<T> {
macro_rules! expr_list {
($([$($g:tt)*] $t:ty),*) => {$(
impl<$($g)*> ExprField for $t {
fn each<'a>(&'a self, f: &mut dyn FnMut(usize, usize, &'a String)) {
fn each<'a>(&'a self, f: &mut dyn FnMut(usize, usize, &'a Expr)) {
let n = self.len();
for (i, t) in self.iter().enumerate() {
t.each(&mut |_, _, e| f(i, n, e));
}
}
fn each_mut<'a>(&'a mut self, f: &mut dyn FnMut(usize, usize, &'a mut String)) {
fn each_mut<'a>(&'a mut self, f: &mut dyn FnMut(usize, usize, &'a mut Expr)) {
let n = self.len();
for (i, t) in self.iter_mut().enumerate() {
t.each_mut(&mut |_, _, e| f(i, n, e));
+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)]
+1 -1
View File
@@ -224,7 +224,7 @@ fn bare_numbers_beside_a_unitless_expression_stay_bare() {
assert_eq!(inputs(&s, 0)[2], "(16) / (2 * 4)", "the revolutions");
assert!(s.model.result(s.doc.features[0].id).is_some_and(|r| r.error.is_none()));
run(&mut s, "solid.box", json!({"length": 20, "width": 10, "height": 10, "center": ["2 * 5", 0, 0], "body_name": "B"}));
assert_eq!(s.doc.features[1].kind.point_expr().map(|p| p[0].clone()), Some("(2 * 5) - (20) / 2".to_string()));
assert_eq!(s.doc.features[1].kind.point_expr().map(|p| p[0].to_string()), Some("(2 * 5) - (20) / 2".to_string()));
let b = shape(&mut s, "B");
assert!(b.bbox[0].abs() < 1e-6 && (b.bbox[3] - 20.0).abs() < 1e-6, "{b:?}");
}
+6 -6
View File
@@ -200,8 +200,8 @@ impl Mapper<'_> {
fn make_expr(&mut self, m: &Mat, v: &Value) -> Option<Value> {
let a = v.as_array().filter(|a| a.len() == 3)?;
let c = |i: usize| match a.get(i)? {
Value::Number(n) => n.as_f64().filter(|x| x.is_finite()).map(|x| format!("{x}")),
Value::String(e) => Some(e.clone()),
Value::Number(n) => n.as_f64().filter(|x| x.is_finite()).map(|x| solidcraft_doc::Expr::new(format!("{x}"))),
Value::String(e) => Some(solidcraft_doc::Expr::new(e.as_str())),
_ => None,
};
let mut p = [c(0)?, c(1)?, c(2)?];
@@ -349,9 +349,9 @@ pub fn to_local(s: &Session, id: &str, params: &Value) -> Option<Value> {
&& let solidcraft_doc::FeatureKind::Revolve { axis: solidcraft_doc::AxisRef::Line { origin, dir }, .. } = &feature.kind
{
let frame = context.active_component_frame();
let displayed = serde_json::json!({"origin":pt(apply_point(&frame,*origin)),"dir":pt(apply_vector(&frame,*dir))});
let displayed = serde_json::json!({"origin":pt(apply_point(&frame,origin.0)),"dir":pt(apply_vector(&frame,dir.0))});
if params.get("params").and_then(|p| p.get("axis")) == Some(&displayed) {
inner["axis"] = serde_json::json!({"origin":pt(*origin),"dir":pt(*dir)});
inner["axis"] = serde_json::json!({"origin":pt(origin.0),"dir":pt(dir.0)});
}
}
let points = match (&feature.kind, cmd) {
@@ -361,9 +361,9 @@ pub fn to_local(s: &Session, id: &str, params: &Value) -> Option<Value> {
};
for (key, point) in points {
if let Some(point) = point {
let displayed = pt(apply_point(&context.active_component_frame(), point));
let displayed = pt(apply_point(&context.active_component_frame(), point.0));
if params.get("params").and_then(|p| p.get(key)) == Some(&displayed) {
inner[key] = pt(point);
inner[key] = pt(point.0);
}
}
}
+4 -3
View File
@@ -7,6 +7,7 @@
//! `SOLIDCRAFT_BENCH_PARTS=400 cargo test --release -p solidcraft-engine bench_generated_assembly -- --ignored --nocapture`
use serde_json::json;
use solidcraft_doc::BodyName;
use solidcraft_doc::{Document, FeatureKind, Operation, mat_inverse, point_expr, rigid};
use solidcraft_geom::Vec3;
@@ -24,7 +25,7 @@ fn generated_assembly(parts: usize) -> Document {
(id, m)
})
.collect();
let mm = |v: f64| format!("{v} mm");
let mm = |v: f64| solidcraft_doc::Expr::new(format!("{v} mm"));
let mut made = Vec::new();
for i in 0..parts {
let o = Vec3::new(30.0 * (i % 20) as f64, 30.0 * (i / 20) as f64, 0.0);
@@ -33,7 +34,7 @@ fn generated_assembly(parts: usize) -> Document {
for (k, x) in [2.0, 9.0, 16.0].into_iter().enumerate() {
let pin = FeatureKind::Cylinder {
base: point_expr(o + Vec3::new(x, 20.0, 0.0)),
axis: Vec3::Z,
axis: solidcraft_doc::Dir(Vec3::Z),
radius: mm(1.5),
height: mm(4.0 + k as f64),
operation: Operation::NewBody,
@@ -47,7 +48,7 @@ fn generated_assembly(parts: usize) -> Document {
for (part, feature) in made {
let (to, m) = comps[part / 4 % groups];
for b in state.bodies.iter().filter(|b| b.feature == feature) {
let kind = FeatureKind::BodyTransfer { bodies: vec![b.name.clone()], from: 0, to, transform: mat_inverse(&m).unwrap() };
let kind = FeatureKind::BodyTransfer { bodies: vec![BodyName::from(&b.name)], from: 0, to, transform: mat_inverse(&m).unwrap() };
doc.add_feature(kind, None).unwrap();
}
}
+1 -1
View File
@@ -258,7 +258,7 @@ impl Session {
let mut removed = std::collections::BTreeSet::new();
for feature in self.doc.features.iter().skip(marker).filter(|f| !f.suppressed) {
if let FeatureKind::Remove { bodies } = &feature.kind {
removed.extend(bodies.iter().filter(|b| !kept.contains(b.as_str()) && shown.body(b).is_some()).cloned());
removed.extend(bodies.iter().filter(|b| !kept.contains(b.0.as_str()) && shown.body(&b.0).is_some()).map(|b| b.0.clone()));
}
}
if removed.is_empty() || self.doc.occurrences.is_empty() {
+5 -4
View File
@@ -1,6 +1,7 @@
//! Helpers to read hostile JSON parameters.
use serde_json::Value;
use solidcraft_doc::Expr;
use solidcraft_geom::{Vec2, Vec3};
use crate::{EngineError, Result};
@@ -58,15 +59,15 @@ pub fn vec2_list(cmd: &str, p: &Value, k: &str, max: usize) -> Result<Vec<Vec2>>
}
/// An expression parameter: a number (mm or degrees by context) or an expression string.
pub fn expr(p: &Value, k: &str) -> Option<String> {
pub fn expr(p: &Value, k: &str) -> Option<Expr> {
match p.get(k)? {
Value::Number(n) => n.as_f64().and_then(finite).map(|v| format!("{v}")),
Value::String(s) if !s.trim().is_empty() && s.len() <= 4096 => Some(s.trim().to_string()),
Value::Number(n) => n.as_f64().and_then(finite).map(|v| Expr::new(format!("{v}"))),
Value::String(s) if !s.trim().is_empty() && s.len() <= 4096 => Some(Expr::new(s.trim())),
_ => None,
}
}
pub fn req_expr(cmd: &str, p: &Value, k: &str) -> Result<String> {
pub fn req_expr(cmd: &str, p: &Value, k: &str) -> Result<Expr> {
expr(p, k).ok_or_else(|| bad(cmd, format!("`{k}` must be a number or an expression")))
}
+1 -1
View File
@@ -121,7 +121,7 @@ fn enclosure_arc_extinction_keeps_a_real_sharp_inner_lip_after_resize_and_reopen
let FeatureKind::Extrude { sketch, profiles, .. } = &feature.kind else {
panic!("lip extrusion");
};
let solved = state.sketch(*sketch).unwrap();
let solved = state.sketch(sketch.0).unwrap();
assert!((solved.plane.origin.z - 30.).abs() < 1e-8);
let inner = &solved.sketch.offsets[1];
assert_eq!(inner.outputs.len(), 4);
+3 -3
View File
@@ -859,7 +859,7 @@ mod tests {
.unwrap();
doc.add_feature(
FeatureKind::Fillet {
edges: vec![Vec3::new(0.0, 0.0, 5.0)],
edges: vec![solidcraft_doc::Position(Vec3::new(0.0, 0.0, 5.0))],
radius: "4".into(),
body: None,
style: Default::default(),
@@ -871,7 +871,7 @@ mod tests {
doc.add_feature(
FeatureKind::Cylinder {
base: point_expr(Vec3::new(80.0, 0.0, 0.0)),
axis: Vec3::Z,
axis: solidcraft_doc::Dir(Vec3::Z),
radius: "6".into(),
height: "15".into(),
operation: Operation::NewBody,
@@ -943,7 +943,7 @@ mod tests {
.unwrap();
doc.add_feature(
FeatureKind::Fillet {
edges: vec![Vec3::new(0.1, -0.3, 5.0 + 1.0 / 3.0)],
edges: vec![solidcraft_doc::Position(Vec3::new(0.1, -0.3, 5.0 + 1.0 / 3.0))],
radius: "4".into(),
body: None,
style: Default::default(),
+100 -84
View File
@@ -637,7 +637,7 @@ impl Dialog {
let preselection: &[Sel] = if matches!(d.kind, Kind::Text(_)) { &[] } else { &s.selection };
for sel in preselection {
let as_body = match sel {
Sel::Face { body, .. } | Sel::Edge { body, .. } | Sel::Vertex { body, .. } => Some(Sel::Body { name: body.clone() }),
Sel::Face { body, .. } | Sel::Edge { body, .. } | Sel::Vertex { body, .. } => Some(Sel::Body { name: body.to_string() }),
_ => None,
};
if let Some(b) = as_body
@@ -3343,7 +3343,7 @@ pub fn apply_commands(app: &SolveApp, d: &Dialog) -> Result<Vec<(String, Value)>
};
if let Some(sketch) = original_all
&& !d.profile_selection_changed
&& let Some(ss) = app.session.world_state().sketch(sketch)
&& let Some(ss) = app.session.world_state().sketch(sketch.0)
{
let mut selected: Vec<usize> = sels(d, 0)
.iter()
@@ -3483,7 +3483,7 @@ fn dialog_commands(app: &SolveApp, d: &Dialog) -> Result<Vec<(String, Value)>, S
_ => None,
});
let has_all = |id: u64| curves.iter().all(|c| st.sketch(id).is_some_and(|ss| ss.sketch.curve_index(c).is_some()));
let sketch = match sketch.or(edited) {
let sketch = match sketch.or(edited.map(u64::from)) {
Some(id) if has_all(id) => id,
_ => curves_sketch(app, &curves, None).ok_or("the curves must be in one sketch")?,
};
@@ -4248,14 +4248,15 @@ fn emboss_face_sel(s: &Session, component: u64, face: &solidcraft_engine::doc::E
let owner = s.doc.model_body_component(body);
let world_name = s.doc.world_body_name(&source, &face.body, owner, &face.path);
let author = solidcraft_engine::doc::EmbossFace::component_frame(&s.doc, component, &face.author_path).ok()?;
let point = solidcraft_engine::doc::apply_point(&author, face.at);
let point = solidcraft_engine::doc::apply_point(&author, face.at.0);
s.world_state().body(&world_name)?;
Some(Sel::Face { body: world_name, index, point })
}
pub(crate) fn plane_sel(s: &Session, pl: &PlaneRef) -> Option<Sel> {
match pl {
PlaneRef::Origin { name } | PlaneRef::Construction { name } => Some(Sel::Plane { name: name.clone() }),
PlaneRef::Origin { name } => Some(Sel::Plane { name: name.clone() }),
PlaneRef::Construction { name } => Some(Sel::Plane { name: name.to_string() }),
PlaneRef::Custom { plane } | PlaneRef::Face { plane, .. } => face_sel(s, plane.origin),
_ => None,
}
@@ -4299,7 +4300,7 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option<usize>) -> Option<Dia
FeatureKind::Extrude { sketch, profiles, extent, operation, targets, thin } => {
let mut d = start("solid.extrude")?;
d.kind = Kind::Extrude {
distance: extent.distance.clone(),
distance: extent.distance.to_string(),
direction: match (extent.direction, &extent.distance2) {
(_, Some(_)) => TWO_SIDES,
(Direction::Positive, None) => 0,
@@ -4309,16 +4310,16 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option<usize>) -> Option<Dia
operation: op_index(operation),
auto_op: false,
smart: crate::smart_extrude::Smart { manual_dir: true, ..Default::default() },
distance2: extent.distance2.clone().unwrap_or_else(|| "10 mm".into()),
taper: extent.taper.clone().unwrap_or_else(|| "0 deg".into()),
distance2: extent.distance2.as_deref().map(str::to_string).unwrap_or_else(|| "10 mm".into()),
taper: extent.taper.as_deref().map(str::to_string).unwrap_or_else(|| "0 deg".into()),
taper_present: extent.taper.is_some(),
to_offset: extent.to_offset.clone().unwrap_or_default(),
start: extent.start_offset.clone().unwrap_or_default(),
to_offset: extent.to_offset.as_deref().map(str::to_string).unwrap_or_default(),
start: extent.start_offset.as_deref().map(str::to_string).unwrap_or_default(),
all: extent.through_all,
thin: thin.is_some(),
wall: thin.as_ref().map_or_else(|| "2 mm".into(), |t| t.thickness.clone()),
wall: thin.as_ref().map_or_else(|| "2 mm".into(), |t| t.thickness.to_string()),
wall_side: thin.as_ref().map_or(0, |t| WALL_SIDES.iter().position(|w| ThinSide::parse(w) == Some(t.side)).unwrap_or(0)),
wall2: thin.as_ref().and_then(|t| t.thickness2.clone()).unwrap_or_else(|| "1 mm".into()),
wall2: thin.as_ref().and_then(|t| t.thickness2.as_deref().map(str::to_string)).unwrap_or_else(|| "1 mm".into()),
};
d.extra.insert("convention".into(), json!(extent.convention));
// Extent references belong to the extrude's authoring frame, including free
@@ -4328,7 +4329,7 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option<usize>) -> Option<Dia
if let Some(point) = point {
set_object(&mut d.inputs, &mut d.active, label, true);
if let Some(input) = d.inputs.iter_mut().find(|i| i.label == label) {
input.items = vec![Sel::Vertex { body: String::new(), point: solidcraft_engine::doc::apply_point(&frame, point) }];
input.items = vec![Sel::Vertex { body: String::new(), point: solidcraft_engine::doc::apply_point(&frame, point.0) }];
}
}
}
@@ -4338,9 +4339,9 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option<usize>) -> Option<Dia
inp.items = thin.curves.iter().map(|id| Sel::SketchCurve { id: id.clone() }).collect();
}
} else {
let items = st.sketch(*sketch).map(|ss| profile_indices(ss, profiles)).unwrap_or_default();
let items = st.sketch(sketch.0).map(|ss| profile_indices(ss, profiles)).unwrap_or_default();
if let Some(inp) = d.inputs.first_mut() {
inp.items = items.into_iter().map(|index| Sel::Profile { sketch: *sketch, index }).collect();
inp.items = items.into_iter().map(|index| Sel::Profile { sketch: sketch.0, index }).collect();
}
}
if !targets.is_empty() {
@@ -4353,12 +4354,12 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option<usize>) -> Option<Dia
let frame = feature_frame(s, f.component);
let stored_axis = match axis {
AxisRef::Line { origin, dir } => Some(json!({
"origin": pt(solidcraft_engine::doc::apply_point(&frame, *origin)),
"dir": pt(solidcraft_engine::doc::apply_vector(&frame, *dir))
"origin": pt(solidcraft_engine::doc::apply_point(&frame, origin.0)),
"dir": pt(solidcraft_engine::doc::apply_vector(&frame, dir.0))
})),
_ => None,
};
d.kind = Kind::Revolve { angle: angle.clone(), operation: op_index(operation), axis: stored_axis };
d.kind = Kind::Revolve { angle: angle.to_string(), operation: op_index(operation), axis: stored_axis };
d.extra.insert("convention".into(), json!(convention));
if let Some(a2) = angle2 {
d.extra.insert("angle2".into(), json!(a2));
@@ -4369,12 +4370,12 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option<usize>) -> Option<Dia
if let Some(point) = to {
set_to_object(&mut d.inputs, &mut d.active, true);
if let Some(input) = d.inputs.iter_mut().find(|i| i.label == TO_OBJECT) {
input.items = vec![Sel::Vertex { body: String::new(), point: solidcraft_engine::doc::apply_point(&frame, *point) }];
input.items = vec![Sel::Vertex { body: String::new(), point: solidcraft_engine::doc::apply_point(&frame, point.0) }];
}
}
let items = st.sketch(*sketch).map(|ss| profile_indices(ss, profiles)).unwrap_or_default();
let items = st.sketch(sketch.0).map(|ss| profile_indices(ss, profiles)).unwrap_or_default();
if let Some(inp) = d.inputs.get_mut(0) {
inp.items = items.into_iter().map(|index| Sel::Profile { sketch: *sketch, index }).collect();
inp.items = items.into_iter().map(|index| Sel::Profile { sketch: sketch.0, index }).collect();
}
let ax = match axis {
AxisRef::World { axis } => Some(Sel::Axis { name: axis.to_ascii_uppercase() }),
@@ -4400,10 +4401,10 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option<usize>) -> Option<Dia
if let FeatureKind::Chamfer { distance2: d2, angle: a, flip: f, .. } = &f.kind {
if let Some(v) = d2 {
*ctype = 1;
*distance2 = v.clone();
*distance2 = v.to_string();
} else if let Some(v) = a {
*ctype = 2;
*angle = v.clone();
*angle = v.to_string();
}
*flip = *f;
}
@@ -4446,51 +4447,51 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option<usize>) -> Option<Dia
FilletStyle::Constant | FilletStyle::Chord | FilletStyle::Curvature => {}
}
// The start was the first edge's last point: ends swapped.
let first_end = edges.first().and_then(|p| edge_sel(s, *p)).and_then(|x| match x {
let first_end = edges.first().and_then(|p| edge_sel(s, p.0)).and_then(|x| match x {
Sel::Edge { body, index, .. } => {
s.world_state().body(&body).and_then(|b| b.mesh().edges.get(index).and_then(|e| e.last().copied()))
}
_ => None,
});
let swapped = first_end.is_some_and(|e| e.dist(*start) < 1e-6);
let swapped = first_end.is_some_and(|e| e.dist(start.0) < 1e-6);
d.extra.insert("swap_ends".into(), json!(swapped));
}
}
}
if let Some(inp) = d.inputs.first_mut() {
inp.items = edges.iter().filter_map(|p| edge_sel(s, *p)).collect();
inp.items = edges.iter().filter_map(|p| edge_sel(s, p.0)).collect();
}
d
}
FeatureKind::Shell { faces, thickness, direction, tangent_chain, .. } => {
let mut d = start("solid.shell")?;
d.kind = Kind::Shell { thickness: thickness.clone() };
d.kind = Kind::Shell { thickness: thickness.to_string() };
d.extra.insert("tangent_chain".into(), json!(tangent_chain));
if !direction.is_empty() && direction != "inside" {
d.extra.insert("direction".into(), json!(direction));
}
if let Some(inp) = d.inputs.first_mut() {
inp.items = faces.iter().filter_map(|p| face_sel(s, *p)).collect();
inp.items = faces.iter().filter_map(|p| face_sel(s, p.0)).collect();
}
d
}
FeatureKind::Draft { faces, angle, neutral, pull, .. } => {
let mut d = start("solid.draft")?;
d.kind = Kind::Draft { angle: angle.clone() };
d.kind = Kind::Draft { angle: angle.to_string() };
if let Some(inp) = d.inputs.get_mut(0) {
inp.items = faces.iter().filter_map(|p| face_sel(s, *p)).collect();
inp.items = faces.iter().filter_map(|p| face_sel(s, p.0)).collect();
}
if let Some(inp) = d.inputs.get_mut(1) {
inp.items = plane_sel(s, neutral).into_iter().collect();
}
d.extra.insert("pull".into(), pt3(*pull));
d.extra.insert("pull".into(), pt3(pull.0));
d
}
FeatureKind::Hole { position, direction, diameter, depth, hole, points, thread, to } => {
let mut d = start("solid.hole")?;
let mut k = hole_defaults();
if let Kind::Hole { diameter: dia, depth: dep, kind, cb_diameter, cb_depth, cs_diameter, cs_angle, opts } = &mut k {
*dia = diameter.clone();
*dia = diameter.to_string();
opts.all = depth.is_none();
opts.angled = matches!(hole, HoleKind::Drilled { .. });
opts.multiple = points.is_some();
@@ -4499,21 +4500,21 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option<usize>) -> Option<Dia
opts.thread = th.clone();
}
if let Some(dd) = depth {
*dep = dd.clone();
*dep = dd.to_string();
}
match hole {
HoleKind::Counterbore { cb_diameter: a, cb_depth: b } => {
*kind = 1;
*cb_diameter = a.clone();
*cb_depth = b.clone();
*cb_diameter = a.to_string();
*cb_depth = b.to_string();
}
HoleKind::Countersink { cs_diameter: a, cs_angle: b } => {
*kind = 2;
*cs_diameter = a.clone();
*cs_angle = b.clone();
*cs_diameter = a.to_string();
*cs_angle = b.to_string();
}
HoleKind::Drilled { tip_angle } => {
opts.tip_angle = tip_angle.clone();
opts.tip_angle = tip_angle.to_string();
}
HoleKind::Simple => {}
}
@@ -4531,16 +4532,16 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option<usize>) -> Option<Dia
}
None => {
if let Some(inp) = d.inputs.first_mut() {
inp.items = vec![face_sel(s, *position).unwrap_or(Sel::Face { body: String::new(), index: 0, point: *position })];
inp.items = vec![face_sel(s, position.0).unwrap_or(Sel::Face { body: String::new(), index: 0, point: position.0 })];
}
d.extra.insert("direction".into(), pt3(*direction));
d.extra.insert("direction".into(), pt3(direction.0));
}
}
if to.is_some() {
if let Kind::Hole { opts, .. } = &mut d.kind {
opts.all = false;
}
with_to_object(&mut d, s, *to);
with_to_object(&mut d, s, to.map(Vec3::from));
}
// A hole at every point of a sketch.
if points.as_ref().is_some_and(|sp| sp.ids.is_empty()) {
@@ -4568,7 +4569,7 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option<usize>) -> Option<Dia
at: None,
};
d.extra.insert("base".into(), pt_expr(base));
d.extra.insert("axis".into(), pt3(*axis));
d.extra.insert("axis".into(), pt3(axis.0));
d
}
FeatureKind::Sphere { center, radius, operation } => {
@@ -4593,10 +4594,10 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option<usize>) -> Option<Dia
let mut d = start("solid.boundary_fill")?;
// The cells again, ticked where a stored cell point lies in the same cell.
let before = s.model.state_before(f.id);
let found = fill_cells(&before, tools);
let found = fill_cells(&before, &solidcraft_engine::doc::plain(tools));
let filled: Vec<String> =
cells.iter().filter_map(|c| found.iter().min_by(|a, b| a.0.dist(*c).total_cmp(&b.0.dist(*c))).map(|x| x.1.clone())).collect();
let items: Vec<Sel> = tools.iter().map(|n| Sel::Body { name: n.clone() }).collect();
cells.iter().filter_map(|c| found.iter().min_by(|a, b| a.0.dist(c.0).total_cmp(&b.0.dist(c.0))).map(|x| x.1.clone())).collect();
let items: Vec<Sel> = tools.iter().map(|n| Sel::Body { name: n.to_string() }).collect();
d.kind = Kind::BoundaryFill {
operation: op_index(operation),
cells: found.into_iter().map(|(p, l)| (p, l.clone(), filled.contains(&l))).collect(),
@@ -4614,10 +4615,10 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option<usize>) -> Option<Dia
let mut d = start("solid.combine")?;
d.kind = Kind::Combine { operation: op_index(operation).max(1), keep_tools: *keep_tools };
if let Some(inp) = d.inputs.get_mut(0) {
inp.items = vec![Sel::Body { name: target.clone() }];
inp.items = vec![Sel::Body { name: target.to_string() }];
}
if let Some(inp) = d.inputs.get_mut(1) {
inp.items = tools.iter().map(|n| Sel::Body { name: n.clone() }).collect();
inp.items = tools.iter().map(|n| Sel::Body { name: n.to_string() }).collect();
}
d
}
@@ -4628,7 +4629,7 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option<usize>) -> Option<Dia
let as_features = ids.iter().any(|id| !st.bodies.iter().any(|b| b.feature == *id));
if let Some(inp) = d.inputs.get_mut(0) {
if !bodies.is_empty() {
inp.items = bodies.iter().map(|name| Sel::Body { name: name.clone() }).collect();
inp.items = bodies.iter().map(|name| Sel::Body { name: name.to_string() }).collect();
} else if as_features {
inp.accept = selection::FEATURES;
inp.label = objects_label(inp.accept);
@@ -4647,28 +4648,28 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option<usize>) -> Option<Dia
let bodies: Vec<Sel> = if copied.is_empty() {
st.bodies.iter().filter(|b| ids.contains(&b.feature)).map(|b| Sel::Body { name: b.name.clone() }).collect()
} else {
copied.iter().map(|name| Sel::Body { name: name.clone() }).collect()
copied.iter().map(|name| Sel::Body { name: name.to_string() }).collect()
};
let mut d = match pattern {
PatternKind::Rectangular { dir1, count1, spacing1, dir2, count2, spacing2 } => {
let mut d = start("solid.pattern.rectangular")?;
d.kind = Kind::PatternRect {
count: count1.clone(),
spacing: spacing1.clone(),
count2: count2.clone().unwrap_or_else(|| "1".into()),
spacing2: spacing2.clone().unwrap_or_else(|| "20 mm".into()),
count: count1.to_string(),
spacing: spacing1.to_string(),
count2: count2.as_deref().map(str::to_string).unwrap_or_else(|| "1".into()),
spacing2: spacing2.as_deref().map(str::to_string).unwrap_or_else(|| "20 mm".into()),
};
// Directions along the origin axes show as those axes; others are kept as is.
for (i, (key, dir)) in [("dir1", Some(*dir1)), ("dir2", *dir2)].into_iter().enumerate() {
let Some(dir) = dir else { continue };
match world_axis(dir) {
match world_axis(dir.0) {
Some(name) => {
if let Some(inp) = d.inputs.get_mut(i + 1) {
inp.items = vec![Sel::Axis { name }];
}
}
None => {
d.extra.insert(key.into(), pt3(dir));
d.extra.insert(key.into(), pt3(dir.0));
}
}
}
@@ -4676,15 +4677,15 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option<usize>) -> Option<Dia
}
PatternKind::Circular { origin, axis, count, angle } => {
let mut d = start("solid.pattern.circular")?;
d.kind = Kind::PatternCirc { count: count.clone(), angle: angle.clone() };
match world_axis(*axis).filter(|_| origin.len() < 1e-9) {
d.kind = Kind::PatternCirc { count: count.to_string(), angle: angle.to_string() };
match world_axis(axis.0).filter(|_| origin.len() < 1e-9) {
Some(name) => {
if let Some(inp) = d.inputs.get_mut(1) {
inp.items = vec![Sel::Axis { name }];
}
}
None => {
d.extra.insert("axis".into(), json!({"origin": pt3(*origin), "dir": pt3(*axis)}));
d.extra.insert("axis".into(), json!({"origin": pt3(origin.0), "dir": pt3(axis.0)}));
}
}
d
@@ -4712,8 +4713,8 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option<usize>) -> Option<Dia
inp.items = sections
.iter()
.flat_map(|sec| {
let idx = st.sketch(sec.sketch).map(|ss| profile_indices(ss, &sec.profiles)).unwrap_or_default();
idx.into_iter().map(move |index| Sel::Profile { sketch: sec.sketch, index })
let idx = st.sketch(sec.sketch.0).map(|ss| profile_indices(ss, &sec.profiles)).unwrap_or_default();
idx.into_iter().map(move |index| Sel::Profile { sketch: sec.sketch.0, index })
})
.collect();
}
@@ -4729,9 +4730,9 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option<usize>) -> Option<Dia
if let Some(inp) = d.inputs.get_mut(2) {
inp.items = direction.into_iter().collect();
}
let items = st.sketch(*sketch).map(|ss| profile_indices(ss, profiles)).unwrap_or_default();
let items = st.sketch(sketch.0).map(|ss| profile_indices(ss, profiles)).unwrap_or_default();
if let Some(inp) = d.inputs.get_mut(0) {
inp.items = items.into_iter().map(|index| Sel::Profile { sketch: *sketch, index }).collect();
inp.items = items.into_iter().map(|index| Sel::Profile { sketch: sketch.0, index }).collect();
}
if let Some(inp) = d.inputs.get_mut(1) {
inp.items = path.iter().map(|id| Sel::SketchCurve { id: id.clone() }).collect();
@@ -4743,7 +4744,7 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option<usize>) -> Option<Dia
}
FeatureKind::ConstructionPlane { plane: PlaneRef::Offset { base, distance } } => {
let mut d = start("construct.plane.offset")?;
d.kind = Kind::OffsetPlane { offset: distance.clone() };
d.kind = Kind::OffsetPlane { offset: distance.to_string() };
if let Some(inp) = d.inputs.first_mut() {
inp.items = plane_sel(s, base).into_iter().collect();
}
@@ -4760,7 +4761,7 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option<usize>) -> Option<Dia
}
_ => return None,
};
d.kind = Kind::PathPlane { t: t.clone(), sketch };
d.kind = Kind::PathPlane { t: t.to_string(), sketch };
if let Some(input) = d.inputs.first_mut() {
input.items = vec![item];
}
@@ -4768,7 +4769,7 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option<usize>) -> Option<Dia
}
FeatureKind::ConstructionPlane { plane: PlaneRef::AtAngle { base, axis_origin, axis_dir, angle } } => {
let mut d = start("construct.plane.at_angle")?;
d.kind = Kind::AnglePlane { angle: angle.clone() };
d.kind = Kind::AnglePlane { angle: angle.to_string() };
if let Some(inp) = d.inputs.first_mut() {
inp.items = plane_sel(s, base).into_iter().collect();
}
@@ -4787,11 +4788,11 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option<usize>) -> Option<Dia
FeatureKind::Split { body, plane, tool } => {
let mut d = start("solid.split_body")?;
if let Some(inp) = d.inputs.first_mut() {
inp.items = vec![Sel::Body { name: body.clone() }];
inp.items = vec![Sel::Body { name: body.to_string() }];
}
if let Some(inp) = d.inputs.get_mut(1) {
inp.items = match tool {
Some(t) => face_sel(s, t.point).into_iter().collect(),
Some(t) => face_sel(s, t.point.0).into_iter().collect(),
None => plane_sel(s, plane).into_iter().collect(),
};
}
@@ -4799,11 +4800,11 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option<usize>) -> Option<Dia
}
FeatureKind::Scale { bodies, origin, factor, factors } => {
let mut d = start("solid.scale")?;
d.kind = Kind::Scale { factor: factor.clone() };
d.kind = Kind::Scale { factor: factor.to_string() };
if let Some(inp) = d.inputs.first_mut() {
inp.items = bodies.iter().map(|n| Sel::Body { name: n.clone() }).collect();
inp.items = bodies.iter().map(|n| Sel::Body { name: n.to_string() }).collect();
}
d.extra.insert("origin".into(), pt3(*origin));
d.extra.insert("origin".into(), pt3(origin.0));
if let Some(f) = factors {
d.extra.insert("factors".into(), json!(f));
}
@@ -4811,9 +4812,9 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option<usize>) -> Option<Dia
}
FeatureKind::OffsetFace { faces, distance, body } => {
let mut d = start("solid.offset_face")?;
d.kind = Kind::OffsetFaces { distance: distance.clone() };
d.kind = Kind::OffsetFaces { distance: distance.to_string() };
if let Some(inp) = d.inputs.first_mut() {
inp.items = faces.iter().filter_map(|p| face_sel(s, *p)).collect();
inp.items = faces.iter().filter_map(|p| face_sel(s, p.0)).collect();
}
if let Some(b) = body {
d.extra.insert("body".into(), json!(b));
@@ -4825,23 +4826,26 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option<usize>) -> Option<Dia
if *modeled {
d.extra.insert("modeled".into(), json!(true));
}
d.kind = Kind::Thread { designation: designation.clone().unwrap_or_default(), length: length.clone().unwrap_or_default() };
d.kind = Kind::Thread {
designation: designation.clone().unwrap_or_default(),
length: length.as_deref().map(str::to_string).unwrap_or_default(),
};
if let Some(inp) = d.inputs.first_mut() {
inp.items = face_sel(s, *face).into_iter().collect();
inp.items = face_sel(s, face.0).into_iter().collect();
}
d
}
FeatureKind::BoundingSolid { bodies, margin } => {
let mut d = start("solid.bounding_solid")?;
d.kind = Kind::Stock { margin: margin.clone() };
d.kind = Kind::Stock { margin: margin.to_string() };
if let Some(inp) = d.inputs.first_mut() {
inp.items = bodies.iter().map(|n| Sel::Body { name: n.clone() }).collect();
inp.items = bodies.iter().map(|n| Sel::Body { name: n.to_string() }).collect();
}
d
}
FeatureKind::Pipe { path, diameter, wall, operation, targets, .. } => {
let mut d = start("solid.pipe")?;
d.kind = Kind::Pipe { diameter: diameter.clone(), wall: wall.clone().unwrap_or_default() };
d.kind = Kind::Pipe { diameter: diameter.to_string(), wall: wall.as_deref().map(str::to_string).unwrap_or_default() };
if let Some(inp) = d.inputs.first_mut() {
inp.items = path.iter().map(|id| Sel::SketchCurve { id: id.clone() }).collect();
}
@@ -4853,10 +4857,10 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option<usize>) -> Option<Dia
}
FeatureKind::Emboss { sketch, profiles, depth, deboss, targets, face } => {
let mut d = start("solid.emboss")?;
d.kind = Kind::Emboss { depth: depth.clone(), deboss: *deboss };
let items = st.sketch(*sketch).map(|ss| profile_indices(ss, profiles)).unwrap_or_default();
d.kind = Kind::Emboss { depth: depth.to_string(), deboss: *deboss };
let items = st.sketch(sketch.0).map(|ss| profile_indices(ss, profiles)).unwrap_or_default();
if let Some(inp) = d.inputs.first_mut() {
inp.items = items.into_iter().map(|index| Sel::Profile { sketch: *sketch, index }).collect();
inp.items = items.into_iter().map(|index| Sel::Profile { sketch: sketch.0, index }).collect();
}
if !targets.is_empty() {
d.extra.insert("targets".into(), json!(targets));
@@ -4870,7 +4874,12 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option<usize>) -> Option<Dia
}
FeatureKind::Rib { curves, thickness, depth, flip, web, .. } => {
let mut d = start(if *web { "solid.web" } else { "solid.rib" })?;
d.kind = Kind::Rib { web: *web, thickness: thickness.clone(), depth: depth.clone().unwrap_or_default(), flip: *flip };
d.kind = Kind::Rib {
web: *web,
thickness: thickness.to_string(),
depth: depth.as_deref().map(str::to_string).unwrap_or_default(),
flip: *flip,
};
if let Some(inp) = d.inputs.first_mut() {
inp.items = curves.iter().map(|id| Sel::SketchCurve { id: id.clone() }).collect();
}
@@ -4879,7 +4888,7 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option<usize>) -> Option<Dia
FeatureKind::ReplaceFace { faces, target, body } => {
let mut d = start("solid.replace_face")?;
if let Some(inp) = d.inputs.first_mut() {
inp.items = faces.iter().filter_map(|p| face_sel(s, *p)).collect();
inp.items = faces.iter().filter_map(|p| face_sel(s, p.0)).collect();
}
if let Some(inp) = d.inputs.get_mut(1) {
inp.items = plane_sel(s, target).into_iter().collect();
@@ -4892,13 +4901,20 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option<usize>) -> Option<Dia
FeatureKind::Remove { bodies } => {
let mut d = start("solid.remove")?;
if let Some(inp) = d.inputs.first_mut() {
inp.items = bodies.iter().map(|n| Sel::Body { name: n.clone() }).collect();
inp.items = bodies.iter().map(|n| Sel::Body { name: n.to_string() }).collect();
}
d
}
FeatureKind::Move { bodies, translate, rotate_axis, angle, copy } => {
let mut d = start("solid.move")?;
crate::dialogs_move::for_feature(&mut d, bodies, translate, *rotate_axis, angle.as_ref(), *copy);
crate::dialogs_move::for_feature(
&mut d,
&solidcraft_engine::doc::plain(bodies),
translate,
rotate_axis.map(Vec3::from),
angle.as_ref(),
*copy,
);
d
}
other => {
+12 -5
View File
@@ -288,8 +288,15 @@ pub fn drag(app: &SolveApp, d: &mut Dialog, g: Drag) {
/// The dialog for editing a move: Free Move with its translation and, about a world axis
/// through the origin, its angle (another axis is kept as it is).
pub fn for_feature(d: &mut Dialog, bodies: &[String], translate: &[String; 3], axis: Option<Vec3>, angle: Option<&String>, copy: bool) {
let [x, y, z] = translate.clone();
pub fn for_feature(
d: &mut Dialog,
bodies: &[String],
translate: &[solidcraft_engine::doc::Expr; 3],
axis: Option<Vec3>,
angle: Option<&solidcraft_engine::doc::Expr>,
copy: bool,
) {
let [x, y, z] = translate.clone().map(String::from);
let mut k = kind(FREE);
if let Kind::Move { x: kx, y: ky, z: kz, rx, ry, rz, .. } = &mut k {
(*kx, *ky, *kz) = (x, y, z);
@@ -297,9 +304,9 @@ pub fn for_feature(d: &mut Dialog, bodies: &[String], translate: &[String; 3], a
let dir = axis.unwrap_or(Vec3::Z);
let world = [Vec3::X, Vec3::Y, Vec3::Z].iter().position(|w| dir.cross(*w).len() < 1e-9 && dir.dot(*w) > 0.0);
match world {
Some(0) => *rx = a.clone(),
Some(1) => *ry = a.clone(),
Some(_) => *rz = a.clone(),
Some(0) => *rx = a.to_string(),
Some(1) => *ry = a.to_string(),
Some(_) => *rz = a.to_string(),
None => {
d.extra.insert("axis".into(), json!([dir.x, dir.y, dir.z]));
d.extra.insert("angle".into(), json!(a));
+24 -24
View File
@@ -410,8 +410,8 @@ pub fn commands(app: &SolveApp, k: &Pl, inputs: &[SelInput], extra: &Map<String,
Ok(vec![(cmd.to_string(), p)])
}
fn s(o: &Option<String>) -> String {
o.clone().unwrap_or_default()
fn s(o: &Option<solidcraft_engine::doc::Expr>) -> String {
o.as_deref().map(str::to_string).unwrap_or_default()
}
/// The dialog editing a plastic feature, filled from it.
@@ -434,7 +434,7 @@ pub fn for_feature(app: &SolveApp, kind: &FeatureKind) -> Option<(Kind, Vec<SelI
rib_offset,
body,
} => {
extra.insert("direction".into(), pt(*direction));
extra.insert("direction".into(), pt(direction.0));
if let Some(b) = body {
extra.insert("body".into(), json!(b));
}
@@ -452,63 +452,63 @@ pub fn for_feature(app: &SolveApp, kind: &FeatureKind) -> Option<(Kind, Vec<SelI
rib_offset: ro,
} = &mut b
{
*d = diameter.clone();
*h = height.clone();
*d = diameter.to_string();
*h = height.to_string();
*hd = s(hole_diameter);
*hp = s(hole_depth);
*dr = s(draft);
*f = s(fillet);
*r = ribs.clone().unwrap_or_else(|| "0".into());
*r = ribs.as_deref().map(str::to_string).unwrap_or_else(|| "0".into());
for (slot, v) in [(rt, rib_thickness), (rl, rib_length), (ro, rib_offset)] {
if let Some(v) = v {
*slot = v.clone();
*slot = v.to_string();
}
}
}
(b, "plastic.boss", *position)
(b, "plastic.boss", position.0)
}
FeatureKind::Lip { face, width, height, groove, gap, outside, body } => {
if let Some(b) = body {
extra.insert("body".into(), json!(b));
}
(Pl::Lip { groove: *groove, width: width.clone(), height: height.clone(), gap: s(gap), outside: *outside }, "plastic.lip", *face)
(Pl::Lip { groove: *groove, width: width.to_string(), height: height.to_string(), gap: s(gap), outside: *outside }, "plastic.lip", face.0)
}
FeatureKind::SnapFit { position, direction, hook, length, thickness, width, catch_depth, catch_length, body } => {
extra.insert("direction".into(), pt(*direction));
extra.insert("direction".into(), pt(direction.0));
if let Some(b) = body {
extra.insert("body".into(), json!(b));
}
let h = hook_dirs(*direction).iter().position(|(_, d)| d.dist(*hook) < 1e-6);
let h = hook_dirs(direction.0).iter().position(|(_, d)| d.dist(hook.0) < 1e-6);
if h.is_none() {
extra.insert("hook".into(), pt(*hook));
extra.insert("hook".into(), pt(hook.0));
}
let f = Pl::SnapFit {
hook: h.unwrap_or(0),
length: length.clone(),
thickness: thickness.clone(),
width: width.clone(),
catch_depth: catch_depth.clone(),
catch_length: catch_length.clone(),
length: length.to_string(),
thickness: thickness.to_string(),
width: width.to_string(),
catch_depth: catch_depth.to_string(),
catch_length: catch_length.to_string(),
};
(f, "plastic.snap_fit", *position)
(f, "plastic.snap_fit", position.0)
}
FeatureKind::Rest { position, direction, along, width, length, height, draft, thickness, body } => {
extra.insert("direction".into(), pt(*direction));
extra.insert("direction".into(), pt(direction.0));
if let Some(a) = along {
extra.insert("along".into(), pt(*a));
extra.insert("along".into(), pt(a.0));
}
if let Some(b) = body {
extra.insert("body".into(), json!(b));
}
let r = Pl::Rest {
round: length.is_none(),
width: width.clone(),
length: length.clone().unwrap_or_else(|| "15 mm".into()),
height: height.clone(),
width: width.to_string(),
length: length.as_deref().map(str::to_string).unwrap_or_else(|| "15 mm".into()),
height: height.to_string(),
draft: s(draft),
thickness: s(thickness),
};
(r, "plastic.rest", *position)
(r, "plastic.rest", position.0)
}
_ => return None,
};
+24 -18
View File
@@ -673,20 +673,20 @@ pub fn for_feature(app: &SolveApp, kind: &FeatureKind) -> Option<(Kind, Vec<SelI
let mut extra = Map::new();
let (k, items): (Sm, Vec<Sel>) = match kind {
FeatureKind::SheetBase { sketch, profiles, rule, flip } => {
let ps = st.sketch(*sketch).map(|ss| profile_indices(ss, profiles)).unwrap_or_default();
let ps = st.sketch(sketch.0).map(|ss| profile_indices(ss, profiles)).unwrap_or_default();
let mut f = new_flange(0);
if let Sm::Flange { flip: fl, rule: r, .. } = &mut f {
*fl = *flip;
*r = rule_index(rule);
}
(f, ps.into_iter().map(|index| Sel::Profile { sketch: *sketch, index }).collect())
(f, ps.into_iter().map(|index| Sel::Profile { sketch: sketch.0, index }).collect())
}
FeatureKind::SheetContour { curves, distance, rule, flip, reverse, .. } => {
let mut f = new_flange(2);
if let Sm::Flange { flip: fl, rule: r, distance: d, reverse: rv, .. } = &mut f {
*fl = *flip;
*r = rule_index(rule);
*d = distance.clone();
*d = distance.to_string();
*rv = *reverse;
}
(f, curves.iter().map(|id| Sel::SketchCurve { id: id.clone() }).collect())
@@ -699,17 +699,21 @@ pub fn for_feature(app: &SolveApp, kind: &FeatureKind) -> Option<(Kind, Vec<SelI
// edge's selection carries).
let (mut picked, mut own) = (Vec::new(), Vec::new());
for e in edges {
let Some(sel) = edge_sel(s, e.point) else { continue };
let Some(sel) = edge_sel(s, e.point.0) else { continue };
if let (Sel::Edge { point, .. }, true) = (&sel, e.angle.is_some() || e.height.is_some()) {
own.push((*point, e.angle.clone().unwrap_or_default(), e.height.clone().unwrap_or_default()));
own.push((
*point,
e.angle.as_deref().map(str::to_string).unwrap_or_default(),
e.height.as_deref().map(str::to_string).unwrap_or_default(),
));
}
picked.push(sel);
}
let f = Sm::Flange {
ty: 1,
height: height.clone(),
angle: angle.clone(),
radius: radius.clone().unwrap_or_default(),
height: height.to_string(),
angle: angle.to_string(),
radius: radius.as_deref().map(str::to_string).unwrap_or_default(),
position: POSITIONS.iter().position(|p| p == position).unwrap_or(0),
flip: *flip,
distance: "20 mm".into(),
@@ -720,7 +724,7 @@ pub fn for_feature(app: &SolveApp, kind: &FeatureKind) -> Option<(Kind, Vec<SelI
.as_deref()
.and_then(|name| RELIEF_SHAPES.iter().position(|s| s.eq_ignore_ascii_case(name.trim())))
.map_or(0, |i| i + 1),
relief_size: corner_relief.clone().unwrap_or_default(),
relief_size: corner_relief.as_deref().map(str::to_string).unwrap_or_default(),
};
(f, picked)
}
@@ -729,8 +733,8 @@ pub fn for_feature(app: &SolveApp, kind: &FeatureKind) -> Option<(Kind, Vec<SelI
extra.insert("body".into(), json!(b));
}
(
Sm::Hem { length: length.clone(), gap: gap.clone().unwrap_or_default(), flip: *flip },
edges.iter().filter_map(|p| edge_sel(s, *p)).collect(),
Sm::Hem { length: length.to_string(), gap: gap.as_deref().map(str::to_string).unwrap_or_default(), flip: *flip },
edges.iter().filter_map(|p| edge_sel(s, p.0)).collect(),
)
}
FeatureKind::SheetFold { curve, a, b, angle, radius, position, flip, fixed, body, .. } => {
@@ -738,8 +742,8 @@ pub fn for_feature(app: &SolveApp, kind: &FeatureKind) -> Option<(Kind, Vec<SelI
extra.insert("body".into(), json!(bd));
}
let k = Sm::Fold {
angle: angle.clone(),
radius: radius.clone().unwrap_or_default(),
angle: angle.to_string(),
radius: radius.as_deref().map(str::to_string).unwrap_or_default(),
position: FOLD_POSITIONS.iter().position(|x| x == position || (*x == "mould" && position == "mold")).unwrap_or(0),
flip: *flip,
};
@@ -752,22 +756,24 @@ pub fn for_feature(app: &SolveApp, kind: &FeatureKind) -> Option<(Kind, Vec<SelI
}
// A fold given by two points keeps them.
None => {
extra.insert("points".into(), json!([pt(*a), pt(*b)]));
extra.insert("points".into(), json!([pt(a.0), pt(b.0)]));
}
}
if let Some(i) = ins.get_mut(1) {
i.items = fixed.and_then(|f| face_sel(s, f)).into_iter().collect();
i.items = fixed.and_then(|f| face_sel(s, f.0)).into_iter().collect();
}
return Some((Kind::Sheet(k), ins, extra));
}
FeatureKind::SheetUnfold { body, refold } => (Sm::Unfold { refold: *refold }, body.iter().map(|n| Sel::Body { name: n.clone() }).collect()),
FeatureKind::SheetUnfold { body, refold } => {
(Sm::Unfold { refold: *refold }, body.iter().map(|n| Sel::Body { name: n.to_string() }).collect())
}
FeatureKind::SheetConvert { face, rule, bends, .. } => {
let (_, mut ins) = start(app, "sheet.convert")?;
if let Some(i) = ins.first_mut() {
i.items = face_sel(s, *face).into_iter().collect();
i.items = face_sel(s, face.0).into_iter().collect();
}
if let Some(i) = ins.get_mut(1) {
i.items = bends.iter().filter_map(|p| edge_sel(s, *p)).collect();
i.items = bends.iter().filter_map(|p| edge_sel(s, p.0)).collect();
}
return Some((Kind::Sheet(Sm::Convert { rule: rule_index(rule) }), ins, extra));
}
+38 -34
View File
@@ -575,7 +575,7 @@ pub fn commands(app: &SolveApp, k: &Surf, inputs: &[SelInput], extra: &Map<Strin
/// A tool as a selection again: a plane, else the face at the stored point.
fn tool_sel(app: &SolveApp, plane: &PlaneRef, tool: Option<&FaceAt>) -> Option<Sel> {
match tool {
Some(t) => crate::dialogs::face_sel(&app.session, t.point),
Some(t) => crate::dialogs::face_sel(&app.session, t.point.0),
None => crate::dialogs::plane_sel(&app.session, plane),
}
}
@@ -584,7 +584,7 @@ fn tool_sel(app: &SolveApp, plane: &PlaneRef, tool: Option<&FaceAt>) -> Option<S
/// `frame` maps the feature's component frame to the world, for an axis stored as a line.
pub fn for_feature(app: &SolveApp, kind: &FeatureKind, frame: &Mat) -> Option<(Kind, Vec<SelInput>, Map<String, Value>)> {
let s = &app.session;
let edge = |p: &Vec3| crate::dialogs::edge_sel(s, *p);
let edge = |p: &solidcraft_engine::doc::Position| crate::dialogs::edge_sel(s, p.0);
let (k, fill): (Surf, Vec<Vec<Sel>>) = match kind {
FeatureKind::SurfaceRevolve { sketch, profiles, curves, axis, angle, angle2, symmetric } => {
let (pick, stored) = match axis {
@@ -594,8 +594,8 @@ pub fn for_feature(app: &SolveApp, kind: &FeatureKind, frame: &Mat) -> Option<(K
AxisRef::Line { origin, dir } => (
None,
Some(json!({
"origin": pt(solidcraft_engine::doc::apply_point(frame, *origin)),
"dir": pt(solidcraft_engine::doc::apply_vector(frame, *dir))
"origin": pt(solidcraft_engine::doc::apply_point(frame, origin.0)),
"dir": pt(solidcraft_engine::doc::apply_vector(frame, dir.0))
})),
),
};
@@ -603,9 +603,9 @@ pub fn for_feature(app: &SolveApp, kind: &FeatureKind, frame: &Mat) -> Option<(K
if let Some(a2) = angle2 {
extra.insert("angle2".into(), json!(a2));
}
let k = Surf::Revolve { angle: angle.clone(), symmetric: *symmetric, axis: stored };
let k = Surf::Revolve { angle: angle.to_string(), symmetric: *symmetric, axis: stored };
let mut ins = inputs(&k);
for (inp, items) in ins.iter_mut().zip([sheet_sels(app, *sketch, profiles.as_ref(), curves), pick.into_iter().collect()]) {
for (inp, items) in ins.iter_mut().zip([sheet_sels(app, sketch.0, profiles.as_ref(), curves), pick.into_iter().collect()]) {
inp.items = items;
}
return Some((Kind::Surface(k), ins, extra));
@@ -615,23 +615,23 @@ pub fn for_feature(app: &SolveApp, kind: &FeatureKind, frame: &Mat) -> Option<(K
(
Surf::Sweep(opts),
vec![
sheet_sels(app, *sketch, profiles.as_ref(), curves),
sheet_sels(app, sketch.0, profiles.as_ref(), curves),
path.iter().map(|id| Sel::SketchCurve { id: id.clone() }).collect(),
direction.into_iter().collect(),
],
)
}
FeatureKind::SurfaceLoft { sections } => {
(Surf::Loft, vec![sections.iter().flat_map(|sec| sheet_sels(app, sec.sketch, sec.profiles.as_ref(), &sec.curves)).collect()])
(Surf::Loft, vec![sections.iter().flat_map(|sec| sheet_sels(app, sec.sketch.0, sec.profiles.as_ref(), &sec.curves)).collect()])
}
FeatureKind::SurfaceExtrude { sketch, profiles, curves, extent } => {
let sels = sheet_sels(app, *sketch, profiles.as_ref(), curves);
let sels = sheet_sels(app, sketch.0, profiles.as_ref(), curves);
let direction = match extent.direction {
Direction::Positive => 0,
Direction::Symmetric => 1,
Direction::Negative => 2,
};
let k = Surf::Extrude { distance: extent.distance.clone(), direction };
let k = Surf::Extrude { distance: extent.distance.to_string(), direction };
let mut ins = inputs(&k);
if let Some(input) = ins.first_mut() {
input.items = sels;
@@ -644,14 +644,14 @@ pub fn for_feature(app: &SolveApp, kind: &FeatureKind, frame: &Mat) -> Option<(K
let mut sels: Vec<Sel> = edges.iter().filter_map(edge).collect();
if let Some(sk) = sketch {
let st = s.world_state();
if let Some(ss) = st.sketch(*sk) {
sels.extend((0..ss.profiles.len()).map(|index| Sel::Profile { sketch: *sk, index }));
if let Some(ss) = st.sketch(sk.0) {
sels.extend((0..ss.profiles.len()).map(|index| Sel::Profile { sketch: sk.0, index }));
}
}
let index = PATCH_CONTINUITY.iter().position(|(_, w)| *w == continuity.all.as_str()).unwrap_or(0);
// What the dialog does not show is kept as it was: the crown, edges with a
// continuity of their own, and points that are not on a face any more.
let through: Vec<Sel> = points.iter().filter_map(|p| crate::dialogs::face_sel(s, *p)).collect();
let through: Vec<Sel> = points.iter().filter_map(|p| crate::dialogs::face_sel(s, p.0)).collect();
let mut extra = Map::new();
if *crown != 0.0 {
extra.insert("crown".into(), json!(crown));
@@ -660,7 +660,7 @@ pub fn for_feature(app: &SolveApp, kind: &FeatureKind, frame: &Mat) -> Option<(K
extra.insert("continuity".into(), serde_json::to_value(continuity).unwrap_or(Value::Null));
}
if through.len() != points.len() {
extra.insert("points".into(), json!(points.iter().map(|p| pt(*p)).collect::<Vec<_>>()));
extra.insert("points".into(), json!(points.iter().map(|p| pt(p.0)).collect::<Vec<_>>()));
}
let k = Surf::Patch { continuity: index };
let mut ins = inputs(&k);
@@ -669,48 +669,49 @@ pub fn for_feature(app: &SolveApp, kind: &FeatureKind, frame: &Mat) -> Option<(K
}
return Some((Kind::Surface(k), ins, extra));
}
FeatureKind::SurfaceOffset { faces, distance, chain, .. } => {
(Surf::Offset { distance: distance.clone(), chain: *chain }, vec![faces.iter().filter_map(|p| crate::dialogs::face_sel(s, *p)).collect()])
}
FeatureKind::SurfaceOffset { faces, distance, chain, .. } => (
Surf::Offset { distance: distance.to_string(), chain: *chain },
vec![faces.iter().filter_map(|p| crate::dialogs::face_sel(s, p.0)).collect()],
),
FeatureKind::SurfaceRuled { edges, distance, mode, angle, direction, flip, corners, face, .. } => {
let k = Surf::Ruled {
distance: distance.clone(),
distance: distance.to_string(),
mode: match mode {
RuledMode::Normal => 0,
RuledMode::Tangent => 1,
RuledMode::Angle => 2,
RuledMode::Direction => 3,
},
angle: angle.clone().unwrap_or_else(|| "5 deg".into()),
angle: angle.as_deref().map(str::to_string).unwrap_or_else(|| "5 deg".into()),
flip: *flip,
round: *corners == RuledCorners::Round,
};
let mut ins = inputs(&k);
let fill = [edges.iter().filter_map(edge).collect(), face.and_then(|p| crate::dialogs::face_sel(s, p)).into_iter().collect()];
let fill = [edges.iter().filter_map(edge).collect(), face.and_then(|p| crate::dialogs::face_sel(s, p.0)).into_iter().collect()];
for (inp, items) in ins.iter_mut().zip(fill) {
inp.items = items;
}
let mut extra = Map::new();
if let Some(d) = direction {
extra.insert("direction".into(), pt(*d));
extra.insert("direction".into(), pt(d.0));
}
return Some((Kind::Surface(k), ins, extra));
}
FeatureKind::Unstitch { body, faces } | FeatureKind::ReverseNormal { body, faces } => {
let sels: Vec<Sel> = if faces.is_empty() {
vec![Sel::Body { name: body.clone() }]
vec![Sel::Body { name: body.to_string() }]
} else {
faces.iter().filter_map(|p| crate::dialogs::face_sel(s, *p)).collect()
faces.iter().filter_map(|p| crate::dialogs::face_sel(s, p.0)).collect()
};
(if matches!(kind, FeatureKind::Unstitch { .. }) { Surf::Unstitch } else { Surf::ReverseNormal }, vec![sels])
}
FeatureKind::Stitch { bodies, tolerance } => (
Surf::Stitch { tolerance: if tolerance.trim() == "0" { String::new() } else { tolerance.clone() } },
vec![bodies.iter().map(|n| Sel::Body { name: n.clone() }).collect()],
Surf::Stitch { tolerance: if tolerance.trim() == "0" { String::new() } else { tolerance.to_string() } },
vec![bodies.iter().map(|n| Sel::Body { name: n.to_string() }).collect()],
),
FeatureKind::Thicken { bodies, thickness, symmetric, operation, .. } => (
Surf::Thicken { thickness: thickness.clone(), symmetric: *symmetric, operation: op_index(operation) },
vec![bodies.iter().map(|n| Sel::Body { name: n.clone() }).collect()],
Surf::Thicken { thickness: thickness.to_string(), symmetric: *symmetric, operation: op_index(operation) },
vec![bodies.iter().map(|n| Sel::Body { name: n.to_string() }).collect()],
),
FeatureKind::SurfaceExtend { edges, distance, mode, up_to, neighbours, .. } => {
// The dialog edits the edges and the distance; the rest is kept as it was.
@@ -726,14 +727,17 @@ pub fn for_feature(app: &SolveApp, kind: &FeatureKind, frame: &Mat) -> Option<(K
extra.insert("up_to".into(), json!("bounds"));
}
Some(solidcraft_engine::doc::ExtendTo::Face(t)) => {
extra.insert("up_to".into(), json!({"body": t.body, "point": pt(t.point)}));
extra.insert("up_to".into(), json!({"body": t.body, "point": pt(t.point.0)}));
}
Some(solidcraft_engine::doc::ExtendTo::Plane(PlaneRef::Origin { name } | PlaneRef::Construction { name })) => {
Some(solidcraft_engine::doc::ExtendTo::Plane(PlaneRef::Origin { name })) => {
extra.insert("up_to".into(), json!(name));
}
Some(solidcraft_engine::doc::ExtendTo::Plane(PlaneRef::Construction { name })) => {
extra.insert("up_to".into(), json!(name));
}
_ => {}
}
let k = Surf::Extend { distance: distance.clone() };
let k = Surf::Extend { distance: distance.to_string() };
let mut ins = inputs(&k);
if let Some(inp) = ins.first_mut() {
inp.items = edges.iter().filter_map(edge).collect();
@@ -743,9 +747,9 @@ pub fn for_feature(app: &SolveApp, kind: &FeatureKind, frame: &Mat) -> Option<(K
FeatureKind::SurfaceTrim { body, plane, tool, keep } => (
Surf::Trim,
vec![
vec![Sel::Body { name: body.clone() }],
vec![Sel::Body { name: body.to_string() }],
tool_sel(app, plane, tool.as_ref()).into_iter().collect(),
crate::dialogs::face_sel(s, *keep).into_iter().collect(),
crate::dialogs::face_sel(s, keep.0).into_iter().collect(),
],
),
FeatureKind::SurfaceUntrim { face, holes, outer, in_place } => {
@@ -756,12 +760,12 @@ pub fn for_feature(app: &SolveApp, kind: &FeatureKind, frame: &Mat) -> Option<(K
};
(
Surf::Untrim { holes: index, outer: *outer, in_place: *in_place },
vec![crate::dialogs::face_sel(s, face.point).into_iter().collect(), picked],
vec![crate::dialogs::face_sel(s, face.point.0).into_iter().collect(), picked],
)
}
FeatureKind::SplitFace { faces, plane, tool, .. } => (
Surf::SplitFace,
vec![faces.iter().filter_map(|p| crate::dialogs::face_sel(s, *p)).collect(), tool_sel(app, plane, tool.as_ref()).into_iter().collect()],
vec![faces.iter().filter_map(|p| crate::dialogs::face_sel(s, p.0)).collect(), tool_sel(app, plane, tool.as_ref()).into_iter().collect()],
),
_ => return None,
};
+5 -5
View File
@@ -3,7 +3,7 @@
use serde_json::{Value, json};
use solidcraft_engine::Sel;
use solidcraft_engine::doc::{Mat, SweepOrientation};
use solidcraft_engine::doc::{Expr, Mat, SweepOrientation};
use solidcraft_engine::geom::Vec3;
use crate::SolveApp;
@@ -40,10 +40,10 @@ impl SweepOpts {
/// A feature's stored options as dialog values; `frame` maps the feature's component frame to
/// the world (a stored direction is in the component's frame). A direction along an origin
/// axis comes back as a pick of that axis.
pub fn of(orientation: &SweepOrientation, twist: &Option<String>, scale: &Option<String>, frame: &Mat) -> (SweepOpts, Option<Sel>) {
pub fn of(orientation: &SweepOrientation, twist: &Option<Expr>, scale: &Option<Expr>, frame: &Mat) -> (SweepOpts, Option<Sel>) {
let mut o = SweepOpts {
twist: twist.clone().unwrap_or_else(|| NO_TWIST.into()),
scale: scale.clone().unwrap_or_else(|| NO_SCALE.into()),
twist: twist.as_ref().map_or_else(|| NO_TWIST.into(), |t| t.to_string()),
scale: scale.as_ref().map_or_else(|| NO_SCALE.into(), |k| k.to_string()),
..SweepOpts::default()
};
let mut pick = None;
@@ -52,7 +52,7 @@ impl SweepOpts {
SweepOrientation::Parallel => o.orientation = 1,
SweepOrientation::Direction(d) => {
o.orientation = DIRECTION;
let world = solidcraft_engine::doc::apply_vector(frame, *d);
let world = solidcraft_engine::doc::apply_vector(frame, d.0);
match world.normalized().and_then(world_axis) {
Some(name) => pick = Some(Sel::Axis { name: name.into() }),
None => o.direction = Some(world),
@@ -25,7 +25,7 @@ fn arrow(h: &Harness) -> (Vec3, Vec3) {
fn last_hole_direction(h: &Harness) -> Vec3 {
match &h.app.session.doc.features.last().unwrap().kind {
FeatureKind::Hole { direction, .. } => *direction,
FeatureKind::Hole { direction, .. } => direction.0,
other => panic!("the last feature is not a hole: {}", other.type_name()),
}
}
@@ -77,7 +77,7 @@ fn hovered_face_point(h: &mut Harness, at: [f64; 2]) -> Vec3 {
fn hole(h: &Harness) -> (u64, Vec3, bool) {
let f = h.app.session.doc.features.last().unwrap();
match &f.kind {
FeatureKind::Hole { position, points, .. } => (f.id, *position, points.is_some()),
FeatureKind::Hole { position, points, .. } => (f.id, position.0, points.is_some()),
other => panic!("the last feature is not a hole: {}", other.type_name()),
}
}
+4 -4
View File
@@ -156,10 +156,10 @@ fn a_centimetre_design_shows_centimetres() {
fn stored(h: &Harness, id: u64) -> Vec<String> {
use solidcraft_engine::doc::FeatureKind;
match h.app.session.doc.feature(id).map(|f| &f.kind) {
Some(FeatureKind::Box { length, width, height, .. }) => vec![length.clone(), width.clone(), height.clone()],
Some(FeatureKind::Cylinder { radius, height, .. }) => vec![radius.clone(), height.clone()],
Some(FeatureKind::Sphere { radius, .. }) => vec![radius.clone()],
Some(FeatureKind::Torus { major, minor, .. }) => vec![major.clone(), minor.clone()],
Some(FeatureKind::Box { length, width, height, .. }) => vec![length.to_string(), width.to_string(), height.to_string()],
Some(FeatureKind::Cylinder { radius, height, .. }) => vec![radius.to_string(), height.to_string()],
Some(FeatureKind::Sphere { radius, .. }) => vec![radius.to_string()],
Some(FeatureKind::Torus { major, minor, .. }) => vec![major.to_string(), minor.to_string()],
other => panic!("not a primitive: {other:?}"),
}
}
@@ -33,7 +33,7 @@ fn volume(h: &Harness) -> f64 {
}
fn angle(h: &Harness) -> String {
let FeatureKind::Revolve { angle, .. } = &h.app.session.doc.find_feature("Turn").unwrap().kind else { panic!("revolve") };
angle.clone()
angle.to_string()
}
fn expected_volume(degrees: f64) -> f64 {
0.5 * (225.0 - 100.0) * 5.0 * degrees.to_radians()
+3 -3
View File
@@ -39,9 +39,9 @@ fn a_new_sweep_takes_its_options_from_the_dialog() {
assert!(h.app.dialog.is_none(), "{:?}", h.app.session.log);
let f = h.app.session.doc.features.last().unwrap();
let FeatureKind::Sweep { orientation, twist, scale, .. } = &f.kind else { panic!("{:?}", f.kind) };
assert_eq!(*orientation, SweepOrientation::Direction(Vec3::X));
assert_eq!(twist.as_deref(), Some("90 deg"));
assert_eq!(scale.as_deref(), Some("0.5"));
assert_eq!(*orientation, SweepOrientation::Direction(solidcraft_engine::doc::Dir(Vec3::X)));
assert_eq!(twist.as_ref().map(|t| t.as_str()), Some("90 deg"));
assert_eq!(scale.as_ref().map(|k| k.as_str()), Some("0.5"));
}
#[test]
+1 -1
View File
@@ -333,7 +333,7 @@ pub fn timeline(app: &mut SolveApp, ui: &mut egui::Ui) {
if let Some(sk) = sketch_of
&& ui.button(crate::i18n::tr_label("Edit Profile Sketch")).clicked()
{
app.edit_sketch(sk);
app.edit_sketch(sk.0);
ui.close();
}
if ui.button(crate::i18n::tr_label("Rename")).clicked() {
+1 -1
View File
@@ -774,7 +774,7 @@ fn vertical_features(app: &mut SolveApp, ui: &mut egui::Ui) {
if let Some(sketch) = sketch
&& ui.button("Edit Profile Sketch").clicked()
{
app.edit_sketch(sketch);
app.edit_sketch(sketch.0);
ui.close();
}
if ui.button("Rename Feature").clicked() {