From 4e5f67676cfeaf939a4044a696c7d54667120fea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bel=C3=A9n=20Albeza?= Date: Thu, 9 Jan 2025 12:21:05 +0100 Subject: [PATCH] :tada: Implement boolean operations (wasm) --- frontend/src/app/render_wasm/api.cljs | 25 ++++++++++++++++++++++++- frontend/src/app/render_wasm/shape.cljs | 2 ++ render-wasm/docs/serialization.md | 20 ++++++++++++++++---- render-wasm/src/main.rs | 25 +++++++++++++++++++++---- render-wasm/src/shapes.rs | 25 +++++++++++++++++++++++-- render-wasm/src/shapes/bools.rs | 25 +++++++++++++++++++++++++ render-wasm/src/shapes/paths.rs | 6 ++++++ render-wasm/src/shapes/renderable.rs | 14 +++++++++----- 8 files changed, 126 insertions(+), 16 deletions(-) create mode 100644 render-wasm/src/shapes/bools.rs diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index eb3f38dd3..3555d6b25 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -93,6 +93,9 @@ (= type :path) (h/call internal-module "_set_shape_kind_path") + (= type :bool) + (h/call internal-module "_set_shape_kind_bool") + :else (h/call internal-module "_set_shape_kind_rect"))) @@ -343,6 +346,24 @@ [hidden] (h/call internal-module "_set_shape_hidden" hidden)) + +(defn- translate-bool-type + [bool-type] + (case bool-type + :union 0 + :difference 1 + :intersection 2 + :exclusion 3 + 0)) + +(defn set-shape-bool-type + [bool-type] + (h/call internal-module "_set_shape_bool_type" (translate-bool-type bool-type))) + +(defn set-shape-bool-content + [content] + (set-shape-path-content content)) + (def debounce-render-without-cache (fns/debounce render-without-cache 100)) (defn set-view @@ -373,7 +394,8 @@ blend-mode (dm/get-prop shape :blend-mode) opacity (dm/get-prop shape :opacity) hidden (dm/get-prop shape :hidden) - content (dm/get-prop shape :content)] + content (dm/get-prop shape :content) + bool-content (dm/get-prop shape :bool-content)] (use-shape id) (set-shape-type type) @@ -386,6 +408,7 @@ (set-shape-opacity opacity) (set-shape-hidden hidden) (when (and (some? content) (= type :path)) (set-shape-path-content content)) + (when (some? bool-content) (set-shape-bool-content bool-content)) (let [pending' (concat (set-shape-fills fills) (set-shape-strokes strokes))] (recur (inc index) (into pending pending')))) pending))] diff --git a/frontend/src/app/render_wasm/shape.cljs b/frontend/src/app/render_wasm/shape.cljs index 9be274472..5fe234ba6 100644 --- a/frontend/src/app/render_wasm/shape.cljs +++ b/frontend/src/app/render_wasm/shape.cljs @@ -112,6 +112,8 @@ (api/use-shape (:id self)) (case k :type (api/set-shape-type v) + :bool-type (api/set-shape-bool-type v) + :bool-content (api/set-shape-bool-content v) :selrect (api/set-shape-selrect v) :show-content (api/set-shape-clip-content (not v)) :rotation (api/set-shape-rotation v) diff --git a/render-wasm/docs/serialization.md b/render-wasm/docs/serialization.md index 6ef7912c0..9cf862bd9 100644 --- a/render-wasm/docs/serialization.md +++ b/render-wasm/docs/serialization.md @@ -40,7 +40,7 @@ Gradient stops are serialized in a `Uint8Array`, each stop taking **5 bytes**. **Stop offset** is the offset, being integer values ranging from `0` to `100` (both inclusive). -## StrokeCap +## Stroke Caps Stroke caps are serialized as `u8`: @@ -53,9 +53,9 @@ Stroke caps are serialized as `u8`: | 5 | Diamond | | 6 | Round | | 7 | Square | -| _ | None | +| \_ | None | -## StrokeStyle +## Stroke Sytles Stroke styles are serialized as `u8`: @@ -64,4 +64,16 @@ Stroke styles are serialized as `u8`: | 1 | Dotted | | 2 | Dashed | | 3 | Mixed | -| _ | Solid | +| \_ | Solid | + +## Bool Operations + +Bool operations (`bool-type`) are serialized as `u8`: + +| Value | Field | +| ----- | ------------ | +| 0 | Union | +| 1 | Difference | +| 2 | Intersection | +| 3 | Exclusion | +| \_ | Union | diff --git a/render-wasm/src/main.rs b/render-wasm/src/main.rs index a16fd3648..044175666 100644 --- a/render-wasm/src/main.rs +++ b/render-wasm/src/main.rs @@ -7,8 +7,7 @@ mod state; mod utils; mod view; -use crate::shapes::Kind; -use crate::shapes::Path; +use crate::shapes::{BoolType, Kind, Path}; use skia_safe as skia; use crate::state::State; @@ -134,8 +133,26 @@ pub unsafe extern "C" fn set_shape_kind_rect() { pub unsafe extern "C" fn set_shape_kind_path() { let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); if let Some(shape) = state.current_shape() { - let p = Path::try_from(Vec::new()).unwrap(); - shape.set_kind(Kind::Path(p)); + shape.set_kind(Kind::Path(Path::default())); + } +} + +#[no_mangle] +pub unsafe extern "C" fn set_shape_kind_bool() { + let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); + if let Some(shape) = state.current_shape() { + match shape.kind() { + Kind::Bool(_, _) => {} + _ => shape.set_kind(Kind::Bool(BoolType::default(), Path::default())), + } + } +} + +#[no_mangle] +pub unsafe extern "C" fn set_shape_bool_type(raw_bool_type: u8) { + let state = unsafe { STATE.as_mut() }.expect("got an invalid state pointer"); + if let Some(shape) = state.current_shape() { + shape.set_bool_type(BoolType::from(raw_bool_type)) } } diff --git a/render-wasm/src/shapes.rs b/render-wasm/src/shapes.rs index 4943d8e64..df390da35 100644 --- a/render-wasm/src/shapes.rs +++ b/render-wasm/src/shapes.rs @@ -4,6 +4,7 @@ use uuid::Uuid; use crate::render::{BlendMode, Renderable}; +mod bools; mod fills; mod images; mod matrix; @@ -11,6 +12,7 @@ mod paths; mod renderable; mod strokes; +pub use bools::*; pub use fills::*; pub use images::*; use matrix::*; @@ -22,6 +24,7 @@ pub enum Kind { Rect(math::Rect), Circle(math::Rect), Path(Path), + Bool(BoolType, Path), } pub type Color = skia::Color; @@ -61,6 +64,10 @@ impl Shape { } } + pub fn kind(&self) -> Kind { + self.kind.clone() + } + pub fn set_selrect(&mut self, left: f32, top: f32, right: f32, bottom: f32) { self.selrect.set_ltrb(left, top, right, bottom); match self.kind { @@ -169,7 +176,12 @@ impl Shape { pub fn set_path_segments(&mut self, buffer: Vec) -> Result<(), String> { let p = Path::try_from(buffer)?; - self.kind = Kind::Path(p); + let kind = match &self.kind { + Kind::Bool(bool_type, _) => Kind::Bool(*bool_type, p), + _ => Kind::Path(p), + }; + self.kind = kind; + Ok(()) } @@ -177,9 +189,18 @@ impl Shape { self.blend_mode = mode; } + pub fn set_bool_type(&mut self, bool_type: BoolType) { + let kind = match &self.kind { + Kind::Bool(_, path) => Kind::Bool(bool_type, path.clone()), + _ => Kind::Bool(bool_type, Path::default()), + }; + + self.kind = kind; + } + fn to_path_transform(&self) -> Option { match self.kind { - Kind::Path(_) => { + Kind::Path(_) | Kind::Bool(_, _) => { let center = self.bounds().center(); let mut matrix = skia::Matrix::new_identity(); matrix.pre_translate(center); diff --git a/render-wasm/src/shapes/bools.rs b/render-wasm/src/shapes/bools.rs new file mode 100644 index 000000000..3f7cdd410 --- /dev/null +++ b/render-wasm/src/shapes/bools.rs @@ -0,0 +1,25 @@ +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum BoolType { + Union, + Difference, + Intersection, + Exclusion, +} + +impl From for BoolType { + fn from(value: u8) -> Self { + match value { + 0 => Self::Union, + 1 => Self::Difference, + 2 => Self::Intersection, + 3 => Self::Exclusion, + _ => Self::default(), + } + } +} + +impl Default for BoolType { + fn default() -> Self { + Self::Union + } +} diff --git a/render-wasm/src/shapes/paths.rs b/render-wasm/src/shapes/paths.rs index 1e77b9ccc..628b8ddd5 100644 --- a/render-wasm/src/shapes/paths.rs +++ b/render-wasm/src/shapes/paths.rs @@ -88,6 +88,12 @@ fn starts_and_ends_at_same_point(path: &skia::Path) -> bool { start_point == end_point } +impl Default for Path { + fn default() -> Self { + Path::try_from(Vec::new()).unwrap() + } +} + impl TryFrom> for Path { type Error = String; diff --git a/render-wasm/src/shapes/renderable.rs b/render-wasm/src/shapes/renderable.rs index 5bbecab4f..aa6558690 100644 --- a/render-wasm/src/shapes/renderable.rs +++ b/render-wasm/src/shapes/renderable.rs @@ -64,7 +64,11 @@ impl Renderable for Shape { } fn children_ids(&self) -> Vec { - self.children.clone() + if let Kind::Bool(_, _) = self.kind { + vec![] + } else { + self.children.clone() + } } } @@ -97,7 +101,7 @@ fn render_fill( (_, Kind::Circle(rect)) => { surface.canvas().draw_oval(rect, &fill.to_paint(&selrect)); } - (_, Kind::Path(path)) => { + (_, Kind::Path(path)) | (_, Kind::Bool(_, path)) => { surface.canvas().draw_path( &path.to_skia_path().transform(path_transform.unwrap()), &fill.to_paint(&selrect), @@ -130,7 +134,7 @@ fn render_stroke( match kind { Kind::Rect(rect) => draw_stroke_on_rect(surface.canvas(), stroke, rect, &selrect), Kind::Circle(rect) => draw_stroke_on_circle(surface.canvas(), stroke, rect, &selrect), - Kind::Path(path) => { + Kind::Path(path) | Kind::Bool(_, path) => { draw_stroke_on_path(surface.canvas(), stroke, path, &selrect, path_transform); } } @@ -439,7 +443,7 @@ pub fn draw_image_fill_in_container( oval_path.add_oval(container, None); canvas.clip_path(&oval_path, skia::ClipOp::Intersect, true); } - Kind::Path(p) => { + Kind::Path(p) | Kind::Bool(_, p) => { canvas.clip_path( &p.to_skia_path().transform(path_transform.unwrap()), skia::ClipOp::Intersect, @@ -476,7 +480,7 @@ pub fn draw_image_stroke_in_container( match kind { Kind::Rect(rect) => draw_stroke_on_rect(canvas, stroke, rect, &outer_rect), Kind::Circle(rect) => draw_stroke_on_circle(canvas, stroke, rect, &outer_rect), - Kind::Path(p) => { + Kind::Path(p) | Kind::Bool(_, p) => { let mut path = p.to_skia_path(); path.transform(path_transform.unwrap()); let stroke_kind = stroke.render_kind(p.is_open());