Improve bool intersection perfomance (#10671)

This commit is contained in:
Elena Torró 2026-07-14 07:43:52 +02:00 committed by GitHub
parent 3add7211a5
commit ab58d00d66
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 90 additions and 23 deletions

View File

@ -838,8 +838,13 @@
(dwsh/update-shapes ids update-shape options)
;; The update to the bool path needs to be in a different operation because it
;; needs to have the updated children info
(dwsh/update-shapes bool-ids path/update-bool-shape (assoc options :with-objects? true)))
;; needs to have the updated children info.
;; `update-layout? false`: recalculating a bool path can never change
;; `:hidden`, and the layout check would recompute the whole boolean
;; path in WASM once per bool shape just to find that out.
(dwsh/update-shapes bool-ids path/update-bool-shape (assoc options
:with-objects? true
:update-layout? false)))
(if undo-transation?
(rx/of (dwu/commit-undo-transaction undo-id))

View File

@ -154,12 +154,14 @@
([ids update-fn
{:as props
:keys [reg-objects? save-undo? stack-undo? attrs ignore-tree page-id
ignore-touched undo-group with-objects? changed-sub-attr translation?]
ignore-touched undo-group with-objects? changed-sub-attr translation?
update-layout?]
:or {reg-objects? false
save-undo? true
stack-undo? false
ignore-touched false
with-objects? false}}]
with-objects? false
update-layout? true}}]
(assert (every? uuid? ids) "expect a coll of uuid for `ids`")
(assert (fn? update-fn) "the `update-fn` should be a valid function")
@ -181,8 +183,17 @@
(filter #(some update-layout-attr? (pcb/changed-attrs % objects update-fn {:attrs attrs :with-objects? with-objects?})))
(map :id))
;; `changed-attrs` runs `update-fn` in full for every shape, which
;; can be expensive (e.g. `update-bool-shape` recalculates the whole
;; boolean path in WASM). Skip the pass entirely when we can prove it
;; cannot match: when the caller declares `attrs`, `changed-attrs`
;; filters its result to that set, so if no layout attr is present
;; the check is always empty.
update-layout-ids
(when-not translation?
(when-not (or translation?
(not update-layout?)
(and (some? attrs)
(not (some update-layout-attr? attrs))))
(->> (into [] xf-update-layout ids)
(not-empty)))

View File

@ -2498,19 +2498,22 @@
;; After the content is returned we discard that temporary context
(h/call wasm/internal-module "_start_temp_objects")
(let [bool-type (get shape :bool-type)
ids (get shape :shapes)
all-children
(->> ids
(mapcat #(cfh/get-children-with-self objects %)))]
(try
(let [bool-type (get shape :bool-type)
ids (get shape :shapes)
all-children
(->> ids
(mapcat #(cfh/get-children-with-self objects %)))]
(h/call wasm/internal-module "_init_shapes_pool" (count all-children))
(run! set-object all-children)
(h/call wasm/internal-module "_init_shapes_pool" (count all-children))
(run! set-object all-children)
(let [content (-> (calculate-bool* bool-type ids)
(path.impl/path-data))]
(h/call wasm/internal-module "_end_temp_objects")
content)))
(-> (calculate-bool* bool-type ids)
(path.impl/path-data)))
(finally
;; Always restore the main shapes pool: leaving the temp pool
;; active would make the next `_start_temp_objects` panic.
(h/call wasm/internal-module "_end_temp_objects"))))
(def POSITION-DATA-U8-SIZE 36)
(def POSITION-DATA-U32-SIZE (/ POSITION-DATA-U8-SIZE 4))

View File

@ -82,14 +82,31 @@ pub fn split_intersections(segment: Bezier, intersections: &[f64]) -> Vec<Bezier
}
let mut result = Vec::new();
let mut intersections = intersections.to_owned();
// Clamp to the valid parametric range: `intersections()`/`project()` can
// return values a hair outside [0,1] due to float error, which would make
// `split` panic on its `(0.0..=1.).contains(&t)` assertion below.
let mut intersections: Vec<f64> = intersections.iter().map(|t| t.clamp(0.0, 1.0)).collect();
intersections.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal));
let mut prev = 0.0;
let mut cur_segment = segment;
for t_i in &intersections {
let rti = (t_i - prev) / (1.0 - prev);
// Skip duplicated split points (the same crossing can be reported by
// two adjacent opposing segments that share an endpoint): re-splitting
// at (almost) the same t would emit a zero-length sliver segment whose
// midpoint containment test is unstable in union/difference/intersection.
if *t_i - prev < 1e-6 {
continue;
}
let denom = 1.0 - prev;
// Degenerate split (prev already at the segment end); nothing left to cut.
if denom <= f64::EPSILON {
continue;
}
// Re-normalize the global t into the remaining segment, clamped so float
// noise / out-of-order duplicates can never push it outside [0,1].
let rti = ((t_i - prev) / denom).clamp(0.0, 1.0);
let [s, rest] = cur_segment.split(TValue::Parametric(rti));
prev = *t_i;
cur_segment = rest;
@ -110,21 +127,52 @@ pub fn split_segments(path_a: &Path, path_b: &Path) -> (Vec<Bezier>, Vec<Bezier>
let mut intersects_b = Vec::<Vec<f64>>::with_capacity(path_b.len());
intersects_b.resize_with(path_b.len(), Default::default);
// Broad-phase: precompute a conservative (control-hull) AABB per segment,
// padded by the intersection tolerance. Two segments can only intersect if
// their boxes overlap, so we skip the expensive `intersections()` call for
// the (typically vast) majority of non-overlapping pairs. This turns the
// O(A*B) inner loop from A*B curve-subdivision solves into A*B cheap box
// tests plus only the handful of solves that can actually produce a hit.
let bbox = |b: &Bezier| {
let [min, max] = b.bounding_box_of_anchors_and_handles();
[
DVec2::new(min.x - INTERSECT_ERROR, min.y - INTERSECT_ERROR),
DVec2::new(max.x + INTERSECT_ERROR, max.y + INTERSECT_ERROR),
]
};
let boxes_a: Vec<[DVec2; 2]> = path_a.iter().map(bbox).collect();
let boxes_b: Vec<[DVec2; 2]> = path_b.iter().map(bbox).collect();
for i in 0..path_a.len() {
let [amin, amax] = boxes_a[i];
for j in 0..path_b.len() {
let [bmin, bmax] = boxes_b[j];
// AABB overlap test; skip pairs that cannot intersect.
if amin.x > bmax.x || bmin.x > amax.x || amin.y > bmax.y || bmin.y > amax.y {
continue;
}
let segment_a = path_a[i];
let segment_b = path_b[j];
let intersections_a = segment_a.intersections(
let mut intersections_a = segment_a.intersections(
&segment_b,
Some(INTERSECT_ERROR),
Some(INTERSECT_MIN_SEPARATION),
);
// Clamp at the source: float error can report a t just outside
// [0,1], and every `TValue::Parametric` consumer downstream
// (`evaluate`, `project`, `split`) asserts `(0.0..=1.).contains(&t)`.
for t in intersections_a.iter_mut() {
*t = t.clamp(0.0, 1.0);
}
intersects_b[j].extend(intersections_a.iter().map(|t_a| {
segment_b.project(
segment_a.evaluate(TValue::Parametric(*t_a)),
Some(PROJECT_OPTS),
)
segment_b
.project(
segment_a.evaluate(TValue::Parametric(*t_a)),
Some(PROJECT_OPTS),
)
.clamp(0.0, 1.0)
}));
intersects_a[i].extend(intersections_a);