mirror of
https://github.com/penpot/penpot.git
synced 2026-10-30 22:40:33 +00:00
♻️ Consolidate scene building macros and helpers
- Shared macro expansion for shapes and containers. - Shared runtime construction for rectangles, paths, and circles. - Centralized container attribute validation. - Simplified imports in the seven callers. - Added behaviour tests for seeded IDs, defaults, expression evaluation order, and container failures. AI-assisted-by: gpt-6.1-sol
This commit is contained in:
parent
1a44071d9d
commit
be10fb784b
@ -1,210 +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 benches.render-wasm.builder
|
||||
"Construction macros for renderer benchmark scenes.
|
||||
|
||||
For runtime helpers and scope state see `builder.cljs`.
|
||||
|
||||
Usage:
|
||||
|
||||
(scene {:seed 42
|
||||
:defaults {:rect {:x (gen-int 0 1920)}}}
|
||||
(rect) ; anonymous, generated attrs
|
||||
(rect {:x 10}) ; literal x over the generated rest
|
||||
(rect :hero {}) ; label stored in snapshot :refs
|
||||
(frame {:x 0 :y 0 :width 100 :height 100}
|
||||
(rect))
|
||||
(group :tiles {}
|
||||
(rect [:tile 3] {}))
|
||||
(bool :diff {:bool-type :difference}
|
||||
(rect {:x 0 :y 0 :width 100 :height 100})
|
||||
(rect {:x 50 :y 50 :width 100 :height 100})))
|
||||
|
||||
Ancestry and order are implicit in the nesting of calls: shape constructors
|
||||
add themselves to the current parent, in the sequence they are called.
|
||||
Coordinates are page-absolute like in penpot. Frames use their state bounds
|
||||
and groups derive their bounds from their children inside out.
|
||||
A masked group takes the geometry of its first child.
|
||||
Booleans derive content and geometry from their children with the standard
|
||||
path engine and inherit style from the head child unless attrs supply it.
|
||||
|
||||
`params` is a map with `:seed` (required), optional `:root` attributes for
|
||||
the canonical root frame, and optional `:defaults` with one attribute
|
||||
generator map per shape type.
|
||||
|
||||
Require this namespace with `:include-macros true` (or `:require-macros`)
|
||||
to use the macros.
|
||||
|
||||
The scope is carried by the dynamic `*state*`, so ordinary `let`, `doseq`
|
||||
and functions defined outside the scope work inside the body. Scopes
|
||||
cannot nest and a body exception unwinds without leaking state.")
|
||||
|
||||
(defmacro scene
|
||||
"Runs `body` in a scene scope and returns the validated snapshot."
|
||||
[params & body]
|
||||
`(let [params# ~params
|
||||
state# (benches.render-wasm.builder/start params#)]
|
||||
(binding [benches.render-wasm.builder/*state* state#
|
||||
benches.render-wasm.builder/*defaults* (:defaults params#)]
|
||||
~@body
|
||||
(benches.render-wasm.builder/finish! state#))))
|
||||
|
||||
(defmacro rect
|
||||
"Adds one rectangle to the current scene scope and returns its uuid.
|
||||
|
||||
Forms: `(rect)`, `(rect attrs)`, `(rect label)`, `(rect label attrs)`.
|
||||
A one-argument call is resolved at runtime: a map is interpreted as attributes
|
||||
and anything else is a label. `label` must be a keyword or vector, stored in
|
||||
the instance `:refs`; duplicates and other label types are rejected. `attrs`
|
||||
override the defaults attribute by attribute."
|
||||
([]
|
||||
`(benches.render-wasm.builder/rect!
|
||||
benches.render-wasm.builder/*state*
|
||||
benches.render-wasm.builder/*defaults*
|
||||
nil
|
||||
{}))
|
||||
|
||||
([label-or-attrs]
|
||||
`(benches.render-wasm.builder/rect!
|
||||
benches.render-wasm.builder/*state*
|
||||
benches.render-wasm.builder/*defaults*
|
||||
~label-or-attrs
|
||||
{}))
|
||||
|
||||
([label attrs]
|
||||
`(benches.render-wasm.builder/rect!
|
||||
benches.render-wasm.builder/*state*
|
||||
benches.render-wasm.builder/*defaults*
|
||||
~label
|
||||
~attrs)))
|
||||
|
||||
(defmacro path
|
||||
"Adds one path to the current scene scope.
|
||||
|
||||
Forms: `(path)`, `(path attrs)`, `(path label)`, `(path label attrs)`.
|
||||
A one-argument call is resolved at runtime: a map is interpreted as attributes
|
||||
and anything else is a label. `label` must be a keyword or vector, stored in
|
||||
the instance `:refs`; duplicates and other label types are rejected. `attrs`
|
||||
override the defaults attribute by attribute.
|
||||
"
|
||||
([]
|
||||
`(benches.render-wasm.builder/path!
|
||||
benches.render-wasm.builder/*state*
|
||||
benches.render-wasm.builder/*defaults*
|
||||
nil
|
||||
{}))
|
||||
([label-or-attrs]
|
||||
`(benches.render-wasm.builder/path!
|
||||
benches.render-wasm.builder/*state*
|
||||
benches.render-wasm.builder/*defaults*
|
||||
~label-or-attrs
|
||||
{}))
|
||||
([label attrs]
|
||||
`(benches.render-wasm.builder/path!
|
||||
benches.render-wasm.builder/*state*
|
||||
benches.render-wasm.builder/*defaults*
|
||||
~label
|
||||
~attrs)))
|
||||
|
||||
(defmacro circle
|
||||
"Adds one ellipse to the current scene scope and returns its uuid.
|
||||
|
||||
Forms: `(circle)`, `(circle attrs)`, `(circle label)`, `(circle label attrs)`.
|
||||
A one-argument call is resolved at runtime: a map is interpreted as attributes
|
||||
and anything else is a label. `label` must be a keyword or vector, stored in
|
||||
the instance `:refs`; duplicates and other label types are rejected. `attrs`
|
||||
override the defaults attribute by attribute."
|
||||
([]
|
||||
`(benches.render-wasm.builder/circle!
|
||||
benches.render-wasm.builder/*state*
|
||||
benches.render-wasm.builder/*defaults*
|
||||
nil
|
||||
{}))
|
||||
|
||||
([label-or-attrs]
|
||||
`(benches.render-wasm.builder/circle!
|
||||
benches.render-wasm.builder/*state*
|
||||
benches.render-wasm.builder/*defaults*
|
||||
~label-or-attrs
|
||||
{}))
|
||||
|
||||
([label attrs]
|
||||
`(benches.render-wasm.builder/circle!
|
||||
benches.render-wasm.builder/*state*
|
||||
benches.render-wasm.builder/*defaults*
|
||||
~label
|
||||
~attrs)))
|
||||
|
||||
(defmacro frame
|
||||
"Adds a frame to the current scope, runs `body` with the frame as parent,
|
||||
and returns the frame uuid.
|
||||
|
||||
Forms: `(frame attrs & body)` and `(frame label attrs & body)`. `label` is
|
||||
a literal keyword or vector stored in the instance `:refs`. Frame attrs
|
||||
must carry `:x :y :width :height`; children keep page-absolute
|
||||
coordinates. Empty frames are allowed."
|
||||
[label-or-attrs & body]
|
||||
(if (or (keyword? label-or-attrs) (vector? label-or-attrs))
|
||||
`(benches.render-wasm.builder/with-container
|
||||
:frame
|
||||
~(first body)
|
||||
~label-or-attrs
|
||||
(fn [] ~@(rest body)))
|
||||
`(benches.render-wasm.builder/with-container
|
||||
:frame
|
||||
~label-or-attrs
|
||||
nil
|
||||
(fn [] ~@body))))
|
||||
|
||||
(defmacro group
|
||||
"Adds a group to the current scope, runs `body` with the group as parent,
|
||||
and returns the group uuid.
|
||||
|
||||
Forms: `(group attrs & body)` and `(group label attrs & body)`. `label` is
|
||||
a literal keyword or vector stored in the instance `:refs`. Groups derive
|
||||
their geometry from children, so geometry attrs are rejected. With
|
||||
`:masked-group true` the first child is the mask. A group needs at least
|
||||
one child."
|
||||
[label-or-attrs & body]
|
||||
(if (or (keyword? label-or-attrs) (vector? label-or-attrs))
|
||||
`(benches.render-wasm.builder/with-container
|
||||
:group
|
||||
~(first body)
|
||||
~label-or-attrs
|
||||
(fn [] ~@(rest body)))
|
||||
`(benches.render-wasm.builder/with-container
|
||||
:group
|
||||
~label-or-attrs
|
||||
nil
|
||||
(fn [] ~@body))))
|
||||
|
||||
(defmacro bool
|
||||
"Adds a boolean shape to the current scope, runs `body` with the bool as
|
||||
parent, and returns the bool uuid.
|
||||
|
||||
Forms: `(bool attrs & body)` and `(bool label attrs & body)`. `label` is a
|
||||
literal keyword or vector stored in the instance `:refs`. `attrs` must
|
||||
carry `:bool-type` (`:union`, `:difference`, `:intersection` or
|
||||
`:exclude`).
|
||||
|
||||
Content and geometry derive from the children after they finalize, so geometry,
|
||||
content and transform attrs are rejected. Fills, strokes, shadows and blurs
|
||||
are inherited from the head child (first for `:difference`, last otherwise)
|
||||
unless attrs supply them. A bool needs at least one child and cannot contain
|
||||
frames."
|
||||
[label-or-attrs & body]
|
||||
(if (or (keyword? label-or-attrs) (vector? label-or-attrs))
|
||||
`(benches.render-wasm.builder/with-container
|
||||
:bool
|
||||
~(first body)
|
||||
~label-or-attrs
|
||||
(fn [] ~@(rest body)))
|
||||
`(benches.render-wasm.builder/with-container
|
||||
:bool
|
||||
~label-or-attrs
|
||||
nil
|
||||
(fn [] ~@body))))
|
||||
513
frontend/performance/benches/render_wasm/builder.cljc
Normal file
513
frontend/performance/benches/render_wasm/builder.cljc
Normal file
@ -0,0 +1,513 @@
|
||||
;; 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 benches.render-wasm.builder
|
||||
"Seeded construction of benchmark scenes.
|
||||
|
||||
A `scene` consists of one seeded number generator, one object map, and one
|
||||
label map.
|
||||
|
||||
Shape and container calls attach objects to the current parent in evaluation
|
||||
order. Coordinates are page-relative. Frames use authored bounds; groups and
|
||||
booleans derive geometry from their children after the body completes.
|
||||
|
||||
`scene` accepts a required :seed, optional :root attributes and :defaults.
|
||||
It returns a validated snapshot with :objects and :refs. Labels name objects
|
||||
within that snapshot. A container whose body or finalizer throws becomes
|
||||
unfinished, and `finish!` rejects the snapshot.
|
||||
|
||||
Default attributes follow the same theme: `*defaults*` maps a shape type to
|
||||
an attribute generator map, bound by the `scene` macro from the scope
|
||||
params. Per-shape attrs are merged over the generated values, so a recipe
|
||||
can override values locally and rebind `*defaults*` around a section.
|
||||
|
||||
Ordinary :require loads the macros and runtime helpers. Scene bodies support
|
||||
let, doseq and calls to functions defined outside the scope."
|
||||
#?(:cljs (:require-macros [benches.render-wasm.builder]))
|
||||
#?(:cljs
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.types.path :as path]
|
||||
[app.common.types.shape :as cts]
|
||||
[app.common.types.shape-tree :as ctst]
|
||||
[app.common.uuid :as uuid]
|
||||
[benches.render-wasm.random :as rnd :refer
|
||||
[rng-float rng-hex-color rng-int rng-opacity rng-uuid]]
|
||||
[benches.render-wasm.snapshot :as snapshot])))
|
||||
|
||||
#?(:clj
|
||||
(do
|
||||
(defn- emit-shape
|
||||
[constructor label attrs]
|
||||
`(~constructor
|
||||
benches.render-wasm.builder/*state*
|
||||
benches.render-wasm.builder/*defaults*
|
||||
~label
|
||||
~attrs))
|
||||
|
||||
(defn- emit-container
|
||||
[type label-or-attrs body]
|
||||
(let [labeled? (or (keyword? label-or-attrs) (vector? label-or-attrs))
|
||||
attrs (if labeled? (first body) label-or-attrs)
|
||||
label (when labeled? label-or-attrs)
|
||||
body (if labeled? (rest body) body)]
|
||||
`(benches.render-wasm.builder/with-container
|
||||
~type ~attrs ~label (fn [] ~@body))))
|
||||
|
||||
(defmacro scene
|
||||
"Runs `body` in a scene scope and returns the validated snapshot."
|
||||
[params & body]
|
||||
`(let [params# ~params
|
||||
state# (benches.render-wasm.builder/start params#)]
|
||||
(binding [benches.render-wasm.builder/*state* state#
|
||||
benches.render-wasm.builder/*defaults* (:defaults params#)]
|
||||
~@body
|
||||
(benches.render-wasm.builder/finish! state#))))
|
||||
|
||||
(defmacro rect
|
||||
"Adds a rectangle and returns its uuid.
|
||||
|
||||
Forms: (rect), (rect attrs), (rect label), (rect label attrs).
|
||||
A single map argument supplies attributes; keywords and vectors label
|
||||
the object in :refs. Attributes override the generated defaults."
|
||||
([]
|
||||
(emit-shape 'benches.render-wasm.builder/rect! nil {}))
|
||||
([label-or-attrs]
|
||||
(emit-shape 'benches.render-wasm.builder/rect! label-or-attrs {}))
|
||||
([label attrs]
|
||||
(emit-shape 'benches.render-wasm.builder/rect! label attrs)))
|
||||
|
||||
(defmacro path
|
||||
"Adds a path and returns its uuid.
|
||||
|
||||
Forms: (path), (path attrs), (path label), (path label attrs).
|
||||
A single map argument supplies attributes; keywords and vectors label
|
||||
the object in :refs. Attributes override the generated defaults."
|
||||
([]
|
||||
(emit-shape 'benches.render-wasm.builder/path! nil {}))
|
||||
([label-or-attrs]
|
||||
(emit-shape 'benches.render-wasm.builder/path! label-or-attrs {}))
|
||||
([label attrs]
|
||||
(emit-shape 'benches.render-wasm.builder/path! label attrs)))
|
||||
|
||||
(defmacro circle
|
||||
"Adds an ellipse and returns its uuid.
|
||||
|
||||
Forms: (circle), (circle attrs), (circle label), (circle label attrs).
|
||||
A single map argument supplies attributes; keywords and vectors label
|
||||
the object in :refs. Attributes override the generated defaults."
|
||||
([]
|
||||
(emit-shape 'benches.render-wasm.builder/circle! nil {}))
|
||||
([label-or-attrs]
|
||||
(emit-shape 'benches.render-wasm.builder/circle! label-or-attrs {}))
|
||||
([label attrs]
|
||||
(emit-shape 'benches.render-wasm.builder/circle! label attrs)))
|
||||
|
||||
(defmacro frame
|
||||
"Adds a frame, runs `body` with the frame as parent, and returns its uuid.
|
||||
|
||||
Forms: (frame attrs & body), (frame label attrs & body).
|
||||
A keyword or vector form supplies a label in :refs. Attributes must
|
||||
carry numeric :x :y :width :height. Children use page-relative
|
||||
coordinates. Frames allow an empty body."
|
||||
[label-or-attrs & body]
|
||||
(emit-container :frame label-or-attrs body))
|
||||
|
||||
(defmacro group
|
||||
"Adds a group, runs `body` with the group as parent, and returns its uuid.
|
||||
|
||||
Forms: (group attrs & body), (group label attrs & body).
|
||||
A keyword or vector form supplies a label in :refs. Groups derive their
|
||||
geometry from children and require at least one child. With
|
||||
:masked-group true, the first child supplies the mask geometry."
|
||||
[label-or-attrs & body]
|
||||
(emit-container :group label-or-attrs body))
|
||||
|
||||
(defmacro bool
|
||||
"Adds a boolean shape, runs `body` with it as parent, and returns its uuid.
|
||||
|
||||
Forms: (bool attrs & body), (bool label attrs & body).
|
||||
A keyword or vector form supplies a label in :refs. Attributes require
|
||||
:bool-type :union, :difference, :intersection or :exclude.
|
||||
|
||||
Content and geometry derive from the children. Styles inherit from the
|
||||
first child for :difference and the last child otherwise; supplied
|
||||
non-nil styles take precedence. A bool requires at least one child and
|
||||
accepts paths, rectangles, ellipses, groups and other booleans."
|
||||
[label-or-attrs & body]
|
||||
(emit-container :bool label-or-attrs body))))
|
||||
|
||||
#?(:cljs
|
||||
(do
|
||||
|
||||
|
||||
;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Attribute generators
|
||||
;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn gen-int
|
||||
"Integer generator over `[min, max)`."
|
||||
[min max]
|
||||
(fn [rng] (rng-int rng min max)))
|
||||
|
||||
(defn gen-float
|
||||
"Float generator rounded to three decimals so snapshots stay readable."
|
||||
[min max]
|
||||
(fn [rng] (rnd/round3 (rng-float rng min max))))
|
||||
|
||||
(defn gen-hex-color
|
||||
"Generator of a random `#rrggbb` string."
|
||||
[]
|
||||
(fn [rng] (rng-hex-color rng)))
|
||||
|
||||
(defn gen-one-of
|
||||
"Generator that picks one value per draw. Rejects an empty collection."
|
||||
[values]
|
||||
(when (empty? values)
|
||||
(throw (ex-info "gen-one-of requires at least one value"
|
||||
{:type ::no-values})))
|
||||
(fn [rng] (nth values (rng-int rng 0 (count values)))))
|
||||
|
||||
(defn gen-fill
|
||||
"Generator of one translucent solid fill."
|
||||
[]
|
||||
(fn [rng]
|
||||
{:fill-color (rng-hex-color rng)
|
||||
:fill-opacity (rng-opacity rng)}))
|
||||
|
||||
(defn gen-stroke
|
||||
"Centered stroke like the interim rectangle workload."
|
||||
[width]
|
||||
(fn [rng]
|
||||
{:stroke-width width
|
||||
:stroke-alignment :center
|
||||
:stroke-color (rng-hex-color rng)
|
||||
:stroke-opacity (rng-opacity rng)}))
|
||||
|
||||
(defn gen-vector
|
||||
"Composes generators into a generator of a vector, one value per generator."
|
||||
[& generators]
|
||||
(fn [rng] (mapv #(% rng) generators)))
|
||||
|
||||
;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Scope state
|
||||
;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def ^:dynamic *state*
|
||||
"Current scene scope; bound by the `scene` macro."
|
||||
nil)
|
||||
|
||||
(def ^:dynamic *defaults*
|
||||
"Per-shape-type attribute generator maps bound by the `scene` macro,
|
||||
e.g. `{:rect {:x (gen-int 0 1920) ...}}`. Values are literals or one-arg
|
||||
functions of the scope random source."
|
||||
{})
|
||||
|
||||
(def ^:dynamic *parent*
|
||||
"Current insertion point as `{:id parent-id :frame-id containing-frame}`;
|
||||
bound by the container scopes, `uuid/zero` at the top."
|
||||
{:id uuid/zero :frame-id uuid/zero})
|
||||
|
||||
(defn- check-seed!
|
||||
"Requires a scene seed from 0 through 4294967295, retaining the builder
|
||||
error type."
|
||||
[seed]
|
||||
(when-not (rnd/seed? seed)
|
||||
(throw (ex-info "scene scope requires an integer :seed in [0, 2^32)"
|
||||
{:type ::invalid-seed
|
||||
:seed seed}))))
|
||||
|
||||
(defn start
|
||||
"Starts a scene scope and returns its state atom with objects, refs and :rng.
|
||||
The seeded generator belongs to this scope; other scopes and report draws
|
||||
cannot advance its state."
|
||||
[params]
|
||||
(when (some? *state*)
|
||||
(throw (ex-info "scene scopes cannot nest"
|
||||
{:type ::nested-scope})))
|
||||
(check-seed! (:seed params))
|
||||
(let [seed (:seed params)
|
||||
root (cts/setup-shape (merge (:root params)
|
||||
{:id uuid/zero
|
||||
:type :frame
|
||||
:name "Root Frame"
|
||||
:parent-id uuid/zero
|
||||
:frame-id uuid/zero
|
||||
:shapes []}))]
|
||||
(atom {:seed seed
|
||||
:rng (rnd/create seed)
|
||||
:objects {uuid/zero root}
|
||||
:refs {}
|
||||
:unfinished {}})))
|
||||
|
||||
(defn- ensure-state
|
||||
[state]
|
||||
(or state
|
||||
(throw (ex-info "shape used outside a scene scope"
|
||||
{:type ::outside-scope}))))
|
||||
|
||||
(defn- resolve-attr
|
||||
[rng spec]
|
||||
(if (fn? spec) (spec rng) spec))
|
||||
|
||||
(defn- resolve-attrs
|
||||
[rng defaults attrs]
|
||||
(merge (reduce-kv (fn [result key spec]
|
||||
(assoc result key (resolve-attr rng spec)))
|
||||
{}
|
||||
defaults)
|
||||
attrs))
|
||||
|
||||
(defn- register-ref!
|
||||
[state label id]
|
||||
(when (some? label)
|
||||
(when (contains? (:refs @state) label)
|
||||
(throw (ex-info (str "duplicate scene label: " label)
|
||||
{:type ::duplicate-label
|
||||
:label label})))
|
||||
(swap! state assoc-in [:refs label] id)))
|
||||
|
||||
(defn- add-object!
|
||||
"Inserts `shape` under the current parent and registers `label`."
|
||||
[state label shape]
|
||||
(let [{:keys [id frame-id]} *parent*]
|
||||
(register-ref! state label (:id shape))
|
||||
(swap! state
|
||||
(fn [scope]
|
||||
(ctst/add-shape (:id shape) shape scope frame-id id nil true)))))
|
||||
|
||||
;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Shape constructors
|
||||
;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn- normalize-label-and-attrs
|
||||
"Resolves map attributes and keyword/vector labels for shape construction."
|
||||
[label attrs]
|
||||
(cond
|
||||
(and (map? label) (empty? attrs)) [nil label]
|
||||
(nil? label) [nil attrs]
|
||||
(or (keyword? label) (vector? label)) [label attrs]
|
||||
:else (throw (ex-info "rect label must be a keyword or vector"
|
||||
{:type ::invalid-label
|
||||
:label label}))))
|
||||
|
||||
(defn- construct-shape
|
||||
[state defaults type label attrs]
|
||||
(let [state (ensure-state state)
|
||||
[label attrs] (normalize-label-and-attrs label attrs)
|
||||
rng (:rng @state)
|
||||
attrs (resolve-attrs rng (get defaults type) attrs)
|
||||
id (rng-uuid rng)
|
||||
shape (cts/setup-shape (merge attrs
|
||||
{:id id
|
||||
:type type}))]
|
||||
(add-object! state label shape)
|
||||
id))
|
||||
|
||||
(defn rect!
|
||||
"Adds a rectangle with the selected defaults and attributes, returning its uuid."
|
||||
[state defaults label attrs]
|
||||
(construct-shape state defaults :rect label attrs))
|
||||
|
||||
(defn path!
|
||||
"Adds a path with the selected defaults and attributes, returning its uuid."
|
||||
[state defaults label attrs]
|
||||
(construct-shape state defaults :path label attrs))
|
||||
|
||||
(defn circle!
|
||||
"Adds an ellipse with the selected defaults and attributes, returning its uuid."
|
||||
[state defaults label attrs]
|
||||
(construct-shape state defaults :circle label attrs))
|
||||
|
||||
(def ^:private frame-bounds-keys
|
||||
"Frames do not derive their bounds from children."
|
||||
[:x :y :width :height])
|
||||
|
||||
(def ^:private group-geometry-keys
|
||||
"Group geometry derives from its children."
|
||||
[:x :y :width :height :selrect :points])
|
||||
|
||||
(def ^:private bool-geometry-keys
|
||||
"Bool geometry, content and placement derive from its children."
|
||||
[:x :y :width :height :selrect :points :content])
|
||||
|
||||
(def ^:private bool-transform-keys
|
||||
"Construction does not accept transforms: `path/update-geometry` would
|
||||
pivot them on the empty pre-finalization selrect."
|
||||
[:transform :transform-inverse :rotation :flip-x :flip-y])
|
||||
|
||||
(defn- reject-attrs!
|
||||
"Throws when `attrs` carries any of `attr-keys`. Shared by containers
|
||||
whose geometry or content derives from their children."
|
||||
[attrs id error-type message attr-keys]
|
||||
(doseq [key attr-keys]
|
||||
(when (some? (get attrs key))
|
||||
(throw (ex-info message
|
||||
{:type error-type
|
||||
:id id
|
||||
:key key})))))
|
||||
|
||||
(defn- setup-frame
|
||||
[id attrs]
|
||||
(doseq [key frame-bounds-keys]
|
||||
(let [value (get attrs key)]
|
||||
(cond
|
||||
(nil? value)
|
||||
(throw (ex-info "frame requires :x :y :width :height"
|
||||
{:type ::frame-bounds
|
||||
:id id
|
||||
:missing key}))
|
||||
|
||||
(not (d/num? value))
|
||||
(throw (ex-info "frame bounds must be numbers"
|
||||
{:type ::frame-bounds
|
||||
:id id
|
||||
:invalid key})))))
|
||||
(cts/setup-shape (assoc attrs :id id :type :frame)))
|
||||
|
||||
(defn- setup-group
|
||||
[id attrs]
|
||||
(reject-attrs! attrs id ::group-geometry
|
||||
"group derives its geometry from children"
|
||||
group-geometry-keys)
|
||||
(cts/setup-shape (assoc attrs :id id :type :group)))
|
||||
|
||||
(defn- setup-bool
|
||||
[id attrs]
|
||||
(reject-attrs! attrs id ::bool-geometry
|
||||
"bool derives its geometry and content from children"
|
||||
bool-geometry-keys)
|
||||
(reject-attrs! attrs id ::bool-transform
|
||||
"bool construction does not accept transforms"
|
||||
bool-transform-keys)
|
||||
(when-not (contains? cts/bool-types (:bool-type attrs))
|
||||
(throw (ex-info "bool requires :bool-type :union :difference :intersection or :exclude"
|
||||
{:type ::bool-type
|
||||
:id id
|
||||
:bool-type (:bool-type attrs)
|
||||
:supported cts/bool-types})))
|
||||
(cts/setup-shape (assoc attrs :id id :type :bool)))
|
||||
|
||||
(defn- bool-head
|
||||
"Canonical style head: the first child is the base for `:difference`, the
|
||||
last child otherwise."
|
||||
[bool-shape children]
|
||||
(if (= :difference (:bool-type bool-shape))
|
||||
(first children)
|
||||
(last children)))
|
||||
|
||||
(defn- finalize-bool!
|
||||
[state id]
|
||||
(let [scope @state
|
||||
bool (get-in scope [:objects id])
|
||||
children (mapv (:objects scope) (:shapes bool))]
|
||||
(when (empty? children)
|
||||
(throw (ex-info "bool requires at least one child"
|
||||
{:type ::empty-bool
|
||||
:id id})))
|
||||
(when-let [frame (first (filter cfh/frame-shape? children))]
|
||||
(throw (ex-info "bool children cannot be frames"
|
||||
{:type ::frame-in-bool
|
||||
:id id
|
||||
:child (:id frame)})))
|
||||
(let [inherited (d/without-nils
|
||||
(select-keys (bool-head bool children)
|
||||
path/bool-style-properties))
|
||||
supplied (d/without-nils
|
||||
(select-keys (get-in scope [:container-attrs id])
|
||||
path/bool-style-properties))
|
||||
bool (-> bool (merge inherited) (merge supplied))
|
||||
|
||||
;; `path/update-bool-shape` dispatches to `path/wasm:calc-bool-content`,
|
||||
;; the renderer override, so scene content would depend on renderer
|
||||
;; initialization and A/B configuration. Compose the pure helpers:
|
||||
;; scene generation is untimed and must not vary with the renderer
|
||||
;; being compared.
|
||||
content (path/calc-bool-content bool (:objects scope))
|
||||
bool (path/update-geometry bool content)]
|
||||
(swap! state assoc-in [:objects id] bool))))
|
||||
|
||||
(defn- finalize-group!
|
||||
[state id]
|
||||
(let [scope @state
|
||||
group (get-in scope [:objects id])
|
||||
children (mapv (:objects scope) (:shapes group))]
|
||||
(when (empty? children)
|
||||
(throw (ex-info "group requires at least one child"
|
||||
{:type ::empty-group
|
||||
:id id})))
|
||||
(let [group (if (:masked-group group)
|
||||
(gsh/update-mask-selrect group children)
|
||||
(gsh/update-group-selrect group children))]
|
||||
(swap! state assoc-in [:objects id] group))))
|
||||
|
||||
(def ^:private container-specs
|
||||
"Per-container behavior: how to build it, which parent scope it opens,
|
||||
and how to finalize it after the body. `:keep-attrs?` records the raw
|
||||
attrs in the scope so the finalizer can let supplied styles win over
|
||||
inherited ones."
|
||||
{:frame {:setup setup-frame
|
||||
:parent (fn [id _parent] {:id id :frame-id id})
|
||||
:finalize (fn [_state _id] nil)}
|
||||
:group {:setup setup-group
|
||||
:parent (fn [id parent] {:id id :frame-id (:frame-id parent)})
|
||||
:finalize finalize-group!}
|
||||
:bool {:setup setup-bool
|
||||
:parent (fn [id parent] {:id id :frame-id (:frame-id parent)})
|
||||
:finalize finalize-bool!
|
||||
:keep-attrs? true}})
|
||||
|
||||
(defn- check-container-attrs
|
||||
[type id attrs]
|
||||
(when-not (map? attrs)
|
||||
(throw (ex-info (str (name type) " attrs must be a map; the label form takes the attrs map next")
|
||||
{:type ::invalid-attrs :id id})))
|
||||
attrs)
|
||||
|
||||
(defn with-container
|
||||
"Creates a frame, group or bool, runs `thunk` with the new parent in
|
||||
scope, finalizes the containers that derive state from children, and
|
||||
returns the container uuid.
|
||||
|
||||
Any exception from the body or the finalizer marks the container
|
||||
unfinished and propagates; `finish!` then rejects the instance even if the
|
||||
caller catches the exception."
|
||||
[type attrs label thunk]
|
||||
(let [{:keys [setup parent finalize keep-attrs?]}
|
||||
(or (get container-specs type)
|
||||
(throw (ex-info "unsupported container type"
|
||||
{:type ::invalid-container
|
||||
:container-type type})))
|
||||
state (ensure-state *state*)
|
||||
id (rng-uuid (:rng @state))
|
||||
attrs (check-container-attrs type id attrs)
|
||||
shape (setup id attrs)]
|
||||
(when keep-attrs?
|
||||
(swap! state assoc-in [:container-attrs id] attrs))
|
||||
(add-object! state label shape)
|
||||
(binding [*parent* (parent id *parent*)]
|
||||
(try
|
||||
(thunk)
|
||||
(finalize state id)
|
||||
(catch :default cause
|
||||
(swap! state assoc-in [:unfinished id] true)
|
||||
(throw cause))))
|
||||
id))
|
||||
|
||||
(defn finish!
|
||||
"Validates the scope snapshot and returns it."
|
||||
[state]
|
||||
(let [{:keys [objects refs unfinished]} @state]
|
||||
(when (seq unfinished)
|
||||
(throw (ex-info "scene contains an unfinished container"
|
||||
{:type ::unfinished-container
|
||||
:ids (vec (keys unfinished))})))
|
||||
(let [instance {:objects objects
|
||||
:refs refs}]
|
||||
(snapshot/validate! instance)
|
||||
instance)))))
|
||||
@ -1,419 +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 benches.render-wasm.builder
|
||||
"Construction of benchmark scenes.
|
||||
|
||||
A `scene` consists of one seeded number generator, one object map, and one
|
||||
label map.
|
||||
|
||||
A container whose body throws is marked unfinished and `finish!` rejects the
|
||||
instance, so a caught exception cannot produce a half-built scene.
|
||||
|
||||
Default attributes follow the same theme: `*defaults*` maps a shape type to
|
||||
an attribute generator map, bound by the `scene` macro from the scope
|
||||
params. Per-shape attrs are merged over the generated values, so a recipe
|
||||
can override anything locally and rebind `*defaults*` around a section."
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.types.path :as path]
|
||||
[app.common.types.shape :as cts]
|
||||
[app.common.types.shape-tree :as ctst]
|
||||
[app.common.uuid :as uuid]
|
||||
[benches.render-wasm.random :as rnd :refer
|
||||
[rng-float rng-hex-color rng-int rng-opacity rng-uuid]]
|
||||
[benches.render-wasm.snapshot :as snapshot]))
|
||||
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Attribute generators
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn gen-int
|
||||
"Integer generator over `[min, max)`."
|
||||
[min max]
|
||||
(fn [rng] (rng-int rng min max)))
|
||||
|
||||
(defn gen-float
|
||||
"Float generator rounded to three decimals so snapshots stay readable."
|
||||
[min max]
|
||||
(fn [rng] (rnd/round3 (rng-float rng min max))))
|
||||
|
||||
(defn gen-hex-color
|
||||
"Generator of a random `#rrggbb` string."
|
||||
[]
|
||||
(fn [rng] (rng-hex-color rng)))
|
||||
|
||||
(defn gen-one-of
|
||||
"Generator that picks one value per draw. Rejects an empty collection."
|
||||
[values]
|
||||
(when (empty? values)
|
||||
(throw (ex-info "gen-one-of requires at least one value"
|
||||
{:type ::no-values})))
|
||||
(fn [rng] (nth values (rng-int rng 0 (count values)))))
|
||||
|
||||
(defn gen-fill
|
||||
"Generator of one translucent solid fill."
|
||||
[]
|
||||
(fn [rng]
|
||||
{:fill-color (rng-hex-color rng)
|
||||
:fill-opacity (rng-opacity rng)}))
|
||||
|
||||
(defn gen-stroke
|
||||
"Centered stroke like the interim rectangle workload."
|
||||
[width]
|
||||
(fn [rng]
|
||||
{:stroke-width width
|
||||
:stroke-alignment :center
|
||||
:stroke-color (rng-hex-color rng)
|
||||
:stroke-opacity (rng-opacity rng)}))
|
||||
|
||||
(defn gen-vector
|
||||
"Composes generators into a generator of a vector, one value per
|
||||
generator."
|
||||
[& generators]
|
||||
(fn [rng] (mapv #(% rng) generators)))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Scope state
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def ^:dynamic *state*
|
||||
"Current scene scope; bound by the `scene` macro."
|
||||
nil)
|
||||
|
||||
(def ^:dynamic *defaults*
|
||||
"Per-shape-type attribute generator maps bound by the `scene` macro,
|
||||
e.g. `{:rect {:x (gen-int 0 1920) ...}}`. Values are literals or one-arg
|
||||
functions of the scope random source."
|
||||
{})
|
||||
|
||||
(def ^:dynamic *parent*
|
||||
"Current insertion point as `{:id parent-id :frame-id containing-frame}`;
|
||||
bound by the container scopes, `uuid/zero` at the top."
|
||||
{:id uuid/zero :frame-id uuid/zero})
|
||||
|
||||
(defn- check-seed!
|
||||
"Requires a scene seed from 0 through 4294967295, retaining the builder error type."
|
||||
[seed]
|
||||
(when-not (rnd/seed? seed)
|
||||
(throw (ex-info "scene scope requires an integer :seed in [0, 2^32)"
|
||||
{:type ::invalid-seed
|
||||
:seed seed}))))
|
||||
|
||||
(defn start
|
||||
"Starts a scene scope and returns its state atom with objects, refs and :rng.
|
||||
The seeded generator belongs to this scope; other scopes and report draws
|
||||
cannot advance its state."
|
||||
[params]
|
||||
(when (some? *state*)
|
||||
(throw (ex-info "scene scopes cannot nest"
|
||||
{:type ::nested-scope})))
|
||||
(check-seed! (:seed params))
|
||||
(let [seed (:seed params)
|
||||
root (cts/setup-shape (merge (:root params)
|
||||
{:id uuid/zero
|
||||
:type :frame
|
||||
:name "Root Frame"
|
||||
:parent-id uuid/zero
|
||||
:frame-id uuid/zero
|
||||
:shapes []}))]
|
||||
(atom {:seed seed
|
||||
:rng (rnd/create seed)
|
||||
:objects {uuid/zero root}
|
||||
:refs {}
|
||||
:unfinished {}})))
|
||||
|
||||
(defn- ensure-state
|
||||
[state]
|
||||
(or state
|
||||
(throw (ex-info "shape used outside a scene scope"
|
||||
{:type ::outside-scope}))))
|
||||
|
||||
(defn- resolve-attr
|
||||
[rng spec]
|
||||
(if (fn? spec) (spec rng) spec))
|
||||
|
||||
(defn- resolve-attrs
|
||||
[rng defaults attrs]
|
||||
(merge (reduce-kv (fn [result key spec]
|
||||
(assoc result key (resolve-attr rng spec)))
|
||||
{}
|
||||
defaults)
|
||||
attrs))
|
||||
|
||||
(defn- register-ref!
|
||||
[state label id]
|
||||
(when (some? label)
|
||||
(when (contains? (:refs @state) label)
|
||||
(throw (ex-info (str "duplicate scene label: " label)
|
||||
{:type ::duplicate-label
|
||||
:label label})))
|
||||
(swap! state assoc-in [:refs label] id)))
|
||||
|
||||
(defn- add-object!
|
||||
"Inserts `shape` under the current parent and registers `label`."
|
||||
[state label shape]
|
||||
(let [{:keys [id frame-id]} *parent*]
|
||||
(register-ref! state label (:id shape))
|
||||
(swap! state
|
||||
(fn [scope]
|
||||
(ctst/add-shape (:id shape) shape scope frame-id id nil true)))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Shape constructors
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn- normalize-label-and-attrs
|
||||
"Resolves the one-argument `rect`/`circle` forms at runtime: a map is attrs,
|
||||
any other value is a label. Rejects label types outside keywords and vectors."
|
||||
[label attrs]
|
||||
(cond
|
||||
(and (map? label) (empty? attrs)) [nil label]
|
||||
(nil? label) [nil attrs]
|
||||
(or (keyword? label) (vector? label)) [label attrs]
|
||||
:else (throw (ex-info "rect label must be a keyword or vector"
|
||||
{:type ::invalid-label
|
||||
:label label}))))
|
||||
|
||||
(defn rect!
|
||||
"Adds one rectangle to the scope and returns its uuid.
|
||||
Runtime primitive behind the `rect` macro."
|
||||
[state defaults label attrs]
|
||||
(let [state (ensure-state state)
|
||||
[label attrs] (normalize-label-and-attrs label attrs)
|
||||
rng (:rng @state)
|
||||
attrs (resolve-attrs rng (:rect defaults) attrs)
|
||||
id (rng-uuid rng)
|
||||
shape (cts/setup-shape (merge attrs
|
||||
{:id id
|
||||
:type :rect}))]
|
||||
(add-object! state label shape)
|
||||
id))
|
||||
|
||||
(defn path!
|
||||
"Adds one path to the scope and returns its uuid.
|
||||
Runtime primitive behind the `path` macro."
|
||||
[state defaults label attrs]
|
||||
(let [state (ensure-state state)
|
||||
[label attrs] (normalize-label-and-attrs label attrs)
|
||||
rng (:rng @state)
|
||||
attrs (resolve-attrs rng (:path defaults) attrs)
|
||||
id (rng-uuid rng)
|
||||
shape (cts/setup-shape (merge attrs
|
||||
{:id id
|
||||
:type :path}))]
|
||||
(add-object! state label shape)
|
||||
id))
|
||||
|
||||
(defn circle!
|
||||
"Adds one circle to the scope and returns its uuid.
|
||||
Runtime primitive behind the `circle` macro."
|
||||
[state defaults label attrs]
|
||||
(let [state (ensure-state state)
|
||||
[label attrs] (normalize-label-and-attrs label attrs)
|
||||
rng (:rng @state)
|
||||
attrs (resolve-attrs rng (:circle defaults) attrs)
|
||||
id (rng-uuid rng)
|
||||
shape (cts/setup-shape (merge attrs
|
||||
{:id id
|
||||
:type :circle}))]
|
||||
(add-object! state label shape)
|
||||
id))
|
||||
|
||||
(def ^:private frame-bounds-keys
|
||||
"Frames do not derive their bounds from children."
|
||||
[:x :y :width :height])
|
||||
|
||||
(def ^:private group-geometry-keys
|
||||
"Group geometry derives from its children."
|
||||
[:x :y :width :height :selrect :points])
|
||||
|
||||
(def ^:private bool-geometry-keys
|
||||
"Bool geometry, content and placement derive from its children."
|
||||
[:x :y :width :height :selrect :points :content])
|
||||
|
||||
(def ^:private bool-transform-keys
|
||||
"Construction does not accept transforms: `path/update-geometry` would
|
||||
pivot them on the empty pre-finalization selrect."
|
||||
[:transform :transform-inverse :rotation :flip-x :flip-y])
|
||||
|
||||
(defn- reject-attrs!
|
||||
"Throws when `attrs` carries any of `attr-keys`. Shared by containers
|
||||
whose geometry or content derives from their children."
|
||||
[attrs id error-type message attr-keys]
|
||||
(doseq [key attr-keys]
|
||||
(when (some? (get attrs key))
|
||||
(throw (ex-info message
|
||||
{:type error-type
|
||||
:id id
|
||||
:key key})))))
|
||||
|
||||
(defn- setup-frame
|
||||
[id attrs]
|
||||
(when-not (map? attrs)
|
||||
(throw (ex-info "frame attrs must be a map; the label form takes the attrs map next"
|
||||
{:type ::invalid-attrs
|
||||
:id id})))
|
||||
(doseq [key frame-bounds-keys]
|
||||
(let [value (get attrs key)]
|
||||
(cond
|
||||
(nil? value)
|
||||
(throw (ex-info "frame requires :x :y :width :height"
|
||||
{:type ::frame-bounds
|
||||
:id id
|
||||
:missing key}))
|
||||
|
||||
(not (d/num? value))
|
||||
(throw (ex-info "frame bounds must be numbers"
|
||||
{:type ::frame-bounds
|
||||
:id id
|
||||
:invalid key})))))
|
||||
(cts/setup-shape (assoc attrs :id id :type :frame)))
|
||||
|
||||
(defn- setup-group
|
||||
[id attrs]
|
||||
(when-not (map? attrs)
|
||||
(throw (ex-info "group attrs must be a map; the label form takes the attrs map next"
|
||||
{:type ::invalid-attrs
|
||||
:id id})))
|
||||
(reject-attrs! attrs id ::group-geometry
|
||||
"group derives its geometry from children"
|
||||
group-geometry-keys)
|
||||
(cts/setup-shape (assoc attrs :id id :type :group)))
|
||||
|
||||
(defn- setup-bool
|
||||
[id attrs]
|
||||
(when-not (map? attrs)
|
||||
(throw (ex-info "bool attrs must be a map; the label form takes the attrs map next"
|
||||
{:type ::invalid-attrs
|
||||
:id id})))
|
||||
(reject-attrs! attrs id ::bool-geometry
|
||||
"bool derives its geometry and content from children"
|
||||
bool-geometry-keys)
|
||||
(reject-attrs! attrs id ::bool-transform
|
||||
"bool construction does not accept transforms"
|
||||
bool-transform-keys)
|
||||
(when-not (contains? cts/bool-types (:bool-type attrs))
|
||||
(throw (ex-info "bool requires :bool-type :union :difference :intersection or :exclude"
|
||||
{:type ::bool-type
|
||||
:id id
|
||||
:bool-type (:bool-type attrs)
|
||||
:supported cts/bool-types})))
|
||||
(cts/setup-shape (assoc attrs :id id :type :bool)))
|
||||
|
||||
(defn- bool-head
|
||||
"Canonical style head: the first child is the base for `:difference`, the
|
||||
last child otherwise."
|
||||
[bool-shape children]
|
||||
(if (= :difference (:bool-type bool-shape))
|
||||
(first children)
|
||||
(last children)))
|
||||
|
||||
(defn- finalize-bool!
|
||||
[state id]
|
||||
(let [scope @state
|
||||
bool (get-in scope [:objects id])
|
||||
children (mapv (:objects scope) (:shapes bool))]
|
||||
(when (empty? children)
|
||||
(throw (ex-info "bool requires at least one child"
|
||||
{:type ::empty-bool
|
||||
:id id})))
|
||||
(when-let [frame (first (filter cfh/frame-shape? children))]
|
||||
(throw (ex-info "bool children cannot be frames"
|
||||
{:type ::frame-in-bool
|
||||
:id id
|
||||
:child (:id frame)})))
|
||||
(let [inherited (d/without-nils
|
||||
(select-keys (bool-head bool children)
|
||||
path/bool-style-properties))
|
||||
supplied (d/without-nils
|
||||
(select-keys (get-in scope [:container-attrs id])
|
||||
path/bool-style-properties))
|
||||
bool (-> bool (merge inherited) (merge supplied))
|
||||
|
||||
;; `path/update-bool-shape` dispatches to `path/wasm:calc-bool-content`,
|
||||
;; the renderer override, so scene content would depend on renderer
|
||||
;; initialization and A/B configuration. Compose the pure helpers:
|
||||
;; scene generation is untimed and must not vary with the renderer
|
||||
;; being compared.
|
||||
content (path/calc-bool-content bool (:objects scope))
|
||||
bool (path/update-geometry bool content)]
|
||||
(swap! state assoc-in [:objects id] bool))))
|
||||
|
||||
(defn- finalize-group!
|
||||
[state id]
|
||||
(let [scope @state
|
||||
group (get-in scope [:objects id])
|
||||
children (mapv (:objects scope) (:shapes group))]
|
||||
(when (empty? children)
|
||||
(throw (ex-info "group requires at least one child"
|
||||
{:type ::empty-group
|
||||
:id id})))
|
||||
(let [group (if (:masked-group group)
|
||||
(gsh/update-mask-selrect group children)
|
||||
(gsh/update-group-selrect group children))]
|
||||
(swap! state assoc-in [:objects id] group))))
|
||||
|
||||
(def ^:private container-specs
|
||||
"Per-container behavior: how to build it, which parent scope it opens,
|
||||
and how to finalize it after the body. `:keep-attrs?` records the raw
|
||||
attrs in the scope so the finalizer can let supplied styles win over
|
||||
inherited ones."
|
||||
{:frame {:setup setup-frame
|
||||
:parent (fn [id _parent] {:id id :frame-id id})
|
||||
:finalize (fn [_state _id] nil)}
|
||||
:group {:setup setup-group
|
||||
:parent (fn [id parent] {:id id :frame-id (:frame-id parent)})
|
||||
:finalize finalize-group!}
|
||||
:bool {:setup setup-bool
|
||||
:parent (fn [id parent] {:id id :frame-id (:frame-id parent)})
|
||||
:finalize finalize-bool!
|
||||
:keep-attrs? true}})
|
||||
|
||||
(defn with-container
|
||||
"Creates a frame, group or bool, runs `thunk` with the new parent in
|
||||
scope, finalizes the containers that derive state from children, and
|
||||
returns the container uuid.
|
||||
|
||||
Any exception from the body or the finalizer marks the container
|
||||
unfinished and propagates; `finish!` then rejects the instance even if the
|
||||
caller catches the exception."
|
||||
[type attrs label thunk]
|
||||
(let [{:keys [setup parent finalize keep-attrs?]}
|
||||
(or (get container-specs type)
|
||||
(throw (ex-info "unsupported container type"
|
||||
{:type ::invalid-container
|
||||
:container-type type})))
|
||||
state (ensure-state *state*)
|
||||
id (rng-uuid (:rng @state))
|
||||
shape (setup id attrs)]
|
||||
(when keep-attrs?
|
||||
(swap! state assoc-in [:container-attrs id] attrs))
|
||||
(add-object! state label shape)
|
||||
(binding [*parent* (parent id *parent*)]
|
||||
(try
|
||||
(thunk)
|
||||
(finalize state id)
|
||||
(catch :default cause
|
||||
(swap! state assoc-in [:unfinished id] true)
|
||||
(throw cause))))
|
||||
id))
|
||||
|
||||
(defn finish!
|
||||
"Validates the scope snapshot and returns it."
|
||||
[state]
|
||||
(let [{:keys [objects refs unfinished]} @state]
|
||||
(when (seq unfinished)
|
||||
(throw (ex-info "scene contains an unfinished container"
|
||||
{:type ::unfinished-container
|
||||
:ids (vec (keys unfinished))})))
|
||||
(let [instance {:objects objects
|
||||
:refs refs}]
|
||||
(snapshot/validate! instance)
|
||||
instance)))
|
||||
@ -7,7 +7,7 @@
|
||||
(ns benches.render-wasm.scenes.masks
|
||||
"Fixed grid of masked groups with circle masks and ordered descendants."
|
||||
(:require
|
||||
[benches.render-wasm.builder :as sb :include-macros true]
|
||||
[benches.render-wasm.builder :as sb]
|
||||
[benches.render-wasm.declarations :as decl :include-macros true]
|
||||
[benches.render-wasm.runtime.camera :as camera]))
|
||||
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
(:require
|
||||
[app.common.schema :as sm]
|
||||
[app.common.types.path :as path]
|
||||
[benches.render-wasm.builder :as sb :include-macros true]
|
||||
[benches.render-wasm.builder :as sb]
|
||||
[benches.render-wasm.declarations :as decl :include-macros true]
|
||||
[benches.render-wasm.random :refer [rng-int]]
|
||||
[benches.render-wasm.runtime.camera :as camera]))
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
"Plus signs made of two open line subpaths"
|
||||
(:require
|
||||
[app.common.types.path :as path]
|
||||
[benches.render-wasm.builder :as sb :include-macros true]
|
||||
[benches.render-wasm.builder :as sb]
|
||||
[benches.render-wasm.declarations :as decl :include-macros true]
|
||||
[benches.render-wasm.random :refer [rng-int]]
|
||||
[benches.render-wasm.runtime.camera :as camera]))
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
they render the same scene."
|
||||
(:require
|
||||
[app.common.schema :as sm]
|
||||
[benches.render-wasm.builder :as sb :include-macros true]
|
||||
[benches.render-wasm.builder :as sb]
|
||||
[benches.render-wasm.declarations :as decl :include-macros true]
|
||||
[benches.render-wasm.runtime.camera :as camera]))
|
||||
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
[app.common.types.path :as path]
|
||||
[app.common.types.path.segment :as segm]
|
||||
[app.common.uuid :as uuid]
|
||||
[benches.render-wasm.builder :as b :include-macros true]
|
||||
[benches.render-wasm.builder :as b]
|
||||
[benches.render-wasm.snapshot :as snapshot]
|
||||
[cljs.test :as t :include-macros true]
|
||||
[performance-tests.render-wasm.test-helpers :as helpers]))
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
Fixed random sequences preserve scene draws during generator extraction."
|
||||
(:require
|
||||
[app.common.uuid :as uuid]
|
||||
[benches.render-wasm.builder :as b :include-macros true]
|
||||
[benches.render-wasm.builder :as b]
|
||||
[benches.render-wasm.declarations :as decl]
|
||||
[benches.render-wasm.random :as random]
|
||||
[benches.render-wasm.scenes.rects]
|
||||
@ -101,6 +101,32 @@
|
||||
(t/is (= 7 (:x rect)))
|
||||
(t/is (= [] (:fills rect)))))
|
||||
|
||||
(t/deftest shapes-use-their-defaults-before-drawing-ids
|
||||
(doseq [[type low high construct] [[:rect 0.1 0.2 #(b/rect %)]
|
||||
[:path 0.3 0.4 #(b/path %)]
|
||||
[:circle 0.5 0.6 #(b/circle %)]]
|
||||
attrs [{} {:opacity 0.75}]]
|
||||
(let [rng (random/create 7)
|
||||
opacity (random/round3 (random/rng-float rng low high))
|
||||
id (random/rng-uuid rng)
|
||||
instance (b/scene {:seed 7
|
||||
:defaults {:rect {:opacity (b/gen-float 0.1 0.2)}
|
||||
:path {:opacity (b/gen-float 0.3 0.4)}
|
||||
:circle {:opacity (b/gen-float 0.5 0.6)}}}
|
||||
(construct attrs))
|
||||
shape (first (helpers/root-shapes instance))]
|
||||
(t/is (= {:type type :id id :opacity (get attrs :opacity opacity)}
|
||||
(select-keys shape [:type :id :opacity]))))))
|
||||
|
||||
(t/deftest scene-and-shape-expressions-evaluate-once-in-order
|
||||
(let [events (atom [])
|
||||
instance (b/scene (do (swap! events conj :params) {:seed 1})
|
||||
(b/rect (do (swap! events conj :label) :hero)
|
||||
(do (swap! events conj :attrs) {:x 11})))
|
||||
hero (get-in instance [:objects (get (:refs instance) :hero)])]
|
||||
(t/is (= [:params :label :attrs] @events))
|
||||
(t/is (= 11 (:x hero)))))
|
||||
|
||||
(t/deftest ordinary-forms-work-inside-the-scope
|
||||
(let [instance (b/scene {:seed 1}
|
||||
(let [first-id (b/rect)]
|
||||
@ -260,4 +286,3 @@
|
||||
(b/circle {:x 0 :y 0 :width 20 :height 20 :rx 5 :ry 7}))
|
||||
circle (first (helpers/root-shapes instance))]
|
||||
(t/is (= [5 7] [(:rx circle) (:ry circle)]))))
|
||||
|
||||
|
||||
@ -7,7 +7,8 @@
|
||||
(ns performance-tests.render-wasm.containers-test
|
||||
(:require
|
||||
[app.common.uuid :as uuid]
|
||||
[benches.render-wasm.builder :as builder :include-macros true]
|
||||
[benches.render-wasm.builder :as builder]
|
||||
[benches.render-wasm.random :as random]
|
||||
[cljs.test :as t :include-macros true]
|
||||
[performance-tests.render-wasm.test-helpers :as helpers]))
|
||||
|
||||
@ -102,6 +103,35 @@
|
||||
(t/is (= (get refs [:g 1]) (first (helpers/child-ids instance (get refs :hero)))))
|
||||
(t/is (= 1 (count (helpers/child-ids instance uuid/zero))))))
|
||||
|
||||
(t/deftest container-expressions-evaluate-once-in-order
|
||||
(let [events (atom [])
|
||||
instance (builder/scene {:seed 1}
|
||||
(builder/frame [:frame (do (swap! events conj :label) 3)]
|
||||
(do (swap! events conj :attrs)
|
||||
{:x 0 :y 0 :width 20 :height 20})
|
||||
(swap! events conj :body)
|
||||
(builder/rect {:x 10})))
|
||||
frame-id (get (:refs instance) [:frame 3])]
|
||||
(t/is (= [:attrs :label :body] @events))
|
||||
(t/is (= [10] (mapv :x (helpers/children-of instance frame-id))))))
|
||||
|
||||
(t/deftest invalid-container-attrs-retain-seeded-construction-order
|
||||
(doseq [construct [#(builder/frame :hero :invalid)
|
||||
#(builder/group :hero :invalid)
|
||||
#(builder/bool :hero :invalid)]]
|
||||
(let [rng (random/create 7)
|
||||
failed-id (random/rng-uuid rng)
|
||||
next-id (random/rng-uuid rng)
|
||||
failure (volatile! nil)
|
||||
state (helpers/run-scope
|
||||
{:seed 7}
|
||||
(fn []
|
||||
(vreset! failure (helpers/failure-data construct))
|
||||
(builder/rect)))
|
||||
instance (builder/finish! state)]
|
||||
(t/is (= {:type ::builder/invalid-attrs :id failed-id} @failure))
|
||||
(t/is (= [next-id] (helpers/child-ids instance uuid/zero))))))
|
||||
|
||||
(t/deftest empty-group-is-rejected
|
||||
(let [data (helpers/failure-data #(builder/scene {:seed 1} (builder/group {})))]
|
||||
(t/is (= ::builder/empty-group (:type data)))))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user