mirror of
https://github.com/penpot/penpot.git
synced 2026-09-08 19:59:54 -04:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bdff7730d6 | ||
|
|
fe6e2b97fa | ||
|
|
37add2c6d4 | ||
|
|
2ed7e55c31 | ||
|
|
cf8ff3828b | ||
|
|
5f1bef6bd9 |
No files matched your search
@@ -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."
|
||||
|
||||
@@ -9,8 +9,9 @@ description = "Wasm-based canvas renderer for Penpot"
|
||||
build = "build.rs"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
default = ["profile"]
|
||||
stats = []
|
||||
|
||||
profile = ["profile-macros", "profile-raf"]
|
||||
profile-macros = []
|
||||
profile-raf = []
|
||||
|
||||
@@ -5,12 +5,13 @@ use crate::emscripten::init_gl;
|
||||
|
||||
use crate::mem;
|
||||
use crate::render::{gpu_state::GpuState, RenderState};
|
||||
use crate::state::{State, TextEditorState, UIState};
|
||||
use crate::state::{DesignState, TextEditorState, UIState};
|
||||
use crate::tiles::TileRenderState;
|
||||
|
||||
static mut DESIGN_STATE: *mut State = std::ptr::null_mut();
|
||||
static mut DESIGN_STATE: *mut DesignState = std::ptr::null_mut();
|
||||
|
||||
/// Design State.
|
||||
pub(crate) fn get_design_state() -> &'static mut State {
|
||||
pub(crate) fn get_design_state() -> &'static mut DesignState {
|
||||
unsafe {
|
||||
debug_assert!(!DESIGN_STATE.is_null(), "Design State is null");
|
||||
&mut *DESIGN_STATE
|
||||
@@ -39,6 +40,16 @@ pub(crate) fn get_render_state() -> &'static mut RenderState {
|
||||
}
|
||||
}
|
||||
|
||||
static mut TILE_RENDER_STATE: *mut TileRenderState = std::ptr::null_mut();
|
||||
|
||||
#[inline(always)]
|
||||
pub(crate) fn get_tile_render_state() -> &'static mut TileRenderState {
|
||||
unsafe {
|
||||
debug_assert!(!TILE_RENDER_STATE.is_null(), "Tile Render State is null");
|
||||
&mut *TILE_RENDER_STATE
|
||||
}
|
||||
}
|
||||
|
||||
/// Text Editor State
|
||||
static mut TEXT_EDITOR_STATE: *mut TextEditorState = std::ptr::null_mut();
|
||||
|
||||
@@ -113,10 +124,16 @@ fn render_init(width: i32, height: i32) {
|
||||
}
|
||||
}
|
||||
|
||||
fn tile_render_init() {
|
||||
unsafe {
|
||||
TILE_RENDER_STATE = Box::into_raw(Box::new(TileRenderState::new()));
|
||||
}
|
||||
}
|
||||
|
||||
/// Initializes DesignState.
|
||||
fn design_init() {
|
||||
unsafe {
|
||||
let design_state = State::new();
|
||||
let design_state = DesignState::new();
|
||||
DESIGN_STATE = Box::into_raw(Box::new(design_state));
|
||||
}
|
||||
}
|
||||
@@ -144,6 +161,7 @@ pub extern "C" fn init(width: i32, height: i32) -> Result<()> {
|
||||
init_gl!();
|
||||
gpu_init();
|
||||
render_init(width, height);
|
||||
tile_render_init();
|
||||
text_editor_init();
|
||||
design_init();
|
||||
ui_init();
|
||||
|
||||
+56
-52
@@ -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;
|
||||
|
||||
use globals::{get_design_state, get_gpu_state, get_render_state};
|
||||
|
||||
@@ -103,10 +103,25 @@ 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();
|
||||
let frame_type = render_state
|
||||
.render_backbuffer_vector(&state.shapes, timestamp)
|
||||
.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 +148,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 +232,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 +328,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 +346,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 +372,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 +387,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 +872,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(())
|
||||
@@ -923,7 +927,7 @@ pub extern "C" fn get_shape_extrect(a: u32, b: u32, c: u32, d: u32) -> Result<*m
|
||||
let Some(shape) = state.shapes.get(&id) else {
|
||||
return Err(Error::CriticalError("Shape not found".to_string()));
|
||||
};
|
||||
let extrect = get_render_state().get_cached_extrect(shape, &state.shapes, 1.0);
|
||||
let extrect = shape.extrect(&state.shapes, 1.0);
|
||||
let mut buf = Vec::with_capacity(16);
|
||||
buf.extend_from_slice(&extrect.x().to_le_bytes());
|
||||
buf.extend_from_slice(&extrect.y().to_le_bytes());
|
||||
|
||||
@@ -3,6 +3,7 @@ use skia_safe as skia;
|
||||
pub mod bools;
|
||||
|
||||
pub type Rect = skia::Rect;
|
||||
pub type IRect = skia::IRect;
|
||||
pub type Matrix = skia::Matrix;
|
||||
pub type Vector = skia::Vector;
|
||||
pub type Point = skia::Point;
|
||||
|
||||
@@ -333,11 +333,7 @@ fn beziers_to_segments(beziers: &[(BezierSource, Bezier)]) -> Vec<Segment> {
|
||||
let mut last_end = (first_bezier.end.x as f32, first_bezier.end.y as f32);
|
||||
let mut cur_end = first_bezier.end;
|
||||
|
||||
loop {
|
||||
let Some((next_src, next_bezier)) = find_next_in_pool(&mut pool, cur_end, cur_src)
|
||||
else {
|
||||
break;
|
||||
};
|
||||
while let Some((next_src, next_bezier)) = find_next_in_pool(&mut pool, cur_end, cur_src) {
|
||||
push_bezier(&mut result, &next_bezier);
|
||||
last_end = (next_bezier.end.x as f32, next_bezier.end.y as f32);
|
||||
cur_end = next_bezier.end;
|
||||
|
||||
+698
-1922
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,94 @@
|
||||
use super::{RenderState, SurfaceId};
|
||||
use crate::shapes::{radius_to_sigma, Shape, Type};
|
||||
use skia_safe::{self as skia, RRect};
|
||||
|
||||
pub fn render_background_blur(
|
||||
render_state: &mut RenderState,
|
||||
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;
|
||||
}
|
||||
let blur = match shape
|
||||
.blur
|
||||
.filter(|b| !b.hidden && b.blur_type == crate::shapes::BlurType::BackgroundBlur)
|
||||
{
|
||||
Some(blur) => blur,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let scale = render_state.get_scale();
|
||||
let scaled_sigma = radius_to_sigma(blur.value * scale);
|
||||
let sigma = if render_state.export_context.is_some() {
|
||||
scaled_sigma
|
||||
} else {
|
||||
let margin = render_state.surfaces.margins().width as f32;
|
||||
let max_sigma = margin / 3.0;
|
||||
scaled_sigma.min(max_sigma)
|
||||
};
|
||||
|
||||
let blur_filter =
|
||||
match skia::image_filters::blur((sigma, sigma), skia::TileMode::Clamp, None, None) {
|
||||
Some(filter) => filter,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let target_surface_snapshot = render_state.surfaces.snapshot(target_surface);
|
||||
let translation = render_state
|
||||
.surfaces
|
||||
.get_render_context_translation(render_state.render_area, scale);
|
||||
|
||||
let center = shape.center();
|
||||
let mut matrix = shape.transform;
|
||||
matrix.post_translate(center);
|
||||
matrix.pre_translate(-center);
|
||||
|
||||
let canvas = render_state.surfaces.canvas(target_surface);
|
||||
canvas.save();
|
||||
|
||||
canvas.scale((scale, scale));
|
||||
canvas.translate(translation);
|
||||
canvas.concat(&matrix);
|
||||
|
||||
match &shape.shape_type {
|
||||
Type::Rect(data) if data.corners.is_some() => {
|
||||
let rrect = RRect::new_rect_radii(shape.selrect, data.corners.as_ref().unwrap());
|
||||
canvas.clip_rrect(rrect, skia::ClipOp::Intersect, true);
|
||||
}
|
||||
Type::Frame(data) if data.corners.is_some() => {
|
||||
let rrect = RRect::new_rect_radii(shape.selrect, data.corners.as_ref().unwrap());
|
||||
canvas.clip_rrect(rrect, skia::ClipOp::Intersect, true);
|
||||
}
|
||||
Type::Rect(_) | Type::Frame(_) => {
|
||||
canvas.clip_rect(shape.selrect, skia::ClipOp::Intersect, true);
|
||||
}
|
||||
Type::Circle => {
|
||||
let mut pb = skia::PathBuilder::new();
|
||||
pb.add_oval(shape.selrect, None, None);
|
||||
canvas.clip_path(&pb.detach(), skia::ClipOp::Intersect, true);
|
||||
}
|
||||
_ => {
|
||||
if let Some(path) = shape.get_skia_path() {
|
||||
canvas.clip_path(&path, skia::ClipOp::Intersect, true);
|
||||
} else {
|
||||
canvas.clip_rect(shape.selrect, skia::ClipOp::Intersect, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
canvas.reset_matrix();
|
||||
|
||||
let mut paint = skia::Paint::default();
|
||||
paint.set_image_filter(blur_filter);
|
||||
paint.set_blend_mode(skia::BlendMode::Src);
|
||||
canvas.draw_image(&target_surface_snapshot, (0, 0), Some(&paint));
|
||||
|
||||
canvas.restore();
|
||||
}
|
||||
@@ -6,6 +6,7 @@ use macros::wasm_error;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use crate::get_render_state;
|
||||
use crate::globals::get_tile_render_state;
|
||||
|
||||
use skia_safe::{self as skia, Rect};
|
||||
|
||||
@@ -36,16 +37,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 +70,8 @@ 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 tile_render_state = get_tile_render_state();
|
||||
let tiles::TileRect(sx, sy, ex, ey) = tile_render_state.viewbox.interest_rect;
|
||||
let canvas = render_state.surfaces.canvas(SurfaceId::Debug);
|
||||
let mut paint = skia::Paint::default();
|
||||
paint.set_color(skia::Color::RED);
|
||||
@@ -163,6 +155,10 @@ pub fn render_debug_shape(
|
||||
shape_selrect: Option<skia::Rect>,
|
||||
shape_extrect: Option<skia::Rect>,
|
||||
) {
|
||||
if shape_selrect.is_none() && shape_extrect.is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
let canvas = render_state.surfaces.canvas(SurfaceId::Debug);
|
||||
|
||||
let mut paint = skia::Paint::default();
|
||||
@@ -260,48 +256,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")]
|
||||
|
||||
@@ -0,0 +1,357 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use skia_safe as skia;
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::shapes::{radius_to_sigma, Blur, Fill, Shadow, Shape, SolidColor, Type};
|
||||
use crate::state::ShapesPoolRef;
|
||||
|
||||
use super::{
|
||||
filters, get_simplified_children, layer_blur, ClipStack, NodeRenderState, RenderState,
|
||||
SurfaceId,
|
||||
};
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn render_drop_black_shadow(
|
||||
render_state: &mut RenderState,
|
||||
shape: &Shape,
|
||||
shape_bounds: &skia::Rect,
|
||||
shadow: &Shadow,
|
||||
clip_bounds: Option<ClipStack>,
|
||||
scale: f32,
|
||||
extra_layer_blur: Option<Blur>,
|
||||
target_surface: SurfaceId,
|
||||
) -> Result<()> {
|
||||
let mut transformed_shadow: Cow<Shadow> = Cow::Borrowed(shadow);
|
||||
transformed_shadow.to_mut().offset = (0.0, 0.0);
|
||||
transformed_shadow.to_mut().color = skia::Color::BLACK;
|
||||
|
||||
let mut plain_shape = Cow::Borrowed(shape);
|
||||
let combined_blur = layer_blur::combine_blur_values(
|
||||
layer_blur::combined_layer_blur(
|
||||
&render_state.nested_blurs,
|
||||
&mut render_state.cached_layer_blur,
|
||||
shape.blur,
|
||||
),
|
||||
extra_layer_blur,
|
||||
);
|
||||
let blur_filter = combined_blur.and_then(|blur| {
|
||||
let sigma = blur.sigma();
|
||||
skia::image_filters::blur((sigma, sigma), None, None, None)
|
||||
});
|
||||
|
||||
let use_low_zoom_path = scale <= 1.0 && combined_blur.is_none();
|
||||
let mut transform_matrix = shape.transform;
|
||||
let center = shape.center();
|
||||
transform_matrix.post_translate(center);
|
||||
transform_matrix.pre_translate(-center);
|
||||
|
||||
let mapped = transform_matrix.map_vector((shadow.offset.0, shadow.offset.1));
|
||||
let world_offset = (mapped.x, mapped.y);
|
||||
|
||||
let plain_shape_mut = plain_shape.to_mut();
|
||||
plain_shape_mut.clear_fills();
|
||||
if shape.has_fills() {
|
||||
plain_shape_mut.add_fill(Fill::Solid(SolidColor(skia::Color::BLACK)));
|
||||
}
|
||||
|
||||
for stroke in plain_shape_mut.strokes.iter_mut() {
|
||||
stroke.fill = Fill::Solid(SolidColor(skia::Color::BLACK));
|
||||
}
|
||||
|
||||
plain_shape_mut.clear_shadows();
|
||||
plain_shape_mut.blur = None;
|
||||
|
||||
plain_shape_mut.clip_content = false;
|
||||
|
||||
let Some(drop_filter) = transformed_shadow.get_drop_shadow_filter() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let mut bounds = drop_filter.compute_fast_bounds(shape_bounds);
|
||||
bounds.offset(world_offset);
|
||||
if !bounds.intersects(render_state.render_area_with_margins)
|
||||
&& target_surface != SurfaceId::Export
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if scale > 1.0 && shadow.blur <= 0.0 {
|
||||
let drop_canvas = render_state.surfaces.canvas(SurfaceId::DropShadows);
|
||||
drop_canvas.save();
|
||||
|
||||
render_state.with_nested_blurs_suppressed(|state| {
|
||||
state.render_shape(
|
||||
&plain_shape,
|
||||
clip_bounds,
|
||||
SurfaceId::DropShadows,
|
||||
SurfaceId::DropShadows,
|
||||
SurfaceId::DropShadows,
|
||||
SurfaceId::DropShadows,
|
||||
false,
|
||||
Some(shadow.offset),
|
||||
None,
|
||||
Some(shadow.spread),
|
||||
target_surface,
|
||||
)
|
||||
})?;
|
||||
|
||||
render_state
|
||||
.surfaces
|
||||
.canvas(SurfaceId::DropShadows)
|
||||
.restore();
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let blur_only_filter = if transformed_shadow.blur > 0.0 {
|
||||
let sigma = radius_to_sigma(transformed_shadow.blur);
|
||||
Some(skia::image_filters::blur((sigma, sigma), None, None, None))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut shadow_paint = skia::Paint::default();
|
||||
if let Some(blur_filter) = blur_only_filter {
|
||||
shadow_paint.set_image_filter(blur_filter);
|
||||
}
|
||||
shadow_paint.set_blend_mode(skia::BlendMode::SrcOver);
|
||||
|
||||
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&shadow_paint);
|
||||
|
||||
if use_low_zoom_path {
|
||||
let drop_canvas = render_state.surfaces.canvas(SurfaceId::DropShadows);
|
||||
drop_canvas.save_layer(&layer_rec);
|
||||
|
||||
render_state.with_nested_blurs_suppressed(|state| {
|
||||
state.render_shape(
|
||||
&plain_shape,
|
||||
clip_bounds,
|
||||
SurfaceId::DropShadows,
|
||||
SurfaceId::DropShadows,
|
||||
SurfaceId::DropShadows,
|
||||
SurfaceId::DropShadows,
|
||||
false,
|
||||
Some(shadow.offset),
|
||||
None,
|
||||
Some(shadow.spread),
|
||||
target_surface,
|
||||
)
|
||||
})?;
|
||||
|
||||
render_state
|
||||
.surfaces
|
||||
.canvas(SurfaceId::DropShadows)
|
||||
.restore();
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let blur_downscale_threshold: f32 = render_state.options.blur_downscale_threshold;
|
||||
let min_blur_downscale: f32 = 1.0 / blur_downscale_threshold;
|
||||
let blur_downscale = if shadow.blur > blur_downscale_threshold {
|
||||
(blur_downscale_threshold / shadow.blur).max(min_blur_downscale)
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
|
||||
let filter_result = filters::render_into_filter_surface(
|
||||
render_state,
|
||||
bounds,
|
||||
blur_downscale,
|
||||
|state, temp_surface| {
|
||||
let canvas = state.surfaces.canvas(temp_surface);
|
||||
canvas.save_layer(&layer_rec);
|
||||
|
||||
state.with_nested_blurs_suppressed(|state| {
|
||||
state.render_shape(
|
||||
&plain_shape,
|
||||
clip_bounds,
|
||||
temp_surface,
|
||||
temp_surface,
|
||||
temp_surface,
|
||||
temp_surface,
|
||||
false,
|
||||
Some(shadow.offset),
|
||||
None,
|
||||
Some(shadow.spread),
|
||||
target_surface,
|
||||
)
|
||||
})?;
|
||||
|
||||
state.surfaces.canvas(temp_surface).restore();
|
||||
Ok(())
|
||||
},
|
||||
)?;
|
||||
|
||||
if let Some((mut surface, filter_scale)) = filter_result {
|
||||
let drop_canvas = render_state.surfaces.canvas(SurfaceId::DropShadows);
|
||||
drop_canvas.save();
|
||||
let mut drop_paint = skia::Paint::default();
|
||||
drop_paint.set_image_filter(blur_filter.clone());
|
||||
|
||||
if filter_scale < 1.0 {
|
||||
drop_canvas.save();
|
||||
drop_canvas.scale((1.0 / filter_scale, 1.0 / filter_scale));
|
||||
drop_canvas.translate((bounds.left * filter_scale, bounds.top * filter_scale));
|
||||
surface.draw(
|
||||
drop_canvas,
|
||||
(0.0, 0.0),
|
||||
render_state.sampling_options,
|
||||
Some(&drop_paint),
|
||||
);
|
||||
drop_canvas.restore();
|
||||
} else {
|
||||
drop_canvas.save();
|
||||
drop_canvas.translate((bounds.left, bounds.top));
|
||||
surface.draw(
|
||||
drop_canvas,
|
||||
(0.0, 0.0),
|
||||
render_state.sampling_options,
|
||||
Some(&drop_paint),
|
||||
);
|
||||
drop_canvas.restore();
|
||||
}
|
||||
drop_canvas.restore();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn render_element_drop_shadows_and_composite(
|
||||
render_state: &mut RenderState,
|
||||
element: &Shape,
|
||||
tree: ShapesPoolRef,
|
||||
extrect: &mut Option<skia::Rect>,
|
||||
clip_bounds: Option<ClipStack>,
|
||||
scale: f32,
|
||||
node_render_state: &NodeRenderState,
|
||||
target_surface: SurfaceId,
|
||||
) -> Result<()> {
|
||||
let element_extrect = extrect.get_or_insert_with(|| element.extrect(tree, scale));
|
||||
let inherited_layer_blur = match element.shape_type {
|
||||
Type::Frame(_) | Type::Group(_) => element.blur,
|
||||
_ => None,
|
||||
};
|
||||
|
||||
for shadow in element.drop_shadows_visible() {
|
||||
let paint = skia::Paint::default();
|
||||
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint);
|
||||
render_state
|
||||
.surfaces
|
||||
.canvas(SurfaceId::DropShadows)
|
||||
.save_layer(&layer_rec);
|
||||
|
||||
render_drop_black_shadow(
|
||||
render_state,
|
||||
element,
|
||||
element_extrect,
|
||||
shadow,
|
||||
clip_bounds.clone(),
|
||||
scale,
|
||||
None,
|
||||
target_surface,
|
||||
)?;
|
||||
|
||||
if !matches!(element.shape_type, Type::Bool(_)) {
|
||||
let mut shadow_children = Vec::new();
|
||||
if element.is_recursive() {
|
||||
get_simplified_children(tree, element, &mut shadow_children);
|
||||
}
|
||||
|
||||
for shadow_shape_id in shadow_children.iter() {
|
||||
let Some(shadow_shape) = tree.get(shadow_shape_id) else {
|
||||
continue;
|
||||
};
|
||||
if shadow_shape.hidden {
|
||||
continue;
|
||||
}
|
||||
|
||||
let nested_clip_bounds =
|
||||
node_render_state.get_nested_shadow_clip_bounds(element, shadow);
|
||||
|
||||
if !matches!(shadow_shape.shape_type, Type::Text(_)) {
|
||||
render_drop_black_shadow(
|
||||
render_state,
|
||||
shadow_shape,
|
||||
&shadow_shape.extrect(tree, scale),
|
||||
shadow,
|
||||
nested_clip_bounds,
|
||||
scale,
|
||||
inherited_layer_blur,
|
||||
target_surface,
|
||||
)?;
|
||||
} else {
|
||||
let paint = skia::Paint::default();
|
||||
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint);
|
||||
render_state
|
||||
.surfaces
|
||||
.canvas(SurfaceId::DropShadows)
|
||||
.save_layer(&layer_rec);
|
||||
|
||||
let mut transformed_shadow: Cow<Shadow> = Cow::Borrowed(shadow);
|
||||
transformed_shadow.to_mut().color = skia::Color::BLACK;
|
||||
transformed_shadow.to_mut().blur = transformed_shadow.blur;
|
||||
transformed_shadow.to_mut().spread = transformed_shadow.spread;
|
||||
|
||||
let mut new_shadow_paint = skia::Paint::default();
|
||||
new_shadow_paint.set_image_filter(transformed_shadow.get_drop_shadow_filter());
|
||||
new_shadow_paint.set_blend_mode(skia::BlendMode::SrcOver);
|
||||
|
||||
render_state.with_nested_blurs_suppressed(|state| {
|
||||
state.render_shape(
|
||||
shadow_shape,
|
||||
nested_clip_bounds,
|
||||
SurfaceId::DropShadows,
|
||||
SurfaceId::DropShadows,
|
||||
SurfaceId::DropShadows,
|
||||
SurfaceId::DropShadows,
|
||||
true,
|
||||
None,
|
||||
Some(vec![new_shadow_paint.clone()]),
|
||||
None,
|
||||
target_surface,
|
||||
)
|
||||
})?;
|
||||
render_state
|
||||
.surfaces
|
||||
.canvas(SurfaceId::DropShadows)
|
||||
.restore();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut paint = skia::Paint::default();
|
||||
paint.set_color(shadow.color);
|
||||
paint.set_blend_mode(skia::BlendMode::SrcIn);
|
||||
render_state
|
||||
.surfaces
|
||||
.canvas(SurfaceId::DropShadows)
|
||||
.draw_paint(&paint);
|
||||
|
||||
render_state
|
||||
.surfaces
|
||||
.canvas(SurfaceId::DropShadows)
|
||||
.restore();
|
||||
}
|
||||
|
||||
if let Some(clips) = clip_bounds.as_ref() {
|
||||
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
|
||||
.surfaces
|
||||
.draw_into(SurfaceId::DropShadows, target_surface, None);
|
||||
render_state.surfaces.canvas(target_surface).restore();
|
||||
} else {
|
||||
render_state
|
||||
.surfaces
|
||||
.draw_into(SurfaceId::DropShadows, target_surface, None);
|
||||
}
|
||||
|
||||
render_state
|
||||
.surfaces
|
||||
.canvas(SurfaceId::DropShadows)
|
||||
.clear(skia::Color::TRANSPARENT);
|
||||
Ok(())
|
||||
}
|
||||
@@ -27,13 +27,18 @@ fn draw_image_fill(
|
||||
|
||||
let mut image_paint = skia::Paint::default();
|
||||
image_paint.set_anti_alias(antialias);
|
||||
if let Some(filter) = shape.image_filter(1.) {
|
||||
let filter = shape.image_filter(1.);
|
||||
if let Some(ref filter) = filter {
|
||||
image_paint.set_image_filter(filter.clone());
|
||||
}
|
||||
|
||||
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&image_paint);
|
||||
// Save the current canvas state
|
||||
canvas.save_layer(&layer_rec);
|
||||
let has_image_filter = filter.is_some();
|
||||
if has_image_filter {
|
||||
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&image_paint);
|
||||
canvas.save_layer(&layer_rec);
|
||||
} else {
|
||||
canvas.save();
|
||||
}
|
||||
|
||||
// Set the clipping rectangle to the container bounds
|
||||
match &shape.shape_type {
|
||||
@@ -87,7 +92,6 @@ fn draw_image_fill(
|
||||
paint,
|
||||
);
|
||||
|
||||
// Restore the canvas to remove the clipping
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
|
||||
@@ -119,11 +119,13 @@ where
|
||||
|
||||
{
|
||||
let canvas = render_state.surfaces.canvas(filter_id);
|
||||
canvas.clear(skia::Color::TRANSPARENT);
|
||||
canvas.save();
|
||||
// Apply scale first, then translate
|
||||
canvas.scale((scale, scale));
|
||||
canvas.translate((-bounds.left, -bounds.top));
|
||||
|
||||
let scaled_bounds = Rect::new(bounds.left, bounds.top, bounds.right, bounds.bottom);
|
||||
canvas.clip_rect(scaled_bounds, skia::ClipOp::Intersect, false);
|
||||
canvas.clear(skia::Color::TRANSPARENT);
|
||||
}
|
||||
|
||||
draw_fn(render_state, filter_id)?;
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::uuid::Uuid;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct FocusMode {
|
||||
shapes: HashSet<Uuid>,
|
||||
active: bool,
|
||||
}
|
||||
|
||||
impl FocusMode {
|
||||
pub fn new() -> Self {
|
||||
FocusMode {
|
||||
shapes: HashSet::new(),
|
||||
active: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.shapes.clear();
|
||||
self.active = false;
|
||||
}
|
||||
|
||||
pub fn set_shapes(&mut self, shapes: Vec<Uuid>) {
|
||||
self.shapes = shapes.into_iter().collect();
|
||||
}
|
||||
|
||||
/// Returns `true` if the given shape ID should be focused.
|
||||
/// If the `shapes` list is empty, focus applies to all shapes.
|
||||
pub fn should_focus(&self, id: &Uuid) -> bool {
|
||||
self.shapes.is_empty() || self.shapes.contains(id)
|
||||
}
|
||||
|
||||
pub fn enter(&mut self, id: &Uuid) {
|
||||
if !self.active && self.should_focus(id) {
|
||||
self.active = true;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn exit(&mut self, id: &Uuid) {
|
||||
if self.active && self.should_focus(id) {
|
||||
self.active = false;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_active(&self) -> bool {
|
||||
self.active
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
self.active = false;
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ pub struct FontStore {
|
||||
debug_font: Font,
|
||||
ui_font: Font,
|
||||
fallback_fonts: HashSet<String>,
|
||||
registered_families: HashSet<String>,
|
||||
}
|
||||
|
||||
impl FontStore {
|
||||
@@ -55,6 +56,7 @@ impl FontStore {
|
||||
debug_font,
|
||||
ui_font,
|
||||
fallback_fonts: HashSet::new(),
|
||||
registered_families: HashSet::new(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -105,7 +107,7 @@ impl FontStore {
|
||||
};
|
||||
|
||||
self.font_provider.register_typeface(typeface, font_name);
|
||||
self.font_collection.clear_caches();
|
||||
self.registered_families.insert(font_name.to_string());
|
||||
|
||||
if is_fallback {
|
||||
self.fallback_fonts.insert(alias);
|
||||
@@ -121,13 +123,17 @@ impl FontStore {
|
||||
} else {
|
||||
alias.as_str()
|
||||
};
|
||||
self.font_provider.family_names().any(|x| x == font_name)
|
||||
self.registered_families.contains(font_name)
|
||||
}
|
||||
|
||||
pub fn get_fallback(&self) -> &HashSet<String> {
|
||||
&self.fallback_fonts
|
||||
}
|
||||
|
||||
pub fn flush_caches(&mut self) {
|
||||
self.font_collection.clear_caches();
|
||||
}
|
||||
|
||||
pub fn get_emoji_font(&self, _size: f32) -> Option<Font> {
|
||||
None
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ use crate::error::Result;
|
||||
use crate::get_gpu_state;
|
||||
use skia_safe::gpu::{surfaces, Budgeted, DirectContext};
|
||||
use skia_safe::{self as skia, Codec, ISize};
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub type Image = skia::Image;
|
||||
@@ -60,8 +61,8 @@ enum StoredImage {
|
||||
}
|
||||
|
||||
pub struct ImageStore {
|
||||
images: HashMap<(Uuid, bool), StoredImage>,
|
||||
context: Box<DirectContext>,
|
||||
images: RefCell<HashMap<(Uuid, bool), StoredImage>>,
|
||||
context: RefCell<Box<DirectContext>>,
|
||||
}
|
||||
|
||||
/// Creates a Skia image from an existing WebGL texture.
|
||||
@@ -148,8 +149,8 @@ impl ImageStore {
|
||||
let gpu_state = get_gpu_state();
|
||||
let context = &gpu_state.context;
|
||||
Self {
|
||||
images: HashMap::with_capacity(2048),
|
||||
context: Box::new(context.clone()),
|
||||
images: RefCell::new(HashMap::new()),
|
||||
context: RefCell::new(Box::new(context.clone())),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,16 +162,18 @@ impl ImageStore {
|
||||
) -> crate::error::Result<()> {
|
||||
let key = (id, is_thumbnail);
|
||||
|
||||
if self.images.contains_key(&key) {
|
||||
if self.images.borrow().contains_key(&key) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let raw_data = image_data.to_vec();
|
||||
|
||||
if let Some(gpu_image) = decode_image(&mut self.context, &raw_data) {
|
||||
self.images.insert(key, StoredImage::Gpu(gpu_image));
|
||||
if let Some(gpu_image) = decode_image(&mut self.context.borrow_mut(), image_data) {
|
||||
self.images
|
||||
.borrow_mut()
|
||||
.insert(key, StoredImage::Gpu(gpu_image));
|
||||
} else {
|
||||
self.images.insert(key, StoredImage::Raw(raw_data));
|
||||
self.images
|
||||
.borrow_mut()
|
||||
.insert(key, StoredImage::Raw(image_data.to_vec()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -188,51 +191,49 @@ impl ImageStore {
|
||||
) -> Result<()> {
|
||||
let key = (id, is_thumbnail);
|
||||
|
||||
if self.images.contains_key(&key) {
|
||||
if self.images.borrow().contains_key(&key) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Create a Skia image from the existing GL texture
|
||||
let image = create_image_from_gl_texture(&mut self.context, texture_id, width, height)?;
|
||||
self.images.insert(key, StoredImage::Gpu(image));
|
||||
let image = create_image_from_gl_texture(
|
||||
&mut self.context.borrow_mut(),
|
||||
texture_id,
|
||||
width,
|
||||
height,
|
||||
)?;
|
||||
self.images
|
||||
.borrow_mut()
|
||||
.insert(key, StoredImage::Gpu(image));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn contains(&self, id: &Uuid, is_thumbnail: bool) -> bool {
|
||||
self.images.contains_key(&(*id, is_thumbnail))
|
||||
self.images.borrow().contains_key(&(*id, is_thumbnail))
|
||||
}
|
||||
|
||||
pub fn get(&mut self, id: &Uuid) -> Option<&Image> {
|
||||
// Try to get full image first, fallback to thumbnail
|
||||
let has_full = self.images.contains_key(&(*id, false));
|
||||
if has_full {
|
||||
self.get_internal(id, false)
|
||||
} else {
|
||||
self.get_internal(id, true)
|
||||
}
|
||||
pub fn get(&mut self, id: &Uuid) -> Option<Image> {
|
||||
self.get_internal(id, false)
|
||||
.or_else(|| self.get_internal(id, true))
|
||||
}
|
||||
|
||||
pub fn get_cpu_image(&mut self, id: &Uuid) -> Option<Image> {
|
||||
let gpu_image = self.get(id)?.clone();
|
||||
gpu_image.make_non_texture_image(self.context.as_mut())
|
||||
let gpu_image = self.get(id)?;
|
||||
gpu_image.make_non_texture_image(self.context.borrow_mut().as_mut())
|
||||
}
|
||||
|
||||
fn get_internal(&mut self, id: &Uuid, is_thumbnail: bool) -> Option<&Image> {
|
||||
fn get_internal(&mut self, id: &Uuid, is_thumbnail: bool) -> Option<Image> {
|
||||
let key = (*id, is_thumbnail);
|
||||
// Use entry API to mutate the HashMap in-place if needed
|
||||
if let Some(entry) = self.images.get_mut(&key) {
|
||||
let mut images = self.images.borrow_mut();
|
||||
if let Some(entry) = images.get_mut(&key) {
|
||||
match entry {
|
||||
StoredImage::Gpu(ref img) => Some(img),
|
||||
StoredImage::Gpu(ref img) => Some(img.clone()),
|
||||
StoredImage::Raw(raw_data) => {
|
||||
let gpu_image = decode_image(&mut self.context, raw_data)?;
|
||||
let gpu_image = decode_image(&mut self.context.borrow_mut(), raw_data)?;
|
||||
let clone = gpu_image.clone();
|
||||
*entry = StoredImage::Gpu(gpu_image);
|
||||
|
||||
if let StoredImage::Gpu(ref img) = entry {
|
||||
Some(img)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
Some(clone)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
use crate::shapes::{Blur, BlurType, Shape};
|
||||
|
||||
/// Combines every visible layer blur currently active (ancestors + shape)
|
||||
/// into a single equivalent blur. Layer blur radii compound by adding their
|
||||
/// variances (σ² = radius²), so we:
|
||||
/// 1. Convert each blur radius into variance via `blur_variance`.
|
||||
/// 2. Sum all variances.
|
||||
/// 3. Convert the total variance back to a radius with `blur_from_variance`.
|
||||
///
|
||||
/// This keeps blur math consistent everywhere we need to merge blur sources.
|
||||
pub fn combined_layer_blur(
|
||||
nested_blurs: &[Option<Blur>],
|
||||
cached_layer_blur: &mut Option<Option<Blur>>,
|
||||
shape_blur: Option<Blur>,
|
||||
) -> Option<Blur> {
|
||||
if let Some(ref cached) = cached_layer_blur {
|
||||
return *cached;
|
||||
}
|
||||
let mut total = 0.;
|
||||
|
||||
for nested_blur in nested_blurs.iter().flatten() {
|
||||
total += blur_variance(Some(*nested_blur));
|
||||
}
|
||||
|
||||
total += blur_variance(shape_blur);
|
||||
|
||||
let result = blur_from_variance(total);
|
||||
*cached_layer_blur = Some(result);
|
||||
result
|
||||
}
|
||||
|
||||
/// Returns the variance (radius²) for a visible layer blur, or zero if the
|
||||
/// blur is hidden/absent. Working in variance space lets us add multiple
|
||||
/// blur radii correctly.
|
||||
pub fn blur_variance(blur: Option<Blur>) -> f32 {
|
||||
match blur {
|
||||
Some(blur) if !blur.hidden && blur.blur_type == BlurType::LayerBlur => blur.value.powi(2),
|
||||
_ => 0.,
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds a blur from an accumulated variance value. If no variance was
|
||||
/// contributed, we return `None`; otherwise the equivalent single radius is
|
||||
/// `sqrt(total)`.
|
||||
pub fn blur_from_variance(total: f32) -> Option<Blur> {
|
||||
(total > 0.).then(|| Blur::new(BlurType::LayerBlur, false, total.sqrt()))
|
||||
}
|
||||
|
||||
/// Convenience helper to merge two optional layer blurs using the same
|
||||
/// variance math as `combined_layer_blur`.
|
||||
pub fn combine_blur_values(base: Option<Blur>, extra: Option<Blur>) -> Option<Blur> {
|
||||
let total = blur_variance(base) + blur_variance(extra);
|
||||
blur_from_variance(total)
|
||||
}
|
||||
|
||||
pub fn frame_clip_layer_blur(shape: &Shape) -> Option<Blur> {
|
||||
shape.frame_clip_layer_blur()
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -297,13 +297,14 @@ pub(super) fn handle_stroke_caps(
|
||||
blur: Option<&ImageFilter>,
|
||||
_antialias: bool,
|
||||
) {
|
||||
// Closed shapes don't have caps
|
||||
if !is_open {
|
||||
return;
|
||||
}
|
||||
|
||||
// When both ends share the same simple line cap, Skia already drew it
|
||||
// natively via `PaintCap` on the stroke paint, so skip the manual overlay.
|
||||
if stroke.cap_start.is_none() && stroke.cap_end.is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
if stroke.to_skia_linecap().is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -28,24 +28,26 @@ pub fn stroke_paragraph_builder_group_from_text(
|
||||
let mut group_layer_opacity: Option<f32> = None;
|
||||
|
||||
for paragraph in text_content.paragraphs() {
|
||||
let mut stroke_paragraphs_map: std::collections::HashMap<usize, ParagraphBuilder> =
|
||||
std::collections::HashMap::new();
|
||||
let mut stroke_paragraphs: Vec<ParagraphBuilder> = Vec::new();
|
||||
let (stroke_paints, stroke_layer_opacity) =
|
||||
get_text_stroke_paints(stroke, bounds, remove_stroke_alpha);
|
||||
|
||||
if group_layer_opacity.is_none() {
|
||||
group_layer_opacity = stroke_layer_opacity;
|
||||
}
|
||||
|
||||
for span in paragraph.children().iter() {
|
||||
let (stroke_paints, stroke_layer_opacity) =
|
||||
get_text_stroke_paints(stroke, bounds, remove_stroke_alpha);
|
||||
|
||||
if group_layer_opacity.is_none() {
|
||||
group_layer_opacity = stroke_layer_opacity;
|
||||
}
|
||||
|
||||
let text: String = span.apply_text_transform();
|
||||
|
||||
for (paint_idx, stroke_paint) in stroke_paints.iter().enumerate() {
|
||||
let builder = stroke_paragraphs_map.entry(paint_idx).or_insert_with(|| {
|
||||
if stroke_paragraphs.len() < stroke_paints.len() {
|
||||
for _stroke_paint in stroke_paints.iter() {
|
||||
let paragraph_style = paragraph.paragraph_to_style();
|
||||
ParagraphBuilder::new(¶graph_style, fonts)
|
||||
});
|
||||
stroke_paragraphs.push(ParagraphBuilder::new(¶graph_style, fonts));
|
||||
}
|
||||
}
|
||||
|
||||
for (paint_idx, stroke_paint) in stroke_paints.iter().enumerate() {
|
||||
let builder = &mut stroke_paragraphs[paint_idx];
|
||||
let stroke_paint = stroke_paint.clone();
|
||||
let remove_alpha = use_shadow.unwrap_or(false) && !span.is_transparent();
|
||||
let stroke_style = span.to_stroke_style(
|
||||
@@ -59,10 +61,6 @@ pub fn stroke_paragraph_builder_group_from_text(
|
||||
}
|
||||
}
|
||||
|
||||
let stroke_paragraphs: Vec<ParagraphBuilder> = (0..stroke_paragraphs_map.len())
|
||||
.filter_map(|i| stroke_paragraphs_map.remove(&i))
|
||||
.collect();
|
||||
|
||||
paragraph_group.push(stroke_paragraphs);
|
||||
}
|
||||
|
||||
@@ -555,7 +553,7 @@ fn draw_text(
|
||||
let layer_rec = SaveLayerRec::default().paint(&opacity_paint);
|
||||
canvas.save_layer(&layer_rec);
|
||||
} else {
|
||||
canvas.save_layer(&SaveLayerRec::default());
|
||||
canvas.save();
|
||||
}
|
||||
|
||||
paint_text_with_emoji_overlay(canvas, shape, paragraph_builder_groups, overlay_emoji);
|
||||
|
||||
@@ -3,7 +3,6 @@ use skia_safe::{self as skia, Color4f};
|
||||
use super::{RenderState, ShapesPoolRef, SurfaceId};
|
||||
use crate::globals::get_ui_state;
|
||||
use crate::render::{grid_layout, rulers};
|
||||
use crate::shapes::{Layout, Type};
|
||||
pub mod guides;
|
||||
|
||||
pub fn render(render_state: &mut RenderState, shapes: ShapesPoolRef) {
|
||||
@@ -30,28 +29,13 @@ pub fn render(render_state: &mut RenderState, shapes: ShapesPoolRef) {
|
||||
}
|
||||
|
||||
// Render overlays for empty grid frames
|
||||
for shape in shapes.iter() {
|
||||
if shape.id.is_nil() || !shape.children.is_empty() {
|
||||
let empty_grid_ids: std::collections::HashSet<crate::uuid::Uuid> =
|
||||
std::mem::take(&mut render_state.empty_grid_frame_ids);
|
||||
for id in &empty_grid_ids {
|
||||
if show_grid_id == Some(*id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if show_grid_id == Some(shape.id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Type::Frame(frame) = &shape.shape_type else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if !matches!(frame.layout, Some(Layout::GridLayout(_, _))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if shape.deleted() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(shape) = shapes.get(&shape.id) {
|
||||
if let Some(shape) = shapes.get(id) {
|
||||
grid_layout::render_overlay(
|
||||
zoom,
|
||||
render_state.options.antialias_threshold,
|
||||
@@ -61,6 +45,7 @@ pub fn render(render_state: &mut RenderState, shapes: ShapesPoolRef) {
|
||||
);
|
||||
}
|
||||
}
|
||||
render_state.empty_grid_frame_ids = empty_grid_ids;
|
||||
|
||||
let viewbox = render_state.viewbox;
|
||||
let ruler_state = render_state.rulers;
|
||||
|
||||
@@ -16,10 +16,11 @@ use super::{get_dest_rect, get_source_rect};
|
||||
// VectorTarget — vector export backend selector
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Vector export backend selector (PDF today; SVG could be added as a variant).
|
||||
/// Vector backend selector (PDF export or realtime backbuffer rendering).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(super) enum VectorTarget {
|
||||
Pdf,
|
||||
Backbuffer,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::render::RenderState;
|
||||
use crate::state::ShapesPoolRef;
|
||||
use crate::uuid::Uuid;
|
||||
|
||||
pub fn viewer_masked_pass(include_filter: &Option<HashSet<Uuid>>) -> bool {
|
||||
include_filter.is_some()
|
||||
}
|
||||
|
||||
pub fn reset_viewer_masked_surfaces(render_state: &mut RenderState) {
|
||||
render_state
|
||||
.surfaces
|
||||
.clear_backbuffer(render_state.background_color);
|
||||
render_state.surfaces.clear_tile_atlas();
|
||||
}
|
||||
|
||||
/// Precompute the set of all ancestor ids that are visible for the viewer
|
||||
/// masked pass, avoiding recursive checks on the hot path.
|
||||
pub fn precompute_viewer_visible_set(render_state: &mut RenderState, tree: ShapesPoolRef) {
|
||||
render_state.viewer_visible_set = None;
|
||||
let Some(ref include) = render_state.include_filter else {
|
||||
return;
|
||||
};
|
||||
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) {
|
||||
match raw.parent_id {
|
||||
Some(ref parent_id) => {
|
||||
visible.insert(*parent_id);
|
||||
current_id = parent_id;
|
||||
}
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
render_state.viewer_visible_set = Some(visible);
|
||||
}
|
||||
|
||||
/// True when the shape or any descendant is whitelisted.
|
||||
pub fn shape_visible_in_include_filter(
|
||||
viewer_visible_set: &Option<HashSet<Uuid>>,
|
||||
shape_id: &Uuid,
|
||||
) -> bool {
|
||||
match viewer_visible_set {
|
||||
Some(visible) => visible.contains(shape_id),
|
||||
None => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// When an include whitelist is active, only those ids are painted.
|
||||
pub fn shape_should_paint_for_viewer_layer(
|
||||
include_filter: &Option<HashSet<Uuid>>,
|
||||
shape_id: &Uuid,
|
||||
) -> bool {
|
||||
match include_filter {
|
||||
Some(include) => include.contains(shape_id),
|
||||
None => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Viewer layer mask: traverse whitelisted subtrees; paint only listed ids.
|
||||
pub fn shape_visible_for_viewer_layer(
|
||||
viewer_render_root: &Option<Uuid>,
|
||||
viewer_visible_set: &Option<HashSet<Uuid>>,
|
||||
shape_id: &Uuid,
|
||||
) -> bool {
|
||||
if viewer_render_root.as_ref() == Some(shape_id) {
|
||||
return true;
|
||||
}
|
||||
shape_visible_in_include_filter(viewer_visible_set, shape_id)
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
use crate::shapes::{Corners, Shadow, Shape, Type};
|
||||
use crate::state::ShapesPoolRef;
|
||||
use crate::uuid::Uuid;
|
||||
use skia_safe::{Matrix, Rect};
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub type ClipStack = Vec<(Rect, Option<Corners>, Matrix, Matrix)>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct NodeRenderState {
|
||||
pub id: Uuid,
|
||||
pub(crate) visited_children: bool,
|
||||
pub(crate) clip_bounds: Option<ClipStack>,
|
||||
pub(crate) visited_mask: bool,
|
||||
pub(crate) mask: bool,
|
||||
pub(crate) flattened: bool,
|
||||
}
|
||||
|
||||
/// Get simplified children of a container, flattening nested flattened containers
|
||||
pub fn get_simplified_children<'a>(
|
||||
tree: ShapesPoolRef<'a>,
|
||||
shape: &'a Shape,
|
||||
result: &mut Vec<Uuid>,
|
||||
) {
|
||||
for child_id in shape.children_ids_iter(false) {
|
||||
if let Some(child) = tree.get(child_id) {
|
||||
if child.can_flatten() {
|
||||
get_simplified_children(tree, child, result);
|
||||
} else {
|
||||
result.push(*child_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl NodeRenderState {
|
||||
pub fn is_root(&self) -> bool {
|
||||
self.id.is_nil()
|
||||
}
|
||||
|
||||
/// Calculates the clip bounds for child elements of a given shape.
|
||||
///
|
||||
/// This function determines the clipping region that should be applied to child elements
|
||||
/// when rendering. It takes into account the element's selection rectangle, transform.
|
||||
///
|
||||
/// # Parameters
|
||||
///
|
||||
/// * `element` - The shape element for which to calculate clip bounds
|
||||
/// * `offset` - Optional offset (x, y) to adjust the bounds position. When provided,
|
||||
/// the bounds are translated by the negative of this offset, effectively moving
|
||||
/// the clipping region to compensate for coordinate system transformations.
|
||||
/// This is useful for nested coordinate systems or when elements are grouped
|
||||
/// and need relative positioning adjustments.
|
||||
fn append_clip(
|
||||
clip_stack: Option<ClipStack>,
|
||||
clip: (Rect, Option<Corners>, Matrix, Matrix),
|
||||
) -> Option<ClipStack> {
|
||||
match clip_stack {
|
||||
Some(mut stack) => {
|
||||
stack.push(clip);
|
||||
Some(stack)
|
||||
}
|
||||
None => Some(vec![clip]),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_children_clip_bounds(
|
||||
&self,
|
||||
element: &Shape,
|
||||
offset: Option<(f32, f32)>,
|
||||
clip_inset: Option<f32>,
|
||||
) -> Option<ClipStack> {
|
||||
if self.id.is_nil() || !element.clip() {
|
||||
return self.clip_bounds.clone();
|
||||
}
|
||||
|
||||
let mut bounds = element.selrect();
|
||||
if let Some(offset) = offset {
|
||||
let x = bounds.x() - offset.0;
|
||||
let y = bounds.y() - offset.1;
|
||||
let width = bounds.width();
|
||||
let height = bounds.height();
|
||||
bounds.set_xywh(x, y, width, height);
|
||||
}
|
||||
let mut transform = element.transform;
|
||||
transform.post_translate(bounds.center());
|
||||
transform.pre_translate(-bounds.center());
|
||||
|
||||
let corners = match &element.shape_type {
|
||||
Type::Rect(data) => data.corners,
|
||||
Type::Frame(data) => data.corners,
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(clip_inset) = clip_inset.filter(|&e| e > 0.0) {
|
||||
bounds.inset((clip_inset, clip_inset));
|
||||
}
|
||||
|
||||
Self::append_clip(
|
||||
self.clip_bounds.clone(),
|
||||
(
|
||||
bounds,
|
||||
corners,
|
||||
transform,
|
||||
transform.invert().unwrap_or_default(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
/// Calculates the clip bounds for shadow rendering of a given shape.
|
||||
///
|
||||
/// This function determines the clipping region that should be applied when rendering a
|
||||
/// shadow for a shape element. For frames, it uses the shadow bounds to clip nested
|
||||
/// shadows. For groups, it returns the existing clip bounds since groups should not
|
||||
/// constrain nested shadows based on their selection rectangle bounds.
|
||||
///
|
||||
/// # Parameters
|
||||
///
|
||||
/// * `element` - The shape element for which to calculate shadow clip bounds
|
||||
/// * `shadow` - The shadow configuration containing blur, offset, and other properties
|
||||
pub fn get_nested_shadow_clip_bounds(
|
||||
&self,
|
||||
element: &Shape,
|
||||
shadow: &Shadow,
|
||||
) -> Option<ClipStack> {
|
||||
if self.id.is_nil() {
|
||||
return self.clip_bounds.clone();
|
||||
}
|
||||
|
||||
// Assert that the shape is either a Frame or Group
|
||||
assert!(
|
||||
matches!(element.shape_type, Type::Frame(_) | Type::Group(_)),
|
||||
"Shape must be a Frame or Group for nested shadow clip bounds calculation"
|
||||
);
|
||||
|
||||
match &element.shape_type {
|
||||
Type::Frame(_) => {
|
||||
let mut bounds = element.get_selrect_shadow_bounds(shadow);
|
||||
let blur_inset = (shadow.blur * 2.).max(0.0);
|
||||
if blur_inset > 0.0 {
|
||||
let max_inset_x = (bounds.width() * 0.5).max(0.0);
|
||||
let max_inset_y = (bounds.height() * 0.5).max(0.0);
|
||||
// Clamp the inset so we never shrink more than half of the width/height;
|
||||
// otherwise the rect could end up inverted on small frames.
|
||||
let inset_x = blur_inset.min(max_inset_x);
|
||||
let inset_y = blur_inset.min(max_inset_y);
|
||||
if inset_x > 0.0 || inset_y > 0.0 {
|
||||
bounds.inset((inset_x, inset_y));
|
||||
}
|
||||
}
|
||||
|
||||
let mut transform = element.transform;
|
||||
transform.post_translate(element.center());
|
||||
transform.pre_translate(-element.center());
|
||||
|
||||
let corners = match &element.shape_type {
|
||||
Type::Frame(data) => data.corners,
|
||||
_ => None,
|
||||
};
|
||||
|
||||
Self::append_clip(
|
||||
self.clip_bounds.clone(),
|
||||
(
|
||||
bounds,
|
||||
corners,
|
||||
transform,
|
||||
transform.invert().unwrap_or_default(),
|
||||
),
|
||||
)
|
||||
}
|
||||
_ => self.clip_bounds.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Sort by z_index descending (higher z renders on top).
|
||||
* The sort is stable so if the values are equal the index for the children
|
||||
* has preference.
|
||||
* When changing this method check the benchmark
|
||||
*/
|
||||
pub fn sort_z_index(tree: ShapesPoolRef, element: &Shape, children_ids: Vec<Uuid>) -> Vec<Uuid> {
|
||||
if element.has_layout() {
|
||||
let mut ids = children_ids;
|
||||
ids.sort_by_cached_key(|id| {
|
||||
std::cmp::Reverse(tree.get(id).map(|s| s.z_index()).unwrap_or(0))
|
||||
});
|
||||
if element.is_flex() && !element.is_flex_reverse() {
|
||||
ids.reverse();
|
||||
}
|
||||
ids
|
||||
} else {
|
||||
children_ids
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RenderStats {
|
||||
pub counts: HashMap<Uuid, i32>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl RenderStats {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
counts: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn count(&mut self, id: Uuid) -> i32 {
|
||||
let counter = self.counts.entry(id).or_insert(0);
|
||||
*counter += 1;
|
||||
*counter
|
||||
}
|
||||
|
||||
fn clear(&mut self) {
|
||||
self.counts.clear();
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn get(&self, id: &Uuid) -> Option<&i32> {
|
||||
self.counts.get(id)
|
||||
}
|
||||
|
||||
pub(crate) fn print(&self) {
|
||||
let mut sum: i32 = 0;
|
||||
for (&id, &count) in self.counts.iter() {
|
||||
println!("{}: {}", id, count);
|
||||
sum += count;
|
||||
}
|
||||
println!("{}: {}", self.counts.len(), sum);
|
||||
}
|
||||
}
|
||||
+23
-103
@@ -2,7 +2,9 @@ use skia_safe::{self as skia};
|
||||
|
||||
use indexmap::IndexSet;
|
||||
|
||||
use crate::tiles::{self, TileRect};
|
||||
use crate::uuid::Uuid;
|
||||
use crate::view::Viewbox;
|
||||
use std::borrow::Cow;
|
||||
use std::cell::{OnceCell, RefCell};
|
||||
use std::collections::HashSet;
|
||||
@@ -51,7 +53,7 @@ pub use svgraw::*;
|
||||
pub use text::*;
|
||||
pub use transform::*;
|
||||
|
||||
use crate::math::{self, Bounds, Matrix, Point};
|
||||
use crate::math::{self, Bounds, IRect, Matrix, Point};
|
||||
|
||||
use crate::state::ShapesPoolRef;
|
||||
|
||||
@@ -179,6 +181,7 @@ pub struct Shape {
|
||||
pub children: Vec<Uuid>,
|
||||
pub selrect: math::Rect,
|
||||
pub transform: Matrix,
|
||||
transform_centered: Option<Matrix>,
|
||||
pub rotation: f32,
|
||||
pub constraint_h: Option<ConstraintH>,
|
||||
pub constraint_v: Option<ConstraintV>,
|
||||
@@ -281,6 +284,7 @@ impl Shape {
|
||||
children: Vec::new(),
|
||||
selrect: math::Rect::new_empty(),
|
||||
transform: Matrix::default(),
|
||||
transform_centered: None,
|
||||
rotation: 0.,
|
||||
constraint_h: None,
|
||||
constraint_v: None,
|
||||
@@ -417,11 +421,29 @@ impl Shape {
|
||||
self.invalidate_extrect();
|
||||
}
|
||||
|
||||
pub fn is_rotated(&self) -> bool {
|
||||
self.rotation != 0.0
|
||||
}
|
||||
|
||||
pub fn set_transform(&mut self, a: f32, b: f32, c: f32, d: f32, e: f32, f: f32) {
|
||||
self.transform = Matrix::new_all(a, c, e, b, d, f, 0.0, 0.0, 1.0);
|
||||
if self.transform_centered.is_none() && self.is_rotated() {
|
||||
let center = self.center();
|
||||
let mut matrix = self.transform;
|
||||
matrix.post_translate(center);
|
||||
matrix.pre_translate(-center);
|
||||
self.transform_centered = Some(matrix);
|
||||
}
|
||||
self.invalidate_extrect();
|
||||
}
|
||||
|
||||
pub fn get_transform(&self) -> Matrix {
|
||||
let Some(transform) = self.transform_centered else {
|
||||
return self.transform;
|
||||
};
|
||||
transform
|
||||
}
|
||||
|
||||
pub fn set_opacity(&mut self, opacity: f32) {
|
||||
self.opacity = opacity;
|
||||
}
|
||||
@@ -1382,108 +1404,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();
|
||||
|
||||
@@ -15,7 +15,7 @@ use crate::shapes::{
|
||||
ConstraintH, ConstraintV, Frame, Group, GrowType, Layout, Modifier, Shape, TransformEntry,
|
||||
TransformEntrySource, Type,
|
||||
};
|
||||
use crate::state::{ShapesPoolRef, State};
|
||||
use crate::state::{DesignState, ShapesPoolRef};
|
||||
use crate::uuid::Uuid;
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
@@ -176,7 +176,7 @@ fn set_pixel_precision(transform: &mut Matrix, bounds: &mut Bounds) {
|
||||
fn propagate_transform(
|
||||
entry: TransformEntry,
|
||||
pixel_precision: bool,
|
||||
state: &State,
|
||||
state: &DesignState,
|
||||
entries: &mut VecDeque<Modifier>,
|
||||
bounds: &mut HashMap<Uuid, Bounds>,
|
||||
modifiers: &mut HashMap<Uuid, Matrix>,
|
||||
@@ -324,7 +324,7 @@ fn propagate_transform(
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn propagate_reflow(
|
||||
id: &Uuid,
|
||||
state: &State,
|
||||
state: &DesignState,
|
||||
entries: &mut VecDeque<Modifier>,
|
||||
bounds: &mut HashMap<Uuid, Bounds>,
|
||||
layout_reflows: &mut HashSet<Uuid>,
|
||||
@@ -380,7 +380,7 @@ fn propagate_reflow(
|
||||
|
||||
fn reflow_shape(
|
||||
id: &Uuid,
|
||||
state: &State,
|
||||
state: &DesignState,
|
||||
reflown: &mut HashSet<Uuid>,
|
||||
entries: &mut VecDeque<Modifier>,
|
||||
bounds: &mut HashMap<Uuid, Bounds>,
|
||||
@@ -409,7 +409,7 @@ fn reflow_shape(
|
||||
}
|
||||
|
||||
pub fn propagate_modifiers(
|
||||
state: &State,
|
||||
state: &DesignState,
|
||||
modifiers: &[TransformEntry],
|
||||
pixel_precision: bool,
|
||||
) -> Result<Vec<TransformEntry>> {
|
||||
|
||||
@@ -315,8 +315,6 @@ fn set_flex_multi_span(
|
||||
for track in tracks[start..end].iter_mut() {
|
||||
if track.track_type == GridTrackType::Flex {
|
||||
let new_size = alloc.clamp(track.size, track.max_size);
|
||||
let aloc = new_size - track.size;
|
||||
dist -= aloc;
|
||||
track.size = new_size;
|
||||
}
|
||||
}
|
||||
|
||||
+37
-31
@@ -11,6 +11,7 @@ pub use text_editor::*;
|
||||
pub use ui::UIState;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::globals::get_tile_render_state;
|
||||
use crate::render::FrameType;
|
||||
use crate::shapes::{grid_layout::grid_cell_data, Shape};
|
||||
use crate::uuid::Uuid;
|
||||
@@ -21,7 +22,7 @@ use crate::{get_render_state, tiles};
|
||||
/// 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 {
|
||||
pub(crate) struct DesignState {
|
||||
pub current_id: Option<Uuid>,
|
||||
pub current_browser: u8,
|
||||
pub shapes: ShapesPool,
|
||||
@@ -30,7 +31,7 @@ pub(crate) struct State {
|
||||
pub loading: bool,
|
||||
}
|
||||
|
||||
impl State {
|
||||
impl DesignState {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
current_id: None,
|
||||
@@ -65,26 +66,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 +102,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();
|
||||
@@ -168,15 +172,17 @@ 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
|
||||
let tile_render_state = get_tile_render_state();
|
||||
let indexed_tiles: Vec<tiles::Tile> = tile_render_state
|
||||
.tiles
|
||||
.get_tiles_of(shape.id)
|
||||
.map(|t| t.iter().copied().collect())
|
||||
.unwrap_or_default();
|
||||
|
||||
let tile_render_state = get_tile_render_state();
|
||||
for tile in indexed_tiles {
|
||||
render_state.remove_cached_tile(tile);
|
||||
render_state.tiles.remove_shape_at(tile, shape.id);
|
||||
tile_render_state.tiles.remove_shape_at(tile, shape.id);
|
||||
}
|
||||
|
||||
if let Some(shape_to_delete) = self.shapes.get(&id) {
|
||||
@@ -242,7 +248,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 +260,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 {
|
||||
|
||||
+185
-14
@@ -1,14 +1,134 @@
|
||||
use crate::render::Surfaces;
|
||||
use crate::globals::{get_design_state, get_render_state, get_tile_render_state};
|
||||
use crate::shapes::Shape;
|
||||
use crate::uuid::Uuid;
|
||||
use crate::view::Viewbox;
|
||||
use crate::{render::Surfaces, state::ShapesPoolRef};
|
||||
use skia_safe as skia;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
#[derive(Debug)]
|
||||
#[repr(u8)]
|
||||
pub enum TileDisplayPhase {
|
||||
Enter = 0,
|
||||
Exit = 1,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TileDisplayItem {
|
||||
pub id: Uuid,
|
||||
pub phase: TileDisplayPhase,
|
||||
}
|
||||
|
||||
impl TileDisplayItem {
|
||||
pub fn enter(id: Uuid) -> Self {
|
||||
Self {
|
||||
id,
|
||||
phase: TileDisplayPhase::Enter,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn exit(id: Uuid) -> Self {
|
||||
Self {
|
||||
id,
|
||||
phase: TileDisplayPhase::Exit,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TileDisplayList {
|
||||
output: HashMap<Tile, Vec<TileDisplayItem>>,
|
||||
}
|
||||
|
||||
impl TileDisplayList {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
output: HashMap::new()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn compute_from(&mut self, root_id: Uuid) {
|
||||
self.output.clear();
|
||||
self.dfs(root_id);
|
||||
}
|
||||
|
||||
pub fn get(&self, tile: Tile) -> Option<&Vec<TileDisplayItem>> {
|
||||
self.output.get(&tile)
|
||||
}
|
||||
|
||||
// Recursive helper that pushes to the output vector
|
||||
fn dfs(
|
||||
&mut self,
|
||||
id: Uuid,
|
||||
) {
|
||||
let design_state = get_design_state();
|
||||
let render_state = get_render_state();
|
||||
let tile_render_state= get_tile_render_state();
|
||||
|
||||
let shapes = &design_state.shapes;
|
||||
if id.is_nil() {
|
||||
let shape = shapes.get(&id).unwrap();
|
||||
for shape_id in shape.children_ids(false) {
|
||||
self.dfs(shape_id);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let shape = shapes.get(&id).unwrap();
|
||||
let tile_rect = TileRect::from_shape_and_viewbox(shape, render_state.viewbox);
|
||||
let intersection_rect = tile_render_state.viewbox.interest_rect.intersection(&tile_rect);
|
||||
if intersection_rect.is_degenerate() {
|
||||
return;
|
||||
}
|
||||
|
||||
for tile in intersection_rect.iter(true) {
|
||||
let _ = self.output.entry(tile).or_insert_with(Vec::new);
|
||||
self.output.get_mut(&tile).unwrap().push(TileDisplayItem::enter(shape.id));
|
||||
}
|
||||
|
||||
for shape_id in shape.children_ids(false) {
|
||||
self.dfs(shape_id);
|
||||
}
|
||||
|
||||
for tile in intersection_rect.iter(true) {
|
||||
self.output.get_mut(&tile).unwrap().push(TileDisplayItem::exit(shape.id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TileRenderState {
|
||||
pub current: Option<Tile>,
|
||||
pub current_had_shapes: bool,
|
||||
pub viewbox: TileViewbox,
|
||||
pub tiles: TileHashMap,
|
||||
pub pending: PendingTiles,
|
||||
pub display_list: TileDisplayList,
|
||||
}
|
||||
|
||||
impl TileRenderState {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
current: None,
|
||||
current_had_shapes: false,
|
||||
tiles: TileHashMap::new(),
|
||||
viewbox: TileViewbox::new(),
|
||||
pending: PendingTiles::new(),
|
||||
display_list: TileDisplayList::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)]
|
||||
pub struct Tile(pub i32, pub i32);
|
||||
|
||||
impl Tile {
|
||||
pub fn from(x: i32, y: i32) -> Self {
|
||||
Tile(x, y)
|
||||
Self(x, y)
|
||||
}
|
||||
|
||||
pub fn new_empty() -> Self {
|
||||
Self(0, 0)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
@@ -30,15 +150,13 @@ 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,
|
||||
)
|
||||
fn itrunc(x: f32) -> f32 {
|
||||
if x < 0.0 {
|
||||
x.floor()
|
||||
} else {
|
||||
x.ceil()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,10 +165,38 @@ pub struct TileRect(pub i32, pub i32, pub i32, pub i32);
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl TileRect {
|
||||
pub fn empty() -> Self {
|
||||
pub fn new_empty() -> Self {
|
||||
Self(0, 0, 0, 0)
|
||||
}
|
||||
|
||||
pub fn from_scaled(other: &TileRect, scale: f32) -> Self {
|
||||
Self(
|
||||
itrunc(other.0 as f32 * scale) as i32,
|
||||
itrunc(other.1 as f32 * scale) as i32,
|
||||
itrunc(other.2 as f32 * scale) as i32,
|
||||
itrunc(other.3 as f32 * scale) as i32,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn from_shape(shape: &Shape, scale: f32) -> Self {
|
||||
let tile_rect = get_tiles_for_rect(shape.selrect, TILE_SIZE);
|
||||
Self::from_scaled(&tile_rect, scale)
|
||||
}
|
||||
|
||||
pub fn from_shape_and_viewbox(shape: &Shape, viewbox: Viewbox) -> Self {
|
||||
Self::from_shape(shape, viewbox.get_scale())
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn intersection(&self, other: &Self) -> Self {
|
||||
Self (
|
||||
self.left().max(other.left()),
|
||||
self.top().max(other.top()),
|
||||
self.right().min(other.right()),
|
||||
self.bottom().min(other.bottom()),
|
||||
)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn is_degenerate(&self) -> bool {
|
||||
self.left() > self.right() || self.top() > self.bottom()
|
||||
@@ -131,6 +277,13 @@ impl TileRect {
|
||||
&& tile.y() <= self.bottom()
|
||||
}
|
||||
|
||||
pub fn set_from_tile_bounds(&mut self, l: f32, t: f32, r: f32, b: f32, tile_size: f32) {
|
||||
self.0 = (l / tile_size) as i32;
|
||||
self.1 = (t / tile_size) as i32;
|
||||
self.2 = (r / tile_size) as i32;
|
||||
self.3 = (b / tile_size) as i32;
|
||||
}
|
||||
|
||||
pub fn iter(self, inclusive: bool) -> TileRectIter {
|
||||
TileRectIter::new(self, inclusive)
|
||||
}
|
||||
@@ -175,7 +328,7 @@ impl Iterator for TileRectIter {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct TileViewbox {
|
||||
pub visible_rect: TileRect,
|
||||
pub interest_rect: TileRect,
|
||||
@@ -184,6 +337,15 @@ pub struct TileViewbox {
|
||||
}
|
||||
|
||||
impl TileViewbox {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
visible_rect: TileRect::new_empty(),
|
||||
interest_rect: TileRect::new_empty(),
|
||||
interest: 0,
|
||||
center: Tile::new_empty(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_with_interest(viewbox: &Viewbox, interest: i32) -> Self {
|
||||
Self {
|
||||
visible_rect: get_tiles_for_viewbox(viewbox),
|
||||
@@ -259,6 +421,7 @@ pub fn get_tile_rect(tile: Tile, scale: f32) -> skia::Rect {
|
||||
}
|
||||
|
||||
// This structure is useful to keep all the shape uuids by shape id.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TileHashMap {
|
||||
grid: HashMap<Tile, HashSet<Uuid>>,
|
||||
index: HashMap<Uuid, HashSet<Tile>>,
|
||||
@@ -279,6 +442,13 @@ impl TileHashMap {
|
||||
true
|
||||
}
|
||||
|
||||
pub fn has_shape_at(&self, tile: Tile, id: Uuid) -> bool {
|
||||
let Some(shapes) = self.grid.get(&tile) else {
|
||||
return false;
|
||||
};
|
||||
shapes.contains(&id)
|
||||
}
|
||||
|
||||
pub fn get_shapes_at(&mut self, tile: Tile) -> Option<&HashSet<Uuid>> {
|
||||
self.grid.get(&tile)
|
||||
}
|
||||
@@ -317,7 +487,7 @@ const VIEWPORT_SPIRAL_DEFAULT_CAPACITY: usize = VIEWPORT_DEFAULT_CAPACITY;
|
||||
/// Cached spiral of tile offsets for a given grid size.
|
||||
///
|
||||
/// Offsets are centered at (0,0) and must be translated by the desired origin/center tile.
|
||||
#[derive(Debug, Default)]
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct TileSpiral {
|
||||
offsets: Vec<Tile>,
|
||||
columns: usize,
|
||||
@@ -404,6 +574,7 @@ impl TileSpiral {
|
||||
|
||||
// This structure keeps the list of tiles that are in the pending list, the
|
||||
// ones that are going to be rendered.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PendingTiles {
|
||||
pub list: Vec<Tile>,
|
||||
pub spiral: TileSpiral,
|
||||
@@ -419,7 +590,7 @@ impl PendingTiles {
|
||||
Self {
|
||||
list: Vec::with_capacity(VIEWPORT_DEFAULT_CAPACITY),
|
||||
spiral: TileSpiral::new(),
|
||||
spiral_rect: TileRect::empty(),
|
||||
spiral_rect: TileRect::new_empty(),
|
||||
visible_cached: Vec::with_capacity(VIEWPORT_DEFAULT_CAPACITY),
|
||||
visible_uncached: Vec::with_capacity(VIEWPORT_DEFAULT_CAPACITY),
|
||||
interest_cached: Vec::with_capacity(VIEWPORT_DEFAULT_CAPACITY),
|
||||
|
||||
@@ -24,7 +24,7 @@ pub fn uuid_from_u32(id: [u32; 4]) -> Uuid {
|
||||
uuid_from_u32_quartet(id[0], id[1], id[2], id[3])
|
||||
}
|
||||
|
||||
pub fn get_image(image_id: &Uuid) -> Option<&Image> {
|
||||
pub fn get_image(image_id: &Uuid) -> Option<Image> {
|
||||
get_render_state().images.get(image_id)
|
||||
}
|
||||
|
||||
|
||||
+64
-12
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -2,13 +2,13 @@ use crate::error::{Error, Result};
|
||||
use crate::get_render_state;
|
||||
use crate::mem;
|
||||
use crate::shapes::Fill;
|
||||
use crate::state::State;
|
||||
use crate::state::DesignState;
|
||||
use crate::uuid::Uuid;
|
||||
use crate::with_state;
|
||||
use crate::{shapes::ImageFill, utils::uuid_from_u32_quartet};
|
||||
use macros::wasm_error;
|
||||
|
||||
fn touch_shapes_with_image(state: &mut State, image_id: Uuid) {
|
||||
fn touch_shapes_with_image(state: &mut DesignState, image_id: Uuid) {
|
||||
let ids: Vec<Uuid> = state
|
||||
.shapes
|
||||
.iter()
|
||||
|
||||
@@ -70,3 +70,8 @@ pub extern "C" fn is_font_uploaded(
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn flush_font_caches() {
|
||||
get_render_state().fonts_mut().flush_caches();
|
||||
}
|
||||
@@ -108,7 +108,7 @@ pub extern "C" fn set_guides() -> Result<()> {
|
||||
// Guides are drawn on the UI overlay composited onto `Target`. Refresh the
|
||||
// presented frame immediately so removed guides do not linger as stale pixels.
|
||||
with_state!(state, {
|
||||
get_render_state().present_frame(&state.shapes);
|
||||
get_render_state().present_frame();
|
||||
});
|
||||
|
||||
Ok(())
|
||||
|
||||
Reference in new issue
Block a user