diff --git a/crates/doc/src/eval.rs b/crates/doc/src/eval.rs index ee8a724a..d5d7ef32 100644 --- a/crates/doc/src/eval.rs +++ b/crates/doc/src/eval.rs @@ -1125,6 +1125,12 @@ fn mirror_matrix(pl: &Plane) -> Mat { const MAX_INSTANCES: usize = 1000; +/// The placements (column-major matrices) of a pattern's copies, the original excluded. +pub fn pattern_matrices(doc: &Document, st: &ModelState, p: &crate::PatternKind) -> Result> { + let (vals, _) = doc.param_values(); + pattern_transforms(&vals, st, p) +} + fn pattern_transforms(vals: &BTreeMap, st: &ModelState, p: &crate::PatternKind) -> Result> { let count = |e: &str| -> Result { let n = val(vals, e, Kind::Unitless)?.round(); diff --git a/crates/doc/src/lib.rs b/crates/doc/src/lib.rs index 4ec4df51..0ad015f0 100644 --- a/crates/doc/src/lib.rs +++ b/crates/doc/src/lib.rs @@ -16,7 +16,7 @@ mod project3d; pub use assembly::{IDENTITY, Mat, Occurrence, apply_point, apply_vector, is_identity, mat_inverse, mat_mul, rigid}; pub use document::*; -pub use eval::{FeatureResult, Model, ModelBody, ModelState, SolvedSketch, ThreadInfo, parse_metric_thread, world_state}; +pub use eval::{FeatureResult, Model, ModelBody, ModelState, SolvedSketch, ThreadInfo, parse_metric_thread, pattern_matrices, world_state}; pub use params::{ParamRow, ParamUser, cycles}; pub use solvecraft_kernel as kernel; pub use solvecraft_sketch as sketch; diff --git a/crates/engine/src/cmd/features.rs b/crates/engine/src/cmd/features.rs index 7532a5e9..12655336 100644 --- a/crates/engine/src/cmd/features.rs +++ b/crates/engine/src/cmd/features.rs @@ -37,11 +37,11 @@ pub static COMMANDS: &[CommandSpec] = &[ CommandSpec::new("PatternRectangular", "Rectangular Pattern", pattern_rect) .at("SOLID", "CREATE") .icon("pattern_rect") - .params("features: [names] | bodies: [names]; dir1: [x,y,z]; count1; spacing1; dir2?, count2?, spacing2?"), + .params("features: [names] | bodies: [names] | components: [names]; dir1: [x,y,z]; count1; spacing1; dir2?, count2?, spacing2?"), CommandSpec::new("PatternCircular", "Circular Pattern", pattern_circ) .at("SOLID", "CREATE") .icon("pattern_circ") - .params("features: [names] | bodies: [names]; axis: X|Y|Z | {origin, dir}; count; angle? (default 360 deg)"), + .params("features: [names] | bodies: [names] | components: [names]; axis: X|Y|Z | {origin, dir}; count; angle? (default 360 deg)"), CommandSpec::new("PatternOnPath", "Pattern on Path", pattern_path) .at("SOLID", "CREATE") .icon("pattern_rect") @@ -522,8 +522,31 @@ fn axis_line(p: &Value, cmd: &str) -> Result<(Vec3, Vec3)> { } } -/// What a pattern copies: features (replayed), or bodies (`bodies: [names]`). +/// A pattern feature, or for `components`, new occurrences of them at each placement. +fn finish_pattern(s: &mut Session, p: &Value, features: Vec, pattern: solvecraft_doc::PatternKind, bodies: Vec) -> Result { + let comps = string_list(p, "components"); + if comps.is_empty() { + return add_feature(s, p, FeatureKind::Pattern { features, pattern, bodies }); + } + let cmd = "pattern (components)"; + let mats = solvecraft_doc::pattern_matrices(&s.doc, &s.model.state(), &pattern)?; + let mut made = Vec::new(); + for c in &comps { + let id = s.doc.find_component(c).filter(|id| *id != 0).ok_or_else(|| bad(cmd, format!("no component `{c}`")))?; + let src = s.doc.occurrence_of(id).cloned().ok_or_else(|| bad(cmd, format!("component `{c}` has no occurrence")))?; + for m in &mats { + made.push(s.doc_mut().add_occurrence(id, src.parent, solvecraft_doc::mat_mul(m, &src.transform))?); + } + } + Ok(json!({"occurrences": made})) +} + +/// What a pattern copies: features (replayed), bodies (`bodies: [names]`), or components +/// (`components: [names]`, placed again as occurrences). fn pattern_sources(s: &Session, p: &Value, cmd: &str) -> Result<(Vec, Vec)> { + if !string_list(p, "components").is_empty() { + return Ok((Vec::new(), Vec::new())); + } let bodies = string_list(p, "bodies"); if bodies.is_empty() { return Ok((source_features(s, p, cmd)?, Vec::new())); @@ -552,7 +575,7 @@ fn pattern_rect(s: &mut Session, p: &Value) -> Result { check_expr(s, c, Kind::Length, cmd, "spacing2")?; } let pattern = solvecraft_doc::PatternKind::Rectangular { dir1, count1, spacing1, dir2, count2, spacing2 }; - add_feature(s, p, FeatureKind::Pattern { features, pattern, bodies }) + finish_pattern(s, p, features, pattern, bodies) } fn pattern_circ(s: &mut Session, p: &Value) -> Result { @@ -564,7 +587,7 @@ fn pattern_circ(s: &mut Session, p: &Value) -> Result { let angle = expr(p, "angle").unwrap_or_else(|| "360 deg".into()); check_expr(s, &angle, Kind::Angle, cmd, "angle")?; let pattern = solvecraft_doc::PatternKind::Circular { origin, axis, count, angle }; - add_feature(s, p, FeatureKind::Pattern { features, pattern, bodies }) + finish_pattern(s, p, features, pattern, bodies) } fn pattern_path(s: &mut Session, p: &Value) -> Result { @@ -590,7 +613,7 @@ fn pattern_path(s: &mut Session, p: &Value) -> Result { }; let flip = bool_(p, "flip").unwrap_or(false); let pattern = solvecraft_doc::PatternKind::Path { path_sketch, path, count, spacing, extent, flip, orient }; - add_feature(s, p, FeatureKind::Pattern { features, pattern, bodies }) + finish_pattern(s, p, features, pattern, bodies) } fn mirror(s: &mut Session, p: &Value) -> Result { diff --git a/crates/engine/src/cmd/patterns_tests.rs b/crates/engine/src/cmd/patterns_tests.rs index 9fa8e25b..986963f2 100644 --- a/crates/engine/src/cmd/patterns_tests.rs +++ b/crates/engine/src/cmd/patterns_tests.rs @@ -73,3 +73,44 @@ fn patterns_of_bodies() { assert!(rel(volume(&mut s), 6000.0) < 1e-4); assert!(s.execute("PatternRectangular", &json!({"bodies": ["Nope"], "dir1": [1, 0, 0], "count1": 2, "spacing1": 5})).is_err()); } + +#[test] +fn patterns_of_components_place_occurrences() { + let mut s = Session::default(); + run(&mut s, "FusionCreateNewComponentCommand", json!({"name": "Peg"})); + run(&mut s, "PrimitiveBox", json!({"length": 10, "width": 10, "height": 10, "corner": [20, 0, 0]})); + run(&mut s, "component.activate", json!({"component": "root"})); + let r = run(&mut s, "PatternRectangular", json!({"components": ["Peg"], "dir1": [0, 0, 1], "count1": 3, "spacing1": 20})); + assert_eq!(r["occurrences"].as_array().map(Vec::len), Some(2), "{r}"); + let m = run(&mut s, "MeasureCommand", json!({})); + assert_eq!(m["body_count"], 3, "{m}"); + let zs: Vec = m["bodies"].as_array().unwrap().iter().filter_map(|b| b["bbox"]["min"][2].as_f64()).collect(); + for z in [0.0, 20.0, 40.0] { + assert!(zs.iter().any(|x| (x - z).abs() < 1e-6), "{zs:?}"); + } + assert!(s.execute("PatternCircular", &json!({"components": ["Nope"], "axis": "Z", "count": 2})).is_err()); +} + +#[test] +fn path_pattern_orientation_follows_the_path() { + let width_of_copy = |orient: &str| { + let mut s = Session::default(); + run(&mut s, "PrimitiveBox", json!({"length": 2, "width": 4, "height": 2, "corner": [19, -2, 0], "body_name": "Tab"})); + run(&mut s, "SketchCreate", json!({"plane": "XY", "name": "Arc"})); + run(&mut s, "ArcCenterTwoPoint", json!({"center": [0, 0], "start": [20, 0], "sweep": 90, "id": "a1"})); + run(&mut s, "SketchStop", json!({})); + run( + &mut s, + "PatternOnPath", + json!({"bodies": ["Tab"], "path_sketch": "Arc", "path": ["a1"], "count": 2, "distance": "pi * 10", "orientation": orient}), + ); + let m = run(&mut s, "MeasureCommand", json!({})); + let b = m["bodies"].as_array().unwrap().iter().find(|b| b["name"] != "Tab").unwrap().clone(); + let (lo, hi) = (b["bbox"]["min"].clone(), b["bbox"]["max"].clone()); + // The copy sits at the arc's end (0, 20). + assert!(((lo[1].as_f64().unwrap() + hi[1].as_f64().unwrap()) / 2.0 - 20.0).abs() < 1e-6, "{b}"); + hi[0].as_f64().unwrap() - lo[0].as_f64().unwrap() + }; + assert!((width_of_copy("identical") - 2.0).abs() < 1e-6); + assert!((width_of_copy("path") - 4.0).abs() < 1e-6); +}