mirror of
https://github.com/penpot/penpot.git
synced 2026-01-17 02:40:19 -05:00
Compare commits
1 Commits
staging-re
...
superalex-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea0aa1fc1d |
@@ -526,25 +526,20 @@
|
||||
ids))
|
||||
|
||||
(defn clean-loops
|
||||
"Clean a list of ids from circular references. Optimized fast-path for single selections."
|
||||
"Clean a list of ids from circular references."
|
||||
[objects ids]
|
||||
(if (<= (count ids) 1)
|
||||
;; For single selection, there can't be circularity; return as ordered-set.
|
||||
(into (d/ordered-set) ids)
|
||||
(let [ids-set (if (set? ids) ids (set ids))
|
||||
parent-selected?
|
||||
(fn [id]
|
||||
;; Stop early as soon as we find any selected parent
|
||||
(let [parents (get-parent-ids objects id)]
|
||||
(some #(contains? ids-set %) parents)))
|
||||
(let [parent-selected?
|
||||
(fn [id]
|
||||
(let [parents (get-parent-ids objects id)]
|
||||
(some ids parents)))
|
||||
|
||||
add-element
|
||||
(fn [result id]
|
||||
(cond-> result
|
||||
(not (parent-selected? id))
|
||||
(conj id)))]
|
||||
add-element
|
||||
(fn [result id]
|
||||
(cond-> result
|
||||
(not (parent-selected? id))
|
||||
(conj id)))]
|
||||
|
||||
(reduce add-element (d/ordered-set) ids))))
|
||||
(reduce add-element (d/ordered-set) ids)))
|
||||
|
||||
(defn- indexed-shapes
|
||||
"Retrieves a vector with the indexes for each element in the layer
|
||||
|
||||
@@ -61,11 +61,6 @@
|
||||
;; Def micro-benchmark iterations
|
||||
(def micro-benchmark-iterations 1e6)
|
||||
|
||||
;; Performance logs
|
||||
(defonce ^:private longtask-observer* (atom nil))
|
||||
(defonce ^:private stall-timer* (atom nil))
|
||||
(defonce ^:private current-op* (atom nil))
|
||||
|
||||
;; --- CONTEXT
|
||||
|
||||
(defn- collect-context
|
||||
@@ -469,72 +464,3 @@
|
||||
(defn event
|
||||
[props]
|
||||
(ptk/data-event ::event props))
|
||||
|
||||
;; --- DEVTOOLS PERF LOGGING
|
||||
|
||||
(defn install-long-task-observer! []
|
||||
(when (and (some? (.-PerformanceObserver js/window)) (nil? @longtask-observer*))
|
||||
(let [observer (js/PerformanceObserver.
|
||||
(fn [list _]
|
||||
(doseq [entry (.getEntries list)]
|
||||
(let [dur (.-duration entry)
|
||||
start (.-startTime entry)
|
||||
attrib (.-attribution entry)
|
||||
attrib-count (when attrib (.-length attrib))
|
||||
first-attrib (when (and attrib-count (> attrib-count 0)) (aget attrib 0))
|
||||
attrib-name (when first-attrib (.-name first-attrib))
|
||||
attrib-ctype (when first-attrib (.-containerType first-attrib))
|
||||
attrib-cid (when first-attrib (.-containerId first-attrib))
|
||||
attrib-csrc (when first-attrib (.-containerSrc first-attrib))]
|
||||
|
||||
(.warn js/console (str "[perf] long task " (Math/round dur) "ms at " (Math/round start) "ms"
|
||||
(when first-attrib
|
||||
(str " attrib:name=" attrib-name
|
||||
" ctype=" attrib-ctype
|
||||
" cid=" attrib-cid
|
||||
" csrc=" attrib-csrc))))))))]
|
||||
(.observe observer #js{:entryTypes #js["longtask"]})
|
||||
(reset! longtask-observer* observer))))
|
||||
|
||||
(defn start-event-loop-stall-logger!
|
||||
"Log event loop stalls by measuring setInterval drift.
|
||||
interval-ms: base interval
|
||||
threshold-ms: drift over which we report"
|
||||
[interval-ms threshold-ms]
|
||||
(when (nil? @stall-timer*)
|
||||
(let [last (atom (.now js/performance))
|
||||
id (js/setInterval
|
||||
(fn []
|
||||
(let [now (.now js/performance)
|
||||
expected (+ @last interval-ms)
|
||||
drift (- now expected)
|
||||
current-op @current-op*
|
||||
measures (.getEntriesByType js/performance "measure")
|
||||
mlen (.-length measures)
|
||||
last-measure (when (> mlen 0) (aget measures (dec mlen)))
|
||||
meas-name (when last-measure (.-name last-measure))
|
||||
meas-detail (when last-measure (.-detail last-measure))
|
||||
meas-count (when meas-detail (unchecked-get meas-detail "count"))]
|
||||
(reset! last now)
|
||||
(when (> drift threshold-ms)
|
||||
(.warn js/console
|
||||
(str "[perf] event loop stall: " (Math/round drift) "ms"
|
||||
(when current-op (str " op=" current-op))
|
||||
(when meas-name (str " last=" meas-name))
|
||||
(when meas-count (str " count=" meas-count)))))))
|
||||
interval-ms)]
|
||||
(reset! stall-timer* id))))
|
||||
|
||||
(defn init!
|
||||
"Install perf observers in dev builds. Safe to call multiple times."
|
||||
[]
|
||||
(when ^boolean js/goog.DEBUG
|
||||
(install-long-task-observer!)
|
||||
(start-event-loop-stall-logger! 50 100)
|
||||
;; Expose simple API on window for manual control in devtools
|
||||
(let [api #js {:reset (fn []
|
||||
(try
|
||||
(.clearMarks js/performance)
|
||||
(.clearMeasures js/performance)
|
||||
(catch :default _ nil)))}]
|
||||
(aset js/window "PenpotPerf" api))))
|
||||
|
||||
@@ -347,12 +347,6 @@
|
||||
(with-meta {:team-id team-id
|
||||
:file-id file-id}))))))
|
||||
|
||||
;; Install dev perf observers once the workspace is ready
|
||||
(->> stream
|
||||
(rx/filter (ptk/type? ::workspace-initialized))
|
||||
(rx/take 1)
|
||||
(rx/map (fn [_] (ev/init!))))
|
||||
|
||||
(->> stream
|
||||
(rx/filter (ptk/type? ::dps/persistence-notification))
|
||||
(rx/take 1)
|
||||
|
||||
@@ -18,13 +18,13 @@
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [expand-fn (fn [expanded]
|
||||
(let [parents-seqs (map (fn [x] (cfh/get-parent-ids objects x)) ids)
|
||||
flat-parents (apply concat parents-seqs)
|
||||
non-root-parents (remove #(= % uuid/zero) flat-parents)
|
||||
distinct-parents (into #{} non-root-parents)]
|
||||
(merge expanded
|
||||
(into {}
|
||||
(map (fn [id] {id true}) distinct-parents)))))]
|
||||
(merge expanded
|
||||
(->> ids
|
||||
(map #(cfh/get-parent-ids objects %))
|
||||
flatten
|
||||
(remove #(= % uuid/zero))
|
||||
(map (fn [id] {id true}))
|
||||
(into {}))))]
|
||||
(update-in state [:workspace-local :expanded] expand-fn)))))
|
||||
|
||||
|
||||
|
||||
@@ -264,13 +264,10 @@
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [objects (dsh/lookup-page-objects state)
|
||||
;; Schedule expanding parents asynchronously to avoid blocking
|
||||
;; the event loop
|
||||
expand-s (->> (rx/of (dwc/expand-all-parents ids objects))
|
||||
(rx/observe-on :async))
|
||||
interrupt-s (rx/of ::dwsp/interrupt)]
|
||||
(rx/merge expand-s interrupt-s)))))
|
||||
(let [objects (dsh/lookup-page-objects state)]
|
||||
(rx/of
|
||||
(dwc/expand-all-parents ids objects)
|
||||
::dwsp/interrupt)))))
|
||||
|
||||
(defn select-all
|
||||
[]
|
||||
|
||||
@@ -305,7 +305,7 @@
|
||||
(l/derived #(dsh/lookup-shape % page-id shape-id) st/state =))
|
||||
|
||||
(def workspace-page-objects
|
||||
(l/derived dsh/lookup-page-objects st/state identical?))
|
||||
(l/derived dsh/lookup-page-objects st/state))
|
||||
|
||||
(def workspace-read-only?
|
||||
(l/derived :read-only? workspace-global))
|
||||
|
||||
@@ -33,24 +33,9 @@
|
||||
[okulary.core :as l]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
;; Coalesce sidebar hover highlights to 1 frame to avoid long tasks
|
||||
(defonce ^:private sidebar-hover-queue (atom {:enter #{} :leave #{}}))
|
||||
(defonce ^:private sidebar-hover-pending? (atom false))
|
||||
|
||||
(defn- schedule-sidebar-hover-flush []
|
||||
(when (compare-and-set! sidebar-hover-pending? false true)
|
||||
(ts/raf
|
||||
(fn []
|
||||
(let [{:keys [enter leave]} (swap! sidebar-hover-queue (constantly {:enter #{} :leave #{}}))]
|
||||
(reset! sidebar-hover-pending? false)
|
||||
(when (seq leave)
|
||||
(apply st/emit! (map dw/dehighlight-shape leave)))
|
||||
(when (seq enter)
|
||||
(apply st/emit! (map dw/highlight-shape enter))))))))
|
||||
|
||||
(mf/defc layer-item-inner
|
||||
{::mf/wrap-props false}
|
||||
[{:keys [item depth parent-size name-ref children ref style
|
||||
[{:keys [item depth parent-size name-ref children ref
|
||||
;; Flags
|
||||
read-only? highlighted? selected? component-tree?
|
||||
filtered? expanded? dnd-over? dnd-over-top? dnd-over-bot? hide-toggle?
|
||||
@@ -97,8 +82,7 @@
|
||||
:dnd-over dnd-over?
|
||||
:dnd-over-top dnd-over-top?
|
||||
:dnd-over-bot dnd-over-bot?
|
||||
:root-board parent-board?)
|
||||
:style style}
|
||||
:root-board parent-board?)}
|
||||
[:span {:class (stl/css-case
|
||||
:tab-indentation true
|
||||
:filtered filtered?)
|
||||
@@ -182,12 +166,10 @@
|
||||
|
||||
children]))
|
||||
|
||||
;; Memoized for performance
|
||||
(mf/defc layer-item
|
||||
{::mf/props :obj
|
||||
::mf/wrap [mf/memo]}
|
||||
[{:keys [index item selected objects sortable? filtered? depth parent-size component-child? highlighted style render-children?]
|
||||
:or {render-children? true}}]
|
||||
::mf/memo true}
|
||||
[{:keys [index item selected objects sortable? filtered? depth parent-size component-child? highlighted]}]
|
||||
(let [id (:id item)
|
||||
blocked? (:blocked item)
|
||||
hidden? (:hidden item)
|
||||
@@ -264,21 +246,13 @@
|
||||
(mf/use-fn
|
||||
(mf/deps id)
|
||||
(fn [_]
|
||||
(swap! sidebar-hover-queue (fn [{:keys [enter leave] :as q}]
|
||||
(-> q
|
||||
(assoc :enter (conj enter id))
|
||||
(assoc :leave (disj leave id)))))
|
||||
(schedule-sidebar-hover-flush)))
|
||||
(st/emit! (dw/highlight-shape id))))
|
||||
|
||||
on-pointer-leave
|
||||
(mf/use-fn
|
||||
(mf/deps id)
|
||||
(fn [_]
|
||||
(swap! sidebar-hover-queue (fn [{:keys [enter leave] :as q}]
|
||||
(-> q
|
||||
(assoc :enter (disj enter id))
|
||||
(assoc :leave (conj leave id)))))
|
||||
(schedule-sidebar-hover-flush)))
|
||||
(st/emit! (dw/dehighlight-shape id))))
|
||||
|
||||
on-context-menu
|
||||
(mf/use-fn
|
||||
@@ -364,18 +338,14 @@
|
||||
component-tree? (or component-child? (ctk/instance-root? item) (ctk/instance-head? item))
|
||||
|
||||
enable-drag (mf/use-fn #(reset! drag-disabled* false))
|
||||
disable-drag (mf/use-fn #(reset! drag-disabled* true))
|
||||
|
||||
;; Lazy loading of child elements via IntersectionObserver
|
||||
children-count* (mf/use-state 0)
|
||||
children-count (deref children-count*)
|
||||
lazy-ref (mf/use-ref nil)
|
||||
observer-var (mf/use-var nil)
|
||||
chunk-size 50]
|
||||
disable-drag (mf/use-fn #(reset! drag-disabled* true))]
|
||||
|
||||
(mf/with-effect [selected? selected]
|
||||
(let [single? (= (count selected) 1)
|
||||
node (mf/ref-val ref)
|
||||
;; NOTE: Neither get-parent-at nor get-parent-with-selector
|
||||
;; work if the component template changes, so we need to
|
||||
;; seek for an alternate solution. Maybe use-context?
|
||||
scroll-node (dom/get-parent-with-data node "scroll-container")
|
||||
parent-node (dom/get-parent-at node 2)
|
||||
first-child-node (dom/get-first-child parent-node)
|
||||
@@ -393,61 +363,6 @@
|
||||
#(when (some? subid)
|
||||
(rx/dispose! subid))))
|
||||
|
||||
;; Setup scroll-driven lazy loading when expanded
|
||||
;; and ensures selected children are loaded immediately
|
||||
(mf/with-effect [expanded? (:shapes item) selected]
|
||||
(let [shapes-vec (:shapes item)
|
||||
total (count shapes-vec)]
|
||||
(if expanded?
|
||||
(let [;; Children are rendered in reverse order, so index 0 in render = last in shapes-vec
|
||||
;; Find if any selected id is a direct child and get its render index
|
||||
selected-child-render-idx
|
||||
(when (and (> total chunk-size) (seq selected))
|
||||
(let [shapes-reversed (vec (reverse shapes-vec))]
|
||||
(some (fn [sel-id]
|
||||
(let [idx (.indexOf shapes-reversed sel-id)]
|
||||
(when (>= idx 0) idx)))
|
||||
selected)))
|
||||
;; Load at least enough to include the selected child plus extra
|
||||
;; for context (so it can be centered in the scroll view)
|
||||
min-count (if selected-child-render-idx
|
||||
(+ selected-child-render-idx chunk-size)
|
||||
chunk-size)
|
||||
current @children-count*
|
||||
new-count (min total (max current chunk-size min-count))]
|
||||
(reset! children-count* new-count))
|
||||
(reset! children-count* 0)))
|
||||
(fn []
|
||||
(when-let [obs ^js @observer-var]
|
||||
(.disconnect obs)
|
||||
(reset! observer-var nil))))
|
||||
|
||||
;; Re-observe sentinel whenever children-count changes (sentinel moves)
|
||||
(mf/with-effect [children-count expanded?]
|
||||
(let [total (count (:shapes item))
|
||||
node (mf/ref-val ref)
|
||||
scroll-node (dom/get-parent-with-data node "scroll-container")
|
||||
lazy-node (mf/ref-val lazy-ref)]
|
||||
;; Disconnect previous observer
|
||||
(when-let [obs ^js @observer-var]
|
||||
(.disconnect obs)
|
||||
(reset! observer-var nil))
|
||||
;; Setup new observer if there are more children to load
|
||||
(when (and expanded?
|
||||
(< children-count total)
|
||||
scroll-node
|
||||
lazy-node)
|
||||
(let [cb (fn [entries]
|
||||
(when (and (seq entries)
|
||||
(.-isIntersecting (first entries)))
|
||||
;; Load next chunk when sentinel intersects
|
||||
(let [current @children-count*
|
||||
next-count (min total (+ current chunk-size))]
|
||||
(reset! children-count* next-count))))
|
||||
observer (js/IntersectionObserver. cb #js {:root scroll-node})]
|
||||
(.observe observer lazy-node)
|
||||
(reset! observer-var observer)))))
|
||||
|
||||
[:& layer-item-inner
|
||||
{:ref dref
|
||||
:item item
|
||||
@@ -472,32 +387,24 @@
|
||||
:on-enable-drag enable-drag
|
||||
:on-disable-drag disable-drag
|
||||
:on-toggle-visibility toggle-visibility
|
||||
:on-toggle-blocking toggle-blocking
|
||||
:style style}
|
||||
:on-toggle-blocking toggle-blocking}
|
||||
|
||||
(when (and render-children?
|
||||
(:shapes item)
|
||||
expanded?)
|
||||
(when (and (:shapes item) expanded?)
|
||||
[:div {:class (stl/css-case
|
||||
:element-children true
|
||||
:parent-selected selected?
|
||||
:sticky-children parent-board?)
|
||||
:data-testid (dm/str "children-" id)}
|
||||
(let [all-children (reverse (d/enumerate (:shapes item)))
|
||||
visible (take children-count all-children)]
|
||||
(for [[index id] visible]
|
||||
(when-let [item (get objects id)]
|
||||
[:& layer-item
|
||||
{:item item
|
||||
:highlighted highlighted
|
||||
:selected selected
|
||||
:index index
|
||||
:objects objects
|
||||
:key (dm/str id)
|
||||
:sortable? sortable?
|
||||
:depth depth
|
||||
:parent-size parent-size
|
||||
:component-child? component-tree?}])))
|
||||
(when (< children-count (count (:shapes item)))
|
||||
[:div {:ref lazy-ref
|
||||
:style {:min-height 1}}])])]))
|
||||
(for [[index id] (reverse (d/enumerate (:shapes item)))]
|
||||
(when-let [item (get objects id)]
|
||||
[:& layer-item
|
||||
{:item item
|
||||
:highlighted highlighted
|
||||
:selected selected
|
||||
:index index
|
||||
:objects objects
|
||||
:key (dm/str id)
|
||||
:sortable? sortable?
|
||||
:depth depth
|
||||
:parent-size parent-size
|
||||
:component-child? component-tree?}]))])]))
|
||||
|
||||
@@ -116,29 +116,13 @@
|
||||
(->> (dm/get-in grid-edition [edition :selected])
|
||||
(map #(dm/get-in objects [edition :layout-grid-cells %])))
|
||||
|
||||
shapes-with-children*
|
||||
(mf/use-state nil)
|
||||
|
||||
_ (mf/use-effect
|
||||
(mf/deps selected objects shapes)
|
||||
(fn []
|
||||
(reset! shapes-with-children* nil)
|
||||
(let [result
|
||||
(loop [queue (into #queue [] selected)
|
||||
visited selected]
|
||||
(if-let [id (peek queue)]
|
||||
(let [shape (get objects id)
|
||||
children (:shapes shape)]
|
||||
(if (seq children)
|
||||
(let [new-children (remove visited children)]
|
||||
(recur (into (pop queue) new-children)
|
||||
(into visited new-children)))
|
||||
(recur (pop queue) visited)))
|
||||
(sequence (keep (d/getf objects)) visited)))]
|
||||
(reset! shapes-with-children* result))))
|
||||
|
||||
shapes-with-children
|
||||
(deref shapes-with-children*)
|
||||
(mf/with-memo [selected objects shapes]
|
||||
(let [xform (comp (remove nil?)
|
||||
(mapcat #(cfh/get-children-ids objects %)))
|
||||
selected (into selected xform selected)]
|
||||
(sequence (keep (d/getf objects)) selected)))
|
||||
|
||||
|
||||
total-selected
|
||||
(count selected)]
|
||||
|
||||
@@ -284,6 +284,10 @@ pub extern "C" fn set_view_end() {
|
||||
performance::end_measure!("set_view_end::clear_tile_index");
|
||||
performance::end_timed_log!("clear_tile_index", _clear_start);
|
||||
}
|
||||
// Sync cached_viewbox with current viewbox to ensure accurate
|
||||
// comparison in subsequent calls, especially during rapid zoom+pan
|
||||
// interactions where multiple set_view calls occur before set_view_end.
|
||||
state.render_state.sync_cached_viewbox();
|
||||
performance::end_measure!("set_view_end");
|
||||
performance::end_timed_log!("set_view_end", _end_start);
|
||||
#[cfg(feature = "profile-macros")]
|
||||
|
||||
@@ -294,6 +294,10 @@ pub(crate) struct RenderState {
|
||||
/// where we must render shapes without inheriting ancestor layer blurs. Toggle it through
|
||||
/// `with_nested_blurs_suppressed` to ensure it's always restored.
|
||||
pub ignore_nested_blurs: bool,
|
||||
/// Cached root_ids and root_ids_map to avoid recalculating them every frame
|
||||
/// These are invalidated when the tree structure changes
|
||||
cached_root_ids: Option<Vec<Uuid>>,
|
||||
cached_root_ids_map: Option<std::collections::HashMap<Uuid, usize>>,
|
||||
}
|
||||
|
||||
pub fn get_cache_size(viewbox: Viewbox, scale: f32) -> skia::ISize {
|
||||
@@ -366,6 +370,8 @@ impl RenderState {
|
||||
focus_mode: FocusMode::new(),
|
||||
touched_ids: HashSet::default(),
|
||||
ignore_nested_blurs: false,
|
||||
cached_root_ids: None,
|
||||
cached_root_ids_map: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -652,77 +658,6 @@ impl RenderState {
|
||||
}
|
||||
|
||||
let antialias = shape.should_use_antialias(self.get_scale());
|
||||
let fast_mode = self.options.is_fast_mode();
|
||||
let has_nested_fills = self
|
||||
.nested_fills
|
||||
.last()
|
||||
.is_some_and(|fills| !fills.is_empty());
|
||||
let has_inherited_blur = !self.ignore_nested_blurs
|
||||
&& self.nested_blurs.iter().flatten().any(|blur| {
|
||||
!blur.hidden && blur.blur_type == BlurType::LayerBlur && blur.value > 0.0
|
||||
});
|
||||
let can_render_directly = apply_to_current_surface
|
||||
&& clip_bounds.is_none()
|
||||
&& offset.is_none()
|
||||
&& parent_shadows.is_none()
|
||||
&& !shape.needs_layer()
|
||||
&& shape.blur.is_none()
|
||||
&& !has_inherited_blur
|
||||
&& shape.shadows.is_empty()
|
||||
&& shape.transform.is_identity()
|
||||
&& matches!(
|
||||
shape.shape_type,
|
||||
Type::Rect(_) | Type::Circle | Type::Path(_) | Type::Bool(_)
|
||||
)
|
||||
&& !(shape.fills.is_empty() && has_nested_fills)
|
||||
&& !shape
|
||||
.svg_attrs
|
||||
.as_ref()
|
||||
.is_some_and(|attrs| attrs.fill_none);
|
||||
|
||||
if can_render_directly {
|
||||
let scale = self.get_scale();
|
||||
let translation = self
|
||||
.surfaces
|
||||
.get_render_context_translation(self.render_area, scale);
|
||||
self.surfaces.apply_mut(SurfaceId::Current as u32, |s| {
|
||||
let canvas = s.canvas();
|
||||
canvas.save();
|
||||
canvas.scale((scale, scale));
|
||||
canvas.translate(translation);
|
||||
});
|
||||
|
||||
for fill in shape.fills().rev() {
|
||||
fills::render(self, shape, fill, antialias, SurfaceId::Current);
|
||||
}
|
||||
|
||||
for stroke in shape.visible_strokes().rev() {
|
||||
strokes::render(
|
||||
self,
|
||||
shape,
|
||||
stroke,
|
||||
Some(SurfaceId::Current),
|
||||
None,
|
||||
antialias,
|
||||
);
|
||||
}
|
||||
|
||||
self.surfaces.apply_mut(SurfaceId::Current as u32, |s| {
|
||||
s.canvas().restore();
|
||||
});
|
||||
|
||||
if self.options.is_debug_visible() {
|
||||
let shape_selrect_bounds = self.get_shape_selrect_bounds(shape);
|
||||
debug::render_debug_shape(self, Some(shape_selrect_bounds), None);
|
||||
}
|
||||
|
||||
if needs_save {
|
||||
self.surfaces.apply_mut(surface_ids, |s| {
|
||||
s.canvas().restore();
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// set clipping
|
||||
if let Some(clips) = clip_bounds.as_ref() {
|
||||
@@ -779,9 +714,6 @@ impl RenderState {
|
||||
} else if shape_has_blur {
|
||||
shape.to_mut().set_blur(None);
|
||||
}
|
||||
if fast_mode {
|
||||
shape.to_mut().set_blur(None);
|
||||
}
|
||||
|
||||
let center = shape.center();
|
||||
let mut matrix = shape.transform;
|
||||
@@ -826,8 +758,18 @@ impl RenderState {
|
||||
});
|
||||
|
||||
let text_content = text_content.new_bounds(shape.selrect());
|
||||
let mut drop_shadows = shape.drop_shadow_paints();
|
||||
|
||||
if let Some(inherited_shadows) = self.get_inherited_drop_shadows() {
|
||||
drop_shadows.extend(inherited_shadows);
|
||||
}
|
||||
|
||||
let inner_shadows = shape.inner_shadow_paints();
|
||||
let blur_filter = shape.image_filter(1.);
|
||||
let count_inner_strokes = shape.count_visible_inner_strokes();
|
||||
let mut paragraph_builders = text_content.paragraph_builder_group_from_text(None);
|
||||
let mut paragraphs_with_shadows =
|
||||
text_content.paragraph_builder_group_from_text(Some(true));
|
||||
let mut stroke_paragraphs_list = shape
|
||||
.visible_strokes()
|
||||
.rev()
|
||||
@@ -841,8 +783,62 @@ impl RenderState {
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if fast_mode {
|
||||
// Fast path: render fills and strokes only (skip shadows/blur).
|
||||
|
||||
let mut stroke_paragraphs_with_shadows_list = shape
|
||||
.visible_strokes()
|
||||
.rev()
|
||||
.map(|stroke| {
|
||||
text::stroke_paragraph_builder_group_from_text(
|
||||
&text_content,
|
||||
stroke,
|
||||
&shape.selrect(),
|
||||
count_inner_strokes,
|
||||
Some(true),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if let Some(parent_shadows) = parent_shadows {
|
||||
if !shape.has_visible_strokes() {
|
||||
for shadow in parent_shadows {
|
||||
text::render(
|
||||
Some(self),
|
||||
None,
|
||||
&shape,
|
||||
&mut paragraphs_with_shadows,
|
||||
text_drop_shadows_surface_id.into(),
|
||||
Some(&shadow),
|
||||
blur_filter.as_ref(),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
shadows::render_text_shadows(
|
||||
self,
|
||||
&shape,
|
||||
&mut paragraphs_with_shadows,
|
||||
&mut stroke_paragraphs_with_shadows_list,
|
||||
text_drop_shadows_surface_id.into(),
|
||||
&parent_shadows,
|
||||
&blur_filter,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// 1. Text drop shadows
|
||||
if !shape.has_visible_strokes() {
|
||||
for shadow in &drop_shadows {
|
||||
text::render(
|
||||
Some(self),
|
||||
None,
|
||||
&shape,
|
||||
&mut paragraphs_with_shadows,
|
||||
text_drop_shadows_surface_id.into(),
|
||||
Some(shadow),
|
||||
blur_filter.as_ref(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Text fills
|
||||
text::render(
|
||||
Some(self),
|
||||
None,
|
||||
@@ -850,9 +846,21 @@ impl RenderState {
|
||||
&mut paragraph_builders,
|
||||
Some(fills_surface_id),
|
||||
None,
|
||||
None,
|
||||
blur_filter.as_ref(),
|
||||
);
|
||||
|
||||
// 3. Stroke drop shadows
|
||||
shadows::render_text_shadows(
|
||||
self,
|
||||
&shape,
|
||||
&mut paragraphs_with_shadows,
|
||||
&mut stroke_paragraphs_with_shadows_list,
|
||||
text_drop_shadows_surface_id.into(),
|
||||
&drop_shadows,
|
||||
&blur_filter,
|
||||
);
|
||||
|
||||
// 4. Stroke fills
|
||||
for stroke_paragraphs in stroke_paragraphs_list.iter_mut() {
|
||||
text::render(
|
||||
Some(self),
|
||||
@@ -861,134 +869,34 @@ impl RenderState {
|
||||
stroke_paragraphs,
|
||||
Some(strokes_surface_id),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
let mut drop_shadows = shape.drop_shadow_paints();
|
||||
|
||||
if let Some(inherited_shadows) = self.get_inherited_drop_shadows() {
|
||||
drop_shadows.extend(inherited_shadows);
|
||||
}
|
||||
|
||||
let inner_shadows = shape.inner_shadow_paints();
|
||||
let blur_filter = shape.image_filter(1.);
|
||||
let mut paragraphs_with_shadows =
|
||||
text_content.paragraph_builder_group_from_text(Some(true));
|
||||
let mut stroke_paragraphs_with_shadows_list = shape
|
||||
.visible_strokes()
|
||||
.rev()
|
||||
.map(|stroke| {
|
||||
text::stroke_paragraph_builder_group_from_text(
|
||||
&text_content,
|
||||
stroke,
|
||||
&shape.selrect(),
|
||||
count_inner_strokes,
|
||||
Some(true),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if let Some(parent_shadows) = parent_shadows {
|
||||
if !shape.has_visible_strokes() {
|
||||
for shadow in parent_shadows {
|
||||
text::render(
|
||||
Some(self),
|
||||
None,
|
||||
&shape,
|
||||
&mut paragraphs_with_shadows,
|
||||
text_drop_shadows_surface_id.into(),
|
||||
Some(&shadow),
|
||||
blur_filter.as_ref(),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
shadows::render_text_shadows(
|
||||
self,
|
||||
&shape,
|
||||
&mut paragraphs_with_shadows,
|
||||
&mut stroke_paragraphs_with_shadows_list,
|
||||
text_drop_shadows_surface_id.into(),
|
||||
&parent_shadows,
|
||||
&blur_filter,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// 1. Text drop shadows
|
||||
if !shape.has_visible_strokes() {
|
||||
for shadow in &drop_shadows {
|
||||
text::render(
|
||||
Some(self),
|
||||
None,
|
||||
&shape,
|
||||
&mut paragraphs_with_shadows,
|
||||
text_drop_shadows_surface_id.into(),
|
||||
Some(shadow),
|
||||
blur_filter.as_ref(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Text fills
|
||||
text::render(
|
||||
Some(self),
|
||||
None,
|
||||
&shape,
|
||||
&mut paragraph_builders,
|
||||
Some(fills_surface_id),
|
||||
None,
|
||||
blur_filter.as_ref(),
|
||||
);
|
||||
}
|
||||
|
||||
// 3. Stroke drop shadows
|
||||
shadows::render_text_shadows(
|
||||
self,
|
||||
&shape,
|
||||
&mut paragraphs_with_shadows,
|
||||
&mut stroke_paragraphs_with_shadows_list,
|
||||
text_drop_shadows_surface_id.into(),
|
||||
&drop_shadows,
|
||||
&blur_filter,
|
||||
);
|
||||
// 5. Stroke inner shadows
|
||||
shadows::render_text_shadows(
|
||||
self,
|
||||
&shape,
|
||||
&mut paragraphs_with_shadows,
|
||||
&mut stroke_paragraphs_with_shadows_list,
|
||||
Some(innershadows_surface_id),
|
||||
&inner_shadows,
|
||||
&blur_filter,
|
||||
);
|
||||
|
||||
// 4. Stroke fills
|
||||
for stroke_paragraphs in stroke_paragraphs_list.iter_mut() {
|
||||
// 6. Fill Inner shadows
|
||||
if !shape.has_visible_strokes() {
|
||||
for shadow in &inner_shadows {
|
||||
text::render(
|
||||
Some(self),
|
||||
None,
|
||||
&shape,
|
||||
stroke_paragraphs,
|
||||
Some(strokes_surface_id),
|
||||
None,
|
||||
&mut paragraphs_with_shadows,
|
||||
Some(innershadows_surface_id),
|
||||
Some(shadow),
|
||||
blur_filter.as_ref(),
|
||||
);
|
||||
}
|
||||
|
||||
// 5. Stroke inner shadows
|
||||
shadows::render_text_shadows(
|
||||
self,
|
||||
&shape,
|
||||
&mut paragraphs_with_shadows,
|
||||
&mut stroke_paragraphs_with_shadows_list,
|
||||
Some(innershadows_surface_id),
|
||||
&inner_shadows,
|
||||
&blur_filter,
|
||||
);
|
||||
|
||||
// 6. Fill Inner shadows
|
||||
if !shape.has_visible_strokes() {
|
||||
for shadow in &inner_shadows {
|
||||
text::render(
|
||||
Some(self),
|
||||
None,
|
||||
&shape,
|
||||
&mut paragraphs_with_shadows,
|
||||
Some(innershadows_surface_id),
|
||||
Some(shadow),
|
||||
blur_filter.as_ref(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1028,25 +936,16 @@ impl RenderState {
|
||||
None,
|
||||
antialias,
|
||||
);
|
||||
if !fast_mode {
|
||||
shadows::render_stroke_inner_shadows(
|
||||
self,
|
||||
shape,
|
||||
stroke,
|
||||
antialias,
|
||||
innershadows_surface_id,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if !fast_mode {
|
||||
shadows::render_fill_inner_shadows(
|
||||
shadows::render_stroke_inner_shadows(
|
||||
self,
|
||||
shape,
|
||||
stroke,
|
||||
antialias,
|
||||
innershadows_surface_id,
|
||||
);
|
||||
}
|
||||
|
||||
shadows::render_fill_inner_shadows(self, shape, antialias, innershadows_surface_id);
|
||||
// bools::debug_render_bool_paths(self, shape, shapes, modifiers, structure);
|
||||
}
|
||||
};
|
||||
@@ -1625,8 +1524,6 @@ impl RenderState {
|
||||
"Error: Element with root_id {} not found in the tree.",
|
||||
node_render_state.id
|
||||
))?;
|
||||
let scale = self.get_scale();
|
||||
let mut extrect: Option<Rect> = None;
|
||||
|
||||
// If the shape is not in the tile set, then we add them.
|
||||
if self.tiles.get_tiles_of(node_id).is_none() {
|
||||
@@ -1657,21 +1554,29 @@ impl RenderState {
|
||||
crate::shapes::Type::Frame(_) | crate::shapes::Type::Group(_)
|
||||
);
|
||||
|
||||
let scale = self.get_scale();
|
||||
let has_effects = transformed_element.has_effects_that_extend_bounds();
|
||||
|
||||
let is_visible = if is_container || has_effects {
|
||||
let element_extrect =
|
||||
extrect.get_or_insert_with(|| transformed_element.extrect(tree, scale));
|
||||
element_extrect.intersects(self.render_area)
|
||||
let is_visible = if is_container {
|
||||
// Containers (frames/groups) must always use extrect to include nested content
|
||||
let extrect = transformed_element.extrect(tree, scale);
|
||||
extrect.intersects(self.render_area)
|
||||
&& !transformed_element.visually_insignificant(scale, tree)
|
||||
} else {
|
||||
} else if !has_effects {
|
||||
// Simple shape: selrect check is sufficient, skip expensive extrect
|
||||
let selrect = transformed_element.selrect();
|
||||
selrect.intersects(self.render_area)
|
||||
&& !transformed_element.visually_insignificant(scale, tree)
|
||||
} else {
|
||||
// Shape with effects: need extrect for accurate bounds
|
||||
let extrect = transformed_element.extrect(tree, scale);
|
||||
extrect.intersects(self.render_area)
|
||||
&& !transformed_element.visually_insignificant(scale, tree)
|
||||
};
|
||||
|
||||
if self.options.is_debug_visible() {
|
||||
let shape_extrect_bounds = self.get_shape_extrect_bounds(element, tree);
|
||||
let shape_extrect_bounds =
|
||||
self.get_shape_extrect_bounds(&transformed_element, tree);
|
||||
debug::render_debug_shape(self, None, Some(shape_extrect_bounds));
|
||||
}
|
||||
|
||||
@@ -1688,6 +1593,7 @@ impl RenderState {
|
||||
}
|
||||
|
||||
if !node_render_state.is_root() && self.focus_mode.is_active() {
|
||||
let scale: f32 = self.get_scale();
|
||||
let translation = self
|
||||
.surfaces
|
||||
.get_render_context_translation(self.render_area, scale);
|
||||
@@ -1718,6 +1624,8 @@ impl RenderState {
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let element_extrect = element.extrect(tree, scale);
|
||||
|
||||
for shadow in element.drop_shadows_visible() {
|
||||
let paint = skia::Paint::default();
|
||||
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint);
|
||||
@@ -1727,11 +1635,9 @@ impl RenderState {
|
||||
.save_layer(&layer_rec);
|
||||
|
||||
// First pass: Render shadow in black to establish alpha mask
|
||||
let element_extrect =
|
||||
extrect.get_or_insert_with(|| element.extrect(tree, scale));
|
||||
self.render_drop_black_shadow(
|
||||
element,
|
||||
element_extrect,
|
||||
&element_extrect,
|
||||
shadow,
|
||||
clip_bounds.clone(),
|
||||
scale,
|
||||
@@ -1746,13 +1652,15 @@ impl RenderState {
|
||||
if shadow_shape.hidden {
|
||||
continue;
|
||||
}
|
||||
|
||||
let clip_bounds = node_render_state
|
||||
.get_nested_shadow_clip_bounds(element, shadow);
|
||||
|
||||
if !matches!(shadow_shape.shape_type, Type::Text(_)) {
|
||||
let shadow_extrect = shadow_shape.extrect(tree, scale);
|
||||
self.render_drop_black_shadow(
|
||||
shadow_shape,
|
||||
&shadow_shape.extrect(tree, scale),
|
||||
&shadow_extrect,
|
||||
shadow,
|
||||
clip_bounds,
|
||||
scale,
|
||||
@@ -1966,16 +1874,6 @@ impl RenderState {
|
||||
allow_stop: bool,
|
||||
) -> Result<(), String> {
|
||||
let mut should_stop = false;
|
||||
let root_ids = {
|
||||
if let Some(shape_id) = base_object {
|
||||
vec![*shape_id]
|
||||
} else {
|
||||
let Some(root) = tree.get(&Uuid::nil()) else {
|
||||
return Err(String::from("Root shape not found"));
|
||||
};
|
||||
root.children_ids(false)
|
||||
}
|
||||
};
|
||||
|
||||
while !should_stop {
|
||||
if let Some(current_tile) = self.current_tile {
|
||||
@@ -2032,6 +1930,42 @@ impl RenderState {
|
||||
.canvas(SurfaceId::Current)
|
||||
.clear(self.background_color);
|
||||
|
||||
// Get or compute root_ids and root_ids_map (cached to avoid recalculation every frame)
|
||||
let root_ids_map = {
|
||||
let root_ids = if let Some(shape_id) = base_object {
|
||||
vec![*shape_id]
|
||||
} else {
|
||||
let Some(root) = tree.get(&Uuid::nil()) else {
|
||||
return Err(String::from("Root shape not found"));
|
||||
};
|
||||
root.children_ids(false)
|
||||
};
|
||||
|
||||
// Check if cache is valid (same root_ids)
|
||||
let cache_valid = self
|
||||
.cached_root_ids
|
||||
.as_ref()
|
||||
.map(|cached| cached.as_slice() == root_ids.as_slice())
|
||||
.unwrap_or(false);
|
||||
|
||||
if cache_valid {
|
||||
// Use cached map
|
||||
self.cached_root_ids_map.as_ref().unwrap().clone()
|
||||
} else {
|
||||
// Recompute and cache
|
||||
let root_ids_map: std::collections::HashMap<Uuid, usize> = root_ids
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, id)| (*id, i))
|
||||
.collect();
|
||||
|
||||
self.cached_root_ids = Some(root_ids.clone());
|
||||
self.cached_root_ids_map = Some(root_ids_map.clone());
|
||||
|
||||
root_ids_map
|
||||
}
|
||||
};
|
||||
|
||||
// If we finish processing every node rendering is complete
|
||||
// let's check if there are more pending nodes
|
||||
if let Some(next_tile) = self.pending_tiles.pop() {
|
||||
@@ -2039,13 +1973,15 @@ impl RenderState {
|
||||
|
||||
if !self.surfaces.has_cached_tile_surface(next_tile) {
|
||||
if let Some(ids) = self.tiles.get_shapes_at(next_tile) {
|
||||
// We only need first level shapes, in the same order as the parent node
|
||||
let mut valid_ids = Vec::with_capacity(ids.len());
|
||||
for root_id in root_ids.iter() {
|
||||
if ids.contains(root_id) {
|
||||
valid_ids.push(*root_id);
|
||||
}
|
||||
}
|
||||
// We only need first level shapes
|
||||
let mut valid_ids: Vec<Uuid> = ids
|
||||
.iter()
|
||||
.filter(|id| root_ids_map.contains_key(id))
|
||||
.copied()
|
||||
.collect();
|
||||
|
||||
// These shapes for the tile should be ordered as they are in the parent node
|
||||
valid_ids.sort_by_key(|id| root_ids_map.get(id).unwrap_or(&usize::MAX));
|
||||
|
||||
self.pending_nodes.extend(valid_ids.into_iter().map(|id| {
|
||||
NodeRenderState {
|
||||
@@ -2292,6 +2228,13 @@ impl RenderState {
|
||||
(self.viewbox.zoom - self.cached_viewbox.zoom).abs() > f32::EPSILON
|
||||
}
|
||||
|
||||
/// Updates the cached viewbox to match the current viewbox.
|
||||
/// This should be called at the end of view interactions to ensure
|
||||
/// that cached_viewbox reflects the most recent state for the next comparison.
|
||||
pub fn sync_cached_viewbox(&mut self) {
|
||||
self.cached_viewbox = self.viewbox;
|
||||
}
|
||||
|
||||
pub fn mark_touched(&mut self, uuid: Uuid) {
|
||||
self.touched_ids.insert(uuid);
|
||||
}
|
||||
|
||||
@@ -1512,8 +1512,6 @@ impl Shape {
|
||||
!self.shadows.is_empty()
|
||||
|| self.blur.is_some()
|
||||
|| !self.strokes.is_empty()
|
||||
|| !self.transform.is_identity()
|
||||
|| !math::is_close_to(self.rotation, 0.0)
|
||||
|| matches!(self.shape_type, Type::Group(_) | Type::Frame(_))
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user