mirror of
https://github.com/penpot/penpot.git
synced 2026-01-27 07:42:03 -05:00
Compare commits
4 Commits
alotor-was
...
staging-re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9808b6ca57 | ||
|
|
de41cb5488 | ||
|
|
b40ccaf030 | ||
|
|
7d3ac38749 |
@@ -124,33 +124,51 @@
|
||||
|
||||
(defn adjust-to-viewport
|
||||
([viewport srect] (adjust-to-viewport viewport srect nil))
|
||||
([viewport srect {:keys [padding] :or {padding 0}}]
|
||||
([viewport srect {:keys [padding min-zoom] :or {padding 0 min-zoom nil}}]
|
||||
(let [gprop (/ (:width viewport)
|
||||
(:height viewport))
|
||||
srect (-> srect
|
||||
(update :x #(- % padding))
|
||||
(update :y #(- % padding))
|
||||
(update :width #(+ % padding padding))
|
||||
(update :height #(+ % padding padding)))
|
||||
width (:width srect)
|
||||
height (:height srect)
|
||||
lprop (/ width height)]
|
||||
(cond
|
||||
(> gprop lprop)
|
||||
(let [width' (* (/ width lprop) gprop)
|
||||
padding (/ (- width' width) 2)]
|
||||
(-> srect
|
||||
(update :x #(- % padding))
|
||||
(assoc :width width')
|
||||
(grc/update-rect :position)))
|
||||
srect-padded (-> srect
|
||||
(update :x #(- % padding))
|
||||
(update :y #(- % padding))
|
||||
(update :width #(+ % padding padding))
|
||||
(update :height #(+ % padding padding)))
|
||||
width (:width srect-padded)
|
||||
height (:height srect-padded)
|
||||
lprop (/ width height)
|
||||
adjusted-rect
|
||||
(cond
|
||||
(> gprop lprop)
|
||||
(let [width' (* (/ width lprop) gprop)
|
||||
padding (/ (- width' width) 2)]
|
||||
(-> srect-padded
|
||||
(update :x #(- % padding))
|
||||
(assoc :width width')
|
||||
(grc/update-rect :position)))
|
||||
|
||||
(< gprop lprop)
|
||||
(let [height' (/ (* height lprop) gprop)
|
||||
padding (/ (- height' height) 2)]
|
||||
(-> srect
|
||||
(update :y #(- % padding))
|
||||
(assoc :height height')
|
||||
(grc/update-rect :position)))
|
||||
(< gprop lprop)
|
||||
(let [height' (/ (* height lprop) gprop)
|
||||
padding (/ (- height' height) 2)]
|
||||
(-> srect-padded
|
||||
(update :y #(- % padding))
|
||||
(assoc :height height')
|
||||
(grc/update-rect :position)))
|
||||
|
||||
:else
|
||||
(grc/update-rect srect :position)))))
|
||||
:else
|
||||
(grc/update-rect srect-padded :position))]
|
||||
;; If min-zoom is specified and the resulting zoom would be below it,
|
||||
;; return a rect with the original top-left corner centered in the viewport
|
||||
;; instead of using the aspect-ratio-adjusted rect (which can push coords
|
||||
;; extremely far with extreme aspect ratios).
|
||||
(if (and (some? min-zoom)
|
||||
(< (/ (:width viewport) (:width adjusted-rect)) min-zoom))
|
||||
(let [anchor-x (:x srect)
|
||||
anchor-y (:y srect)
|
||||
vbox-width (/ (:width viewport) min-zoom)
|
||||
vbox-height (/ (:height viewport) min-zoom)]
|
||||
(-> adjusted-rect
|
||||
(assoc :x (- anchor-x (/ vbox-width 2))
|
||||
:y (- anchor-y (/ vbox-height 2))
|
||||
:width vbox-width
|
||||
:height vbox-height)
|
||||
(grc/update-rect :position)))
|
||||
adjusted-rect))))
|
||||
|
||||
@@ -214,8 +214,8 @@
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [change-fn
|
||||
(fn [shape attrs]
|
||||
(update shape :fills types.fills/prepend attrs))
|
||||
(fn [node attrs]
|
||||
(update node :fills types.fills/prepend attrs))
|
||||
undo-id
|
||||
(js/Symbol)]
|
||||
(rx/concat
|
||||
|
||||
@@ -105,15 +105,9 @@
|
||||
(if (dsh/lookup-page state file-id page-id)
|
||||
(rx/concat
|
||||
(rx/of (initialize-page* file-id page-id)
|
||||
(fdf/fix-deleted-fonts-for-page file-id page-id))
|
||||
|
||||
;; Disable thumbnail generation in wasm renderer
|
||||
(if (features/active-feature? state "render-wasm/v1")
|
||||
(rx/empty)
|
||||
(rx/of (dwth/watch-state-changes file-id page-id)))
|
||||
|
||||
(rx/of (dwl/watch-component-changes))
|
||||
|
||||
(fdf/fix-deleted-fonts-for-page file-id page-id)
|
||||
(dwth/watch-state-changes file-id page-id)
|
||||
(dwl/watch-component-changes))
|
||||
(let [profile (:profile state)
|
||||
props (get profile :props)]
|
||||
(when (not (:workspace-visited props))
|
||||
|
||||
@@ -191,63 +191,59 @@
|
||||
[page-id [event [old-data new-data]]]
|
||||
|
||||
(let [changes (:changes event)
|
||||
;; cache for the get-frame-ids function
|
||||
frame-id-cache (atom {})]
|
||||
lookup-data-objects
|
||||
(fn [data page-id]
|
||||
(dm/get-in data [:pages-index page-id :objects]))
|
||||
|
||||
(letfn [(lookup-data-objects [data page-id]
|
||||
(dm/get-in data [:pages-index page-id :objects]))
|
||||
|
||||
(extract-ids [{:keys [page-id type] :as change}]
|
||||
(case type
|
||||
:add-obj [[page-id (:id change)]]
|
||||
:mod-obj [[page-id (:id change)]]
|
||||
:del-obj [[page-id (:id change)]]
|
||||
:mov-objects (->> (:shapes change) (map #(vector page-id %)))
|
||||
[]))
|
||||
extract-ids
|
||||
(fn [{:keys [page-id type] :as change}]
|
||||
(case type
|
||||
:add-obj [[page-id (:id change)]]
|
||||
:mod-obj [[page-id (:id change)]]
|
||||
:del-obj [[page-id (:id change)]]
|
||||
:mov-objects (->> (:shapes change) (map #(vector page-id %)))
|
||||
[]))
|
||||
|
||||
(get-frame-ids [id]
|
||||
(let [old-objects (lookup-data-objects old-data page-id)
|
||||
new-objects (lookup-data-objects new-data page-id)
|
||||
get-frame-ids
|
||||
(fn get-frame-ids [id]
|
||||
(let [old-objects (lookup-data-objects old-data page-id)
|
||||
new-objects (lookup-data-objects new-data page-id)
|
||||
|
||||
new-shape (get new-objects id)
|
||||
old-shape (get old-objects id)
|
||||
new-shape (get new-objects id)
|
||||
old-shape (get old-objects id)
|
||||
|
||||
old-frame-id (if (cfh/frame-shape? old-shape) id (:frame-id old-shape))
|
||||
new-frame-id (if (cfh/frame-shape? new-shape) id (:frame-id new-shape))
|
||||
old-frame-id (if (cfh/frame-shape? old-shape) id (:frame-id old-shape))
|
||||
new-frame-id (if (cfh/frame-shape? new-shape) id (:frame-id new-shape))
|
||||
|
||||
root-frame-old? (cfh/root-frame? old-objects old-frame-id)
|
||||
root-frame-new? (cfh/root-frame? new-objects new-frame-id)
|
||||
instance-root? (ctc/instance-root? new-shape)]
|
||||
root-frame-old? (cfh/root-frame? old-objects old-frame-id)
|
||||
root-frame-new? (cfh/root-frame? new-objects new-frame-id)
|
||||
instance-root? (ctc/instance-root? new-shape)]
|
||||
|
||||
(cond-> #{}
|
||||
root-frame-old?
|
||||
(conj ["frame" old-frame-id])
|
||||
(cond-> #{}
|
||||
root-frame-old?
|
||||
(conj ["frame" old-frame-id])
|
||||
|
||||
root-frame-new?
|
||||
(conj ["frame" new-frame-id])
|
||||
root-frame-new?
|
||||
(conj ["frame" new-frame-id])
|
||||
|
||||
instance-root?
|
||||
(conj ["component" id])
|
||||
instance-root?
|
||||
(conj ["component" id])
|
||||
|
||||
(and (uuid? (:frame-id old-shape))
|
||||
(not= uuid/zero (:frame-id old-shape)))
|
||||
(into (get-frame-ids (:frame-id old-shape)))
|
||||
(and (uuid? (:frame-id old-shape))
|
||||
(not= uuid/zero (:frame-id old-shape)))
|
||||
(into (get-frame-ids (:frame-id old-shape)))
|
||||
|
||||
(and (uuid? (:frame-id new-shape))
|
||||
(not= uuid/zero (:frame-id new-shape)))
|
||||
(into (get-frame-ids (:frame-id new-shape))))))
|
||||
(and (uuid? (:frame-id new-shape))
|
||||
(not= uuid/zero (:frame-id new-shape)))
|
||||
(into (get-frame-ids (:frame-id new-shape))))))]
|
||||
|
||||
(get-frame-ids-cached [id]
|
||||
(or (get @frame-id-cache id)
|
||||
(let [result (get-frame-ids id)]
|
||||
(swap! frame-id-cache assoc id result)
|
||||
result)))]
|
||||
(into #{}
|
||||
(comp (mapcat extract-ids)
|
||||
(filter (fn [[page-id']] (= page-id page-id')))
|
||||
(map (fn [[_ id]] id))
|
||||
(mapcat get-frame-ids-cached))
|
||||
changes))))
|
||||
(into #{}
|
||||
(comp (mapcat extract-ids)
|
||||
(filter (fn [[page-id']] (= page-id page-id')))
|
||||
(map (fn [[_ id]] id))
|
||||
(mapcat get-frame-ids))
|
||||
changes)))
|
||||
|
||||
(defn watch-state-changes
|
||||
"Watch the state for changes inside frames. If a change is detected will force a rendering
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
|
||||
(or (> (:width srect) width)
|
||||
(> (:height srect) height))
|
||||
(let [srect (gal/adjust-to-viewport size srect {:padding 40})
|
||||
(let [srect (gal/adjust-to-viewport size srect {:padding 40 :min-zoom 0.01})
|
||||
zoom (/ (:width size) (:width srect))]
|
||||
|
||||
(-> local
|
||||
|
||||
@@ -97,7 +97,7 @@
|
||||
state
|
||||
(update state :workspace-local
|
||||
(fn [{:keys [vport] :as local}]
|
||||
(let [srect (gal/adjust-to-viewport vport srect {:padding 160})
|
||||
(let [srect (gal/adjust-to-viewport vport srect {:padding 160 :min-zoom 0.01})
|
||||
zoom (/ (:width vport) (:width srect))]
|
||||
(-> local
|
||||
(assoc :zoom zoom)
|
||||
@@ -118,7 +118,7 @@
|
||||
(gsh/shapes->rect))]
|
||||
(update state :workspace-local
|
||||
(fn [{:keys [vport] :as local}]
|
||||
(let [srect (gal/adjust-to-viewport vport srect {:padding 40})
|
||||
(let [srect (gal/adjust-to-viewport vport srect {:padding 40 :min-zoom 0.01})
|
||||
zoom (/ (:width vport) (:width srect))]
|
||||
(-> local
|
||||
(assoc :zoom zoom)
|
||||
@@ -142,7 +142,7 @@
|
||||
(fn [{:keys [vport] :as local}]
|
||||
(let [srect (gal/adjust-to-viewport
|
||||
vport srect
|
||||
{:padding 40})
|
||||
{:padding 40 :min-zoom 0.01})
|
||||
zoom (/ (:width vport)
|
||||
(:width srect))]
|
||||
(-> local
|
||||
|
||||
@@ -238,7 +238,8 @@ export class SelectionController extends EventTarget {
|
||||
#applyStylesFromElementToCurrentStyle(element) {
|
||||
for (let index = 0; index < element.style.length; index++) {
|
||||
const styleName = element.style.item(index);
|
||||
if (styleName === "--fills") {
|
||||
// Only merge fill styles from text spans.
|
||||
if (!isTextSpan(element) && styleName === "--fills") {
|
||||
continue;
|
||||
}
|
||||
let styleValue = element.style.getPropertyValue(styleName);
|
||||
|
||||
@@ -275,29 +275,26 @@ pub extern "C" fn set_view_end() {
|
||||
state.render_state.options.set_fast_mode(false);
|
||||
state.render_state.cancel_animation_frame();
|
||||
|
||||
let zoom_changed = state.render_state.zoom_changed();
|
||||
// Only rebuild tile indices when zoom has changed.
|
||||
// During pan-only operations, shapes stay in the same tiles
|
||||
// because tile_size = 1/scale * TILE_SIZE (depends only on zoom).
|
||||
if zoom_changed {
|
||||
let _rebuild_start = performance::begin_timed_log!("rebuild_tiles");
|
||||
performance::begin_measure!("set_view_end::rebuild_tiles");
|
||||
if state.render_state.options.is_profile_rebuild_tiles() {
|
||||
state.rebuild_tiles();
|
||||
} else {
|
||||
state.rebuild_tiles_shallow();
|
||||
}
|
||||
performance::end_measure!("set_view_end::rebuild_tiles");
|
||||
performance::end_timed_log!("rebuild_tiles", _rebuild_start);
|
||||
// Update tile_viewbox first so that get_tiles_for_shape uses the correct interest area
|
||||
// This is critical because we limit tiles to the interest area for optimization
|
||||
let scale = state.render_state.get_scale();
|
||||
state
|
||||
.render_state
|
||||
.tile_viewbox
|
||||
.update(state.render_state.viewbox, scale);
|
||||
|
||||
// We rebuild the tile index on both pan and zoom because `get_tiles_for_shape`
|
||||
// clips each shape to the current `TileViewbox::interest_rect` (viewport-dependent).
|
||||
let _rebuild_start = performance::begin_timed_log!("rebuild_tiles");
|
||||
performance::begin_measure!("set_view_end::rebuild_tiles");
|
||||
if state.render_state.options.is_profile_rebuild_tiles() {
|
||||
state.rebuild_tiles();
|
||||
} else {
|
||||
// During pan, we only clear the tile index without
|
||||
// invalidating cached textures, which is more efficient.
|
||||
let _clear_start = performance::begin_timed_log!("clear_tile_index");
|
||||
performance::begin_measure!("set_view_end::clear_tile_index");
|
||||
state.clear_tile_index();
|
||||
performance::end_measure!("set_view_end::clear_tile_index");
|
||||
performance::end_timed_log!("clear_tile_index", _clear_start);
|
||||
state.rebuild_tiles_shallow();
|
||||
}
|
||||
performance::end_measure!("set_view_end::rebuild_tiles");
|
||||
performance::end_timed_log!("rebuild_tiles", _rebuild_start);
|
||||
|
||||
state.render_state.sync_cached_viewbox();
|
||||
performance::end_measure!("set_view_end");
|
||||
performance::end_timed_log!("set_view_end", _end_start);
|
||||
|
||||
@@ -1168,7 +1168,6 @@ impl RenderState {
|
||||
let scale = self.get_scale();
|
||||
|
||||
self.tile_viewbox.update(self.viewbox, scale);
|
||||
|
||||
self.focus_mode.reset();
|
||||
|
||||
performance::begin_measure!("render");
|
||||
@@ -2111,13 +2110,44 @@ impl RenderState {
|
||||
}
|
||||
|
||||
/*
|
||||
* Given a shape returns the TileRect with the range of tiles that the shape is in
|
||||
* Given a shape returns the TileRect with the range of tiles that the shape is in.
|
||||
* This is always limited to the interest area to optimize performance and prevent
|
||||
* processing unnecessary tiles outside the viewport. The interest area already
|
||||
* includes a margin (VIEWPORT_INTEREST_AREA_THRESHOLD) calculated via
|
||||
* get_tiles_for_viewbox_with_interest, ensuring smooth pan/zoom interactions.
|
||||
*
|
||||
* When the viewport changes (pan/zoom), the interest area is updated and shapes
|
||||
* are dynamically added to the tile index via the fallback mechanism in
|
||||
* render_shape_tree_partial_uncached, ensuring all shapes render correctly.
|
||||
*/
|
||||
pub fn get_tiles_for_shape(&mut self, shape: &Shape, tree: ShapesPoolRef) -> TileRect {
|
||||
let scale = self.get_scale();
|
||||
let extrect = self.get_cached_extrect(shape, tree, scale);
|
||||
let tile_size = tiles::get_tile_size(scale);
|
||||
tiles::get_tiles_for_rect(extrect, tile_size)
|
||||
let shape_tiles = tiles::get_tiles_for_rect(extrect, tile_size);
|
||||
let interest_rect = &self.tile_viewbox.interest_rect;
|
||||
// Calculate the intersection of shape_tiles with interest_rect
|
||||
// This returns only the tiles that are both in the shape and in the interest area
|
||||
let intersection_x1 = shape_tiles.x1().max(interest_rect.x1());
|
||||
let intersection_y1 = shape_tiles.y1().max(interest_rect.y1());
|
||||
let intersection_x2 = shape_tiles.x2().min(interest_rect.x2());
|
||||
let intersection_y2 = shape_tiles.y2().min(interest_rect.y2());
|
||||
|
||||
// Return the intersection if valid (there is overlap), otherwise return empty rect
|
||||
if intersection_x1 <= intersection_x2 && intersection_y1 <= intersection_y2 {
|
||||
// Valid intersection: return the tiles that are in both shape_tiles and interest_rect
|
||||
TileRect(
|
||||
intersection_x1,
|
||||
intersection_y1,
|
||||
intersection_x2,
|
||||
intersection_y2,
|
||||
)
|
||||
} else {
|
||||
// No intersection: shape is completely outside interest area
|
||||
// The shape will be added dynamically via add_shape_tiles when it enters
|
||||
// the interest area during pan/zoom operations
|
||||
TileRect(0, 0, -1, -1)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -2198,17 +2228,6 @@ impl RenderState {
|
||||
performance::end_measure!("rebuild_tiles_shallow");
|
||||
}
|
||||
|
||||
/// Clears the tile index without invalidating cached tile textures.
|
||||
/// This is useful when tile positions don't change (e.g., during pan operations)
|
||||
/// but the tile index needs to be synchronized. The cached tile textures remain
|
||||
/// valid since they don't depend on the current view position, only on zoom level.
|
||||
/// This is much more efficient than clearing the entire cache surface.
|
||||
pub fn clear_tile_index(&mut self) {
|
||||
performance::begin_measure!("clear_tile_index");
|
||||
self.surfaces.clear_tiles();
|
||||
performance::end_measure!("clear_tile_index");
|
||||
}
|
||||
|
||||
pub fn rebuild_tiles_from(&mut self, tree: ShapesPoolRef, base_id: Option<&Uuid>) {
|
||||
performance::begin_measure!("rebuild_tiles");
|
||||
|
||||
|
||||
@@ -207,10 +207,6 @@ impl State {
|
||||
self.render_state.rebuild_tiles_shallow(&self.shapes);
|
||||
}
|
||||
|
||||
pub fn clear_tile_index(&mut self) {
|
||||
self.render_state.clear_tile_index();
|
||||
}
|
||||
|
||||
pub fn rebuild_tiles(&mut self) {
|
||||
self.render_state.rebuild_tiles_from(&self.shapes, None);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user