diff --git a/frontend/src/app/main/data/workspace/selection.cljs b/frontend/src/app/main/data/workspace/selection.cljs index 4798517122..2530a2c742 100644 --- a/frontend/src/app/main/data/workspace/selection.cljs +++ b/frontend/src/app/main/data/workspace/selection.cljs @@ -29,10 +29,12 @@ [app.main.data.workspace.undo :as dwu] [app.main.data.workspace.viewport-wasm :as dwvw] [app.main.data.workspace.zoom :as dwz] + [app.main.features :as features] [app.main.refs :as refs] [app.main.router :as rt] [app.main.streams :as ms] [app.main.worker :as mw] + [app.render-wasm.api :as wasm.api] [app.util.mouse :as mse] [beicon.v2.core :as rx] [beicon.v2.operators :as rxo] @@ -159,7 +161,13 @@ ::dwsp/interrupt (if (some #(= % uuid/zero) frame-ids) (rt/nav :workspace params-without-board {::rt/replace true}) - (rt/nav :workspace params-board {::rt/replace true})))))))) + (rt/nav :workspace params-board {::rt/replace true}))))) + + ptk/EffectEvent + (effect [_ state _] + (when (features/active-feature? state "render-wasm/v1") + (js/console.log "select-shape" id) + (wasm.api/set-selected id)))))) (defn select-prev-shape ([] @@ -276,7 +284,14 @@ expand-s (->> (rx/of (dwc/expand-all-parents ids objects)) (rx/observe-on :async)) interrupt-s (rx/of ::dwsp/interrupt)] - (rx/merge expand-s interrupt-s))))) + (rx/merge expand-s interrupt-s))) + + ptk/EffectEvent + (effect [_ state _] + (js/console.log "select-shapes" (clj->js ids)) + ;; TODO: Que set-selected permita tanto un único identificador + ;; como una lista de identificadores. + #_(wasm.api/set-selected ids)))) (defn select-all [] diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index 71402806b4..58e3c69fa2 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -1614,6 +1614,13 @@ (keep #(get objects %)) all-ordered-ids)))) +(defn set-selected + [id] + (let [c1 (uuid/get-u32 id)] + (h/call wasm/internal-module "_set_selected" (aget c1 0) (aget c1 1) (aget c1 2) (aget c1 3)))) + +(unchecked-set js/globalThis "setSelected" set-selected) + (defn set-objects "Set all shape objects for rendering. diff --git a/render-wasm/Cargo.toml b/render-wasm/Cargo.toml index 77c4c10715..a0aed3ad36 100644 --- a/render-wasm/Cargo.toml +++ b/render-wasm/Cargo.toml @@ -9,7 +9,7 @@ description = "Wasm-based canvas renderer for Penpot" build = "build.rs" [features] -default = [] +default = ["profile"] stats = [] profile = ["profile-macros", "profile-raf"] profile-macros = [] diff --git a/render-wasm/src/globals.rs b/render-wasm/src/globals.rs index 0c9008c005..13c3b9bf06 100644 --- a/render-wasm/src/globals.rs +++ b/render-wasm/src/globals.rs @@ -5,12 +5,12 @@ use crate::emscripten::init_gl; use crate::mem; use crate::render::{gpu_state::GpuState, RenderState}; -use crate::state::{State, TextEditorState}; +use crate::state::{DesignState, TextEditorState}; -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 @@ -105,7 +105,7 @@ fn render_init(width: i32, height: i32) { /// 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)); } } diff --git a/render-wasm/src/main.rs b/render-wasm/src/main.rs index 83b3ad23f8..5e7082a558 100644 --- a/render-wasm/src/main.rs +++ b/render-wasm/src/main.rs @@ -932,6 +932,21 @@ pub extern "C" fn get_shape_extrect(a: u32, b: u32, c: u32, d: u32) -> Result<*m }) } +#[no_mangle] +#[wasm_error] +pub extern "C" fn set_selected(a: u32, b: u32, c: u32, d: u32) -> Result<()> { + let id = uuid_from_u32_quartet(a, b, c, d); + with_state!(state, { + println!("set_selected {}", id); + // TODO: Right now we're working only with one shape. + state.selected.clear(); + state.selected.insert(id); + Ok(()) + }) +} + +// TODO: Add `add_selected, delete_selected, clear_selected` + #[no_mangle] #[wasm_error] pub extern "C" fn render_shape_pixels( diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index f5712be7e6..d5881b5ffa 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -22,6 +22,7 @@ use options::RenderOptions; pub use surfaces::{SurfaceId, Surfaces}; use crate::error::{Error, Result}; +use crate::globals::get_design_state; use crate::math; use crate::shapes::{ all_with_ancestors, radius_to_sigma, Blur, BlurType, Corners, Fill, Shadow, Shape, SolidColor, @@ -400,21 +401,61 @@ pub(crate) struct RenderState { pub interactive_target_seeded: bool, /// GPU crops from `Backbuffer` or tile atlas keyed by shape id. Filled on full-frame completion; during /// drag, entries for the moved top-level selection are ensured here - pub backbuffer_crop_cache: HashMap, + pub shape_cache: ShapeCacheMap, } -pub struct InteractiveDragCrop { +#[derive(Debug)] +pub struct ShapeCache { 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, } +#[derive(Debug)] +pub struct PotentiallyCacheableShape { + pub id: Uuid, + bounds: Rect, + selrect: Rect, +} + +impl PartialEq for PotentiallyCacheableShape { + fn eq(&self, other: &PotentiallyCacheableShape) -> bool { + self.id == other.id + } +} + +#[derive(Debug, Default)] +pub struct ShapeCacheMap { + potentially_cacheable: HashMap, + pub map: HashMap, +} + +impl ShapeCacheMap { + pub fn new() -> Self { + Self { + potentially_cacheable: HashMap::new(), + map: HashMap::new(), + } + } + + pub fn add_potentially_cacheable(&mut self, shape: &Shape, bounds: Rect, selrect: Rect) { + self.potentially_cacheable.insert(shape.id, PotentiallyCacheableShape { + id: shape.id, + bounds, + selrect + }); + } +} + /// 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. @@ -475,7 +516,7 @@ impl RenderState { moved_ids: &[Uuid], moved_bounds: Option, ) -> bool { - if !self.backbuffer_crop_cache.contains_key(&node_id) { + if !self.shape_cache.map.contains_key(&node_id) { return false; } let Some(raw) = tree.get_raw(&node_id) else { @@ -499,7 +540,7 @@ impl RenderState { return false; } - if !self.backbuffer_crop_cache.contains_key(&node_id) { + if !self.shape_cache.map.contains_key(&node_id) { return false; } @@ -516,7 +557,8 @@ impl RenderState { // becomes valid again (stationary shape unchanged). if let Some(moved) = moved_bounds { let intersects = self - .backbuffer_crop_cache + .shape_cache + .map .get(&node_id) .is_some_and(|crop| moved.intersects(crop.src_doc_bounds)); @@ -581,7 +623,7 @@ impl RenderState { cache_cleared_this_render: false, current_tile_had_shapes: false, interactive_target_seeded: false, - backbuffer_crop_cache: HashMap::default(), + shape_cache: ShapeCacheMap::default(), }) } @@ -1787,15 +1829,16 @@ impl RenderState { self.surfaces.update_render_context(self.render_area, scale); } - fn rebuild_backbuffer_crop_cache(&mut self, tree: ShapesPoolRef) { - self.backbuffer_crop_cache.clear(); + fn rebuild_shape_cache(&mut self, tree: ShapesPoolRef) { + performance::begin_measure!("rebuild_backbuffer_crop_cache"); + self.shape_cache.map.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 = self.viewbox.area; + let viewport = self.viewbox.area(); let scale = self.get_scale(); let mut candidates: Vec<(Uuid, Rect, Rect)> = Vec::new(); // (id, doc_bounds, selrect) @@ -1863,8 +1906,8 @@ impl RenderState { }) .collect(); - let vb_left = self.viewbox.area.left; - let vb_top = self.viewbox.area.top; + let vb_left = self.viewbox.area().left; + let vb_top = self.viewbox.area().top; let (bb_w, bb_h) = self.surfaces.surface_size(SurfaceId::Backbuffer); let max_snap_px = get_gpu_state().max_texture_size(); @@ -1971,9 +2014,9 @@ impl RenderState { img }; - self.backbuffer_crop_cache.insert( + self.shape_cache.map.insert( id, - InteractiveDragCrop { + ShapeCache { src_doc_bounds: src_doc_window, src_selrect: selrect, capture_vb_left: vb_left, @@ -1984,6 +2027,8 @@ impl RenderState { }, ); } + println!("rebuild_backbuffer_crop_cache {:?}", self.shape_cache.map); + performance::end_measure!("rebuild_backbuffer_crop_cache"); } pub fn render_from_cache(&mut self, shapes: ShapesPoolRef) { @@ -2005,21 +2050,21 @@ impl RenderState { } // Check if we have a valid cached viewbox (non-zero dimensions indicate valid cache) - if self.cached_viewbox.area.width() > 0.0 { + if self.cached_viewbox.area().width() > 0.0 { // Scale and translate the target according to the cached data - let navigate_zoom = self.viewbox.zoom / self.cached_viewbox.zoom; + let navigate_zoom = self.viewbox.zoom() / self.cached_viewbox.zoom(); let interest = self.options.dpr_viewport_interest_area_threshold; let TileRect(start_tile_x, start_tile_y, _, _) = tiles::get_tiles_for_viewbox_with_interest(&self.cached_viewbox, interest); - let offset_x = self.viewbox.area.left * self.cached_viewbox.zoom * self.options.dpr; - let offset_y = self.viewbox.area.top * self.cached_viewbox.zoom * self.options.dpr; + let offset_x = self.viewbox.area().left * self.cached_viewbox.zoom() * self.options.dpr; + let offset_y = self.viewbox.area().top * self.cached_viewbox.zoom() * self.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 = self.viewbox.zoom < self.cached_viewbox.zoom; + let zooming_out = self.viewbox.zoom() < self.cached_viewbox.zoom(); if zooming_out { let cache_dim = self.surfaces.cache_dimensions(); let cache_w = cache_dim.width as f32; @@ -2079,14 +2124,11 @@ impl RenderState { // render after set_view_end handle it. if !self.zoom_changed() { let visible_rect = tiles::get_tiles_for_viewbox(&self.viewbox); - let offset = self.viewbox.get_offset(); - for tx in visible_rect.x1()..=visible_rect.x2() { - for ty in visible_rect.y1()..=visible_rect.y2() { - let tile = tiles::Tile::from(tx, ty); - if self.surfaces.has_cached_tile_surface(tile) { - let rect = tile.get_rect_with_offset(&offset); - self.surfaces.draw_cached_tile_into_backbuffer(tile, &rect); - } + let offset = self.viewbox.offset(); + for tile in visible_rect.iter(true) { + if self.surfaces.has_cached_tile_surface(tile) { + let rect = tile.get_rect_with_offset(&offset); + self.surfaces.draw_cached_tile_into_backbuffer(tile, &rect); } } } @@ -2317,7 +2359,7 @@ impl RenderState { // cache from the clean Backbuffer (no UI overlay yet) so that // interactive drag backgrounds don't include the grid overlay. if !self.options.is_fast_mode() && !self.options.is_interactive_transform() { - self.rebuild_backbuffer_crop_cache(tree); + self.rebuild_shape_cache(tree); } // present_frame: copy clean Backbuffer → Target, draw UI/debug // overlays on Target only, then flush. Backbuffer stays overlay-free. @@ -2717,6 +2759,7 @@ impl RenderState { } self.focus_mode.exit(&element.id); + Ok(()) } @@ -2724,17 +2767,16 @@ impl RenderState { let tile = self .current_tile .ok_or(Error::CriticalError("Current tile not found".to_string()))?; - let offset = self.viewbox.get_offset(); + let offset = self.viewbox.offset(); Ok(tile.get_rect_with_offset(&offset)) } pub fn get_rect_bounds(&mut self, rect: skia::Rect) -> Rect { let scale = self.get_scale(); - let offset_x = self.viewbox.area.left * scale; - let offset_y = self.viewbox.area.top * scale; + let offset = self.viewbox.offset(); Rect::from_xywh( - (rect.left * scale) - offset_x, - (rect.top * scale) - offset_y, + (rect.left * scale) - offset.x, + (rect.top * scale) - offset.y, rect.width() * scale, rect.height() * scale, ) @@ -2752,11 +2794,11 @@ impl RenderState { } pub fn get_aligned_tile_bounds(&mut self, tile: tiles::Tile) -> Rect { - let scale = self.get_scale(); + let offset = self.viewbox.offset(); let start_tile_x = - (self.viewbox.area.left * scale / tiles::TILE_SIZE).floor() * tiles::TILE_SIZE; + (offset.x / tiles::TILE_SIZE).floor() * tiles::TILE_SIZE; let start_tile_y = - (self.viewbox.area.top * scale / tiles::TILE_SIZE).floor() * tiles::TILE_SIZE; + (offset.y / tiles::TILE_SIZE).floor() * tiles::TILE_SIZE; Rect::from_xywh( (tile.x() as f32 * tiles::TILE_SIZE) - start_tile_x, (tile.y() as f32 * tiles::TILE_SIZE) - start_tile_y, @@ -3266,7 +3308,7 @@ impl RenderState { let has_effects = transformed_element.has_effects_that_extend_bounds(); - let is_visible = export + let is_renderable = export || mask || if is_container || has_effects { let element_extrect = @@ -3278,13 +3320,12 @@ impl RenderState { selrect.intersects(self.render_area_with_margins) && !transformed_element.visually_insignificant(scale, tree) }; - if self.options.is_debug_visible() { let shape_extrect_bounds = self.get_shape_extrect_bounds(element, tree); debug::render_debug_shape(self, None, Some(shape_extrect_bounds)); } - if !is_visible { + if !is_renderable { continue; } } @@ -3301,7 +3342,7 @@ impl RenderState { ); if use_cached { - if let Some(crop) = self.backbuffer_crop_cache.get(&node_id) { + if let Some(crop) = self.shape_cache.map.get(&node_id) { let crop_image = &crop.image; let crop_src_selrect = crop.src_selrect; @@ -4030,7 +4071,7 @@ impl RenderState { } pub fn zoom_changed(&self) -> bool { - (self.viewbox.zoom - self.cached_viewbox.zoom).abs() > f32::EPSILON + (self.viewbox.zoom() - self.cached_viewbox.zoom()).abs() > f32::EPSILON } pub fn mark_touched(&mut self, uuid: Uuid) { @@ -4056,7 +4097,7 @@ impl RenderState { pub fn prepare_context_loss_cleanup(&mut self) { // Drop cached GPU-backed snapshots before dropping the render state. - self.backbuffer_crop_cache.clear(); + self.shape_cache.map.clear(); self.surfaces.invalidate_tile_cache(); // Mark context as abandoned so resource destructors avoid issuing // GL commands when the browser has already lost/restored the context. diff --git a/render-wasm/src/render/debug.rs b/render-wasm/src/render/debug.rs index 0574e3ab28..fb9a8b03c3 100644 --- a/render-wasm/src/render/debug.rs +++ b/render-wasm/src/render/debug.rs @@ -29,7 +29,7 @@ fn render_debug_view(render_state: &mut RenderState) { paint.set_color(skia::Color::GREEN); paint.set_stroke_width(1.); - let rect = get_debug_rect(render_state.viewbox.area); + let rect = get_debug_rect(render_state.viewbox.area()); render_state .surfaces .canvas(SurfaceId::Debug) @@ -99,7 +99,7 @@ pub fn render_debug_viewbox_tiles(render_state: &mut RenderState) { paint.set_stroke_width(1.); let tile_size = tiles::get_tile_size(scale); - let tile_rect = tiles::get_tiles_for_rect(render_state.viewbox.area, tile_size); + let tile_rect = tiles::get_tiles_for_rect(render_state.viewbox.area(), tile_size); let tiles::TileRect(sx, sy, ex, ey) = tile_rect; let str_rect = format!("{} {} {} {}", sx, sy, ex, ey); diff --git a/render-wasm/src/render/surfaces.rs b/render-wasm/src/render/surfaces.rs index 867b657c7b..bb063602fd 100644 --- a/render-wasm/src/render/surfaces.rs +++ b/render-wasm/src/render/surfaces.rs @@ -563,8 +563,8 @@ impl Surfaces { let s = viewbox.get_scale(); let scale = self.atlas.scale.max(0.01); canvas.translate(( - (self.atlas.origin.x + viewbox.pan.x) * s, - (self.atlas.origin.y + viewbox.pan.y) * s, + (self.atlas.origin.x + viewbox.pan().x) * s, + (self.atlas.origin.y + viewbox.pan().y) * s, )); canvas.scale((s / scale, s / scale)); @@ -1589,7 +1589,7 @@ impl TileTextureCache { texture.set_empty(); } - let offset = viewbox.get_offset(); + let offset = viewbox.offset(); let mut index = 0; for y in tile_viewbox.visible_rect.top()..=tile_viewbox.visible_rect.bottom() { for x in tile_viewbox.visible_rect.left()..=tile_viewbox.visible_rect.right() { diff --git a/render-wasm/src/render/text_editor.rs b/render-wasm/src/render/text_editor.rs index 8c990942ec..479c98be9c 100644 --- a/render-wasm/src/render/text_editor.rs +++ b/render-wasm/src/render/text_editor.rs @@ -21,16 +21,15 @@ pub fn render_overlay( }; canvas.save(); - let zoom = viewbox.zoom * options.dpr; - canvas.scale((zoom, zoom)); - canvas.translate((-viewbox.area.left, -viewbox.area.top)); + canvas.scale(viewbox.scale()); + canvas.translate(viewbox.pan()); if editor_state.selection.is_selection() { render_selection(canvas, editor_state, text_content, shape); } if editor_state.cursor_visible { - render_cursor(canvas, zoom, editor_state, text_content, shape); + render_cursor(canvas, viewbox.get_scale(), editor_state, text_content, shape); } canvas.restore(); diff --git a/render-wasm/src/render/ui.rs b/render-wasm/src/render/ui.rs index 2d48cdda6d..d6f7e8de3e 100644 --- a/render-wasm/src/render/ui.rs +++ b/render-wasm/src/render/ui.rs @@ -7,18 +7,17 @@ use crate::shapes::{Layout, Type}; pub fn render(render_state: &mut RenderState, shapes: ShapesPoolRef) { let canvas = render_state.surfaces.canvas(SurfaceId::UI); let viewbox = render_state.viewbox; - let zoom = viewbox.zoom * render_state.options.dpr; let show_grid_id = render_state.show_grid; canvas.clear(Color4f::new(0.0, 0.0, 0.0, 0.0)); canvas.save(); - canvas.scale((zoom, zoom)); - canvas.translate((-viewbox.area.left, -viewbox.area.top)); + canvas.scale(viewbox.scale()); + canvas.translate(viewbox.pan()); if let Some(id) = show_grid_id { if let Some(shape) = shapes.get(&id) { grid_layout::render_overlay( - zoom, + viewbox.get_scale(), render_state.options.antialias_threshold, canvas, shape, @@ -51,7 +50,7 @@ pub fn render(render_state: &mut RenderState, shapes: ShapesPoolRef) { if let Some(shape) = shapes.get(&shape.id) { grid_layout::render_overlay( - zoom, + viewbox.get_scale(), render_state.options.antialias_threshold, canvas, shape, diff --git a/render-wasm/src/shapes.rs b/render-wasm/src/shapes.rs index a1e79a3637..a0d4ab86ac 100644 --- a/render-wasm/src/shapes.rs +++ b/render-wasm/src/shapes.rs @@ -335,6 +335,23 @@ impl Shape { self.shape_type = shape_type; } + pub fn is_potentially_cacheable_as_image(&self) -> bool { + // It is a container. + let is_container = self.is_recursive(); + + // It is a costly to draw shape. + let is_costly = matches!(self.shape_type, Type::Path(_) | Type::Bool(_) | Type::Text(_)) + || self.has_layout(); + + // It is prolific. + let is_prolific = self.children.len() > 2; + + // We know how big the shape is. + let is_extrect_cached = !self.extrect_cache.borrow().is_none(); + + (is_container || is_costly || is_prolific) && is_extrect_cached + } + #[allow(dead_code)] pub fn is_frame(&self) -> bool { matches!(self.shape_type, Type::Frame(_)) @@ -345,7 +362,7 @@ impl Shape { } pub fn is_group_like(&self) -> bool { - matches!(self.shape_type, Type::Group(_)) || matches!(self.shape_type, Type::Bool(_)) + matches!(self.shape_type, Type::Group(_) | Type::Bool(_)) } pub fn has_layout(&self) -> bool { diff --git a/render-wasm/src/shapes/modifiers.rs b/render-wasm/src/shapes/modifiers.rs index 109fc605b2..7649e9cfc6 100644 --- a/render-wasm/src/shapes/modifiers.rs +++ b/render-wasm/src/shapes/modifiers.rs @@ -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::{ShapesPoolRef, DesignState}; 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, bounds: &mut HashMap, modifiers: &mut HashMap, @@ -324,7 +324,7 @@ fn propagate_transform( #[allow(clippy::too_many_arguments)] fn propagate_reflow( id: &Uuid, - state: &State, + state: &DesignState, entries: &mut VecDeque, bounds: &mut HashMap, layout_reflows: &mut HashSet, @@ -380,7 +380,7 @@ fn propagate_reflow( fn reflow_shape( id: &Uuid, - state: &State, + state: &DesignState, reflown: &mut HashSet, entries: &mut VecDeque, bounds: &mut HashMap, @@ -409,7 +409,7 @@ fn reflow_shape( } pub fn propagate_modifiers( - state: &State, + state: &DesignState, modifiers: &[TransformEntry], pixel_precision: bool, ) -> Result> { diff --git a/render-wasm/src/state.rs b/render-wasm/src/state.rs index 3b6835fd30..489cfdaa71 100644 --- a/render-wasm/src/state.rs +++ b/render-wasm/src/state.rs @@ -1,5 +1,5 @@ use skia_safe::{self as skia, textlayout::FontCollection, Path, Point}; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; mod rulers; mod shapes_pool; @@ -19,7 +19,8 @@ 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 selected: HashSet, pub current_id: Option, pub current_browser: u8, pub shapes: ShapesPool, @@ -28,9 +29,10 @@ pub(crate) struct State { pub loading: bool, } -impl State { +impl DesignState { pub fn new() -> Self { Self { + selected: HashSet::new(), current_id: None, current_browser: 0, shapes: ShapesPool::new(), @@ -137,6 +139,10 @@ impl State { self.current_id = Some(id); } + pub fn is_potentially_cacheable_as_image(&self, id: Uuid) -> bool { + return self.selected.contains(&id) + } + pub fn has_shape(&mut self, id: Uuid) -> bool { self.shapes.has(&id) } diff --git a/render-wasm/src/tiles.rs b/render-wasm/src/tiles.rs index 0d32b1ae1c..585f2ed84a 100644 --- a/render-wasm/src/tiles.rs +++ b/render-wasm/src/tiles.rs @@ -228,7 +228,7 @@ pub fn get_tiles_for_rect(rect: skia::Rect, tile_size: f32) -> TileRect { pub fn get_tiles_for_viewbox(viewbox: &Viewbox) -> TileRect { let tile_size = get_tile_size(viewbox.get_scale()); - get_tiles_for_rect(viewbox.area, tile_size) + get_tiles_for_rect(viewbox.area(), tile_size) } pub fn get_tiles_for_viewbox_with_interest(viewbox: &Viewbox, interest: i32) -> TileRect { diff --git a/render-wasm/src/view.rs b/render-wasm/src/view.rs index efa5394012..fed52d90cd 100644 --- a/render-wasm/src/view.rs +++ b/render-wasm/src/view.rs @@ -3,11 +3,11 @@ use std::ops::Mul; #[derive(Debug, Copy, Clone)] pub(crate) struct Viewbox { - pub pan: Point, - pub size: Size, - pub zoom: f32, - pub dpr: f32, - pub area: Rect, + pan: Point, + size: Size, + zoom: f32, + dpr: f32, + area: Rect, } impl Default for Viewbox { @@ -67,18 +67,28 @@ impl Viewbox { .set_wh(self.size.width / self.zoom, self.size.height / self.zoom); } - pub fn set_dpr(&mut self, dpr: f32) { + pub fn set_dpr(&mut self, dpr: f32) -> f32 { self.dpr = dpr; + self.dpr + } + + pub fn set_zoom(&mut self, new_zoom: f32) -> f32 { + self.zoom = new_zoom; + self.zoom } pub fn get_scale(&self) -> f32 { self.zoom * self.dpr } - pub fn get_offset(&self) -> Point { + pub fn offset(&self) -> Point { self.area.tl().mul(self.get_scale()) } + pub fn area(&self) -> Rect { + self.area + } + pub fn pan(&self) -> Point { self.pan } @@ -87,6 +97,10 @@ impl Viewbox { self.zoom } + pub fn scale(&self) -> (f32, f32) { + (self.get_scale(), self.get_scale()) + } + pub fn get_matrix(&self) -> Matrix { let mut matrix = Matrix::new_identity(); matrix.post_translate(self.pan()); diff --git a/render-wasm/src/wasm/fills/image.rs b/render-wasm/src/wasm/fills/image.rs index 65a4697181..6b526fcfba 100644 --- a/render-wasm/src/wasm/fills/image.rs +++ b/render-wasm/src/wasm/fills/image.rs @@ -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 = state .shapes .iter()