Compare commits

..

1 Commits

Author SHA1 Message Date
Alejandro Alonso
ea0aa1fc1d :bug Fix empty or invalid tile 2026-01-14 14:38:31 +01:00
11 changed files with 237 additions and 489 deletions

View File

@@ -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

View File

@@ -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))))

View File

@@ -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)

View File

@@ -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)))))

View File

@@ -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
[]

View File

@@ -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))

View File

@@ -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?}]))])]))

View File

@@ -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)]

View File

@@ -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")]

View File

@@ -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);
}

View File

@@ -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(_))
}