Merge pull request #8155 from penpot/elenatorro-13089-improve-page-load-render

🔧 Improve render UX on first load
This commit is contained in:
Alejandro Alonso 2026-01-26 10:40:44 +01:00 committed by GitHub
commit 3dc9e28230
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 752 additions and 292 deletions

View File

@ -28,6 +28,7 @@
[app.main.ui.shapes.text]
[app.main.worker :as mw]
[app.render-wasm.api.fonts :as f]
[app.render-wasm.api.shapes :as shapes]
[app.render-wasm.api.texts :as t]
[app.render-wasm.api.webgl :as webgl]
[app.render-wasm.deserializers :as dr]
@ -68,12 +69,25 @@
(def ^:const DEBOUNCE_DELAY_MS 100)
(def ^:const THROTTLE_DELAY_MS 10)
;; Number of shapes to process before yielding to browser
(def ^:const SHAPES_CHUNK_SIZE 100)
;; Threshold below which we use synchronous processing (no chunking overhead)
(def ^:const ASYNC_THRESHOLD 100)
(def dpr
(if use-dpr? (if (exists? js/window) js/window.devicePixelRatio 1.0) 1.0))
(def noop-fn
(constantly nil))
(defn- yield-to-browser
"Returns a promise that resolves after yielding to the browser's event loop.
Uses requestAnimationFrame for smooth visual updates during loading."
[]
(p/create
(fn [resolve _reject]
(js/requestAnimationFrame (fn [_] (resolve nil))))))
;; Based on app.main.render/object-svg
(mf/defc object-svg
{::mf/props :obj}
@ -120,17 +134,56 @@
(aget buffer 3))
(set! wasm/internal-frame-id nil))))
(defn render-preview!
"Render a lightweight preview without tile caching.
Used during progressive loading for fast feedback."
[]
(when (and wasm/context-initialized? (not @wasm/context-lost?))
(h/call wasm/internal-module "_render_preview")))
(defonce pending-render (atom false))
(defonce shapes-loading? (atom false))
(defonce deferred-render? (atom false))
(defn- register-deferred-render!
[]
(reset! deferred-render? true))
(defn request-render
[_requester]
(when (and wasm/context-initialized? (not @pending-render) (not @wasm/context-lost?))
(reset! pending-render true)
(js/requestAnimationFrame
(fn [ts]
(reset! pending-render false)
(render ts)))))
(when (and wasm/context-initialized? (not @wasm/context-lost?))
(if @shapes-loading?
(register-deferred-render!)
(when-not @pending-render
(reset! pending-render true)
(let [frame-id
(js/requestAnimationFrame
(fn [ts]
(reset! pending-render false)
(set! wasm/internal-frame-id nil)
(render ts)))]
(set! wasm/internal-frame-id frame-id))))))
(defn- begin-shapes-loading!
[]
(reset! shapes-loading? true)
(let [frame-id wasm/internal-frame-id
was-pending @pending-render]
(when frame-id
(js/cancelAnimationFrame frame-id)
(set! wasm/internal-frame-id nil))
(reset! pending-render false)
(reset! deferred-render? was-pending)))
(defn- end-shapes-loading!
[]
(let [was-loading (compare-and-set! shapes-loading? true false)]
(reset! deferred-render? false)
;; Always trigger a render after loading completes
;; This ensures shapes are displayed even if no deferred render was requested
(when was-loading
(request-render "set-objects:flush"))))
(declare get-text-dimensions)
@ -895,24 +948,12 @@
id (dm/get-prop shape :id)
type (dm/get-prop shape :type)
parent-id (get shape :parent-id)
masked (get shape :masked-group)
selrect (get shape :selrect)
constraint-h (get shape :constraints-h)
constraint-v (get shape :constraints-v)
clip-content (if (= type :frame)
(not (get shape :show-content))
false)
rotation (get shape :rotation)
transform (get shape :transform)
fills (get shape :fills)
strokes (if (= type :group)
[] (get shape :strokes))
children (get shape :shapes)
blend-mode (get shape :blend-mode)
opacity (get shape :opacity)
hidden (get shape :hidden)
content (let [content (get shape :content)]
(if (= type :text)
(ensure-text-content content)
@ -921,22 +962,12 @@
grow-type (get shape :grow-type)
blur (get shape :blur)
svg-attrs (get shape :svg-attrs)
shadows (get shape :shadow)
corners (map #(get shape %) [:r1 :r2 :r3 :r4])]
shadows (get shape :shadow)]
(use-shape id)
(set-parent-id parent-id)
(set-shape-type type)
(set-shape-clip-content clip-content)
(set-shape-constraints constraint-h constraint-v)
(shapes/set-shape-base-props shape)
(set-shape-rotation rotation)
(set-shape-transform transform)
(set-shape-blend-mode blend-mode)
(set-shape-opacity opacity)
(set-shape-hidden hidden)
;; Remaining properties that need separate calls (variable-length or conditional)
(set-shape-children children)
(set-shape-corners corners)
(set-shape-blur blur)
(when (= type :group)
(set-masked (boolean masked)))
@ -956,7 +987,6 @@
(set-shape-layout shape)
(set-layout-data shape)
(set-shape-selrect selrect)
(let [pending_thumbnails (into [] (concat
(set-shape-text-content id content)
@ -1012,30 +1042,143 @@
(let [{:keys [thumbnails full]} (set-object shape)]
(process-pending [shape] thumbnails full noop-fn)))
(defn- process-shapes-chunk
"Process a chunk of shapes synchronously, returning accumulated pending operations.
Returns {:thumbnails [...] :full [...] :next-index n}"
[shapes start-index chunk-size thumbnails-acc full-acc]
(let [total (count shapes)
end-index (min total (+ start-index chunk-size))]
(loop [index start-index
t-acc thumbnails-acc
f-acc full-acc]
(if (< index end-index)
(let [shape (nth shapes index)
{:keys [thumbnails full]} (set-object shape)]
(recur (inc index)
(into t-acc thumbnails)
(into f-acc full)))
{:thumbnails t-acc
:full f-acc
:next-index end-index}))))
(defn- set-objects-async
"Asynchronously process shapes in chunks, yielding to the browser between chunks.
Returns a promise that resolves when all shapes are processed.
Renders a preview only periodically during loading to show progress,
then does a full tile-based render at the end."
[shapes render-callback]
(let [total-shapes (count shapes)
total-chunks (mth/ceil (/ total-shapes SHAPES_CHUNK_SIZE))
;; Render at 25%, 50%, 75% of loading
render-at-chunks (set [(mth/floor (* total-chunks 0.25))
(mth/floor (* total-chunks 0.5))
(mth/floor (* total-chunks 0.75))])]
(p/create
(fn [resolve _reject]
(letfn [(process-next-chunk [index thumbnails-acc full-acc chunk-count]
(if (< index total-shapes)
;; Process one chunk
(let [{:keys [thumbnails full next-index]}
(process-shapes-chunk shapes index SHAPES_CHUNK_SIZE
thumbnails-acc full-acc)
new-chunk-count (inc chunk-count)]
;; Only render at specific progress milestones
(when (contains? render-at-chunks new-chunk-count)
(render-preview!))
;; Yield to browser, then continue with next chunk
(-> (yield-to-browser)
(p/then (fn [_]
(process-next-chunk next-index thumbnails full new-chunk-count)))))
;; All chunks done - finalize
(do
(perf/end-measure "set-objects")
(process-pending shapes thumbnails-acc full-acc noop-fn
(fn []
(end-shapes-loading!)
(if render-callback
(render-callback)
(render-finish))
(ug/dispatch! (ug/event "penpot:wasm:set-objects"))
(resolve nil))))))]
(process-next-chunk 0 [] [] 0))))))
(defn- set-objects-sync
"Synchronously process all shapes (for small shape counts)."
[shapes render-callback]
(let [total-shapes (count shapes)
{:keys [thumbnails full]}
(loop [index 0 thumbnails-acc [] full-acc []]
(if (< index total-shapes)
(let [shape (nth shapes index)
{:keys [thumbnails full]} (set-object shape)]
(recur (inc index)
(into thumbnails-acc thumbnails)
(into full-acc full)))
{:thumbnails thumbnails-acc :full full-acc}))]
(perf/end-measure "set-objects")
(process-pending shapes thumbnails full noop-fn
(fn []
(if render-callback
(render-callback)
(render-finish))
(ug/dispatch! (ug/event "penpot:wasm:set-objects"))))))
(defn- shapes-in-tree-order
"Returns shapes sorted in tree order (parents before children).
This ensures parent shapes are processed before their children,
maintaining proper shape reference consistency in WASM."
[objects]
;; Get IDs in tree order starting from root (uuid/zero)
;; If root doesn't exist (e.g., filtered thumbnail data), fall back to
;; finding top-level shapes (those without a parent in objects) and
;; traversing from there.
(if (contains? objects uuid/zero)
;; Normal case: traverse from root
(let [ordered-ids (cfh/get-children-ids-with-self objects uuid/zero)]
(into []
(keep #(get objects %))
ordered-ids))
;; Fallback for filtered data (thumbnails): find top-level shapes and traverse
(let [;; Find shapes whose parent is not in the objects map (top-level in this subset)
top-level-ids (->> (vals objects)
(filter (fn [shape]
(not (contains? objects (:parent-id shape)))))
(map :id))
;; Get all children in order for each top-level shape
all-ordered-ids (into []
(mapcat #(cfh/get-children-ids-with-self objects %))
top-level-ids)]
(into []
(keep #(get objects %))
all-ordered-ids))))
(defn set-objects
"Set all shape objects for rendering.
Shapes are processed in tree order (parents before children)
to maintain proper shape reference consistency in WASM."
([objects]
(set-objects objects nil))
([objects render-callback]
(perf/begin-measure "set-objects")
(let [shapes (into [] (vals objects))
total-shapes (count shapes)
;; Collect pending operations - set-object returns {:thumbnails [...] :full [...]}
{:keys [thumbnails full]}
(loop [index 0 thumbnails-acc [] full-acc []]
(if (< index total-shapes)
(let [shape (nth shapes index)
{:keys [thumbnails full]} (set-object shape)]
(recur (inc index)
(into thumbnails-acc thumbnails)
(into full-acc full)))
{:thumbnails thumbnails-acc :full full-acc}))]
(perf/end-measure "set-objects")
(process-pending shapes thumbnails full noop-fn
(fn []
(if render-callback
(render-callback)
(render-finish))
(ug/dispatch! (ug/event "penpot:wasm:set-objects")))))))
(let [shapes (shapes-in-tree-order objects)
total-shapes (count shapes)]
(if (< total-shapes ASYNC_THRESHOLD)
(set-objects-sync shapes render-callback)
(do
(begin-shapes-loading!)
(try
(-> (set-objects-async shapes render-callback)
(p/catch (fn [error]
(end-shapes-loading!)
(js/console.error "Async WASM shape loading failed" error))))
(catch :default error
(end-shapes-loading!)
(js/console.error "Async WASM shape loading failed" error)
(throw error)))
nil)))))
(defn clear-focus-mode
[]

View File

@ -0,0 +1,193 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.render-wasm.api.shapes
"Batched shape property serialization for improved WASM performance.
This module provides a single WASM call to set all base shape properties,
replacing multiple individual calls (use_shape, set_parent, set_shape_type,
etc.) with one batched operation."
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.uuid :as uuid]
[app.render-wasm.helpers :as h]
[app.render-wasm.mem :as mem]
[app.render-wasm.serializers :as sr]
[app.render-wasm.wasm :as wasm]))
;; Binary layout constants matching Rust implementation:
;;
;; | Offset | Size | Field | Type |
;; |--------|------|--------------|-----------------------------------|
;; | 0 | 16 | id | UUID (4 × u32 LE) |
;; | 16 | 16 | parent_id | UUID (4 × u32 LE) |
;; | 32 | 1 | shape_type | u8 |
;; | 33 | 1 | flags | u8 (bit0: clip, bit1: hidden) |
;; | 34 | 1 | blend_mode | u8 |
;; | 35 | 1 | constraint_h | u8 (0xFF = None) |
;; | 36 | 1 | constraint_v | u8 (0xFF = None) |
;; | 37 | 3 | padding | - |
;; | 40 | 4 | opacity | f32 LE |
;; | 44 | 4 | rotation | f32 LE |
;; | 48 | 24 | transform | 6 × f32 LE (a,b,c,d,e,f) |
;; | 72 | 16 | selrect | 4 × f32 LE (x1,y1,x2,y2) |
;; | 88 | 16 | corners | 4 × f32 LE (r1,r2,r3,r4) |
;; |--------|------|--------------|-----------------------------------|
;; | Total | 104 | | |
(def ^:const BASE-PROPS-SIZE 104)
(def ^:const FLAG-CLIP-CONTENT 0x01)
(def ^:const FLAG-HIDDEN 0x02)
(def ^:const CONSTRAINT-NONE 0xFF)
(defn- write-uuid-to-heap
"Write a UUID to the heap at the given byte offset using DataView."
[dview offset id]
(let [buffer (uuid/get-u32 id)]
(.setUint32 dview offset (aget buffer 0) true)
(.setUint32 dview (+ offset 4) (aget buffer 1) true)
(.setUint32 dview (+ offset 8) (aget buffer 2) true)
(.setUint32 dview (+ offset 12) (aget buffer 3) true)))
(defn- serialize-transform
"Extract transform matrix values, defaulting to identity matrix."
[transform]
(if (some? transform)
[(dm/get-prop transform :a)
(dm/get-prop transform :b)
(dm/get-prop transform :c)
(dm/get-prop transform :d)
(dm/get-prop transform :e)
(dm/get-prop transform :f)]
[1.0 0.0 0.0 1.0 0.0 0.0])) ; identity matrix
(defn- serialize-selrect
"Extract selrect values."
[selrect]
(if (some? selrect)
[(dm/get-prop selrect :x1)
(dm/get-prop selrect :y1)
(dm/get-prop selrect :x2)
(dm/get-prop selrect :y2)]
[0.0 0.0 0.0 0.0]))
(defn set-shape-base-props
"Set all base shape properties in a single WASM call.
This replaces the following individual calls:
- use-shape
- set-parent-id
- set-shape-type
- set-shape-clip-content
- set-shape-rotation
- set-shape-transform
- set-shape-blend-mode
- set-shape-opacity
- set-shape-hidden
- set-shape-selrect
- set-shape-corners
- set-shape-constraints (clear + h + v)
Returns nil."
[shape]
(when wasm/context-initialized?
(let [id (dm/get-prop shape :id)
parent-id (get shape :parent-id)
shape-type (dm/get-prop shape :type)
clip-content (if (= shape-type :frame)
(not (get shape :show-content))
false)
hidden (get shape :hidden false)
flags (cond-> 0
clip-content (bit-or FLAG-CLIP-CONTENT)
hidden (bit-or FLAG-HIDDEN))
blend-mode (sr/translate-blend-mode (get shape :blend-mode))
constraint-h (let [c (get shape :constraints-h)]
(if (some? c)
(sr/translate-constraint-h c)
CONSTRAINT-NONE))
constraint-v (let [c (get shape :constraints-v)]
(if (some? c)
(sr/translate-constraint-v c)
CONSTRAINT-NONE))
opacity (d/nilv (get shape :opacity) 1.0)
rotation (d/nilv (get shape :rotation) 0.0)
;; Transform matrix
[ta tb tc td te tf] (serialize-transform (get shape :transform))
;; Selrect
selrect (get shape :selrect)
[sx1 sy1 sx2 sy2] (serialize-selrect selrect)
;; Corners
r1 (d/nilv (get shape :r1) 0.0)
r2 (d/nilv (get shape :r2) 0.0)
r3 (d/nilv (get shape :r3) 0.0)
r4 (d/nilv (get shape :r4) 0.0)
;; Allocate buffer and get DataView
offset (mem/alloc BASE-PROPS-SIZE)
heap (mem/get-heap-u8)
dview (js/DataView. (.-buffer heap))]
;; Write id (offset 0, 16 bytes)
(write-uuid-to-heap dview offset id)
;; Write parent_id (offset 16, 16 bytes)
(write-uuid-to-heap dview (+ offset 16) (d/nilv parent-id uuid/zero))
;; Write shape_type (offset 32, 1 byte)
(.setUint8 dview (+ offset 32) (sr/translate-shape-type shape-type))
;; Write flags (offset 33, 1 byte)
(.setUint8 dview (+ offset 33) flags)
;; Write blend_mode (offset 34, 1 byte)
(.setUint8 dview (+ offset 34) blend-mode)
;; Write constraint_h (offset 35, 1 byte)
(.setUint8 dview (+ offset 35) constraint-h)
;; Write constraint_v (offset 36, 1 byte)
(.setUint8 dview (+ offset 36) constraint-v)
;; Padding at offset 37-39 (already zero from alloc)
;; Write opacity (offset 40, f32)
(.setFloat32 dview (+ offset 40) opacity true)
;; Write rotation (offset 44, f32)
(.setFloat32 dview (+ offset 44) rotation true)
;; Write transform matrix (offset 48, 6 × f32)
(.setFloat32 dview (+ offset 48) ta true)
(.setFloat32 dview (+ offset 52) tb true)
(.setFloat32 dview (+ offset 56) tc true)
(.setFloat32 dview (+ offset 60) td true)
(.setFloat32 dview (+ offset 64) te true)
(.setFloat32 dview (+ offset 68) tf true)
;; Write selrect (offset 72, 4 × f32)
(.setFloat32 dview (+ offset 72) sx1 true)
(.setFloat32 dview (+ offset 76) sy1 true)
(.setFloat32 dview (+ offset 80) sx2 true)
(.setFloat32 dview (+ offset 84) sy2 true)
;; Write corners (offset 88, 4 × f32)
(.setFloat32 dview (+ offset 88) r1 true)
(.setFloat32 dview (+ offset 92) r2 true)
(.setFloat32 dview (+ offset 96) r3 true)
(.setFloat32 dview (+ offset 100) r4 true)
(h/call wasm/internal-module "_set_shape_base_props")
nil)))

View File

@ -23,7 +23,7 @@ use std::collections::HashMap;
use utils::uuid_from_u32_quartet;
use uuid::Uuid;
pub(crate) static mut STATE: Option<Box<State<'static>>> = None;
pub(crate) static mut STATE: Option<Box<State>> = None;
#[macro_export]
macro_rules! with_state_mut {
@ -191,6 +191,20 @@ pub extern "C" fn render_from_cache(_: i32) {
});
}
#[no_mangle]
pub extern "C" fn set_preview_mode(enabled: bool) {
with_state_mut!(state, {
state.render_state.set_preview_mode(enabled);
});
}
#[no_mangle]
pub extern "C" fn render_preview() {
with_state_mut!(state, {
state.render_preview(performance::get_time());
});
}
#[no_mangle]
pub extern "C" fn process_animation_frame(timestamp: i32) {
let result = std::panic::catch_unwind(|| {

View File

@ -294,6 +294,8 @@ 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,
/// Preview render mode - when true, uses simplified rendering for progressive loading
pub preview_mode: bool,
}
pub fn get_cache_size(viewbox: Viewbox, scale: f32) -> skia::ISize {
@ -366,6 +368,7 @@ impl RenderState {
focus_mode: FocusMode::new(),
touched_ids: HashSet::default(),
ignore_nested_blurs: false,
preview_mode: false,
}
}
@ -486,6 +489,10 @@ impl RenderState {
self.background_color = color;
}
pub fn set_preview_mode(&mut self, enabled: bool) {
self.preview_mode = enabled;
}
pub fn resize(&mut self, width: i32, height: i32) {
let dpr_width = (width as f32 * self.options.dpr()).floor() as i32;
let dpr_height = (height as f32 * self.options.dpr()).floor() as i32;
@ -1127,6 +1134,25 @@ impl RenderState {
performance::end_timed_log!("render_from_cache", _start);
}
/// Render a preview of the shapes during loading.
/// This rebuilds tiles for touched shapes and renders synchronously.
pub fn render_preview(&mut self, tree: ShapesPoolRef, timestamp: i32) -> Result<(), String> {
let _start = performance::begin_timed_log!("render_preview");
performance::begin_measure!("render_preview");
// Skip tile rebuilding during preview - we'll do it at the end
// Just rebuild tiles for touched shapes and render synchronously
self.rebuild_touched_tiles(tree);
// Use the sync render path
self.start_render_loop(None, tree, timestamp, true)?;
performance::end_measure!("render_preview");
performance::end_timed_log!("render_preview", _start);
Ok(())
}
pub fn start_render_loop(
&mut self,
base_object: Option<&Uuid>,
@ -1622,10 +1648,11 @@ impl RenderState {
is_empty = false;
let element = tree.get(&node_id).ok_or(format!(
"Error: Element with root_id {} not found in the tree.",
node_render_state.id
))?;
let Some(element) = tree.get(&node_id) else {
// The shape isn't available yet (likely still streaming in from WASM).
// Skip it for this pass; a subsequent render will pick it up once present.
continue;
};
let scale = self.get_scale();
let mut extrect: Option<Rect> = None;
@ -1743,7 +1770,9 @@ impl RenderState {
if !matches!(element.shape_type, Type::Bool(_)) {
// Nested shapes shadowing - apply black shadow to child shapes too
for shadow_shape_id in element.children.iter() {
let shadow_shape = tree.get(shadow_shape_id).unwrap();
let Some(shadow_shape) = tree.get(shadow_shape_id) else {
continue;
};
if shadow_shape.hidden {
continue;
}
@ -2141,9 +2170,7 @@ impl RenderState {
}
pub fn remove_cached_tile(&mut self, tile: tiles::Tile) {
let rect = self.get_aligned_tile_bounds(tile);
self.surfaces
.remove_cached_tile_surface(tile, rect, self.background_color);
self.surfaces.remove_cached_tile_surface(tile);
}
pub fn rebuild_tiles_shallow(&mut self, tree: ShapesPoolRef) {
@ -2164,7 +2191,7 @@ impl RenderState {
}
}
// Update the changed tiles
// Invalidate changed tiles - old content stays visible until new tiles render
self.surfaces.remove_cached_tiles(self.background_color);
for tile in all_tiles {
self.remove_cached_tile(tile);
@ -2211,7 +2238,7 @@ impl RenderState {
}
}
// Update the changed tiles
// Invalidate changed tiles - old content stays visible until new tiles render
self.surfaces.remove_cached_tiles(self.background_color);
for tile in all_tiles {
self.remove_cached_tile(tile);

View File

@ -401,11 +401,10 @@ impl Surfaces {
self.tiles.has(tile)
}
pub fn remove_cached_tile_surface(&mut self, tile: Tile, rect: skia::Rect, color: skia::Color) {
// Clear the specific tile area in the cache surface with color
let mut paint = skia::Paint::default();
paint.set_color(color);
self.cache.canvas().draw_rect(rect, &paint);
pub fn remove_cached_tile_surface(&mut self, tile: Tile) {
// Mark tile as invalid
// Old content stays visible until new tile overwrites it atomically,
// preventing flickering during tile re-renders.
self.tiles.remove(tile);
}

View File

@ -18,16 +18,16 @@ use crate::shapes::modifiers::grid_layout::grid_cell_data;
/// It is created by [init] and passed to the other exported functions.
/// Note that rust-skia data structures are not thread safe, so a state
/// must not be shared between different Web Workers.
pub(crate) struct State<'a> {
pub(crate) struct State {
pub render_state: RenderState,
pub text_editor_state: TextEditorState,
pub current_id: Option<Uuid>,
pub current_browser: u8,
pub shapes: ShapesPool<'a>,
pub saved_shapes: Option<ShapesPool<'a>>,
pub shapes: ShapesPool,
pub saved_shapes: Option<ShapesPool>,
}
impl<'a> State<'a> {
impl State {
pub fn new(width: i32, height: i32) -> Self {
State {
render_state: RenderState::new(width, height),
@ -223,17 +223,14 @@ impl<'a> State<'a> {
self.render_state.rebuild_touched_tiles(&self.shapes);
}
pub fn render_preview(&mut self, timestamp: i32) {
let _ = self.render_state.render_preview(&self.shapes, timestamp);
}
pub fn rebuild_modifier_tiles(&mut self, ids: Vec<Uuid>) {
// SAFETY: We're extending the lifetime of the mutable borrow to 'a.
// This is safe because:
// 1. shapes has lifetime 'a in the struct
// 2. The reference won't outlive the struct
// 3. No other references to shapes exist during this call
unsafe {
let shapes_ptr = &mut self.shapes as *mut ShapesPool<'a>;
self.render_state
.rebuild_modifier_tiles(&mut *shapes_ptr, ids);
}
// Index-based storage is safe
self.render_state
.rebuild_modifier_tiles(&mut self.shapes, ids);
}
pub fn font_collection(&self) -> &FontCollection {

View File

@ -28,29 +28,44 @@ const SHAPES_POOL_ALLOC_MULTIPLIER: f32 = 1.3;
/// Shapes are stored in a `Vec<Shape>`, which keeps the `Shape` instances
/// in a contiguous memory block.
///
pub struct ShapesPoolImpl<'a> {
/// # Index-based Design
///
/// All auxiliary HashMaps (modifiers, structure, scale_content, modified_shape_cache)
/// use `usize` indices instead of `&'a Uuid` references. This eliminates:
/// - Unsafe lifetime extensions
/// - The need for `rebuild_references()` after Vec reallocation
/// - Complex lifetime annotations
///
/// The `uuid_to_idx` HashMap maps `Uuid` (owned) to indices, avoiding lifetime issues.
///
pub struct ShapesPoolImpl {
shapes: Vec<Shape>,
counter: usize,
shapes_uuid_to_idx: HashMap<&'a Uuid, usize>,
/// Maps UUID to index in the shapes Vec. Uses owned Uuid, no lifetime needed.
uuid_to_idx: HashMap<Uuid, usize>,
modified_shape_cache: HashMap<&'a Uuid, OnceCell<Shape>>,
modifiers: HashMap<&'a Uuid, skia::Matrix>,
structure: HashMap<&'a Uuid, Vec<StructureEntry>>,
scale_content: HashMap<&'a Uuid, f32>,
/// Cache for modified shapes, keyed by index
modified_shape_cache: HashMap<usize, OnceCell<Shape>>,
/// Transform modifiers, keyed by index
modifiers: HashMap<usize, skia::Matrix>,
/// Structure entries, keyed by index
structure: HashMap<usize, Vec<StructureEntry>>,
/// Scale content values, keyed by index
scale_content: HashMap<usize, f32>,
}
// Type aliases to avoid writing lifetimes everywhere
pub type ShapesPool<'a> = ShapesPoolImpl<'a>;
pub type ShapesPoolRef<'a> = &'a ShapesPoolImpl<'a>;
pub type ShapesPoolMutRef<'a> = &'a mut ShapesPoolImpl<'a>;
// Type aliases - no longer need lifetimes!
pub type ShapesPool = ShapesPoolImpl;
pub type ShapesPoolRef<'a> = &'a ShapesPoolImpl;
pub type ShapesPoolMutRef<'a> = &'a mut ShapesPoolImpl;
impl<'a> ShapesPoolImpl<'a> {
impl ShapesPoolImpl {
pub fn new() -> Self {
ShapesPoolImpl {
shapes: vec![],
counter: 0,
shapes_uuid_to_idx: HashMap::default(),
uuid_to_idx: HashMap::default(),
modified_shape_cache: HashMap::default(),
modifiers: HashMap::default(),
@ -62,15 +77,14 @@ impl<'a> ShapesPoolImpl<'a> {
pub fn initialize(&mut self, capacity: usize) {
performance::begin_measure!("shapes_pool_initialize");
self.counter = 0;
self.shapes_uuid_to_idx = HashMap::with_capacity(capacity);
self.uuid_to_idx = HashMap::with_capacity(capacity);
let additional = capacity as i32 - self.shapes.len() as i32;
if additional <= 0 {
return;
}
// Reserve exact capacity to avoid any future reallocations
// This is critical because we store &'a Uuid references that would be invalidated
// Reserve extra capacity to avoid future reallocations
let target_capacity = (capacity as f32 * SHAPES_POOL_ALLOC_MULTIPLIER) as usize;
self.shapes
.reserve_exact(target_capacity.saturating_sub(self.shapes.len()));
@ -81,15 +95,15 @@ impl<'a> ShapesPoolImpl<'a> {
}
pub fn add_shape(&mut self, id: Uuid) -> &mut Shape {
let did_reallocate = if self.counter >= self.shapes.len() {
// We need more space. Check if we'll need to reallocate the Vec.
if self.counter >= self.shapes.len() {
// We need more space
let current_capacity = self.shapes.capacity();
let additional = (self.shapes.len() as f32 * SHAPES_POOL_ALLOC_MULTIPLIER) as usize;
// Ensure we add at least 1 shape when the pool is empty
let additional =
((self.shapes.len() as f32 * SHAPES_POOL_ALLOC_MULTIPLIER) as usize).max(1);
let needed_capacity = self.shapes.len() + additional;
let will_reallocate = needed_capacity > current_capacity;
if will_reallocate {
if needed_capacity > current_capacity {
// Reserve extra space to minimize future reallocations
let extra_reserve = (needed_capacity as f32 * 0.5) as usize;
self.shapes
@ -98,165 +112,68 @@ impl<'a> ShapesPoolImpl<'a> {
self.shapes
.extend(iter::repeat_with(|| Shape::new(Uuid::nil())).take(additional));
will_reallocate
} else {
false
};
}
let idx = self.counter;
let new_shape = &mut self.shapes[idx];
new_shape.id = id;
// Get a reference to the id field in the shape with lifetime 'a
// SAFETY: This is safe because:
// 1. We pre-allocate enough capacity to avoid Vec reallocation
// 2. The shape and its id field won't move within the Vec
// 3. The reference won't outlive the ShapesPoolImpl
let id_ref: &'a Uuid = unsafe { &*(&self.shapes[idx].id as *const Uuid) };
self.shapes_uuid_to_idx.insert(id_ref, idx);
// Simply store the UUID -> index mapping. No unsafe lifetime tricks needed!
self.uuid_to_idx.insert(id, idx);
self.counter += 1;
// If the Vec reallocated, we need to rebuild all references in the HashMaps
// because the old references point to deallocated memory
if did_reallocate {
self.rebuild_references();
}
&mut self.shapes[idx]
}
/// Rebuilds all &'a Uuid references in the HashMaps after a Vec reallocation.
/// This is necessary because Vec reallocation invalidates all existing references.
fn rebuild_references(&mut self) {
// Rebuild shapes_uuid_to_idx with fresh references
let mut new_map = HashMap::with_capacity(self.shapes_uuid_to_idx.len());
for (_, idx) in self.shapes_uuid_to_idx.drain() {
let id_ref: &'a Uuid = unsafe { &*(&self.shapes[idx].id as *const Uuid) };
new_map.insert(id_ref, idx);
}
self.shapes_uuid_to_idx = new_map;
// Rebuild modifiers with fresh references
if !self.modifiers.is_empty() {
let old_modifiers: Vec<(Uuid, skia::Matrix)> = self
.modifiers
.drain()
.map(|(uuid_ref, matrix)| (*uuid_ref, matrix))
.collect();
for (uuid, matrix) in old_modifiers {
if let Some(uuid_ref) = self.get_uuid_ref(&uuid) {
self.modifiers.insert(uuid_ref, matrix);
}
}
}
// Rebuild structure with fresh references
if !self.structure.is_empty() {
let old_structure: Vec<(Uuid, Vec<StructureEntry>)> = self
.structure
.drain()
.map(|(uuid_ref, entries)| (*uuid_ref, entries))
.collect();
for (uuid, entries) in old_structure {
if let Some(uuid_ref) = self.get_uuid_ref(&uuid) {
self.structure.insert(uuid_ref, entries);
}
}
}
// Rebuild scale_content with fresh references
if !self.scale_content.is_empty() {
let old_scale_content: Vec<(Uuid, f32)> = self
.scale_content
.drain()
.map(|(uuid_ref, scale)| (*uuid_ref, scale))
.collect();
for (uuid, scale) in old_scale_content {
if let Some(uuid_ref) = self.get_uuid_ref(&uuid) {
self.scale_content.insert(uuid_ref, scale);
}
}
}
// Rebuild modified_shape_cache with fresh references
if !self.modified_shape_cache.is_empty() {
let old_cache: Vec<(Uuid, OnceCell<Shape>)> = self
.modified_shape_cache
.drain()
.map(|(uuid_ref, cell)| (*uuid_ref, cell))
.collect();
for (uuid, cell) in old_cache {
if let Some(uuid_ref) = self.get_uuid_ref(&uuid) {
self.modified_shape_cache.insert(uuid_ref, cell);
}
}
}
}
// No longer needed! Index-based storage means no references to rebuild.
// The old rebuild_references() function has been removed entirely.
pub fn len(&self) -> usize {
self.shapes_uuid_to_idx.len()
self.uuid_to_idx.len()
}
pub fn has(&self, id: &Uuid) -> bool {
self.shapes_uuid_to_idx.contains_key(&id)
self.uuid_to_idx.contains_key(id)
}
pub fn get_mut(&mut self, id: &Uuid) -> Option<&mut Shape> {
let idx = *self.shapes_uuid_to_idx.get(&id)?;
let idx = *self.uuid_to_idx.get(id)?;
Some(&mut self.shapes[idx])
}
pub fn get(&self, id: &Uuid) -> Option<&'a Shape> {
let idx = *self.shapes_uuid_to_idx.get(&id)?;
/// Get a shape by UUID. Returns the modified shape if modifiers/structure
/// are applied, otherwise returns the base shape.
pub fn get(&self, id: &Uuid) -> Option<&Shape> {
let idx = *self.uuid_to_idx.get(id)?;
// SAFETY: We're extending the lifetimes to 'a.
// This is safe because:
// 1. All internal HashMaps and the shapes Vec have fields with lifetime 'a
// 2. The shape at idx won't be moved or reallocated (pre-allocated Vec)
// 3. The id is stored in shapes[idx].id which has lifetime 'a
// 4. The references won't outlive the ShapesPoolImpl
unsafe {
let shape_ptr = &self.shapes[idx] as *const Shape;
let modifiers_ptr = &self.modifiers as *const HashMap<&'a Uuid, skia::Matrix>;
let structure_ptr = &self.structure as *const HashMap<&'a Uuid, Vec<StructureEntry>>;
let scale_content_ptr = &self.scale_content as *const HashMap<&'a Uuid, f32>;
let cache_ptr = &self.modified_shape_cache as *const HashMap<&'a Uuid, OnceCell<Shape>>;
let shape = &self.shapes[idx];
// Extend the lifetime of id to 'a - safe because it's the same Uuid stored in shapes[idx].id
let id_ref: &'a Uuid = &*(id as *const Uuid);
// Check if this shape needs modification (has modifiers, structure changes, or is a bool)
let needs_modification = shape.is_bool()
|| self.modifiers.contains_key(&idx)
|| self.structure.contains_key(&idx)
|| self.scale_content.contains_key(&idx);
if (*shape_ptr).is_bool()
|| (*modifiers_ptr).contains_key(&id_ref)
|| (*structure_ptr).contains_key(&id_ref)
|| (*scale_content_ptr).contains_key(&id_ref)
{
if let Some(cell) = (*cache_ptr).get(&id_ref) {
Some(cell.get_or_init(|| {
let mut shape = (*shape_ptr).transformed(
(*modifiers_ptr).get(&id_ref),
(*structure_ptr).get(&id_ref),
);
if needs_modification {
// Check if we have a cached modified version
if let Some(cell) = self.modified_shape_cache.get(&idx) {
Some(cell.get_or_init(|| {
let mut modified_shape =
shape.transformed(self.modifiers.get(&idx), self.structure.get(&idx));
if self.to_update_bool(&shape) {
math_bools::update_bool_to_path(&mut shape, self);
}
if self.to_update_bool(&modified_shape) {
math_bools::update_bool_to_path(&mut modified_shape, self);
}
if let Some(scale) = (*scale_content_ptr).get(&id_ref) {
shape.scale_content(*scale);
}
shape
}))
} else {
Some(&*shape_ptr)
}
if let Some(scale) = self.scale_content.get(&idx) {
modified_shape.scale_content(*scale);
}
modified_shape
}))
} else {
Some(&*shape_ptr)
Some(shape)
}
} else {
Some(shape)
}
}
@ -275,69 +192,68 @@ impl<'a> ShapesPoolImpl<'a> {
}
pub fn set_modifiers(&mut self, modifiers: HashMap<Uuid, skia::Matrix>) {
// Convert HashMap<Uuid, V> to HashMap<&'a Uuid, V> using references from shapes and
// Initialize the cache cells because later we don't want to have the mutable pointer
// Convert HashMap<Uuid, V> to HashMap<usize, V> using indices
// Initialize the cache cells for affected shapes
let mut ids = Vec::<Uuid>::new();
let mut modifiers_with_idx = HashMap::with_capacity(modifiers.len());
let mut modifiers_with_refs = HashMap::with_capacity(modifiers.len());
for (uuid, matrix) in modifiers {
if let Some(uuid_ref) = self.get_uuid_ref(&uuid) {
// self.modified_shape_cache.insert(uuid_ref, OnceCell::new());
modifiers_with_refs.insert(uuid_ref, matrix);
ids.push(*uuid_ref);
if let Some(idx) = self.uuid_to_idx.get(&uuid).copied() {
modifiers_with_idx.insert(idx, matrix);
ids.push(uuid);
}
}
self.modifiers = modifiers_with_refs;
self.modifiers = modifiers_with_idx;
let all_ids = shapes::all_with_ancestors(&ids, self, true);
for uuid in all_ids {
if let Some(uuid_ref) = self.get_uuid_ref(&uuid) {
self.modified_shape_cache.insert(uuid_ref, OnceCell::new());
if let Some(idx) = self.uuid_to_idx.get(&uuid).copied() {
self.modified_shape_cache.insert(idx, OnceCell::new());
}
}
}
pub fn set_structure(&mut self, structure: HashMap<Uuid, Vec<StructureEntry>>) {
// Convert HashMap<Uuid, V> to HashMap<&'a Uuid, V> using references from shapes and
// Initialize the cache cells because later we don't want to have the mutable pointer
let mut structure_with_refs = HashMap::with_capacity(structure.len());
// Convert HashMap<Uuid, V> to HashMap<usize, V> using indices
// Initialize the cache cells for affected shapes
let mut structure_with_idx = HashMap::with_capacity(structure.len());
let mut ids = Vec::<Uuid>::new();
for (uuid, entries) in structure {
if let Some(uuid_ref) = self.get_uuid_ref(&uuid) {
structure_with_refs.insert(uuid_ref, entries);
ids.push(*uuid_ref);
if let Some(idx) = self.uuid_to_idx.get(&uuid).copied() {
structure_with_idx.insert(idx, entries);
ids.push(uuid);
}
}
self.structure = structure_with_refs;
self.structure = structure_with_idx;
let all_ids = shapes::all_with_ancestors(&ids, self, true);
for uuid in all_ids {
if let Some(uuid_ref) = self.get_uuid_ref(&uuid) {
self.modified_shape_cache.insert(uuid_ref, OnceCell::new());
if let Some(idx) = self.uuid_to_idx.get(&uuid).copied() {
self.modified_shape_cache.insert(idx, OnceCell::new());
}
}
}
pub fn set_scale_content(&mut self, scale_content: HashMap<Uuid, f32>) {
// Convert HashMap<Uuid, V> to HashMap<&'a Uuid, V> using references from shapes and
// Initialize the cache cells because later we don't want to have the mutable pointer
let mut scale_content_with_refs = HashMap::with_capacity(scale_content.len());
// Convert HashMap<Uuid, V> to HashMap<usize, V> using indices
// Initialize the cache cells for affected shapes
let mut scale_content_with_idx = HashMap::with_capacity(scale_content.len());
let mut ids = Vec::<Uuid>::new();
for (uuid, value) in scale_content {
if let Some(uuid_ref) = self.get_uuid_ref(&uuid) {
scale_content_with_refs.insert(uuid_ref, value);
ids.push(*uuid_ref);
if let Some(idx) = self.uuid_to_idx.get(&uuid).copied() {
scale_content_with_idx.insert(idx, value);
ids.push(uuid);
}
}
self.scale_content = scale_content_with_refs;
self.scale_content = scale_content_with_idx;
let all_ids = shapes::all_with_ancestors(&ids, self, true);
for uuid in all_ids {
if let Some(uuid_ref) = self.get_uuid_ref(&uuid) {
self.modified_shape_cache.insert(uuid_ref, OnceCell::new());
if let Some(idx) = self.uuid_to_idx.get(&uuid).copied() {
self.modified_shape_cache.insert(idx, OnceCell::new());
}
}
}
@ -349,47 +265,33 @@ impl<'a> ShapesPoolImpl<'a> {
self.scale_content = HashMap::default();
}
/// Get a reference to the Uuid stored in a shape, if it exists
pub fn get_uuid_ref(&self, id: &Uuid) -> Option<&'a Uuid> {
let idx = *self.shapes_uuid_to_idx.get(&id)?;
// SAFETY: We're returning a reference with lifetime 'a to a Uuid stored
// in the shapes Vec. This is safe because the Vec is stable (pre-allocated)
// and won't be reallocated.
unsafe { Some(&*(&self.shapes[idx].id as *const Uuid)) }
}
pub fn subtree(&self, id: &Uuid) -> ShapesPoolImpl<'a> {
pub fn subtree(&self, id: &Uuid) -> ShapesPoolImpl {
let Some(shape) = self.get(id) else {
panic!("Subtree not found");
};
let mut shapes = vec![];
let mut idx = 0;
let mut shapes_uuid_to_idx = HashMap::default();
let mut new_idx = 0;
let mut uuid_to_idx = HashMap::default();
for id in shape.all_children_iter(self, true, true) {
let Some(shape) = self.get(&id) else {
for child_id in shape.all_children_iter(self, true, true) {
let Some(child_shape) = self.get(&child_id) else {
panic!("Not found");
};
shapes.push(shape.clone());
let id_ref: &'a Uuid = unsafe { &*(&self.shapes[idx].id as *const Uuid) };
shapes_uuid_to_idx.insert(id_ref, idx);
idx += 1;
shapes.push(child_shape.clone());
uuid_to_idx.insert(child_id, new_idx);
new_idx += 1;
}
let mut result = ShapesPoolImpl {
ShapesPoolImpl {
shapes,
counter: idx,
shapes_uuid_to_idx,
counter: new_idx,
uuid_to_idx,
modified_shape_cache: HashMap::default(),
modifiers: HashMap::default(),
structure: HashMap::default(),
scale_content: HashMap::default(),
};
result.rebuild_references();
result
}
}
fn to_update_bool(&self, shape: &Shape) -> bool {
@ -398,11 +300,21 @@ impl<'a> ShapesPoolImpl<'a> {
}
let default = &Matrix::default();
let parent_modifier = self.modifiers.get(&shape.id).unwrap_or(default);
// Get parent modifier by index
let parent_idx = self.uuid_to_idx.get(&shape.id);
let parent_modifier = parent_idx
.and_then(|idx| self.modifiers.get(idx))
.unwrap_or(default);
// Returns true if the transform of any child is different to the parent's
shape.all_children_iter(self, true, false).any(|id| {
!math::is_close_matrix(parent_modifier, self.modifiers.get(&id).unwrap_or(default))
shape.all_children_iter(self, true, false).any(|child_id| {
let child_modifier = self
.uuid_to_idx
.get(&child_id)
.and_then(|idx| self.modifiers.get(idx))
.unwrap_or(default);
!math::is_close_matrix(parent_modifier, child_modifier)
})
}
}

View File

@ -3,7 +3,7 @@ use crate::{with_current_shape_mut, STATE};
use macros::ToJs;
mod align;
mod constraints;
pub mod constraints;
mod flex;
mod grid;

View File

@ -0,0 +1,173 @@
use crate::mem;
use crate::shapes::{BlendMode, ConstraintH, ConstraintV};
use crate::utils::uuid_from_u32_quartet;
use crate::uuid::Uuid;
use crate::wasm::blend::RawBlendMode;
use crate::wasm::layouts::constraints::{RawConstraintH, RawConstraintV};
use crate::{with_state_mut, STATE};
use super::RawShapeType;
/// Binary layout for batched shape base properties:
///
/// | Offset | Size | Field | Type |
/// |--------|------|--------------|-----------------------------------|
/// | 0 | 16 | id | UUID (4 × u32 LE) |
/// | 16 | 16 | parent_id | UUID (4 × u32 LE) |
/// | 32 | 1 | shape_type | u8 |
/// | 33 | 1 | flags | u8 (bit0: clip, bit1: hidden) |
/// | 34 | 1 | blend_mode | u8 |
/// | 35 | 1 | constraint_h | u8 (0xFF = None) |
/// | 36 | 1 | constraint_v | u8 (0xFF = None) |
/// | 37 | 3 | padding | - |
/// | 40 | 4 | opacity | f32 LE |
/// | 44 | 4 | rotation | f32 LE |
/// | 48 | 24 | transform | 6 × f32 LE (a,b,c,d,e,f) |
/// | 72 | 16 | selrect | 4 × f32 LE (x1,y1,x2,y2) |
/// | 88 | 16 | corners | 4 × f32 LE (r1,r2,r3,r4) |
/// |--------|------|--------------|-----------------------------------|
/// | Total | 104 | | |
pub const BASE_PROPS_SIZE: usize = 104;
const FLAG_CLIP_CONTENT: u8 = 0b0000_0001;
const FLAG_HIDDEN: u8 = 0b0000_0010;
const CONSTRAINT_NONE: u8 = 0xFF;
/// Reads a f32 from a byte slice at the given offset (little-endian)
#[inline]
fn read_f32_le(bytes: &[u8], offset: usize) -> f32 {
f32::from_le_bytes([
bytes[offset],
bytes[offset + 1],
bytes[offset + 2],
bytes[offset + 3],
])
}
/// Reads a u32 from a byte slice at the given offset (little-endian)
#[inline]
fn read_u32_le(bytes: &[u8], offset: usize) -> u32 {
u32::from_le_bytes([
bytes[offset],
bytes[offset + 1],
bytes[offset + 2],
bytes[offset + 3],
])
}
/// Parses UUID from bytes at given offset
#[inline]
fn read_uuid(bytes: &[u8], offset: usize) -> Uuid {
uuid_from_u32_quartet(
read_u32_le(bytes, offset),
read_u32_le(bytes, offset + 4),
read_u32_le(bytes, offset + 8),
read_u32_le(bytes, offset + 12),
)
}
#[no_mangle]
pub extern "C" fn set_shape_base_props() {
let bytes = mem::bytes();
if bytes.len() < BASE_PROPS_SIZE {
return;
}
// Parse all fields from the buffer
let id = read_uuid(&bytes, 0);
let parent_id = read_uuid(&bytes, 16);
let shape_type = bytes[32];
let flags = bytes[33];
let blend_mode = bytes[34];
let constraint_h = bytes[35];
let constraint_v = bytes[36];
// bytes[37..40] are padding
let opacity = read_f32_le(&bytes, 40);
let rotation = read_f32_le(&bytes, 44);
// Transform matrix (a, b, c, d, e, f)
let transform_a = read_f32_le(&bytes, 48);
let transform_b = read_f32_le(&bytes, 52);
let transform_c = read_f32_le(&bytes, 56);
let transform_d = read_f32_le(&bytes, 60);
let transform_e = read_f32_le(&bytes, 64);
let transform_f = read_f32_le(&bytes, 68);
// Selrect (x1, y1, x2, y2)
let selrect_x1 = read_f32_le(&bytes, 72);
let selrect_y1 = read_f32_le(&bytes, 76);
let selrect_x2 = read_f32_le(&bytes, 80);
let selrect_y2 = read_f32_le(&bytes, 84);
// Corners (r1, r2, r3, r4)
let corner_r1 = read_f32_le(&bytes, 88);
let corner_r2 = read_f32_le(&bytes, 92);
let corner_r3 = read_f32_le(&bytes, 96);
let corner_r4 = read_f32_le(&bytes, 100);
// Decode flags
let clip_content = (flags & FLAG_CLIP_CONTENT) != 0;
let hidden = (flags & FLAG_HIDDEN) != 0;
// Convert raw enum values
let shape_type_enum = RawShapeType::from(shape_type);
let blend_mode_enum: BlendMode = RawBlendMode::from(blend_mode).into();
let constraint_h_opt: Option<ConstraintH> = if constraint_h == CONSTRAINT_NONE {
None
} else {
Some(RawConstraintH::from(constraint_h).into())
};
let constraint_v_opt: Option<ConstraintV> = if constraint_v == CONSTRAINT_NONE {
None
} else {
Some(RawConstraintV::from(constraint_v).into())
};
with_state_mut!(state, {
// Select/create the shape
state.use_shape(id);
// Set parent relationship
state.set_parent_for_current_shape(parent_id);
// Mark shape as touched
state.touch_current();
// Apply all properties to the current shape
if let Some(shape) = state.current_shape_mut() {
// Type
shape.set_shape_type(shape_type_enum.into());
// Boolean flags
shape.set_clip(clip_content);
shape.set_hidden(hidden);
// Blend mode and opacity
shape.set_blend_mode(blend_mode_enum);
shape.set_opacity(opacity);
// Constraints
shape.set_constraint_h(constraint_h_opt);
shape.set_constraint_v(constraint_v_opt);
// Transform
shape.set_rotation(rotation);
shape.set_transform(
transform_a,
transform_b,
transform_c,
transform_d,
transform_e,
transform_f,
);
// Geometry
shape.set_selrect(selrect_x1, selrect_y1, selrect_x2, selrect_y2);
shape.set_corners((corner_r1, corner_r2, corner_r3, corner_r4));
}
});
}

View File

@ -1,3 +1,5 @@
mod base_props;
use macros::ToJs;
use crate::shapes::{Bool, Frame, Group, Path, Rect, SVGRaw, TextContent, Type};