0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-02-20 22:06:07 -05:00

🎉 Implement boolean operations (wasm)

This commit is contained in:
Belén Albeza 2025-01-09 12:21:05 +01:00
parent 1514faca55
commit 4e5f67676c
8 changed files with 126 additions and 16 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -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<RawPathData>) -> 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<skia::Matrix> {
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);

View file

@ -0,0 +1,25 @@
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum BoolType {
Union,
Difference,
Intersection,
Exclusion,
}
impl From<u8> 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
}
}

View file

@ -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<Vec<RawPathData>> for Path {
type Error = String;

View file

@ -64,7 +64,11 @@ impl Renderable for Shape {
}
fn children_ids(&self) -> Vec<Uuid> {
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());