diff --git a/crates/doc/src/eval.rs b/crates/doc/src/eval.rs index 6756277d..4bf3fd26 100644 --- a/crates/doc/src/eval.rs +++ b/crates/doc/src/eval.rs @@ -1948,11 +1948,29 @@ fn path_at(segs: &[kernel::PathSeg], d: f64) -> (Vec3, Vec3) { } /// Apply transformed copies of the tools of `features` (patterns, mirrors). -fn replay(doc: &Document, vals: &BTreeMap, f: &Feature, st: &mut ModelState, features: &[String], mats: &[Mat]) -> Result<()> { +fn replay( + doc: &Document, + vals: &BTreeMap, + f: &Feature, + st: &mut ModelState, + features: &[String], + mats: &[Mat], + warning: &mut Option, +) -> Result<()> { if features.is_empty() { return Err(DocError::Invalid("no features selected".into())); } - for name in features { + // A suppressed feature has nothing to copy: its copies go with it. + let suppressed: Vec<&String> = features.iter().filter(|n| doc.find_feature(n).is_some_and(|x| x.suppressed)).collect(); + if !suppressed.is_empty() { + let list = suppressed.iter().map(|n| format!("`{n}`")).collect::>().join(", "); + *warning = Some(if suppressed.len() == features.len() { + format!("nothing to copy: {list} suppressed") + } else { + format!("{list} suppressed and not copied") + }); + } + for name in features.iter().filter(|n| !suppressed.contains(n)) { // A sketch and its extrude often share a name; the feature that makes geometry wins. let src = doc .features @@ -2515,7 +2533,7 @@ fn eval_feature(doc: &Document, vals: &BTreeMap, f: &Feature, st: FeatureKind::Pattern { features, pattern, bodies } => { let mats = pattern_transforms(vals, st, pattern)?; if bodies.is_empty() { - return replay(doc, vals, f, st, features, &mats); + return replay(doc, vals, f, st, features, &mats, warning); } // Bodies: each copy is a new body named after its source. for n in bodies { @@ -2531,7 +2549,7 @@ fn eval_feature(doc: &Document, vals: &BTreeMap, f: &Feature, st: FeatureKind::Mirror { features, plane, bodies, combine } => { let pl = doc.resolve_plane_in(vals, plane, 0, Some(st))?; if bodies.is_empty() { - return replay(doc, vals, f, st, features, &[mirror_matrix(&pl)]); + return replay(doc, vals, f, st, features, &[mirror_matrix(&pl)], warning); } let m = mirror_matrix(&pl); for n in bodies { diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index ebe9aa8b..5c43f68f 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -434,6 +434,8 @@ mod delete_face_tests; #[cfg(test)] mod extent_tests; #[cfg(test)] +mod pattern_suppress_tests; +#[cfg(test)] mod profile_refs_tests; #[cfg(test)] mod split_face_tests; diff --git a/crates/engine/src/pattern_suppress_tests.rs b/crates/engine/src/pattern_suppress_tests.rs new file mode 100644 index 00000000..f52dd25f --- /dev/null +++ b/crates/engine/src/pattern_suppress_tests.rs @@ -0,0 +1,35 @@ +//! A pattern of a suppressed feature has nothing to copy (#42): its copies go too, and the +//! pattern says so. + +use serde_json::{Value, json}; + +use crate::Session; + +fn run(s: &mut Session, id: &str, p: Value) -> Value { + match s.execute(id, &p) { + Ok(v) => v, + Err(e) => panic!("{id} {p}: {e}"), + } +} + +fn volume(s: &Session, body: &str) -> f64 { + s.model.state().body(body).map(|b| solvecraft_kernel::measure(&b.body).unwrap().volume).unwrap_or(0.0) +} + +#[test] +fn suppressing_a_patterned_feature_removes_its_copies() { + let mut s = Session::default(); + let script: Value = serde_json::from_str(include_str!("../../../examples/acceptance/sensor_enclosure.json")).unwrap(); + s.run_script(&script).unwrap(); + let full = volume(&s, "Case"); + run(&mut s, "timeline.suppress", json!({"feature": "Standoff", "suppressed": true})); + let without = volume(&s, "Case"); + // All four standoffs go (each 116.8 mm³), not just the seed. + assert!((full - without - 4.0 * 116.8).abs() < 2.0, "{full} → {without}"); + let pattern = s.doc.find_feature("RectangularPattern1").unwrap().id; + let r = s.model.result(pattern).unwrap(); + assert!(r.error.is_none() && r.warning.as_deref().is_some_and(|w| w.contains("Standoff")), "{r:?}"); + // Back on: the four standoffs return. + run(&mut s, "timeline.suppress", json!({"feature": "Standoff", "suppressed": false})); + assert!((volume(&s, "Case") - full).abs() < 1e-6 * full); +} diff --git a/crates/ui-egui/tests/scenarios/acceptance_pattern_of_suppressed_feature.json b/crates/ui-egui/tests/scenarios/acceptance_pattern_of_suppressed_feature.json index 898c0d0b..32a481f7 100644 --- a/crates/ui-egui/tests/scenarios/acceptance_pattern_of_suppressed_feature.json +++ b/crates/ui-egui/tests/scenarios/acceptance_pattern_of_suppressed_feature.json @@ -1,7 +1,4 @@ [ - { - "pending": "#42: suppressing a patterned feature keeps the pattern's copies" - }, { "note": "Acceptance (#27): suppressing the PCB standoff takes the patterned copies with it." },