From 464a6dfad43eee34bb6ec5c48fd7c3a8534211b7 Mon Sep 17 00:00:00 2001 From: JIJINBEI <87472238+jijinbei@users.noreply.github.com> Date: Thu, 15 Oct 2026 13:13:53 +0900 Subject: [PATCH] Feature walks: generate them from field roles with #[derive(Walk)] (#1251) The three walks over a feature (expressions, references, geometry) are generated from FeatureKind by #[derive(Walk)] in a new proc-macro crate, solidcraft-derive, from #[walk(...)] attributes written next to each field. The hand-written walks in params.rs, feature_refs.rs and feature_geometry.rs are gone; Ruled Surface and Extend have their attributes, and the three golden walk records are unchanged. Co-authored-by: jijinbei --- Cargo.lock | 10 + Cargo.toml | 5 + crates/derive/Cargo.toml | 17 ++ crates/derive/src/lib.rs | 294 +++++++++++++++++++++ crates/doc/Cargo.toml | 1 + crates/doc/src/assembly.rs | 1 + crates/doc/src/document.rs | 306 ++++++++++++++++++++-- crates/doc/src/feature_geometry.rs | 318 +---------------------- crates/doc/src/feature_refs.rs | 229 +---------------- crates/doc/src/lib.rs | 1 + crates/doc/src/params.rs | 393 +---------------------------- crates/doc/src/walk.rs | 358 ++++++++++++++++++++++++++ xtask/src/layers.rs | 2 + 13 files changed, 998 insertions(+), 937 deletions(-) create mode 100644 crates/derive/Cargo.toml create mode 100644 crates/derive/src/lib.rs create mode 100644 crates/doc/src/walk.rs diff --git a/Cargo.lock b/Cargo.lock index 787a523b..704ae952 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3479,6 +3479,15 @@ dependencies = [ "solidcraft-mcp", ] +[[package]] +name = "solidcraft-derive" +version = "0.11.0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "solidcraft-doc" version = "0.11.0" @@ -3487,6 +3496,7 @@ dependencies = [ "serde", "serde_json", "sha2", + "solidcraft-derive", "solidcraft-geom", "solidcraft-kernel", "solidcraft-render", diff --git a/Cargo.toml b/Cargo.toml index 22d28b57..2c62dacf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,10 +24,15 @@ solidcraft-io = { path = "crates/io" } solidcraft-engine = { path = "crates/engine" } solidcraft-ui-egui = { path = "crates/ui-egui" } solidcraft-mcp = { path = "crates/mcp" } +solidcraft-derive = { path = "crates/derive" } # foundation thiserror = "2" serde = { version = "1", features = ["derive", "rc"] } +# derive macros (crates/derive); already in the tree through serde_derive +proc-macro2 = "1" +quote = "1" +syn = { version = "2", features = ["full"] } serde_json = { version = "1", features = ["float_roundtrip"] } log = "0.4" sha2 = "0.10" diff --git a/crates/derive/Cargo.toml b/crates/derive/Cargo.toml new file mode 100644 index 00000000..a4969317 --- /dev/null +++ b/crates/derive/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "solidcraft-derive" +description = "SolidCraft derive macros: the walk over a feature's geometry, from role attributes on its fields" +version.workspace = true +edition.workspace = true +license.workspace = true + +[lib] +proc-macro = true + +[lints] +workspace = true + +[dependencies] +proc-macro2 = { workspace = true } +quote = { workspace = true } +syn = { workspace = true } diff --git a/crates/derive/src/lib.rs b/crates/derive/src/lib.rs new file mode 100644 index 00000000..04a12dae --- /dev/null +++ b/crates/derive/src/lib.rs @@ -0,0 +1,294 @@ +//! `#[derive(Walk)]`: the walks over a feature (`crate::walk::Walk`), generated from the fields of +//! a struct or an enum's variants. Each walk visits the fields in declaration order, and comes in +//! `&` and `&mut`: +//! +//! - expressions (`walk_exprs`): each with its label, kind and role; the inputs' order names their +//! parameters (`dN`); +//! - references (`walk_refs`): sketches by id, features and bodies by name; +//! - geometry (`walk_geometry`): points with their roles, planes, construction references, +//! placement points and profile selections. +//! +//! 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 +//! 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; +//! - `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 +//! the parameter names already given need it); +//! - `picks_faces`, on a nested value: its positions pick faces here (a `FaceAt` that names the +//! face it is on). +//! +//! Conditions and labels are worked out before the walk starts. The generated code names +//! `crate::walk`, so the derive is for `solidcraft-doc`. + +#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] + +use proc_macro::TokenStream; +use proc_macro2::{Span, TokenStream as Tokens}; +use quote::{format_ident, quote}; +use syn::{Data, DeriveInput, Expr, Fields, Ident, Member, parse_macro_input}; + +#[proc_macro_derive(Walk, attributes(walk))] +pub fn derive_walk(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + match expand(&input) { + Ok(t) => t.into(), + Err(e) => e.to_compile_error().into(), + } +} + +/// A field's role in the geometry walk. +enum Geo { + Points(&'static str), + PickIf(Expr), + Placement, + Profiles { sketch: Ident, when: Option }, +} + +/// What a field is, from its `#[walk(...)]` attributes. +#[derive(Default)] +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, +} + +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() + } +} + +fn roles(field: &syn::Field) -> syn::Result { + let mut r = Roles::default(); + for attr in field.attrs.iter().filter(|a| a.path().is_ident("walk")) { + let mut sketch = None; + let mut when = None; + 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::()?), + "when" => when = Some(meta.value()?.parse::()?), + "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")), + } + Ok(()) + })?; + if let Some(sketch) = sketch { + r.geo = Some(Geo::Profiles { sketch, when }); + } else if when.is_some() { + return Err(syn::Error::new_spanned(attr, "`when` goes with `profiles_of`")); + } + } + Ok(r) +} + +#[derive(Clone, Copy, PartialEq)] +enum Which { + Exprs, + Refs, + Geometry, +} + +/// A value worked out before the walk (a condition, a label, a sketch id) for field `b`. +fn pre(b: &Ident, what: &str) -> Ident { + format_ident!("__{}_{}", what, b) +} + +/// The statements that walk field `b` in walk `which`. +fn walk_field(b: &Ident, r: &Roles, which: Which, mutable: bool) -> Tokens { + let w = quote!(crate::walk); + let each = if mutable { quote!(each_mut) } else { quote!(each) }; + if r.nested() { + let m = match which { + Which::Exprs => "walk_exprs", + Which::Refs => "walk_refs", + Which::Geometry => "walk_geometry", + }; + let m = if mutable { format_ident!("{}_mut", m) } else { format_ident!("{}", m) }; + if r.picks_faces && which == Which::Geometry { + return quote! { + #w::Walk::#m(#b, &mut |g| f(match g { #w::Geo::Pos(p) => #w::Geo::Pick(p), g => g })); + }; + } + return quote! { #w::Walk::#m(#b, f); }; + } + match which { + Which::Exprs => { + let label = pre(b, "label"); + match (&r.expr, &r.geo) { + (Some((kind, _)), _) => { + let kind = Ident::new(kind, Span::call_site()); + quote! { + #w::ExprField::#each(#b, &mut |i, n, e| f((#w::Label::at(&#label, i, n), e, #w::Kind::#kind, #w::ExprRole::Input))); + } + } + (None, Some(Geo::Placement)) => quote! { + #w::ExprField::#each(#b, &mut |i, n, e| f((#w::Label::at(&["X", "Y", "Z"], i, n), e, #w::Kind::Length, #w::ExprRole::Placement))); + }, + _ => 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::Geometry => match &r.geo { + Some(Geo::Points(v)) => { + let v = Ident::new(v, Span::call_site()); + quote! { #w::Each::<#w::Vec3>::#each(#b, &mut |p| f(#w::Geo::#v(p))); } + } + Some(Geo::PickIf(_)) => { + let pick = pre(b, "pick"); + quote! { + #w::Each::<#w::Vec3>::#each(#b, &mut |p| f(if #pick { #w::Geo::Pick(p) } else { #w::Geo::Pos(p) })); + } + } + Some(Geo::Placement) => quote! { f(#w::Geo::Placement(#b)); }, + Some(Geo::Profiles { .. }) => { + let (id, when) = (pre(b, "sketch"), pre(b, "when")); + quote! { + if #when && let Some(id) = #id { + #w::Each::<#w::ProfileSel>::#each(#b, &mut |sel| f(#w::Geo::Profiles(id, sel))); + } + } + } + None => quote!(), + }, + } +} + +/// The values field `b`'s walk needs, worked out before the walk starts (the walk may hand out +/// the fields they read). +fn pre_values(b: &Ident, r: &Roles, which: Which) -> Tokens { + let w = quote!(crate::walk); + match (which, &r.expr, &r.geo) { + (Which::Exprs, Some((_, label)), _) => { + let l = pre(b, "label"); + quote! { let #l = #label; } + } + (Which::Geometry, _, Some(Geo::PickIf(cond))) => { + let p = pre(b, "pick"); + quote! { let #p: bool = #cond; } + } + (Which::Geometry, _, Some(Geo::Profiles { sketch, when })) => { + let (id, wh) = (pre(b, "sketch"), pre(b, "when")); + let when = when.as_ref().map_or(quote!(true), |c| quote!(#c)); + quote! { + let #wh: bool = #when; + let mut #id: Option = None; + #w::Each::::each(&*#sketch, &mut |s| #id = Some(*s)); + } + } + _ => quote!(), + } +} + +/// A pattern that binds every field of `fields` by name (`_0`, `_1`… for tuple fields), and the +/// body of walk `which` over them. +fn arm(fields: &Fields, which: Which, mutable: bool) -> syn::Result<(Tokens, Tokens)> { + let mut members = Vec::new(); + let mut walked: Vec<(Ident, Roles)> = Vec::new(); + for (i, field) in fields.iter().enumerate() { + let b = field.ident.clone().unwrap_or_else(|| format_ident!("_{}", i)); + members.push(field.ident.clone().map_or_else(|| Member::Unnamed(i.into()), Member::Named)); + walked.push((b, roles(field)?)); + } + let bindings: Vec = walked.iter().map(|(b, _)| b.clone()).collect(); + // `after = x` moves a field to just after `x`. + for b in &bindings { + let Some(pos) = walked.iter().position(|(x, _)| x == b) else { continue }; + let Some(after) = walked.get(pos).and_then(|(_, r)| r.after.clone()) else { continue }; + let moved = walked.remove(pos); + let Some(at) = walked.iter().position(|(x, _)| *x == after) else { + return Err(syn::Error::new_spanned(after, "no such field")); + }; + walked.insert(at + 1, moved); + } + let mut body = Tokens::new(); + for (b, r) in &walked { + body.extend(pre_values(b, r, which)); + } + for (b, r) in &walked { + body.extend(walk_field(b, r, which, mutable)); + } + let pattern = match fields { + Fields::Named(_) => quote!({ #(#bindings),* }), + Fields::Unnamed(_) => quote!({ #(#members: #bindings),* }), + Fields::Unit => quote!(), + }; + Ok((pattern, body)) +} + +fn expand(input: &DeriveInput) -> syn::Result { + let name = &input.ident; + let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); + let w = quote!(crate::walk); + let mut methods = Tokens::new(); + for (which, method, item) in + [(Which::Exprs, "walk_exprs", "ExprItem"), (Which::Refs, "walk_refs", "RefItem"), (Which::Geometry, "walk_geometry", "GeoItem")] + { + for mutable in [false, true] { + let body = match &input.data { + Data::Struct(s) => { + let (pattern, body) = arm(&s.fields, which, mutable)?; + quote! { let Self #pattern = self; #body } + } + Data::Enum(e) => { + let mut arms = Tokens::new(); + for v in &e.variants { + let ident = &v.ident; + let (pattern, body) = arm(&v.fields, which, mutable)?; + arms.extend(quote! { Self::#ident #pattern => { #body } }); + } + quote! { match self { #arms } } + } + Data::Union(u) => return Err(syn::Error::new_spanned(u.union_token, "unions are not walked")), + }; + let (method, item, r) = if mutable { + (format_ident!("{}_mut", method), format_ident!("{}Mut", item), quote!(&'a mut)) + } else { + (format_ident!("{}", method), format_ident!("{}", item), quote!(&'a)) + }; + methods.extend(quote! { + #[allow(unused_variables, unused_mut, clippy::needless_borrow)] + fn #method<'a>(#r self, f: &mut dyn FnMut(#w::#item<'a>)) { #body } + }); + } + } + Ok(quote! { + impl #impl_generics #w::Walk for #name #ty_generics #where_clause { #methods } + }) +} diff --git a/crates/doc/Cargo.toml b/crates/doc/Cargo.toml index 4c6253da..4fac21b0 100644 --- a/crates/doc/Cargo.toml +++ b/crates/doc/Cargo.toml @@ -13,6 +13,7 @@ solidcraft-geom = { workspace = true } solidcraft-sketch = { workspace = true } solidcraft-kernel = { workspace = true } solidcraft-render = { workspace = true } +solidcraft-derive = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } thiserror = { workspace = true } diff --git a/crates/doc/src/assembly.rs b/crates/doc/src/assembly.rs index 0d2f5a1e..aeda6e26 100644 --- a/crates/doc/src/assembly.rs +++ b/crates/doc/src/assembly.rs @@ -205,6 +205,7 @@ impl FeatureKind { /// Map the feature's world geometry (points, directions, explicit planes) by `m`. pub fn transform_geometry(&mut self, m: &Mat) { use crate::feature_geometry::Geo; + use crate::walk::Walk; self.walk_geometry_mut(&mut |g| match g { Geo::Pick(p) | Geo::Pos(p) => *p = apply_point(m, *p), Geo::Dir(v) => *v = apply_vector(m, *v), diff --git a/crates/doc/src/document.rs b/crates/doc/src/document.rs index 076513c6..d9b19331 100644 --- a/crates/doc/src/document.rs +++ b/crates/doc/src/document.rs @@ -187,48 +187,57 @@ pub enum RevolveConvention { } /// Extent of an extrude. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, solidcraft_derive::Walk)] pub struct Extent { + #[walk(length = "Distance")] pub distance: String, #[serde(default)] pub direction: Direction, /// 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, /// 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, /// 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, /// 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, /// 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, /// 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, } /// Thin Extrude: a wall of constant thickness along sketch curves instead of the profiles. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, solidcraft_derive::Walk)] pub struct ThinWall { /// The sketch curves the wall follows: open chains and closed loops, joined where they /// meet end to end. pub curves: Vec, /// The wall thickness (expression); with `side: two_sides`, the thickness inside. + #[walk(length = "Thickness")] pub thickness: String, #[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, } @@ -263,7 +272,7 @@ impl ThinSide { } /// Revolve axis. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, solidcraft_derive::Walk)] #[serde(tag = "type", rename_all = "snake_case")] pub enum AxisRef { /// A line of the profile's sketch. @@ -273,13 +282,20 @@ pub enum AxisRef { /// World X / Y / Z axis through the origin. World { axis: String }, /// Any line in the sketch plane, in world coordinates. - Line { origin: Vec3, dir: Vec3 }, + Line { + #[walk(pos)] + origin: Vec3, + #[walk(dir)] + dir: Vec3, + }, } /// Persistent cylindrical Emboss selection, authored in the feature component frame. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, solidcraft_derive::Walk)] pub struct EmbossFace { + #[walk(body)] pub body: String, + #[walk(pinned)] pub at: Vec3, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub path: Vec, @@ -359,10 +375,13 @@ impl EmbossFace { /// edge's own angle and height (expressions) in place of the feature's. In a design file an /// edge with neither is the point `[x, y, z]`, as every edge was before; one with its own /// values is `{"point": [x, y, z], "angle": "…", "height": "…"}`. -#[derive(Clone, Debug, Default, PartialEq)] +#[derive(Clone, Debug, Default, PartialEq, solidcraft_derive::Walk)] pub struct FlangeEdge { + #[walk(pos)] pub point: Vec3, + #[walk(angle = "Edge angle")] pub angle: Option, + #[walk(length = "Edge height")] pub height: Option, } @@ -423,12 +442,13 @@ impl<'de> Deserialize<'de> for FlangeEdge { } /// Feature definitions. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, solidcraft_derive::Walk)] #[serde(tag = "type", rename_all = "snake_case")] 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, from: u64, to: u64, @@ -440,29 +460,38 @@ pub enum FeatureKind { }, Extrude { /// Feature id of the sketch. + #[walk(sketch)] sketch: u64, + // A thin extrude follows curves; its profiles are not used. + #[walk(profiles_of = sketch, when = thin.is_none())] profiles: ProfileSel, extent: Extent, #[serde(default)] operation: Operation, /// Target body names for join/cut/intersect (empty = all bodies). #[serde(default, skip_serializing_if = "Vec::is_empty")] + #[walk(body)] 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, + #[walk(profiles_of = sketch)] profiles: ProfileSel, axis: AxisRef, + #[walk(angle = "Angle")] angle: String, #[serde(default)] operation: Operation, #[serde(default, skip_serializing_if = "Vec::is_empty")] + #[walk(body)] targets: Vec, /// Two Sides: the angle on the other side. #[serde(default, skip_serializing_if = "Option::is_none")] + #[walk(angle = "Angle 2")] angle2: Option, /// Symmetric: `angle` split evenly either side of the profile. #[serde(default, skip_serializing_if = "std::ops::Not::not")] @@ -472,13 +501,17 @@ 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, }, Fillet { /// Reference points on the edges to round (each re-finds the nearest edge). + #[walk(pos)] edges: Vec, + #[walk(length = style.radius_label())] radius: String, #[serde(default, skip_serializing_if = "Option::is_none")] + #[walk(body)] body: Option, /// Constant radius (the default), chord length (`radius` is the width across), or a /// radius varying along each edge. @@ -489,56 +522,76 @@ pub enum FeatureKind { fit: FilletFit, }, Chamfer { + #[walk(pos)] edges: Vec, + #[walk(length = "Distance")] distance: String, /// Unequal chamfers: the distance along the second face… #[serde(default, skip_serializing_if = "Option::is_none")] + #[walk(length = "Distance 2")] 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, /// 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, }, Box { #[serde(with = "point_expr")] + #[walk(placement)] corner: PointExpr, + #[walk(length = "Length")] length: String, + #[walk(length = "Width")] width: String, + #[walk(length = "Height")] height: String, #[serde(default)] operation: Operation, }, Cylinder { #[serde(with = "point_expr")] + #[walk(placement)] base: PointExpr, #[serde(default = "z_axis")] + #[walk(dir)] axis: Vec3, + #[walk(length = "Radius")] radius: String, + #[walk(length = "Height")] height: String, #[serde(default)] operation: Operation, }, Sphere { #[serde(with = "point_expr")] + #[walk(placement)] center: PointExpr, + #[walk(length = "Radius")] radius: String, #[serde(default)] operation: Operation, }, Torus { #[serde(with = "point_expr")] + #[walk(placement)] center: PointExpr, + #[walk(length = "Major radius")] major: String, + #[walk(length = "Minor radius")] minor: String, #[serde(default)] operation: Operation, }, Combine { + #[walk(body)] target: String, + #[walk(body)] tools: Vec, operation: Operation, #[serde(default)] @@ -546,18 +599,22 @@ pub enum FeatureKind { }, /// Copies of other features' results, or of bodies (rectangular, circular or along a path). Pattern { + #[walk(feature)] 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, }, /// Mirror copies of other features' results in a plane. Mirror { + #[walk(feature)] features: Vec, plane: PlaneRef, /// Mirror these bodies instead of replaying features. #[serde(default, skip_serializing_if = "Vec::is_empty")] + #[walk(body)] bodies: Vec, /// Join each mirrored body with its original. #[serde(default, skip_serializing_if = "std::ops::Not::not")] @@ -565,12 +622,16 @@ 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, /// Drilling direction (into the material). + #[walk(dir)] direction: Vec3, + #[walk(length = "Diameter")] diameter: String, /// Depth to the shoulder; `None` = through all. #[serde(default, skip_serializing_if = "Option::is_none")] + #[walk(length = "Depth")] depth: Option, #[serde(default)] hole: HoleKind, @@ -583,17 +644,20 @@ 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, }, /// 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, #[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, #[serde(default, skip_serializing_if = "std::ops::Not::not")] modeled: bool, @@ -604,24 +668,32 @@ pub enum FeatureKind { #[serde(default)] operation: Operation, #[serde(default, skip_serializing_if = "Vec::is_empty")] + #[walk(body)] targets: Vec, }, /// Sweep profiles along a chain of sketch curves. Sweep { + #[walk(sketch)] sketch: u64, + #[walk(profiles_of = sketch)] profiles: ProfileSel, + #[walk(sketch)] path_sketch: u64, path: Vec, #[serde(default)] operation: Operation, #[serde(default, skip_serializing_if = "Vec::is_empty")] + #[walk(body)] targets: Vec, }, /// Hollow a body leaving walls of `thickness`, removing the faces at the given points. Shell { + #[walk(pick)] faces: Vec, + #[walk(length = "Thickness")] thickness: String, #[serde(default, skip_serializing_if = "Option::is_none")] + #[walk(body)] body: Option, /// inside (default) | outside | both (the wall straddles the faces). #[serde(default, skip_serializing_if = "String::is_empty")] @@ -632,11 +704,15 @@ pub enum FeatureKind { }, /// Tilt faces (at the given points) about a neutral plane, leaning toward `pull`. Draft { + #[walk(pick)] faces: Vec, + #[walk(angle = "Angle")] angle: String, neutral: PlaneRef, + #[walk(dir)] pull: Vec3, #[serde(default, skip_serializing_if = "Option::is_none")] + #[walk(body)] body: Option, }, /// A construction plane (sketch placement, split tool). @@ -646,6 +722,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, plane: PlaneRef, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -663,38 +740,50 @@ 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, #[serde(default, skip_serializing_if = "Option::is_none")] + #[walk(body)] 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, }, /// Scale bodies about a point (uniform, or per axis). Scale { + #[walk(body)] bodies: Vec, #[serde(default)] + #[walk(pos)] origin: Vec3, + #[walk(unitless = "Scale factor")] factor: String, /// 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]>, }, /// Move planar faces (at the given points) along their normals; the body keeps its shape. OffsetFace { + #[walk(pick)] faces: Vec, + #[walk(length = "Distance")] distance: String, #[serde(default, skip_serializing_if = "Option::is_none")] + #[walk(body)] 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, #[serde(default, skip_serializing_if = "Option::is_none")] + #[walk(body)] body: Option, /// Close the gap by extending the neighbouring faces (the default); `false` leaves it /// open, the faces left a surface body. @@ -703,42 +792,55 @@ pub enum FeatureKind { }, /// A box around bodies, grown by a margin. BoundingSolid { + #[walk(body)] bodies: Vec, #[serde(default = "zero_expr")] + #[walk(length = "Margin")] margin: String, }, /// A tube along a chain of sketch curves (solid, or hollow with a wall thickness). Pipe { + #[walk(sketch)] path_sketch: u64, path: Vec, + #[walk(length = "Diameter")] diameter: String, #[serde(default, skip_serializing_if = "Option::is_none")] + #[walk(length = "Wall")] wall: Option, #[serde(default)] operation: Operation, #[serde(default, skip_serializing_if = "Vec::is_empty")] + #[walk(body)] targets: Vec, }, /// Raise or sink profiles on a planar face, or wrap them on a selected cylindrical wall. Emboss { + #[walk(sketch)] sketch: u64, + #[walk(profiles_of = sketch)] profiles: ProfileSel, + #[walk(length = "Depth")] depth: String, #[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, }, /// 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, curves: Vec, + #[walk(length = "Thickness")] thickness: String, /// 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, /// Fill toward the other side of the curves. #[serde(default, skip_serializing_if = "std::ops::Not::not")] @@ -749,19 +851,26 @@ pub enum FeatureKind { }, /// Move planar faces (at the given points) onto a target plane parallel to them. ReplaceFace { + #[walk(pick)] faces: Vec, target: PlaneRef, #[serde(default, skip_serializing_if = "Option::is_none")] + #[walk(body)] 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, #[serde(default, skip_serializing_if = "Option::is_none")] + #[walk(dir)] from_normal: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + #[walk(dir)] to_normal: Option, /// Normals the same way instead of facing each other. #[serde(default, skip_serializing_if = "std::ops::Not::not")] @@ -771,12 +880,18 @@ pub enum FeatureKind { /// `pitch` per turn, on a helix of `diameter` (the section's centre, or its inside or outside). Coil { #[serde(with = "point_expr")] + #[walk(placement)] base: PointExpr, #[serde(default = "z_axis")] + #[walk(dir)] axis: Vec3, + #[walk(length = "Diameter")] diameter: String, + #[walk(length = "Pitch")] pitch: String, + #[walk(unitless = "Revolutions")] turns: String, + #[walk(length = "Section size")] section_size: String, #[serde(default)] section: CoilSection, @@ -786,6 +901,7 @@ pub enum FeatureKind { /// Start angle about the axis (expression; 0 = from the axis toward +x, or a /// perpendicular direction when the axis is x). #[serde(default, skip_serializing_if = "Option::is_none")] + #[walk(angle = "Start angle")] start_angle: Option, /// Turn the other way (left-handed). #[serde(default, skip_serializing_if = "std::ops::Not::not")] @@ -793,11 +909,14 @@ pub enum FeatureKind { #[serde(default)] operation: Operation, #[serde(default, skip_serializing_if = "Vec::is_empty")] + #[walk(body)] targets: Vec, }, /// Sheet metal base flange: a sheet from a closed profile (thickness from the rule). SheetBase { + #[walk(sketch)] sketch: u64, + #[walk(profiles_of = sketch)] profiles: ProfileSel, #[serde(default, skip_serializing_if = "Option::is_none")] rule: Option, @@ -807,8 +926,10 @@ pub enum FeatureKind { }, /// Sheet metal contour flange: a sheet bent along an open chain of sketch lines. SheetContour { + #[walk(sketch)] sketch: u64, curves: Vec, + #[walk(length = "Distance")] distance: String, #[serde(default, skip_serializing_if = "Option::is_none")] rule: Option, @@ -822,10 +943,14 @@ pub enum FeatureKind { /// Edge flanges on sheet edges (points on the top or bottom edges). `height` and `angle` /// hold for every edge that doesn't carry its own (see [`FlangeEdge`]). SheetFlange { + #[walk(after = radius)] edges: Vec, + #[walk(length = "Height")] height: String, + #[walk(angle = "Angle")] angle: String, #[serde(default, skip_serializing_if = "Option::is_none")] + #[walk(length = "Bend radius")] radius: Option, /// inside | outside | middle. #[serde(default = "inside_str")] @@ -833,17 +958,22 @@ pub enum FeatureKind { #[serde(default, skip_serializing_if = "std::ops::Not::not")] flip: bool, #[serde(default, skip_serializing_if = "Option::is_none")] + #[walk(body)] body: Option, }, /// Flat hems on sheet edges. SheetHem { + #[walk(pos)] edges: Vec, + #[walk(length = "Length")] length: String, #[serde(default, skip_serializing_if = "Option::is_none")] + #[walk(length = "Gap")] 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, }, /// A perforation field (#732): round holes of `diameter` on a staggered (60°) or square grid of `pitch` over the @@ -851,26 +981,35 @@ pub enum FeatureKind { /// a cosmetic thread: the holes go to the flat pattern, the DXF and the measured volume, not into the B-rep, so a /// panel can carry tens of thousands of them. SheetPerforate { + #[walk(length = "Diameter")] diameter: String, + #[walk(length = "Pitch")] pitch: String, #[serde(default, skip_serializing_if = "Option::is_none")] + #[walk(length = "Margin")] margin: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pattern: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + #[walk(body)] 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, #[serde(default, skip_serializing_if = "Option::is_none")] curve: Option, + #[walk(pos)] a: Vec3, + #[walk(pos)] b: Vec3, + #[walk(angle = "Angle")] angle: String, #[serde(default, skip_serializing_if = "Option::is_none")] + #[walk(length = "Bend radius")] radius: Option, /// centerline | start | end | mould position: String, @@ -878,13 +1017,16 @@ pub enum FeatureKind { 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, #[serde(default, skip_serializing_if = "Option::is_none")] + #[walk(body)] 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, #[serde(default, skip_serializing_if = "std::ops::Not::not")] refold: bool, @@ -893,105 +1035,146 @@ 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, + #[walk(pick_if = !bends.is_empty())] face: Vec3, #[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, }, /// Plastic boss: a round post on a face (draft, root fillet, hole, ribs). Boss { + #[walk(pick)] position: Vec3, /// Out of the face (the boss grows along it). #[serde(default = "z_axis")] + #[walk(dir)] direction: Vec3, + #[walk(length = "Diameter")] diameter: String, + #[walk(length = "Height")] height: String, #[serde(default, skip_serializing_if = "Option::is_none")] + #[walk(length = "Hole diameter")] 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, #[serde(default, skip_serializing_if = "Option::is_none")] + #[walk(angle = "Draft angle")] draft: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + #[walk(length = "Root fillet")] fillet: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + #[walk(unitless = "Ribs")] ribs: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + #[walk(length = "Rib thickness")] 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 top below the boss top. #[serde(default, skip_serializing_if = "Option::is_none")] + #[walk(length = "Rib offset")] rib_offset: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + #[walk(body)] body: Option, }, /// Plastic lip (raised band) or groove (recess) along the rim face picked at `face`. Lip { + #[walk(pos)] face: Vec3, + #[walk(length = "Width")] width: String, + #[walk(length = "Height")] height: String, #[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, /// 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, }, /// Plastic snap fit: a cantilever arm on a face with a hook (catch) at its tip. SnapFit { + #[walk(pick)] position: Vec3, /// The arm grows along this (out of the face). #[serde(default = "z_axis")] + #[walk(dir)] direction: Vec3, /// The catch sticks out this way. + #[walk(dir)] hook: Vec3, + #[walk(length = "Length")] length: String, + #[walk(length = "Thickness")] thickness: String, + #[walk(length = "Width")] width: String, + #[walk(length = "Catch depth")] catch_depth: String, /// The catch's height along the arm. + #[walk(length = "Catch length")] catch_length: String, #[serde(default, skip_serializing_if = "Option::is_none")] + #[walk(body)] 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, /// Out of the face (the pad grows along it). #[serde(default = "z_axis")] + #[walk(dir)] direction: Vec3, /// 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, /// Diameter of a round rest, or the rectangle's width. + #[walk(length = "Width")] width: String, /// The rectangle's length (none: round). #[serde(default, skip_serializing_if = "Option::is_none")] + #[walk(length = "Length", after = height)] length: Option, + #[walk(length = "Height")] height: String, #[serde(default, skip_serializing_if = "Option::is_none")] + #[walk(angle = "Draft angle")] draft: Option, /// Wall thickness of a hollow rest (none: solid). #[serde(default, skip_serializing_if = "Option::is_none")] + #[walk(length = "Wall thickness")] thickness: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + #[walk(body)] 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, #[serde(default)] operation: Operation, @@ -1001,43 +1184,55 @@ pub enum FeatureKind { }, /// Remove bodies from the model (from here on in the timeline). Remove { + #[walk(body)] 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, #[serde(default = "all_profiles")] + #[walk(profiles_of = sketch)] profiles: ProfileSel, #[serde(default, skip_serializing_if = "Option::is_none")] + #[walk(body)] body: Option, #[serde(default, skip_serializing_if = "Vec::is_empty")] + #[walk(pos)] edges: Vec, }, /// Surface bodies sewn along shared edges into the first one (a solid when they close). Stitch { + #[walk(body)] bodies: Vec, #[serde(default = "zero_expr")] + #[walk(length = "Tolerance")] tolerance: String, }, /// Surface bodies thickened into solids along their normals (by half each way when /// `symmetric`). Thicken { + #[walk(body)] bodies: Vec, + #[walk(length = "Thickness")] thickness: String, #[serde(default)] symmetric: bool, operation: Operation, #[serde(default, skip_serializing_if = "Vec::is_empty")] + #[walk(body)] 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, /// Profiles whose boundary loops are swept (none: only `curves`). #[serde(default, skip_serializing_if = "Option::is_none")] + #[walk(profiles_of = sketch)] profiles: Option, /// Sketch curve ids, grouped into chains of curves that meet end to end. #[serde(default, skip_serializing_if = "Vec::is_empty")] @@ -1047,14 +1242,18 @@ 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, #[serde(default, skip_serializing_if = "Vec::is_empty")] + #[walk(pos)] faces: Vec, }, /// A surface body with the faces at `faces` (every face when empty) turned over. ReverseNormal { + #[walk(body)] body: String, #[serde(default, skip_serializing_if = "Vec::is_empty")] + #[walk(pos)] faces: Vec, }, /// The face at `face` restored towards its surface: the inner loops at the `holes` points @@ -1062,8 +1261,11 @@ pub enum FeatureKind { /// back at the surface's natural bounds (an analytic surface's: the face's own span). A new /// surface body; with `in_place`, a surface body's face is untrimmed where it is. SurfaceUntrim { + // The face is picked (and named); the holes are picked by points on their edges. + #[walk(picks_faces)] face: FaceAt, #[serde(default, skip_serializing_if = "Option::is_none")] + #[walk(pos)] holes: Option>, #[serde(default, skip_serializing_if = "std::ops::Not::not")] outer: bool, @@ -1073,15 +1275,19 @@ 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, #[serde(default, skip_serializing_if = "Option::is_none")] + #[walk(profiles_of = sketch)] profiles: Option, #[serde(default, skip_serializing_if = "Vec::is_empty")] curves: Vec, axis: AxisRef, + #[walk(angle = "Angle")] angle: String, /// Two Sides: the angle on the other side. #[serde(default, skip_serializing_if = "Option::is_none")] + #[walk(angle = "Angle 2")] angle2: Option, /// Symmetric: `angle` split evenly either side of the sketch. #[serde(default, skip_serializing_if = "std::ops::Not::not")] @@ -1090,11 +1296,14 @@ 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, #[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: Vec, }, @@ -1107,13 +1316,17 @@ 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, #[serde(default, skip_serializing_if = "Option::is_none")] + #[walk(body)] body: Option, + #[walk(length = "Distance")] distance: String, #[serde(default, skip_serializing_if = "std::ops::Not::not")] chain: bool, #[serde(default, skip_serializing_if = "Option::is_none")] + #[walk(length = "Tolerance")] tolerance: Option, }, /// A strip of ruled surface `distance` wide hanging off the chain of `body`'s edges through @@ -1121,20 +1334,26 @@ pub enum FeatureKind { /// 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, + #[walk(length = "Distance")] distance: String, #[serde(default, skip_serializing_if = "RuledMode::is_normal")] mode: RuledMode, #[serde(default, skip_serializing_if = "Option::is_none")] + #[walk(angle = "Angle")] angle: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + #[walk(dir)] 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, }, /// A surface body's boundary edges through `edges` extended outward: by `distance`, or up @@ -1142,8 +1361,11 @@ pub enum FeatureKind { /// (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, + #[walk(length = "Distance")] distance: String, #[serde(default, skip_serializing_if = "ExtendMode::is_natural")] mode: ExtendMode, @@ -1155,18 +1377,24 @@ 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, plane: PlaneRef, #[serde(default, skip_serializing_if = "Option::is_none")] tool: Option, + #[walk(pos)] keep: Vec3, }, Move { + #[walk(body)] bodies: Vec, + #[walk(length = ["X distance", "Y distance", "Z distance"])] translate: [String; 3], #[serde(default, skip_serializing_if = "Option::is_none")] + #[walk(dir)] rotate_axis: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + #[walk(angle = "Angle")] 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. @@ -1187,7 +1415,9 @@ pub enum FeatureKind { }, /// Rebuild an upstream constant-section mesh with analytic planar/cylindrical faces. MeshConvert { + #[walk(body)] body: String, + #[walk(length = "Tolerance")] tolerance: String, }, /// Mesh bodies imported from a 3MF or STL file, embedded in the design. @@ -1218,7 +1448,7 @@ impl ExtendMode { } /// What Extend extends up to instead of a distance. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, solidcraft_derive::Walk)] #[serde(rename_all = "snake_case")] pub enum ExtendTo { /// A plane. @@ -1230,7 +1460,7 @@ pub enum ExtendTo { } /// How a fillet's size is given. -#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, solidcraft_derive::Walk)] #[serde(tag = "type", rename_all = "snake_case")] pub enum FilletStyle { #[default] @@ -1243,9 +1473,12 @@ pub enum FilletStyle { /// `radius` at the end of each edge nearest `start`, `radius2` at the other; `mid` radii at /// 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, #[serde(default, skip_serializing_if = "Vec::is_empty")] + #[walk(length = "Mid radius")] mid: Vec<(f64, String)>, #[serde(default, skip_serializing_if = "std::ops::Not::not")] smooth: bool, @@ -1253,7 +1486,13 @@ pub enum FilletStyle { /// The radius along each edge as a clamped B-spline of the position from the end nearest /// `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 { start: Vec3, knots: Vec, radii: Vec }, + Spline { + #[walk(pos)] + start: Vec3, + knots: Vec, + #[walk(length = |i: usize, n: usize| if i + 1 == n { "End radius" } else { "Control radius" })] + radii: Vec, + }, } /// A fillet radius too large for the geometry around its edges: refused (`Exact`, the default), @@ -1280,6 +1519,16 @@ impl FilletStyle { *self == FilletStyle::Constant } + /// What the feature's `radius` is in this style (the label of its parameter). + pub fn radius_label(&self) -> &'static str { + match self { + FilletStyle::Constant => "Radius", + FilletStyle::Curvature => "Equivalent radius", + FilletStyle::Chord => "Chord length", + FilletStyle::Variable { .. } | FilletStyle::Spline { .. } => "Start radius", + } + } + /// The spline of a [`FilletStyle::Spline`] with its control radii `values` (the first is /// the feature's `radius`). pub fn spline(knots: &[f64], values: Vec) -> solidcraft_kernel::RadiusSpline { @@ -1289,9 +1538,11 @@ impl FilletStyle { } /// A face picked by a point on it, on a named body. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, solidcraft_derive::Walk)] pub struct FaceAt { + #[walk(body)] pub body: String, + #[walk(pos)] pub point: Vec3, } @@ -1308,8 +1559,9 @@ pub struct MeshData { } /// Points of a sketch, by id. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, solidcraft_derive::Walk)] pub struct SketchPoints { + #[walk(sketch)] pub sketch: u64, pub ids: Vec, /// Drill the other way: away from the side the holes would go into by themselves. @@ -1329,9 +1581,11 @@ pub struct Component { } /// One loft section: profiles of a sketch. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, solidcraft_derive::Walk)] pub struct LoftSection { + #[walk(sketch)] pub sketch: u64, + #[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). #[serde(default, skip_serializing_if = "Option::is_none")] @@ -1340,10 +1594,12 @@ pub struct LoftSection { /// A section of a surface loft: one chain from a sketch (one profile's boundary, or curves /// meeting end to end). -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, solidcraft_derive::Walk)] pub struct SheetSection { + #[walk(sketch)] pub sketch: u64, #[serde(default, skip_serializing_if = "Option::is_none")] + #[walk(profiles_of = sketch)] pub profiles: Option, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub curves: Vec, @@ -1363,54 +1619,72 @@ fn is_zero_i8(v: &i8) -> bool { } /// Hole shapes. -#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize, solidcraft_derive::Walk)] #[serde(tag = "type", rename_all = "snake_case")] pub enum HoleKind { /// Plain hole; blind holes get a drill point when `tip_angle` is set. #[default] Simple, Drilled { + #[walk(angle = "Tip angle")] tip_angle: String, }, Counterbore { + #[walk(length = "Counterbore diameter")] cb_diameter: String, + #[walk(length = "Counterbore depth")] cb_depth: String, }, Countersink { + #[walk(length = "Countersink diameter")] cs_diameter: String, + #[walk(angle = "Countersink angle")] cs_angle: String, }, } /// Pattern layouts. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, solidcraft_derive::Walk)] #[serde(tag = "type", rename_all = "snake_case")] pub enum PatternKind { Rectangular { + #[walk(dir)] dir1: Vec3, + #[walk(unitless = "Quantity")] count1: String, + #[walk(length = "Spacing")] spacing1: String, #[serde(default, skip_serializing_if = "Option::is_none")] + #[walk(dir)] dir2: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + #[walk(unitless = "Quantity 2")] count2: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + #[walk(length = "Spacing 2")] spacing2: Option, }, Circular { + #[walk(pos)] origin: Vec3, + #[walk(dir)] axis: Vec3, + #[walk(unitless = "Quantity")] count: String, /// Total angle (360 deg = evenly around). + #[walk(angle = "Total angle")] angle: String, }, /// 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: Vec, + #[walk(unitless = "Quantity")] count: String, /// Distance along the path between instances, or the whole extent when `extent`. + #[walk(length = "Distance")] spacing: String, /// `spacing` is the distance from the first to the last instance. #[serde(default, skip_serializing_if = "std::ops::Not::not")] diff --git a/crates/doc/src/feature_geometry.rs b/crates/doc/src/feature_geometry.rs index 8d1aad92..0994beeb 100644 --- a/crates/doc/src/feature_geometry.rs +++ b/crates/doc/src/feature_geometry.rs @@ -1,14 +1,13 @@ //! The geometry a feature holds, each piece with its role: the points that pick faces (moved //! with the feature, and the source of its persistent face names), other positions, directions, //! planes, construction references, a primitive's placement point and its profile selections. -//! One walk, written once for `&` and `&mut`, behind `transform_geometry`, `face_points`, -//! `profile_sels` and `point_expr`. Every variant is listed, so a new one has to say what each of -//! its points is. +//! The walk (`crate::walk`) is behind `transform_geometry`, `face_points`, `profile_sels` and +//! `point_expr`. use solidcraft_geom::Vec3; -use crate::construct::GeoRef; -use crate::{AxisRef, FeatureKind, PatternKind, PlaneRef, PointExpr, ProfileSel}; +use crate::walk::Walk; +use crate::{FeatureKind, PointExpr, ProfileSel}; /// A piece of a feature's geometry: `V` is `&Vec3` or `&mut Vec3`, and so on for the others. pub(crate) enum Geo { @@ -30,316 +29,7 @@ pub(crate) enum Geo { Profiles(u64, S), } -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>; - -/// Every piece of a feature's geometry, written once for `&` and `&mut`: `$item` is `GeoItem` or -/// `GeoItemMut`, `$refs` the matching `refs` / `refs_mut`, `$m` is `mut` or nothing. Face picks -/// come in the order the feature's face names follow. -macro_rules! geometry_walk { - ($name:ident, $item:ident, $refs:ident $(, $m:tt)?) => { - pub(crate) fn $name<'a>(&'a $($m)? self, f: &mut impl FnMut($item<'a>)) { - use Geo::{Construct, Dir, Pick, Pinned, Placement, Plane, Pos, Profiles}; - match self { - FeatureKind::Sketch { plane, .. } | FeatureKind::ConstructionPlane { plane } | FeatureKind::Mirror { plane, .. } => f(Plane(plane)), - FeatureKind::ConstructionAxis { def } => { - for r in def.$refs() { - f(Construct(r)); - } - } - FeatureKind::ConstructionPoint { def } => { - for r in def.$refs() { - f(Construct(r)); - } - } - FeatureKind::Extrude { sketch, profiles, extent, thin, .. } => { - // A thin extrude follows curves; its profiles are not used. - if thin.is_none() { - f(Profiles(*sketch, profiles)); - } - if let Some(p) = & $($m)? extent.to { - f(Pick(p)); - } - if let Some(p) = & $($m)? extent.from { - f(Pick(p)); - } - } - // Surface Extrude refuses `to` and `from` for now; they would pick faces as for - // Extrude. - FeatureKind::SurfaceExtrude { sketch, profiles, extent, .. } => { - if let Some(s) = profiles { - f(Profiles(*sketch, s)); - } - if let Some(p) = & $($m)? extent.to { - f(Pick(p)); - } - if let Some(p) = & $($m)? extent.from { - f(Pick(p)); - } - } - FeatureKind::Revolve { sketch, profiles, axis, to, .. } => { - f(Profiles(*sketch, profiles)); - if let AxisRef::Line { origin, dir } = axis { - f(Pos(origin)); - f(Dir(dir)); - } - if let Some(p) = to { - f(Pick(p)); - } - } - FeatureKind::SurfaceRevolve { sketch, profiles, axis, .. } => { - if let Some(s) = profiles { - f(Profiles(*sketch, s)); - } - if let AxisRef::Line { origin, dir } = axis { - f(Pos(origin)); - f(Dir(dir)); - } - } - FeatureKind::Sweep { sketch, profiles, .. } | FeatureKind::SheetBase { sketch, profiles, .. } => f(Profiles(*sketch, profiles)), - FeatureKind::SurfaceSweep { sketch, profiles, .. } => { - if let Some(s) = profiles { - f(Profiles(*sketch, s)); - } - } - FeatureKind::Emboss { sketch, profiles, face, .. } => { - f(Profiles(*sketch, profiles)); - if let Some(face) = face { - f(Pinned(& $($m)? face.at)); - } - } - FeatureKind::Loft { sections, .. } => { - // A section that is a point has no profile. - for s in sections { - if s.point.is_none() { - f(Profiles(s.sketch, & $($m)? s.profiles)); - } - } - } - FeatureKind::SurfaceLoft { sections } => { - for s in sections { - if let Some(p) = & $($m)? s.profiles { - f(Profiles(s.sketch, p)); - } - } - } - FeatureKind::Patch { sketch, profiles, edges, .. } => { - if let Some(sketch) = sketch { - f(Profiles(*sketch, profiles)); - } - for e in edges { - f(Pos(e)); - } - } - FeatureKind::Fillet { edges, style, .. } => { - for e in edges { - f(Pos(e)); - } - if let crate::FilletStyle::Variable { start, .. } | crate::FilletStyle::Spline { start, .. } = style { - f(Pos(start)); - } - } - FeatureKind::Chamfer { edges, .. } | FeatureKind::SheetHem { edges, .. } => { - for e in edges { - f(Pos(e)); - } - } - FeatureKind::SurfaceExtend { edges, up_to, .. } => { - for e in edges { - f(Pos(e)); - } - match up_to { - Some(crate::ExtendTo::Plane(plane)) => f(Plane(plane)), - Some(crate::ExtendTo::Face(t)) => f(Pos(& $($m)? t.point)), - Some(crate::ExtendTo::Bounds) | None => {} - } - } - FeatureKind::Shell { faces, .. } - | FeatureKind::OffsetFace { faces, .. } - | FeatureKind::DeleteFace { faces, .. } - | FeatureKind::SurfaceOffset { faces, .. } => { - for p in faces { - f(Pick(p)); - } - } - FeatureKind::SurfaceRuled { edges, direction, face, .. } => { - for e in edges { - f(Pos(e)); - } - if let Some(d) = direction { - f(Dir(d)); - } - if let Some(p) = face { - f(Pick(p)); - } - } - FeatureKind::Unstitch { faces, .. } | FeatureKind::ReverseNormal { faces, .. } => { - for p in faces { - f(Pos(p)); - } - } - // The face is picked (and named); the holes are picked by points on their edges. - FeatureKind::SurfaceUntrim { face, holes, .. } => { - f(Pick(& $($m)? face.point)); - if let Some(points) = holes { - for p in points { - f(Pos(p)); - } - } - } - FeatureKind::BoundaryFill { cells, .. } => { - for p in cells { - f(Pos(p)); - } - } - FeatureKind::Draft { faces, neutral, pull, .. } => { - for p in faces { - f(Pick(p)); - } - f(Plane(neutral)); - f(Dir(pull)); - } - FeatureKind::ReplaceFace { faces, target, .. } => { - for p in faces { - f(Pick(p)); - } - f(Plane(target)); - } - FeatureKind::SplitFace { faces, plane, tool, .. } => { - for p in faces { - f(Pick(p)); - } - f(Plane(plane)); - if let Some(t) = tool { - f(Pos(& $($m)? t.point)); - } - } - FeatureKind::Split { plane, tool, .. } => { - f(Plane(plane)); - if let Some(t) = tool { - f(Pos(& $($m)? t.point)); - } - } - FeatureKind::SurfaceTrim { plane, tool, keep, .. } => { - f(Plane(plane)); - if let Some(t) = tool { - f(Pos(& $($m)? t.point)); - } - f(Pos(keep)); - } - FeatureKind::Box { corner: p, .. } | FeatureKind::Sphere { center: p, .. } | FeatureKind::Torus { center: p, .. } => { - f(Placement(p)) - } - FeatureKind::Cylinder { base, axis, .. } | FeatureKind::Coil { base, axis, .. } => { - f(Placement(base)); - f(Dir(axis)); - } - FeatureKind::Pattern { pattern, .. } => match pattern { - PatternKind::Rectangular { dir1, dir2, .. } => { - f(Dir(dir1)); - if let Some(d) = dir2 { - f(Dir(d)); - } - } - PatternKind::Circular { origin, axis, .. } => { - f(Pos(origin)); - f(Dir(axis)); - } - // Follows its path sketch. - PatternKind::Path { .. } => {} - }, - FeatureKind::Hole { position, direction, points, to, .. } => { - // Placed at sketch points, the hole names no face by its position. - if points.is_none() { - f(Pick(position)); - } else { - f(Pos(position)); - } - f(Dir(direction)); - if let Some(p) = to { - f(Pick(p)); - } - } - FeatureKind::Thread { face, .. } => f(Pick(face)), - FeatureKind::SheetFlange { edges, .. } => { - for e in edges { - f(Pos(& $($m)? e.point)); - } - } - FeatureKind::SheetFold { a, b, fixed, .. } => { - f(Pos(a)); - f(Pos(b)); - if let Some(p) = fixed { - f(Pos(p)); - } - } - FeatureKind::SheetConvert { face, bends, .. } => { - // With bend edges the fixed face is found by name (a plate keeps its point). - if bends.is_empty() { - f(Pos(face)); - } else { - f(Pick(face)); - } - for p in bends { - f(Pos(p)); - } - } - FeatureKind::Boss { position, direction, .. } => { - f(Pick(position)); - f(Dir(direction)); - } - FeatureKind::SnapFit { position, direction, hook, .. } => { - f(Pick(position)); - f(Dir(direction)); - f(Dir(hook)); - } - FeatureKind::Rest { position, direction, along, .. } => { - f(Pick(position)); - f(Dir(direction)); - if let Some(a) = along { - f(Dir(a)); - } - } - FeatureKind::Lip { face, .. } => f(Pos(face)), - FeatureKind::Align { from, to, from_normal, to_normal, .. } => { - f(Pos(from)); - f(Pos(to)); - if let Some(n) = from_normal { - f(Dir(n)); - } - if let Some(n) = to_normal { - f(Dir(n)); - } - } - FeatureKind::Scale { origin, .. } => f(Pos(origin)), - FeatureKind::Move { rotate_axis, .. } => { - if let Some(a) = rotate_axis { - f(Dir(a)); - } - } - FeatureKind::SheetContour { .. } - | FeatureKind::SheetPerforate { .. } - | FeatureKind::SheetUnfold { .. } - | FeatureKind::Combine { .. } - | FeatureKind::BoundingSolid { .. } - | FeatureKind::Pipe { .. } - | FeatureKind::Rib { .. } - | FeatureKind::Remove { .. } - | FeatureKind::Stitch { .. } - | FeatureKind::Thicken { .. } - | FeatureKind::Import { .. } - | FeatureKind::MeshImport { .. } - | FeatureKind::MeshConvert { .. } - | FeatureKind::BodyTransfer { .. } => {} - } - } - }; -} - impl FeatureKind { - geometry_walk!(walk_geometry, GeoItem, refs); - geometry_walk!(walk_geometry_mut, GeoItemMut, refs_mut, mut); - /// The points that pick faces (shell, draft, offset and replace faces, a hole's placement and /// `to`, an extrude's `to` and `from`…), in the order of the feature's persistent face names. pub fn face_points_mut(&mut self) -> Vec<&mut Vec3> { diff --git a/crates/doc/src/feature_refs.rs b/crates/doc/src/feature_refs.rs index 9a8623a0..0c1b4a59 100644 --- a/crates/doc/src/feature_refs.rs +++ b/crates/doc/src/feature_refs.rs @@ -1,12 +1,12 @@ //! References from a feature to the rest of the design: the sketches it is built from (by id), //! the features it names (patterned and mirrored features, the construction plane a plane is -//! built on) and the bodies it works on (by name). One walk, written once for `&` and `&mut`, -//! behind `input_sketches`, `remap_refs` and `remap_bodies`. Every variant is listed, so a new -//! one has to say what it refers to. +//! built on) and the bodies it works on (by name). The walk (`crate::walk`) is behind +//! `input_sketches`, `remap_refs` and `remap_bodies`. use std::collections::BTreeMap; -use crate::{FeatureKind, PatternKind, PlaneRef}; +use crate::FeatureKind; +use crate::walk::Walk; /// 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). @@ -16,228 +16,7 @@ pub(crate) enum Ref { Body(N), } -/// The construction plane a plane is built on (through offsets and angles), for `&` and `&mut` -/// (`$m` is `mut` or nothing). Nesting is capped like every walk over a plane. -macro_rules! plane_walk { - ($name:ident $(, $m:tt)?) => { - fn $name<'a>(mut p: &'a $($m)? PlaneRef, f: &mut impl FnMut(Ref<&'a $($m)? u64, &'a $($m)? String>)) { - for _ in 0..64 { - match p { - PlaneRef::Offset { base, .. } | PlaneRef::AtAngle { base, .. } => p = base, - PlaneRef::Construction { name } => return f(Ref::Feature(name)), - _ => return, - } - } - } - }; -} - -plane_walk!(plane_refs); -plane_walk!(plane_refs_mut, mut); - -/// Every reference of a feature, written once for `&` and `&mut`: `$plane` is the matching plane -/// walk, `$m` is `mut` or nothing. -macro_rules! feature_walk { - ($name:ident, $plane:ident $(, $m:tt)?) => { - pub(crate) fn $name<'a>(&'a $($m)? self, f: &mut impl FnMut(Ref<&'a $($m)? u64, &'a $($m)? String>)) { - use Ref::{Body as B, Feature as F, Sketch as S}; - match self { - FeatureKind::Sketch { plane, .. } | FeatureKind::ConstructionPlane { plane } => $plane(plane, f), - FeatureKind::Extrude { sketch, targets, .. } | FeatureKind::Revolve { sketch, targets, .. } => { - f(S(sketch)); - for b in targets { - f(B(b)); - } - } - FeatureKind::Emboss { sketch, targets, face, .. } => { - f(S(sketch)); - if let Some(face) = face { - f(B(& $($m)? face.body)); - } - for b in targets { - f(B(b)); - } - } - FeatureKind::SurfaceExtrude { sketch, .. } - | FeatureKind::SurfaceRevolve { sketch, .. } - | FeatureKind::Rib { sketch, .. } - | FeatureKind::SheetBase { sketch, .. } - | FeatureKind::SheetContour { sketch, .. } => f(S(sketch)), - FeatureKind::SurfaceSweep { sketch, path_sketch, .. } => { - f(S(sketch)); - f(S(path_sketch)); - } - FeatureKind::SurfaceLoft { sections } => { - for s in sections { - f(S(& $($m)? s.sketch)); - } - } - FeatureKind::Sweep { sketch, path_sketch, targets, .. } => { - f(S(sketch)); - f(S(path_sketch)); - for b in targets { - f(B(b)); - } - } - FeatureKind::Pipe { path_sketch, targets, .. } => { - f(S(path_sketch)); - for b in targets { - f(B(b)); - } - } - FeatureKind::Loft { sections, targets, .. } => { - for s in sections { - f(S(& $($m)? s.sketch)); - } - for b in targets { - f(B(b)); - } - } - FeatureKind::Coil { targets, .. } => { - for b in targets { - f(B(b)); - } - } - FeatureKind::Hole { points, .. } => { - if let Some(p) = points { - f(S(& $($m)? p.sketch)); - } - } - FeatureKind::Pattern { features, pattern, bodies } => { - for n in features { - f(F(n)); - } - if let PatternKind::Path { path_sketch, .. } = pattern { - f(S(path_sketch)); - } - for b in bodies { - f(B(b)); - } - } - FeatureKind::Mirror { features, plane, bodies, .. } => { - for n in features { - f(F(n)); - } - $plane(plane, f); - for b in bodies { - f(B(b)); - } - } - FeatureKind::Split { body, plane, tool } | FeatureKind::SurfaceTrim { body, plane, tool, .. } => { - f(B(body)); - $plane(plane, f); - if let Some(t) = tool { - f(B(& $($m)? t.body)); - } - } - FeatureKind::SplitFace { body, plane, tool, sketch, .. } => { - $plane(plane, f); - if let Some(s) = sketch { - f(S(s)); - } - if let Some(b) = body { - f(B(b)); - } - if let Some(t) = tool { - f(B(& $($m)? t.body)); - } - } - FeatureKind::Draft { neutral: plane, body, .. } | FeatureKind::ReplaceFace { target: plane, body, .. } => { - $plane(plane, f); - if let Some(b) = body { - f(B(b)); - } - } - FeatureKind::SheetFold { sketch, body, .. } | FeatureKind::Patch { sketch, body, .. } => { - if let Some(s) = sketch { - f(S(s)); - } - if let Some(b) = body { - f(B(b)); - } - } - FeatureKind::Fillet { body, .. } - | FeatureKind::Chamfer { body, .. } - | FeatureKind::Shell { body, .. } - | FeatureKind::OffsetFace { body, .. } - | FeatureKind::DeleteFace { body, .. } - | FeatureKind::SheetFlange { body, .. } - | FeatureKind::SheetHem { body, .. } - | FeatureKind::SheetPerforate { body, .. } - | FeatureKind::SheetUnfold { body, .. } - | FeatureKind::Boss { body, .. } - | FeatureKind::Lip { body, .. } - | FeatureKind::SnapFit { body, .. } - | FeatureKind::Rest { body, .. } - | FeatureKind::SurfaceOffset { body, .. } => { - if let Some(b) = body { - f(B(b)); - } - } - FeatureKind::SurfaceUntrim { face, .. } => f(B(& $($m)? face.body)), - FeatureKind::SurfaceExtend { body, up_to, .. } => { - f(B(body)); - match up_to { - Some(crate::ExtendTo::Plane(plane)) => $plane(plane, f), - Some(crate::ExtendTo::Face(t)) => f(B(& $($m)? t.body)), - Some(crate::ExtendTo::Bounds) | None => {} - } - } - FeatureKind::SheetConvert { body, .. } - | FeatureKind::SurfaceRuled { body, .. } - | FeatureKind::Unstitch { body, .. } - | FeatureKind::ReverseNormal { body, .. } - | FeatureKind::MeshConvert { body, .. } => f(B(body)), - FeatureKind::Combine { target, tools, .. } => { - f(B(target)); - for b in tools { - f(B(b)); - } - } - FeatureKind::Thicken { bodies, targets, .. } => { - for b in bodies { - f(B(b)); - } - for b in targets { - f(B(b)); - } - } - FeatureKind::BodyTransfer { bodies, .. } - | FeatureKind::Scale { bodies, .. } - | FeatureKind::BoundingSolid { bodies, .. } - | FeatureKind::Align { bodies, .. } - | FeatureKind::Remove { bodies } - | FeatureKind::Stitch { bodies, .. } - | FeatureKind::Move { bodies, .. } => { - for b in bodies { - f(B(b)); - } - } - FeatureKind::BoundaryFill { tools, .. } => { - for b in tools { - f(B(b)); - } - } - // Construction axes and points refer to geometry, which copies remap on their own - // (`Feature::remap_clone`). - FeatureKind::ConstructionAxis { .. } - | FeatureKind::ConstructionPoint { .. } - | FeatureKind::Box { .. } - | FeatureKind::Cylinder { .. } - | FeatureKind::Sphere { .. } - | FeatureKind::Torus { .. } - | FeatureKind::Thread { .. } - | FeatureKind::Import { .. } - | FeatureKind::MeshImport { .. } => {} - } - } - }; -} - impl FeatureKind { - feature_walk!(walk_refs, plane_refs); - feature_walk!(walk_refs_mut, plane_refs_mut, mut); - /// Sketches the feature is built from (profiles, paths, points): deleting one of them /// deletes the feature. pub fn input_sketches(&self) -> Vec { diff --git a/crates/doc/src/lib.rs b/crates/doc/src/lib.rs index 11920c49..e6fc0f69 100644 --- a/crates/doc/src/lib.rs +++ b/crates/doc/src/lib.rs @@ -39,6 +39,7 @@ pub mod physical_material; mod piece_names; mod profile_refs; pub mod progress; +mod walk; pub use profile_refs::{capture_profile_selection, profile_selection_indices}; pub mod plastic; pub mod project; diff --git a/crates/doc/src/params.rs b/crates/doc/src/params.rs index 5d28ba87..ae18f4a0 100644 --- a/crates/doc/src/params.rs +++ b/crates/doc/src/params.rs @@ -9,7 +9,8 @@ use std::collections::{BTreeMap, BTreeSet}; use serde::{Deserialize, Serialize}; use crate::expr::{self, Kind, ParamDef, Value}; -use crate::{DocError, Document, FeatureKind, HoleKind, MAX_PARAMS, PatternKind, PlaneRef, Result}; +use crate::walk::Walk; +use crate::{DocError, Document, FeatureKind, MAX_PARAMS, Result}; /// Whether an expression of a feature is one of its inputs, which become model parameters named /// in walk order (`dN`), or a coordinate of a primitive's placement point, which gets no name and @@ -20,387 +21,11 @@ pub(crate) enum ExprRole { Placement, } -/// The offsets and angles of a plane, outermost first, for `&` and `&mut` (`$m` is `mut` or -/// nothing). Nesting is capped like every walk over a plane. -macro_rules! plane_walk { - ($name:ident $(, $m:tt)?) => { - fn $name<'a>(mut p: &'a $($m)? PlaneRef, f: &mut impl FnMut(&'static str, &'a $($m)? String, Kind, ExprRole)) { - for _ in 0..64 { - match p { - PlaneRef::Offset { base, distance } => { - f("Offset", distance, Kind::Length, ExprRole::Input); - p = base; - } - PlaneRef::AtAngle { base, angle, .. } => { - f("Angle", angle, Kind::Angle, ExprRole::Input); - p = base; - } - PlaneRef::AlongPath { t, .. } => { - f("Distance (0 to 1)", t, Kind::Unitless, ExprRole::Input); - break; - } - _ => break, - } - } - } - }; -} - -plane_walk!(plane_exprs); -plane_walk!(plane_exprs_mut, mut); - -/// Every expression of a feature with its label, kind and role, in a fixed order (the inputs' -/// order names their parameters), written once for `&` and `&mut`: `$plane` is the matching plane -/// walk, `$refs` the matching `refs`/`refs_mut`, `$m` is `mut` or nothing. -macro_rules! feature_walk { - ($name:ident, $plane:ident, $refs:ident $(, $m:tt)?) => { - pub(crate) fn $name<'a>(&'a $($m)? self, f: &mut impl FnMut(&'static str, &'a $($m)? String, Kind, ExprRole)) { - use ExprRole::{Input as I, Placement as P}; - use Kind::{Angle as A, Length as L, Unitless as U}; - const XYZ: [&str; 3] = ["X", "Y", "Z"]; - match self { - FeatureKind::Sketch { plane, .. } - | FeatureKind::ConstructionPlane { plane } - | FeatureKind::Split { plane, .. } - | FeatureKind::SplitFace { plane, .. } - | FeatureKind::Mirror { plane, .. } => $plane(plane, f), - FeatureKind::ConstructionAxis { def } => { - for r in def.$refs() { - if let crate::construct::GeoRef::Plane { plane } = r { - $plane(plane, f); - } - } - } - FeatureKind::ConstructionPoint { def } => match def { - crate::construct::PointDef::AlongPath { t, .. } => f("Distance (0 to 1)", t, U, I), - other => { - for r in other.$refs() { - if let crate::construct::GeoRef::Plane { plane } = r { - $plane(plane, f); - } - } - } - }, - FeatureKind::Extrude { extent, thin, .. } => { - f("Distance", & $($m)? extent.distance, L, I); - if let Some(d) = & $($m)? extent.distance2 { - f("Distance 2", d, L, I); - } - if let Some(d) = & $($m)? extent.start_offset { - f("Start offset", d, L, I); - } - if let Some(d) = & $($m)? extent.taper { - f("Taper angle", d, A, I); - } - if let Some(d) = & $($m)? extent.to_offset { - f("To offset", d, L, I); - } - if let Some(thin) = thin { - f("Thickness", & $($m)? thin.thickness, L, I); - if let Some(t) = & $($m)? thin.thickness2 { - f("Thickness 2", t, L, I); - } - } - } - FeatureKind::SurfaceExtrude { extent, .. } => { - f("Distance", & $($m)? extent.distance, L, I); - if let Some(d) = & $($m)? extent.distance2 { - f("Distance 2", d, L, I); - } - if let Some(d) = & $($m)? extent.start_offset { - f("Start offset", d, L, I); - } - if let Some(d) = & $($m)? extent.taper { - f("Taper angle", d, A, I); - } - if let Some(d) = & $($m)? extent.to_offset { - f("To offset", d, L, I); - } - } - FeatureKind::Revolve { angle, angle2, .. } | FeatureKind::SurfaceRevolve { angle, angle2, .. } => { - f("Angle", angle, A, I); - if let Some(a) = angle2 { - f("Angle 2", a, A, I); - } - } - FeatureKind::Fillet { radius, style, .. } => match style { - crate::FilletStyle::Constant => f("Radius", radius, L, I), - crate::FilletStyle::Curvature => f("Equivalent radius", radius, L, I), - crate::FilletStyle::Chord => f("Chord length", radius, L, I), - crate::FilletStyle::Variable { radius2, mid, .. } => { - f("Start radius", radius, L, I); - f("End radius", radius2, L, I); - for (_, r) in mid { - f("Mid radius", r, L, I); - } - } - crate::FilletStyle::Spline { radii, .. } => { - f("Start radius", radius, L, I); - let n = radii.len(); - for (i, r) in radii.into_iter().enumerate() { - f(if i + 1 == n { "End radius" } else { "Control radius" }, r, L, I); - } - } - }, - FeatureKind::Chamfer { distance, distance2, angle, .. } => { - f("Distance", distance, L, I); - if let Some(d) = distance2 { - f("Distance 2", d, L, I); - } - if let Some(a) = angle { - f("Angle", a, A, I); - } - } - FeatureKind::Box { length, width, height, corner, .. } => { - f("Length", length, L, I); - f("Width", width, L, I); - f("Height", height, L, I); - for (l, c) in XYZ.into_iter().zip(corner) { - f(l, c, L, P); - } - } - FeatureKind::Cylinder { radius, height, base, .. } => { - f("Radius", radius, L, I); - f("Height", height, L, I); - for (l, c) in XYZ.into_iter().zip(base) { - f(l, c, L, P); - } - } - FeatureKind::Sphere { radius, center, .. } => { - f("Radius", radius, L, I); - for (l, c) in XYZ.into_iter().zip(center) { - f(l, c, L, P); - } - } - FeatureKind::Torus { major, minor, center, .. } => { - f("Major radius", major, L, I); - f("Minor radius", minor, L, I); - for (l, c) in XYZ.into_iter().zip(center) { - f(l, c, L, P); - } - } - FeatureKind::Pattern { pattern, .. } => match pattern { - PatternKind::Rectangular { count1, spacing1, count2, spacing2, .. } => { - f("Quantity", count1, U, I); - f("Spacing", spacing1, L, I); - if let Some(c) = count2 { - f("Quantity 2", c, U, I); - } - if let Some(sp) = spacing2 { - f("Spacing 2", sp, L, I); - } - } - PatternKind::Path { count, spacing, .. } => { - f("Quantity", count, U, I); - f("Distance", spacing, L, I); - } - PatternKind::Circular { count, angle, .. } => { - f("Quantity", count, U, I); - f("Total angle", angle, A, I); - } - }, - FeatureKind::Shell { thickness, .. } => f("Thickness", thickness, L, I), - FeatureKind::Draft { angle, neutral, .. } => { - f("Angle", angle, A, I); - $plane(neutral, f); - } - FeatureKind::Hole { diameter, depth, hole, .. } => { - f("Diameter", diameter, L, I); - if let Some(d) = depth { - f("Depth", d, L, I); - } - match hole { - HoleKind::Simple => {} - HoleKind::Drilled { tip_angle } => f("Tip angle", tip_angle, A, I), - HoleKind::Counterbore { cb_diameter, cb_depth } => { - f("Counterbore diameter", cb_diameter, L, I); - f("Counterbore depth", cb_depth, L, I); - } - HoleKind::Countersink { cs_diameter, cs_angle } => { - f("Countersink diameter", cs_diameter, L, I); - f("Countersink angle", cs_angle, A, I); - } - } - } - FeatureKind::Thread { length, .. } => { - if let Some(l) = length { - f("Length", l, L, I); - } - } - FeatureKind::Scale { factor, factors, .. } => { - f("Scale factor", factor, U, I); - if let Some([x, y, z]) = factors { - f("X scale", x, U, I); - f("Y scale", y, U, I); - f("Z scale", z, U, I); - } - } - FeatureKind::OffsetFace { distance, .. } => f("Distance", distance, L, I), - FeatureKind::BoundingSolid { margin, .. } => f("Margin", margin, L, I), - FeatureKind::Pipe { diameter, wall, .. } => { - f("Diameter", diameter, L, I); - if let Some(w) = wall { - f("Wall", w, L, I); - } - } - FeatureKind::Move { translate, angle, .. } => { - let [x, y, z] = translate; - f("X distance", x, L, I); - f("Y distance", y, L, I); - f("Z distance", z, L, I); - if let Some(a) = angle { - f("Angle", a, A, I); - } - } - FeatureKind::Emboss { depth, .. } => f("Depth", depth, L, I), - FeatureKind::Boss { diameter, height, hole_diameter, hole_depth, draft, fillet, ribs, rib_thickness, rib_length, rib_offset, .. } => { - f("Diameter", diameter, L, I); - f("Height", height, L, I); - for (l, o, k) in [ - ("Hole diameter", hole_diameter, L), - ("Hole depth", hole_depth, L), - ("Draft angle", draft, A), - ("Root fillet", fillet, L), - ("Ribs", ribs, U), - ("Rib thickness", rib_thickness, L), - ("Rib length", rib_length, L), - ("Rib offset", rib_offset, L), - ] { - if let Some(e) = o { - f(l, e, k, I); - } - } - } - FeatureKind::Lip { width, height, gap, .. } => { - f("Width", width, L, I); - f("Height", height, L, I); - if let Some(g) = gap { - f("Gap", g, L, I); - } - } - FeatureKind::Rest { width, length, height, draft, thickness, .. } => { - f("Width", width, L, I); - f("Height", height, L, I); - for (l, o, k) in [("Length", length, L), ("Draft angle", draft, A), ("Wall thickness", thickness, L)] { - if let Some(e) = o { - f(l, e, k, I); - } - } - } - FeatureKind::SnapFit { length, thickness, width, catch_depth, catch_length, .. } => { - f("Length", length, L, I); - f("Thickness", thickness, L, I); - f("Width", width, L, I); - f("Catch depth", catch_depth, L, I); - f("Catch length", catch_length, L, I); - } - FeatureKind::SheetContour { distance, .. } => f("Distance", distance, L, I), - FeatureKind::SheetFlange { edges, height, angle, radius, .. } => { - f("Height", height, L, I); - f("Angle", angle, A, I); - if let Some(r) = radius { - f("Bend radius", r, L, I); - } - // The edges' own values come last, so the inputs above keep their names. - for e in edges { - if let Some(a) = & $($m)? e.angle { - f("Edge angle", a, A, I); - } - if let Some(h) = & $($m)? e.height { - f("Edge height", h, L, I); - } - } - } - FeatureKind::SheetFold { angle, radius, .. } => { - f("Angle", angle, A, I); - if let Some(r) = radius { - f("Bend radius", r, L, I); - } - } - FeatureKind::SheetHem { length, gap, .. } => { - f("Length", length, L, I); - if let Some(g) = gap { - f("Gap", g, L, I); - } - } - FeatureKind::SheetPerforate { diameter, pitch, margin, .. } => { - f("Diameter", diameter, L, I); - f("Pitch", pitch, L, I); - if let Some(m) = margin { - f("Margin", m, L, I); - } - } - FeatureKind::Coil { diameter, pitch, turns, section_size, start_angle, base, .. } => { - f("Diameter", diameter, L, I); - f("Pitch", pitch, L, I); - f("Revolutions", turns, U, I); - f("Section size", section_size, L, I); - if let Some(a) = start_angle { - f("Start angle", a, A, I); - } - for (l, c) in XYZ.into_iter().zip(base) { - f(l, c, L, P); - } - } - FeatureKind::Rib { thickness, depth, .. } => { - f("Thickness", thickness, L, I); - if let Some(d) = depth { - f("Depth", d, L, I); - } - } - FeatureKind::ReplaceFace { target, .. } => $plane(target, f), - FeatureKind::Stitch { tolerance, .. } => f("Tolerance", tolerance, L, I), - FeatureKind::MeshConvert { tolerance, .. } => f("Tolerance", tolerance, L, I), - FeatureKind::Thicken { thickness, .. } => f("Thickness", thickness, L, I), - FeatureKind::SurfaceTrim { plane, .. } => $plane(plane, f), - FeatureKind::SurfaceExtend { distance, up_to, .. } => { - f("Distance", distance, L, I); - if let Some(crate::ExtendTo::Plane(plane)) = up_to { - $plane(plane, f); - } - } - FeatureKind::SurfaceRuled { distance, angle, .. } => { - f("Distance", distance, L, I); - if let Some(a) = angle { - f("Angle", a, A, I); - } - } - FeatureKind::Unstitch { .. } | FeatureKind::ReverseNormal { .. } | FeatureKind::SurfaceUntrim { .. } => {} - FeatureKind::SurfaceOffset { distance, tolerance, .. } => { - f("Distance", distance, L, I); - if let Some(t) = tolerance { - f("Tolerance", t, L, I); - } - } - FeatureKind::Patch { .. } => {} - FeatureKind::Combine { .. } - | FeatureKind::SheetBase { .. } - | FeatureKind::SheetUnfold { .. } - | FeatureKind::SheetConvert { .. } - | FeatureKind::Align { .. } - | FeatureKind::Remove { .. } - | FeatureKind::DeleteFace { .. } - | FeatureKind::BoundaryFill { .. } - | FeatureKind::Loft { .. } - | FeatureKind::Sweep { .. } - | FeatureKind::SurfaceSweep { .. } - | FeatureKind::SurfaceLoft { .. } - | FeatureKind::Import { .. } - | FeatureKind::MeshImport { .. } - | FeatureKind::BodyTransfer { .. } => {} - } - } - }; -} - impl FeatureKind { - feature_walk!(walk_exprs, plane_exprs, refs); - feature_walk!(walk_exprs_mut, plane_exprs_mut, refs_mut, mut); - /// The feature's numeric inputs: (label, expression, kind), in a fixed order. pub fn inputs_mut(&mut self) -> Vec<(&'static str, &mut String, Kind)> { let mut v = Vec::new(); - self.walk_exprs_mut(&mut |label, e, kind, role| { + self.walk_exprs_mut(&mut |(label, e, kind, role)| { if role == ExprRole::Input { v.push((label, e, kind)); } @@ -411,7 +36,7 @@ impl FeatureKind { /// Read-only view of [`FeatureKind::inputs_mut`]: (label, expression, kind). pub fn inputs(&self) -> Vec<(&'static str, String, Kind)> { let mut v = Vec::new(); - self.walk_exprs(&mut |label, e, kind, role| { + self.walk_exprs(&mut |(label, e, kind, role)| { if role == ExprRole::Input { v.push((label, e.clone(), kind)); } @@ -421,9 +46,13 @@ impl FeatureKind { /// Every expression the feature uses: its inputs, then a primitive's placement coordinates. pub fn expressions(&self) -> Vec<&str> { - let mut v = Vec::new(); - self.walk_exprs(&mut |_, e, _, _| v.push(e.as_str())); - v + let (mut inputs, mut placement) = (Vec::new(), Vec::new()); + self.walk_exprs(&mut |(_, e, _, role)| match role { + ExprRole::Input => inputs.push(e.as_str()), + ExprRole::Placement => placement.push(e.as_str()), + }); + inputs.extend(placement); + inputs } } diff --git a/crates/doc/src/walk.rs b/crates/doc/src/walk.rs new file mode 100644 index 00000000..c9e23363 --- /dev/null +++ b/crates/doc/src/walk.rs @@ -0,0 +1,358 @@ +//! The walks over a feature: its expressions (`params`), its references to the rest of the design +//! (`feature_refs`) and its geometry (`feature_geometry`). `#[derive(Walk)]` +//! (`solidcraft-derive`) generates them from `#[walk(...)]` roles on the fields of `FeatureKind` +//! and the types inside it, so what a field is, is written once, where it is declared. The +//! traits here carry the walks through the types the fields have: options, lists, arrays, planes +//! and construction definitions. + +pub(crate) use solidcraft_geom::Vec3; + +pub(crate) use crate::ProfileSel; +use crate::construct::{AxisDef, GeoRef, PointDef}; +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}; + +/// 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 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>; + +/// A value's expressions, references and geometry, each walked in order. A type with none of +/// them implements it empty. +pub(crate) trait Walk { + fn walk_exprs<'a>(&'a self, _: &mut dyn FnMut(ExprItem<'a>)) {} + fn walk_exprs_mut<'a>(&'a mut self, _: &mut dyn FnMut(ExprItemMut<'a>)) {} + fn walk_refs<'a>(&'a self, _: &mut dyn FnMut(RefItem<'a>)) {} + fn walk_refs_mut<'a>(&'a mut self, _: &mut dyn FnMut(RefItemMut<'a>)) {} + fn walk_geometry<'a>(&'a self, _: &mut dyn FnMut(GeoItem<'a>)) {} + fn walk_geometry_mut<'a>(&'a mut self, _: &mut dyn FnMut(GeoItemMut<'a>)) {} +} + +impl Walk for String {} +impl Walk for std::sync::Arc {} +impl Walk for bool {} +impl Walk for u64 {} +impl Walk for i8 {} +impl Walk for f64 {} +impl Walk for ProfileSel {} +impl Walk for solidcraft_sketch::Sketch {} +impl Walk for solidcraft_kernel::StepNode {} +impl Walk for crate::document::Operation {} +impl Walk for crate::document::Direction {} +impl Walk for crate::document::FilletFit {} +impl Walk for crate::document::MeshData {} +impl Walk for crate::document::RevolveConvention {} +impl Walk for crate::document::CoilSection {} +impl Walk for crate::document::ThinSide {} +impl Walk for crate::document::ExtendMode {} +impl Walk for crate::document::RuledMode {} +impl Walk for crate::document::RuledCorners {} + +/// Containers walk what they hold, in order. +macro_rules! walk_each { + ($([$($g:tt)*] $t:ty),*) => {$( + impl<$($g)*> Walk for $t { + fn walk_exprs<'a>(&'a self, f: &mut dyn FnMut(ExprItem<'a>)) { + for t in self { + t.walk_exprs(f) + } + } + fn walk_exprs_mut<'a>(&'a mut self, f: &mut dyn FnMut(ExprItemMut<'a>)) { + for t in self { + t.walk_exprs_mut(f) + } + } + fn walk_refs<'a>(&'a self, f: &mut dyn FnMut(RefItem<'a>)) { + for t in self { + t.walk_refs(f) + } + } + fn walk_refs_mut<'a>(&'a mut self, f: &mut dyn FnMut(RefItemMut<'a>)) { + for t in self { + t.walk_refs_mut(f) + } + } + fn walk_geometry<'a>(&'a self, f: &mut dyn FnMut(GeoItem<'a>)) { + for t in self { + t.walk_geometry(f) + } + } + fn walk_geometry_mut<'a>(&'a mut self, f: &mut dyn FnMut(GeoItemMut<'a>)) { + for t in self { + t.walk_geometry_mut(f) + } + } + } + )*}; +} + +walk_each!([T: Walk] Option, [T: Walk] Vec, [T: Walk, const N: usize] [T; N]); + +/// The values of a field with a role (`T`, an optional one or a list), in order. +pub(crate) trait Each { + fn each<'a>(&'a self, f: &mut dyn FnMut(&'a T)); + fn each_mut<'a>(&'a mut self, f: &mut dyn FnMut(&'a mut T)); +} + +impl Each for T { + fn each<'a>(&'a self, f: &mut dyn FnMut(&'a T)) { + f(self) + } + fn each_mut<'a>(&'a mut self, f: &mut dyn FnMut(&'a mut T)) { + f(self) + } +} + +impl Each for Option { + fn each<'a>(&'a self, f: &mut dyn FnMut(&'a T)) { + if let Some(t) = self { + f(t) + } + } + fn each_mut<'a>(&'a mut self, f: &mut dyn FnMut(&'a mut T)) { + if let Some(t) = self { + f(t) + } + } +} + +impl Each for Vec { + fn each<'a>(&'a self, f: &mut dyn FnMut(&'a T)) { + for t in self { + f(t) + } + } + fn each_mut<'a>(&'a mut self, f: &mut dyn FnMut(&'a mut T)) { + for t in self { + f(t) + } + } +} + +impl Each for Option> { + fn each<'a>(&'a self, f: &mut dyn FnMut(&'a T)) { + for t in self.iter().flatten() { + f(t) + } + } + fn each_mut<'a>(&'a mut self, f: &mut dyn FnMut(&'a mut T)) { + for t in self.iter_mut().flatten() { + f(t) + } + } +} + +/// The expressions of an expression field, each with its index and their count (for its label). +pub(crate) trait ExprField { + fn each<'a>(&'a self, f: &mut dyn FnMut(usize, usize, &'a String)); + fn each_mut<'a>(&'a mut self, f: &mut dyn FnMut(usize, usize, &'a mut String)); +} + +impl ExprField for String { + fn each<'a>(&'a self, f: &mut dyn FnMut(usize, usize, &'a String)) { + f(0, 1, self) + } + fn each_mut<'a>(&'a mut self, f: &mut dyn FnMut(usize, usize, &'a mut String)) { + 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)) { + f(0, 1, &self.1) + } + fn each_mut<'a>(&'a mut self, f: &mut dyn FnMut(usize, usize, &'a mut String)) { + f(0, 1, &mut self.1) + } +} + +impl ExprField for Option { + fn each<'a>(&'a self, f: &mut dyn FnMut(usize, usize, &'a String)) { + if let Some(t) = self { + t.each(f) + } + } + fn each_mut<'a>(&'a mut self, f: &mut dyn FnMut(usize, usize, &'a mut String)) { + if let Some(t) = self { + t.each_mut(f) + } + } +} + +/// Lists and arrays number their elements. +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)) { + 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)) { + let n = self.len(); + for (i, t) in self.iter_mut().enumerate() { + t.each_mut(&mut |_, _, e| f(i, n, e)); + } + } + } + )*}; +} + +expr_list!([T: ExprField] Vec, [T: ExprField, const N: usize] [T; N]); + +/// An expression's label: one for every element, one per element, or worked out from the +/// element's index and the count. +pub(crate) trait Label { + fn at(&self, i: usize, n: usize) -> &'static str; +} + +impl Label for &'static str { + fn at(&self, _: usize, _: usize) -> &'static str { + self + } +} + +impl Label for [&'static str; N] { + fn at(&self, i: usize, _: usize) -> &'static str { + self.get(i).copied().unwrap_or("") + } +} + +impl &'static str> Label for F { + fn at(&self, i: usize, n: usize) -> &'static str { + self(i, n) + } +} + +/// The offsets and angles of a plane, outermost first (nesting is capped like every walk over a +/// plane), the construction plane it is built on, and the plane itself. +impl Walk for PlaneRef { + fn walk_exprs<'a>(&'a self, f: &mut dyn FnMut(ExprItem<'a>)) { + let mut p = self; + for _ in 0..64 { + match p { + PlaneRef::Offset { base, distance } => { + f(("Offset", distance, Kind::Length, ExprRole::Input)); + p = base; + } + PlaneRef::AtAngle { base, angle, .. } => { + f(("Angle", angle, Kind::Angle, ExprRole::Input)); + p = base; + } + PlaneRef::AlongPath { t, .. } => return f(("Distance (0 to 1)", t, Kind::Unitless, ExprRole::Input)), + _ => return, + } + } + } + fn walk_exprs_mut<'a>(&'a mut self, f: &mut dyn FnMut(ExprItemMut<'a>)) { + let mut p = self; + for _ in 0..64 { + match p { + PlaneRef::Offset { base, distance } => { + f(("Offset", distance, Kind::Length, ExprRole::Input)); + p = base; + } + PlaneRef::AtAngle { base, angle, .. } => { + f(("Angle", angle, Kind::Angle, ExprRole::Input)); + p = base; + } + PlaneRef::AlongPath { t, .. } => return f(("Distance (0 to 1)", t, Kind::Unitless, ExprRole::Input)), + _ => return, + } + } + } + fn walk_refs<'a>(&'a self, f: &mut dyn FnMut(RefItem<'a>)) { + let mut p = self; + for _ in 0..64 { + match p { + PlaneRef::Offset { base, .. } | PlaneRef::AtAngle { base, .. } => p = base, + PlaneRef::Construction { name } => return f(Ref::Feature(name)), + _ => return, + } + } + } + fn walk_refs_mut<'a>(&'a mut self, f: &mut dyn FnMut(RefItemMut<'a>)) { + let mut p = self; + for _ in 0..64 { + match p { + PlaneRef::Offset { base, .. } | PlaneRef::AtAngle { base, .. } => p = base, + PlaneRef::Construction { name } => return f(Ref::Feature(name)), + _ => return, + } + } + } + fn walk_geometry<'a>(&'a self, f: &mut dyn FnMut(GeoItem<'a>)) { + f(Geo::Plane(self)) + } + fn walk_geometry_mut<'a>(&'a mut self, f: &mut dyn FnMut(GeoItemMut<'a>)) { + f(Geo::Plane(self)) + } +} + +/// Construction axes and points: the expressions of the planes they are built from (and a point +/// along a path's distance), and their references to the model. They name no sketches, features +/// or bodies: copies remap their geometry references on their own (`Feature::remap_clone`). +fn plane_exprs<'a>(refs: Vec<&'a GeoRef>, f: &mut dyn FnMut(ExprItem<'a>)) { + for r in refs { + if let GeoRef::Plane { plane } = r { + plane.walk_exprs(f); + } + } +} + +fn plane_exprs_mut<'a>(refs: Vec<&'a mut GeoRef>, f: &mut dyn FnMut(ExprItemMut<'a>)) { + for r in refs { + if let GeoRef::Plane { plane } = r { + plane.walk_exprs_mut(f); + } + } +} + +macro_rules! construct_geometry { + () => { + fn walk_geometry<'a>(&'a self, f: &mut dyn FnMut(GeoItem<'a>)) { + for r in self.refs() { + f(Geo::Construct(r)); + } + } + fn walk_geometry_mut<'a>(&'a mut self, f: &mut dyn FnMut(GeoItemMut<'a>)) { + for r in self.refs_mut() { + f(Geo::Construct(r)); + } + } + }; +} + +impl Walk for AxisDef { + fn walk_exprs<'a>(&'a self, f: &mut dyn FnMut(ExprItem<'a>)) { + plane_exprs(self.refs(), f) + } + fn walk_exprs_mut<'a>(&'a mut self, f: &mut dyn FnMut(ExprItemMut<'a>)) { + plane_exprs_mut(self.refs_mut(), f) + } + construct_geometry!(); +} + +impl Walk for PointDef { + fn walk_exprs<'a>(&'a self, f: &mut dyn FnMut(ExprItem<'a>)) { + match self { + PointDef::AlongPath { t, .. } => f(("Distance (0 to 1)", t, Kind::Unitless, ExprRole::Input)), + other => plane_exprs(other.refs(), f), + } + } + fn walk_exprs_mut<'a>(&'a mut self, f: &mut dyn FnMut(ExprItemMut<'a>)) { + match self { + PointDef::AlongPath { t, .. } => f(("Distance (0 to 1)", t, Kind::Unitless, ExprRole::Input)), + other => plane_exprs_mut(other.refs_mut(), f), + } + } + construct_geometry!(); +} diff --git a/xtask/src/layers.rs b/xtask/src/layers.rs index 3b482150..b2a5413f 100644 --- a/xtask/src/layers.rs +++ b/xtask/src/layers.rs @@ -36,6 +36,8 @@ impl Class { /// prefix. pub const TABLE: &[(&str, Class)] = &[ ("geom", Class::Layer(0)), + // the derive macros (proc-macro, no workspace dependencies) + ("derive", Class::Standalone), ("sketch", Class::Layer(1)), ("kernel", Class::Layer(1)), ("render", Class::Layer(1)),