diff --git a/frontend/src/app/main/ui/workspace/sidebar/debug.cljs b/frontend/src/app/main/ui/workspace/sidebar/debug.cljs index f5c73aab21..4c6a715b23 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/debug.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/debug.cljs @@ -43,7 +43,7 @@ [:div {:class (stl/css :debug-panel-inner)} (for [option (sort-by d/name dbg/options)] - [:div {:class (stl/css :checkbox-wrapper)} + [:div {:key (d/name option) :class (stl/css :checkbox-wrapper)} [:span {:class (stl/css-case :checkbox-icon true :global/checked (dbg/enabled? option)) :on-click #(on-toggle-enabled % option)} (when (dbg/enabled? option) i/status-tick)] diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index d881c212a3..acac754377 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -9,9 +9,11 @@ (:require [app.common.data.macros :as dm] [app.common.math :as mth] + [app.common.svg.path :as path] [app.common.uuid :as uuid] [app.config :as cf] [app.render-wasm.helpers :as h] + [app.util.debug :as dbg] [app.util.functions :as fns] [app.util.http :as http] [app.util.webapi :as wapi] @@ -185,6 +187,16 @@ (store-image id)))))) fills)) +(defn set-shape-path-content + [content] + (let [buffer (path/content->buffer content) + size (.-byteLength buffer) + ptr (h/call internal-module "_alloc_bytes" size) + heap (gobj/get ^js internal-module "HEAPU8") + mem (js/Uint8Array. (.-buffer heap) ptr size)] + (.set mem (js/Uint8Array. buffer)) + (h/call internal-module "_set_shape_path_content"))) + (defn- translate-blend-mode [blend-mode] (case blend-mode @@ -236,15 +248,18 @@ (loop [index 0 pending []] (if (< index total-shapes) (let [shape (nth shapes index) + type (dm/get-prop shape :type) id (dm/get-prop shape :id) selrect (dm/get-prop shape :selrect) rotation (dm/get-prop shape :rotation) transform (dm/get-prop shape :transform) - fills (dm/get-prop shape :fills) + fills (if (= type :group) + [] (dm/get-prop shape :fills)) children (dm/get-prop shape :shapes) blend-mode (dm/get-prop shape :blend-mode) opacity (dm/get-prop shape :opacity) - hidden (dm/get-prop shape :hidden)] + hidden (dm/get-prop shape :hidden) + content (dm/get-prop shape :content)] (use-shape id) (set-shape-selrect selrect) @@ -254,6 +269,7 @@ (set-shape-children children) (set-shape-opacity opacity) (set-shape-hidden hidden) + (when (and (some? content) (= type :path)) (set-shape-path-content content)) (let [pending-fills (doall (set-shape-fills fills))] (recur (inc index) (into pending pending-fills)))) pending))] @@ -279,9 +295,16 @@ [width height] (h/call internal-module "_resize_viewbox" width height)) +(defn- debug-flags + [] + (cond-> 0 + (dbg/enabled? :wasm-viewbox) + (bit-or 2r00000000000000000000000000000001))) + (defn assign-canvas [canvas] (let [gl (unchecked-get internal-module "GL") + flags (debug-flags) context (.getContext ^js canvas "webgl2" canvas-options) ;; Register the context with emscripten @@ -290,7 +313,7 @@ ;; Initialize Wasm Render Engine (h/call internal-module "_init" (/ (.-width ^js canvas) dpr) (/ (.-height ^js canvas) dpr)) - (h/call internal-module "_set_render_options" 0x01 dpr)) + (h/call internal-module "_set_render_options" flags dpr)) (set! (.-width canvas) (* dpr (.-clientWidth ^js canvas))) (set! (.-height canvas) (* dpr (.-clientHeight ^js canvas)))) diff --git a/frontend/src/app/render_wasm/path.cljs b/frontend/src/app/render_wasm/path.cljs new file mode 100644 index 0000000000..e62eadb66c --- /dev/null +++ b/frontend/src/app/render_wasm/path.cljs @@ -0,0 +1,72 @@ +(ns app.render-wasm.path) + +(def command-size 28) + +#_(defn content->buffer + "Converts the path content into binary format." + [content] + (let [total (count content) + buffer (new js/ArrayBuffer (* total command-size)) + dview (new js/DataView buffer)] + (loop [index 0] + (when (< index total) + (let [segment (nth content index) + offset (* index command-size)] + (case (:command segment) + :move-to + (let [{:keys [x y]} (:params segment)] + (.setUint16 dview (+ offset 0) 1) + (.setFloat32 dview (+ offset 20) x) + (.setFloat32 dview (+ offset 24) y)) + :line-to + (let [{:keys [x y]} (:params segment)] + (.setUint16 dview (+ offset 0) 2) + (.setFloat32 dview (+ offset 20) x) + (.setFloat32 dview (+ offset 24) y)) + :curve-to + (let [{:keys [c1x c1y c2x c2y x y]} (:params segment)] + (.setUint16 dview (+ offset 0) 3) + (.setFloat32 dview (+ offset 4) c1x) + (.setFloat32 dview (+ offset 8) c1y) + (.setFloat32 dview (+ offset 12) c2x) + (.setFloat32 dview (+ offset 16) c2y) + (.setFloat32 dview (+ offset 20) x) + (.setFloat32 dview (+ offset 24) y)) + + :close-path + (.setUint16 dview (+ offset 0) 4)) + (recur (inc index))))) + buffer)) + +#_(defn buffer->content + "Converts the a buffer to a path content vector" + [buffer] + (assert (instance? js/ArrayBuffer buffer) "expected ArrayBuffer instance") + (let [total (/ (.-byteLength buffer) command-size) + dview (new js/DataView buffer)] + (loop [index 0 + result []] + (if (< index total) + (let [offset (* index command-size) + type (.getUint16 dview (+ offset 0)) + command (case type + 1 :move-to + 2 :line-to + 3 :curve-to + 4 :close-path) + params (case type + 1 {:x (.getFloat32 dview (+ offset 20)) + :y (.getFloat32 dview (+ offset 24))} + 2 {:x (.getFloat32 dview (+ offset 20)) + :y (.getFloat32 dview (+ offset 24))} + 3 {:c1x (.getFloat32 dview (+ offset 4)) + :c1y (.getFloat32 dview (+ offset 8)) + :c2x (.getFloat32 dview (+ offset 12)) + :c2y (.getFloat32 dview (+ offset 16)) + :x (.getFloat32 dview (+ offset 20)) + :y (.getFloat32 dview (+ offset 24))} + 4 {})] + (recur (inc index) + (conj result {:command command + :params params}))) + result)))) diff --git a/frontend/src/app/render_wasm/shape.cljs b/frontend/src/app/render_wasm/shape.cljs index 6290dbe79b..2eb61b6db4 100644 --- a/frontend/src/app/render_wasm/shape.cljs +++ b/frontend/src/app/render_wasm/shape.cljs @@ -8,7 +8,6 @@ (:require [app.common.transit :as t] [app.common.types.shape :as shape] - ;; [app.common.svg.path :as path] [app.render-wasm.api :as api] [clojure.core :as c] [cuerdas.core :as str])) @@ -120,6 +119,7 @@ :opacity (api/set-shape-opacity v) :hidden (api/set-shape-hidden v) :shapes (api/set-shape-children v) + :content (api/set-shape-path-content v) nil) ;; when something synced with wasm ;; is modified, we need to request diff --git a/frontend/src/app/util/debug.cljs b/frontend/src/app/util/debug.cljs index 8342da71c9..d14f0793cf 100644 --- a/frontend/src/app/util/debug.cljs +++ b/frontend/src/app/util/debug.cljs @@ -92,7 +92,10 @@ :bool-shapes ;; Show some information about the WebGL context. - :gl-context}) + :gl-context + + ;; Show viewbox + :wasm-viewbox}) (defn enable! [option] diff --git a/render-wasm/src/main.rs b/render-wasm/src/main.rs index efa0566bf0..c316961a3a 100644 --- a/render-wasm/src/main.rs +++ b/render-wasm/src/main.rs @@ -103,16 +103,15 @@ pub extern "C" fn use_shape(a: u32, b: u32, c: u32, d: u32) { } #[no_mangle] -pub unsafe extern "C" fn set_shape_selrect(left: f32, top: f32, right: f32, bottom: f32) { +pub 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.set_ltrb(left, top, right, bottom); + shape.set_selrect(left, top, right, bottom); } } #[no_mangle] -pub unsafe extern "C" fn set_shape_rotation(rotation: f32) { +pub extern "C" fn set_shape_rotation(rotation: f32) { let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); if let Some(shape) = state.current_shape() { shape.rotation = rotation; @@ -120,7 +119,7 @@ pub unsafe extern "C" fn set_shape_rotation(rotation: f32) { } #[no_mangle] -pub unsafe extern "C" fn set_shape_transform(a: f32, b: f32, c: f32, d: f32, e: f32, f: f32) { +pub extern "C" fn set_shape_transform(a: f32, b: f32, c: f32, d: f32, e: f32, f: f32) { let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); if let Some(shape) = state.current_shape() { shape.transform.a = a; @@ -271,6 +270,22 @@ pub extern "C" fn set_shape_hidden(hidden: bool) { } } +#[no_mangle] +pub extern "C" fn set_shape_path_content() { + let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); + + if let Some(shape) = state.current_shape() { + let bytes = mem::bytes(); + let raw_segments = bytes + .chunks(size_of::()) + .map(|data| shapes::RawPathData { + data: data.try_into().unwrap(), + }) + .collect(); + shape.set_path_segments(raw_segments).unwrap(); + } +} + fn main() { init_gl(); } diff --git a/render-wasm/src/math.rs b/render-wasm/src/math.rs index f58d04584a..d402b23dc0 100644 --- a/render-wasm/src/math.rs +++ b/render-wasm/src/math.rs @@ -1,3 +1,4 @@ use skia_safe as skia; pub type Rect = skia::Rect; +pub type Point = (f32, f32); diff --git a/render-wasm/src/mem.rs b/render-wasm/src/mem.rs index a23010df20..ea0df45fc8 100644 --- a/render-wasm/src/mem.rs +++ b/render-wasm/src/mem.rs @@ -7,7 +7,7 @@ pub extern "C" fn alloc_bytes(len: usize) -> *mut u8 { panic!("Bytes already allocated"); } - let mut buffer = Box::new(Vec::::with_capacity(len)); + let mut buffer = Box::new(vec![0u8; len]); let ptr = buffer.as_mut_ptr(); unsafe { BUFFERU8 = Some(buffer) }; @@ -23,3 +23,8 @@ pub fn buffer_ptr() -> *mut u8 { let buffer = unsafe { BUFFERU8.as_mut() }.expect("uninitializied buffer"); buffer.as_mut_ptr() } + +pub fn bytes() -> Vec { + let buffer = unsafe { BUFFERU8.take() }.expect("uninitialized buffer"); + *buffer +} diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index d07cb96eb9..74639741f4 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -6,7 +6,7 @@ use uuid::Uuid; use crate::debug; use crate::math::Rect; -use crate::shapes::{draw_image_in_container, Fill, Image, Shape}; +use crate::shapes::{draw_image_in_container, Fill, Image, Kind, Shape}; use crate::view::Viewbox; struct GpuState { @@ -224,7 +224,7 @@ impl RenderState { self.drawing_surface.canvas().concat(&matrix); for fill in shape.fills().rev() { - self.render_fill(fill, shape.selrect); + self.render_fill(fill, shape.selrect, &shape.kind); } let mut paint = skia::Paint::default(); @@ -281,22 +281,30 @@ impl RenderState { self.flush(); } - fn render_fill(&mut self, fill: &Fill, selrect: Rect) { - if let Fill::Image(image_fill) = fill { - let image = self.images.get(&image_fill.id()); - if let Some(image) = image { - draw_image_in_container( - &self.drawing_surface.canvas(), - &image, - image_fill.size(), - selrect, - &fill.to_paint(&selrect), - ); + fn render_fill(&mut self, fill: &Fill, selrect: Rect, kind: &Kind) { + match (fill, kind) { + (Fill::Image(image_fill), kind) => { + let image = self.images.get(&image_fill.id()); + if let Some(image) = image { + draw_image_in_container( + &self.drawing_surface.canvas(), + &image, + image_fill.size(), + kind, + &fill.to_paint(&selrect), + ); + } + } + (_, Kind::Rect(rect)) => { + self.drawing_surface + .canvas() + .draw_rect(rect, &fill.to_paint(&selrect)); + } + (_, Kind::Path(path)) => { + self.drawing_surface + .canvas() + .draw_path(&path.to_skia_path(), &fill.to_paint(&selrect)); } - } else { - self.drawing_surface - .canvas() - .draw_rect(selrect, &fill.to_paint(&selrect)); } } diff --git a/render-wasm/src/shapes.rs b/render-wasm/src/shapes.rs index 11785885e1..273908511c 100644 --- a/render-wasm/src/shapes.rs +++ b/render-wasm/src/shapes.rs @@ -5,13 +5,16 @@ use uuid::Uuid; mod blend; mod fills; mod images; +mod paths; pub use blend::*; pub use fills::*; pub use images::*; +pub use paths::*; -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, PartialEq)] pub enum Kind { - Rect, + Rect(math::Rect), + Path(Path), } pub type Color = skia::Color; @@ -59,7 +62,7 @@ impl Shape { Self { id, children: Vec::::new(), - kind: Kind::Rect, + kind: Kind::Rect(math::Rect::new_empty()), selrect: math::Rect::new_empty(), transform: Matrix::identity(), rotation: 0., @@ -70,6 +73,13 @@ impl Shape { } } + pub fn set_selrect(&mut self, left: f32, top: f32, right: f32, bottom: f32) { + self.selrect.set_ltrb(left, top, right, bottom); + if let Kind::Rect(_) = self.kind { + self.kind = Kind::Rect(self.selrect.to_owned()); + } + } + pub fn translation(&self) -> (f32, f32) { (self.transform.e, self.transform.f) } @@ -108,6 +118,12 @@ impl Shape { Ok(()) } + pub fn set_path_segments(&mut self, buffer: Vec) -> Result<(), String> { + let p = Path::try_from(buffer)?; + self.kind = Kind::Path(p); + Ok(()) + } + pub fn set_blend_mode(&mut self, mode: BlendMode) { self.blend_mode = mode; } diff --git a/render-wasm/src/shapes/images.rs b/render-wasm/src/shapes/images.rs index d073d4b7ee..85b96d1e29 100644 --- a/render-wasm/src/shapes/images.rs +++ b/render-wasm/src/shapes/images.rs @@ -3,17 +3,24 @@ use skia_safe as skia; pub type Image = skia::Image; +use crate::shapes::Kind; + pub fn draw_image_in_container( canvas: &skia::Canvas, image: &Image, size: (i32, i32), - container: skia::Rect, + kind: &Kind, paint: &skia::Paint, ) { let width = size.0 as f32; let height = size.1 as f32; let image_aspect_ratio = width / height; + let container = match kind { + Kind::Rect(r) => r.to_owned(), + Kind::Path(p) => p.to_skia_path().bounds().to_owned(), + }; + // Container size let container_width = container.width(); let container_height = container.height(); @@ -42,7 +49,14 @@ pub fn draw_image_in_container( canvas.save(); // Set the clipping rectangle to the container bounds - canvas.clip_rect(container, skia::ClipOp::Intersect, true); + match kind { + Kind::Rect(_) => { + canvas.clip_rect(container, skia::ClipOp::Intersect, true); + } + Kind::Path(p) => { + canvas.clip_path(&p.to_skia_path(), skia::ClipOp::Intersect, true); + } + } // Draw the image with the calculated destination rectangle canvas.draw_image_rect(image, None, dest_rect, &paint); diff --git a/render-wasm/src/shapes/paths.rs b/render-wasm/src/shapes/paths.rs new file mode 100644 index 0000000000..ae2367fb7f --- /dev/null +++ b/render-wasm/src/shapes/paths.rs @@ -0,0 +1,116 @@ +use skia_safe as skia; +use std::array::TryFromSliceError; + +use crate::math::Point; + +fn stringify_slice_err(_: TryFromSliceError) -> String { + format!("Error deserializing path") +} + +#[derive(Debug)] +pub struct RawPathData { + pub data: [u8; 28], +} + +impl RawPathData { + fn command(&self) -> Result { + let cmd = u16::from_be_bytes(self.data[0..2].try_into().map_err(stringify_slice_err)?); + Ok(cmd) + } + + fn xy(&self) -> Result { + let x = f32::from_be_bytes(self.data[20..24].try_into().map_err(stringify_slice_err)?); + let y = f32::from_be_bytes(self.data[24..].try_into().map_err(stringify_slice_err)?); + Ok((x, y)) + } + + fn c1(&self) -> Result { + let c1_x = f32::from_be_bytes(self.data[4..8].try_into().map_err(stringify_slice_err)?); + let c1_y = f32::from_be_bytes(self.data[8..12].try_into().map_err(stringify_slice_err)?); + + Ok((c1_x, c1_y)) + } + + fn c2(&self) -> Result { + let c2_x = f32::from_be_bytes(self.data[12..16].try_into().map_err(stringify_slice_err)?); + let c2_y = f32::from_be_bytes(self.data[16..20].try_into().map_err(stringify_slice_err)?); + + Ok((c2_x, c2_y)) + } +} + +const MOVE_TO: u16 = 1; +const LINE_TO: u16 = 2; +const CURVE_TO: u16 = 3; +const CLOSE: u16 = 4; + +#[derive(Debug, PartialEq, Copy, Clone)] +enum Segment { + MoveTo(Point), + LineTo(Point), + CurveTo((Point, Point, Point)), + Close, +} + +impl TryFrom for Segment { + type Error = String; + fn try_from(value: RawPathData) -> Result { + let cmd = value.command()?; + match cmd { + MOVE_TO => Ok(Segment::MoveTo(value.xy()?)), + LINE_TO => Ok(Segment::LineTo(value.xy()?)), + CURVE_TO => Ok(Segment::CurveTo((value.c1()?, value.c2()?, value.xy()?))), + CLOSE => Ok(Segment::Close), + _ => Err(format!( + "Error deserializing path. Unknown command/flags: {:#010x}", + cmd + )), + } + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct Path { + segments: Vec, + skia_path: skia::Path, +} + +impl TryFrom> for Path { + type Error = String; + + fn try_from(value: Vec) -> Result { + let segments = value + .into_iter() + .map(|raw| Segment::try_from(raw)) + .collect::, String>>()?; + + let mut skia_path = skia::Path::new(); + for segment in segments.iter() { + match *segment { + Segment::MoveTo(xy) => { + skia_path.move_to(xy); + } + Segment::LineTo(xy) => { + skia_path.line_to(xy); + } + Segment::CurveTo((c1, c2, xy)) => { + skia_path.cubic_to(c1, c2, xy); + } + Segment::Close => { + skia_path.close(); + } + } + } + + Ok(Path { + segments, + skia_path, + }) + } +} + +impl Path { + pub fn to_skia_path(&self) -> skia::Path { + self.skia_path.snapshot() + } +}