mirror of
https://github.com/penpot/penpot.git
synced 2026-09-10 12:49:07 -04:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
103afe7e8b | ||
|
|
8666610920 | ||
|
|
c88015f70e |
No files matched your search
@@ -268,11 +268,20 @@
|
||||
(declare request-render)
|
||||
(declare set-shape-vertical-align fonts-from-text-content)
|
||||
|
||||
;; This should never be called from the outside.
|
||||
;; Performs a render
|
||||
(defn- render
|
||||
([]
|
||||
(render (js/performance.now)))
|
||||
([timestamp]
|
||||
(let [queue-frame (h/call wasm/internal-module "_render" timestamp)]
|
||||
(when (= queue-frame 1)
|
||||
(request-render "queue-frame")))))
|
||||
|
||||
;; This should never be called from the outside.
|
||||
(defn- on-render
|
||||
[timestamp]
|
||||
(when (and wasm/context-initialized? (not @wasm/context-lost?))
|
||||
(h/call wasm/internal-module "_render" timestamp)
|
||||
(render timestamp)
|
||||
|
||||
;; Update text editor blink (so cursor toggles) using the same timestamp
|
||||
(try
|
||||
@@ -297,14 +306,12 @@
|
||||
(catch :default e
|
||||
(js/console.error "text-editor overlay/update failed:" e)))
|
||||
|
||||
(set! wasm/internal-frame-id nil)
|
||||
(ug/dispatch! (ug/event "penpot:wasm:render"))))
|
||||
|
||||
(defn render-sync
|
||||
[]
|
||||
(when (and wasm/context-initialized? (not @wasm/context-lost?))
|
||||
(h/call wasm/internal-module "_render_sync")
|
||||
(set! wasm/internal-frame-id nil)))
|
||||
(h/call wasm/internal-module "_render_sync")))
|
||||
|
||||
(defn render-sync-shape
|
||||
[id]
|
||||
@@ -314,49 +321,25 @@
|
||||
(aget buffer 0)
|
||||
(aget buffer 1)
|
||||
(aget buffer 2)
|
||||
(aget buffer 3))
|
||||
(set! wasm/internal-frame-id nil))))
|
||||
(aget buffer 3)))))
|
||||
|
||||
(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 @wasm/context-lost?) (not @wasm/disable-request-render?))
|
||||
(when (and wasm/context-initialized?
|
||||
(not @wasm/context-lost?)
|
||||
(not @wasm/disable-request-render?))
|
||||
(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))))))
|
||||
(reset! deferred-render? true)
|
||||
(wasm/request-frame))))
|
||||
|
||||
(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)
|
||||
(let [was-pending (wasm/frame-requested?)]
|
||||
(wasm/cancel-frame)
|
||||
(reset! deferred-render? was-pending)))
|
||||
|
||||
(defn- end-shapes-loading!
|
||||
@@ -1104,7 +1087,7 @@
|
||||
;; completes in the first frame. For zoom, interest-
|
||||
;; area tiles (~3 tile margin) don't block the main
|
||||
;; thread.
|
||||
(h/call wasm/internal-module "_render" 0)))]
|
||||
(render)))]
|
||||
(fns/debounce do-render DEBOUNCE_DELAY_MS)))
|
||||
|
||||
(defn set-view-box
|
||||
@@ -1708,12 +1691,9 @@
|
||||
(set! wasm/context-initialized? false)
|
||||
|
||||
;; Cancel any pending animation frame to prevent race conditions
|
||||
(when wasm/internal-frame-id
|
||||
(js/cancelAnimationFrame wasm/internal-frame-id)
|
||||
(set! wasm/internal-frame-id nil))
|
||||
(wasm/cancel-frame)
|
||||
|
||||
;; Reset render flags to prevent new renders from being scheduled
|
||||
(reset! pending-render false)
|
||||
(reset! shapes-loading? false)
|
||||
(reset! deferred-render? false)
|
||||
|
||||
@@ -1991,6 +1971,7 @@
|
||||
(p/fmap
|
||||
(fn [default]
|
||||
(set! wasm/internal-module default)
|
||||
(wasm/set-frame-function on-render)
|
||||
true))
|
||||
(p/merr
|
||||
(fn [cause]
|
||||
|
||||
@@ -8,8 +8,52 @@
|
||||
(:require ["./api/shared.js" :as shared]))
|
||||
|
||||
(defonce internal-frame-id nil)
|
||||
(defonce internal-frame-fn nil)
|
||||
|
||||
(defonce internal-stats #js {:requests 0 :cancelled 0 :queued 0 :frames 0})
|
||||
|
||||
(defonce internal-module #js {})
|
||||
|
||||
(unchecked-set js/globalThis "internalStats" internal-stats)
|
||||
|
||||
;; Is a frame requested?
|
||||
(defn frame-requested?
|
||||
"Returns true if a frame was requested"
|
||||
[]
|
||||
(not (nil? internal-frame-id)))
|
||||
|
||||
;; Cancels a frame
|
||||
(defn cancel-frame
|
||||
"Cancels the current requested frame"
|
||||
[]
|
||||
(when (frame-requested?)
|
||||
(unchecked-set internal-stats "cancelled" (inc (unchecked-get internal-stats "cancelled")))
|
||||
(js/cancelAnimationFrame internal-frame-id)
|
||||
(set! internal-frame-id nil)
|
||||
true)
|
||||
false)
|
||||
|
||||
(defn- on-frame
|
||||
"The frame function"
|
||||
[timestamp]
|
||||
(let [frame-id internal-frame-id]
|
||||
(set! internal-frame-id nil)
|
||||
(unchecked-set internal-stats "frames" (inc (unchecked-get internal-stats "frames")))
|
||||
(internal-frame-fn timestamp frame-id)))
|
||||
|
||||
(defn set-frame-function
|
||||
[f]
|
||||
(set! internal-frame-fn f))
|
||||
|
||||
;; Requests a frame
|
||||
(defn request-frame
|
||||
"Requests a new frame"
|
||||
[]
|
||||
(unchecked-set internal-stats "requests" (inc (unchecked-get internal-stats "requests")))
|
||||
(when-not (frame-requested?)
|
||||
(unchecked-set internal-stats "queued" (inc (unchecked-get internal-stats "queued")))
|
||||
(set! internal-frame-id (js/requestAnimationFrame on-frame))))
|
||||
|
||||
;; Reference to the HTML canvas element.
|
||||
(defonce canvas nil)
|
||||
;; Snapshot of the current canvas suitable for `<img src=...>` overlays.
|
||||
@@ -29,7 +73,6 @@
|
||||
;; When we're rendering in a sync way we want to stop the asynchrous `request-render`
|
||||
(defonce disable-request-render? (atom false))
|
||||
|
||||
|
||||
(defonce serializers
|
||||
#js {:blur-type shared/RawBlurType
|
||||
:blend-mode shared/RawBlendMode
|
||||
|
||||
@@ -50,7 +50,7 @@ export CARGO_PARAMS="${@:2}";
|
||||
|
||||
if [ "$BUILD_MODE" = "release" ]; then
|
||||
export CARGO_PARAMS="--release $CARGO_PARAMS"
|
||||
export EMCC_CFLAGS="-O3 -sASSERTIONS=0 $EMCC_CFLAGS"
|
||||
export EMCC_CFLAGS="-O3 -sASSERTIONS=0 --profiling $EMCC_CFLAGS"
|
||||
else
|
||||
# TODO: Extra parameters that could be good to look into:
|
||||
# -gseparate-dwarf
|
||||
@@ -80,6 +80,9 @@ function copy_artifacts {
|
||||
|
||||
cp target/wasm32-unknown-emscripten/$BUILD_MODE/render_wasm.js $DEST/$BUILD_NAME.js;
|
||||
cp target/wasm32-unknown-emscripten/$BUILD_MODE/render_wasm.wasm $DEST/$BUILD_NAME.wasm;
|
||||
if [ -f target/wasm32-unknown-emscripten/$BUILD_MODE/render_wasm.wasm.map ]; then
|
||||
cp target/wasm32-unknown-emscripten/$BUILD_MODE/render_wasm.wasm.map $DEST/$BUILD_NAME.wasm.map;
|
||||
fi
|
||||
|
||||
sed -i "s/render_wasm.wasm/$BUILD_NAME.wasm?version=$VERSION_TAG/g" $DEST/$BUILD_NAME.js;
|
||||
|
||||
|
||||
@@ -1,18 +1,4 @@
|
||||
addToLibrary({
|
||||
wapi_requestAnimationFrame: function wapi_requestAnimationFrame() {
|
||||
if (typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope) {
|
||||
setTimeout(Module._process_animation_frame);
|
||||
} else {
|
||||
return window.requestAnimationFrame(Module._process_animation_frame);
|
||||
}
|
||||
},
|
||||
wapi_cancelAnimationFrame: function wapi_cancelAnimationFrame(frameId) {
|
||||
if (typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope) {
|
||||
clearTimeout(frameId);
|
||||
} else {
|
||||
return window.cancelAnimationFrame(frameId);
|
||||
}
|
||||
},
|
||||
wapi_notifyTilesRenderComplete: function wapi_notifyTilesRenderComplete() {
|
||||
// The corresponding listener lives on `document` (main thread), so in a
|
||||
// worker context we simply skip the dispatch instead of crashing.
|
||||
|
||||
+8
-34
@@ -18,6 +18,8 @@ use std::collections::HashMap;
|
||||
|
||||
#[allow(unused_imports)]
|
||||
use crate::error::{Error, Result};
|
||||
use crate::render::RenderQueueFrame;
|
||||
|
||||
use macros::wasm_error;
|
||||
use math::{Bounds, Matrix};
|
||||
use mem::SerializableResult;
|
||||
@@ -123,12 +125,6 @@ pub extern "C" fn set_browser(browser: u8) -> Result<()> {
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
pub extern "C" fn clean_up() -> Result<()> {
|
||||
with_state_mut!(state, {
|
||||
// Cancel the current animation frame if it exists so
|
||||
// it won't try to render without context
|
||||
let render_state = state.render_state_mut();
|
||||
render_state.cancel_animation_frame();
|
||||
});
|
||||
unsafe { STATE = None }
|
||||
mem::free_bytes()?;
|
||||
Ok(())
|
||||
@@ -222,7 +218,7 @@ pub extern "C" fn set_canvas_background(raw_color: u32) -> Result<()> {
|
||||
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
pub extern "C" fn render(_: i32) -> Result<()> {
|
||||
pub extern "C" fn render(timestamp: i32) -> Result<RenderQueueFrame> {
|
||||
with_state_mut!(state, {
|
||||
state.rebuild_touched_tiles();
|
||||
// Drain the throttled modifier-tile invalidation accumulated
|
||||
@@ -236,11 +232,10 @@ pub extern "C" fn render(_: i32) -> Result<()> {
|
||||
state.rebuild_modifier_tiles(ids)?;
|
||||
}
|
||||
}
|
||||
state
|
||||
.start_render_loop(performance::get_time())
|
||||
.map_err(|_| Error::RecoverableError("Error rendering".to_string()))?;
|
||||
return state
|
||||
.start_render_loop(timestamp)
|
||||
.map_err(|_| Error::RecoverableError("Error rendering".to_string()));
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -249,7 +244,7 @@ pub extern "C" fn render_sync() -> Result<()> {
|
||||
with_state_mut!(state, {
|
||||
state.rebuild_tiles();
|
||||
state
|
||||
.render_sync(performance::get_time())
|
||||
.render_sync()
|
||||
.map_err(|_| Error::RecoverableError("Error rendering".to_string()))?;
|
||||
});
|
||||
Ok(())
|
||||
@@ -275,7 +270,7 @@ pub extern "C" fn render_sync_shape(a: u32, b: u32, c: u32, d: u32) -> Result<()
|
||||
|
||||
state.rebuild_tiles_from(Some(&id));
|
||||
state
|
||||
.render_sync_shape(&id, performance::get_time())
|
||||
.render_sync_shape(&id)
|
||||
.map_err(|e| Error::RecoverableError(e.to_string()))?;
|
||||
});
|
||||
Ok(())
|
||||
@@ -306,15 +301,6 @@ pub extern "C" fn set_preview_mode(enabled: bool) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
pub extern "C" fn render_preview() -> Result<()> {
|
||||
with_state_mut!(state, {
|
||||
state.render_preview(performance::get_time());
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Enter bulk-loading mode. While active, `state.loading` is `true`.
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
@@ -351,16 +337,6 @@ pub extern "C" fn render_loading_overlay() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
pub extern "C" fn process_animation_frame(timestamp: i32) -> Result<()> {
|
||||
let result = with_state_mut!(state, { state.process_animation_frame(timestamp) });
|
||||
if let Err(err) = result {
|
||||
eprintln!("process_animation_frame error: {}", err);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
pub extern "C" fn reset_canvas() -> Result<()> {
|
||||
@@ -420,7 +396,6 @@ pub extern "C" fn set_view_end() -> Result<()> {
|
||||
with_state_mut!(state, {
|
||||
performance::begin_measure!("set_view_end");
|
||||
state.render_state.options.set_fast_mode(false);
|
||||
state.render_state.cancel_animation_frame();
|
||||
|
||||
let scale = state.render_state.get_scale();
|
||||
state
|
||||
@@ -480,7 +455,6 @@ pub extern "C" fn set_modifiers_end() -> Result<()> {
|
||||
performance::begin_measure!("set_modifiers_end");
|
||||
state.render_state.options.set_fast_mode(false);
|
||||
state.render_state.options.set_interactive_transform(false);
|
||||
state.render_state.cancel_animation_frame();
|
||||
performance::end_measure!("set_modifiers_end");
|
||||
});
|
||||
Ok(())
|
||||
|
||||
+17
-48
@@ -39,6 +39,12 @@ pub use images::*;
|
||||
|
||||
type ClipStack = Vec<(Rect, Option<Corners>, Matrix)>;
|
||||
|
||||
#[repr(u8)]
|
||||
pub enum RenderQueueFrame {
|
||||
Yes = 1,
|
||||
No = 0,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct NodeRenderState {
|
||||
pub id: Uuid,
|
||||
@@ -336,8 +342,6 @@ pub(crate) struct RenderState {
|
||||
pub cached_viewbox: Viewbox,
|
||||
pub images: ImageStore,
|
||||
pub background_color: skia::Color,
|
||||
// Identifier of the current requestAnimationFrame call, if any.
|
||||
pub render_request_id: Option<i32>,
|
||||
// Indicates whether the rendering process has pending frames.
|
||||
pub render_in_progress: bool,
|
||||
// Stack of nodes pending to be rendered.
|
||||
@@ -508,7 +512,6 @@ impl RenderState {
|
||||
cached_viewbox: Viewbox::new(0., 0.),
|
||||
images: ImageStore::new(gpu_state.context.clone()),
|
||||
background_color: skia::Color::TRANSPARENT,
|
||||
render_request_id: None,
|
||||
render_in_progress: false,
|
||||
pending_nodes: vec![],
|
||||
current_tile: None,
|
||||
@@ -521,7 +524,7 @@ impl RenderState {
|
||||
options.viewport_interest_area_threshold,
|
||||
1.0,
|
||||
),
|
||||
pending_tiles: PendingTiles::new_empty(),
|
||||
pending_tiles: PendingTiles::new(),
|
||||
nested_fills: vec![],
|
||||
nested_blurs: vec![],
|
||||
nested_shadows: vec![],
|
||||
@@ -1608,14 +1611,6 @@ impl RenderState {
|
||||
self.surfaces.update_render_context(self.render_area, scale);
|
||||
}
|
||||
|
||||
pub fn cancel_animation_frame(&mut self) {
|
||||
if self.render_in_progress {
|
||||
if let Some(frame_id) = self.render_request_id {
|
||||
wapi::cancel_animation_frame!(frame_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn rebuild_backbuffer_crop_cache(&mut self, tree: ShapesPoolRef) {
|
||||
self.backbuffer_crop_cache.clear();
|
||||
|
||||
@@ -1866,39 +1861,13 @@ 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<()> {
|
||||
let _start = performance::begin_timed_log!("render_preview");
|
||||
performance::begin_measure!("render_preview");
|
||||
|
||||
// Enable fast_mode during preview to skip expensive effects (blur, shadows).
|
||||
// Restore the previous state afterward so the final render is full quality.
|
||||
let current_fast_mode = self.options.is_fast_mode();
|
||||
self.options.set_fast_mode(true);
|
||||
|
||||
// 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)?;
|
||||
|
||||
self.options.set_fast_mode(current_fast_mode);
|
||||
|
||||
performance::end_measure!("render_preview");
|
||||
performance::end_timed_log!("render_preview", _start);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn start_render_loop(
|
||||
&mut self,
|
||||
base_object: Option<&Uuid>,
|
||||
tree: ShapesPoolRef,
|
||||
timestamp: i32,
|
||||
sync_render: bool,
|
||||
) -> Result<()> {
|
||||
) -> Result<RenderQueueFrame> {
|
||||
#[cfg(feature = "stats")]
|
||||
self.stats.clear();
|
||||
|
||||
@@ -1991,10 +1960,11 @@ impl RenderState {
|
||||
|
||||
self.apply_drawing_to_render_canvas(None, SurfaceId::Current);
|
||||
|
||||
let mut result = RenderQueueFrame::No;
|
||||
if sync_render {
|
||||
self.render_shape_tree_sync(base_object, tree, timestamp)?;
|
||||
} else {
|
||||
self.process_animation_frame(base_object, tree, timestamp)?;
|
||||
result = self.render_loop_step(base_object, tree, timestamp)?;
|
||||
// Update cached_viewbox after visible tiles render
|
||||
// synchronously so that render_from_cache uses the correct
|
||||
// zoom ratio even if interest-area tiles are still rendering
|
||||
@@ -2009,7 +1979,7 @@ impl RenderState {
|
||||
|
||||
performance::end_measure!("start_render_loop");
|
||||
performance::end_timed_log!("start_render_loop", _start);
|
||||
Ok(())
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn compute_document_bounds(
|
||||
@@ -2043,12 +2013,12 @@ impl RenderState {
|
||||
acc
|
||||
}
|
||||
|
||||
pub fn process_animation_frame(
|
||||
pub fn render_loop_step(
|
||||
&mut self,
|
||||
base_object: Option<&Uuid>,
|
||||
tree: ShapesPoolRef,
|
||||
timestamp: i32,
|
||||
) -> Result<()> {
|
||||
) -> Result<RenderQueueFrame> {
|
||||
performance::begin_measure!("process_animation_frame");
|
||||
if self.render_in_progress {
|
||||
if tree.len() != 0 {
|
||||
@@ -2069,8 +2039,7 @@ impl RenderState {
|
||||
}
|
||||
|
||||
if self.render_in_progress {
|
||||
self.cancel_animation_frame();
|
||||
self.render_request_id = Some(wapi::request_animation_frame!());
|
||||
return Ok(RenderQueueFrame::Yes);
|
||||
} else {
|
||||
// A full-quality frame is now complete. Refresh Backbuffer and regenerate
|
||||
// the per-shape crop cache so interactive drags can reuse pixels.
|
||||
@@ -2083,7 +2052,7 @@ impl RenderState {
|
||||
}
|
||||
}
|
||||
performance::end_measure!("process_animation_frame");
|
||||
Ok(())
|
||||
Ok(RenderQueueFrame::No)
|
||||
}
|
||||
|
||||
pub fn render_shape_tree_sync(
|
||||
@@ -2091,12 +2060,12 @@ impl RenderState {
|
||||
base_object: Option<&Uuid>,
|
||||
tree: ShapesPoolRef,
|
||||
timestamp: i32,
|
||||
) -> Result<()> {
|
||||
) -> Result<RenderQueueFrame> {
|
||||
if tree.len() != 0 {
|
||||
self.render_shape_tree_partial(base_object, tree, timestamp, false)?;
|
||||
}
|
||||
self.flush_and_submit();
|
||||
Ok(())
|
||||
Ok(RenderQueueFrame::No)
|
||||
}
|
||||
|
||||
pub fn render_shape_pixels(
|
||||
|
||||
@@ -7,7 +7,7 @@ const SHOW_WASM_INFO: u32 = 0x08;
|
||||
// Render performance options
|
||||
// This is the extra area used for tile rendering (tiles beyond viewport).
|
||||
// Higher values pre-render more tiles, reducing empty squares during pan but using more memory.
|
||||
const VIEWPORT_INTEREST_AREA_THRESHOLD: i32 = 3;
|
||||
const VIEWPORT_INTEREST_AREA_THRESHOLD: i32 = 1;
|
||||
const MAX_BLOCKING_TIME_MS: i32 = 32;
|
||||
const NODE_BATCH_THRESHOLD: i32 = 3;
|
||||
const BLUR_DOWNSCALE_THRESHOLD: f32 = 8.0;
|
||||
|
||||
@@ -7,13 +7,11 @@ pub use shapes_pool::{ShapesPool, ShapesPoolMutRef, ShapesPoolRef};
|
||||
pub use text_editor::*;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::render::RenderState;
|
||||
use crate::shapes::Shape;
|
||||
use crate::render::{RenderQueueFrame, RenderState};
|
||||
use crate::shapes::{modifiers::grid_layout::grid_cell_data, Shape};
|
||||
use crate::tiles;
|
||||
use crate::uuid::Uuid;
|
||||
|
||||
use crate::shapes::modifiers::grid_layout::grid_cell_data;
|
||||
|
||||
/// This struct holds the state of the Rust application between JS calls.
|
||||
///
|
||||
/// It is created by [init] and passed to the other exported functions.
|
||||
@@ -93,14 +91,14 @@ impl State {
|
||||
self.render_state.render_from_cache(&self.shapes);
|
||||
}
|
||||
|
||||
pub fn render_sync(&mut self, timestamp: i32) -> Result<()> {
|
||||
pub fn render_sync(&mut self) -> Result<RenderQueueFrame> {
|
||||
self.render_state
|
||||
.start_render_loop(None, &self.shapes, timestamp, true)
|
||||
.start_render_loop(None, &self.shapes, 0, true)
|
||||
}
|
||||
|
||||
pub fn render_sync_shape(&mut self, id: &Uuid, timestamp: i32) -> Result<()> {
|
||||
pub fn render_sync_shape(&mut self, id: &Uuid) -> Result<RenderQueueFrame> {
|
||||
self.render_state
|
||||
.start_render_loop(Some(id), &self.shapes, timestamp, true)
|
||||
.start_render_loop(Some(id), &self.shapes, 0, true)
|
||||
}
|
||||
|
||||
pub fn render_shape_pixels(
|
||||
@@ -113,7 +111,7 @@ impl State {
|
||||
.render_shape_pixels(id, &self.shapes, scale, timestamp)
|
||||
}
|
||||
|
||||
pub fn start_render_loop(&mut self, timestamp: i32) -> Result<()> {
|
||||
pub fn start_render_loop(&mut self, timestamp: i32) -> Result<RenderQueueFrame> {
|
||||
// If zoom changed (e.g. interrupted zoom render followed by pan), the
|
||||
// tile index may be stale for the new viewport position. Rebuild the
|
||||
// index so shapes are mapped to the correct tiles. We use
|
||||
@@ -128,11 +126,6 @@ impl State {
|
||||
.start_render_loop(None, &self.shapes, timestamp, false)
|
||||
}
|
||||
|
||||
pub fn process_animation_frame(&mut self, timestamp: i32) -> Result<()> {
|
||||
self.render_state
|
||||
.process_animation_frame(None, &self.shapes, timestamp)
|
||||
}
|
||||
|
||||
pub fn clear_focus_mode(&mut self) {
|
||||
self.render_state.clear_focus_mode();
|
||||
}
|
||||
@@ -253,10 +246,6 @@ impl State {
|
||||
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>) -> Result<()> {
|
||||
// Index-based storage is safe
|
||||
self.render_state
|
||||
|
||||
+133
-31
@@ -22,43 +22,95 @@ impl Tile {
|
||||
pub struct TileRect(pub i32, pub i32, pub i32, pub i32);
|
||||
|
||||
impl TileRect {
|
||||
pub fn empty() -> Self {
|
||||
Self(0, 0, 0, 0)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn x1(&self) -> i32 {
|
||||
self.0
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn y1(&self) -> i32 {
|
||||
self.1
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn x2(&self) -> i32 {
|
||||
self.2
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn y2(&self) -> i32 {
|
||||
self.3
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn left(&self) -> i32 {
|
||||
self.0
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn top(&self) -> i32 {
|
||||
self.1
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn right(&self) -> i32 {
|
||||
self.2
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn bottom(&self) -> i32 {
|
||||
self.3
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn x(&self) -> i32 {
|
||||
self.0
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn y(&self) -> i32 {
|
||||
self.1
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn width(&self) -> i32 {
|
||||
self.x2() - self.x1()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn half_width(&self) -> i32 {
|
||||
self.width() / 2
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn height(&self) -> i32 {
|
||||
self.y2() - self.y1()
|
||||
}
|
||||
|
||||
pub fn center_x(&self) -> i32 {
|
||||
self.x1() + self.width() / 2
|
||||
#[inline]
|
||||
pub fn half_height(&self) -> i32 {
|
||||
self.height() / 2
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn center_x(&self) -> i32 {
|
||||
self.x() + self.half_width()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn center_y(&self) -> i32 {
|
||||
self.y1() + self.height() / 2
|
||||
self.y() + self.half_height()
|
||||
}
|
||||
|
||||
pub fn contains(&self, tile: &Tile) -> bool {
|
||||
tile.x() >= self.x1()
|
||||
&& tile.y() >= self.y1()
|
||||
&& tile.x() <= self.x2()
|
||||
&& tile.y() <= self.y2()
|
||||
tile.x() >= self.left()
|
||||
&& tile.y() >= self.top()
|
||||
&& tile.x() <= self.right()
|
||||
&& tile.y() <= self.bottom()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,43 +247,76 @@ impl TileHashMap {
|
||||
}
|
||||
|
||||
const VIEWPORT_DEFAULT_CAPACITY: usize = 24 * 12;
|
||||
const VIEWPORT_SPIRAL_DEFAULT_CAPACITY: usize = 64;
|
||||
|
||||
// This structure keeps the list of tiles that are in the pending list, the
|
||||
// ones that are going to be rendered.
|
||||
pub struct PendingTiles {
|
||||
pub list: Vec<Tile>,
|
||||
pub spiral: Vec<Tile>,
|
||||
pub spiral_rect: TileRect,
|
||||
}
|
||||
|
||||
impl PendingTiles {
|
||||
pub fn new_empty() -> Self {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
list: Vec::with_capacity(VIEWPORT_DEFAULT_CAPACITY),
|
||||
spiral: Vec::with_capacity(VIEWPORT_SPIRAL_DEFAULT_CAPACITY),
|
||||
spiral_rect: TileRect::empty(),
|
||||
}
|
||||
}
|
||||
|
||||
// Generate tiles ordered by distance to the center (closest processed first).
|
||||
fn generate_spiral(rect: &TileRect) -> Vec<Tile> {
|
||||
let cx = rect.center_x();
|
||||
let cy = rect.center_y();
|
||||
|
||||
// TileRect is inclusive (x1..=x2, y1..=y2).
|
||||
let mut tiles = Vec::new();
|
||||
for x in rect.x1()..=rect.x2() {
|
||||
for y in rect.y1()..=rect.y2() {
|
||||
tiles.push(Tile(x, y));
|
||||
}
|
||||
// Generate tiles in spiral order from center
|
||||
fn generate_spiral(columns: i32, rows: i32) -> Vec<Tile> {
|
||||
let total = columns * rows;
|
||||
if total <= 0 {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
// We pop() from the end, so keep nearest-to-center tiles at the end.
|
||||
tiles.sort_unstable_by(|a, b| {
|
||||
let da = (a.x() - cx).abs() + (a.y() - cy).abs();
|
||||
let db = (b.x() - cx).abs() + (b.y() - cy).abs();
|
||||
da.cmp(&db)
|
||||
.then_with(|| a.x().cmp(&b.x()))
|
||||
.then_with(|| a.y().cmp(&b.y()))
|
||||
});
|
||||
tiles.reverse();
|
||||
tiles
|
||||
let mut result = Vec::with_capacity(total as usize);
|
||||
let mut cx = 0;
|
||||
let mut cy = 0;
|
||||
|
||||
let ratio = (columns as f32 / rows as f32).ceil() as i32;
|
||||
|
||||
let mut direction_current = 0;
|
||||
let mut direction_total_x = ratio;
|
||||
let mut direction_total_y = 1;
|
||||
let mut direction = 0;
|
||||
let mut current = 0;
|
||||
|
||||
result.push(Tile(cx, cy));
|
||||
while current < total {
|
||||
match direction {
|
||||
0 => cx += 1,
|
||||
1 => cy += 1,
|
||||
2 => cx -= 1,
|
||||
3 => cy -= 1,
|
||||
_ => unreachable!("Invalid direction"),
|
||||
}
|
||||
|
||||
result.push(Tile(cx, cy));
|
||||
|
||||
direction_current += 1;
|
||||
let direction_total = if direction % 2 == 0 {
|
||||
direction_total_x
|
||||
} else {
|
||||
direction_total_y
|
||||
};
|
||||
|
||||
if direction_current == direction_total {
|
||||
if direction % 2 == 0 {
|
||||
direction_total_x += 1;
|
||||
} else {
|
||||
direction_total_y += 1;
|
||||
}
|
||||
direction = (direction + 1) % 4;
|
||||
direction_current = 0;
|
||||
}
|
||||
current += 1;
|
||||
}
|
||||
result.reverse();
|
||||
result
|
||||
}
|
||||
|
||||
pub fn update(&mut self, tile_viewbox: &TileViewbox, surfaces: &Surfaces, only_visible: bool) {
|
||||
@@ -247,7 +332,22 @@ impl PendingTiles {
|
||||
} else {
|
||||
&tile_viewbox.interest_rect
|
||||
};
|
||||
let spiral = Self::generate_spiral(spiral_rect);
|
||||
|
||||
// If the spiral rect doesn't change we do not
|
||||
// need to recalculate anything.
|
||||
// if self.spiral_rect == *spiral_rect {
|
||||
// return;
|
||||
// }
|
||||
|
||||
self.spiral_rect = *spiral_rect;
|
||||
|
||||
// We do not regenerate spiral if the spiral_rect
|
||||
// doesn't change. The spiral_rect is based on the
|
||||
// viewbox so, if the viewbox doesn't change
|
||||
// the spiral should not change.
|
||||
if self.spiral.len() != (spiral_rect.width() * spiral_rect.height()) as usize {
|
||||
self.spiral = Self::generate_spiral(spiral_rect.width(), spiral_rect.height());
|
||||
}
|
||||
|
||||
// Partition tiles into 4 priority groups (highest priority = processed last due to pop()):
|
||||
// 1. visible + cached (fastest - just blit from cache)
|
||||
@@ -259,7 +359,9 @@ impl PendingTiles {
|
||||
let mut interest_cached = Vec::new();
|
||||
let mut interest_uncached = Vec::new();
|
||||
|
||||
for tile in spiral {
|
||||
let center_tile = Tile(spiral_rect.center_x(), spiral_rect.center_y());
|
||||
for spiral_tile in self.spiral.iter() {
|
||||
let tile = Tile(spiral_tile.0 + center_tile.0, spiral_tile.1 + center_tile.1);
|
||||
let is_visible = tile_viewbox.visible_rect.contains(&tile);
|
||||
let is_cached = surfaces.has_cached_tile_surface(tile);
|
||||
|
||||
|
||||
@@ -1,40 +1,3 @@
|
||||
#[macro_export]
|
||||
macro_rules! request_animation_frame {
|
||||
() => {{
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
unsafe extern "C" {
|
||||
pub fn wapi_requestAnimationFrame() -> i32;
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let result = unsafe { wapi_requestAnimationFrame() };
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let result = 0;
|
||||
|
||||
result
|
||||
}};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! cancel_animation_frame {
|
||||
($frame_id:expr) => {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
unsafe extern "C" {
|
||||
pub fn wapi_cancelAnimationFrame(frame_id: i32);
|
||||
}
|
||||
|
||||
{
|
||||
let frame_id = $frame_id;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
unsafe {
|
||||
wapi_cancelAnimationFrame(frame_id)
|
||||
};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let _ = frame_id;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! notify_tiles_render_complete {
|
||||
() => {{
|
||||
@@ -50,6 +13,4 @@ macro_rules! notify_tiles_render_complete {
|
||||
}};
|
||||
}
|
||||
|
||||
pub use cancel_animation_frame;
|
||||
pub use notify_tiles_render_complete;
|
||||
pub use request_animation_frame;
|
||||
Reference in new issue
Block a user