mirror of
https://github.com/penpot/penpot.git
synced 2026-09-30 15:56:17 +00:00
🐛 Refactor batch serialization and fix derived svg-attrs in exporter (#11909)
* ♻️ Share structural batch upload through common helper - Merge svg-filters and svg-fills to app.common.render-wasm.svg-derived - Add serialize-shapes-batch! in common, shared by the sync and chunked workspace paths - Add a routing test for the helper and wires the svg-filters test. AI-assisted-by: muse-spark, GLM 5.3 * 🐛 Derive SVG effects inside single-shape serializer - Single and batch paths: one svg effect derivation step owned by shared serializers. - set-object forwards the derived shape to its host attrs, and the exporter reads the derived fills, so SVG-attr fills, blur and shadow render as in the frontend. - Adds regression test to the exporter. AI-assisted-by: muse-spark, GLM 5.3, deepseek-flash
This commit is contained in:
parent
0255bed6c4
commit
fa81a3f648
@ -25,16 +25,52 @@
|
|||||||
dispatching per changed key through the same underlying `props` setters."
|
dispatching per changed key through the same underlying `props` setters."
|
||||||
(:require
|
(:require
|
||||||
[app.common.render-wasm.api.props :as props]
|
[app.common.render-wasm.api.props :as props]
|
||||||
[app.common.render-wasm.api.upload :as upload]))
|
[app.common.render-wasm.api.upload :as upload]
|
||||||
|
[app.common.render-wasm.svg-derived :as svg-derived]))
|
||||||
|
|
||||||
|
(defn needs-shape-tail?
|
||||||
|
"True when `shape` needs the per-shape svg-attrs/path tail after a
|
||||||
|
structural upload: svg-attrs present, or `:path`/`:bool` with content."
|
||||||
|
[shape]
|
||||||
|
(let [type (:type shape)]
|
||||||
|
(or (some? (:svg-attrs shape))
|
||||||
|
(and (contains? #{:path :bool} type)
|
||||||
|
(some? (:content shape))))))
|
||||||
|
|
||||||
|
(defn- write-shape-tail!
|
||||||
|
"Per-shape svg-attrs/path tail after a structural upload."
|
||||||
|
[shape]
|
||||||
|
(when (some? (:svg-attrs shape))
|
||||||
|
(props/set-shape-svg-attrs (:svg-attrs shape)))
|
||||||
|
|
||||||
|
(let [type (:type shape)]
|
||||||
|
(when (and (contains? #{:path :bool} type) (some? (:content shape)))
|
||||||
|
(props/set-shape-path-content (:content shape)))))
|
||||||
|
|
||||||
(defn serialize-shape!
|
(defn serialize-shape!
|
||||||
"Applies every host-independent WASM property of `shape`."
|
"Applies every host-independent WASM property of `shape`."
|
||||||
[shape]
|
[shape]
|
||||||
(let [type (get shape :type)]
|
(let [shape (svg-derived/apply-svg-derived shape)]
|
||||||
(upload/set-shape-upload! shape {:include-layout? false})
|
(upload/set-shape-upload! shape {:include-layout? false})
|
||||||
|
|
||||||
(when (some? (get shape :svg-attrs))
|
(when (needs-shape-tail? shape)
|
||||||
(props/set-shape-svg-attrs (get shape :svg-attrs)))
|
(write-shape-tail! shape))
|
||||||
|
|
||||||
(when (and (contains? #{:path :bool} type) (some? (get shape :content)))
|
shape))
|
||||||
(props/set-shape-path-content (get shape :content)))))
|
|
||||||
|
(defn serialize-shapes-batch!
|
||||||
|
"Structural batch upload plus per-shape svg-attrs/path tail.
|
||||||
|
|
||||||
|
Derives via svg-derived, uploads one `_set_shapes_batch` with `opts`,
|
||||||
|
then selects each shape needing svg-attrs/path content and applies it.
|
||||||
|
Host text/grid/image sequencing stays in callers. Returns the prepared
|
||||||
|
vector for the downstream host-attrs loop."
|
||||||
|
[shapes opts select-fn]
|
||||||
|
(let [prepared (mapv svg-derived/apply-svg-derived shapes)]
|
||||||
|
(when (seq prepared)
|
||||||
|
(upload/flush-shapes-batch! prepared opts)
|
||||||
|
(doseq [shape prepared]
|
||||||
|
(when (needs-shape-tail? shape)
|
||||||
|
(select-fn (:id shape))
|
||||||
|
(write-shape-tail! shape))))
|
||||||
|
prepared))
|
||||||
|
|||||||
@ -4,7 +4,8 @@
|
|||||||
;;
|
;;
|
||||||
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
;; Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||||
|
|
||||||
(ns app.render-wasm.svg-fills
|
(ns app.common.render-wasm.svg-derived
|
||||||
|
"SVG-derived WASM attrs (fills, blur, shadows), shared by frontend and export"
|
||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.data.macros :as dm]
|
[app.common.data.macros :as dm]
|
||||||
@ -12,6 +13,7 @@
|
|||||||
[app.common.geom.rect :as grc]
|
[app.common.geom.rect :as grc]
|
||||||
[app.common.svg :as csvg]
|
[app.common.svg :as csvg]
|
||||||
[app.common.types.color :as clr]
|
[app.common.types.color :as clr]
|
||||||
|
[app.common.uuid :as uuid]
|
||||||
[clojure.string :as str]))
|
[clojure.string :as str]))
|
||||||
|
|
||||||
(def ^:private url-fill-pattern
|
(def ^:private url-fill-pattern
|
||||||
@ -380,3 +382,86 @@
|
|||||||
[{:fill-color "#000000" :fill-opacity 1}]
|
[{:fill-color "#000000" :fill-opacity 1}]
|
||||||
:else [])))
|
:else [])))
|
||||||
|
|
||||||
|
(def ^:private drop-shadow-tags
|
||||||
|
#{:feOffset :feGaussianBlur :feColorMatrix})
|
||||||
|
|
||||||
|
(defn- find-filter-element
|
||||||
|
"Finds a filter element by tag in filter content."
|
||||||
|
[filter-content tag]
|
||||||
|
(some #(when (= tag (:tag %)) %) filter-content))
|
||||||
|
|
||||||
|
(defn- find-filter-def
|
||||||
|
[shape]
|
||||||
|
(let [filter-attr (or (dm/get-in shape [:svg-attrs :filter])
|
||||||
|
(dm/get-in shape [:svg-attrs :style :filter]))
|
||||||
|
svg-defs (dm/get-prop shape :svg-defs)]
|
||||||
|
(when (and filter-attr svg-defs)
|
||||||
|
(let [filter-ids (csvg/extract-ids filter-attr)]
|
||||||
|
(some #(get svg-defs %) filter-ids)))))
|
||||||
|
|
||||||
|
(defn- build-blur
|
||||||
|
[gaussian-blur]
|
||||||
|
(when gaussian-blur
|
||||||
|
{:id (uuid/next)
|
||||||
|
:type :layer-blur
|
||||||
|
;; For layer blur the value matches stdDeviation directly
|
||||||
|
:value (-> (dm/get-in gaussian-blur [:attrs :stdDeviation])
|
||||||
|
(d/parse-double 0))
|
||||||
|
:hidden false}))
|
||||||
|
|
||||||
|
(defn- build-drop-shadow
|
||||||
|
[filter-content drop-shadow-elements]
|
||||||
|
(let [offset-elem (find-filter-element filter-content :feOffset)]
|
||||||
|
(when (and offset-elem (seq drop-shadow-elements))
|
||||||
|
(let [blur-elem (find-filter-element drop-shadow-elements :feGaussianBlur)
|
||||||
|
dx (-> (dm/get-in offset-elem [:attrs :dx])
|
||||||
|
(d/parse-double 0))
|
||||||
|
dy (-> (dm/get-in offset-elem [:attrs :dy])
|
||||||
|
(d/parse-double 0))
|
||||||
|
blur-value (if blur-elem
|
||||||
|
(-> (dm/get-in blur-elem [:attrs :stdDeviation])
|
||||||
|
(d/parse-double 0)
|
||||||
|
(* 2))
|
||||||
|
0)]
|
||||||
|
[{:id (uuid/next)
|
||||||
|
:style :drop-shadow
|
||||||
|
:offset-x dx
|
||||||
|
:offset-y dy
|
||||||
|
:blur blur-value
|
||||||
|
:spread 0
|
||||||
|
:hidden false
|
||||||
|
;; TODO: parse feColorMatrix to extract color/opacity
|
||||||
|
:color {:color "#000000" :opacity 1}}]))))
|
||||||
|
|
||||||
|
(defn apply-svg-filters
|
||||||
|
"Derives native blur/shadow from SVG filter definitions when the shape does
|
||||||
|
not already have them. The SVG attributes are left untouched so SVG fallback
|
||||||
|
rendering keeps working the same way as gradient fills."
|
||||||
|
[shape]
|
||||||
|
(let [existing-blur (:blur shape)
|
||||||
|
existing-shadow (:shadow shape)]
|
||||||
|
(if-let [filter-def (find-filter-def shape)]
|
||||||
|
(let [content (:content filter-def)
|
||||||
|
gaussian-blur (find-filter-element content :feGaussianBlur)
|
||||||
|
drop-shadow-elements (filter #(contains? drop-shadow-tags (:tag %)) content)
|
||||||
|
blur (or existing-blur (build-blur gaussian-blur))
|
||||||
|
shadow (if (seq existing-shadow)
|
||||||
|
existing-shadow
|
||||||
|
(build-drop-shadow content drop-shadow-elements))]
|
||||||
|
(cond-> shape
|
||||||
|
blur (assoc :blur blur)
|
||||||
|
(seq shadow) (assoc :shadow shadow)))
|
||||||
|
shape)))
|
||||||
|
|
||||||
|
(defn apply-svg-derived
|
||||||
|
"Applies SVG-derived effects (fills, blur, shadows) uniformly.
|
||||||
|
- Keeps user fills if present; otherwise derives from SVG.
|
||||||
|
- Converts SVG filters into native blur/shadow when needed.
|
||||||
|
- Always returns shape with :fills (possibly []) and blur/shadow keys."
|
||||||
|
[shape]
|
||||||
|
(let [shape' (apply-svg-filters shape)
|
||||||
|
fills (or (resolve-shape-fills shape') [])]
|
||||||
|
(assoc shape'
|
||||||
|
:fills fills
|
||||||
|
:blur (:blur shape')
|
||||||
|
:shadow (:shadow shape'))))
|
||||||
@ -2,4 +2,7 @@
|
|||||||
|
|
||||||
set -ex
|
set -ex
|
||||||
pnpm install;
|
pnpm install;
|
||||||
|
pushd ../render-wasm;
|
||||||
|
./build export;
|
||||||
|
popd
|
||||||
pnpm run test;
|
pnpm run test;
|
||||||
|
|||||||
@ -28,13 +28,13 @@
|
|||||||
host-specific parts are handled here: fills/strokes (image bytes are provisioned
|
host-specific parts are handled here: fills/strokes (image bytes are provisioned
|
||||||
separately) and text content (fonts provisioned separately)."
|
separately) and text content (fonts provisioned separately)."
|
||||||
[shape]
|
[shape]
|
||||||
(let [type (get shape :type)]
|
(let [prepared (serialize-shape/serialize-shape! shape)
|
||||||
(serialize-shape/serialize-shape! shape)
|
type (:type prepared)]
|
||||||
(props/write-shape-fills! (get shape :fills))
|
(props/write-shape-fills! (:fills prepared))
|
||||||
(when-not (= type :group)
|
(when-not (= type :group)
|
||||||
(props/write-shape-strokes! (get shape :strokes)))
|
(props/write-shape-strokes! (:strokes prepared)))
|
||||||
(when (= type :text)
|
(when (= type :text)
|
||||||
(text/set-shape-text! (get shape :content)))))
|
(text/set-shape-text! (:content prepared)))))
|
||||||
|
|
||||||
(defn serialize-scene!
|
(defn serialize-scene!
|
||||||
"Loads every shape of an `objects` map into the WASM design state. Resets the
|
"Loads every shape of an `objects` map into the WASM design state. Resets the
|
||||||
|
|||||||
@ -7,7 +7,11 @@
|
|||||||
(ns exporter-tests.renderer-svg-test
|
(ns exporter-tests.renderer-svg-test
|
||||||
(:require
|
(:require
|
||||||
[app.renderer.svg-gradient :as svg-gradient]
|
[app.renderer.svg-gradient :as svg-gradient]
|
||||||
[cljs.test :refer [deftest is testing]]))
|
[app.wasm :as wasm-io]
|
||||||
|
[app.wasm.serialize :as serialize]
|
||||||
|
[cljs.test :refer [deftest is testing async]]
|
||||||
|
[clojure.string :as str]
|
||||||
|
[promesa.core :as p]))
|
||||||
|
|
||||||
(def gradient-stops
|
(def gradient-stops
|
||||||
[{"color" "#000000" "offset" 0 "opacity" 1}
|
[{"color" "#000000" "offset" 0 "opacity" 1}
|
||||||
@ -23,3 +27,46 @@
|
|||||||
"stops" gradient-stops}}
|
"stops" gradient-stops}}
|
||||||
result (svg-gradient/data->gradient-def "text-id" ["#000001" gradient-data])]
|
result (svg-gradient/data->gradient-def "text-id" ["#000001" gradient-data])]
|
||||||
(is (= element-name (get result "name")))))))
|
(is (= element-name (get result "name")))))))
|
||||||
|
|
||||||
|
|
||||||
|
(def ^:private svg-red-id (random-uuid))
|
||||||
|
(def ^:private blue-id (random-uuid))
|
||||||
|
|
||||||
|
(defn- scene []
|
||||||
|
{svg-red-id {:id svg-red-id
|
||||||
|
:type :rect
|
||||||
|
:name "svg-red"
|
||||||
|
:x 10 :y 10 :width 100 :height 100
|
||||||
|
:rotation 0
|
||||||
|
:selrect {:x 10 :y 10 :x1 10 :y1 10 :x2 110 :y2 110
|
||||||
|
:width 100 :height 100}
|
||||||
|
:strokes []
|
||||||
|
:svg-attrs {:fill "#ff0000"}}
|
||||||
|
blue-id {:id blue-id
|
||||||
|
:type :rect
|
||||||
|
:name "user-blue"
|
||||||
|
:x 130 :y 10 :width 100 :height 100
|
||||||
|
:rotation 0
|
||||||
|
:selrect {:x 130 :y 10 :x1 130 :y1 10 :x2 230 :y2 110
|
||||||
|
:width 100 :height 100}
|
||||||
|
:strokes []
|
||||||
|
:fills [{:fill-color "#0000ff" :fill-opacity 1}]}})
|
||||||
|
|
||||||
|
(defn- render-svg-string
|
||||||
|
[shape-id]
|
||||||
|
(.toString (js/Buffer.from (wasm-io/render-shape-svg shape-id 1))))
|
||||||
|
|
||||||
|
(deftest exporter-honours-svg-attr-fills
|
||||||
|
(async done
|
||||||
|
(->> (wasm-io/init!)
|
||||||
|
(p/mcat
|
||||||
|
(fn [_]
|
||||||
|
(serialize/serialize-scene! (scene))
|
||||||
|
(let [red (render-svg-string svg-red-id)
|
||||||
|
blue (render-svg-string blue-id)]
|
||||||
|
(is (str/includes? blue "blue")
|
||||||
|
"control: user fills render, so the harness works")
|
||||||
|
(is (str/includes? red "fill=\"red\"")
|
||||||
|
"svg-attr fill survives headless serialization")
|
||||||
|
(p/resolved nil))))
|
||||||
|
(p/mcat (fn [_] (done))))))
|
||||||
|
|||||||
@ -10,6 +10,7 @@
|
|||||||
[app.common.data.macros :as dm]
|
[app.common.data.macros :as dm]
|
||||||
[app.common.files.helpers :as cfh]
|
[app.common.files.helpers :as cfh]
|
||||||
[app.common.geom.point :as gpt]
|
[app.common.geom.point :as gpt]
|
||||||
|
[app.common.render-wasm.svg-derived :as svg-derived]
|
||||||
[app.common.types.path :as path]
|
[app.common.types.path :as path]
|
||||||
[app.common.types.path.helpers :as path.helpers]
|
[app.common.types.path.helpers :as path.helpers]
|
||||||
[app.main.data.helpers :as dsh]
|
[app.main.data.helpers :as dsh]
|
||||||
@ -22,7 +23,6 @@
|
|||||||
[app.main.data.workspace.path.tools :as tools]
|
[app.main.data.workspace.path.tools :as tools]
|
||||||
[app.main.data.workspace.path.undo :as undo]
|
[app.main.data.workspace.path.undo :as undo]
|
||||||
[app.main.streams :as ms]
|
[app.main.streams :as ms]
|
||||||
[app.render-wasm.svg-fills :as svg-fills]
|
|
||||||
[app.util.mouse :as mse]
|
[app.util.mouse :as mse]
|
||||||
[beicon.v2.core :as rx]
|
[beicon.v2.core :as rx]
|
||||||
[beicon.v2.operators :as rxo]
|
[beicon.v2.operators :as rxo]
|
||||||
@ -885,7 +885,7 @@
|
|||||||
"Resolves the fills inherited by the editing copy.
|
"Resolves the fills inherited by the editing copy.
|
||||||
Frames stop group fill inheritance."
|
Frames stop group fill inheritance."
|
||||||
[shape objects]
|
[shape objects]
|
||||||
(let [own (svg-fills/resolve-shape-fills shape)]
|
(let [own (svg-derived/resolve-shape-fills shape)]
|
||||||
(if (seq own)
|
(if (seq own)
|
||||||
own
|
own
|
||||||
(loop [parent-id (:parent-id shape)
|
(loop [parent-id (:parent-id shape)
|
||||||
@ -897,7 +897,7 @@
|
|||||||
(let [parent (get objects parent-id)]
|
(let [parent (get objects parent-id)]
|
||||||
(cond
|
(cond
|
||||||
(nil? parent) []
|
(nil? parent) []
|
||||||
(cfh/group-shape? parent) (let [fills (svg-fills/resolve-shape-fills parent)]
|
(cfh/group-shape? parent) (let [fills (svg-derived/resolve-shape-fills parent)]
|
||||||
(if (seq fills)
|
(if (seq fills)
|
||||||
fills
|
fills
|
||||||
(recur (:parent-id parent)
|
(recur (:parent-id parent)
|
||||||
|
|||||||
@ -17,7 +17,6 @@
|
|||||||
[app.common.logging :as log]
|
[app.common.logging :as log]
|
||||||
[app.common.math :as mth]
|
[app.common.math :as mth]
|
||||||
[app.common.render-wasm.api.props :as props]
|
[app.common.render-wasm.api.props :as props]
|
||||||
[app.common.render-wasm.api.upload :as upload]
|
|
||||||
[app.common.render-wasm.helpers :as h]
|
[app.common.render-wasm.helpers :as h]
|
||||||
[app.common.render-wasm.mem :as mem]
|
[app.common.render-wasm.mem :as mem]
|
||||||
[app.common.render-wasm.mem.heap32 :as mem.h32]
|
[app.common.render-wasm.mem.heap32 :as mem.h32]
|
||||||
@ -50,7 +49,6 @@
|
|||||||
[app.render-wasm.gesture :as wasm-gesture]
|
[app.render-wasm.gesture :as wasm-gesture]
|
||||||
[app.render-wasm.performance :as perf]
|
[app.render-wasm.performance :as perf]
|
||||||
[app.render-wasm.rulers-state :as rulers-state]
|
[app.render-wasm.rulers-state :as rulers-state]
|
||||||
[app.render-wasm.svg-filters :as svg-filters]
|
|
||||||
[app.render-wasm.text-editor :as text-editor]
|
[app.render-wasm.text-editor :as text-editor]
|
||||||
[app.util.debug :as dbg]
|
[app.util.debug :as dbg]
|
||||||
[app.util.dom :as dom]
|
[app.util.dom :as dom]
|
||||||
@ -1645,11 +1643,10 @@
|
|||||||
{:thumbnails [] :full [] :font-face-keys #{} :pending-font-face-keys #{}}
|
{:thumbnails [] :full [] :font-face-keys #{} :pending-font-face-keys #{}}
|
||||||
(do
|
(do
|
||||||
(perf/begin-measure "set-object")
|
(perf/begin-measure "set-object")
|
||||||
(let [shape (svg-filters/apply-svg-derived shape)]
|
(let [prepared (serialize-shape/serialize-shape! shape)
|
||||||
(serialize-shape/serialize-shape! shape)
|
result (set-object-host-attrs prepared false)]
|
||||||
(let [result (set-object-host-attrs shape false)]
|
(perf/end-measure "set-object")
|
||||||
(perf/end-measure "set-object")
|
result))))
|
||||||
result)))))
|
|
||||||
|
|
||||||
(defn- update-text-layouts
|
(defn- update-text-layouts
|
||||||
"Synchronously update text layouts for all shapes and send rect updates
|
"Synchronously update text layouts for all shapes and send rect updates
|
||||||
@ -1814,24 +1811,11 @@
|
|||||||
end-index (min total (+ start-index BATCH_MAX_SHAPES))
|
end-index (min total (+ start-index BATCH_MAX_SHAPES))
|
||||||
chunk (into [] (subvec (if (vector? shapes) shapes (vec shapes))
|
chunk (into [] (subvec (if (vector? shapes) shapes (vec shapes))
|
||||||
start-index end-index))
|
start-index end-index))
|
||||||
prepared (mapv svg-filters/apply-svg-derived chunk)]
|
prepared (serialize-shape/serialize-shapes-batch!
|
||||||
|
chunk
|
||||||
;; One multi-shape structural upload (base+children+blur+shadows+flex+item+fills+strokes).
|
{:include-layout? true
|
||||||
(when (seq prepared)
|
:include-fills-strokes? true}
|
||||||
(upload/flush-shapes-batch! prepared {:include-layout? true
|
use-shape)]
|
||||||
:include-fills-strokes? true}))
|
|
||||||
|
|
||||||
;; Path + svg-attrs still need the legacy per-shape path (variable/large).
|
|
||||||
(doseq [shape prepared]
|
|
||||||
(let [id (dm/get-prop shape :id)
|
|
||||||
type (dm/get-prop shape :type)]
|
|
||||||
(when (or (some? (get shape :svg-attrs))
|
|
||||||
(and (contains? #{:path :bool} type) (some? (get shape :content))))
|
|
||||||
(use-shape id)
|
|
||||||
(when (some? (get shape :svg-attrs))
|
|
||||||
(props/set-shape-svg-attrs (get shape :svg-attrs)))
|
|
||||||
(when (and (contains? #{:path :bool} type) (some? (get shape :content)))
|
|
||||||
(props/set-shape-path-content (get shape :content))))))
|
|
||||||
|
|
||||||
(loop [xs prepared
|
(loop [xs prepared
|
||||||
t-acc (transient thumbnails-acc)
|
t-acc (transient thumbnails-acc)
|
||||||
@ -1977,53 +1961,44 @@
|
|||||||
(defn- set-objects-sync
|
(defn- set-objects-sync
|
||||||
"Synchronously process all shapes (for small shape counts)."
|
"Synchronously process all shapes (for small shape counts)."
|
||||||
[shapes render-callback on-shapes-ready]
|
[shapes render-callback on-shapes-ready]
|
||||||
(let [prepared (mapv svg-filters/apply-svg-derived shapes)]
|
(let [prepared (serialize-shape/serialize-shapes-batch!
|
||||||
(when (seq prepared)
|
shapes
|
||||||
(upload/flush-shapes-batch! prepared {:include-layout? true
|
{:include-layout? true
|
||||||
:include-fills-strokes? true}))
|
:include-fills-strokes? true}
|
||||||
(doseq [shape prepared]
|
use-shape)
|
||||||
(let [id (dm/get-prop shape :id)
|
total-shapes (count prepared)
|
||||||
type (dm/get-prop shape :type)]
|
{:keys [thumbnails full text-font-state]}
|
||||||
(when (or (some? (get shape :svg-attrs))
|
(loop [index 0
|
||||||
(and (contains? #{:path :bool} type) (some? (get shape :content))))
|
thumbnails-acc (transient [])
|
||||||
(use-shape id)
|
full-acc (transient [])
|
||||||
(when (some? (get shape :svg-attrs))
|
font-state-acc empty-text-font-state]
|
||||||
(props/set-shape-svg-attrs (get shape :svg-attrs)))
|
(if (< index total-shapes)
|
||||||
(when (and (contains? #{:path :bool} type) (some? (get shape :content)))
|
(let [shape (nth prepared index)
|
||||||
(props/set-shape-path-content (get shape :content))))))
|
{:keys [thumbnails full font-face-keys pending-font-face-keys]}
|
||||||
(let [total-shapes (count prepared)
|
(set-object-host-attrs shape true :skip-fills-strokes? true)]
|
||||||
{:keys [thumbnails full text-font-state]}
|
(recur (inc index)
|
||||||
(loop [index 0
|
(reduce conj! thumbnails-acc thumbnails)
|
||||||
thumbnails-acc (transient [])
|
(reduce conj! full-acc full)
|
||||||
full-acc (transient [])
|
(acc-text-font-state font-state-acc
|
||||||
font-state-acc empty-text-font-state]
|
(:id shape)
|
||||||
(if (< index total-shapes)
|
font-face-keys
|
||||||
(let [shape (nth prepared index)
|
pending-font-face-keys)))
|
||||||
{:keys [thumbnails full font-face-keys pending-font-face-keys]}
|
{:thumbnails (persistent! thumbnails-acc)
|
||||||
(set-object-host-attrs shape true :skip-fills-strokes? true)]
|
:full (persistent! full-acc)
|
||||||
(recur (inc index)
|
:text-font-state font-state-acc}))]
|
||||||
(reduce conj! thumbnails-acc thumbnails)
|
(perf/end-measure "set-objects")
|
||||||
(reduce conj! full-acc full)
|
(when on-shapes-ready (on-shapes-ready))
|
||||||
(acc-text-font-state font-state-acc
|
(when (wasm/live?)
|
||||||
(:id shape)
|
;; Rebuild the tile index so _render knows which shapes
|
||||||
font-face-keys
|
;; map to which tiles after a page switch.
|
||||||
pending-font-face-keys)))
|
(h/call wasm/internal-module "_set_view_end")
|
||||||
{:thumbnails (persistent! thumbnails-acc)
|
(reset! view-interaction-active? false)
|
||||||
:full (persistent! full-acc)
|
(process-pending shapes thumbnails full text-font-state
|
||||||
:text-font-state font-state-acc}))]
|
(fn []
|
||||||
(perf/end-measure "set-objects")
|
(if render-callback
|
||||||
(when on-shapes-ready (on-shapes-ready))
|
(render-callback)
|
||||||
(when (wasm/live?)
|
(request-render "set-objects-sync-complete"))
|
||||||
;; Rebuild the tile index so _render knows which shapes
|
(ug/dispatch! (ug/event "penpot:wasm:set-objects")))))))
|
||||||
;; map to which tiles after a page switch.
|
|
||||||
(h/call wasm/internal-module "_set_view_end")
|
|
||||||
(reset! view-interaction-active? false)
|
|
||||||
(process-pending shapes thumbnails full text-font-state
|
|
||||||
(fn []
|
|
||||||
(if render-callback
|
|
||||||
(render-callback)
|
|
||||||
(request-render "set-objects-sync-complete"))
|
|
||||||
(ug/dispatch! (ug/event "penpot:wasm:set-objects"))))))))
|
|
||||||
|
|
||||||
(defn- shapes-in-tree-order
|
(defn- shapes-in-tree-order
|
||||||
"Returns shapes sorted in tree order (parents before children).
|
"Returns shapes sorted in tree order (parents before children).
|
||||||
|
|||||||
@ -9,12 +9,12 @@
|
|||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.data.macros :as dm]
|
[app.common.data.macros :as dm]
|
||||||
[app.common.files.helpers :as cfh]
|
[app.common.files.helpers :as cfh]
|
||||||
|
[app.common.render-wasm.svg-derived :as svg-derived]
|
||||||
[app.common.transit :as t]
|
[app.common.transit :as t]
|
||||||
[app.common.types.shape :as shape]
|
[app.common.types.shape :as shape]
|
||||||
[app.common.types.shape.layout :as ctl]
|
[app.common.types.shape.layout :as ctl]
|
||||||
[app.main.refs :as refs]
|
[app.main.refs :as refs]
|
||||||
[app.render-wasm.api :as api]
|
[app.render-wasm.api :as api]
|
||||||
[app.render-wasm.svg-filters :as svg-filters]
|
|
||||||
[beicon.v2.core :as rx]
|
[beicon.v2.core :as rx]
|
||||||
[cljs.core :as c]
|
[cljs.core :as c]
|
||||||
[cuerdas.core :as str]))
|
[cuerdas.core :as str]))
|
||||||
@ -130,8 +130,8 @@
|
|||||||
[shape k]
|
[shape k]
|
||||||
(when (api/initialized?)
|
(when (api/initialized?)
|
||||||
(let [shape (case k
|
(let [shape (case k
|
||||||
:svg-attrs (svg-filters/apply-svg-derived (assoc shape :svg-attrs (get shape :svg-attrs)))
|
:svg-attrs (svg-derived/apply-svg-derived (assoc shape :svg-attrs (get shape :svg-attrs)))
|
||||||
(:fills :blur :shadow) (svg-filters/apply-svg-derived shape)
|
(:fills :blur :shadow) (svg-derived/apply-svg-derived shape)
|
||||||
shape)
|
shape)
|
||||||
v (get shape k)
|
v (get shape k)
|
||||||
id (get shape :id)]
|
id (get shape :id)]
|
||||||
|
|||||||
@ -1,98 +0,0 @@
|
|||||||
;; 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 SUBSIDIARY SL
|
|
||||||
|
|
||||||
(ns app.render-wasm.svg-filters
|
|
||||||
(:require
|
|
||||||
[app.common.data :as d]
|
|
||||||
[app.common.data.macros :as dm]
|
|
||||||
[app.common.svg :as csvg]
|
|
||||||
[app.common.uuid :as uuid]
|
|
||||||
[app.render-wasm.svg-fills :as svg-fills]))
|
|
||||||
|
|
||||||
(def ^:private drop-shadow-tags
|
|
||||||
#{:feOffset :feGaussianBlur :feColorMatrix})
|
|
||||||
|
|
||||||
(defn- find-filter-element
|
|
||||||
"Finds a filter element by tag in filter content."
|
|
||||||
[filter-content tag]
|
|
||||||
(some #(when (= tag (:tag %)) %) filter-content))
|
|
||||||
|
|
||||||
(defn- find-filter-def
|
|
||||||
[shape]
|
|
||||||
(let [filter-attr (or (dm/get-in shape [:svg-attrs :filter])
|
|
||||||
(dm/get-in shape [:svg-attrs :style :filter]))
|
|
||||||
svg-defs (dm/get-prop shape :svg-defs)]
|
|
||||||
(when (and filter-attr svg-defs)
|
|
||||||
(let [filter-ids (csvg/extract-ids filter-attr)]
|
|
||||||
(some #(get svg-defs %) filter-ids)))))
|
|
||||||
|
|
||||||
(defn- build-blur
|
|
||||||
[gaussian-blur]
|
|
||||||
(when gaussian-blur
|
|
||||||
{:id (uuid/next)
|
|
||||||
:type :layer-blur
|
|
||||||
;; For layer blur the value matches stdDeviation directly
|
|
||||||
:value (-> (dm/get-in gaussian-blur [:attrs :stdDeviation])
|
|
||||||
(d/parse-double 0))
|
|
||||||
:hidden false}))
|
|
||||||
|
|
||||||
(defn- build-drop-shadow
|
|
||||||
[filter-content drop-shadow-elements]
|
|
||||||
(let [offset-elem (find-filter-element filter-content :feOffset)]
|
|
||||||
(when (and offset-elem (seq drop-shadow-elements))
|
|
||||||
(let [blur-elem (find-filter-element drop-shadow-elements :feGaussianBlur)
|
|
||||||
dx (-> (dm/get-in offset-elem [:attrs :dx])
|
|
||||||
(d/parse-double 0))
|
|
||||||
dy (-> (dm/get-in offset-elem [:attrs :dy])
|
|
||||||
(d/parse-double 0))
|
|
||||||
blur-value (if blur-elem
|
|
||||||
(-> (dm/get-in blur-elem [:attrs :stdDeviation])
|
|
||||||
(d/parse-double 0)
|
|
||||||
(* 2))
|
|
||||||
0)]
|
|
||||||
[{:id (uuid/next)
|
|
||||||
:style :drop-shadow
|
|
||||||
:offset-x dx
|
|
||||||
:offset-y dy
|
|
||||||
:blur blur-value
|
|
||||||
:spread 0
|
|
||||||
:hidden false
|
|
||||||
;; TODO: parse feColorMatrix to extract color/opacity
|
|
||||||
:color {:color "#000000" :opacity 1}}]))))
|
|
||||||
|
|
||||||
(defn apply-svg-filters
|
|
||||||
"Derives native blur/shadow from SVG filter definitions when the shape does
|
|
||||||
not already have them. The SVG attributes are left untouched so SVG fallback
|
|
||||||
rendering keeps working the same way as gradient fills."
|
|
||||||
[shape]
|
|
||||||
(let [existing-blur (:blur shape)
|
|
||||||
existing-shadow (:shadow shape)]
|
|
||||||
(if-let [filter-def (find-filter-def shape)]
|
|
||||||
(let [content (:content filter-def)
|
|
||||||
gaussian-blur (find-filter-element content :feGaussianBlur)
|
|
||||||
drop-shadow-elements (filter #(contains? drop-shadow-tags (:tag %)) content)
|
|
||||||
blur (or existing-blur (build-blur gaussian-blur))
|
|
||||||
shadow (if (seq existing-shadow)
|
|
||||||
existing-shadow
|
|
||||||
(build-drop-shadow content drop-shadow-elements))]
|
|
||||||
(cond-> shape
|
|
||||||
blur (assoc :blur blur)
|
|
||||||
(seq shadow) (assoc :shadow shadow)))
|
|
||||||
shape)))
|
|
||||||
|
|
||||||
(defn apply-svg-derived
|
|
||||||
"Applies SVG-derived effects (fills, blur, shadows) uniformly.
|
|
||||||
- Keeps user fills if present; otherwise derives from SVG.
|
|
||||||
- Converts SVG filters into native blur/shadow when needed.
|
|
||||||
- Always returns shape with :fills (possibly []) and blur/shadow keys."
|
|
||||||
[shape]
|
|
||||||
(let [shape' (apply-svg-filters shape)
|
|
||||||
fills (or (svg-fills/resolve-shape-fills shape') [])]
|
|
||||||
(assoc shape'
|
|
||||||
:fills fills
|
|
||||||
:blur (:blur shape')
|
|
||||||
:shadow (:shadow shape'))))
|
|
||||||
|
|
||||||
164
frontend/test/frontend_tests/render_wasm/serialization_test.cljs
Normal file
164
frontend/test/frontend_tests/render_wasm/serialization_test.cljs
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
;; 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 SUBSIDIARY SL
|
||||||
|
|
||||||
|
(ns frontend-tests.render-wasm.serialization-test
|
||||||
|
"Routing tests for `serialize-shape!` and `serialize-shapes-batch!`.
|
||||||
|
|
||||||
|
A shared `tail-cases` table states, per shape, whether the svg-attrs/path
|
||||||
|
tail must fire. The pure `needs-shape-tail?` predicate is tested against
|
||||||
|
the table with no stubs; the two thin routing tests drive both serializers
|
||||||
|
from the same table with the FFI boundary stubbed. Guard drift between the
|
||||||
|
single and batch paths fails loudly. These tests assert routing only;
|
||||||
|
render output below the FFI line needs headed or exporter pixel runs."
|
||||||
|
(:require
|
||||||
|
[app.common.render-wasm.api.props :as props]
|
||||||
|
[app.common.render-wasm.api.upload :as upload]
|
||||||
|
[app.common.render-wasm.serialize-shape :as serialize-shape]
|
||||||
|
[app.common.render-wasm.svg-derived :as svg-derived]
|
||||||
|
[cljs.test :as t :include-macros true]))
|
||||||
|
|
||||||
|
(defn- with-ffi-stubs*
|
||||||
|
"Stubs the single-arity FFI boundary fns (`flush-shapes-batch!`,
|
||||||
|
`set-shape-svg-attrs`, `set-shape-path-content`). `set-shape-upload!` is
|
||||||
|
deliberately left real: it is a trivial one-liner over the stubbed flush,
|
||||||
|
so the single test exercises its actual delegation and default opts. A
|
||||||
|
plain fn `set!` onto a multi-arity var breaks under the `:esm` test build
|
||||||
|
(call sites dispatch via `cljs$core$IFn$_invoke$arity$N`; see `mock/stub`)."
|
||||||
|
[stubs thunk]
|
||||||
|
(let [orig-flush upload/flush-shapes-batch!
|
||||||
|
orig-svg-attrs props/set-shape-svg-attrs
|
||||||
|
orig-path props/set-shape-path-content]
|
||||||
|
(set! upload/flush-shapes-batch! (:flush stubs))
|
||||||
|
(set! props/set-shape-svg-attrs (:svg-attrs stubs))
|
||||||
|
(set! props/set-shape-path-content (:path stubs))
|
||||||
|
(try
|
||||||
|
(thunk)
|
||||||
|
(finally
|
||||||
|
(set! upload/flush-shapes-batch! orig-flush)
|
||||||
|
(set! props/set-shape-svg-attrs orig-svg-attrs)
|
||||||
|
(set! props/set-shape-path-content orig-path)))))
|
||||||
|
|
||||||
|
(def ^:private tail-cases
|
||||||
|
"Shared spec for the svg-attrs/path tail. Each entry states whether the
|
||||||
|
tail must fire (`:tail?`) and which writes it must perform (`:svg?`,
|
||||||
|
`:path?`). Tests for single and batch serialization share this table."
|
||||||
|
[{:shape {:id (random-uuid) :type :frame} :tail? false :svg? false :path? false}
|
||||||
|
{:shape {:id (random-uuid) :type :rect} :tail? false :svg? false :path? false}
|
||||||
|
{:shape {:id (random-uuid) :type :rect :svg-attrs {:fill "blue"}}
|
||||||
|
:tail? true :svg? true :path? false}
|
||||||
|
{:shape {:id (random-uuid) :type :group :svg-attrs {:fill "blue"}}
|
||||||
|
:tail? true :svg? true :path? false}
|
||||||
|
{:shape {:id (random-uuid) :type :text :svg-attrs {:fill "blue"}}
|
||||||
|
:tail? true :svg? true :path? false}
|
||||||
|
{:shape {:id (random-uuid) :type :path :content {:type :path-content}}
|
||||||
|
:tail? true :svg? false :path? true}
|
||||||
|
{:shape {:id (random-uuid) :type :path}
|
||||||
|
:tail? false :svg? false :path? false}
|
||||||
|
{:shape {:id (random-uuid) :type :path
|
||||||
|
:content {:type :path-content} :svg-attrs {:fill "red"}}
|
||||||
|
:tail? true :svg? true :path? true}
|
||||||
|
{:shape {:id (random-uuid) :type :bool :content {:type :bool-content}}
|
||||||
|
:tail? true :svg? false :path? true}
|
||||||
|
{:shape {:id (random-uuid) :type :bool}
|
||||||
|
:tail? false :svg? false :path? false}
|
||||||
|
{:shape {:id (random-uuid) :type :text :content {:type :text-content}}
|
||||||
|
:tail? false :svg? false :path? false}
|
||||||
|
;; svg-attrs fires the tail, but the inner type guard must still withhold
|
||||||
|
;; the path write from text content.
|
||||||
|
{:shape {:id (random-uuid) :type :text
|
||||||
|
:svg-attrs {:fill "blue"} :content {:type :text-content}}
|
||||||
|
:tail? true :svg? true :path? false}
|
||||||
|
;; svg-attrs alone fires the tail for path shapes; the svg write must not
|
||||||
|
;; hide under the content check.
|
||||||
|
{:shape {:id (random-uuid) :type :path :svg-attrs {:fill "red"}}
|
||||||
|
:tail? true :svg? true :path? false}])
|
||||||
|
|
||||||
|
(defn- expected-events
|
||||||
|
[cases]
|
||||||
|
(into []
|
||||||
|
(mapcat (fn [{:keys [shape tail? svg? path?]}]
|
||||||
|
(let [id (:id shape)]
|
||||||
|
(cond-> []
|
||||||
|
tail? (conj [:select id])
|
||||||
|
svg? (conj [:svg-attrs id (:svg-attrs shape)])
|
||||||
|
path? (conj [:path id (:content shape)])))))
|
||||||
|
cases))
|
||||||
|
|
||||||
|
(t/deftest tail-cases-flags-are-consistent
|
||||||
|
(doseq [{:keys [shape tail? svg? path?]} tail-cases]
|
||||||
|
(t/is (boolean? tail?) "tail? is a boolean")
|
||||||
|
(t/is (= tail? (boolean (or svg? path?))) "tail? matches the write flags")
|
||||||
|
(t/is (= svg? (some? (:svg-attrs shape))) "svg? matches the shape")
|
||||||
|
(t/is (= path? (boolean (and (contains? #{:path :bool} (:type shape))
|
||||||
|
(some? (:content shape)))))
|
||||||
|
"path? matches the shape")))
|
||||||
|
|
||||||
|
(t/deftest needs-shape-tail-matches-spec-table
|
||||||
|
(doseq [{:keys [shape tail?]} tail-cases]
|
||||||
|
(t/is (= tail? (serialize-shape/needs-shape-tail? shape))
|
||||||
|
(str "predicate matches spec for " (:type shape)
|
||||||
|
" svg? " (some? (:svg-attrs shape))
|
||||||
|
" content? " (some? (:content shape))))))
|
||||||
|
|
||||||
|
(t/deftest serialize-shapes-batch-routes-flush-and-tail
|
||||||
|
(let [shapes (mapv :shape tail-cases)
|
||||||
|
opts {:include-layout? true :include-fills-strokes? true}
|
||||||
|
flush-calls (atom [])
|
||||||
|
events (atom [])
|
||||||
|
current (atom nil)
|
||||||
|
select-fn (fn [id]
|
||||||
|
(reset! current id)
|
||||||
|
(swap! events conj [:select id]))
|
||||||
|
stubs {:flush (fn [s o] (swap! flush-calls conj {:shapes s :opts o}) nil)
|
||||||
|
:svg-attrs (fn [attrs] (swap! events conj [:svg-attrs @current attrs]) nil)
|
||||||
|
:path (fn [content] (swap! events conj [:path @current content]) nil)}
|
||||||
|
result (with-ffi-stubs* stubs
|
||||||
|
#(serialize-shape/serialize-shapes-batch! shapes opts select-fn))
|
||||||
|
flushed (first @flush-calls)]
|
||||||
|
(t/is (= 1 (count @flush-calls)) "exactly one flush")
|
||||||
|
(t/is (= opts (:opts flushed)) "flush carries passed opts")
|
||||||
|
(t/is (= (count shapes) (count (:shapes flushed))) "flush covers all shapes")
|
||||||
|
(t/is (= (mapv :id shapes) (mapv :id (:shapes flushed))) "order preserved")
|
||||||
|
(t/is (= (expected-events tail-cases) @events)
|
||||||
|
"select precedes each tail write, tails hit the right ids")
|
||||||
|
(t/is (= (mapv :id shapes) (mapv :id result)) "returns prepared in order")))
|
||||||
|
|
||||||
|
(t/deftest serialize-shape-routes-single-upload-and-tail
|
||||||
|
(doseq [{:keys [shape tail? svg? path?]} tail-cases]
|
||||||
|
(let [flush-calls (atom [])
|
||||||
|
svg-applied (atom [])
|
||||||
|
path-applied (atom [])
|
||||||
|
stubs {:flush (fn [s o] (swap! flush-calls conj {:shapes s :opts o}) nil)
|
||||||
|
:svg-attrs (fn [attrs] (swap! svg-applied conj attrs) nil)
|
||||||
|
:path (fn [content] (swap! path-applied conj content) nil)}]
|
||||||
|
(let [expected (svg-derived/apply-svg-derived shape)
|
||||||
|
result (with-ffi-stubs* stubs #(serialize-shape/serialize-shape! shape))]
|
||||||
|
(t/is (= [{:shapes [expected] :opts {:include-layout? false}}] @flush-calls)
|
||||||
|
(str "single upload carries derived shape without layout for " (:type shape)))
|
||||||
|
(t/is (= expected result)
|
||||||
|
(str "returns the derived shape for downstream host attrs for " (:type shape))))
|
||||||
|
(t/is (= (if svg? [(:svg-attrs shape)] []) @svg-applied)
|
||||||
|
(str "svg-attrs write iff spec for " (:type shape)))
|
||||||
|
(t/is (= (if path? [(:content shape)] []) @path-applied)
|
||||||
|
(str "path write iff spec for " (:type shape)))
|
||||||
|
(t/is (= tail? (boolean (or (seq @svg-applied) (seq @path-applied))))
|
||||||
|
(str "tail fires iff spec for " (:type shape))))))
|
||||||
|
|
||||||
|
(t/deftest serialize-shapes-batch-empty-batch-is-noop
|
||||||
|
(let [opts {:include-layout? true :include-fills-strokes? true}
|
||||||
|
flush-calls (atom [])
|
||||||
|
selected (atom [])
|
||||||
|
stubs {:flush (fn [s o] (swap! flush-calls conj {:shapes s :opts o}) nil)
|
||||||
|
:svg-attrs (fn [_] (t/is false "no svg-attrs write on empty batch") nil)
|
||||||
|
:path (fn [_] (t/is false "no path write on empty batch") nil)}
|
||||||
|
result (with-ffi-stubs* stubs
|
||||||
|
#(serialize-shape/serialize-shapes-batch!
|
||||||
|
[]
|
||||||
|
opts
|
||||||
|
(fn [id] (swap! selected conj id))))]
|
||||||
|
(t/is (= [] result) "returns empty prepared")
|
||||||
|
(t/is (empty? @flush-calls) "no flush on empty batch")
|
||||||
|
(t/is (empty? @selected) "no select on empty batch")))
|
||||||
@ -78,10 +78,12 @@
|
|||||||
[frontend-tests.plugins.value-objects-test]
|
[frontend-tests.plugins.value-objects-test]
|
||||||
[frontend-tests.render-dimensions-test]
|
[frontend-tests.render-dimensions-test]
|
||||||
[frontend-tests.render-wasm.process-objects-test]
|
[frontend-tests.render-wasm.process-objects-test]
|
||||||
|
[frontend-tests.render-wasm.serialization-test]
|
||||||
[frontend-tests.render-wasm.text-editor-apply-styles-test]
|
[frontend-tests.render-wasm.text-editor-apply-styles-test]
|
||||||
[frontend-tests.render-wasm.text-editor-caret-color-test]
|
[frontend-tests.render-wasm.text-editor-caret-color-test]
|
||||||
[frontend-tests.router-test]
|
[frontend-tests.router-test]
|
||||||
[frontend-tests.svg-fills-test]
|
[frontend-tests.svg-fills-test]
|
||||||
|
[frontend-tests.svg-filters-test]
|
||||||
[frontend-tests.text-editor-paste-guard-test]
|
[frontend-tests.text-editor-paste-guard-test]
|
||||||
[frontend-tests.tokens.copy-paste-props-test]
|
[frontend-tests.tokens.copy-paste-props-test]
|
||||||
[frontend-tests.tokens.import-export-test]
|
[frontend-tests.tokens.import-export-test]
|
||||||
@ -205,10 +207,12 @@
|
|||||||
'frontend-tests.plugins.utils-test
|
'frontend-tests.plugins.utils-test
|
||||||
'frontend-tests.plugins.value-objects-test
|
'frontend-tests.plugins.value-objects-test
|
||||||
'frontend-tests.render-wasm.process-objects-test
|
'frontend-tests.render-wasm.process-objects-test
|
||||||
|
'frontend-tests.render-wasm.serialization-test
|
||||||
'frontend-tests.render-wasm.text-editor-apply-styles-test
|
'frontend-tests.render-wasm.text-editor-apply-styles-test
|
||||||
'frontend-tests.render-wasm.text-editor-caret-color-test
|
'frontend-tests.render-wasm.text-editor-caret-color-test
|
||||||
'frontend-tests.router-test
|
'frontend-tests.router-test
|
||||||
'frontend-tests.svg-fills-test
|
'frontend-tests.svg-fills-test
|
||||||
|
'frontend-tests.svg-filters-test
|
||||||
'frontend-tests.tokens.copy-paste-props-test
|
'frontend-tests.tokens.copy-paste-props-test
|
||||||
'frontend-tests.tokens.import-export-test
|
'frontend-tests.tokens.import-export-test
|
||||||
'frontend-tests.tokens.logic.token-actions-test
|
'frontend-tests.tokens.logic.token-actions-test
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
(ns frontend-tests.svg-fills-test
|
(ns frontend-tests.svg-fills-test
|
||||||
(:require
|
(:require
|
||||||
[app.render-wasm.svg-fills :as svg-fills]
|
[app.common.render-wasm.svg-derived :as svg-derived]
|
||||||
[cljs.test :refer [deftest is testing]]))
|
[cljs.test :refer [deftest is testing]]))
|
||||||
|
|
||||||
(def sample-shape
|
(def sample-shape
|
||||||
@ -28,7 +28,7 @@
|
|||||||
:style "stop-color:#00ff00;stop-opacity:0"}}]}}})
|
:style "stop-color:#00ff00;stop-opacity:0"}}]}}})
|
||||||
|
|
||||||
(deftest builds-gradient-fill-from-svg-defs
|
(deftest builds-gradient-fill-from-svg-defs
|
||||||
(let [fills (svg-fills/svg-fill->fills sample-shape)
|
(let [fills (svg-derived/svg-fill->fills sample-shape)
|
||||||
gradient (get-in (first fills) [:fill-color-gradient])]
|
gradient (get-in (first fills) [:fill-color-gradient])]
|
||||||
(testing "fallback fill is generated"
|
(testing "fallback fill is generated"
|
||||||
(is (= 1 (count fills))))
|
(is (= 1 (count fills))))
|
||||||
@ -40,7 +40,7 @@
|
|||||||
(is (= 0.5 (:fill-opacity (first fills)))))))
|
(is (= 0.5 (:fill-opacity (first fills)))))))
|
||||||
|
|
||||||
(deftest skips-when-no-svg-fill
|
(deftest skips-when-no-svg-fill
|
||||||
(is (nil? (svg-fills/svg-fill->fills {:svg-attrs {:fill "none"}}))))
|
(is (nil? (svg-derived/svg-fill->fills {:svg-attrs {:fill "none"}}))))
|
||||||
|
|
||||||
(def elliptical-shape
|
(def elliptical-shape
|
||||||
{:selrect {:x 0 :y 0 :width 200 :height 100}
|
{:selrect {:x 0 :y 0 :width 200 :height 100}
|
||||||
@ -61,7 +61,7 @@
|
|||||||
:style "stop-color:#ffffff;stop-opacity:1"}}]}}})
|
:style "stop-color:#ffffff;stop-opacity:1"}}]}}})
|
||||||
|
|
||||||
(deftest builds-elliptical-radial-gradient-with-transform
|
(deftest builds-elliptical-radial-gradient-with-transform
|
||||||
(let [fills (svg-fills/svg-fill->fills elliptical-shape)
|
(let [fills (svg-derived/svg-fill->fills elliptical-shape)
|
||||||
gradient (get-in (first fills) [:fill-color-gradient])]
|
gradient (get-in (first fills) [:fill-color-gradient])]
|
||||||
(testing "ellipse from gradientTransform is preserved"
|
(testing "ellipse from gradientTransform is preserved"
|
||||||
(is (= 1 (count fills)))
|
(is (= 1 (count fills)))
|
||||||
@ -75,22 +75,22 @@
|
|||||||
|
|
||||||
(deftest resolve-shape-fills-prefers-existing-fills
|
(deftest resolve-shape-fills-prefers-existing-fills
|
||||||
(let [fills [{:fill-color "#ff00ff" :fill-opacity 0.75}]
|
(let [fills [{:fill-color "#ff00ff" :fill-opacity 0.75}]
|
||||||
resolved (svg-fills/resolve-shape-fills {:fills fills})]
|
resolved (svg-derived/resolve-shape-fills {:fills fills})]
|
||||||
(is (= fills resolved))))
|
(is (= fills resolved))))
|
||||||
|
|
||||||
(deftest resolve-shape-fills-falls-back-to-svg-fill
|
(deftest resolve-shape-fills-falls-back-to-svg-fill
|
||||||
(let [resolved (svg-fills/resolve-shape-fills (assoc sample-shape :fills []))]
|
(let [resolved (svg-derived/resolve-shape-fills (assoc sample-shape :fills []))]
|
||||||
(is (= (svg-fills/svg-fill->fills sample-shape) resolved))))
|
(is (= (svg-derived/svg-fill->fills sample-shape) resolved))))
|
||||||
|
|
||||||
(deftest resolve-shape-fills-defaults-to-black
|
(deftest resolve-shape-fills-defaults-to-black
|
||||||
(is (= [{:fill-color "#000000" :fill-opacity 1}]
|
(is (= [{:fill-color "#000000" :fill-opacity 1}]
|
||||||
(svg-fills/resolve-shape-fills {:type :group
|
(svg-derived/resolve-shape-fills {:type :group
|
||||||
:svg-attrs {}}))))
|
:svg-attrs {}}))))
|
||||||
|
|
||||||
(deftest resolve-shape-fills-accepts-hex-fill
|
(deftest resolve-shape-fills-accepts-hex-fill
|
||||||
(let [fills (svg-fills/resolve-shape-fills {:fills []
|
(let [fills (svg-derived/resolve-shape-fills {:fills []
|
||||||
:type :svg-raw
|
:type :svg-raw
|
||||||
:svg-attrs {:fill "#fabada"}})]
|
:svg-attrs {:fill "#fabada"}})]
|
||||||
(is (= 1 (count fills)))
|
(is (= 1 (count fills)))
|
||||||
(is (= "#fabada" (:fill-color (first fills))))))
|
(is (= "#fabada" (:fill-color (first fills))))))
|
||||||
|
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
(ns frontend-tests.svg-filters-test
|
(ns frontend-tests.svg-filters-test
|
||||||
(:require
|
(:require
|
||||||
[app.render-wasm.svg-filters :as svg-filters]
|
[app.common.render-wasm.svg-derived :as svg-derived]
|
||||||
[cljs.test :refer [deftest is testing]]))
|
[cljs.test :refer [deftest is testing]]))
|
||||||
|
|
||||||
(def sample-filter-shape
|
(def sample-filter-shape
|
||||||
@ -17,7 +17,7 @@
|
|||||||
{:tag :feGaussianBlur :attrs {:stdDeviation "4"}}]}}})
|
{:tag :feGaussianBlur :attrs {:stdDeviation "4"}}]}}})
|
||||||
|
|
||||||
(deftest derives-blur-and-shadow-from-svg-filter
|
(deftest derives-blur-and-shadow-from-svg-filter
|
||||||
(let [shape (svg-filters/apply-svg-filters sample-filter-shape)
|
(let [shape (svg-derived/apply-svg-filters sample-filter-shape)
|
||||||
blur (:blur shape)
|
blur (:blur shape)
|
||||||
shadow (:shadow shape)]
|
shadow (:shadow shape)]
|
||||||
(testing "layer blur derived from feGaussianBlur"
|
(testing "layer blur derived from feGaussianBlur"
|
||||||
@ -38,12 +38,12 @@
|
|||||||
(deftest keeps-existing-native-filters
|
(deftest keeps-existing-native-filters
|
||||||
(let [existing {:blur {:id :existing :type :layer-blur :value 1.0}
|
(let [existing {:blur {:id :existing :type :layer-blur :value 1.0}
|
||||||
:shadow [{:id :shadow :style :drop-shadow}]}
|
:shadow [{:id :shadow :style :drop-shadow}]}
|
||||||
shape (svg-filters/apply-svg-filters (merge sample-filter-shape existing))]
|
shape (svg-derived/apply-svg-filters (merge sample-filter-shape existing))]
|
||||||
(is (= (:blur existing) (:blur shape)))
|
(is (= (:blur existing) (:blur shape)))
|
||||||
(is (= (:shadow existing) (:shadow shape)))))
|
(is (= (:shadow existing) (:shadow shape)))))
|
||||||
|
|
||||||
(deftest skips-when-no-filter-definition
|
(deftest skips-when-no-filter-definition
|
||||||
(let [shape {:svg-attrs {:fill "#fff"}}
|
(let [shape {:svg-attrs {:fill "#fff"}}
|
||||||
result (svg-filters/apply-svg-filters shape)]
|
result (svg-derived/apply-svg-filters shape)]
|
||||||
(is (= shape result))))
|
(is (= shape result))))
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user