0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-01-06 14:50:20 -05:00

Merge pull request #5388 from penpot/azazeln28-feat-start-drawing-paths

🎉 Start drawing paths
This commit is contained in:
Alejandro 2024-12-09 16:27:28 +01:00 committed by GitHub
commit 0eedc036be
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 307 additions and 34 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -92,7 +92,10 @@
:bool-shapes
;; Show some information about the WebGL context.
:gl-context})
:gl-context
;; Show viewbox
:wasm-viewbox})
(defn enable!
[option]

View file

@ -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::<shapes::RawPathData>())
.map(|data| shapes::RawPathData {
data: data.try_into().unwrap(),
})
.collect();
shape.set_path_segments(raw_segments).unwrap();
}
}
fn main() {
init_gl();
}

View file

@ -1,3 +1,4 @@
use skia_safe as skia;
pub type Rect = skia::Rect;
pub type Point = (f32, f32);

View file

@ -7,7 +7,7 @@ pub extern "C" fn alloc_bytes(len: usize) -> *mut u8 {
panic!("Bytes already allocated");
}
let mut buffer = Box::new(Vec::<u8>::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<u8> {
let buffer = unsafe { BUFFERU8.take() }.expect("uninitialized buffer");
*buffer
}

View file

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

View file

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

View file

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

View file

@ -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<u16, String> {
let cmd = u16::from_be_bytes(self.data[0..2].try_into().map_err(stringify_slice_err)?);
Ok(cmd)
}
fn xy(&self) -> Result<Point, String> {
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<Point, String> {
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<Point, String> {
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<RawPathData> for Segment {
type Error = String;
fn try_from(value: RawPathData) -> Result<Self, Self::Error> {
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<Segment>,
skia_path: skia::Path,
}
impl TryFrom<Vec<RawPathData>> for Path {
type Error = String;
fn try_from(value: Vec<RawPathData>) -> Result<Self, Self::Error> {
let segments = value
.into_iter()
.map(|raw| Segment::try_from(raw))
.collect::<Result<Vec<Segment>, 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()
}
}