diff --git a/crates/derive/src/lib.rs b/crates/derive/src/lib.rs index 04a12dae..74d4ff4e 100644 --- a/crates/derive/src/lib.rs +++ b/crates/derive/src/lib.rs @@ -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, /// The kind (`Length`, `Angle`, `Unitless`) and the label. expr: Option<(&'static str, Expr)>, - /// `Sketch`, `Feature` or `Body`. - reference: Option<&'static str>, after: Option, /// 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 { 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::()?), @@ -93,9 +89,6 @@ fn roles(field: &syn::Field) -> syn::Result { "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 = None; - #w::Each::::each(&*#sketch, &mut |s| #id = Some(*s)); + #w::Each::::each(&*#sketch, &mut |s| #id = Some(s.0)); } } _ => quote!(), diff --git a/crates/doc/src/assembly.rs b/crates/doc/src/assembly.rs index 20172b34..66870784 100644 --- a/crates/doc/src/assembly.rs +++ b/crates/doc/src/assembly.rs @@ -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::().ok().filter(|v| v.is_finite()); + let num = |e: &crate::Expr| e.trim().parse::().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(); } } diff --git a/crates/doc/src/body_transfer.rs b/crates/doc/src/body_transfer.rs index d29c2a34..1eca9bbe 100644 --- a/crates/doc/src/body_transfer.rs +++ b/crates/doc/src/body_transfer.rs @@ -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); } } diff --git a/crates/doc/src/clipboard.rs b/crates/doc/src/clipboard.rs index 13fa9829..b8679b44 100644 --- a/crates/doc/src/clipboard.rs +++ b/crates/doc/src/clipboard.rs @@ -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 { diff --git a/crates/doc/src/config_shell_tests.rs b/crates/doc/src/config_shell_tests.rs index 3aa5dce1..d94a72ef 100644 --- a/crates/doc/src/config_shell_tests.rs +++ b/crates/doc/src/config_shell_tests.rs @@ -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, diff --git a/crates/doc/src/construct.rs b/crates/doc/src/construct.rs index 21b5a3c4..da42a77e 100644 --- a/crates/doc/src/construct.rs +++ b/crates/doc/src/construct.rs @@ -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. diff --git a/crates/doc/src/document.rs b/crates/doc/src/document.rs index 3aeb554c..688a0a57 100644 --- a/crates/doc/src/document.rs +++ b/crates/doc/src/document.rs @@ -4,7 +4,9 @@ use solidcraft_kernel::Continuity; use solidcraft_sketch::Sketch; use crate::construct::{AxisDef, GeoRef, PointDef}; -use crate::expr::{self, Kind}; +use crate::expr::{self, Expr, Kind}; +use crate::names::{BodyName, FeatureName, SketchId}; +use crate::points::{Dir, FacePick, Position}; use crate::{DocError, Result}; pub const MAX_FEATURES: usize = 10_000; @@ -51,11 +53,11 @@ pub enum PlaneRef { y_dir: Vec3, }, /// Another plane offset along its normal by an expression. - Offset { base: Box, distance: String }, + Offset { base: Box, distance: Expr }, /// Another plane rotated about a line (in world coordinates) by an angle expression. - AtAngle { base: Box, axis_origin: Vec3, axis_dir: Vec3, angle: String }, + AtAngle { base: Box, axis_origin: Vec3, axis_dir: Vec3, angle: Expr }, /// A construction plane feature (by name). - Construction { name: String }, + Construction { name: FeatureName }, /// Halfway between two planes (or planar faces); bisecting them when they meet at an angle. Midplane { a: Box, b: Box }, /// Through two straight edges (lines) that lie in one plane. @@ -65,7 +67,7 @@ pub enum PlaneRef { /// Tangent to a face (cylinder, cone, sphere, torus) where the point `at` lies over it. Tangent { face: Box, at: Vec3 }, /// Square to an edge or sketch curve, a fraction `t` (expression, 0…1) of the way along it. - AlongPath { path: Box, t: String }, + AlongPath { path: Box, t: Expr }, /// Through a straight edge (or line, axis) and square to a plane (or planar face). Perpendicular { plane: Box, line: Box }, /// A planar body face. Certified attachments resolve only their exact source and report @@ -218,7 +220,7 @@ impl ExtrudeConvention { #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, solidcraft_derive::Walk)] pub struct Extent { #[walk(length = "Distance")] - pub distance: String, + pub distance: Expr, #[serde(default)] pub convention: ExtrudeConvention, #[serde(default)] @@ -226,32 +228,30 @@ pub struct Extent { /// Optional second side distance (two-sided extrude), along the negative normal. #[serde(default, skip_serializing_if = "Option::is_none")] #[walk(length = "Distance 2")] - pub distance2: Option, + pub distance2: Option, /// Start offset from the sketch plane (expression), along the sketch normal whatever the /// direction. #[serde(default, skip_serializing_if = "Option::is_none")] #[walk(length = "Start offset")] - pub start_offset: Option, + pub start_offset: Option, /// Through all bodies in the direction(s) instead of `distance`. #[serde(default, skip_serializing_if = "std::ops::Not::not")] pub through_all: bool, /// Taper angle (expression; positive grows the profile). #[serde(default, skip_serializing_if = "Option::is_none")] #[walk(angle = "Taper angle")] - pub taper: Option, + pub taper: Option, /// To Object: end at the face (or vertex/point) at this point, instead of `distance`. #[serde(default, skip_serializing_if = "Option::is_none")] - #[walk(pick)] - pub to: Option, + pub to: Option, /// Offset past the To Object face (expression; negative stops short). #[serde(default, skip_serializing_if = "Option::is_none")] #[walk(length = "To offset")] - pub to_offset: Option, + pub to_offset: Option, /// From Object: start at the face (or point) here instead of the sketch plane (plus /// `start_offset`). #[serde(default, skip_serializing_if = "Option::is_none")] - #[walk(pick)] - pub from: Option, + pub from: Option, } /// Thin Extrude: a wall of constant thickness along sketch curves instead of the profiles. @@ -262,13 +262,13 @@ pub struct ThinWall { pub curves: Vec, /// The wall thickness (expression); with `side: two_sides`, the thickness inside. #[walk(length = "Thickness")] - pub thickness: String, + pub thickness: Expr, #[serde(default, skip_serializing_if = "ThinSide::is_center")] pub side: ThinSide, /// With `side: two_sides`, the thickness outside (expression). #[serde(default, skip_serializing_if = "Option::is_none")] #[walk(length = "Thickness 2")] - pub thickness2: Option, + pub thickness2: Option, } /// How a swept profile turns as it follows its path. @@ -282,7 +282,7 @@ pub enum SweepOrientation { Parallel, /// Square to the path, its turn about the path fixed by this direction (in the feature's /// frame): the direction's projection onto the profile stays where it was at the start. - Direction(#[walk(dir)] Vec3), + Direction(Dir), } impl SweepOrientation { @@ -332,21 +332,15 @@ pub enum AxisRef { /// World X / Y / Z axis through the origin. World { axis: String }, /// Any line in the sketch plane, in world coordinates. - Line { - #[walk(pos)] - origin: Vec3, - #[walk(dir)] - dir: Vec3, - }, + Line { origin: Position, dir: Dir }, } /// Persistent cylindrical Emboss selection, authored in the feature component frame. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, solidcraft_derive::Walk)] pub struct EmbossFace { - #[walk(body)] - pub body: String, + pub body: BodyName, #[walk(pinned)] - pub at: Vec3, + pub at: FacePick, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub path: Vec, #[serde(default, skip_serializing_if = "Vec::is_empty")] @@ -427,18 +421,17 @@ impl EmbossFace { /// values is `{"point": [x, y, z], "angle": "…", "height": "…"}`. #[derive(Clone, Debug, Default, PartialEq, solidcraft_derive::Walk)] pub struct FlangeEdge { - #[walk(pos)] - pub point: Vec3, + pub point: Position, #[walk(angle = "Edge angle")] - pub angle: Option, + pub angle: Option, #[walk(length = "Edge height")] - pub height: Option, + pub height: Option, } impl FlangeEdge { /// An edge that takes the feature's angle and height. pub fn at(point: Vec3) -> Self { - Self { point, angle: None, height: None } + Self { point: Position(point), angle: None, height: None } } } @@ -469,9 +462,9 @@ impl<'de> Deserialize<'de> for FlangeEdge { struct Own { point: Vec3, #[serde(default)] - angle: Option, + angle: Option, #[serde(default)] - height: Option, + height: Option, } struct Edge; impl<'de> serde::de::Visitor<'de> for Edge { @@ -484,7 +477,7 @@ impl<'de> Deserialize<'de> for FlangeEdge { } fn visit_map>(self, map: A) -> std::result::Result { let o = Own::deserialize(MapAccessDeserializer::new(map))?; - Ok(FlangeEdge { point: o.point, angle: o.angle, height: o.height }) + Ok(FlangeEdge { point: Position(o.point), angle: o.angle, height: o.height }) } } d.deserialize_any(Edge) @@ -498,8 +491,7 @@ pub enum FeatureKind { /// A recorded timeline ownership boundary. Geometry is converted from its /// current local frame into the destination component frame exactly once. BodyTransfer { - #[walk(body)] - bodies: Vec, + bodies: Vec, from: u64, to: u64, transform: crate::Mat, @@ -510,8 +502,7 @@ pub enum FeatureKind { }, Extrude { /// Feature id of the sketch. - #[walk(sketch)] - sketch: u64, + sketch: SketchId, // A thin extrude follows curves; its profiles are not used. #[walk(profiles_of = sketch, when = thin.is_none())] profiles: ProfileSel, @@ -520,29 +511,26 @@ pub enum FeatureKind { operation: Operation, /// Target body names for join/cut/intersect (empty = all bodies). #[serde(default, skip_serializing_if = "Vec::is_empty")] - #[walk(body)] - targets: Vec, + targets: Vec, /// Thin: a wall along sketch curves instead of the profiles (which are then unused). #[serde(default, skip_serializing_if = "Option::is_none")] thin: Option, }, Revolve { - #[walk(sketch)] - sketch: u64, + sketch: SketchId, #[walk(profiles_of = sketch)] profiles: ProfileSel, axis: AxisRef, #[walk(angle = "Angle")] - angle: String, + angle: Expr, #[serde(default)] operation: Operation, #[serde(default, skip_serializing_if = "Vec::is_empty")] - #[walk(body)] - targets: Vec, + targets: Vec, /// Two Sides: the angle on the other side. #[serde(default, skip_serializing_if = "Option::is_none")] #[walk(angle = "Angle 2")] - angle2: Option, + angle2: Option, /// Symmetric: `angle` split evenly either side of the profile. #[serde(default, skip_serializing_if = "std::ops::Not::not")] symmetric: bool, @@ -551,18 +539,15 @@ pub enum FeatureKind { convention: RevolveConvention, /// To Object: turn until the face, vertex or point here. #[serde(default, skip_serializing_if = "Option::is_none")] - #[walk(pick)] - to: Option, + to: Option, }, Fillet { /// Reference points on the edges to round (each re-finds the nearest edge). - #[walk(pos)] - edges: Vec, + edges: Vec, #[walk(length = style.radius_label())] - radius: String, + radius: Expr, #[serde(default, skip_serializing_if = "Option::is_none")] - #[walk(body)] - body: Option, + body: Option, /// Constant radius (the default), chord length (`radius` is the width across), or a /// radius varying along each edge. #[serde(default, skip_serializing_if = "FilletStyle::is_constant")] @@ -572,35 +557,33 @@ pub enum FeatureKind { fit: FilletFit, }, Chamfer { - #[walk(pos)] - edges: Vec, + edges: Vec, #[walk(length = "Distance")] - distance: String, + distance: Expr, /// Unequal chamfers: the distance along the second face… #[serde(default, skip_serializing_if = "Option::is_none")] #[walk(length = "Distance 2")] - distance2: Option, + distance2: Option, /// …or the angle the chamfer makes with the first face. #[serde(default, skip_serializing_if = "Option::is_none")] #[walk(angle = "Angle")] - angle: Option, + angle: Option, /// Swap which face is the first (by default the one facing up most). #[serde(default, skip_serializing_if = "std::ops::Not::not")] flip: bool, #[serde(default, skip_serializing_if = "Option::is_none")] - #[walk(body)] - body: Option, + body: Option, }, Box { #[serde(with = "point_expr")] #[walk(placement)] corner: PointExpr, #[walk(length = "Length")] - length: String, + length: Expr, #[walk(length = "Width")] - width: String, + width: Expr, #[walk(length = "Height")] - height: String, + height: Expr, #[serde(default)] operation: Operation, }, @@ -609,12 +592,11 @@ pub enum FeatureKind { #[walk(placement)] base: PointExpr, #[serde(default = "z_axis")] - #[walk(dir)] - axis: Vec3, + axis: Dir, #[walk(length = "Radius")] - radius: String, + radius: Expr, #[walk(length = "Height")] - height: String, + height: Expr, #[serde(default)] operation: Operation, }, @@ -623,7 +605,7 @@ pub enum FeatureKind { #[walk(placement)] center: PointExpr, #[walk(length = "Radius")] - radius: String, + radius: Expr, #[serde(default)] operation: Operation, }, @@ -632,40 +614,34 @@ pub enum FeatureKind { #[walk(placement)] center: PointExpr, #[walk(length = "Major radius")] - major: String, + major: Expr, #[walk(length = "Minor radius")] - minor: String, + minor: Expr, #[serde(default)] operation: Operation, }, Combine { - #[walk(body)] - target: String, - #[walk(body)] - tools: Vec, + target: BodyName, + tools: Vec, operation: Operation, #[serde(default)] keep_tools: bool, }, /// Copies of other features' results, or of bodies (rectangular, circular or along a path). Pattern { - #[walk(feature)] - features: Vec, + features: Vec, pattern: PatternKind, /// Copy these bodies (as new bodies) instead of replaying features. #[serde(default, skip_serializing_if = "Vec::is_empty")] - #[walk(body)] - bodies: Vec, + bodies: Vec, }, /// Mirror copies of other features' results in a plane. Mirror { - #[walk(feature)] - features: Vec, + features: Vec, plane: PlaneRef, /// Mirror these bodies instead of replaying features. #[serde(default, skip_serializing_if = "Vec::is_empty")] - #[walk(body)] - bodies: Vec, + bodies: Vec, /// Join each mirrored body with its original. #[serde(default, skip_serializing_if = "std::ops::Not::not")] combine: bool, @@ -673,16 +649,15 @@ pub enum FeatureKind { /// A drilled hole (simple, counterbore or countersink) at a point, into the material. Hole { #[walk(pick_if = points.is_none())] - position: Vec3, + position: FacePick, /// Drilling direction (into the material). - #[walk(dir)] - direction: Vec3, + direction: Dir, #[walk(length = "Diameter")] - diameter: String, + diameter: Expr, /// Depth to the shoulder; `None` = through all. #[serde(default, skip_serializing_if = "Option::is_none")] #[walk(length = "Depth")] - depth: Option, + depth: Option, #[serde(default)] hole: HoleKind, /// Drill at these sketch points instead (one hole each, perpendicular to the sketch, into the material: against the sketch normal, or along it when only that side has material); @@ -694,21 +669,19 @@ pub enum FeatureKind { thread: Option, /// To Object: drill down to the face (or point) here instead of `depth`. #[serde(default, skip_serializing_if = "Option::is_none")] - #[walk(pick)] - to: Option, + to: Option, }, /// A thread on a cylindrical face. Cosmetic by default (no geometry change; drawn and /// exported as an annotation); `modeled` cuts the real helical ISO profile into the face. /// `designation` is an ISO metric size such as "M8" or "M8x1". Thread { - #[walk(pick)] - face: Vec3, + face: FacePick, #[serde(default, skip_serializing_if = "Option::is_none")] designation: Option, /// Thread length from the face's end nearest `face`; `None` = full length. #[serde(default, skip_serializing_if = "Option::is_none")] #[walk(length = "Length")] - length: Option, + length: Option, #[serde(default, skip_serializing_if = "std::ops::Not::not")] modeled: bool, }, @@ -718,17 +691,14 @@ pub enum FeatureKind { #[serde(default)] operation: Operation, #[serde(default, skip_serializing_if = "Vec::is_empty")] - #[walk(body)] - targets: Vec, + targets: Vec, }, /// Sweep profiles along a chain of sketch curves. Sweep { - #[walk(sketch)] - sketch: u64, + sketch: SketchId, #[walk(profiles_of = sketch)] profiles: ProfileSel, - #[walk(sketch)] - path_sketch: u64, + path_sketch: SketchId, path: Vec, /// How the profile turns along the path (default: perpendicular to it). #[serde(default, skip_serializing_if = "SweepOrientation::is_perpendicular")] @@ -736,26 +706,23 @@ pub enum FeatureKind { /// Angle the profile turns about the path over its length. #[serde(default, skip_serializing_if = "Option::is_none")] #[walk(angle = "Twist")] - twist: Option, + twist: Option, /// The profile's scale at the end of the path (linear along its length). #[serde(default, skip_serializing_if = "Option::is_none")] #[walk(unitless = "Scale")] - scale: Option, + scale: Option, #[serde(default)] operation: Operation, #[serde(default, skip_serializing_if = "Vec::is_empty")] - #[walk(body)] - targets: Vec, + targets: Vec, }, /// Hollow a body leaving walls of `thickness`, removing the faces at the given points. Shell { - #[walk(pick)] - faces: Vec, + faces: Vec, #[walk(length = "Thickness")] - thickness: String, + thickness: Expr, #[serde(default, skip_serializing_if = "Option::is_none")] - #[walk(body)] - body: Option, + body: Option, /// inside (default) | outside | both (the wall straddles the faces). #[serde(default, skip_serializing_if = "String::is_empty")] direction: String, @@ -765,16 +732,13 @@ pub enum FeatureKind { }, /// Tilt faces (at the given points) about a neutral plane, leaning toward `pull`. Draft { - #[walk(pick)] - faces: Vec, + faces: Vec, #[walk(angle = "Angle")] - angle: String, + angle: Expr, neutral: PlaneRef, - #[walk(dir)] - pull: Vec3, + pull: Dir, #[serde(default, skip_serializing_if = "Option::is_none")] - #[walk(body)] - body: Option, + body: Option, }, /// A construction plane (sketch placement, split tool). ConstructionPlane { @@ -783,8 +747,7 @@ pub enum FeatureKind { /// Split a body into the parts on either side of a plane, or of a face of a body (the face /// through `point`, its surface extended) when `tool` is set. Split { - #[walk(body)] - body: String, + body: BodyName, plane: PlaneRef, #[serde(default, skip_serializing_if = "Option::is_none")] tool: Option, @@ -801,51 +764,42 @@ pub enum FeatureKind { /// a face of a body (`tool`, its surface extended) or a sketch's curves (`sketch`, swept /// along its normal). The pieces are named after the face (`#k`). SplitFace { - #[walk(pick)] - faces: Vec, + faces: Vec, #[serde(default, skip_serializing_if = "Option::is_none")] - #[walk(body)] - body: Option, + body: Option, plane: PlaneRef, #[serde(default, skip_serializing_if = "Option::is_none")] tool: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - #[walk(sketch)] - sketch: Option, + sketch: Option, }, /// Scale bodies about a point (uniform, or per axis). Scale { - #[walk(body)] - bodies: Vec, + bodies: Vec, #[serde(default)] - #[walk(pos)] - origin: Vec3, + origin: Position, #[walk(unitless = "Scale factor")] - factor: String, + factor: Expr, /// Per-axis factors instead of `factor`. #[serde(default, skip_serializing_if = "Option::is_none")] #[walk(unitless = ["X scale", "Y scale", "Z scale"])] - factors: Option<[String; 3]>, + factors: Option<[Expr; 3]>, }, /// Move planar faces (at the given points) along their normals; the body keeps its shape. OffsetFace { - #[walk(pick)] - faces: Vec, + faces: Vec, #[walk(length = "Distance")] - distance: String, + distance: Expr, #[serde(default, skip_serializing_if = "Option::is_none")] - #[walk(body)] - body: Option, + body: Option, }, /// Remove faces (at the given points) and heal the gap from the neighbouring planes, as when /// a fillet, chamfer, hole or boss is taken off. A picked face brings the rest of its surface /// (both halves of a hole's wall). DeleteFace { - #[walk(pick)] - faces: Vec, + faces: Vec, #[serde(default, skip_serializing_if = "Option::is_none")] - #[walk(body)] - body: Option, + body: Option, /// Close the gap by extending the neighbouring faces (the default); `false` leaves it /// open, the faces left a surface body. #[serde(default = "yes", skip_serializing_if = "is_yes")] @@ -853,56 +807,50 @@ pub enum FeatureKind { }, /// A box around bodies, grown by a margin. BoundingSolid { - #[walk(body)] - bodies: Vec, + bodies: Vec, #[serde(default = "zero_expr")] #[walk(length = "Margin")] - margin: String, + margin: Expr, }, /// A tube along a chain of sketch curves (solid, or hollow with a wall thickness). Pipe { - #[walk(sketch)] - path_sketch: u64, + path_sketch: SketchId, path: Vec, #[walk(length = "Diameter")] - diameter: String, + diameter: Expr, #[serde(default, skip_serializing_if = "Option::is_none")] #[walk(length = "Wall")] - wall: Option, + wall: Option, #[serde(default)] operation: Operation, #[serde(default, skip_serializing_if = "Vec::is_empty")] - #[walk(body)] - targets: Vec, + targets: Vec, }, /// Raise or sink profiles on a planar face, or wrap them on a selected cylindrical wall. Emboss { - #[walk(sketch)] - sketch: u64, + sketch: SketchId, #[walk(profiles_of = sketch)] profiles: ProfileSel, #[walk(length = "Depth")] - depth: String, + depth: Expr, #[serde(default, skip_serializing_if = "std::ops::Not::not")] deboss: bool, #[serde(default, skip_serializing_if = "Option::is_none")] face: Option, #[serde(default, skip_serializing_if = "Vec::is_empty")] - #[walk(body)] - targets: Vec, + targets: Vec, }, /// A thin wall from open sketch curves: `thickness` across the sketch plane, filling from /// the curves toward the body (to the next face, or `depth`). A web makes one per curve. Rib { - #[walk(sketch)] - sketch: u64, + sketch: SketchId, curves: Vec, #[walk(length = "Thickness")] - thickness: String, + thickness: Expr, /// Depth from the curves; `None` = to the next face of the body. #[serde(default, skip_serializing_if = "Option::is_none")] #[walk(length = "Depth")] - depth: Option, + depth: Option, /// Fill toward the other side of the curves. #[serde(default, skip_serializing_if = "std::ops::Not::not")] flip: bool, @@ -912,27 +860,20 @@ pub enum FeatureKind { }, /// Move planar faces (at the given points) onto a target plane parallel to them. ReplaceFace { - #[walk(pick)] - faces: Vec, + faces: Vec, target: PlaneRef, #[serde(default, skip_serializing_if = "Option::is_none")] - #[walk(body)] - body: Option, + body: Option, }, /// Move bodies so that `from` lands on `to` (and, with normals, the faces meet face to face). Align { - #[walk(body)] - bodies: Vec, - #[walk(pos)] - from: Vec3, - #[walk(pos)] - to: Vec3, + bodies: Vec, + from: Position, + to: Position, #[serde(default, skip_serializing_if = "Option::is_none")] - #[walk(dir)] - from_normal: Option, + from_normal: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - #[walk(dir)] - to_normal: Option, + to_normal: Option, /// Normals the same way instead of facing each other. #[serde(default, skip_serializing_if = "std::ops::Not::not")] flip: bool, @@ -944,16 +885,15 @@ pub enum FeatureKind { #[walk(placement)] base: PointExpr, #[serde(default = "z_axis")] - #[walk(dir)] - axis: Vec3, + axis: Dir, #[walk(length = "Diameter")] - diameter: String, + diameter: Expr, #[walk(length = "Pitch")] - pitch: String, + pitch: Expr, #[walk(unitless = "Revolutions")] - turns: String, + turns: Expr, #[walk(length = "Section size")] - section_size: String, + section_size: Expr, #[serde(default)] section: CoilSection, /// -1 inside, 0 on the centre, 1 outside of the diameter. @@ -963,20 +903,18 @@ pub enum FeatureKind { /// perpendicular direction when the axis is x). #[serde(default, skip_serializing_if = "Option::is_none")] #[walk(angle = "Start angle")] - start_angle: Option, + start_angle: Option, /// Turn the other way (left-handed). #[serde(default, skip_serializing_if = "std::ops::Not::not")] clockwise: bool, #[serde(default)] operation: Operation, #[serde(default, skip_serializing_if = "Vec::is_empty")] - #[walk(body)] - targets: Vec, + targets: Vec, }, /// Sheet metal base flange: a sheet from a closed profile (thickness from the rule). SheetBase { - #[walk(sketch)] - sketch: u64, + sketch: SketchId, #[walk(profiles_of = sketch)] profiles: ProfileSel, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -987,11 +925,10 @@ pub enum FeatureKind { }, /// Sheet metal contour flange: a sheet bent along an open chain of sketch lines. SheetContour { - #[walk(sketch)] - sketch: u64, + sketch: SketchId, curves: Vec, #[walk(length = "Distance")] - distance: String, + distance: Expr, #[serde(default, skip_serializing_if = "Option::is_none")] rule: Option, /// Material on the other side of the chain. @@ -1007,20 +944,19 @@ pub enum FeatureKind { #[walk(after = radius)] edges: Vec, #[walk(length = "Height")] - height: String, + height: Expr, #[walk(angle = "Angle")] - angle: String, + angle: Expr, #[serde(default, skip_serializing_if = "Option::is_none")] #[walk(length = "Bend radius")] - radius: Option, + radius: Option, /// inside | outside | middle. #[serde(default = "inside_str")] position: String, #[serde(default, skip_serializing_if = "std::ops::Not::not")] flip: bool, #[serde(default, skip_serializing_if = "Option::is_none")] - #[walk(body)] - body: Option, + body: Option, /// The relief cut where two of this feature's bends meet at a corner of the base, in place /// of the rule's: `corner_relief_shape` (notch | round | rectangular | tear) and /// `corner_relief`, the size across. Either one left out follows the rule. @@ -1029,22 +965,20 @@ pub enum FeatureKind { /// Walked last, after the edges' values, so the inputs before it keep their names. #[serde(default, skip_serializing_if = "Option::is_none")] #[walk(length = "Corner relief")] - corner_relief: Option, + corner_relief: Option, }, /// Flat hems on sheet edges. SheetHem { - #[walk(pos)] - edges: Vec, + edges: Vec, #[walk(length = "Length")] - length: String, + length: Expr, #[serde(default, skip_serializing_if = "Option::is_none")] #[walk(length = "Gap")] - gap: Option, + gap: Option, #[serde(default, skip_serializing_if = "std::ops::Not::not")] flip: bool, #[serde(default, skip_serializing_if = "Option::is_none")] - #[walk(body)] - body: Option, + body: Option, }, /// A perforation field (#732): round holes of `diameter` on a staggered (60°) or square grid of `pitch` over the /// whole flat sheet, kept `margin` (default: the diameter) clear of its edges, cut-outs and bend zones. Stored like @@ -1052,52 +986,45 @@ pub enum FeatureKind { /// panel can carry tens of thousands of them. SheetPerforate { #[walk(length = "Diameter")] - diameter: String, + diameter: Expr, #[walk(length = "Pitch")] - pitch: String, + pitch: Expr, #[serde(default, skip_serializing_if = "Option::is_none")] #[walk(length = "Margin")] - margin: Option, + margin: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pattern: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - #[walk(body)] - body: Option, + body: Option, }, /// Fold a sheet along a line on its base face: a sketch line (followed when the sketch /// changes), else the points `a`, `b`. SheetFold { #[serde(default, skip_serializing_if = "Option::is_none")] - #[walk(sketch)] - sketch: Option, + sketch: Option, #[serde(default, skip_serializing_if = "Option::is_none")] curve: Option, - #[walk(pos)] - a: Vec3, - #[walk(pos)] - b: Vec3, + a: Position, + b: Position, #[walk(angle = "Angle")] - angle: String, + angle: Expr, #[serde(default, skip_serializing_if = "Option::is_none")] #[walk(length = "Bend radius")] - radius: Option, + radius: Option, /// centerline | start | end | mould position: String, #[serde(default, skip_serializing_if = "std::ops::Not::not")] flip: bool, /// A point on the side that stays put (default: the larger side). #[serde(default, skip_serializing_if = "Option::is_none")] - #[walk(pos)] - fixed: Option, + fixed: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - #[walk(body)] - body: Option, + body: Option, }, /// Unfold a sheet body flat (all bends), or refold it. SheetUnfold { #[serde(default, skip_serializing_if = "Option::is_none")] - #[walk(body)] - body: Option, + body: Option, #[serde(default, skip_serializing_if = "std::ops::Not::not")] refold: bool, }, @@ -1105,147 +1032,130 @@ pub enum FeatureKind { /// thickness or, with `bends`, a solid or shelled body whose faces reached from the fixed /// face across those edges become the sheet. SheetConvert { - #[walk(body)] - body: String, + body: BodyName, #[walk(pick_if = !bends.is_empty())] - face: Vec3, + face: FacePick, #[serde(default, skip_serializing_if = "Option::is_none")] rule: Option, /// Points on the sharp edges that become bends. #[serde(default, skip_serializing_if = "Vec::is_empty")] - #[walk(pos)] - bends: Vec, + bends: Vec, }, /// Plastic boss: a round post on a face (draft, root fillet, hole, ribs). Boss { - #[walk(pick)] - position: Vec3, + position: FacePick, /// Out of the face (the boss grows along it). #[serde(default = "z_axis")] - #[walk(dir)] - direction: Vec3, + direction: Dir, #[walk(length = "Diameter")] - diameter: String, + diameter: Expr, #[walk(length = "Height")] - height: String, + height: Expr, #[serde(default, skip_serializing_if = "Option::is_none")] #[walk(length = "Hole diameter")] - hole_diameter: Option, + hole_diameter: Option, /// Hole depth from the top (default: down to the face). #[serde(default, skip_serializing_if = "Option::is_none")] #[walk(length = "Hole depth")] - hole_depth: Option, + hole_depth: Option, #[serde(default, skip_serializing_if = "Option::is_none")] #[walk(angle = "Draft angle")] - draft: Option, + draft: Option, #[serde(default, skip_serializing_if = "Option::is_none")] #[walk(length = "Root fillet")] - fillet: Option, + fillet: Option, #[serde(default, skip_serializing_if = "Option::is_none")] #[walk(unitless = "Ribs")] - ribs: Option, + ribs: Option, #[serde(default, skip_serializing_if = "Option::is_none")] #[walk(length = "Rib thickness")] - rib_thickness: Option, + rib_thickness: Option, /// Rib reach out from the boss wall. #[serde(default, skip_serializing_if = "Option::is_none")] #[walk(length = "Rib length")] - rib_length: Option, + rib_length: Option, /// Rib top below the boss top. #[serde(default, skip_serializing_if = "Option::is_none")] #[walk(length = "Rib offset")] - rib_offset: Option, + rib_offset: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - #[walk(body)] - body: Option, + body: Option, }, /// Plastic lip (raised band) or groove (recess) along the rim face picked at `face`. Lip { - #[walk(pos)] - face: Vec3, + face: Position, #[walk(length = "Width")] - width: String, + width: Expr, #[walk(length = "Height")] - height: String, + height: Expr, #[serde(default, skip_serializing_if = "std::ops::Not::not")] groove: bool, /// Clearance added to a groove's width (and depth). #[serde(default, skip_serializing_if = "Option::is_none")] #[walk(length = "Gap")] - gap: Option, + gap: Option, /// Along the rim's outer edge instead of its inner one. #[serde(default, skip_serializing_if = "std::ops::Not::not")] outside: bool, #[serde(default, skip_serializing_if = "Option::is_none")] - #[walk(body)] - body: Option, + body: Option, }, /// Plastic snap fit: a cantilever arm on a face with a hook (catch) at its tip. SnapFit { - #[walk(pick)] - position: Vec3, + position: FacePick, /// The arm grows along this (out of the face). #[serde(default = "z_axis")] - #[walk(dir)] - direction: Vec3, + direction: Dir, /// The catch sticks out this way. - #[walk(dir)] - hook: Vec3, + hook: Dir, #[walk(length = "Length")] - length: String, + length: Expr, #[walk(length = "Thickness")] - thickness: String, + thickness: Expr, #[walk(length = "Width")] - width: String, + width: Expr, #[walk(length = "Catch depth")] - catch_depth: String, + catch_depth: Expr, /// The catch's height along the arm. #[walk(length = "Catch length")] - catch_length: String, + catch_length: Expr, #[serde(default, skip_serializing_if = "Option::is_none")] - #[walk(body)] - body: Option, + body: Option, }, /// Plastic rest: a raised pad on a face (round, or a rectangle), solid or a hollow wall, /// with draft. Rest { - #[walk(pick)] - position: Vec3, + position: FacePick, /// Out of the face (the pad grows along it). #[serde(default = "z_axis")] - #[walk(dir)] - direction: Vec3, + direction: Dir, /// The rectangle's length runs along this (default: any direction in the face). #[serde(default, skip_serializing_if = "Option::is_none")] - #[walk(dir)] - along: Option, + along: Option, /// Diameter of a round rest, or the rectangle's width. #[walk(length = "Width")] - width: String, + width: Expr, /// The rectangle's length (none: round). #[serde(default, skip_serializing_if = "Option::is_none")] #[walk(length = "Length", after = height)] - length: Option, + length: Option, #[walk(length = "Height")] - height: String, + height: Expr, #[serde(default, skip_serializing_if = "Option::is_none")] #[walk(angle = "Draft angle")] - draft: Option, + draft: Option, /// Wall thickness of a hollow rest (none: solid). #[serde(default, skip_serializing_if = "Option::is_none")] #[walk(length = "Wall thickness")] - thickness: Option, + thickness: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - #[walk(body)] - body: Option, + body: Option, }, /// Boundary Fill: solids from the cells the tool bodies enclose (a cell: inside some /// tools and outside the rest), each picked by a point inside it. BoundaryFill { - #[walk(body)] - tools: Vec, - #[walk(pos)] - cells: Vec, + tools: Vec, + cells: Vec, #[serde(default)] operation: Operation, /// Remove the tool bodies afterwards. @@ -1254,24 +1164,20 @@ pub enum FeatureKind { }, /// Remove bodies from the model (from here on in the timeline). Remove { - #[walk(body)] - bodies: Vec, + bodies: Vec, }, /// Surface bodies: planar patches filling sketch regions (`profiles` of `sketch`), or a /// closed loop of a body's edges (`edges`: points on them, on `body`). Patch { #[serde(default, skip_serializing_if = "Option::is_none")] - #[walk(sketch)] - sketch: Option, + sketch: Option, #[serde(default = "all_profiles")] #[walk(profiles_of = sketch)] profiles: ProfileSel, #[serde(default, skip_serializing_if = "Option::is_none")] - #[walk(body)] - body: Option, + body: Option, #[serde(default, skip_serializing_if = "Vec::is_empty")] - #[walk(pos)] - edges: Vec, + edges: Vec, /// How full a fitted patch's middle is (`crown`, -0.9…3, default 0; a planar loop is an /// exact plane whatever this says). #[serde(default, skip_serializing_if = "is_zero")] @@ -1282,36 +1188,31 @@ pub enum FeatureKind { continuity: PatchContinuity, /// Points a patch on edges passes through. #[serde(default, skip_serializing_if = "Vec::is_empty")] - #[walk(pos)] - points: Vec, + points: Vec, }, /// Surface bodies sewn along shared edges into the first one (a solid when they close). Stitch { - #[walk(body)] - bodies: Vec, + bodies: Vec, #[serde(default = "zero_expr")] #[walk(length = "Tolerance")] - tolerance: String, + tolerance: Expr, }, /// Surface bodies thickened into solids along their normals (by half each way when /// `symmetric`). Thicken { - #[walk(body)] - bodies: Vec, + bodies: Vec, #[walk(length = "Thickness")] - thickness: String, + thickness: Expr, #[serde(default)] symmetric: bool, operation: Operation, #[serde(default, skip_serializing_if = "Vec::is_empty")] - #[walk(body)] - targets: Vec, + targets: Vec, }, /// A surface body swept straight along the sketch normal from open curve chains and the /// boundaries of profiles: walls only, no caps. SurfaceExtrude { - #[walk(sketch)] - sketch: u64, + sketch: SketchId, /// Profiles whose boundary loops are swept (none: only `curves`). #[serde(default, skip_serializing_if = "Option::is_none")] #[walk(profiles_of = sketch)] @@ -1324,19 +1225,15 @@ pub enum FeatureKind { /// A body taken apart: each face at `faces` (every face when empty) its own surface body, /// the faces left one surface body under the body's name. A solid comes apart into surfaces. Unstitch { - #[walk(body)] - body: String, + body: BodyName, #[serde(default, skip_serializing_if = "Vec::is_empty")] - #[walk(pos)] - faces: Vec, + faces: Vec, }, /// A surface body with the faces at `faces` (every face when empty) turned over. ReverseNormal { - #[walk(body)] - body: String, + body: BodyName, #[serde(default, skip_serializing_if = "Vec::is_empty")] - #[walk(pos)] - faces: Vec, + faces: Vec, }, /// The face at `face` restored towards its surface: the inner loops at the `holes` points /// taken away (every one when `None`; none when empty), and with `outer` its outer boundary @@ -1347,8 +1244,7 @@ pub enum FeatureKind { #[walk(picks_faces)] face: FaceAt, #[serde(default, skip_serializing_if = "Option::is_none")] - #[walk(pos)] - holes: Option>, + holes: Option>, #[serde(default, skip_serializing_if = "std::ops::Not::not")] outer: bool, #[serde(default, skip_serializing_if = "std::ops::Not::not")] @@ -1357,8 +1253,7 @@ pub enum FeatureKind { /// A surface body turned about an axis from open curve chains and the boundaries of /// profiles: walls only, no caps (a solid where a full turn closes them). SurfaceRevolve { - #[walk(sketch)] - sketch: u64, + sketch: SketchId, #[serde(default, skip_serializing_if = "Option::is_none")] #[walk(profiles_of = sketch)] profiles: Option, @@ -1366,11 +1261,11 @@ pub enum FeatureKind { curves: Vec, axis: AxisRef, #[walk(angle = "Angle")] - angle: String, + angle: Expr, /// Two Sides: the angle on the other side. #[serde(default, skip_serializing_if = "Option::is_none")] #[walk(angle = "Angle 2")] - angle2: Option, + angle2: Option, /// Symmetric: `angle` split evenly either side of the sketch. #[serde(default, skip_serializing_if = "std::ops::Not::not")] symmetric: bool, @@ -1378,15 +1273,13 @@ pub enum FeatureKind { /// A surface body swept along a chain of sketch curves from open curve chains and the /// boundaries of profiles: walls only, no caps. SurfaceSweep { - #[walk(sketch)] - sketch: u64, + sketch: SketchId, #[serde(default, skip_serializing_if = "Option::is_none")] #[walk(profiles_of = sketch)] profiles: Option, #[serde(default, skip_serializing_if = "Vec::is_empty")] curves: Vec, - #[walk(sketch)] - path_sketch: u64, + path_sketch: SketchId, path: Vec, /// How the profile turns along the path (default: perpendicular to it). #[serde(default, skip_serializing_if = "SweepOrientation::is_perpendicular")] @@ -1394,11 +1287,11 @@ pub enum FeatureKind { /// Angle the profile turns about the path over its length. #[serde(default, skip_serializing_if = "Option::is_none")] #[walk(angle = "Twist")] - twist: Option, + twist: Option, /// The profile's scale at the end of the path (linear along its length). #[serde(default, skip_serializing_if = "Option::is_none")] #[walk(unitless = "Scale")] - scale: Option, + scale: Option, }, /// A surface body lofted through sections in order, each one open or closed chain of /// sketch curves: walls only, no caps. @@ -1409,57 +1302,49 @@ pub enum FeatureKind { /// surface body (0 copies them); `chain` takes each pick's whole smooth chain of faces; /// fitted faces within `tolerance`. SurfaceOffset { - #[walk(pick)] - faces: Vec, + faces: Vec, #[serde(default, skip_serializing_if = "Option::is_none")] - #[walk(body)] - body: Option, + body: Option, #[walk(length = "Distance")] - distance: String, + distance: Expr, #[serde(default, skip_serializing_if = "std::ops::Not::not")] chain: bool, #[serde(default, skip_serializing_if = "Option::is_none")] #[walk(length = "Tolerance")] - tolerance: Option, + tolerance: Option, }, /// A strip of ruled surface `distance` wide hanging off the chain of `body`'s edges through /// `edges`, as a new surface body: along `mode` (`angle` from `direction`, or from the /// face's normal; along `direction`), from the face at `face` where it borders the chain; /// `flip` puts it on the other side; sharp corners as `corners` says. SurfaceRuled { - #[walk(body)] - body: String, - #[walk(pos)] - edges: Vec, + body: BodyName, + edges: Vec, #[walk(length = "Distance")] - distance: String, + distance: Expr, #[serde(default, skip_serializing_if = "RuledMode::is_normal")] mode: RuledMode, #[serde(default, skip_serializing_if = "Option::is_none")] #[walk(angle = "Angle")] - angle: Option, + angle: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - #[walk(dir)] - direction: Option, + direction: Option, #[serde(default, skip_serializing_if = "std::ops::Not::not")] flip: bool, #[serde(default, skip_serializing_if = "RuledCorners::is_mitre")] corners: RuledCorners, #[serde(default, skip_serializing_if = "Option::is_none")] - #[walk(pick)] - face: Option, + face: Option, }, /// A surface body's boundary edges through `edges` extended outward: by `distance`, or up /// to `up_to`; the surface continued (`mode` natural) or a run-off face along each edge /// (tangent, curvature). `neighbours`: the edges next to an extended edge are lengthened to /// meet it (else the extension's sides run square to the edge). SurfaceExtend { - #[walk(body)] - body: String, - #[walk(pos)] - edges: Vec, + body: BodyName, + edges: Vec, #[walk(length = "Distance")] - distance: String, + distance: Expr, #[serde(default, skip_serializing_if = "ExtendMode::is_natural")] mode: ExtendMode, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -1480,8 +1365,7 @@ pub enum FeatureKind { bulge: [f64; 2], /// Points the blend leaves towards, when the faces at the picks do not say: [from, to]. #[serde(default, skip_serializing_if = "Option::is_none")] - #[walk(pos)] - align: Option<[Vec3; 2]>, + align: Option<[Position; 2]>, /// Sew the blend onto its two bodies where the boundaries meet. #[serde(default, skip_serializing_if = "std::ops::Not::not")] stitch: bool, @@ -1489,25 +1373,21 @@ pub enum FeatureKind { /// A surface body trimmed by a plane, or by a face of a body (`tool`), keeping the side of /// `keep`. SurfaceTrim { - #[walk(body)] - body: String, + body: BodyName, plane: PlaneRef, #[serde(default, skip_serializing_if = "Option::is_none")] tool: Option, - #[walk(pos)] - keep: Vec3, + keep: Position, }, Move { - #[walk(body)] - bodies: Vec, + bodies: Vec, #[walk(length = ["X distance", "Y distance", "Z distance"])] - translate: [String; 3], + translate: [Expr; 3], #[serde(default, skip_serializing_if = "Option::is_none")] - #[walk(dir)] - rotate_axis: Option, + rotate_axis: Option, #[serde(default, skip_serializing_if = "Option::is_none")] #[walk(angle = "Angle")] - angle: Option, + angle: Option, /// When set, the original bodies are kept and the transform is applied to a new copy /// of each instead of moving them in place. #[serde(default, skip_serializing_if = "std::ops::Not::not")] @@ -1532,10 +1412,9 @@ pub enum FeatureKind { }, /// Rebuild an upstream constant-section mesh with analytic planar/cylindrical faces. MeshConvert { - #[walk(body)] - body: String, + body: BodyName, #[walk(length = "Tolerance")] - tolerance: String, + tolerance: Expr, }, /// Mesh bodies imported from a 3MF or STL file, embedded in the design. MeshImport { @@ -1591,12 +1470,11 @@ pub enum FilletStyle { /// positions between (0 to 1 from the start end); straight between them, or `smooth`. Variable { #[walk(length = "End radius")] - radius2: String, - #[walk(pos)] - start: Vec3, + radius2: Expr, + start: Position, #[serde(default, skip_serializing_if = "Vec::is_empty")] #[walk(length = "Mid radius")] - mid: Vec<(f64, String)>, + mid: Vec<(f64, Expr)>, #[serde(default, skip_serializing_if = "std::ops::Not::not")] smooth: bool, }, @@ -1604,11 +1482,10 @@ pub enum FilletStyle { /// `start` (0) to the other (1): `radius` is its first control radius and `radii` the rest, /// its degree what `knots` leave (knots = control radii + degree + 1). Spline { - #[walk(pos)] - start: Vec3, + start: Position, knots: Vec, #[walk(length = |i: usize, n: usize| if i + 1 == n { "End radius" } else { "Control radius" })] - radii: Vec, + radii: Vec, }, } @@ -1657,10 +1534,8 @@ impl FilletStyle { /// A face picked by a point on it, on a named body. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, solidcraft_derive::Walk)] pub struct FaceAt { - #[walk(body)] - pub body: String, - #[walk(pos)] - pub point: Vec3, + pub body: BodyName, + pub point: Position, } /// One end of a blend: a body and the points that pick the edges of its boundary chain. With two @@ -1668,10 +1543,8 @@ pub struct FaceAt { /// pick (the order of the picks does not matter). #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, solidcraft_derive::Walk)] pub struct BlendSide { - #[walk(body)] - pub body: String, - #[walk(pos)] - pub edges: Vec, + pub body: BodyName, + pub edges: Vec, } /// How a patch on a loop of edges meets the faces beside the loop: every edge with `all`, but the @@ -1687,8 +1560,7 @@ pub struct PatchContinuity { /// One edge of a patch's loop (a point on it) and how the patch meets the face beside it. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, solidcraft_derive::Walk)] pub struct EdgeContinuity { - #[walk(pos)] - pub edge: Vec3, + pub edge: Position, pub g: Continuity, } @@ -1733,8 +1605,7 @@ pub struct MeshData { /// Points of a sketch, by id. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, solidcraft_derive::Walk)] pub struct SketchPoints { - #[walk(sketch)] - pub sketch: u64, + pub sketch: SketchId, pub ids: Vec, /// Drill the other way: away from the side the holes would go into by themselves. #[serde(default, skip_serializing_if = "std::ops::Not::not")] @@ -1755,8 +1626,7 @@ pub struct Component { /// One loft section: profiles of a sketch. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, solidcraft_derive::Walk)] pub struct LoftSection { - #[walk(sketch)] - pub sketch: u64, + pub sketch: SketchId, #[walk(profiles_of = sketch, when = point.is_none())] pub profiles: ProfileSel, /// A sketch point instead of a profile (the loft closes to it, as a pyramid's apex). @@ -1768,8 +1638,7 @@ pub struct LoftSection { /// meeting end to end). #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, solidcraft_derive::Walk)] pub struct SheetSection { - #[walk(sketch)] - pub sketch: u64, + pub sketch: SketchId, #[serde(default, skip_serializing_if = "Option::is_none")] #[walk(profiles_of = sketch)] pub profiles: Option, @@ -1804,19 +1673,19 @@ pub enum HoleKind { Simple, Drilled { #[walk(angle = "Tip angle")] - tip_angle: String, + tip_angle: Expr, }, Counterbore { #[walk(length = "Counterbore diameter")] - cb_diameter: String, + cb_diameter: Expr, #[walk(length = "Counterbore depth")] - cb_depth: String, + cb_depth: Expr, }, Countersink { #[walk(length = "Countersink diameter")] - cs_diameter: String, + cs_diameter: Expr, #[walk(angle = "Countersink angle")] - cs_angle: String, + cs_angle: Expr, }, } @@ -1825,44 +1694,39 @@ pub enum HoleKind { #[serde(tag = "type", rename_all = "snake_case")] pub enum PatternKind { Rectangular { - #[walk(dir)] - dir1: Vec3, + dir1: Dir, #[walk(unitless = "Quantity")] - count1: String, + count1: Expr, #[walk(length = "Spacing")] - spacing1: String, + spacing1: Expr, #[serde(default, skip_serializing_if = "Option::is_none")] - #[walk(dir)] - dir2: Option, + dir2: Option, #[serde(default, skip_serializing_if = "Option::is_none")] #[walk(unitless = "Quantity 2")] - count2: Option, + count2: Option, #[serde(default, skip_serializing_if = "Option::is_none")] #[walk(length = "Spacing 2")] - spacing2: Option, + spacing2: Option, }, Circular { - #[walk(pos)] - origin: Vec3, - #[walk(dir)] - axis: Vec3, + origin: Position, + axis: Dir, #[walk(unitless = "Quantity")] - count: String, + count: Expr, /// Total angle (360 deg = evenly around). #[walk(angle = "Total angle")] - angle: String, + angle: Expr, }, /// Copies along a chain of sketch curves: instance k moves by path(k·spacing) − path(0), /// measured along the path from its start. Path { - #[walk(sketch)] - path_sketch: u64, + path_sketch: SketchId, path: Vec, #[walk(unitless = "Quantity")] - count: String, + count: Expr, /// Distance along the path between instances, or the whole extent when `extent`. #[walk(length = "Distance")] - spacing: String, + spacing: Expr, /// `spacing` is the distance from the first to the last instance. #[serde(default, skip_serializing_if = "std::ops::Not::not")] extent: bool, @@ -1875,17 +1739,17 @@ pub enum PatternKind { }, } -fn z_axis() -> Vec3 { - Vec3::Z +fn z_axis() -> Dir { + Dir(Vec3::Z) } /// A point whose coordinates are length expressions (`["basin_width / 2", "10 mm", "0"]`), so /// it follows parameter changes. Primitives are placed by one. -pub type PointExpr = [String; 3]; +pub type PointExpr = [Expr; 3]; /// A point as expressions (plain numbers, mm). pub fn point_expr(p: Vec3) -> PointExpr { - [format!("{}", p.x), format!("{}", p.y), format!("{}", p.z)] + [Expr::new(format!("{}", p.x)), Expr::new(format!("{}", p.y)), Expr::new(format!("{}", p.z))] } /// Stored as `[x, y, z]`: plain numbers as numbers (as points were before they took @@ -1901,17 +1765,17 @@ mod point_expr { } pub fn serialize(p: &super::PointExpr, s: S) -> Result { - let c = |e: &String| match e.parse::() { - Ok(v) if v.is_finite() && format!("{v}") == *e => Coord::Num(v), - _ => Coord::Expr(e.clone()), + let c = |e: &crate::Expr| match e.parse::() { + Ok(v) if v.is_finite() && format!("{v}") == e.as_str() => Coord::Num(v), + _ => Coord::Expr(e.to_string()), }; [c(&p[0]), c(&p[1]), c(&p[2])].serialize(s) } pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result { let c = |x: Coord| match x { - Coord::Num(v) => format!("{v}"), - Coord::Expr(e) => e, + Coord::Num(v) => crate::Expr::new(format!("{v}")), + Coord::Expr(e) => crate::Expr::new(e), }; let [x, y, z] = <[Coord; 3]>::deserialize(d)?; Ok([c(x), c(y), c(z)]) @@ -2151,7 +2015,7 @@ fn is_yes(b: &bool) -> bool { *b } -fn zero_expr() -> String { +fn zero_expr() -> Expr { "0".into() } @@ -2671,7 +2535,7 @@ impl Document { return Err(DocError::Invalid("too many features".into())); } let component = if let FeatureKind::BodyTransfer { bodies, from, to, transform } = &kind { - crate::validate_body_transfer(self, bodies, *from, *to, transform)?; + crate::validate_body_transfer(self, &crate::plain(bodies), *from, *to, transform)?; *to } else { 0 @@ -2727,7 +2591,7 @@ impl Document { return Err(DocError::Invalid(format!("feature `{}` belongs to an unknown component", f.name))); } if let FeatureKind::BodyTransfer { bodies, from, to, transform } = &f.kind { - crate::validate_body_transfer(self, bodies, *from, *to, transform)?; + crate::validate_body_transfer(self, &crate::plain(bodies), *from, *to, transform)?; if f.component != *to { return Err(DocError::Invalid("a body transfer belongs to its destination component".into())); } diff --git a/crates/doc/src/eval.rs b/crates/doc/src/eval.rs index e468e913..8f54b1fa 100644 --- a/crates/doc/src/eval.rs +++ b/crates/doc/src/eval.rs @@ -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, points: &[Vec3]) -> Result { +fn body_at(st: &ModelState, body: &Option, points: &[Vec3]) -> Result { 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, pts: &[Vec3]) -> Vec { +pub fn edge_refs(st: &ModelState, body: &Option, pts: &[Vec3]) -> Vec { 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, pts: &[Vec3]) -> Vec, pts: &[Vec3]) -> Vec { +pub fn edge_names_for(st: &ModelState, body: &Option, pts: &[Vec3]) -> Vec { 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, edges: &[Vec3]) -> Result { +fn blend_target(state: &ModelState, body: &Option, edges: &[Vec3]) -> Result { 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)> { 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)> { } 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)> { /// 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, depth: &Option) -> Result> { +fn hole_depth_to(st: &ModelState, at: &Vec3, dir: Vec3, to: &Option, depth: &Option) -> Result> { 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, 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)> { 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 = 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)> { } 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, 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, + depth: &Option, hole: &HoleKind, ) -> Result> { 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, + depth: &Option, hole: &HoleKind, ) -> Result)>> { 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 = 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 = 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::>(); 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::>>()?; @@ -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::>>()?; // 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] = §ions[..] 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] = ®ions[..] 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] = ®ions[..] 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, orientation: &crate::SweepOrientation, - twist: &Option, - scale: &Option, + twist: &Option, + scale: &Option, ) -> Result { 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, r: f64, wall: &Option) -> Result { +fn pipe_region(vals: &BTreeMap, r: f64, wall: &Option) -> Result { let holes = match wall { Some(w) => { let wt = val(vals, w, Kind::Length)?; @@ -3485,15 +3487,15 @@ pub(crate) fn extent_sides(vals: &BTreeMap, extent: &crate::Exten /// Operation and targets of a feature that makes tools. fn feature_op(f: &Feature) -> (Operation, Vec) { 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, 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, 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 = 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, - angle: &Option, + distance2: &Option, + angle: &Option, flip: bool, ) -> Result { let side = match (distance2, angle) { @@ -4102,7 +4104,9 @@ fn chamfer_with( fn eval_feature(doc: &Document, vals: &BTreeMap, f: &Feature, st: &mut ModelState, warning: &mut Option) -> 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, 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 = 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, 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, 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, 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, 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 = tb.iter().map(|b| b.mesh().contains(*p)).collect(); + let inside: Vec = tb.iter().map(|b| b.mesh().contains(p.0)).collect(); let mut acc: Option = 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, 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 = 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, 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> = (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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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 = 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, 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, 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, diff --git a/crates/doc/src/eval_more.rs b/crates/doc/src/eval_more.rs index e2407d87..2592cf2a 100644 --- a/crates/doc/src/eval_more.rs +++ b/crates/doc/src/eval_more.rs @@ -10,7 +10,7 @@ use solidcraft_geom::{Loop2, Seg2}; pub(super) fn eval(doc: &Document, vals: &BTreeMap, 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, 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, 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, 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, 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, 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, pick: Option) -> Result { +fn sheet_for(st: &ModelState, body: &Option, pick: Option) -> Result { 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 = edges.iter().map(|e| e.point).collect(); + let points: Vec = edges.iter().map(|e| e.point.0).collect(); let each: Vec = 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 = - 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 = 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, p: Vec3) -> Result { +fn body_under(st: &ModelState, body: &Option, p: Vec3) -> Result { 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, 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, 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, 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, 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, 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, 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, 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::>() } 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, 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, 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, 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, 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, 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, ®ion, h + delta, 1.0, -dr)?] } else { diff --git a/crates/doc/src/eval_surface.rs b/crates/doc/src/eval_surface.rs index 09969eef..19a1d35f 100644 --- a/crates/doc/src/eval_surface.rs +++ b/crates/doc/src/eval_surface.rs @@ -11,22 +11,22 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap, 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 ®ions { 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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 = Vec::new(); for p in &picks { let fi = @@ -288,10 +300,10 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap, 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, 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, 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(); diff --git a/crates/doc/src/expr.rs b/crates/doc/src/expr.rs index 5e86b2bb..cb4b4d56 100644 --- a/crates/doc/src/expr.rs +++ b/crates/doc/src/expr.rs @@ -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) -> 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 for Expr { + fn as_ref(&self) -> &str { + &self.0 + } +} + +impl From 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 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 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 for String { + fn eq(&self, other: &Expr) -> bool { + *self == other.0 + } +} + +impl PartialEq 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")] diff --git a/crates/doc/src/face_anchor.rs b/crates/doc/src/face_anchor.rs index 8609ae36..a6e1df44 100644 --- a/crates/doc/src/face_anchor.rs +++ b/crates/doc/src/face_anchor.rs @@ -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, diff --git a/crates/doc/src/feature_refs.rs b/crates/doc/src/feature_refs.rs index 0c1b4a59..9c3045f7 100644 --- a/crates/doc/src/feature_refs.rs +++ b/crates/doc/src/feature_refs.rs @@ -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 { +/// A reference a feature holds: `S` is `&SketchId` or `&mut SketchId`, and so on. +pub(crate) enum Ref { 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, names: &BTreeMap) { 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) { 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()); } }); } diff --git a/crates/doc/src/feature_walk_tests.rs b/crates/doc/src/feature_walk_tests.rs index 818bc3b6..2db17da0 100644 --- a/crates/doc/src/feature_walk_tests.rs +++ b/crates/doc/src/feature_walk_tests.rs @@ -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::>().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()); } diff --git a/crates/doc/src/format.rs b/crates/doc/src/format.rs index 7f667d6e..75920042 100644 --- a/crates/doc/src/format.rs +++ b/crates/doc/src/format.rs @@ -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); diff --git a/crates/doc/src/format_offsets.rs b/crates/doc/src/format_offsets.rs index aa3f52f4..073a1edd 100644 --- a/crates/doc/src/format_offsets.rs +++ b/crates/doc/src/format_offsets.rs @@ -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, diff --git a/crates/doc/src/lib.rs b/crates/doc/src/lib.rs index 4d8b705b..29f1d93a 100644 --- a/crates/doc/src/lib.rs +++ b/crates/doc/src/lib.rs @@ -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; diff --git a/crates/doc/src/names.rs b/crates/doc/src/names.rs new file mode 100644 index 00000000..e84b17a8 --- /dev/null +++ b/crates/doc/src/names.rs @@ -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 for $name { + fn as_ref(&self) -> &str { + &self.0 + } + } + + impl std::borrow::Borrow for $name { + fn borrow(&self) -> &str { + &self.0 + } + } + + impl From 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 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 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 for SketchId { + fn from(id: u64) -> Self { + SketchId(id) + } +} + +impl From for u64 { + fn from(id: SketchId) -> u64 { + id.0 + } +} + +impl PartialEq for SketchId { + fn eq(&self, other: &u64) -> bool { + self.0 == *other + } +} + +impl PartialEq 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) + } +} diff --git a/crates/doc/src/naming.rs b/crates/doc/src/naming.rs index 72965136..6b1f302e 100644 --- a/crates/doc/src/naming.rs +++ b/crates/doc/src/naming.rs @@ -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" diff --git a/crates/doc/src/params.rs b/crates/doc/src/params.rs index f956bcf1..76ab0746 100644 --- a/crates/doc/src/params.rs +++ b/crates/doc/src/params.rs @@ -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; } } diff --git a/crates/doc/src/points.rs b/crates/doc/src/points.rs new file mode 100644 index 00000000..f8c785f9 --- /dev/null +++ b/crates/doc/src/points.rs @@ -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 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>(ps: &[P]) -> Vec { + ps.iter().map(|p| p.clone().into()).collect() +} diff --git a/crates/doc/src/profile_refs.rs b/crates/doc/src/profile_refs.rs index dca0678a..c1881d95 100644 --- a/crates/doc/src/profile_refs.rs +++ b/crates/doc/src/profile_refs.rs @@ -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, diff --git a/crates/doc/src/project.rs b/crates/doc/src/project.rs index b2aed274..696d3e7d 100644 --- a/crates/doc/src/project.rs +++ b/crates/doc/src/project.rs @@ -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() } diff --git a/crates/doc/src/tests.rs b/crates/doc/src/tests.rs index a4f5aaa9..c3dab4b2 100644 --- a/crates/doc/src/tests.rs +++ b/crates/doc/src/tests.rs @@ -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, diff --git a/crates/doc/src/walk.rs b/crates/doc/src/walk.rs index 534af73c..d7c54c8f 100644 --- a/crates/doc/src/walk.rs +++ b/crates/doc/src/walk.rs @@ -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 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 Each for Option> { /// 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 ExprField for Option { - 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 ExprField for Option { 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)); diff --git a/crates/engine/src/cmd/browser.rs b/crates/engine/src/cmd/browser.rs index 83b3a50a..607cd25d 100644 --- a/crates/engine/src/cmd/browser.rs +++ b/crates/engine/src/cmd/browser.rs @@ -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())); } diff --git a/crates/engine/src/cmd/component_clone.rs b/crates/engine/src/cmd/component_clone.rs index bf03c14d..368d324d 100644 --- a/crates/engine/src/cmd/component_clone.rs +++ b/crates/engine/src/cmd/component_clone.rs @@ -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)); } } diff --git a/crates/engine/src/cmd/component_clone_tests.rs b/crates/engine/src/cmd/component_clone_tests.rs index 4d9faaa2..081d4038 100644 --- a/crates/engine/src/cmd/component_clone_tests.rs +++ b/crates/engine/src/cmd/component_clone_tests.rs @@ -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(); diff --git a/crates/engine/src/cmd/component_transfer.rs b/crates/engine/src/cmd/component_transfer.rs index 68592727..08ede204 100644 --- a/crates/engine/src/cmd/component_transfer.rs +++ b/crates/engine/src/cmd/component_transfer.rs @@ -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, 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; } diff --git a/crates/engine/src/cmd/construct.rs b/crates/engine/src/cmd/construct.rs index 38e4103e..460cbbdd 100644 --- a/crates/engine/src/cmd/construct.rs +++ b/crates/engine/src/cmd/construct.rs @@ -135,8 +135,8 @@ fn geo(s: &Session, v: Option<&Value>, cmd: &str, what: &str) -> Result Ok(g) } -fn t_param(s: &Session, p: &Value, cmd: &str) -> Result { - 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 { + 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) } diff --git a/crates/engine/src/cmd/edit.rs b/crates/engine/src/cmd/edit.rs index 2c90bf77..9ac6a07c 100644 --- a/crates/engine/src/cmd/edit.rs +++ b/crates/engine/src/cmd/edit.rs @@ -767,7 +767,7 @@ fn edit_feature(s: &mut Session, p: &Value) -> Result { 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 { && 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::>(); 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 { && 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(); } diff --git a/crates/engine/src/cmd/face_tests.rs b/crates/engine/src/cmd/face_tests.rs index 43740ebf..5ff74c3a 100644 --- a/crates/engine/src/cmd/face_tests.rs +++ b/crates/engine/src/cmd/face_tests.rs @@ -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(); diff --git a/crates/engine/src/cmd/features.rs b/crates/engine/src/cmd/features.rs index c9055cb4..eaf371ae 100644 --- a/crates/engine/src/cmd/features.rs +++ b/crates/engine/src/cmd/features.rs @@ -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 { 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 { }, 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 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 { 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 { 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 { } } -fn radius_expr(s: &Session, p: &Value, cmd: &str) -> Result { +fn radius_expr(s: &Session, p: &Value, cmd: &str) -> Result { 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 { (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::(), size.parse::()) { - (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::(), size.parse::()) { + (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 { 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 { @@ -968,8 +975,8 @@ fn press_pull(s: &mut Session, p: &Value) -> Result { // 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::() { - 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 { _ => 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 { _ => 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 { "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 { 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 { 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 { @@ -1091,7 +1113,12 @@ fn combine(s: &mut Session, p: &Value) -> Result { 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 { } 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 { 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, pattern: solidcraft_doc::PatternKind, bodies: Vec) -> Result { 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 { 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 { 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 { 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 { 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 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 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 { 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 { 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 { 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 { (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 { if let Some(d) = &depth { check_expr(s, d, Kind::Length, cmd, "depth")?; } - let get = |k: &str| -> Result { + let get = |k: &str| -> Result { let e = req_expr(cmd, p, k)?; Ok(e) }; @@ -1570,7 +1632,20 @@ fn hole(s: &mut Session, p: &Value) -> Result { { 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 { 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 { 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 { @@ -1673,22 +1758,22 @@ fn sweep(s: &mut Session, p: &Value) -> Result { 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, Option)> { +pub(super) fn sweep_options(s: &Session, p: &Value, cmd: &str) -> Result<(SweepOrientation, Option, Option)> { 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 { 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 { @@ -1769,7 +1862,7 @@ fn thread(s: &mut Session, p: &Value) -> Result { 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 { 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 { 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 { @@ -1810,7 +1914,7 @@ fn scale(s: &mut Session, p: &Value) -> Result { 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 { }; 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 { 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 { @@ -1836,7 +1940,11 @@ fn offset_face(s: &mut Session, p: &Value) -> Result { 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)] diff --git a/crates/engine/src/cmd/features_more.rs b/crates/engine/src/cmd/features_more.rs index f2dd11e3..556a0319 100644 --- a/crates/engine/src/cmd/features_more.rs +++ b/crates/engine/src/cmd/features_more.rs @@ -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 { (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 { p, FeatureKind::Coil { base, - axis, + axis: Dir(axis), diameter, pitch, turns, @@ -101,7 +103,7 @@ fn coil(s: &mut Session, p: &Value) -> Result { 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 { 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 { @@ -212,11 +214,15 @@ fn emboss(s: &mut Session, p: &Value) -> Result { } 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 { @@ -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 { 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 { @@ -376,7 +382,18 @@ fn align(s: &mut Session, p: &Value) -> Result { _ => (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 { @@ -389,7 +406,7 @@ fn remove(s: &mut Session, p: &Value) -> Result { 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 { 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), + }, + ) } diff --git a/crates/engine/src/cmd/plastic.rs b/crates/engine/src/cmd/plastic.rs index 4423f3bd..e825ab75 100644 --- a/crates/engine/src/cmd/plastic.rs +++ b/crates/engine/src/cmd/plastic.rs @@ -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 { 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 { 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> { +fn checked(s: &Session, p: &Value, k: &str, kind: Kind, cmd: &str) -> Result> { 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 Result { +fn required(s: &Session, p: &Value, k: &str, kind: Kind, cmd: &str) -> Result { 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 { 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 { 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 { 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 { 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) } diff --git a/crates/engine/src/cmd/sheet.rs b/crates/engine/src/cmd/sheet.rs index 0d0d55a1..630a028d 100644 --- a/crates/engine/src/cmd/sheet.rs +++ b/crates/engine/src/cmd/sheet.rs @@ -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> { 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> { + let own = |k: &str, kind: Kind| -> Result> { 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> { 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 { "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 { s, p, FeatureKind::SheetContour { - sketch, + sketch: SketchId(sketch), curves, distance, rule, @@ -225,7 +231,7 @@ fn flange(s: &mut Session, p: &Value) -> Result { 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 { 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 { @@ -264,7 +270,13 @@ fn hem(s: &mut Session, p: &Value) -> Result { 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 { 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 { } fn unfold(s: &mut Session, p: &Value) -> Result { - 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 { - 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 { @@ -355,7 +378,11 @@ fn convert(s: &mut Session, p: &Value) -> Result { 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 { @@ -386,7 +413,7 @@ fn rules(s: &mut Session, p: &Value) -> Result { ("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()) { diff --git a/crates/engine/src/cmd/sketch_create.rs b/crates/engine/src/cmd/sketch_create.rs index 9cf50fbb..f82396cb 100644 --- a/crates/engine/src/cmd/sketch_create.rs +++ b/crates/engine/src/cmd/sketch_create.rs @@ -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 { 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`")), }; diff --git a/crates/engine/src/cmd/sketch_modify.rs b/crates/engine/src/cmd/sketch_modify.rs index 6c6249ee..c8f58afb 100644 --- a/crates/engine/src/cmd/sketch_modify.rs +++ b/crates/engine/src/cmd/sketch_modify.rs @@ -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)?; diff --git a/crates/engine/src/cmd/surface.rs b/crates/engine/src/cmd/surface.rs index a2e9deac..93d7a19a 100644 --- a/crates/engine/src/cmd/surface.rs +++ b/crates/engine/src/cmd/surface.rs @@ -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 { 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 = patch_points(p, cmd)?.into_iter().map(Position).collect(); let edges: Vec = 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 { 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 { 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 { 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 { let point = |x: Option<&Value>| -> Result { 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 { 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 Result { 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 { 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 { @@ -294,7 +308,12 @@ fn untrim(s: &mut Session, p: &Value) -> Result { 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 { 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 { @@ -334,7 +353,11 @@ fn surface_sweep(s: &mut Session, p: &Value) -> Result { 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 { @@ -351,7 +374,7 @@ fn surface_loft(s: &mut Session, p: &Value) -> Result { 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 { 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 { 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 { 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 { @@ -452,11 +485,11 @@ fn thicken(s: &mut Session, p: &Value) -> Result { 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 { 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 { }; 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 { @@ -514,11 +551,11 @@ fn trim(s: &mut Session, p: &Value) -> Result { 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)] diff --git a/crates/engine/src/derived_units_tests.rs b/crates/engine/src/derived_units_tests.rs index 9da2efaf..bfce30b7 100644 --- a/crates/engine/src/derived_units_tests.rs +++ b/crates/engine/src/derived_units_tests.rs @@ -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:?}"); } diff --git a/crates/engine/src/frames.rs b/crates/engine/src/frames.rs index 87ceecc3..10fed494 100644 --- a/crates/engine/src/frames.rs +++ b/crates/engine/src/frames.rs @@ -200,8 +200,8 @@ impl Mapper<'_> { fn make_expr(&mut self, m: &Mat, v: &Value) -> Option { 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 { && 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 { }; 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); } } } diff --git a/crates/engine/src/large_assembly_tests.rs b/crates/engine/src/large_assembly_tests.rs index db29a6ac..3fc50e33 100644 --- a/crates/engine/src/large_assembly_tests.rs +++ b/crates/engine/src/large_assembly_tests.rs @@ -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(); } } diff --git a/crates/engine/src/occurrences.rs b/crates/engine/src/occurrences.rs index 2592e375..daf3ea8d 100644 --- a/crates/engine/src/occurrences.rs +++ b/crates/engine/src/occurrences.rs @@ -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() { diff --git a/crates/engine/src/params.rs b/crates/engine/src/params.rs index 4a7ff069..b05f70a0 100644 --- a/crates/engine/src/params.rs +++ b/crates/engine/src/params.rs @@ -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> } /// An expression parameter: a number (mm or degrees by context) or an expression string. -pub fn expr(p: &Value, k: &str) -> Option { +pub fn expr(p: &Value, k: &str) -> Option { 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 { +pub fn req_expr(cmd: &str, p: &Value, k: &str) -> Result { expr(p, k).ok_or_else(|| bad(cmd, format!("`{k}` must be a number or an expression"))) } diff --git a/crates/engine/src/pattern_suppress_tests.rs b/crates/engine/src/pattern_suppress_tests.rs index 0627f772..89182fc8 100644 --- a/crates/engine/src/pattern_suppress_tests.rs +++ b/crates/engine/src/pattern_suppress_tests.rs @@ -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); diff --git a/crates/io/src/lib.rs b/crates/io/src/lib.rs index be8ae3be..334f01f2 100644 --- a/crates/io/src/lib.rs +++ b/crates/io/src/lib.rs @@ -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(), diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 280c1973..3f264c7e 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -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 }; 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 = sels(d, 0) .iter() @@ -3483,7 +3483,7 @@ fn dialog_commands(app: &SolveApp, d: &Dialog) -> Result, 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 { 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) -> Option { 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) -> Option) -> Option) -> Option) -> Option 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) -> Option Some(Sel::Axis { name: axis.to_ascii_uppercase() }), @@ -4400,10 +4401,10 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option) -> Option) -> Option {} } // 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) -> Option { *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) -> Option { 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) -> Option { @@ -4593,10 +4594,10 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option) -> Option = - 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 = 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 = 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) -> Option) -> Option) -> Option = 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) -> Option { 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) -> Option) -> Option) -> Option { 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) -> Option 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) -> Option { 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) -> Option { 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) -> Option { 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) -> Option { 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) -> Option { 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) -> Option { 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) -> Option { 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) -> Option { 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) -> Option { 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 => { diff --git a/crates/ui-egui/src/dialogs_move.rs b/crates/ui-egui/src/dialogs_move.rs index a7698f22..b555f4c3 100644 --- a/crates/ui-egui/src/dialogs_move.rs +++ b/crates/ui-egui/src/dialogs_move.rs @@ -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, 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, + 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)); diff --git a/crates/ui-egui/src/dialogs_plastic.rs b/crates/ui-egui/src/dialogs_plastic.rs index 4fbc4ec9..8a9d85b4 100644 --- a/crates/ui-egui/src/dialogs_plastic.rs +++ b/crates/ui-egui/src/dialogs_plastic.rs @@ -410,8 +410,8 @@ pub fn commands(app: &SolveApp, k: &Pl, inputs: &[SelInput], extra: &Map) -> String { - o.clone().unwrap_or_default() +fn s(o: &Option) -> 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 { - 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 { 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, }; diff --git a/crates/ui-egui/src/dialogs_sheet.rs b/crates/ui-egui/src/dialogs_sheet.rs index 1b52d08d..91b5683b 100644 --- a/crates/ui-egui/src/dialogs_sheet.rs +++ b/crates/ui-egui/src/dialogs_sheet.rs @@ -673,20 +673,20 @@ pub fn for_feature(app: &SolveApp, kind: &FeatureKind) -> Option<(Kind, Vec) = 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 Option<(Kind, Vec Option<(Kind, Vec { @@ -738,8 +742,8 @@ pub fn for_feature(app: &SolveApp, kind: &FeatureKind) -> Option<(Kind, Vec Option<(Kind, Vec { - 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)); } diff --git a/crates/ui-egui/src/dialogs_surface.rs b/crates/ui-egui/src/dialogs_surface.rs index 7621066b..7694ad36 100644 --- a/crates/ui-egui/src/dialogs_surface.rs +++ b/crates/ui-egui/src/dialogs_surface.rs @@ -575,7 +575,7 @@ pub fn commands(app: &SolveApp, k: &Surf, inputs: &[SelInput], extra: &Map) -> Option { 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 Option<(Kind, Vec, Map)> { 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>) = 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 = 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 = points.iter().filter_map(|p| crate::dialogs::face_sel(s, *p)).collect(); + let through: Vec = 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::>())); + extra.insert("points".into(), json!(points.iter().map(|p| pt(p.0)).collect::>())); } 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 = 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, }; diff --git a/crates/ui-egui/src/dialogs_sweep.rs b/crates/ui-egui/src/dialogs_sweep.rs index ad5c7ecc..dd5e2334 100644 --- a/crates/ui-egui/src/dialogs_sweep.rs +++ b/crates/ui-egui/src/dialogs_sweep.rs @@ -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, scale: &Option, frame: &Mat) -> (SweepOpts, Option) { + pub fn of(orientation: &SweepOrientation, twist: &Option, scale: &Option, frame: &Mat) -> (SweepOpts, Option) { 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), diff --git a/crates/ui-egui/src/hole_direction_ui_tests.rs b/crates/ui-egui/src/hole_direction_ui_tests.rs index 014394f6..c6be4157 100644 --- a/crates/ui-egui/src/hole_direction_ui_tests.rs +++ b/crates/ui-egui/src/hole_direction_ui_tests.rs @@ -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()), } } diff --git a/crates/ui-egui/src/hole_placement_ui_tests.rs b/crates/ui-egui/src/hole_placement_ui_tests.rs index d93ac6fb..d87ec460 100644 --- a/crates/ui-egui/src/hole_placement_ui_tests.rs +++ b/crates/ui-egui/src/hole_placement_ui_tests.rs @@ -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()), } } diff --git a/crates/ui-egui/src/primitive_units_tests.rs b/crates/ui-egui/src/primitive_units_tests.rs index 7a4c0b97..7de003a3 100644 --- a/crates/ui-egui/src/primitive_units_tests.rs +++ b/crates/ui-egui/src/primitive_units_tests.rs @@ -156,10 +156,10 @@ fn a_centimetre_design_shows_centimetres() { fn stored(h: &Harness, id: u64) -> Vec { 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:?}"), } } diff --git a/crates/ui-egui/src/revolve_manipulator_tests.rs b/crates/ui-egui/src/revolve_manipulator_tests.rs index e4e42167..1b8c85ad 100644 --- a/crates/ui-egui/src/revolve_manipulator_tests.rs +++ b/crates/ui-egui/src/revolve_manipulator_tests.rs @@ -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() diff --git a/crates/ui-egui/src/sweep_dialog_tests.rs b/crates/ui-egui/src/sweep_dialog_tests.rs index 6f50cf34..a2803e2d 100644 --- a/crates/ui-egui/src/sweep_dialog_tests.rs +++ b/crates/ui-egui/src/sweep_dialog_tests.rs @@ -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] diff --git a/crates/ui-egui/src/timeline.rs b/crates/ui-egui/src/timeline.rs index a1876011..4e7fccd2 100644 --- a/crates/ui-egui/src/timeline.rs +++ b/crates/ui-egui/src/timeline.rs @@ -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() { diff --git a/crates/ui-egui/src/timeline_left.rs b/crates/ui-egui/src/timeline_left.rs index 98d3429e..d701ec49 100644 --- a/crates/ui-egui/src/timeline_left.rs +++ b/crates/ui-egui/src/timeline_left.rs @@ -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() {