diff --git a/docker/devenv/Dockerfile b/docker/devenv/Dockerfile index 45e4e0851..79169665c 100644 --- a/docker/devenv/Dockerfile +++ b/docker/devenv/Dockerfile @@ -99,6 +99,7 @@ RUN set -ex; \ libnss3 \ libgbm1 \ xvfb \ + libfontconfig-dev \ ; \ rm -rf /var/lib/apt/lists/*; diff --git a/frontend/src/app/main/ui/workspace/viewport_wasm.cljs b/frontend/src/app/main/ui/workspace/viewport_wasm.cljs index ade8e78ff..4150fc7da 100644 --- a/frontend/src/app/main/ui/workspace/viewport_wasm.cljs +++ b/frontend/src/app/main/ui/workspace/viewport_wasm.cljs @@ -285,6 +285,10 @@ (fn [] (wasm.api/clear-canvas)))) + (mf/with-effect [vport] + (when @canvas-init? + (wasm.api/resize-canvas (:width vport) (:height vport)))) + (mf/with-effect [base-objects canvas-init?] (when @canvas-init? (wasm.api/set-objects base-objects))) diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index fa17a80f6..05e0088f9 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -120,13 +120,13 @@ ;; https://rust-skia.github.io/doc/skia_safe/enum.BlendMode.html (h/call internal-module "_set_shape_blend_mode" (translate-blend-mode blend-mode))) -(def debounce_render (fns/debounce render 100)) +(def debounce-render (fns/debounce render 100)) (defn set-view [zoom vbox] - (h/call internal-module "_set_view" zoom (- (:x vbox)) (- (:y vbox)) (:width vbox) (:height vbox)) + (h/call internal-module "_set_view" zoom (- (:x vbox)) (- (:y vbox))) (h/call internal-module "_navigate") - (debounce_render)) + (debounce-render)) (defn set-objects [objects] @@ -163,6 +163,10 @@ ;; TODO: perform corresponding cleaning ) +(defn resize-canvas + [width height] + (h/call internal-module "_resize_canvas" width height)) + (defn assign-canvas [canvas] (let [gl (unchecked-get internal-module "GL") @@ -175,7 +179,8 @@ (.makeContextCurrent ^js gl handle) ;; Initialize Skia (^function init-fn (.-width ^js canvas) - (.-height ^js canvas)) + (.-height ^js canvas) + 1) (set! (.-width canvas) (.-clientWidth ^js canvas)) (set! (.-height canvas) (.-clientHeight ^js canvas)))) diff --git a/render-wasm/src/debug.rs b/render-wasm/src/debug.rs new file mode 100644 index 000000000..d5f0a17b2 --- /dev/null +++ b/render-wasm/src/debug.rs @@ -0,0 +1,2 @@ +pub const DEBUG_VISIBLE: u32 = 0x01; + diff --git a/render-wasm/src/main.rs b/render-wasm/src/main.rs index 63b5002b6..58bdb0286 100644 --- a/render-wasm/src/main.rs +++ b/render-wasm/src/main.rs @@ -1,9 +1,11 @@ -pub mod images; pub mod render; pub mod shapes; pub mod state; pub mod utils; pub mod view; +pub mod math; +pub mod images; +pub mod debug; use skia_safe as skia; @@ -29,8 +31,8 @@ fn init_gl() { /// This is called from JS after the WebGL context has been created. #[no_mangle] -pub extern "C" fn init(width: i32, height: i32) { - let state_box = Box::new(State::with_capacity(width, height, 2048)); +pub extern "C" fn init(width: i32, height: i32, debug: u32) { + let state_box = Box::new(State::with_capacity(width, height, debug, 2048)); unsafe { STATE = Some(state_box); } @@ -39,7 +41,7 @@ pub extern "C" fn init(width: i32, height: i32) { #[no_mangle] pub unsafe extern "C" fn render() { let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); - state.draw_all_shapes(); + state.render_all(); } #[no_mangle] @@ -55,9 +57,27 @@ pub extern "C" fn reset_canvas() { } #[no_mangle] -pub extern "C" fn set_view(zoom: f32, x: f32, y: f32, width: f32, height: f32) { +pub extern "C" fn resize_canvas(width: i32, height: i32) { let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); - state.set_view(zoom, (x, y), (width, height)); + state.resize(width, height); +} + +#[no_mangle] +pub extern "C" fn set_view(zoom: f32, x: f32, y: f32) { + let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); + state.viewbox.set_all(zoom, x, y); +} + +#[no_mangle] +pub extern "C" fn set_view_zoom(zoom: f32) { + let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); + state.viewbox.set_zoom(zoom); +} + +#[no_mangle] +pub extern "C" fn set_view_xy(x: f32, y: f32) { + let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); + state.viewbox.set_xy(x, y); } #[no_mangle] @@ -68,14 +88,11 @@ pub extern "C" fn use_shape(a: u32, b: u32, c: u32, d: u32) { } #[no_mangle] -pub unsafe extern "C" fn set_shape_selrect(x1: f32, y1: f32, x2: f32, y2: f32) { +pub unsafe extern "C" fn set_shape_selrect(left: f32, top: f32, right: f32, bottom: f32) { let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); if let Some(shape) = state.current_shape() { - shape.selrect.x1 = x1; - shape.selrect.y1 = y1; - shape.selrect.x2 = x2; - shape.selrect.y2 = y2; + shape.selrect.set_ltrb(left, top, right, bottom); } } diff --git a/render-wasm/src/math.rs b/render-wasm/src/math.rs new file mode 100644 index 000000000..1086eb66f --- /dev/null +++ b/render-wasm/src/math.rs @@ -0,0 +1,4 @@ +use skia_safe as skia; + +pub type Rect = skia::Rect; +pub type Matrix = skia::Matrix; diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index ea08e9cf8..f0d34cfee 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -1,11 +1,12 @@ -use skia_safe::gpu::{self, gl::FramebufferInfo, DirectContext}; -use skia_safe::{self as skia}; +use skia_safe as skia; +use skia::gpu::{self, gl::FramebufferInfo, DirectContext}; use std::collections::HashMap; use uuid::Uuid; use crate::shapes::Shape; -use crate::view::View; +use crate::view::Viewbox; use crate::images::Image; +use crate::debug; struct GpuState { pub context: DirectContext, @@ -14,16 +15,16 @@ struct GpuState { impl GpuState { fn new() -> Self { - let interface = skia_safe::gpu::gl::Interface::new_native().unwrap(); - let context = skia_safe::gpu::direct_contexts::make_gl(interface, None).unwrap(); + let interface = skia::gpu::gl::Interface::new_native().unwrap(); + let context = skia::gpu::direct_contexts::make_gl(interface, None).unwrap(); let framebuffer_info = { let mut fboid: gl::types::GLint = 0; unsafe { gl::GetIntegerv(gl::FRAMEBUFFER_BINDING, &mut fboid) }; FramebufferInfo { fboid: fboid.try_into().unwrap(), - format: skia_safe::gpu::gl::Format::RGBA8.into(), - protected: skia_safe::gpu::Protected::No, + format: skia::gpu::gl::Format::RGBA8.into(), + protected: skia::gpu::Protected::No, } }; @@ -41,8 +42,8 @@ impl GpuState { gpu::surfaces::wrap_backend_render_target( &mut self.context, &backend_render_target, - skia_safe::gpu::SurfaceOrigin::BottomLeft, - skia_safe::ColorType::RGBA8888, + skia::gpu::SurfaceOrigin::BottomLeft, + skia::ColorType::RGBA8888, None, None, ) @@ -52,13 +53,14 @@ impl GpuState { pub(crate) struct CachedSurfaceImage { pub image: Image, - pub view: View, + pub viewbox: Viewbox, } pub(crate) struct RenderState { gpu_state: GpuState, pub final_surface: skia::Surface, pub drawing_surface: skia::Surface, + pub debug_surface: skia::Surface, pub cached_surface_image: Option, } @@ -70,11 +72,15 @@ impl RenderState { let drawing_surface = final_surface .new_surface_with_dimensions((width, height)) .unwrap(); + let debug_surface = final_surface + .new_surface_with_dimensions((width, height)) + .unwrap(); RenderState { gpu_state, final_surface, drawing_surface, + debug_surface, cached_surface_image: None, } } @@ -86,6 +92,10 @@ impl RenderState { .final_surface .new_surface_with_dimensions((width, height)) .unwrap(); + self.debug_surface = self + .final_surface + .new_surface_with_dimensions((width, height)) + .unwrap(); } pub fn flush(&mut self) { @@ -105,22 +115,19 @@ impl RenderState { pub fn reset_canvas(&mut self) { self.drawing_surface .canvas() - .clear(skia_safe::Color::TRANSPARENT) + .clear(skia::Color::TRANSPARENT) .reset_matrix(); self.final_surface .canvas() - .clear(skia_safe::Color::TRANSPARENT) + .clear(skia::Color::TRANSPARENT) + .reset_matrix(); + self.debug_surface + .canvas() + .clear(skia::Color::TRANSPARENT) .reset_matrix(); } pub fn render_single_shape(&mut self, shape: &Shape) { - let r = skia::Rect::new( - shape.selrect.x1, - shape.selrect.y1, - shape.selrect.x2, - shape.selrect.y2, - ); - // Check transform-matrix code from common/src/app/common/geom/shapes/transforms.cljc let mut matrix = skia::Matrix::new_identity(); let (translate_x, translate_y) = shape.translation(); @@ -139,7 +146,7 @@ impl RenderState { 1., ); - let mut center = r.center(); + let mut center = shape.selrect.center(); matrix.post_translate(center); center.negate(); matrix.pre_translate(center); @@ -147,7 +154,7 @@ impl RenderState { self.drawing_surface.canvas().concat(&matrix); for fill in shape.fills().rev() { - self.drawing_surface.canvas().draw_rect(r, &fill.to_paint()); + self.drawing_surface.canvas().draw_rect(shape.selrect, &fill.to_paint()); } let mut paint = skia::Paint::default(); @@ -165,29 +172,30 @@ impl RenderState { pub fn navigate( &mut self, - view: &View, + viewbox: &Viewbox, shapes: &HashMap, + debug: u32, ) { self.reset_canvas(); - if let Some(cached_surface_image) = &self.cached_surface_image { // If we are drawing something bigger than the visible let's do a redraw - if (view.x > cached_surface_image.view.x) || - (-view.x + view.width > -cached_surface_image.view.x + cached_surface_image.view.width) || - (view.y > cached_surface_image.view.y) || - (-view.y + view.height > -cached_surface_image.view.y + cached_surface_image.view.height) { - self.draw_all_shapes(view, shapes); + if (viewbox.x > cached_surface_image.viewbox.x) || + (-viewbox.x + viewbox.area.width() > -cached_surface_image.viewbox.x + cached_surface_image.viewbox.area.width()) || + (viewbox.y > cached_surface_image.viewbox.y) || + (-viewbox.y + viewbox.area.height() > -cached_surface_image.viewbox.y + cached_surface_image.viewbox.area.height()) + { + self.render_all(viewbox, shapes, debug); } - else { - + else + { let image = &cached_surface_image.image; let paint = skia::Paint::default(); self.final_surface.canvas().save(); self.drawing_surface.canvas().save(); - let navigate_zoom = view.zoom / cached_surface_image.view.zoom; - let navigate_x = cached_surface_image.view.zoom * (view.x - cached_surface_image.view.x); - let navigate_y = cached_surface_image.view.zoom * (view.y - cached_surface_image.view.y); + let navigate_zoom = viewbox.zoom / cached_surface_image.viewbox.zoom; + let navigate_x = cached_surface_image.viewbox.zoom * (viewbox.x - cached_surface_image.viewbox.x); + let navigate_y = cached_surface_image.viewbox.zoom * (viewbox.y - cached_surface_image.viewbox.y); self.final_surface.canvas().scale((navigate_zoom, navigate_zoom)); self.final_surface.canvas().translate((navigate_x, navigate_y)); @@ -201,39 +209,99 @@ impl RenderState { self.flush(); } - pub fn draw_all_shapes( + pub fn render_all( &mut self, - view: &View, + viewbox: &Viewbox, shapes: &HashMap, + debug: u32, // Debug flags ) { self.reset_canvas(); - self.scale(view.zoom, view.zoom); - self.translate(view.x, view.y); - self.render_shape_tree(Uuid::nil(), shapes); - + self.scale(viewbox.zoom, viewbox.zoom); + self.translate(viewbox.x, viewbox.y); + self.render_shape_tree(&Uuid::nil(), viewbox, shapes); self.cached_surface_image = Some(CachedSurfaceImage { image: self.final_surface.image_snapshot(), - view: view.clone(), + viewbox: viewbox.clone(), }); + if debug & debug::DEBUG_VISIBLE == debug::DEBUG_VISIBLE { + self.render_debug(viewbox); + } self.flush(); } - fn render_shape_tree(&mut self, id: Uuid, shapes: &HashMap) { + fn render_debug_view(&mut self, viewbox: &Viewbox) { + let mut paint = skia::Paint::default(); + paint.set_style(skia::PaintStyle::Stroke); + paint.set_color(skia::Color::from_argb(255, 255, 0, 255)); + paint.set_stroke_width(1.); + let mut scaled_rect = viewbox.area.clone(); + let x = 100. + scaled_rect.x() * 0.2; + let y = 100. + scaled_rect.y() * 0.2; + let width = scaled_rect.width() * 0.2; + let height = scaled_rect.height() * 0.2; + scaled_rect.set_xywh(x, y, width, height); + self.debug_surface.canvas().draw_rect(scaled_rect, &paint); + } + + fn render_debug_shape(&mut self, shape: &Shape, intersected: bool) { + let mut paint = skia::Paint::default(); + paint.set_style(skia::PaintStyle::Stroke); + paint.set_color( + if intersected { + skia::Color::from_argb(255, 255, 255, 0) + } else { + skia::Color::from_argb(255, 0, 255, 255) + } + ); + paint.set_stroke_width(1.); + let mut scaled_rect = shape.selrect.clone(); + let x = 100. + scaled_rect.x() * 0.2; + let y = 100. + scaled_rect.y() * 0.2; + let width = scaled_rect.width() * 0.2; + let height = scaled_rect.height() * 0.2; + scaled_rect.set_xywh(x, y, width, height); + self.debug_surface.canvas().draw_rect(scaled_rect, &paint); + } + + fn render_debug(&mut self, viewbox: &Viewbox) { + let paint = skia::Paint::default(); + self.render_debug_view(viewbox); + self.debug_surface.draw( + &mut self.final_surface.canvas(), + (0.0, 0.0), + skia::SamplingOptions::new(skia::FilterMode::Linear, skia::MipmapMode::Nearest), + Some(&paint), + ); + + } + + fn render_shape_tree(&mut self, id: &Uuid, viewbox: &Viewbox, shapes: &HashMap) { let shape = shapes.get(&id).unwrap(); + if !id.is_nil() { + if !shape.selrect.intersects(viewbox.area) { + self.render_debug_shape(shape, false); + // TODO: This means that not all the shapes are renderer so we + // need to call a render_all on the zoom out. + return; + } else { + self.render_debug_shape(shape, true); + } + } + // This is needed so the next non-children shape does not carry this shape's transform self.final_surface.canvas().save(); self.drawing_surface.canvas().save(); - if id != Uuid::nil() { + if !id.is_nil() { self.render_single_shape(shape); } // draw all the children shapes - let shape_ids = shape.children.clone(); + let shape_ids = shape.children.iter(); for shape_id in shape_ids { - self.render_shape_tree(shape_id, shapes); + self.render_shape_tree(shape_id, viewbox, shapes); } self.final_surface.canvas().restore(); diff --git a/render-wasm/src/shapes.rs b/render-wasm/src/shapes.rs index d37ed8c19..019f6f7e9 100644 --- a/render-wasm/src/shapes.rs +++ b/render-wasm/src/shapes.rs @@ -1,4 +1,5 @@ use skia_safe as skia; +use crate::math; use uuid::Uuid; #[derive(Debug, Clone, Copy)] @@ -15,19 +16,6 @@ pub enum Kind { Frame, } -pub struct Point { - pub x: f32, - pub y: f32, -} - -#[derive(Debug, Clone, Copy, Default)] -pub struct Rect { - pub x1: f32, - pub y1: f32, - pub x2: f32, - pub y2: f32, -} - type Color = skia::Color; #[derive(Debug, Clone, Copy)] @@ -111,7 +99,7 @@ pub struct Shape { pub id: Uuid, pub children: Vec, pub kind: Kind, - pub selrect: Rect, + pub selrect: math::Rect, pub transform: Matrix, pub rotation: f32, fills: Vec, @@ -124,7 +112,7 @@ impl Shape { id, children: Vec::::new(), kind: Kind::Rect, - selrect: Rect::default(), + selrect: math::Rect::new_empty(), transform: Matrix::identity(), rotation: 0., fills: vec![], diff --git a/render-wasm/src/state.rs b/render-wasm/src/state.rs index 4a853cbbc..2344fab5c 100644 --- a/render-wasm/src/state.rs +++ b/render-wasm/src/state.rs @@ -3,7 +3,8 @@ use uuid::Uuid; use crate::render::RenderState; use crate::shapes::Shape; -use crate::view::View; +use crate::view::Viewbox; +use crate::math; /// This struct holds the state of the Rust application between JS calls. /// @@ -11,40 +12,50 @@ use crate::view::View; /// 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<'a> { + pub debug: u32, pub render_state: RenderState, pub current_id: Option, pub current_shape: Option<&'a mut Shape>, pub shapes: HashMap, - pub view: View, + pub viewbox: Viewbox, } impl<'a> State<'a> { - pub fn with_capacity(width: i32, height: i32, capacity: usize) -> Self { + pub fn with_capacity(width: i32, height: i32, debug: u32, capacity: usize) -> Self { State { + debug, render_state: RenderState::new(width, height), current_id: None, current_shape: None, shapes: HashMap::with_capacity(capacity), - view: View { + viewbox: Viewbox { x: 0., y: 0., zoom: 1., - width: 0., - height: 0., - }, + width: width as f32, + height: height as f32, + area: math::Rect::new_empty(), + } } } + pub fn resize(&mut self, width: i32, height: i32) { + self.render_state.resize(width, height); + self.viewbox.set_wh(width as f32, height as f32); + } + pub fn render_state(&'a mut self) -> &'a mut RenderState { &mut self.render_state } pub fn navigate(&mut self) { - self.render_state.navigate(&self.view, &self.shapes); + self.render_state + .navigate(&self.viewbox, &self.shapes, self.debug); } - pub fn draw_all_shapes(&mut self) { - self.render_state.draw_all_shapes(&self.view, &self.shapes); + pub fn render_all(&mut self) { + self.render_state + .render_all(&self.viewbox, &self.shapes, self.debug); } pub fn use_shape(&'a mut self, id: Uuid) { @@ -60,20 +71,4 @@ impl<'a> State<'a> { pub fn current_shape(&'a mut self) -> Option<&'a mut Shape> { self.current_shape.as_deref_mut() } - - pub fn set_view(&mut self, zoom: f32, pan: (f32, f32), size: (f32, f32)) { - let (x, y) = pan; - self.view.x = x; - self.view.y = y; - - self.view.zoom = zoom; - - let (w, h) = size; - if self.view.width != w || self.view.height != h { - self.view.width = w; - self.view.height = h; - - self.render_state.resize(w as i32, h as i32); - } - } } diff --git a/render-wasm/src/view.rs b/render-wasm/src/view.rs index f2892f34e..5b671c7f8 100644 --- a/render-wasm/src/view.rs +++ b/render-wasm/src/view.rs @@ -1,9 +1,55 @@ -#[derive(Debug, Clone, Copy)] -pub(crate) struct View +use skia_safe as skia; + +#[derive(Debug, Copy, Clone)] +pub(crate) struct Viewbox { pub x: f32, pub y: f32, - pub zoom: f32, pub width: f32, pub height: f32, + pub zoom: f32, + pub area: skia::Rect, } + +impl Viewbox { + pub fn set_all(&mut self, zoom: f32, x: f32, y: f32) -> &Self { + self.x = x; + self.y = y; + self.zoom = zoom; + self.area.set_xywh( + -self.x, + -self.y, + self.width / self.zoom, + self.height / self.zoom + ); + self + } + + pub fn set_zoom(&mut self, zoom: f32) -> &Self { + self.zoom = zoom; + self.area.set_wh( + self.width / self.zoom, + self.height / self.zoom + ); + self + } + + pub fn set_xy(&mut self, x: f32, y: f32) -> &Self { + self.x = x; + self.y = y; + self.area.left = -x; + self.area.top = -y; + self + } + + pub fn set_wh(&mut self, width: f32, height: f32) -> &Self { + self.width = width; + self.height = height; + self.area.set_wh( + self.width / self.zoom, + self.height / self.zoom + ); + self + } +} + diff --git a/render-wasm/test b/render-wasm/test new file mode 100755 index 000000000..602c03f73 --- /dev/null +++ b/render-wasm/test @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +cargo test --bin render_wasm -- --show-output