mirror of
https://github.com/penpot/penpot.git
synced 2026-07-27 00:18:17 +00:00
🎉 Serialize guides to wasm
This commit is contained in:
parent
c0188b6c58
commit
4f4f195113
@ -538,6 +538,12 @@
|
||||
(when @canvas-init?
|
||||
(wasm.api/render-ui-only)))
|
||||
|
||||
;; Ruler guides: push the page guides to the render engine whenever they
|
||||
;; change or their visibility toggles. When hidden we send an empty set.
|
||||
(mf/with-effect [@canvas-init? guides show-rulers? show-grids?]
|
||||
(when @canvas-init?
|
||||
(wasm.api/set-guides (if (and show-rulers? show-grids?) guides {}))))
|
||||
|
||||
(hooks/setup-dom-events zoom disable-paste-ref in-viewport-ref read-only? drawing-tool path-drawing?)
|
||||
(hooks/setup-viewport-size vport viewport-ref)
|
||||
(hooks/setup-cursor cursor alt? mod? space? panning drawing-tool path-drawing? path-editing? z? read-only?)
|
||||
@ -824,36 +830,19 @@
|
||||
[:& presence/active-cursors
|
||||
{:page-id page-id}])
|
||||
|
||||
(when-not hide-ui?
|
||||
[:> rulers/rulers*
|
||||
{:zoom zoom
|
||||
:zoom-inverse zoom-inverse
|
||||
:vbox vbox
|
||||
:selected-shapes selected-shapes
|
||||
:offset-x offset-x
|
||||
:offset-y offset-y
|
||||
:show-rulers show-rulers?}])
|
||||
;; NOTE: ruler guides are being migrated to the WASM render engine.
|
||||
;; The SVG-overlay rendering is temporarily disabled while we implement
|
||||
;; the new path.
|
||||
(when (and show-rulers? show-grids?)
|
||||
[:> guides/viewport-guides*
|
||||
{:zoom zoom
|
||||
:vbox vbox
|
||||
:guides guides
|
||||
;; :guides guides
|
||||
:guides #{}
|
||||
:hover-frame guide-frame
|
||||
:disabled-guides disabled-guides?
|
||||
:modifiers wasm-modifiers}])
|
||||
|
||||
;; NOTE: ruler guides are being migrated to the WASM render engine.
|
||||
;; The SVG-overlay rendering is temporarily disabled while we implement
|
||||
;; the new path.
|
||||
#_(when (and show-rulers? show-grids?)
|
||||
[:> guides/viewport-guides*
|
||||
{:zoom zoom
|
||||
:vbox vbox
|
||||
:guides guides
|
||||
:hover-frame guide-frame
|
||||
:disabled-guides disabled-guides?
|
||||
:modifiers wasm-modifiers}])
|
||||
|
||||
;; DEBUG LAYOUT DROP-ZONES
|
||||
(when (dbg/enabled? :layout-drop-zones)
|
||||
[:> wvd/debug-drop-zones* {:selected-shapes selected-shapes
|
||||
|
||||
@ -2240,6 +2240,20 @@
|
||||
(h/call wasm/internal-module "_hide_grid")
|
||||
(request-render "clear-grid"))
|
||||
|
||||
;; Ruler guides ----------------------------------------------------------------
|
||||
|
||||
(defn set-guides
|
||||
"Serializes the page guides and sends them to the render engine.
|
||||
`guides` is the page `:guides` map (id -> guide)."
|
||||
[guides]
|
||||
(let [size (sr/get-guides-byte-size guides)
|
||||
offset (mem/alloc->offset-32 size)
|
||||
heapu32 (mem/get-heap-u32)
|
||||
heapf32 (mem/get-heap-f32)]
|
||||
(sr/write-guides guides heapu32 heapf32 offset)
|
||||
(h/call wasm/internal-module "_set_guides")
|
||||
(request-render "set-guides")))
|
||||
|
||||
(defn get-grid-coords
|
||||
[position]
|
||||
(let [offset (h/call wasm/internal-module
|
||||
|
||||
@ -8,7 +8,9 @@
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.types.color :as clr]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.render-wasm.serializers.color :as sr-clr]
|
||||
[app.render-wasm.wasm :as wasm]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
@ -281,3 +283,47 @@
|
||||
(let [values (unchecked-get wasm/serializers "transform-entry-kind")
|
||||
default (unchecked-get values "parent")]
|
||||
(d/nilv (unchecked-get values (d/name kind)) default)))
|
||||
|
||||
;; --- Guides
|
||||
|
||||
;; Each guide is serialized as 3 x 32-bit words:
|
||||
;; kind (u32) | color (u32 argb) | position (f32)
|
||||
(def ^:private guide-entry-size 12)
|
||||
|
||||
;; Default guide color used when a guide has no explicit color (matches the
|
||||
;; previous SVG overlay `default-guide-color`).
|
||||
(def ^:private default-guide-color clr/new-danger)
|
||||
|
||||
(defn- translate-guide-axis
|
||||
"Maps a guide axis to the RawGuideKind discriminant expected by WASM.
|
||||
`:x` (constant x) is a vertical guide, `:y` is a horizontal one."
|
||||
[axis]
|
||||
(let [values (unchecked-get wasm/serializers "guide-kind")
|
||||
default (unchecked-get values "vertical")]
|
||||
(case axis
|
||||
:x (unchecked-get values "vertical")
|
||||
:y (unchecked-get values "horizontal")
|
||||
default)))
|
||||
|
||||
(defn get-guides-byte-size
|
||||
"Total heap size (in bytes) needed to serialize `guides` (a map id -> guide),
|
||||
including the 4-byte header that holds the guide count."
|
||||
[guides]
|
||||
(+ 4 (* (count guides) guide-entry-size)))
|
||||
|
||||
(defn write-guides
|
||||
"Writes `guides` (a map id -> guide) into the heap views starting at the
|
||||
32-bit `offset`. Layout: count header (u32) followed by
|
||||
`kind | color | position` per guide."
|
||||
[guides heapu32 heapf32 offset]
|
||||
(let [guides (vec (vals guides))
|
||||
total (count guides)]
|
||||
(aset heapu32 offset total)
|
||||
(loop [i 0]
|
||||
(when (< i total)
|
||||
(let [guide (nth guides i)
|
||||
base (+ offset 1 (* i 3))]
|
||||
(aset heapu32 base (translate-guide-axis (get guide :axis)))
|
||||
(aset heapu32 (+ base 1) (sr-clr/hex->u32argb (or (get guide :color) default-guide-color) 1))
|
||||
(aset heapf32 (+ base 2) (get guide :position)))
|
||||
(recur (inc i))))))
|
||||
|
||||
@ -62,6 +62,7 @@
|
||||
:wrap-type shared/RawWrapType
|
||||
:grid-track-type shared/RawGridTrackType
|
||||
:shadow-style shared/RawShadowStyle
|
||||
:guide-kind shared/RawGuideKind
|
||||
:stroke-style shared/RawStrokeStyle
|
||||
:stroke-cap shared/RawStrokeCap
|
||||
:shape-type shared/RawShapeType
|
||||
|
||||
@ -5,7 +5,7 @@ use crate::emscripten::init_gl;
|
||||
|
||||
use crate::mem;
|
||||
use crate::render::{gpu_state::GpuState, RenderState};
|
||||
use crate::state::{State, TextEditorState};
|
||||
use crate::state::{State, TextEditorState, UIState};
|
||||
|
||||
static mut DESIGN_STATE: *mut State = std::ptr::null_mut();
|
||||
|
||||
@ -50,6 +50,17 @@ pub(crate) fn get_text_editor_state() -> &'static mut TextEditorState {
|
||||
}
|
||||
}
|
||||
|
||||
/// UI State
|
||||
static mut UI_STATE: *mut UIState = std::ptr::null_mut();
|
||||
|
||||
#[inline(always)]
|
||||
pub(crate) fn get_ui_state() -> &'static mut UIState {
|
||||
unsafe {
|
||||
debug_assert!(!UI_STATE.is_null(), "UI State is null");
|
||||
&mut *UI_STATE
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: These with_state* macros should be using our CriticalError instead of expect.
|
||||
// But to do that, we need to not use them at domain-level (i.e. in business logic), just
|
||||
// in the context of the wasm call.
|
||||
@ -118,6 +129,14 @@ fn text_editor_init() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Initializes UIState.
|
||||
fn ui_init() {
|
||||
unsafe {
|
||||
let ui_state = UIState::new();
|
||||
UI_STATE = Box::into_raw(Box::new(ui_state));
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
pub extern "C" fn init(width: i32, height: i32) -> Result<()> {
|
||||
@ -127,6 +146,7 @@ pub extern "C" fn init(width: i32, height: i32) -> Result<()> {
|
||||
render_init(width, height);
|
||||
text_editor_init();
|
||||
design_init();
|
||||
ui_init();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@ -9,6 +9,7 @@ mod render;
|
||||
mod shapes;
|
||||
mod state;
|
||||
mod tiles;
|
||||
mod ui;
|
||||
mod utils;
|
||||
mod uuid;
|
||||
mod view;
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
use skia_safe::{self as skia, Color4f};
|
||||
|
||||
use super::{RenderState, ShapesPoolRef, SurfaceId};
|
||||
use crate::globals::get_ui_state;
|
||||
use crate::render::{grid_layout, rulers};
|
||||
use crate::shapes::{Layout, Type};
|
||||
|
||||
pub mod guides;
|
||||
|
||||
pub fn render(render_state: &mut RenderState, shapes: ShapesPoolRef) {
|
||||
@ -66,7 +66,7 @@ pub fn render(render_state: &mut RenderState, shapes: ShapesPoolRef) {
|
||||
let ruler_state = render_state.rulers;
|
||||
rulers::render(canvas, viewbox, &render_state.fonts, &ruler_state);
|
||||
// TODO: pass guides data here
|
||||
guides::render(canvas, zoom, viewbox.area);
|
||||
guides::render(canvas, zoom, viewbox.area, &get_ui_state().guides);
|
||||
|
||||
canvas.restore();
|
||||
|
||||
|
||||
@ -1,36 +1,26 @@
|
||||
use skia_safe::{self as skia};
|
||||
|
||||
use crate::math::Rect;
|
||||
use crate::state::Guide;
|
||||
use crate::ui::{Guide, GuideKind};
|
||||
|
||||
const LABEL_TEXT_COLOR: Color = Color::new(255, 255, 255);
|
||||
const LABEL_FONT_SIZE: f32 = 12.0;
|
||||
|
||||
/// Renders the ruler guides overlay.
|
||||
///
|
||||
/// NOTE: temporary hardcoded implementation while we figure out exactly which
|
||||
/// data we need to receive from ClojureScript. For now it draws a single
|
||||
/// vertical 1px magenta (#ff00ff) guide at x=100, spanning the visible area.
|
||||
pub fn render(canvas: &skia::Canvas, zoom: f32, area: Rect) {
|
||||
let guides = vec![
|
||||
Guide::new(GuideKind::Vertical(100.0), Color::new(255, 0, 255)),
|
||||
Guide::new(GuideKind::Horizontal(200.0), Color::new(0, 255, 0)),
|
||||
];
|
||||
/// Renders the ruler guides overlay using the guides provided by the host
|
||||
/// (ClojureScript) and stored in the render state.
|
||||
pub fn render(canvas: &skia::Canvas, zoom: f32, area: Rect, guides: &[Guide]) {
|
||||
for guide in guides {
|
||||
render_guide(canvas, zoom, area, guide);
|
||||
render_guide(canvas, zoom, area, *guide);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_guide(canvas: &skia::Canvas, zoom: f32, area: Rect, guide: Guide) {
|
||||
let mut paint = skia::Paint::default();
|
||||
paint.set_style(skia::PaintStyle::Stroke);
|
||||
paint.set_color(guide.color.into());
|
||||
paint.set_color(Into::<skia::Color>::into(guide.color));
|
||||
paint.set_anti_alias(true);
|
||||
paint.set_stroke_width(1.0 / zoom);
|
||||
|
||||
let (x1, y1, x2, y2) = match guide {
|
||||
Guide::Vertical(x) => (x, area.top, x, area.bottom),
|
||||
Guide::Horizontal(y) => (area.left, y, area.right, y),
|
||||
let (x1, y1, x2, y2) = match guide.kind {
|
||||
GuideKind::Vertical(x) => (x, area.top, x, area.bottom),
|
||||
GuideKind::Horizontal(y) => (area.left, y, area.right, y),
|
||||
};
|
||||
|
||||
canvas.draw_line((x1, y1), (x2, y2), &paint);
|
||||
|
||||
@ -8,7 +8,7 @@ mod ui;
|
||||
pub use rulers::RulerState;
|
||||
pub use shapes_pool::{ShapesPool, ShapesPoolMutRef, ShapesPoolRef};
|
||||
pub use text_editor::*;
|
||||
pub use ui::*;
|
||||
pub use ui::UIState;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::render::FrameType;
|
||||
|
||||
@ -1,19 +1,16 @@
|
||||
use crate::shapes::Color;
|
||||
use crate::ui::Guide;
|
||||
use crate::uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Copy)]
|
||||
pub enum GuideKind {
|
||||
Vertical(f32),
|
||||
Horizontal(f32),
|
||||
pub struct UIState {
|
||||
pub guides: Vec<Guide>,
|
||||
pub _show_grid: Option<Uuid>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Copy)]
|
||||
pub struct Guide {
|
||||
pub kind: GuideKind,
|
||||
pub color: Color,
|
||||
}
|
||||
|
||||
impl Guide {
|
||||
pub fn new(kind: GuideKind, color: Color) -> Self {
|
||||
Self { kind, color }
|
||||
impl UIState {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
guides: Vec::new(),
|
||||
_show_grid: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
19
render-wasm/src/ui.rs
Normal file
19
render-wasm/src/ui.rs
Normal file
@ -0,0 +1,19 @@
|
||||
use crate::shapes::Color;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Copy)]
|
||||
pub enum GuideKind {
|
||||
Vertical(f32),
|
||||
Horizontal(f32),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Copy)]
|
||||
pub struct Guide {
|
||||
pub kind: GuideKind,
|
||||
pub color: Color,
|
||||
}
|
||||
|
||||
impl Guide {
|
||||
pub fn new(kind: GuideKind, color: Color) -> Self {
|
||||
Self { kind, color }
|
||||
}
|
||||
}
|
||||
@ -13,3 +13,4 @@ pub mod svg_attrs;
|
||||
pub mod text;
|
||||
pub mod text_editor;
|
||||
pub mod transforms;
|
||||
pub mod ui;
|
||||
|
||||
90
render-wasm/src/wasm/ui.rs
Normal file
90
render-wasm/src/wasm/ui.rs
Normal file
@ -0,0 +1,90 @@
|
||||
use crate::mem;
|
||||
use crate::{
|
||||
error::{Error, Result},
|
||||
globals::get_ui_state,
|
||||
ui::{Guide, GuideKind},
|
||||
};
|
||||
use macros::{wasm_error, ToJs};
|
||||
|
||||
const RAW_GUIDE_SIZE: usize = std::mem::size_of::<RawGuide>();
|
||||
|
||||
#[repr(u8)]
|
||||
#[derive(Debug, Clone, PartialEq, Copy, ToJs)]
|
||||
pub enum RawGuideKind {
|
||||
Vertical = 0,
|
||||
Horizontal = 1,
|
||||
}
|
||||
|
||||
impl From<u32> for RawGuideKind {
|
||||
fn from(value: u32) -> Self {
|
||||
match value {
|
||||
1 => RawGuideKind::Horizontal,
|
||||
_ => RawGuideKind::Vertical,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Flat, FFI-friendly representation of a guide.
|
||||
///
|
||||
/// The layout uses only 32-bit fields so it can be written from ClojureScript
|
||||
/// straight into the `HEAPU32`/`HEAPF32` views without padding surprises.
|
||||
#[repr(C)]
|
||||
#[repr(align(4))]
|
||||
#[derive(Debug, Clone, PartialEq, Copy)]
|
||||
pub struct RawGuide {
|
||||
kind: u32,
|
||||
color: u32,
|
||||
position: f32,
|
||||
}
|
||||
|
||||
impl From<RawGuide> for Guide {
|
||||
fn from(value: RawGuide) -> Self {
|
||||
let kind = match RawGuideKind::from(value.kind) {
|
||||
RawGuideKind::Vertical => GuideKind::Vertical(value.position),
|
||||
RawGuideKind::Horizontal => GuideKind::Horizontal(value.position),
|
||||
};
|
||||
Guide::new(kind, value.color.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<[u8; RAW_GUIDE_SIZE]> for RawGuide {
|
||||
fn from(bytes: [u8; RAW_GUIDE_SIZE]) -> Self {
|
||||
unsafe { std::mem::transmute(bytes) }
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&[u8]> for RawGuide {
|
||||
type Error = Error;
|
||||
fn try_from(bytes: &[u8]) -> Result<Self> {
|
||||
let bytes: [u8; RAW_GUIDE_SIZE] = bytes
|
||||
.try_into()
|
||||
.map_err(|_| Error::CriticalError("Invalid guide data".to_string()))?;
|
||||
Ok(RawGuide::from(bytes))
|
||||
}
|
||||
}
|
||||
|
||||
fn read_guides_from_bytes(buffer: &[u8], count: usize) -> Result<Vec<Guide>> {
|
||||
buffer
|
||||
.chunks_exact(RAW_GUIDE_SIZE)
|
||||
.take(count)
|
||||
.map(|bytes| RawGuide::try_from(bytes).map(|guide| guide.into()))
|
||||
.collect::<Result<Vec<Guide>>>()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
pub extern "C" fn set_guides() -> Result<()> {
|
||||
let bytes = mem::bytes();
|
||||
// The first 4 bytes are a header holding the number of guides.
|
||||
let count = u32::from_le_bytes(
|
||||
bytes
|
||||
.get(0..4)
|
||||
.and_then(|slice| slice.try_into().ok())
|
||||
.unwrap_or([0; 4]),
|
||||
) as usize;
|
||||
let guides = read_guides_from_bytes(&bytes[4..], count)?;
|
||||
get_ui_state().guides = guides;
|
||||
|
||||
mem::free_bytes()?;
|
||||
Ok(())
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user