From 829912c6c5461e283cdd1df4f312eb90f8a5611d Mon Sep 17 00:00:00 2001 From: Jonas Wrede Date: Mon, 12 Oct 2026 22:09:46 +0200 Subject: [PATCH] perf: cut many clear tools of any cut in one boolean (#368) (#387) One extrude cutting many circles from one sketch still cut the plate once per circle, each boolean rebuilding and re-meshing a plate grown by every hole before it (32 circles 2 s, 128 circles 60 s). The #261 fix joined clear tools with kernel::union_apart only in pattern replay; apply_traced now does it for every cut with 8 or more tools (patterns included), keeping each piece's face lineage. --- crates/doc/src/eval.rs | 26 +++++++++++++++---------- crates/engine/src/cmd/patterns_tests.rs | 1 - 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/crates/doc/src/eval.rs b/crates/doc/src/eval.rs index df660ad1..c038fc51 100644 --- a/crates/doc/src/eval.rs +++ b/crates/doc/src/eval.rs @@ -1132,6 +1132,21 @@ fn apply_op(state: &mut ModelState, f: &Feature, tools: Vec, op: Operation apply_traced(state, f, tools.into_iter().map(Traced::unnamed).collect(), op, targets) } +/// Many cut tools clear of each other (a pattern's copies, the circles of a vent field in one +/// sketch) as one tool of many pieces, so the target is cut once instead of rebuilding an ever +/// more complex body for every tool (#261, #368). Tools that may meet are left as they are. +fn clear_tools_as_one(tools: Vec) -> Vec { + if tools.len() < 8 { + return tools; + } + let bodies: Vec = tools.iter().map(|t| t.body.clone()).collect(); + match kernel::union_apart(&bodies) { + // Each piece's faces keep the names of the tool it is. + Ok(Some(one)) => vec![Traced::step(one, &tools.iter().collect::>())], + _ => tools, + } +} + /// [`apply_op`] with tools whose lineage is known: the bodies it makes or changes keep theirs. fn apply_traced(state: &mut ModelState, f: &Feature, tools: Vec, op: Operation, targets: &[String]) -> Result<()> { let tools = if op == Operation::Intersect { intersection_tool_union(tools)? } else { tools }; @@ -1213,6 +1228,7 @@ fn apply_traced(state: &mut ModelState, f: &Feature, tools: Vec, op: Ope } Operation::Cut | Operation::Intersect => { let bop = if op == Operation::Cut { BoolOp::Cut } else { BoolOp::Intersect }; + let tools = if op == Operation::Cut { clear_tools_as_one(tools) } else { tools }; let affected: std::collections::BTreeSet = target_idx.iter().filter_map(|i| state.bodies.get(*i).map(|b| b.name.clone())).collect(); let mut remove = Vec::new(); @@ -2798,16 +2814,6 @@ fn replay( } } } - // Many cut copies clear of each other: one tool of many pieces, so the target is cut - // once instead of rebuilding an ever more complex body for every copy (#261). - if op == Operation::Cut && copies.len() >= 8 { - let bodies: Vec = copies.iter().map(|t| t.body.clone()).collect(); - if let Ok(Some(one)) = kernel::union_apart(&bodies) { - // Each piece's faces keep the names of the copy it is. - let joined = Traced::step(one, &copies.iter().collect::>()); - copies = vec![joined]; - } - } // New bodies from a pattern join their source when they touch it (as one feature would). if f.body_names.is_empty() { apply_traced(st, f, copies, op, &targets)?; diff --git a/crates/engine/src/cmd/patterns_tests.rs b/crates/engine/src/cmd/patterns_tests.rs index 08313c3f..33a94461 100644 --- a/crates/engine/src/cmd/patterns_tests.rs +++ b/crates/engine/src/cmd/patterns_tests.rs @@ -285,7 +285,6 @@ fn sketch_hole_cut(n: usize) -> std::time::Duration { /// field: 32 took 2 s and 128 took 60 s, though a feature pattern of the same holes is near /// linear since #261. Linear is 4×. A ratio, not a time (see the 512-hole pattern test). #[test] -#[ignore = "fails until #368 is fixed"] fn one_extrude_of_many_sketch_circles_scales_linearly() { let t32 = sketch_hole_cut(32); let t128 = sketch_hole_cut(128);