mirror of
https://github.com/penpot/penpot.git
synced 2026-06-27 01:32:05 +00:00
WIP
This commit is contained in:
parent
5f1bef6bd9
commit
cf8ff3828b
@ -197,11 +197,7 @@
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state
|
||||
(assoc-in [:workspace-local :panning] true)))
|
||||
|
||||
ptk/EffectEvent
|
||||
(effect [_ state _]
|
||||
(dwvw/maybe-view-interaction-start! state))))
|
||||
(assoc-in [:workspace-local :panning] true)))))
|
||||
|
||||
(defn start-panning []
|
||||
(ptk/reify ::start-panning
|
||||
@ -229,8 +225,4 @@
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state
|
||||
(update :workspace-local dissoc :panning)))
|
||||
|
||||
ptk/EffectEvent
|
||||
(effect [_ state _]
|
||||
(dwvw/maybe-view-interaction-end! state))))
|
||||
(update :workspace-local dissoc :panning)))))
|
||||
|
||||
@ -19,12 +19,12 @@
|
||||
(when (and (features/active-feature? state "render-wasm/v1") (not (render-context-lost? state)))
|
||||
(wasm.api/sync-workspace-local-viewport! state)))
|
||||
|
||||
(defn maybe-view-interaction-start!
|
||||
#_(defn maybe-view-interaction-start!
|
||||
[state]
|
||||
(when (and (features/active-feature? state "render-wasm/v1") (not (render-context-lost? state)))
|
||||
(wasm.api/view-interaction-start!)))
|
||||
|
||||
(defn maybe-view-interaction-end!
|
||||
#_(defn maybe-view-interaction-end!
|
||||
[state]
|
||||
(when (and (features/active-feature? state "render-wasm/v1") (not (render-context-lost? state)))
|
||||
(wasm.api/view-interaction-end!)))
|
||||
(wasm.api/view-interaction-end!)))
|
||||
|
||||
@ -199,7 +199,6 @@
|
||||
(when (and (not (dwvw/render-context-lost? state))
|
||||
(not (get-in state [:workspace-local :zooming])))
|
||||
(rx/concat
|
||||
(rx/of (fn [s] (dwvw/maybe-view-interaction-start! s) s))
|
||||
(rx/of #(-> % (assoc-in [:workspace-local :zooming] true)))
|
||||
(->> stream
|
||||
(rx/filter mse/pointer-event?)
|
||||
@ -215,7 +214,4 @@
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state
|
||||
(update :workspace-local dissoc :zooming)))
|
||||
ptk/EffectEvent
|
||||
(effect [_ state _]
|
||||
(dwvw/maybe-view-interaction-end! state))))
|
||||
(update :workspace-local dissoc :zooming)))))
|
||||
|
||||
@ -362,7 +362,7 @@
|
||||
([]
|
||||
(internal-render 0))
|
||||
([timestamp]
|
||||
(set! wasm/internal-frame-type (h/call wasm/internal-module "_render" timestamp wasm/internal-frame-type))
|
||||
(set! wasm/internal-frame-type (h/call wasm/internal-module "_render2" timestamp wasm/internal-frame-type))
|
||||
(when (= wasm/internal-frame-type FRAME_TYPE_PARTIAL)
|
||||
(request-render "frame-type-partial"))))
|
||||
|
||||
@ -1297,13 +1297,13 @@
|
||||
(= result 1))
|
||||
false))
|
||||
|
||||
(defn view-interaction-start!
|
||||
#_(defn view-interaction-start!
|
||||
[]
|
||||
(when-not @view-interaction-active?
|
||||
(h/call wasm/internal-module "_set_view_start")
|
||||
(reset! view-interaction-active? true)))
|
||||
|
||||
(defn view-interaction-end!
|
||||
#_(defn view-interaction-end!
|
||||
[]
|
||||
(when @view-interaction-active?
|
||||
(perf/begin-measure "render-finish")
|
||||
@ -1311,33 +1311,35 @@
|
||||
(perf/end-measure "render-finish")
|
||||
(reset! view-interaction-active? false)))
|
||||
|
||||
(def render-finish
|
||||
(letfn [(do-render []
|
||||
;; Check if context is still initialized before executing
|
||||
;; to prevent errors when navigating quickly
|
||||
(when (initialized?)
|
||||
(view-interaction-end!)
|
||||
;; Use async _render: visible tiles render synchronously
|
||||
;; (no yield), interest-area tiles render progressively
|
||||
;; via rAF. _set_view_end already rebuilt the tile
|
||||
;; index. For pan, most tiles are cached so the render
|
||||
;; completes in the first frame. For zoom, interest-
|
||||
;; area tiles (~3 tile margin) don't block the main
|
||||
;; thread.
|
||||
(internal-render)))]
|
||||
(fns/debounce do-render DEBOUNCE_DELAY_MS)))
|
||||
#_(def render-finish
|
||||
(letfn [(do-render []
|
||||
;; Check if context is still initialized before executing
|
||||
;; to prevent errors when navigating quickly
|
||||
(when (initialized?)
|
||||
#_(view-interaction-end!)
|
||||
;; Use async _render: visible tiles render synchronously
|
||||
;; (no yield), interest-area tiles render progressively
|
||||
;; via rAF. _set_view_end already rebuilt the tile
|
||||
;; index. For pan, most tiles are cached so the render
|
||||
;; completes in the first frame. For zoom, interest-
|
||||
;; area tiles (~3 tile margin) don't block the main
|
||||
;; thread.
|
||||
(internal-render)))]
|
||||
(fns/debounce do-render DEBOUNCE_DELAY_MS)))
|
||||
|
||||
(defn set-view-box
|
||||
[zoom vbox]
|
||||
(perf/begin-measure "set-view-box")
|
||||
(view-interaction-start!)
|
||||
#_(view-interaction-start!)
|
||||
(h/call wasm/internal-module "_set_view" zoom (- (:x vbox)) (- (:y vbox)))
|
||||
(perf/end-measure "set-view-box")
|
||||
|
||||
(perf/begin-measure "render-from-cache")
|
||||
(h/call wasm/internal-module "_render_from_cache" 0)
|
||||
(render-finish)
|
||||
(perf/end-measure "render-from-cache"))
|
||||
#_(perf/begin-measure "render-from-cache")
|
||||
#_(h/call wasm/internal-module "_render_from_cache" 0)
|
||||
#_(render-finish)
|
||||
#_(view-interaction-end!)
|
||||
(internal-render)
|
||||
#_(perf/end-measure "render-from-cache"))
|
||||
|
||||
(defn sync-workspace-local-viewport!
|
||||
"Pushes `[:workspace-local :zoom]` and `:vbox` into WASM."
|
||||
|
||||
@ -11,6 +11,7 @@ build = "build.rs"
|
||||
[features]
|
||||
default = []
|
||||
stats = []
|
||||
|
||||
profile = ["profile-macros", "profile-raf"]
|
||||
profile-macros = []
|
||||
profile-raf = []
|
||||
|
||||
@ -20,7 +20,7 @@ use std::collections::HashMap;
|
||||
|
||||
#[allow(unused_imports)]
|
||||
use crate::error::{Error, Result};
|
||||
use crate::render::{FrameType, RenderFlag};
|
||||
use crate::{render::{FrameType, RenderFlag}, shapes::Frame};
|
||||
|
||||
use globals::{get_design_state, get_gpu_state, get_render_state};
|
||||
|
||||
@ -103,10 +103,42 @@ pub extern "C" fn set_canvas_background(raw_color: u32) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
pub extern "C" fn render2(timestamp: i32, flags: u8) -> Result<FrameType> {
|
||||
with_state!(state, {
|
||||
let render_state = get_render_state();
|
||||
|
||||
render_state.prepare_render_loop(&mut state.shapes)?;
|
||||
let frame_type = if flags & RenderFlag::Partial as u8 == RenderFlag::Partial as u8 {
|
||||
// TODO: Meter una flag que lo que haga es indicar
|
||||
// si el render es sync o no y que esto no permita
|
||||
let allow_stop = true;
|
||||
render_state
|
||||
.continue_render_loop(
|
||||
timestamp,
|
||||
allow_stop
|
||||
)
|
||||
.map_err(|_| Error::RecoverableError("Error rendering".to_string()))?
|
||||
} else {
|
||||
let sync_render = false;
|
||||
render_state.start_render_loop(
|
||||
timestamp,
|
||||
sync_render
|
||||
).map_err(|_| Error::RecoverableError("Error rendering".to_string()))?
|
||||
};
|
||||
render_state.end_render_loop(&frame_type);
|
||||
return Ok(frame_type);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
pub extern "C" fn render(timestamp: i32, flags: u8) -> Result<FrameType> {
|
||||
with_state!(state, {
|
||||
panic!("No debería llamarse");
|
||||
state.rebuild_touched_tiles();
|
||||
// Drain the throttled modifier-tile invalidation accumulated
|
||||
// since the previous rAF. set_modifiers skips this work during
|
||||
@ -133,11 +165,13 @@ pub extern "C" fn render(timestamp: i32, flags: u8) -> Result<FrameType> {
|
||||
return Ok(frame_type);
|
||||
});
|
||||
}
|
||||
*/
|
||||
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
pub extern "C" fn render_ui_only() -> Result<()> {
|
||||
with_state!(state, {
|
||||
// panic!("render_ui_only");
|
||||
state.render_ui_only();
|
||||
});
|
||||
Ok(())
|
||||
@ -215,21 +249,21 @@ pub extern "C" fn render_sync_shape(a: u32, b: u32, c: u32, d: u32) -> Result<()
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
pub extern "C" fn render_from_cache(_: i32) -> Result<()> {
|
||||
with_state!(state, {
|
||||
// Don't cancel the animation frame — let the async render
|
||||
// continue populating the tile HashMap in the background.
|
||||
// `continue_render_loop` skips flush_and_submit in fast
|
||||
// mode so it won't present stale Target content. The
|
||||
// tile HashMap is position-independent, so tiles rendered
|
||||
// for the old viewport can be reused by the next full
|
||||
// render at the new viewport position.
|
||||
state.render_from_cache();
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
// #[no_mangle]
|
||||
// #[wasm_error]
|
||||
// pub extern "C" fn render_from_cache(_: i32) -> Result<()> {
|
||||
// with_state!(state, {
|
||||
// // Don't cancel the animation frame — let the async render
|
||||
// // continue populating the tile HashMap in the background.
|
||||
// // `continue_render_loop` skips flush_and_submit in fast
|
||||
// // mode so it won't present stale Target content. The
|
||||
// // tile HashMap is position-independent, so tiles rendered
|
||||
// // for the old viewport can be reused by the next full
|
||||
// // render at the new viewport position.
|
||||
// state.render_from_cache();
|
||||
// });
|
||||
// Ok(())
|
||||
// }
|
||||
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
@ -311,13 +345,13 @@ static mut VIEW_INTERACTION_START: i32 = 0;
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
pub extern "C" fn set_view_start() -> Result<()> {
|
||||
#[cfg(feature = "profile-macros")]
|
||||
unsafe {
|
||||
VIEW_INTERACTION_START = performance::get_time();
|
||||
}
|
||||
performance::begin_measure!("set_view_start");
|
||||
get_render_state().options.set_fast_mode(true);
|
||||
performance::end_measure!("set_view_start");
|
||||
// #[cfg(feature = "profile-macros")]
|
||||
// unsafe {
|
||||
// VIEW_INTERACTION_START = performance::get_time();
|
||||
// }
|
||||
// performance::begin_measure!("set_view_start");
|
||||
// get_render_state().options.set_fast_mode(true);
|
||||
// performance::end_measure!("set_view_start");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -329,32 +363,19 @@ pub extern "C" fn set_view_start() -> Result<()> {
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
pub extern "C" fn set_view_end() -> Result<()> {
|
||||
with_state!(state, {
|
||||
performance::begin_measure!("set_view_end");
|
||||
let render_state = get_render_state();
|
||||
render_state.options.set_fast_mode(false);
|
||||
render_state.tile_viewbox.update(&render_state.viewbox);
|
||||
// with_state!(state, {
|
||||
// performance::begin_measure!("set_view_end");
|
||||
// let render_state = get_render_state();
|
||||
// render_state.options.set_fast_mode(false);
|
||||
// render_state.tile_viewbox.update(&render_state.viewbox);
|
||||
|
||||
if render_state.options.is_profile_rebuild_tiles() {
|
||||
state.rebuild_tiles();
|
||||
} else if render_state.zoom_changed() {
|
||||
// Zoom changed: tile sizes differ so all cached tile
|
||||
// textures are invalid (wrong scale). Rebuild the tile
|
||||
// index and clear the tile texture cache, but *preserve*
|
||||
// the cache canvas so render_from_cache can show a scaled
|
||||
// preview of the old content while new tiles render.
|
||||
render_state.rebuild_tile_index(&state.shapes);
|
||||
render_state.surfaces.invalidate_tile_cache();
|
||||
} else {
|
||||
// Pure pan at the same zoom level: tile contents have not
|
||||
// changed — only the viewport position moved. Update the
|
||||
// tile index (which tiles are in the interest area) but
|
||||
// keep cached tile textures so the render can blit them
|
||||
// instead of re-drawing every visible tile from scratch.
|
||||
render_state.rebuild_tile_index(&state.shapes);
|
||||
}
|
||||
performance::end_measure!("set_view_end");
|
||||
});
|
||||
// render_state.rebuild_tile_index(&state.shapes);
|
||||
// if render_state.viewbox.is_zoom_changed() {
|
||||
// render_state.surfaces.invalidate_tile_cache();
|
||||
// }
|
||||
|
||||
// performance::end_measure!("set_view_end");
|
||||
// });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -368,7 +389,7 @@ pub extern "C" fn set_view_end() -> Result<()> {
|
||||
pub extern "C" fn set_modifiers_start() -> Result<()> {
|
||||
performance::begin_measure!("set_modifiers_start");
|
||||
let render_state = get_render_state();
|
||||
render_state.options.set_fast_mode(true);
|
||||
// render_state.options.set_fast_mode(true);
|
||||
render_state.options.set_interactive_transform(true);
|
||||
performance::end_measure!("set_modifiers_start");
|
||||
Ok(())
|
||||
@ -383,7 +404,7 @@ pub extern "C" fn set_modifiers_start() -> Result<()> {
|
||||
pub extern "C" fn set_modifiers_end() -> Result<()> {
|
||||
performance::begin_measure!("set_modifiers_end");
|
||||
let render_state = get_render_state();
|
||||
render_state.options.set_fast_mode(false);
|
||||
// render_state.options.set_fast_mode(false);
|
||||
render_state.options.set_interactive_transform(false);
|
||||
performance::end_measure!("set_modifiers_end");
|
||||
Ok(())
|
||||
@ -868,7 +889,7 @@ pub extern "C" fn clean_modifiers() -> Result<()> {
|
||||
// the same tiles for the active modifier set, so the eviction
|
||||
// here is redundant and doubles the per-emission cost.
|
||||
if !prev_modifier_ids.is_empty() && !render_state.options.is_interactive_transform() {
|
||||
render_state.update_tiles_shapes(&prev_modifier_ids, &mut state.shapes)?;
|
||||
render_state.update_tiles_shapes(&prev_modifier_ids)?;
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -7,9 +7,12 @@ pub fn render_background_blur(
|
||||
shape: &Shape,
|
||||
target_surface: SurfaceId,
|
||||
) {
|
||||
/*
|
||||
if render_state.options.is_fast_mode() {
|
||||
return;
|
||||
}
|
||||
*/
|
||||
|
||||
if matches!(shape.shape_type, Type::Text(_)) || matches!(shape.shape_type, Type::SVGRaw(_)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1,150 +0,0 @@
|
||||
use crate::error::{Error, Result};
|
||||
use crate::state::ShapesPoolRef;
|
||||
use crate::tiles;
|
||||
|
||||
pub fn apply_render_to_final_canvas(render_state: &mut crate::render::RenderState) -> Result<()> {
|
||||
// During interactive transforms we render tiles directly into Target; updating the cache
|
||||
// (snapshot -> atlas blit -> tiles.add) can force GPU stalls. Defer cache rebuild until
|
||||
// the interaction ends.
|
||||
if render_state.options.is_interactive_transform() {
|
||||
let tile_rect = render_state.get_current_aligned_tile_bounds()?;
|
||||
render_state.surfaces.draw_current_tile_into_backbuffer(
|
||||
&tile_rect,
|
||||
render_state.background_color,
|
||||
surfaces::DrawOnCache::No,
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Viewer masked passes render a partial scene. Reusing the tile texture cache would
|
||||
// SrcOver-blend onto textures from the previous pass and leak pixels into the blob.
|
||||
if render_state.viewer_masked_pass() {
|
||||
// Use viewbox-aligned bounds (not grid-snapped) to match interactive-transform
|
||||
// compositing and avoid a visible offset vs the DOM canvas.
|
||||
let tile_rect = render_state.get_current_tile_bounds()?;
|
||||
render_state.surfaces.draw_current_tile_into_backbuffer(
|
||||
&tile_rect,
|
||||
render_state.background_color,
|
||||
surfaces::DrawOnCache::No,
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let fast_mode = render_state.options.is_fast_mode();
|
||||
// Decide *now* (at the first real cache blit) whether we need to clear Cache.
|
||||
// This avoids clearing Cache on renders that don't actually paint tiles (e.g. hover/UI),
|
||||
// while still preventing stale pixels from surviving across full-quality renders.
|
||||
if !fast_mode && !render_state.cache_cleared_this_render {
|
||||
render_state.surfaces.clear_cache(render_state.background_color);
|
||||
render_state.cache_cleared_this_render = true;
|
||||
}
|
||||
// In fast mode the viewport is moving (pan/zoom) so Cache surface
|
||||
// positions would be wrong — only save to the tile HashMap.
|
||||
let tile_rect = render_state.get_current_aligned_tile_bounds()?;
|
||||
|
||||
let current_tile = *render_state
|
||||
.current_tile
|
||||
.as_ref()
|
||||
.ok_or(Error::CriticalError("Current tile not found".to_string()))?;
|
||||
|
||||
render_state.surfaces.draw_current_tile_into_tile_atlas(
|
||||
&render_state.tile_viewbox,
|
||||
¤t_tile,
|
||||
&tile_rect,
|
||||
fast_mode,
|
||||
render_state.render_area,
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn render_from_cache(render_state: &mut crate::render::RenderState, shapes: ShapesPoolRef) {
|
||||
let _start = performance::begin_timed_log!("render_from_cache");
|
||||
performance::begin_measure!("render_from_cache");
|
||||
let bg_color = render_state.background_color;
|
||||
|
||||
// During fast mode (pan/zoom), if a previous full-quality render still has pending tiles,
|
||||
// always prefer the persistent atlas. The atlas is incrementally updated as tiles finish,
|
||||
// and drawing from it avoids mixing a partially-updated Cache surface with missing tiles.
|
||||
if render_state.options.is_fast_mode() && !render_state.surfaces.atlas.is_empty() {
|
||||
render_state.surfaces
|
||||
.draw_atlas_to_backbuffer(render_state.viewbox, bg_color);
|
||||
|
||||
render_state.present_frame(shapes);
|
||||
performance::end_measure!("render_from_cache");
|
||||
performance::end_timed_log!("render_from_cache", _start);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we have a valid cached viewbox (non-zero dimensions indicate valid cache)
|
||||
if render_state.cached_viewbox.area.width() > 0.0 {
|
||||
// Scale and translate the target according to the cached data
|
||||
let navigate_zoom = render_state.viewbox.zoom / render_state.cached_viewbox.zoom;
|
||||
|
||||
let interest = render_state.options.dpr_viewport_interest_area_threshold;
|
||||
let TileRect(start_tile_x, start_tile_y, _, _) =
|
||||
tiles::get_tiles_for_viewbox_with_interest(&render_state.cached_viewbox, interest);
|
||||
let offset_x = render_state.viewbox.area.left * render_state.cached_viewbox.zoom * render_state.options.dpr;
|
||||
let offset_y = render_state.viewbox.area.top * render_state.cached_viewbox.zoom * render_state.options.dpr;
|
||||
let translate_x = (start_tile_x as f32 * tiles::TILE_SIZE) - offset_x;
|
||||
let translate_y = (start_tile_y as f32 * tiles::TILE_SIZE) - offset_y;
|
||||
|
||||
// For zoom-out, prefer cache only if it fully covers the viewport.
|
||||
// Otherwise, atlas will provide a more correct full-viewport preview.
|
||||
let zooming_out = render_state.viewbox.zoom < render_state.cached_viewbox.zoom;
|
||||
if zooming_out {
|
||||
let cache_dim = render_state.surfaces.cache_dimensions();
|
||||
let cache_w = cache_dim.width as f32;
|
||||
let cache_h = cache_dim.height as f32;
|
||||
|
||||
// Viewport in target pixels.
|
||||
let vw = render_state.viewbox.dpr_width().max(1.0);
|
||||
let vh = render_state.viewbox.dpr_height().max(1.0);
|
||||
|
||||
// Inverse-map viewport corners into cache coordinates.
|
||||
// target = (cache * navigate_zoom) translated by (translate_x, translate_y) (in cache coords).
|
||||
// => cache = (target / navigate_zoom) - translate
|
||||
let inv = if navigate_zoom.abs() > f32::EPSILON {
|
||||
1.0 / navigate_zoom
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
// let cx0 = (0.0 * inv) - translate_x;
|
||||
// let cy0 = (0.0 * inv) - translate_y;
|
||||
// NOTA: 0.0 * inv => siempre 0
|
||||
let cx0 = -translate_x;
|
||||
let cy0 = -translate_y;
|
||||
let cx1 = (vw * inv) - translate_x;
|
||||
let cy1 = (vh * inv) - translate_y;
|
||||
|
||||
let min_x = cx0.min(cx1);
|
||||
let min_y = cy0.min(cy1);
|
||||
let max_x = cx0.max(cx1);
|
||||
let max_y = cy0.max(cy1);
|
||||
|
||||
let cache_covers =
|
||||
min_x >= 0.0 && min_y >= 0.0 && max_x <= cache_w && max_y <= cache_h;
|
||||
if !cache_covers {
|
||||
// Early return only if atlas exists; otherwise keep cache path.
|
||||
if !render_state.surfaces.atlas.is_empty() {
|
||||
render_state.surfaces
|
||||
.draw_atlas_to_backbuffer(render_state.viewbox, bg_color);
|
||||
|
||||
render_state.present_frame(shapes);
|
||||
performance::end_measure!("render_from_cache");
|
||||
performance::end_timed_log!("render_from_cache", _start);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw directly from cache surface, avoiding snapshot overhead
|
||||
render_state.surfaces.draw_cache_to_backbuffer();
|
||||
|
||||
render_state.present_frame(shapes);
|
||||
}
|
||||
|
||||
performance::end_measure!("render_from_cache");
|
||||
performance::end_timed_log!("render_from_cache", _start);
|
||||
}
|
||||
@ -36,16 +36,6 @@ fn render_debug_view(render_state: &mut RenderState) {
|
||||
.draw_rect(rect, &paint);
|
||||
}
|
||||
|
||||
pub fn render_debug_cache_surface(render_state: &mut RenderState) {
|
||||
let canvas = render_state.surfaces.canvas(SurfaceId::Debug);
|
||||
canvas.save();
|
||||
canvas.scale((0.1, 0.1));
|
||||
render_state
|
||||
.surfaces
|
||||
.draw_into(SurfaceId::Cache, SurfaceId::Debug, None);
|
||||
render_state.surfaces.canvas(SurfaceId::Debug).restore();
|
||||
}
|
||||
|
||||
pub fn render_wasm_label(render_state: &mut RenderState) {
|
||||
if render_state.preview_mode || !render_state.options.show_wasm_info() {
|
||||
return;
|
||||
@ -79,7 +69,7 @@ pub fn render_wasm_label(render_state: &mut RenderState) {
|
||||
}
|
||||
|
||||
pub fn render_debug_tiles_for_viewbox(render_state: &mut RenderState) {
|
||||
let tiles::TileRect(sx, sy, ex, ey) = render_state.tile_viewbox.interest_rect;
|
||||
let tiles::TileRect(sx, sy, ex, ey) = render_state.tile.tile_viewbox.interest_rect;
|
||||
let canvas = render_state.surfaces.canvas(SurfaceId::Debug);
|
||||
let mut paint = skia::Paint::default();
|
||||
paint.set_color(skia::Color::RED);
|
||||
@ -264,48 +254,6 @@ pub fn console_debug_surface_rect(render_state: &mut RenderState, id: SurfaceId,
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub extern "C" fn capture_frames(capture_frames: i32) -> Result<()> {
|
||||
get_render_state()
|
||||
.options
|
||||
.set_capture_frames(capture_frames);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub extern "C" fn debug_cache_console() -> Result<()> {
|
||||
console_debug_surface(get_render_state(), SurfaceId::Cache);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub extern "C" fn debug_cache_base64() -> Result<()> {
|
||||
console_debug_surface_base64(get_render_state(), SurfaceId::Cache);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub extern "C" fn debug_atlas_console() -> Result<()> {
|
||||
console_debug_surface(get_render_state(), SurfaceId::Atlas);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub extern "C" fn debug_atlas_base64() -> Result<()> {
|
||||
console_debug_surface_base64(get_render_state(), SurfaceId::Atlas);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
|
||||
@ -1,330 +0,0 @@
|
||||
use skia_safe::{self as skia, Rect};
|
||||
|
||||
use super::RenderState;
|
||||
use super::SurfaceId;
|
||||
use crate::get_gpu_state;
|
||||
use crate::math;
|
||||
use crate::state::ShapesPoolRef;
|
||||
use crate::uuid::Uuid;
|
||||
|
||||
pub struct InteractiveDragCrop {
|
||||
pub src_doc_bounds: Rect,
|
||||
pub src_selrect: Rect,
|
||||
/// Viewbox origin (doc-space) at capture time.
|
||||
pub capture_vb_left: f32,
|
||||
pub capture_vb_top: f32,
|
||||
/// Backbuffer pixel origin used for `snapshot_rect` (so we can do 1:1 blits).
|
||||
pub capture_src_left: i32,
|
||||
pub capture_src_top: i32,
|
||||
pub image: skia::Image,
|
||||
}
|
||||
|
||||
/// Chooses a window inside the full workspace-pixel crop `[0, out_w) × [0, out_h)` with each side
|
||||
/// at most `max_side_px` (**without scaling**): centered on the projection of
|
||||
/// `viewport_doc ∩ src_doc_bounds`, or on the full crop if that intersection is empty.
|
||||
/// `max_side_px` should match [`GpuState::max_texture_size`] (same budget as the atlas).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn drag_crop_snapshot_window_px(
|
||||
max_side_px: i32,
|
||||
out_w: i32,
|
||||
out_h: i32,
|
||||
viewport_doc: Rect,
|
||||
vb_left: f32,
|
||||
vb_top: f32,
|
||||
scale: f32,
|
||||
src_left_px: i32,
|
||||
src_top_px: i32,
|
||||
src_doc_bounds: Rect,
|
||||
) -> (i32, i32, i32, i32) {
|
||||
let cap = max_side_px.max(1);
|
||||
if out_w <= cap && out_h <= cap {
|
||||
return (0, 0, out_w, out_h);
|
||||
}
|
||||
let win_w = out_w.min(cap);
|
||||
let win_h = out_h.min(cap);
|
||||
|
||||
let mut vis = viewport_doc;
|
||||
let has_vis = vis.intersect(src_doc_bounds);
|
||||
let (cx, cy) = if !has_vis || vis.is_empty() {
|
||||
(out_w as f32 * 0.5, out_h as f32 * 0.5)
|
||||
} else {
|
||||
let lx0 = (vis.left - vb_left) * scale - src_left_px as f32;
|
||||
let ly0 = (vis.top - vb_top) * scale - src_top_px as f32;
|
||||
let lx1 = (vis.right - vb_left) * scale - src_left_px as f32;
|
||||
let ly1 = (vis.bottom - vb_top) * scale - src_top_px as f32;
|
||||
((lx0 + lx1) * 0.5, (ly0 + ly1) * 0.5)
|
||||
};
|
||||
|
||||
let mut ox = (cx - win_w as f32 * 0.5).round() as i32;
|
||||
let mut oy = (cy - win_h as f32 * 0.5).round() as i32;
|
||||
ox = ox.clamp(0, out_w - win_w);
|
||||
oy = oy.clamp(0, out_h - win_h);
|
||||
(ox, oy, win_w, win_h)
|
||||
}
|
||||
|
||||
/// Decide whether a top-level node can be served from `backbuffer_crop_cache` during an
|
||||
/// interactive transform (drag/resize/rotate).
|
||||
///
|
||||
/// We only reuse cached pixels when it is safe and visually correct:
|
||||
/// - **Top-level only**: cache entries are built for direct children of the root.
|
||||
/// - **Moved node**: only allow cache reuse for *pure translations* (no scale/rotate/skew),
|
||||
/// because other transforms would require resampling and can diverge from the live render.
|
||||
/// - **Other cached nodes**: if the moving bounds overlap this cached crop, invalidate it so
|
||||
/// we don't show stale content while something moves over/inside it.
|
||||
pub fn should_use_cached_top_level_during_interactive(
|
||||
render_state: &mut RenderState,
|
||||
node_id: Uuid,
|
||||
tree: ShapesPoolRef,
|
||||
moved_ids: &[Uuid],
|
||||
moved_bounds: Option<Rect>,
|
||||
) -> bool {
|
||||
if !render_state.backbuffer_crop_cache.contains_key(&node_id) {
|
||||
return false;
|
||||
}
|
||||
let Some(raw) = tree.get_raw(&node_id) else {
|
||||
return false;
|
||||
};
|
||||
if raw.parent_id != Some(Uuid::nil()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If this top-level shape itself is being moved, always allow using its cached pixels.
|
||||
// BUT only for pure translations. For non-translation transforms (scale/rotate/skew),
|
||||
// cached pixels won't match the live result (and may require resampling), so render live.
|
||||
if moved_ids.contains(&node_id) {
|
||||
let Some(m) = tree.get_modifier(&node_id) else {
|
||||
return false;
|
||||
};
|
||||
// Only allow using the cached pixels for pure translations.
|
||||
// For non-translation transforms (scale/rotate/skew), cached pixels won't match.
|
||||
// If the transform is the identity means a reflow, we need to redraw as well.
|
||||
if math::identitish(m) || !math::is_move_only_matrix(m) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Additionally require this node to be safe to serve from a rectangular backbuffer
|
||||
// crop while moving; otherwise it must be rendered live (e.g. text, overflow frames).
|
||||
return tree
|
||||
.get(&node_id)
|
||||
.is_some_and(|s| s.is_safe_for_drag_crop_cache(tree));
|
||||
}
|
||||
|
||||
// If the moving content overlaps this cached crop, do not use the cached pixels
|
||||
// for this frame. We intentionally keep the cache entry: overlap is typically
|
||||
// transient during drag, and once the moving content leaves the area the crop
|
||||
// becomes valid again (stationary shape unchanged).
|
||||
if let Some(moved) = moved_bounds {
|
||||
let intersects = render_state
|
||||
.backbuffer_crop_cache
|
||||
.get(&node_id)
|
||||
.is_some_and(|crop| moved.intersects(crop.src_doc_bounds));
|
||||
|
||||
if intersects {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub fn rebuild_backbuffer_crop_cache(render_state: &mut RenderState, tree: ShapesPoolRef) {
|
||||
render_state.backbuffer_crop_cache.clear();
|
||||
|
||||
// Collect candidate shapes that are "recortable" and visible in the current viewport.
|
||||
|
||||
// This is intentionally conservative; we only cache shapes that do not overlap with
|
||||
// ANY other candidate to guarantee the pixels under their bounds belong exclusively
|
||||
// to that shape in Backbuffer.
|
||||
let viewport = render_state.viewbox.area;
|
||||
let scale = render_state.get_scale();
|
||||
let mut candidates: Vec<(Uuid, Rect, Rect)> = Vec::new(); // (id, doc_bounds, selrect)
|
||||
|
||||
let root_ids: Vec<Uuid> = match tree.get(&Uuid::nil()) {
|
||||
Some(root) => root.children_ids(false),
|
||||
None => Vec::new(),
|
||||
};
|
||||
|
||||
for shape_id in root_ids {
|
||||
let Some(shape) = tree.get(&shape_id) else {
|
||||
continue;
|
||||
};
|
||||
if shape.hidden {
|
||||
continue;
|
||||
}
|
||||
|
||||
let doc_bounds = shape.extrect(tree, 1.0);
|
||||
if !doc_bounds.intersects(viewport) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Also require selrect to be visible; used for drag delta placement.
|
||||
let selrect = shape.selrect();
|
||||
if !selrect.intersects(viewport) {
|
||||
continue;
|
||||
}
|
||||
|
||||
candidates.push((shape.id, doc_bounds, selrect));
|
||||
}
|
||||
|
||||
// Filter out any candidate that overlaps with any other candidate.
|
||||
// Sort by left edge so the inner loop can break early once no further
|
||||
// x-overlap is possible, reducing comparisons from O(N²) to O(N log N)
|
||||
// in typical layouts where shapes are spread out.
|
||||
candidates.sort_unstable_by(|a, b| {
|
||||
a.1.left
|
||||
.partial_cmp(&b.1.left)
|
||||
.unwrap_or(std::cmp::Ordering::Equal)
|
||||
});
|
||||
let n = candidates.len();
|
||||
let mut is_overlapping = vec![false; n];
|
||||
for i in 0..n {
|
||||
for j in (i + 1)..n {
|
||||
if candidates[j].1.left >= candidates[i].1.right {
|
||||
break; // sorted: no further x-overlap possible for i
|
||||
}
|
||||
if is_overlapping[i] && is_overlapping[j] {
|
||||
continue; // both already excluded, skip check
|
||||
}
|
||||
if candidates[i].1.intersects(candidates[j].1) {
|
||||
is_overlapping[i] = true;
|
||||
is_overlapping[j] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
let non_overlapping: Vec<(Uuid, Rect, Rect)> = candidates
|
||||
.iter()
|
||||
.zip(is_overlapping.iter())
|
||||
.filter_map(|((id, bounds, selrect), ov)| {
|
||||
if !ov {
|
||||
Some((*id, *bounds, *selrect))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let vb_left = render_state.viewbox.area.left;
|
||||
let vb_top = render_state.viewbox.area.top;
|
||||
let (bb_w, bb_h) = render_state.surfaces.surface_size(SurfaceId::Backbuffer);
|
||||
let max_snap_px = get_gpu_state().max_texture_size();
|
||||
|
||||
// Snapshot the atlas once for the whole pass so that all shapes sharing
|
||||
// the tile/atlas fallback path reuse the same GPU image rather than each
|
||||
// triggering a separate `image_snapshot` flush.
|
||||
let atlas_snap = render_state.surfaces.atlas.snapshot_for_drag_crop();
|
||||
|
||||
// Scratch surface reused across all shapes that need the tile/atlas
|
||||
// fallback — avoids one WebGL texture allocation per shape.
|
||||
// Created lazily on first use and grown if a later shape needs more space.
|
||||
let mut scratch_surface: Option<skia::Surface> = None;
|
||||
|
||||
for (id, doc_bounds, selrect) in non_overlapping {
|
||||
let left = ((doc_bounds.left - vb_left) * scale).floor() as i32;
|
||||
let top = ((doc_bounds.top - vb_top) * scale).floor() as i32;
|
||||
let right = ((doc_bounds.right - vb_left) * scale).ceil() as i32;
|
||||
let bottom = ((doc_bounds.bottom - vb_top) * scale).ceil() as i32;
|
||||
if right <= left || bottom <= top {
|
||||
continue;
|
||||
}
|
||||
let src_irect = skia::IRect::new(left, top, right, bottom);
|
||||
|
||||
let src_doc_bounds = Rect::new(
|
||||
src_irect.left as f32 / scale + vb_left,
|
||||
src_irect.top as f32 / scale + vb_top,
|
||||
src_irect.right as f32 / scale + vb_left,
|
||||
src_irect.bottom as f32 / scale + vb_top,
|
||||
);
|
||||
|
||||
let full_w = src_irect.width();
|
||||
let full_h = src_irect.height();
|
||||
let (win_ox, win_oy, win_w, win_h) = drag_crop_snapshot_window_px(
|
||||
max_snap_px,
|
||||
full_w,
|
||||
full_h,
|
||||
viewport,
|
||||
vb_left,
|
||||
vb_top,
|
||||
scale,
|
||||
src_irect.left,
|
||||
src_irect.top,
|
||||
src_doc_bounds,
|
||||
);
|
||||
let window_irect = skia::IRect::new(
|
||||
src_irect.left + win_ox,
|
||||
src_irect.top + win_oy,
|
||||
src_irect.left + win_ox + win_w,
|
||||
src_irect.top + win_oy + win_h,
|
||||
);
|
||||
|
||||
let src_doc_window = Rect::new(
|
||||
window_irect.left as f32 / scale + vb_left,
|
||||
window_irect.top as f32 / scale + vb_top,
|
||||
window_irect.right as f32 / scale + vb_left,
|
||||
window_irect.bottom as f32 / scale + vb_top,
|
||||
);
|
||||
|
||||
let in_backbuffer = window_irect.left >= 0
|
||||
&& window_irect.top >= 0
|
||||
&& window_irect.right <= bb_w
|
||||
&& window_irect.bottom <= bb_h;
|
||||
|
||||
let backbuffer_snap = if in_backbuffer {
|
||||
render_state
|
||||
.surfaces
|
||||
.snapshot_rect(SurfaceId::Backbuffer, window_irect)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let image = if let Some(img) = backbuffer_snap {
|
||||
img
|
||||
} else {
|
||||
// Ensure the scratch surface is large enough for this window.
|
||||
// Grow (reallocate) only when necessary so that the common case
|
||||
// of similarly-sized shapes pays zero extra allocation cost.
|
||||
let needs_alloc = scratch_surface
|
||||
.as_ref()
|
||||
.is_none_or(|s| s.width() < win_w || s.height() < win_h);
|
||||
if needs_alloc {
|
||||
scratch_surface = get_gpu_state()
|
||||
.create_surface_with_isize(
|
||||
"drag_crop_scratch".to_string(),
|
||||
skia::ISize::new(win_w, win_h),
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
let Some(scratch) = scratch_surface.as_mut() else {
|
||||
continue;
|
||||
};
|
||||
let Some(img) = render_state
|
||||
.surfaces
|
||||
.try_snapshot_doc_rect_from_tiles_and_atlas(
|
||||
scratch,
|
||||
atlas_snap.as_ref(),
|
||||
src_doc_window,
|
||||
window_irect,
|
||||
win_w,
|
||||
win_h,
|
||||
vb_left,
|
||||
vb_top,
|
||||
scale,
|
||||
)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
img
|
||||
};
|
||||
|
||||
render_state.backbuffer_crop_cache.insert(
|
||||
id,
|
||||
InteractiveDragCrop {
|
||||
src_doc_bounds: src_doc_window,
|
||||
src_selrect: selrect,
|
||||
capture_vb_left: vb_left,
|
||||
capture_vb_top: vb_top,
|
||||
capture_src_left: window_irect.left,
|
||||
capture_src_top: window_irect.top,
|
||||
image,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -335,8 +335,7 @@ pub fn render_element_drop_shadows_and_composite(
|
||||
}
|
||||
|
||||
if let Some(clips) = clip_bounds.as_ref() {
|
||||
let antialias = !render_state.options.is_fast_mode()
|
||||
&& element.should_use_antialias(scale, render_state.options.antialias_threshold);
|
||||
let antialias = element.should_use_antialias(scale, render_state.options.antialias_threshold);
|
||||
render_state.surfaces.canvas(target_surface).save();
|
||||
render_state.clip_target_surface_to_stack(clips, target_surface, scale, antialias);
|
||||
render_state
|
||||
|
||||
@ -27,8 +27,7 @@ pub fn render_shape_enter(
|
||||
render_state.surfaces.canvas(target_surface).save();
|
||||
if let Some(clips) = clip_bounds {
|
||||
let scale = render_state.get_scale();
|
||||
let antialias = !render_state.options.is_fast_mode()
|
||||
&& element
|
||||
let antialias = element
|
||||
.should_use_antialias(scale, render_state.options.antialias_threshold);
|
||||
render_state.clip_target_surface_to_stack(
|
||||
clips,
|
||||
@ -40,15 +39,13 @@ pub fn render_shape_enter(
|
||||
}
|
||||
|
||||
let mut paint = skia::Paint::default();
|
||||
if !render_state.options.is_fast_mode() {
|
||||
if let Some(blur) = element.masked_group_layer_blur() {
|
||||
let scale = render_state.get_scale();
|
||||
let sigma = radius_to_sigma(blur.value * scale);
|
||||
if let Some(filter) =
|
||||
skia::image_filters::blur((sigma, sigma), None, None, None)
|
||||
{
|
||||
paint.set_image_filter(filter);
|
||||
}
|
||||
if let Some(blur) = element.masked_group_layer_blur() {
|
||||
let scale = render_state.get_scale();
|
||||
let sigma = radius_to_sigma(blur.value * scale);
|
||||
if let Some(filter) =
|
||||
skia::image_filters::blur((sigma, sigma), None, None, None)
|
||||
{
|
||||
paint.set_image_filter(filter);
|
||||
}
|
||||
}
|
||||
|
||||
@ -81,13 +78,11 @@ pub fn render_shape_enter(
|
||||
paint.set_blend_mode(element.blend_mode().into());
|
||||
paint.set_alpha_f(element.opacity());
|
||||
|
||||
if !render_state.options.is_fast_mode() {
|
||||
if let Some(frame_blur) = layer_blur::frame_clip_layer_blur(element) {
|
||||
let scale = render_state.get_scale();
|
||||
let sigma = radius_to_sigma(frame_blur.value * scale);
|
||||
if let Some(filter) = skia::image_filters::blur((sigma, sigma), None, None, None) {
|
||||
paint.set_image_filter(filter);
|
||||
}
|
||||
if let Some(frame_blur) = layer_blur::frame_clip_layer_blur(element) {
|
||||
let scale = render_state.get_scale();
|
||||
let sigma = radius_to_sigma(frame_blur.value * scale);
|
||||
if let Some(filter) = skia::image_filters::blur((sigma, sigma), None, None, None) {
|
||||
paint.set_image_filter(filter);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -19,7 +19,7 @@ pub fn render_shape_pixels(
|
||||
let saved_export_context = render_state.export_context;
|
||||
let saved_render_area = render_state.render_area;
|
||||
let saved_render_area_with_margins = render_state.render_area_with_margins;
|
||||
let saved_current_tile = render_state.current_tile;
|
||||
let saved_current_tile = render_state.tile.current_tile;
|
||||
let saved_pending_nodes = std::mem::take(&mut render_state.pending_nodes);
|
||||
let saved_nested_fills = std::mem::take(&mut render_state.nested_fills);
|
||||
let saved_nested_blurs = std::mem::take(&mut render_state.nested_blurs);
|
||||
@ -56,7 +56,7 @@ pub fn render_shape_pixels(
|
||||
mask: false,
|
||||
flattened: false,
|
||||
});
|
||||
render_state.render_shape_tree_partial_uncached(tree, timestamp, false, true)?;
|
||||
render_state.render_shape_tree_tile(tree, timestamp, false, true)?;
|
||||
}
|
||||
|
||||
render_state.export_context = None;
|
||||
@ -77,7 +77,7 @@ pub fn render_shape_pixels(
|
||||
render_state.export_context = saved_export_context;
|
||||
render_state.render_area = saved_render_area;
|
||||
render_state.render_area_with_margins = saved_render_area_with_margins;
|
||||
render_state.current_tile = saved_current_tile;
|
||||
render_state.tile.current_tile = saved_current_tile;
|
||||
render_state.pending_nodes = saved_pending_nodes;
|
||||
render_state.nested_fills = saved_nested_fills;
|
||||
render_state.nested_blurs = saved_nested_blurs;
|
||||
@ -86,7 +86,7 @@ pub fn render_shape_pixels(
|
||||
render_state.preview_mode = saved_preview_mode;
|
||||
|
||||
let workspace_scale = render_state.get_scale();
|
||||
if let Some(tile) = render_state.current_tile {
|
||||
if let Some(tile) = render_state.tile.current_tile {
|
||||
render_state.update_render_context(tile);
|
||||
} else if !render_state.render_area.is_empty() {
|
||||
render_state
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
// Render options flags
|
||||
const DEBUG_VISIBLE: u32 = 0x01;
|
||||
const PROFILE_REBUILD_TILES: u32 = 0x02;
|
||||
const TEXT_EDITOR_V3: u32 = 0x04;
|
||||
const SHOW_WASM_INFO: u32 = 0x08;
|
||||
|
||||
@ -10,14 +9,13 @@ const SHOW_WASM_INFO: u32 = 0x08;
|
||||
const VIEWPORT_INTEREST_AREA_THRESHOLD: i32 = 1;
|
||||
const MIN_DPR_VIEWPORT_INTEREST_AREA_THRESHOLD: i32 = 2;
|
||||
const MAX_BLOCKING_TIME_MS: i32 = 32;
|
||||
const NODE_BATCH_THRESHOLD: i32 = 3;
|
||||
const NODE_BATCH_THRESHOLD: i32 = 100;
|
||||
const BLUR_DOWNSCALE_THRESHOLD: f32 = 8.0;
|
||||
const ANTIALIAS_THRESHOLD: f32 = 7.0;
|
||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||
pub struct RenderOptions {
|
||||
pub flags: u32,
|
||||
pub dpr: f32,
|
||||
fast_mode: bool,
|
||||
/// Active while the user is interacting with a shape (drag, resize,
|
||||
/// rotate). Implies `fast_mode` semantics for expensive effects but
|
||||
/// keeps per-frame flushing enabled (unlike pan/zoom, where
|
||||
@ -30,7 +28,6 @@ pub struct RenderOptions {
|
||||
pub max_blocking_time_ms: i32,
|
||||
pub node_batch_threshold: i32,
|
||||
pub blur_downscale_threshold: f32,
|
||||
pub capture_frames: i32,
|
||||
}
|
||||
|
||||
impl Default for RenderOptions {
|
||||
@ -38,7 +35,6 @@ impl Default for RenderOptions {
|
||||
Self {
|
||||
flags: 0,
|
||||
dpr: 1.0,
|
||||
fast_mode: false,
|
||||
interactive_transform: false,
|
||||
antialias_threshold: ANTIALIAS_THRESHOLD,
|
||||
viewport_interest_area_threshold: VIEWPORT_INTEREST_AREA_THRESHOLD,
|
||||
@ -46,7 +42,6 @@ impl Default for RenderOptions {
|
||||
max_blocking_time_ms: MAX_BLOCKING_TIME_MS,
|
||||
node_batch_threshold: NODE_BATCH_THRESHOLD,
|
||||
blur_downscale_threshold: BLUR_DOWNSCALE_THRESHOLD,
|
||||
capture_frames: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -56,23 +51,6 @@ impl RenderOptions {
|
||||
self.flags & DEBUG_VISIBLE == DEBUG_VISIBLE
|
||||
}
|
||||
|
||||
pub fn is_profile_rebuild_tiles(&self) -> bool {
|
||||
self.flags & PROFILE_REBUILD_TILES == PROFILE_REBUILD_TILES
|
||||
}
|
||||
|
||||
/// Use fast mode to enable / disable expensive operations
|
||||
pub fn is_fast_mode(&self) -> bool {
|
||||
self.fast_mode
|
||||
}
|
||||
|
||||
pub fn set_fast_mode(&mut self, enabled: bool) {
|
||||
self.fast_mode = enabled;
|
||||
}
|
||||
|
||||
pub fn set_capture_frames(&mut self, capture_frames: i32) {
|
||||
self.capture_frames = capture_frames;
|
||||
}
|
||||
|
||||
/// Updates the dpr viewport interest area threshold.
|
||||
/// This function is updated when the dpr or the
|
||||
/// viewport_interest_area_threshold is changed
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -22,7 +22,7 @@ pub fn precompute_viewer_visible_set(render_state: &mut RenderState, tree: Shape
|
||||
let Some(ref include) = render_state.include_filter else {
|
||||
return;
|
||||
};
|
||||
let mut visible = include.clone();
|
||||
let mut visible: HashSet<Uuid> = include.clone();
|
||||
for id in include.iter() {
|
||||
let mut current_id = id;
|
||||
while let Some(raw) = tree.get_raw(current_id) {
|
||||
|
||||
@ -1382,108 +1382,6 @@ impl Shape {
|
||||
}
|
||||
}
|
||||
|
||||
/// Same `concat` applied around [`center`](Self::center) as in `render_shape` (non-text branch).
|
||||
fn shape_document_transform(&self) -> Matrix {
|
||||
let c = self.center();
|
||||
let mut m = self.transform;
|
||||
m.post_translate(c);
|
||||
m.pre_translate(-c);
|
||||
m
|
||||
}
|
||||
|
||||
/// Fill silhouette only, document space (matches fill rendering).
|
||||
fn drag_crop_fill_clip_path_skia(&self) -> Option<skia::Path> {
|
||||
match &self.shape_type {
|
||||
Type::Rect(r) => {
|
||||
let p = Path::new(shape_to_path::rect_segments(self, r.corners));
|
||||
Some(p.to_skia_path(self.svg_attrs.as_ref()))
|
||||
}
|
||||
Type::Circle => {
|
||||
let p = Path::new(shape_to_path::circle_segments(self));
|
||||
Some(p.to_skia_path(self.svg_attrs.as_ref()))
|
||||
}
|
||||
Type::Path(_) | Type::Bool(_) => {
|
||||
let sk = self.get_skia_path()?;
|
||||
Some(sk.make_transform(&self.shape_document_transform()))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether this shape may use the backbuffer crop fast path during interactive drag.
|
||||
///
|
||||
/// Conservative: only effects and fills that match what we snapshot and clip in
|
||||
/// [`drag_crop_clip_path`](Self::drag_crop_clip_path). Text is never safe (glyph layout,
|
||||
/// no `drag_crop_clip_path`).
|
||||
pub fn is_safe_for_drag_crop_cache(&self, shapes_pool: ShapesPoolRef) -> bool {
|
||||
if matches!(self.shape_type, Type::Text(_)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If a frame shows overflow (clip_content=false) and its visible content exceeds the
|
||||
// frame bounds, a cached crop anchored to the frame can easily become incorrect while
|
||||
// moving (children can extend beyond selrect). Be conservative and render live.
|
||||
if matches!(self.shape_type, Type::Frame(_)) && !self.clip_content {
|
||||
let extrect = self.extrect(shapes_pool, 1.0);
|
||||
let sr = self.selrect;
|
||||
let exceeds = extrect.left < sr.left
|
||||
|| extrect.top < sr.top
|
||||
|| extrect.right > sr.right
|
||||
|| extrect.bottom > sr.bottom;
|
||||
if exceeds {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
self.blur.is_none()
|
||||
&& self.background_blur.is_none()
|
||||
&& self.shadows.is_empty()
|
||||
&& (self.opacity - 1.0).abs() <= 1e-4
|
||||
&& self.blend_mode().0 == skia::BlendMode::SrcOver
|
||||
}
|
||||
|
||||
/// Fill + visible strokes in **document space** for clipping interactive drag textures.
|
||||
///
|
||||
/// The backbuffer crop uses an axis-aligned `extrect`; we clip the blit so backdrop pixels
|
||||
/// outside the real silhouette (fill and stroke regions) are not smeared. Strokes use
|
||||
/// [`stroke_to_path`](stroke_to_path) like the main renderer, then union with the fill path.
|
||||
pub fn drag_crop_clip_path(&self) -> Option<skia::Path> {
|
||||
let mut acc = self.drag_crop_fill_clip_path_skia()?;
|
||||
if !self.has_visible_strokes() {
|
||||
return Some(acc);
|
||||
}
|
||||
|
||||
let shape_path = match &self.shape_type {
|
||||
Type::Rect(r) => Path::new(shape_to_path::rect_segments(self, r.corners)),
|
||||
Type::Circle => Path::new(shape_to_path::circle_segments(self)),
|
||||
Type::Path(_) | Type::Bool(_) => self.shape_type.path()?.clone(),
|
||||
_ => return Some(acc),
|
||||
};
|
||||
|
||||
let path_transform = self.to_path_transform();
|
||||
let apply_doc_transform = path_transform.is_some();
|
||||
|
||||
for stroke in self.visible_strokes() {
|
||||
let Some(stroke_region) = stroke_to_path(
|
||||
stroke,
|
||||
&shape_path,
|
||||
path_transform.as_ref(),
|
||||
&self.selrect,
|
||||
self.svg_attrs.as_ref(),
|
||||
true,
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
let mut sk = stroke_region.to_skia_path(self.svg_attrs.as_ref());
|
||||
if apply_doc_transform {
|
||||
sk = sk.make_transform(&self.shape_document_transform());
|
||||
}
|
||||
acc = acc.op(&sk, skia::PathOp::Union).unwrap_or(acc);
|
||||
}
|
||||
|
||||
Some(acc)
|
||||
}
|
||||
|
||||
fn transform_selrect(&mut self, transform: &Matrix) {
|
||||
if math::is_move_only_matrix(transform) {
|
||||
let tx = transform.translate_x();
|
||||
|
||||
@ -65,26 +65,27 @@ impl State {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn render_from_cache(&mut self) {
|
||||
get_render_state().render_from_cache(&self.shapes);
|
||||
}
|
||||
// pub fn render_from_cache(&mut self) {
|
||||
// get_render_state().render_from_cache(&self.shapes);
|
||||
// }
|
||||
|
||||
pub fn render_ui_only(&mut self) {
|
||||
get_render_state().render_ui_only(&self.shapes);
|
||||
}
|
||||
|
||||
pub fn render_blurred_snapshot(&mut self, blur_radius: f32) {
|
||||
get_render_state().render_blurred_snapshot(&self.shapes, blur_radius);
|
||||
get_render_state().render_blurred_snapshot(blur_radius);
|
||||
}
|
||||
|
||||
pub fn render_sync(&mut self, timestamp: i32) -> Result<FrameType> {
|
||||
get_render_state().start_render_loop(None, &self.shapes, timestamp, true)
|
||||
get_render_state().start_render_loop(timestamp, true)
|
||||
}
|
||||
|
||||
pub fn render_sync_shape(&mut self, id: &Uuid, timestamp: i32) -> Result<FrameType> {
|
||||
let render_state = get_render_state();
|
||||
render_state.prepare_sync_shape_render();
|
||||
render_state.start_render_loop(Some(id), &self.shapes, timestamp, true)
|
||||
render_state.base_object = Some(*id);
|
||||
render_state.start_render_loop(timestamp, true)
|
||||
}
|
||||
|
||||
pub fn render_shape_pixels(
|
||||
@ -100,24 +101,26 @@ impl State {
|
||||
crate::render::pdf::render_to_pdf(get_render_state(), id, &self.shapes, scale)
|
||||
}
|
||||
|
||||
pub fn start_render_loop(&mut self, timestamp: i32) -> Result<FrameType> {
|
||||
let render_state = get_render_state();
|
||||
// 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
|
||||
// rebuild_tile_index (NOT rebuild_tiles_shallow) to preserve the tile
|
||||
// texture cache — otherwise cached tiles with shadows/blur would be
|
||||
// cleared and re-rendered in fast mode without effects.
|
||||
if render_state.zoom_changed() {
|
||||
render_state.rebuild_tile_index(&self.shapes);
|
||||
}
|
||||
render_state.start_render_loop(None, &self.shapes, timestamp, false)
|
||||
}
|
||||
// pub fn start_render_loop(&mut self, timestamp: i32) -> Result<FrameType> {
|
||||
// let render_state = get_render_state();
|
||||
|
||||
pub fn continue_render_loop(&mut self, timestamp: i32) -> Result<FrameType> {
|
||||
let allow_stop = true;
|
||||
get_render_state().continue_render_loop(None, &self.shapes, timestamp, allow_stop)
|
||||
}
|
||||
// render_state.tile_viewbox.update(&render_state.viewbox);
|
||||
// render_state.rebuild_tile_index(&self.shapes);
|
||||
// if render_state.is_zoom_changed() {
|
||||
// render_state.surfaces.invalidate_tile_cache();
|
||||
// }
|
||||
// render_state.start_render_loop(
|
||||
// None,
|
||||
// &self.shapes,
|
||||
// timestamp,
|
||||
// false
|
||||
// )
|
||||
// }
|
||||
|
||||
// pub fn continue_render_loop(&mut self, timestamp: i32) -> Result<FrameType> {
|
||||
// let allow_stop = true;
|
||||
// get_render_state().continue_render_loop(None, &self.shapes, timestamp, allow_stop)
|
||||
// }
|
||||
|
||||
pub fn clear_focus_mode(&mut self) {
|
||||
get_render_state().clear_focus_mode();
|
||||
@ -169,6 +172,7 @@ impl State {
|
||||
// Instead, remove the shape from *all* tiles where it was indexed, and
|
||||
// drop cached tiles for those entries.
|
||||
let indexed_tiles: Vec<tiles::Tile> = render_state
|
||||
.tile
|
||||
.tiles
|
||||
.get_tiles_of(shape.id)
|
||||
.map(|t| t.iter().copied().collect())
|
||||
@ -176,7 +180,7 @@ impl State {
|
||||
|
||||
for tile in indexed_tiles {
|
||||
render_state.remove_cached_tile(tile);
|
||||
render_state.tiles.remove_shape_at(tile, shape.id);
|
||||
render_state.tile.tiles.remove_shape_at(tile, shape.id);
|
||||
}
|
||||
|
||||
if let Some(shape_to_delete) = self.shapes.get(&id) {
|
||||
@ -242,7 +246,7 @@ impl State {
|
||||
}
|
||||
|
||||
pub fn rebuild_tiles_shallow(&mut self) {
|
||||
get_render_state().rebuild_tiles_shallow(&self.shapes);
|
||||
get_render_state().rebuild_tiles_shallow();
|
||||
}
|
||||
|
||||
pub fn rebuild_tiles(&mut self) {
|
||||
@ -254,15 +258,15 @@ impl State {
|
||||
}
|
||||
|
||||
pub fn rebuild_touched_tiles(&mut self) {
|
||||
get_render_state().rebuild_touched_tiles(&self.shapes);
|
||||
get_render_state().rebuild_touched_tiles();
|
||||
}
|
||||
|
||||
pub fn render_preview(&mut self, timestamp: i32) {
|
||||
let _ = get_render_state().render_preview(&self.shapes, timestamp);
|
||||
let _ = get_render_state().render_preview(timestamp);
|
||||
}
|
||||
|
||||
pub fn rebuild_modifier_tiles(&mut self, ids: &[Uuid]) -> Result<()> {
|
||||
get_render_state().rebuild_modifier_tiles(&mut self.shapes, ids)
|
||||
get_render_state().rebuild_modifier_tiles(ids)
|
||||
}
|
||||
|
||||
pub fn font_collection(&self) -> &FontCollection {
|
||||
|
||||
@ -30,16 +30,6 @@ impl Tile {
|
||||
tile_size,
|
||||
)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn get_rect_with_offset(&self, offset: &skia::Point) -> skia::Rect {
|
||||
skia::Rect::from_xywh(
|
||||
self.0 as f32 * TILE_SIZE - offset.x,
|
||||
self.1 as f32 * TILE_SIZE - offset.y,
|
||||
TILE_SIZE,
|
||||
TILE_SIZE,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)]
|
||||
|
||||
@ -1,23 +1,34 @@
|
||||
use crate::math::{Matrix, Point, Rect, Size};
|
||||
use std::ops::Mul;
|
||||
|
||||
#[repr(u32)]
|
||||
pub enum ViewboxUpdated {
|
||||
None = 0b0000,
|
||||
Position = 0b0001,
|
||||
Zoom = 0b0010,
|
||||
Size = 0b0100,
|
||||
All = 0b0111,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub(crate) struct Viewbox {
|
||||
pub pan: Point,
|
||||
pub position: Point,
|
||||
pub size: Size,
|
||||
pub zoom: f32,
|
||||
pub dpr: f32,
|
||||
pub area: Rect,
|
||||
pub updated: u32,
|
||||
}
|
||||
|
||||
impl Default for Viewbox {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
pan: Point::new(0.0, 0.0),
|
||||
position: Point::new(0.0, 0.0),
|
||||
size: Size::new(0.0, 0.0),
|
||||
zoom: 1.0,
|
||||
dpr: 1.0,
|
||||
area: Rect::new_empty(),
|
||||
updated: ViewboxUpdated::All as u32,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -50,21 +61,42 @@ impl Viewbox {
|
||||
self.size.height
|
||||
}
|
||||
|
||||
pub fn set_all(&mut self, zoom: f32, pan_x: f32, pan_y: f32) {
|
||||
self.pan.set(pan_x, pan_y);
|
||||
self.zoom = zoom;
|
||||
self.area.set_xywh(
|
||||
-self.pan.x,
|
||||
-self.pan.y,
|
||||
self.size.width / self.zoom,
|
||||
self.size.height / self.zoom,
|
||||
);
|
||||
pub fn set_all(&mut self, zoom: f32, x: f32, y: f32) {
|
||||
self.set_position(x, y);
|
||||
self.set_zoom(zoom);
|
||||
if self.updated != ViewboxUpdated::None as u32 {
|
||||
self.area.set_xywh(
|
||||
-self.position.x,
|
||||
-self.position.y,
|
||||
self.size.width / self.zoom,
|
||||
self.size.height / self.zoom,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_position(&mut self, x: f32, y: f32) {
|
||||
if self.position.x != x {
|
||||
self.position.x = x;
|
||||
self.updated |= ViewboxUpdated::Position as u32;
|
||||
}
|
||||
if self.position.y != y {
|
||||
self.position.y = y;
|
||||
self.updated |= ViewboxUpdated::Position as u32;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_zoom(&mut self, zoom: f32) {
|
||||
if self.zoom != zoom {
|
||||
self.zoom = zoom;
|
||||
self.updated = ViewboxUpdated::Zoom as u32;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_wh(&mut self, width: f32, height: f32) {
|
||||
self.size.set(width, height);
|
||||
self.area
|
||||
.set_wh(self.size.width / self.zoom, self.size.height / self.zoom);
|
||||
self.updated = ViewboxUpdated::Size as u32;
|
||||
}
|
||||
|
||||
pub fn set_dpr(&mut self, dpr: f32) {
|
||||
@ -80,7 +112,7 @@ impl Viewbox {
|
||||
}
|
||||
|
||||
pub fn pan(&self) -> Point {
|
||||
self.pan
|
||||
self.position
|
||||
}
|
||||
|
||||
pub fn zoom(&self) -> f32 {
|
||||
@ -93,4 +125,24 @@ impl Viewbox {
|
||||
matrix.post_scale((self.zoom, self.zoom), None);
|
||||
matrix
|
||||
}
|
||||
|
||||
pub fn is_updated(&self, flags: u32) -> bool {
|
||||
self.updated & flags == flags
|
||||
}
|
||||
|
||||
pub fn is_zoom_changed(&self) -> bool {
|
||||
self.is_updated(ViewboxUpdated::Zoom as u32)
|
||||
}
|
||||
|
||||
pub fn is_position_changed(&self) -> bool {
|
||||
self.is_updated(ViewboxUpdated::Position as u32)
|
||||
}
|
||||
|
||||
pub fn is_size_changed(&self) -> bool {
|
||||
self.is_updated(ViewboxUpdated::Size as u32)
|
||||
}
|
||||
|
||||
pub fn update_handled(&mut self) {
|
||||
self.updated = ViewboxUpdated::None as u32;
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user