penpot/frontend/src/app/render_wasm/serializers.cljs

418 lines
14 KiB
Clojure

;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.render-wasm.serializers
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.files.helpers :as cfh]
[app.common.types.color :as clr]
[app.common.types.shape-tree :as ctst]
[app.common.uuid :as uuid]
[app.render-wasm.serializers.color :as sr-clr]
[app.render-wasm.wasm :as wasm]
[cuerdas.core :as str]))
(defn u8
[value]
(let [u8-arr (js/Uint8Array. 1)]
(aset u8-arr 0 value)
u8-arr))
(defn f32->u8
[value]
(let [f32-arr (js/Float32Array. 1)]
(aset f32-arr 0 value)
(js/Uint8Array. (.-buffer f32-arr))))
(defn i32->u8
[value]
(let [i32-arr (js/Int32Array. 1)]
(aset i32-arr 0 value)
(js/Uint8Array. (.-buffer i32-arr))))
(defn bool->u8
[value]
(let [result (js/Uint8Array. 1)]
(aset result 0 (if value 1 0))
result))
(defn uuid->u8
[id]
(let [buffer (uuid/get-u32 id)
u32-arr (js/Uint32Array. 4)]
(aset u32-arr 0 (aget buffer 0))
(aset u32-arr 1 (aget buffer 1))
(aset u32-arr 2 (aget buffer 2))
(aset u32-arr 3 (aget buffer 3))
(js/Uint8Array. (.-buffer u32-arr))))
(defn serialize-uuid
[id]
(try
(if (nil? id)
(do
[uuid/zero])
(let [as-uuid (uuid/uuid id)]
(uuid/get-u32 as-uuid)))
(catch :default _e
[uuid/zero])))
(defn translate-shape-type
[type]
(let [values (unchecked-get wasm/serializers "shape-type")
default (unchecked-get values "rect")]
(d/nilv (unchecked-get values (d/name type)) default)))
(defn translate-stroke-linecap
[stroke-linecap]
(let [values (unchecked-get wasm/serializers "stroke-linecap")
default (unchecked-get values "butt")]
(d/nilv (unchecked-get values (d/name stroke-linecap)) default)))
(defn translate-stroke-linejoin
[stroke-linejoin]
(let [values (unchecked-get wasm/serializers "stroke-linejoin")
default (unchecked-get values "miter")]
(d/nilv (unchecked-get values (d/name stroke-linejoin)) default)))
(defn translate-fill-rule
[fill-rule]
(let [values (unchecked-get wasm/serializers "fill-rule")
default (unchecked-get values "nonzero")]
(d/nilv (unchecked-get values (d/name fill-rule)) default)))
(defn translate-stroke-style
[stroke-style]
(let [values (unchecked-get wasm/serializers "stroke-style")
default (unchecked-get values "solid")]
(d/nilv (unchecked-get values (d/name stroke-style)) default)))
(defn translate-stroke-cap
[stroke-cap]
(let [values (unchecked-get wasm/serializers "stroke-cap")
default (unchecked-get values "none")]
(d/nilv (unchecked-get values (d/name stroke-cap)) default)))
(defn serialize-path-attrs
[svg-attrs]
(reduce-kv
(fn [acc key value]
(dm/str
acc
(str/kebab key) "\0"
value "\0")) ""
svg-attrs))
(defn translate-blend-mode
[blend-mode]
(let [values (unchecked-get wasm/serializers "blend-mode")
default (unchecked-get values "normal")]
(d/nilv (unchecked-get values (d/name blend-mode)) default)))
(defn translate-constraint-h
[type]
(let [values (unchecked-get wasm/serializers "constraint-h")
default 5] ;; TODO: fix code in rust so we have a proper None variant
(d/nilv (unchecked-get values (d/name type)) default)))
(defn translate-constraint-v
[type]
(let [values (unchecked-get wasm/serializers "constraint-v")
default 5] ;; TODO: fix code in rust so we have a proper None variant
(d/nilv (unchecked-get values (d/name type)) default)))
(defn translate-bool-type
[bool-type]
(let [values (unchecked-get wasm/serializers "bool-type")
default (unchecked-get values "union")]
(d/nilv (unchecked-get values (d/name bool-type)) default)))
(defn translate-blur-type
[blur-type]
(let [values (unchecked-get wasm/serializers "blur-type")
default (unchecked-get values "layer-blur")]
(d/nilv (unchecked-get values (d/name blur-type)) default)))
(defn translate-raster-format
"Export image format keyword (:png/:jpeg/:webp) -> `RasterFormat` code.
Unlike the other translators this one has NO default: falling back to png
would emit png bytes under a jpeg/webp mtype, which is exactly what the
explicit format threading exists to prevent."
[format]
(let [values (unchecked-get wasm/serializers "raster-format")]
(or (unchecked-get values (d/name format))
(throw (ex-info "unsupported raster format" {:format format})))))
(defn translate-layout-flex-dir
[flex-dir]
(let [values (unchecked-get wasm/serializers "flex-direction")]
(unchecked-get values (d/name flex-dir))))
(defn translate-layout-grid-dir
[grid-dir]
(let [values (unchecked-get wasm/serializers "grid-direction")]
(unchecked-get values (d/name grid-dir))))
(defn translate-layout-align-items
[align-items]
(let [values (unchecked-get wasm/serializers "align-items")
default (unchecked-get values "start")]
(d/nilv (unchecked-get values (d/name align-items)) default)))
(defn translate-layout-align-content
[align-content]
(let [values (unchecked-get wasm/serializers "align-content")
default (unchecked-get values "stretch")]
(d/nilv (unchecked-get values (d/name align-content)) default)))
(defn translate-layout-justify-items
[justify-items]
(let [values (unchecked-get wasm/serializers "justify-items")
default (unchecked-get values "start")]
(d/nilv (unchecked-get values (d/name justify-items)) default)))
(defn translate-layout-justify-content
[justify-content]
(let [values (unchecked-get wasm/serializers "justify-content")
default (unchecked-get values "stretch")]
(d/nilv (unchecked-get values (d/name justify-content)) default)))
(defn translate-layout-wrap-type
[wrap-type]
(let [values (unchecked-get wasm/serializers "wrap-type")
default (unchecked-get values "nowrap")]
(d/nilv (unchecked-get values (d/name wrap-type)) default)))
(defn translate-grid-track-type
[type]
(let [values (unchecked-get wasm/serializers "grid-track-type")]
(unchecked-get values (d/name type))))
(defn translate-layout-sizing
[sizing]
(let [values (unchecked-get wasm/serializers "sizing")
default (unchecked-get values "fix")]
(d/nilv (unchecked-get values (d/name sizing)) default)))
(defn translate-align-self
[align-self]
(let [values (unchecked-get wasm/serializers "align-self")
default (unchecked-get values "none")]
(d/nilv (unchecked-get values (d/name align-self)) default)))
(defn translate-justify-self
[justify-self]
(let [values (unchecked-get wasm/serializers "justify-self")
default (unchecked-get values "none")]
(d/nilv (unchecked-get values (d/name justify-self)) default)))
(defn translate-shadow-style
[style]
(let [values (unchecked-get wasm/serializers "shadow-style")
default (unchecked-get values "drop-shadow")]
(d/nilv (unchecked-get values (d/name style)) default)))
;; TODO: Find/Create a Rust enum for this
(defn translate-structure-modifier-type
[type]
(case type
:remove-children 1
:add-children 2
:scale-content 3))
(def ^:private reverse-enum
(memoize
(fn [id]
(let [values (unchecked-get wasm/serializers id)]
(reduce (fn [acc k] (assoc acc (unchecked-get values k) k))
{}
(js/Object.keys values))))))
(defn- untranslate
"Reverse of `translate`: maps a wasm numeric discriminant back to its name.
Keywords (eg. `:multiple`) pass through untouched."
[id value default]
(if (keyword? value)
value
(d/nilv (get (reverse-enum id) value) default)))
(defn translate-grow-type
[grow-type]
(let [values (unchecked-get wasm/serializers "grow-type")
default (unchecked-get values "fixed")]
(d/nilv (unchecked-get values (d/name grow-type)) default)))
(defn translate-vertical-align
[vertical-align]
(let [values (unchecked-get wasm/serializers "vertical-align")
default (unchecked-get values "top")]
(d/nilv (unchecked-get values (d/name vertical-align)) default)))
(defn untranslate-vertical-align
[vertical-align]
(untranslate "vertical-align" vertical-align "top"))
(defn translate-text-align
[text-align]
(let [values (unchecked-get wasm/serializers "text-align")
default (unchecked-get values "left")]
(d/nilv (unchecked-get values (d/name text-align)) default)))
(defn untranslate-text-align
[text-align]
(untranslate "text-align" text-align "left"))
;; TODO: Find/Create a Rust enum for this
(defn translate-text-transform
[text-transform]
(let [values (unchecked-get wasm/serializers "text-transform")
default (unchecked-get values "none")]
(d/nilv (unchecked-get values (d/name text-transform)) default)))
(defn untranslate-text-transform
[text-transform]
(untranslate "text-transform" text-transform "none"))
(defn translate-text-decoration
[text-decoration]
(let [values (unchecked-get wasm/serializers "text-decoration")
default (unchecked-get values "none")]
(d/nilv (unchecked-get values (d/name text-decoration)) default)))
(defn untranslate-text-decoration
[text-decoration]
(untranslate "text-decoration" text-decoration "none"))
(defn translate-text-direction
[text-direction]
(let [values (unchecked-get wasm/serializers "text-direction")
default (unchecked-get values "ltr")]
(d/nilv (unchecked-get values (d/name text-direction)) default)))
(defn untranslate-text-direction
[text-direction]
(untranslate "text-direction" text-direction "ltr"))
(defn translate-font-style
[font-style]
(let [values (unchecked-get wasm/serializers "font-style")
default (unchecked-get values "normal")]
(case font-style
;; NOTE: normal == regular!
;; is it OK to keep those two values in our cljs model?
"normal" (unchecked-get values "normal")
"regular" (unchecked-get values "normal")
"italic" (unchecked-get values "italic")
default)))
(defn untranslate-font-style
[font-style]
(untranslate "font-style" font-style "normal"))
(defn translate-browser
[browser]
(case browser
:firefox 0
:chrome 1
:safari 2
:safari-16 2
:safari-17 2
:edge 3
:unknown 4
4))
(defn translate-transform-entry-kind [kind]
(let [values (unchecked-get wasm/serializers "transform-entry-kind")
default (unchecked-get values "parent")]
(d/nilv (unchecked-get values (d/name kind)) default)))
(defn translate-multiple-state
[multiple-state]
(let [values (unchecked-get wasm/serializers "multiple-state")
default (unchecked-get values "undefined")]
(d/nilv (unchecked-get values (d/name multiple-state)) default)))
;; --- Guides
;; Each guide is serialized as 5 x 32-bit words:
;; kind (u32) | color (u32 argb) | position (f32) | frame-start (f32) | frame-end (f32)
;; `frame-start`/`frame-end` hold the board clip range (along the guide's line
;; direction); they are NaN when the guide is not bound to a board.
(def ^:private guide-entry-size 20)
;; 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)
;; Sentinel clip range for a guide whose board is rotated or not a root frame.
;; The engine clips each guide to `[start end]` and skips drawing when `start >
;; end`; both bounds at +Infinity collapse to an empty range, so the guide is
;; hidden (matching the SVG renderer, which drops it from the DOM).
;; We hide via the range rather than by removing the guide from the set so the
;; guide count stays stable and board rendering is unaffected.
(def ^:private guide-hidden-range [js/Infinity js/Infinity])
(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 (or guides {})) guide-entry-size)))
(defn- guide-frame-range
"Returns the `[start end]` clip range (along the guide's line direction) for a
board-bound guide: the board's y-range for vertical `:x` guides, its x-range
for horizontal `:y` guides. Returns nil for free guides (drawn full-length).
A guide bound to a rotated or non-root board returns `guide-hidden-range`, an
empty range the engine clips out, hiding it like the SVG renderer does."
[guide objects]
(when-let [frame (some->> (get guide :frame-id) (get objects))]
(if (and (cfh/root-frame? frame)
(not (ctst/rotated-frame? frame)))
(if (= :x (get guide :axis))
[(:y frame) (+ (:y frame) (:height frame))]
[(:x frame) (+ (:x frame) (:width frame))])
guide-hidden-range)))
(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 | frame-start | frame-end` per guide. The frame
range is resolved from `objects` and written as NaN for free guides."
[guides objects heapu32 heapf32 offset]
(let [guides (vec (vals (or guides {})))
total (count guides)]
(aset heapu32 offset total)
(loop [i 0]
(when (< i total)
(let [guide (nth guides i)
base (+ offset 1 (* i 5))
[frame-start frame-end] (guide-frame-range guide objects)]
(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))
(aset heapf32 (+ base 3) (or frame-start js/NaN))
(aset heapf32 (+ base 4) (or frame-end js/NaN)))
(recur (inc i))))))