mirror of
https://github.com/penpot/penpot.git
synced 2026-07-22 14:08:03 +00:00
Merge remote-tracking branch 'origin/staging' into develop
This commit is contained in:
commit
5cc6477e3f
@ -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))
|
||||
|
||||
@ -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)))
|
||||
|
||||
|
||||
@ -2503,19 +2503,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))
|
||||
|
||||
@ -10,13 +10,23 @@
|
||||
[app.main.store :as st]
|
||||
[app.util.dom :as dom]))
|
||||
|
||||
(defn- get-element
|
||||
"Given a DOM node, returns the nearest Element, walking up from text nodes."
|
||||
[target]
|
||||
(if (and (some? target)
|
||||
(not= (.-nodeType target) js/Node.ELEMENT_NODE))
|
||||
(.-parentElement target)
|
||||
target))
|
||||
|
||||
(defn v1-closest-text-editor-content
|
||||
[target]
|
||||
(.closest ^js target ".public-DraftEditor-content"))
|
||||
(when-let [el (get-element target)]
|
||||
(.closest ^js el ".public-DraftEditor-content")))
|
||||
|
||||
(defn v2-closest-text-editor-content
|
||||
[target]
|
||||
(.closest ^js target "[data-itype=\"editor\"]"))
|
||||
(when-let [el (get-element target)]
|
||||
(.closest ^js el "[data-itype=\"editor\"]")))
|
||||
|
||||
(defn closest-text-editor-content
|
||||
[target]
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user