Merge remote-tracking branch 'origin/main' into staging

This commit is contained in:
Andrey Antukh 2026-07-16 13:52:28 +02:00
commit 8e8fb67793
5 changed files with 134 additions and 27 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);

View File

@ -108,6 +108,15 @@ fn calculate_group_bounds(
shape_bounds.with_points(result)
}
fn calculate_masked_group_bounds(
shape: &Shape,
shapes: ShapesPoolRef,
bounds: &HashMap<Uuid, Bounds>,
) -> Option<Bounds> {
let mask = shape.mask_id().and_then(|id| shapes.get(id))?;
Some(bounds.find(mask))
}
fn calculate_bool_bounds(
shape: &Shape,
shapes: ShapesPoolRef,
@ -349,10 +358,10 @@ fn propagate_reflow(
layout_reflows.insert(*id);
}
Type::Group(Group { masked: true }) => {
let children_ids = shape.children_ids(true);
if let Some(child) = children_ids.first().and_then(|id| shapes.get(id)) {
let child_bounds = bounds.find(child);
bounds.insert(shape.id, child_bounds);
// Masked group bounds are the mask shape bounds (first child in stored
// order, children_ids returns children in reverse order)
if let Some(shape_bounds) = calculate_masked_group_bounds(shape, shapes, bounds) {
bounds.insert(shape.id, shape_bounds);
}
reflown.insert(*id);
}
@ -588,4 +597,35 @@ mod tests {
assert_eq!(bounds.width(), 3.0);
assert_eq!(bounds.height(), 3.0);
}
#[test]
fn test_masked_group_bounds_are_mask_bounds() {
let parent_id = Uuid::new_v4();
let shapes = {
let mut shapes = ShapesPool::new();
shapes.initialize(10);
let mask_id = Uuid::new_v4();
let mask = shapes.add_shape(mask_id);
mask.set_selrect(1.0, 1.0, 5.0, 5.0);
let content_id = Uuid::new_v4();
let content = shapes.add_shape(content_id);
content.set_selrect(0.0, 0.0, 10.0, 10.0);
let parent = shapes.add_shape(parent_id);
parent.set_shape_type(Type::Group(Group { masked: true }));
parent.add_child(mask_id);
parent.add_child(content_id);
parent.set_selrect(1.0, 1.0, 5.0, 5.0);
shapes
};
let parent = shapes.get(&parent_id).unwrap();
let bounds = calculate_masked_group_bounds(parent, &shapes, &HashMap::new()).unwrap();
assert_eq!(bounds.width(), 4.0);
assert_eq!(bounds.height(), 4.0);
}
}