This commit is contained in:
Aitor Moreno 2026-06-02 17:32:20 +02:00
parent 7fd4e35203
commit b282bd753a
16 changed files with 194 additions and 81 deletions

View File

@ -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
[]

View File

@ -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.

View File

@ -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 = []

View File

@ -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));
}
}

View File

@ -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(

View File

@ -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<Uuid, InteractiveDragCrop>,
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<Uuid, PotentiallyCacheableShape>,
pub map: HashMap<Uuid, ShapeCache>,
}
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<Rect>,
) -> 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.

View File

@ -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);

View File

@ -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() {

View File

@ -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();

View File

@ -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,

View File

@ -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 {

View File

@ -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<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>> {

View File

@ -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<Uuid>,
pub current_id: Option<Uuid>,
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)
}

View File

@ -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 {

View File

@ -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());

View File

@ -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()