✨ Extend generated component checks to structure and geometry

Cover text, layouts, nesting, duplication and transforms with independent
geometry and override predictions. Compare raw design geometry within a
pixel tolerance and keep exact undo snapshots. Add corrupt-state controls
and replay shrunk programs through the async frontend.

Route pure-runner changes by file so later library scenarios can share
ordered sync and inverse changes. Refresh every frontend file on replay.

Final-generator MVP and structure gates each pass 5000 scenarios, seed712.
Isolated stage check: 40 tests, 156 assertions. The full working-tree
frontend run passes 1528 tests and 8877 assertions.
Existing golden changes record reviewed fixes; raw undo stays strict.

AI-assisted-by: GPT-6
This commit is contained in:
alonso.torres 2026-10-24 14:35:45 +02:00
parent 9f42e20a15
commit fbc0b7fd93
13 changed files with 889 additions and 191 deletions

View File

@ -0,0 +1,101 @@
;; 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.common.test-helpers.composable.comp.files
"File routing for the pure composable runner, including library inverses."
(:require
[app.common.files.changes-builder :as pcb]
[app.common.logic.libraries :as cll]
[app.common.test-helpers.composable.core :as tm]
[app.common.test-helpers.files :as thf]))
(defn all-files
[situation]
(assoc (tm/aux-files situation) (:id (tm/file situation)) (tm/file situation)))
(defn- with-files
[situation files]
(let [id (:id (tm/file situation))]
(-> situation
(tm/with-file (get files id))
(tm/with-aux-files (dissoc files id)))))
(defn route
"Tag changes destined for another file, preserving their original order."
[changes file-id]
(let [tag #(mapv (fn [change] (assoc change ::destination file-id)) %)]
(-> changes (update :redo-changes tag) (update :undo-changes tag))))
(defn route-update-main
[changes current-id main-id]
(let [tag (fn [change]
(cond-> (dissoc change :local-change?)
(and (not (:local-change? change)) (not= current-id main-id))
(assoc ::destination main-id)))
tag-all #(mapv tag %)]
(-> changes (update :redo-changes tag-all) (update :undo-changes tag-all))))
(defn apply-changes
"Apply each consecutive run to its file, stripping runner routing metadata."
([situation changes] (apply-changes situation changes :redo-changes))
([situation changes side]
(let [current-id (:id (tm/file situation))]
(with-files situation
(reduce (fn [files group]
(let [id (or (::destination (first group)) current-id)
file (get files id)]
(when-not file
(throw (ex-info "Missing composable change destination" {:file-id id})))
(assoc files id
(thf/apply-changes file
{:redo-changes (mapv #(dissoc % ::destination) group)}
:validate? false))))
(all-files situation)
(partition-by #(or (::destination %) current-id) (get changes side)))))))
(defn changed-components
[before changes]
(let [current-id (:id (tm/file before))
files (all-files before)]
(into #{}
(mapcat (fn [[id group]]
(cll/changed-components (:data (get files id)) {:redo-changes group})))
(group-by #(or (::destination %) current-id) (:redo-changes changes)))))
(defn- sync-local
[situation file-id ids]
(let [files (all-files situation)
result (cll/generate-sync-components-cascade (pcb/empty-changes) file-id ids files
(:id (tm/file situation)))
changes (route (:changes result) file-id)]
{:situation (with-files situation (assoc-in files [file-id :data] (:data result)))
:syncs (if (seq (:redo-changes changes)) [changes] [])}))
(defn sync-world
"Settle changed library mains first, pull their copies into the current
file, then settle local inputs. Each cascade joins the same undo group."
[situation component-ids]
(let [current-id (:id (tm/file situation))
owners (into {} (mapcat (fn [[id file]]
(map #(vector % id) (keys (get-in file [:data :components])))))
(all-files situation))
by-file (group-by owners component-ids)
foreign (sort-by (comp str key) (dissoc by-file current-id))
result
(reduce (fn [{:keys [situation syncs]} [library-id ids]]
(let [settled (sync-local situation library-id ids)
situation (:situation settled)
pulled (cll/generate-sync-file-cascade
(pcb/empty-changes) nil :components current-id nil library-id
(all-files situation) current-id)
changes (:changes pulled)]
{:situation (apply-changes situation changes)
:syncs (cond-> (into syncs (:syncs settled))
(seq (:redo-changes changes)) (conj changes))}))
{:situation situation :syncs []} foreign)
local (sync-local (:situation result) current-id (get by-file current-id []))]
{:situation (:situation local)
:syncs (into (:syncs result) (:syncs local))}))

View File

@ -28,6 +28,7 @@
[app.common.math :as mth]
[app.common.path-names :as cpn]
[app.common.test-helpers.components :as thc]
[app.common.test-helpers.composable.comp.files :as cf]
[app.common.test-helpers.composable.core :as tm]
[app.common.test-helpers.compositions :as tho]
[app.common.test-helpers.files :as thf]
@ -832,13 +833,6 @@
;; the order the sync built them.
;; ---------------------------------------------------------------------------
(defn- strip-local-change
[changes]
(let [strip (partial mapv #(dissoc % :local-change?))]
(-> changes
(update :redo-changes strip)
(update :undo-changes strip))))
(defrecord UpdateMain [target]
IComponentOperation
(op-kind [_] :user)
@ -854,10 +848,9 @@
(cll/generate-sync-shape-inverse (libraries situation)
container
shape-id)
(strip-local-change))
file' (thf/apply-changes the-file changes :validate? false)]
(cf/route-update-main (:id the-file) (:component-file (get-in page [:objects shape-id]))))
situation (cf/apply-changes situation changes)]
(-> situation
(tm/with-file file')
(record-changes (when (seq (:redo-changes changes)) changes))
(tm/record-application this {:target target :updated shape-id})))))
@ -1139,12 +1132,12 @@
;; ---------------------------------------------------------------------------
(defn swap-target-id
"The component id `swap-shape`'s `to` names: a lineage name (a string) or a
component label (a keyword)."
"Resolve a lineage name, component label, or situation-to-component fn."
[situation to]
(if (keyword? to)
(thi/id to)
(lineage-component-id situation to)))
(cond
(fn? to) (to situation)
(keyword? to) (thi/id to)
:else (lineage-component-id situation to)))
(defrecord SwapShape [target to keep-touched?]
IComponentOperation

View File

@ -33,6 +33,7 @@
(:require
[app.common.files.changes-builder :as pcb]
[app.common.logic.libraries :as cll]
[app.common.test-helpers.composable.comp.files :as cf]
[app.common.test-helpers.composable.comp.nodes :as n]
[app.common.test-helpers.composable.comp.undo-check :as uc]
[app.common.test-helpers.composable.core :as tm]
@ -46,19 +47,15 @@
`old-data`, the file data before `changes` applied."
cll/changed-components)
(defn- libraries
"The situation's files by id, with `file` as its current file."
[situation file]
(assoc (tm/aux-files situation) (:id file) file))
(defn- validate-settled!
"Validate the situation's file, resolving components against its other
files. Only a settled file is valid: between two passes of a sync cascade,
an outer copy can still point at the component its main no longer uses.
Throws when the file is invalid."
[situation]
(let [file (tm/file situation)]
(thf/validate-file! file (libraries situation file))
(let [files (cf/all-files situation)]
(doseq [file (vals files)]
(thf/validate-file! file files))
situation))
(defn sync-cascade
@ -80,14 +77,10 @@
:passes passes}))
(defn- sync-file
"Sync `situation`'s file, starting from the components that `changes`
modified (judged against `old-data`), and validate the result. Returns
`[situation syncs]`, with `syncs` the applied sync changes in order."
[situation old-data changes]
(let [{:keys [file syncs]} (sync-cascade (tm/file situation)
(tm/aux-files situation)
(changed-components old-data changes))]
[(validate-settled! (tm/with-file situation file)) syncs]))
[situation before changes]
(let [{:keys [situation syncs]}
(cf/sync-world situation (cf/changed-components before changes))]
[(validate-settled! situation) syncs]))
(defn- push-undo-group
"Add `group`, the changes of one user operation and its syncs in order, to
@ -101,19 +94,9 @@
(assoc ::redo-stack [])))))
(defn- apply-group
"Apply to the situation's file the `side` changes (`:undo-changes` or
`:redo-changes`) of each changes in `group`, in order, and validate the
result."
[situation side group]
(let [file (reduce (fn [file changes]
(thf/apply-changes file
{:redo-changes (get changes side)}
:validate? false))
(tm/file situation)
group)]
(-> situation
(tm/with-file file)
(validate-settled!))))
(validate-settled!
(reduce #(cf/apply-changes %1 %2 side) situation group)))
(defn- move-group
"Move the latest group of the `from` stack to the `to` stack, applying its
@ -290,7 +273,7 @@
(when `:sync?`), pushes its undo group, and gets its round trip (when
`:undo-check?`). An assembly operation moves the round trip's baseline."
[situation op kind {:keys [sync? undo-check?]}]
(let [old-data (:data (tm/file situation))
(let [before-situation situation
before (when undo-check? (snapshot situation))
depth (undo-depth situation)
[recorded? changes situation] (n/take-changes (tm/apply-to op situation))]
@ -298,12 +281,12 @@
(cond-> (if (nil? changes)
situation
(let [[situation syncs] (if sync?
(sync-file situation old-data changes)
(sync-file situation before-situation changes)
[situation []])]
(cond-> (push-undo-group situation (into [changes] syncs))
(not sync?)
(update ::pending-components (fnil into #{})
(changed-components old-data changes)))))
(cf/changed-components before-situation changes)))))
(and undo-check? (= kind :user))
(check-step op before depth)
@ -388,13 +371,9 @@
"Sync selected pending components in the latest user's undo group."
([situation] (settle situation (pending-components situation)))
([situation component-ids]
(let [{:keys [file syncs]} (sync-cascade (tm/file situation)
(tm/aux-files situation)
component-ids)
(let [{:keys [situation syncs]} (cf/sync-world situation component-ids)
stack (get situation ::undo-stack [])]
(cond-> (-> situation
(tm/with-file file)
(update ::pending-components #(apply disj (or % #{}) component-ids)))
(cond-> (update situation ::pending-components #(apply disj (or % #{}) component-ids))
(and (seq syncs) (seq stack))
(assoc ::undo-stack (conj (pop stack) (into (peek stack) syncs)))))))

View File

@ -169,25 +169,26 @@
(into (entities-signature :component (:components ed) (:components ad)
#{} (constantly nil)))))))
(defn- same-state?
"Reflow can recompute geometry at machine precision after undo. Keep
snapshots exact, allowing only absolute geometry noise below 1e-10."
([expected actual] (same-state? expected actual false))
([expected actual geometry?]
(defn same-state?
"Compare exact data with an absolute tolerance only in shape geometry.
Undo defaults to 1e-10; design equivalence can supply its own tolerance."
([expected actual] (same-state? expected actual 1e-10 false))
([expected actual tolerance] (same-state? expected actual tolerance false))
([expected actual tolerance geometry?]
(or (= expected actual)
(cond
(and geometry? (number? expected) (number? actual))
(mth/close? expected actual 1e-10)
(mth/close? expected actual tolerance)
(and geometry? (gmt/matrix? expected) (gmt/matrix? actual))
(every? #(same-state? (get expected %) (get actual %) true) [:a :b :c :d :e :f])
(every? #(same-state? (get expected %) (get actual %) tolerance true) [:a :b :c :d :e :f])
(and geometry? (gpt/point? expected) (gpt/point? actual))
(every? #(same-state? (get expected %) (get actual %) true) [:x :y])
(every? #(same-state? (get expected %) (get actual %) tolerance true) [:x :y])
(and (map? expected) (map? actual) (= (set (keys expected)) (set (keys actual))))
(every? (fn [key]
(same-state? (get expected key) (get actual key)
(same-state? (get expected key) (get actual key) tolerance
(or geometry?
(and (contains? expected :parent-id)
(contains? #{:x :y :width :height :rotation :points
@ -195,7 +196,7 @@
(keys expected))
(and (sequential? expected) (sequential? actual) (= (count expected) (count actual)))
(every? true? (map #(same-state? %1 %2 geometry?) expected actual))
(every? true? (map #(same-state? %1 %2 tolerance geometry?) expected actual))
:else false))))

View File

@ -12,6 +12,7 @@
(:require
[app.common.files.helpers :as cfh]
[app.common.logic.shapes :as cls]
[app.common.test-helpers.composable.comp.files :as cf]
[app.common.test-helpers.composable.comp.nodes :as n]
[app.common.test-helpers.composable.comp.runner :as r]
[app.common.test-helpers.composable.comp.setups :as setup]
@ -52,6 +53,22 @@
file (cond-> file (= layout :grid) (tho/place-in-grid-columns root 2 {1 child}))]
(tm/with-file situation file))))
(defn forest
"All live page trees, with stable file/page prefixes for model replay."
[situation]
(vec (for [[file-index [_ file]] (map-indexed vector (sort-by (comp str key) (cf/all-files situation)))
[page-index page-id] (map-indexed vector (get-in file [:data :pages]))
entry (tree (tm/with-file situation (with-meta file {:current-page-id page-id})))]
(update entry :path #(into [:file file-index :page page-index] %)))))
(defn- all-objects
[situation]
(reduce (fn [objects component] (merge (:objects component) objects))
(into {} (map (juxt (comp :id :shape) :shape)) (forest situation))
(mapcat #(vals (get-in % [:data :components])) (vals (cf/all-files situation)))))
(defn world
[[components depth opts]]
(thi/reset-idmap!)
@ -88,7 +105,13 @@
(some overridden (:shapes (get objects id)))))
eligible (filterv (fn [{:keys [shape]}]
(case kind
:edit (and (not= :text (:type shape)) (or (not= scope 1) (:shape-ref shape)))
:edit (and (not= :text (:type shape))
(case scope
1 (:shape-ref shape)
2 (and (:shape-ref shape)
(some #(contains? (tm/aux-files situation) (:component-file %))
(cfh/get-parents-with-self objects (:id shape))))
true))
:reset (and (copy-head shape) (live-component shape))
:update-main (and (top-copy shape) (live-component shape) (overridden (:id shape)))
:detach (top-copy shape)
@ -125,7 +148,15 @@
"Resolve one tuple. Empty target sets produce an explicit skip."
[situation model [kind index property value scope :as tuple]]
(let [index (or index 0)
items (candidates situation model kind scope)
items (cond->> (candidates situation model kind scope)
(and (= kind :edit) (= 2 (mod (or property 0) 3)))
(remove (fn [{:keys [shape]}]
;; Variant names encode properties or rename the set.
;; Those actions have their own nodes, not scalar edits.
(or (ctk/is-variant-container? shape)
(:variant-id shape)
(get-in (tm/file situation)
[:data :components (:component-id shape) :variant-id])))))
selected (when (seq items) (nth items (mod index (count items))))
target (when selected (constantly (get-in selected [:shape :id])))
property (nth [:fills :opacity :name] (mod (or property 0) 3))
@ -162,6 +193,7 @@
:status (if (or op (= kind :sync)) :executed :skip)}
selected (assoc :target (:path selected))
(:into selected) (assoc :into (:into selected))
(:destination selected) (assoc :to (get-in selected [:destination :path]))
(and (nil? op) (not= kind :sync))
(assoc :reason (str "No eligible " (name kind) " target")))}))
@ -177,30 +209,34 @@
(defn settle-state
[{:keys [situation model checkpoint] :as state}]
(let [settled (r/settle situation)
all (keys (get-in (tm/file situation) [:data :components]))
full-before (r/sync-cascade (tm/file situation) (tm/aux-files situation) all)
full (r/sync-cascade (tm/file settled) (tm/aux-files settled)
all)]
model (-> model
(shadow/parent-orientation (all-objects situation) (all-objects settled))
(shadow/settle (all-objects settled)))
all (mapcat #(keys (get-in % [:data :components])) (vals (cf/all-files situation)))
full-before (:situation (cf/sync-world situation all))
full (:situation (cf/sync-world settled all))
settled-design (oracle/canonical settled)
full-before-design (oracle/canonical full-before)
full-design (oracle/canonical full)]
(oracle/check settled model)
(when-not (= (oracle/canonical settled)
(oracle/canonical (tm/with-file situation (:file full-before))))
(oracle/fail :I6 {:diff (thn/diff (oracle/canonical settled) (oracle/canonical (tm/with-file situation (:file full-before))))}))
(when-not (= (oracle/canonical settled)
(oracle/canonical (tm/with-file settled (:file full))))
(when-not (oracle/equivalent? settled-design full-before-design)
(oracle/fail :I6 {:diff (thn/diff settled-design full-before-design)}))
(when-not (oracle/equivalent? settled-design full-design)
(oracle/fail :I5 {}))
(when checkpoint (assert-round-trip checkpoint settled))
(assoc state :situation settled :checkpoint nil)))
(cond-> (assoc state :situation settled :model model :checkpoint nil)
(and (not= model (:model state)) (seq (:undo state)))
(assoc-in [:undo (dec (count (:undo state))) :after] model))))
(defn- update-model
[model before after {:keys [shape property value op]} kind]
(let [objects (:objects (thf/current-page (tm/file before)))
(let [objects (all-objects before)
id (:id shape)]
(case kind
:edit (shadow/edit model shape property value)
:reset (shadow/clear-subtree model objects id)
:update-main (shadow/update-main model objects id)
:detach (shadow/detach model objects
(:objects (thf/current-page (tm/file after))) id)
:update-main (shadow/update-main model objects (all-objects after) id)
:detach (shadow/detach model objects (all-objects after) id)
:text (shadow/edit-text model shape (:content (n/text-edit-commit shape (:edit op) (:value op))))
:duplicate (shadow/duplicate model objects id
(:objects (thf/current-page (tm/file after)))
@ -236,13 +272,17 @@
:undo (:before (peek history))
:redo (:after (peek redo))
(update-model model situation after resolved kind))
next-model (or next-model model)
next-model (cond-> (or next-model model)
(not (#{:undo :redo} kind))
(shadow/parent-orientation (all-objects situation) (all-objects after)))
next-model (shadow/settle next-model (all-objects after))
state (-> state
(assoc :situation after :model next-model)
(update-in [:executed kind] (fnil inc 0)))]
(oracle/check after next-model (empty? (r/pending-components after)))
(case kind
:sync state
:sync (cond-> state
(seq history) (assoc-in [:undo (dec (count history)) :after] next-model))
:undo (-> state (assoc :undo (pop history)) (update :redo conj (peek history)))
:redo (-> state (assoc :redo (pop redo)) (update :undo conj (peek redo)))
(cond-> state

View File

@ -13,11 +13,14 @@
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh]
[app.common.math :as mth]
[app.common.test-helpers.composable.comp.files :as cf]
[app.common.test-helpers.composable.comp.undo-check :as uc]
[app.common.test-helpers.composable.core :as tm]
[app.common.test-helpers.composable.shadow :as shadow]
[app.common.test-helpers.files :as thf]
[app.common.test-helpers.normalize :as thn]
[app.common.test-helpers.texts :as thtx]
[app.common.types.component :as ctk]
[app.common.types.container :as ctn]
[app.common.types.file :as ctf]
[app.common.types.modifiers :as ctm]
@ -53,20 +56,30 @@
height-owned (conj :height)
(or layout-child width-owned height-owned) (into #{:points :selrect}))))
(defn- matrix-effect
"Matrix coefficients have no pixel unit. Compare their effect over the
shape's local bounds, so the geometry tolerance has the same unit."
[shape matrix]
(let [half-width (/ (:width shape) 2)
half-height (/ (:height shape) 2)]
(mapv #(gpt/transform % (or matrix (gmt/matrix)))
[(gpt/point (- half-width) (- half-height))
(gpt/point half-width (- half-height))
(gpt/point half-width half-height)
(gpt/point (- half-width) half-height)])))
(defn design-snapshot
"Sync equivalence ignores remote-sync bookkeeping, text run splitting and
geometry noise below 0.01 px. Undo snapshots deliberately keep them."
"Design inputs without derived placement, dimensions and bookkeeping.
Geometry remains unrounded; equivalent? applies its tolerance directly."
[situation]
(walk/postwalk
(fn [value]
(if (and (map? value) (contains? value :parent-id))
(let [round-geometry (fn [value]
(walk/postwalk #(if (number? %) (/ (mth/round (* % 100)) 100) %) value))]
(cond-> (reduce (fn [shape key]
(if (contains? shape key) (update shape key round-geometry) shape))
(dissoc value :remote-synced)
[:x :y :width :height :points :selrect :transform :transform-inverse :rotation])
(= :text (:type value)) (update :content thtx/view)))
(cond-> (-> value
(dissoc :remote-synced)
(update :flip-x boolean)
(update :flip-y boolean))
(= :text (:type value)) (update :content thtx/view))
value))
(walk/postwalk
(fn [value]
@ -77,7 +90,13 @@
[id (-> (gsh/move shape
(gpt/point (- (get-in objects [(:parent-id shape) :x] 0))
(- (get-in objects [(:parent-id shape) :y] 0))))
(as-> shape (apply dissoc shape (derived-attributes objects shape)))
(as-> moved (assoc moved
:transform (matrix-effect shape (:transform moved))
:transform-inverse (matrix-effect shape (:transform-inverse moved))))
(as-> shape (apply dissoc shape
(cond-> (derived-attributes objects shape)
(and (:component-id shape) (:shape-ref shape))
(into #{:x :y :points :selrect}))))
(update :constraints-h #(or % (gsh/default-constraints-h shape)))
(update :constraints-v #(or % (gsh/default-constraints-v shape))))])) objects)))
value))
@ -111,6 +130,20 @@
(name-tree (:objects component) (:main-instance-id component) (conj path :main)))))
(walk/postwalk #(if (uuid? %) (get @ids % %) %) (design-snapshot situation)))))
(defn equivalent?
"Compare canonical data, allowing 0.01 only within shape geometry."
[expected actual]
(uc/same-state? expected actual 0.01))
(defn- orientation-matrix
"Resize uses constraints and dimensions. Parent placement supplies axis
directions, without applying residual minimum-size scale a second time."
[shape]
(let [{:keys [a b c d]} (or (:transform shape) (gmt/matrix))
horizontal (gpt/unit (gpt/point a b))
vertical (gpt/unit (gpt/point c d))]
(gmt/matrix (:x horizontal) (:y horizontal) (:x vertical) (:y vertical) 0 0)))
(defn- child-placement
"Place a main child against its expected copy parent. Only lower-level
constraint and matrix math is shared with production, not sync code."
@ -127,14 +160,21 @@
main (get resized (:id main))
main-parent (get resized (:id main-parent))
center (gsh/shape->center main-parent)
turn (gmt/multiply (or (:transform parent) (gmt/matrix))
(or (:transform-inverse main-parent) (gmt/matrix)))
turn (gmt/multiply (orientation-matrix parent)
(gmt/inverse (orientation-matrix main-parent)))
placed (if (gmt/unit? turn) main
(gsh/apply-transform main (gmt/transform-in center turn)))]
(gsh/apply-transform main (gmt/transform-in center turn)))
;; Flip flags follow the parent's composition history. A single
;; large turn can toggle both flags where several small turns do not.
flipped? (fn [key]
(odd? (count (filter identity [(get main key) (get main-parent key)
(get parent key)]))))]
(-> placed
(gsh/move (gpt/subtract (gsh/shape->center parent) center))
(assoc :rotation (mod (+ (or (:rotation main) 0)
(- (or (:rotation parent) 0) (or (:rotation main-parent) 0))) 360)))))
(- (or (:rotation parent) 0) (or (:rotation main-parent) 0))) 360)
:flip-x (flipped? :flip-x)
:flip-y (flipped? :flip-y)))))
(defn- with-derived-geometry
[objects shape expected]
@ -157,19 +197,42 @@
(fail :I1 {:shape (:name shape) :reason :parent-cycle}))
(if (contains? (:touched shape) :geometry-group)
shape
(with-derived-geometry
objects shape
(if-let [main (ctf/find-ref-shape file container libraries shape :with-context? true)]
(let [parent (get objects (:parent-id shape))
main-objects (get-in (meta main) [:container :objects])
main-parent (get main-objects (:parent-id main))]
(if (and (:shape-ref parent) main-parent)
(child-placement main main-parent (expected parent (conj visited (:id shape)))
main-objects)
;; Root placement is authored independently of overrides.
(gsh/move main (gpt/subtract (gpt/point (:x shape) (:y shape))
(gpt/point (:x main) (:y main))))))
shape))))]
(let [main (ctf/find-ref-shape file container libraries shape :with-context? true)
parent (get objects (:parent-id shape))
main-objects (get-in (meta main) [:container :objects])
main-parent (get main-objects (:parent-id main))
placed (cond
(nil? main) shape
(and (:shape-ref parent) main-parent)
(child-placement main main-parent (expected parent (conj visited (:id shape)))
main-objects)
:else
;; Ordinary parents left by detach still place their nested copies.
(let [turn (gmt/multiply (or (:transform shape) (gmt/matrix))
(or (:transform-inverse main) (gmt/matrix)))
placed (if (gmt/unit? turn) main
(gsh/apply-transform main (gmt/transform-in (gsh/shape->center main) turn)))]
(-> placed
(gsh/move (gpt/subtract (gsh/shape->center shape) (gsh/shape->center placed)))
;; This is the placement matrix just applied.
;; Re-decomposing a .01px frame can add scale
;; noise that grows in its larger children.
(assoc :transform (:transform shape)
:transform-inverse (:transform-inverse shape)
:rotation (:rotation shape)
:flip-x (:flip-x shape) :flip-y (:flip-y shape)))))
content (when (ctk/swapped? shape)
(ctf/find-content-ref-shape file container libraries shape))
placed (if content
;; Content supplies size; the position source
;; still owns the untransformed top-left anchor.
(-> placed
(gsh/transform-shape
(ctm/change-size placed (:width content) (:height content)))
(gsh/absolute-move (gpt/point (get-in placed [:selrect :x])
(get-in placed [:selrect :y]))))
placed)]
(with-derived-geometry objects shape placed))))]
(expected shape #{}))))
(defn- geometry-differences
@ -217,10 +280,10 @@
(fail :I4 {:shape (:name shape) :group :layout-grid-cells
:expected (grid-cells main) :actual (grid-cells shape)}))))
(defn check
(defn- check-file
"I1–I4 and I9. Before settlement only authored groups/values are checked;
pending main changes may legitimately leave copies stale."
([situation model] (check situation model true))
([situation model] (check-file situation model true))
([situation model settled]
(let [file (tm/file situation)
libraries (assoc (tm/aux-files situation) (:id file) file)]
@ -246,7 +309,11 @@
:when (not (and (= group :name-group) (= :text (:type shape))
(= (:name main) (txt/generate-shape-name (txt/content->text (:content main))))))
;; A copy root's name follows component renaming too.
:let [expected (comparable-values main shape group)
:let [source (if (or (= group :constraints-group)
(.startsWith (name group) "layout-item-"))
(or (ctf/find-ref-shape file container libraries shape) main)
main)
expected (comparable-values source shape group)
actual (comparable-values shape shape group)]]
(when-not (= expected actual)
(fail :I4 {:shape (:name shape) :group group
@ -264,3 +331,13 @@
:expected (select-keys expected [:width :height :points])
:actual (select-keys shape [:width :height :points])}))))))
nil)))
(defn check
"Check both the working file and every library in the generated world."
([situation model] (check situation model true))
([situation model settled]
(let [files (cf/all-files situation)]
(doseq [[id file] files]
(check-file (-> situation (tm/with-file file) (tm/with-aux-files (dissoc files id)))
model settled)))
nil))

View File

@ -8,6 +8,7 @@
"Authored overrides, kept independently of production touched writes.
Each shape id maps groups to the attribute values the user authored."
(:require
[app.common.math :as mth]
[app.common.test-helpers.composable.comp.nodes :as n]
[app.common.test-helpers.texts :as thtx]))
@ -62,7 +63,14 @@
[shape group expected]
(if-let [unit (get text-groups group)]
(not (contains? (thtx/override-units (:content expected) (:content shape)) unit))
(= expected (values shape group))))
(if (= group :geometry-group)
(every? (fn [[key value]]
(let [actual (get shape key)]
(cond
(#{:flip-x :flip-y} key) (= (boolean value) (boolean actual))
(and (number? value) (number? actual)) (mth/close? value actual 1e-10)
:else (= value actual)))) expected)
(= expected (values shape group)))))
(defn edit-text
[model before content]
@ -99,21 +107,54 @@
(reduce clear (dissoc model id) (:shapes (get objects id))))]
(clear model root-id)))
(defn- in-orientation
[values source target]
(reduce (fn [values key]
(cond-> values
(not= (boolean (get source key)) (boolean (get target key)))
(update key not)))
(update values :rotation
#(mod (+ (or % 0) (- (or (:rotation target) 0) (or (:rotation source) 0))) 360))
[:flip-x :flip-y]))
(defn detach
"A detached outer head loses its groups. Surviving nested copies inherit
the authored groups of the near-main links the detach skips."
[model before after root-id]
(letfn [(inherit [shape]
(loop [shape shape groups (get model (:id shape)) seen #{}]
(let [ref (:shape-ref shape)]
(if (or (nil? ref) (contains? seen ref)
(= ref (:shape-ref (get after (:id shape)))))
groups
(recur (get before ref) (merge (get model ref) groups)
(conj seen ref))))))
(let [original shape
new-ref (:shape-ref (get after (:id shape)))
swapped? (contains? (get model (:id shape)) :swap-group)]
(loop [shape shape groups (get model (:id shape)) seen #{}]
(let [ref (:shape-ref shape)
source (get before ref)
inherited (get model ref)
inherited (cond-> inherited
(:geometry-group inherited)
(update :geometry-group in-orientation source original))]
(if (or (nil? ref) (contains? seen ref) (= ref new-ref))
groups
(recur source
(merge (if swapped?
(select-keys inherited
(filter #(or (#{:geometry-group :constraints-group} %)
(.startsWith (name %) "layout-item-"))
(keys inherited)))
inherited) groups)
(conj seen ref)))))))
(visit [result id]
(let [shape (get after id)
groups (when (:shape-ref shape) (inherit (get before id)))
groups (when (:shape-ref shape)
(cond
(= (:shape-ref shape) (:shape-ref (get before id)))
(get model id)
(and (contains? (get model id) :swap-group)
(= (:component-id shape)
(:component-id (get after (:shape-ref shape)))))
;; Leaving the enclosing copy makes this a plain
;; copy of its own main, without position inheritance.
(dissoc (get model id) :swap-group)
:else (inherit (get before id))))
result (cond-> (dissoc result id) (seq groups) (assoc id groups))]
(reduce visit result (:shapes (get before id)))))]
(visit model root-id)))
@ -121,13 +162,93 @@
(defn update-main
"Pushing overrides into a nested copy in a main makes them authored at
that level. Ordinary main shapes do not have touched groups."
[model objects root-id]
(letfn [(push [result id]
(let [shape (get objects id)
ref (get objects (:shape-ref shape))
[model before after root-id]
(letfn [(content-overrides [shape reference]
(when reference
(let [content-groups (remove #(or (#{:geometry-group :constraints-group} %)
(.startsWith (name %) "layout-item-"))
(keys attributes))
groups (into {}
(keep (fn [group]
(let [expected (values reference group)]
(when-not (same-value? shape group expected)
[group (values shape group)]))))
content-groups)
units (when (= :text (:type shape))
(thtx/override-units (:content reference) (:content shape)))]
(reduce (fn [groups [group unit]]
(cond-> groups
(contains? units unit)
(assoc group {:content (:content shape)})))
(cond-> groups (seq units) (assoc :content-group {}))
text-groups))))
(push [result id content?]
(let [shape (get before id)
updated (get after id)
ref (get after (:shape-ref updated))
groups (get model id)
content-main (get before (:shape-ref ref))
pushed-swap? (and content-main (contains? groups :swap-group)
(= (:component-id ref) (:component-id content-main)))
content? (or content? pushed-swap?)
groups (merge (when (and content?
(= (:component-id ref) (:component-id content-main)))
(content-overrides shape content-main))
groups)
root (get before root-id)
main-root (get before (:shape-ref root))
geometry (cond-> (values shape :geometry-group)
(get-in model [(:id main-root) :geometry-group])
(in-orientation root main-root))
expected (when pushed-swap?
(in-orientation (values content-main :geometry-group)
(get before (:parent-id content-main))
(get after (:parent-id ref))))
groups (cond-> groups
(and pushed-swap?
(not (same-value? geometry :geometry-group
(select-keys expected [:rotation :flip-x :flip-y]))))
(assoc :geometry-group geometry))
result (cond-> (dissoc result id)
(and (contains? groups :swap-group)
(not= (:component-id updated) (:component-id ref)))
(assoc id {:swap-group {}})
(and (:shape-ref ref) (seq groups))
(update (:id ref) merge groups))]
(reduce push result (:shapes shape))))]
(push model root-id)))
(update (:id ref) merge
(cond-> groups
(= (:component-id ref)
(:component-id (get after (:shape-ref ref))))
(dissoc :swap-group))))]
(reduce #(push %1 %2 content?) result (:shapes shape))))]
(push model root-id false)))
(defn parent-orientation
"Authored child geometry moves with its parent's orientation. Compare the
parent frames, without reading the child's resulting geometry."
[model before after]
(reduce-kv
(fn [result id groups]
(let [old (get before id)
shape (get after id)
parent (get before (:parent-id old))
new-parent (get after (:parent-id shape))]
(if (and (:geometry-group groups) old shape
(= (:parent-id old) (:parent-id shape)) parent new-parent)
(update-in result [id :geometry-group] in-orientation parent new-parent)
result)))
model model))
(defn settle
"A detached position turns its copies into ordinary shapes. Their former
component choice stops applying; other authored groups stay in place."
[model objects]
(reduce-kv
(fn [result id groups]
(let [shape (get objects id)
position (get objects (:shape-ref shape))
ordinary? (and shape position
(nil? (:component-id shape))
(nil? (:component-id position)))
groups (cond-> groups ordinary? (dissoc :swap-group))]
(if (seq groups) (assoc result id groups) (dissoc result id))))
model model))

View File

@ -7,6 +7,7 @@
(ns common-tests.logic.composable-gen-test
(:require
[app.common.schema.test :as smt]
[app.common.test-helpers.composable.comp.nodes :as n]
[app.common.test-helpers.composable.comp.runner :as r]
[app.common.test-helpers.composable.comp.setups :as setup]
[app.common.test-helpers.composable.core :as tm]
@ -104,14 +105,132 @@
(t/is (pos? (get @coverage kind 0)) (str "No executed " kind)))))
(t/deftest partial-sync-keeps-independent-components-pending
(let [initial (cg/initial-state [3 0])
source (get-in (cg/resolve-op (:situation initial) {} [:nest 0]) [:shape :id])
nested (-> initial (cg/step [:nest 0]) (cg/settle-state))
candidates (cg/candidates (:situation nested) (:model nested) :nest nil)
index (first (keep-indexed #(when (= source (get-in %2 [:shape :id])) %1) candidates))
state (cg/step nested [:nest index])
partial (cg/step state [:sync 1])
settled (cg/settle-state partial)]
(t/is (= 2 (count (r/pending-components (:situation state)))))
(t/is (= 1 (count (r/pending-components (:situation partial)))))
(t/is (empty? (r/pending-components (:situation settled))))))
(let [situation (cg/world [3 0])
mains (take 2 (filter #(get-in % [:shape :main-instance]) (cg/tree situation)))
pending (reduce (fn [s entry]
(r/step s (n/change-property
(constantly (get-in entry [:shape :id])) :fills "#ff0000")))
situation mains)
ids (r/pending-components pending)
partial (r/settle pending [(first (sort-by str ids))])
settled (r/settle partial)]
(t/is (= 2 (count ids)))
(t/is (= 1 (count (r/pending-components partial))))
(t/is (empty? (r/pending-components settled)))))
(t/deftest detach-from-a-deleted-near-main-inherits-only-the-skipped-level
(t/is (map? (cg/run {:world [3 2 {:text true :layout nil}]
:ops [[:add-child 0] [:remove-child 0] [:duplicate 0] [:nest 0]
[:delete 0] [:restore 0] [:text 0 0 1] [:text 1 1 1]
[:resize 0 0 2] [:rotate 0 0 0] [:reset 0]
[:edit 0 0 1 1] [:update-main 0] [:sync 1]
[:detach 0] [:undo] [:redo] [:delete 1 0 0 0]
[:detach 13 0 0 0]]}))))
(t/deftest undo-after-a-main-child-add-and-root-rotation-is-fully-settled
(t/is (map? (cg/run {:world [2 0 {:text true :layout nil}]
:ops [[:add-child 0] [:remove-child 0] [:duplicate 0] [:nest 0]
[:delete 0] [:restore 0] [:text 0 0 1] [:text 1 1 1]
[:resize 0 0 2] [:rotate 0 0 0] [:reset 0]
[:edit 0 0 1 1] [:update-main 0] [:sync 1]
[:detach 0] [:undo] [:redo] [:undo 0 0 0 0]
[:add-child 17 0 0 0] [:rotate 19 0 0 0] [:undo 0 0 0 0]]}))))
(t/deftest minimum-size-transform-sync-shrunk-regression
(t/is (map? (cg/run {:world [2 0 {:text true :layout nil}]
:ops [[:add-child 0] [:remove-child 0] [:duplicate 0] [:nest 0]
[:delete 0] [:restore 0] [:text 0 0 1] [:text 1 1 1]
[:resize 0 0 2] [:rotate 0 0 0] [:reset 0]
[:edit 0 0 1 1] [:update-main 0] [:sync 1]
[:detach 0] [:undo] [:redo] [:rotate 2 0 0 0]
[:resize 7 0 0 0] [:duplicate 0 0 0 0]]}))))
(t/deftest updating-a-rotated-outer-main-composes-authored-child-orientation
(t/is (map? (cg/run {:world [2 0 {:text true :layout nil}]
:ops [[:add-child 0] [:remove-child 0] [:duplicate 0] [:nest 0]
[:delete 0] [:restore 0] [:text 0 0 1] [:text 1 1 1]
[:resize 0 0 2] [:rotate 0 0 0] [:reset 0]
[:edit 0 0 1 1] [:update-main 0] [:sync 1]
[:detach 0] [:undo] [:redo] [:duplicate 0 0 0 0]
[:resize 0 0 0 0] [:nest 0 0 0 0] [:rotate 4 0 0 0]
[:detach 3 0 0 0] [:update-main 4 0 0 0]]}))))
(t/deftest minimum-size-parent-orientation-does-not-amplify-oracle-drift
(t/is (map? (cg/run {:world [3 0 {:text true :layout nil}]
:ops [[:add-child 0] [:remove-child 0] [:duplicate 0] [:nest 0]
[:delete 0] [:restore 0] [:text 0 0 1] [:text 1 1 1]
[:resize 0 0 2] [:rotate 0 0 0] [:reset 0]
[:edit 0 0 1 1] [:update-main 0] [:sync 1]
[:detach 0] [:undo] [:redo] [:edit 4 0 0 0]
[:duplicate 3 0 0 0] [:nest 10 0 0 0] [:rotate 9 0 0 0]
[:update-main 7 0 0 0] [:delete 0 0 0 0]]}))))
(t/deftest detaching-a-rotated-parent-inherits-geometry-in-the-copy-frame
(t/is (map? (cg/run {:world [2 0 {:text true :layout nil}]
:ops [[:add-child 0] [:remove-child 0] [:duplicate 0] [:nest 0]
[:delete 0] [:restore 0] [:text 0 0 1] [:text 1 1 1]
[:resize 0 0 2] [:rotate 0 0 0] [:reset 0]
[:edit 0 0 1 1] [:update-main 0] [:sync 1]
[:detach 0] [:undo] [:redo] [:resize 0 0 0 0]
[:delete 0 0 0 0] [:rotate 0 0 0 0] [:detach 0 0 0 0]]}))))
(t/deftest detach-shadow-does-not-read-the-resulting-child-orientation
(let [before {:root {:id :root :shapes [:child]}
:child {:id :child :shape-ref :near :rotation 90}
:near {:id :near :shape-ref :main :rotation 45}}
after {:root {:id :root :shapes [:child]}
:child {:id :child :shape-ref :main :rotation 0}}
model (shadow/detach {:near {:geometry-group {:rotation 45}}} before after :root)]
(t/is (= 90 (get-in model [:child :geometry-group :rotation])))
(t/is (false? (shadow/same-value? (:child after) :geometry-group
(get-in model [:child :geometry-group]))))))
(t/deftest pushed-swap-shadow-predicts-orientation-without-reading-the-new-child
(let [before {:copy {:id :copy :shape-ref :main :shapes [:child]}
:main {:id :main :rotation 0}
:child {:id :child :parent-id :copy :component-id :target :shape-ref :position
:rotation 45 :width 100 :height 100}
:target-main {:id :target-main :component-id :target :parent-id :page
:rotation 0 :width 100 :height 100}
:page {:id :page :rotation 0}}
after (-> before
(assoc-in [:child :shape-ref] :pushed)
(assoc :pushed (assoc (:target-main before) :id :pushed
:shape-ref :target-main :parent-id :main)))
model (shadow/update-main {:child {:swap-group {}}} before after :copy)
expected (get-in model [:pushed :geometry-group])]
(t/is (= 45 (:rotation expected)))
(t/is (false? (shadow/same-value? (:pushed after) :geometry-group expected)))))
(t/deftest pushed-swap-shadow-predicts-content-without-reading-the-new-child
(let [before {:copy {:id :copy :shape-ref :main :shapes [:child]}
:main {:id :main}
:child {:id :child :parent-id :copy :component-id :target
:fills [:white] :shapes [:leaf]}
:leaf {:id :leaf :parent-id :child :fills [:white]}
:position {:id :position :component-id :target :fills [:green]}
:position-leaf {:id :position-leaf :fills [:green]}}
after (-> before
(assoc-in [:child :shape-ref] :pushed)
(assoc-in [:leaf :shape-ref] :pushed-leaf)
(assoc :pushed {:id :pushed :component-id :target :shape-ref :position
:parent-id :main :fills [:green]}
:pushed-leaf {:id :pushed-leaf :shape-ref :position-leaf
:parent-id :pushed :fills [:green]}))
model (shadow/update-main {:child {:swap-group {}}} before after :copy)]
(doseq [id [:pushed :pushed-leaf]]
(t/is (= {:fills [:white] :hide-fill-on-export nil} (get-in model [id :fill-group])))
(t/is (false? (shadow/same-value? (get after id) :fill-group (get-in model [id :fill-group])))))))
(t/deftest resizing-a-copy-of-an-empty-flex-main-keeps-layout-valid
(t/is (map? (cg/run {:world [3 0 {:text true :layout :flex}]
:ops [[:add-child 0] [:remove-child 0] [:duplicate 0] [:nest 0]
[:delete 0] [:restore 0] [:text 0 0 1] [:text 1 1 1]
[:resize 0 0 2] [:rotate 0 0 0] [:reset 0]
[:edit 0 0 1 1] [:update-main 0] [:sync 1]
[:detach 0] [:undo] [:redo] [:remove-child 0 0 0 0]
[:reset 0 0 0 0] [:nest 0 0 0 0] [:resize 0 0 0 0]]}))))
(t/deftest updating-a-main-after-successive-rotations-keeps-child-flips
(t/is (map? (cg/run {:world [2 1]
:ops [[:rotate 1 0 0 0] [:rotate 1 0 1 0] [:update-main 0 0 0 0]]}))))

View File

@ -7,6 +7,7 @@
(ns common-tests.logic.composable-oracle-test
(:require
[app.common.files.changes-builder :as pcb]
[app.common.geom.matrix :as gmt]
[app.common.geom.modifiers :as gm]
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh]
@ -14,6 +15,8 @@
[app.common.logic.shapes :as cls]
[app.common.test-helpers.components :as thc]
[app.common.test-helpers.composable.comp.nodes :as n]
[app.common.test-helpers.composable.comp.runner :as r]
[app.common.test-helpers.composable.comp.setups :as setup]
[app.common.test-helpers.composable.core :as tm]
[app.common.test-helpers.composable.gen :as cg]
[app.common.test-helpers.composable.oracle :as oracle]
@ -27,6 +30,65 @@
(t/use-fixtures :each thi/test-fixture)
(t/deftest sync-equivalence-compares-geometry-without-rounding-boundaries
(let [initial (setup/simple-component-with-labeled-copy)
file (ths/update-shape (tm/file initial) :copy-child :opacity 0.4)
page (thf/current-page file)
id (thi/id :copy-child)
move-to (fn [x]
(tm/with-file initial
(thf/apply-changes file
(cls/generate-update-shapes
(pcb/empty-changes nil (:id page)) [id]
#(gsh/absolute-move % (gpt/point x (:y %)))
(:objects page) {:ignore-touched true}))))
before (move-to (- 103.005 1e-6))
after (move-to (+ 103.005 1e-6))
scalar (tm/with-file before
(assoc-in (tm/file before) [:data :pages-index (:id page) :objects id :opacity]
(+ 0.4 1e-15)))]
(t/is (oracle/equivalent? (oracle/canonical before) (oracle/canonical after)))
(t/is (not (oracle/equivalent? (oracle/canonical before) (oracle/canonical (move-to 101)))))
(t/is (not (oracle/equivalent? (oracle/canonical before) (oracle/canonical scalar))))))
(t/deftest swapped-head-inputs-follow-position-and-content-separately
(let [file (-> (thf/sample-file :file1)
(tho/add-simple-component :a :a-root :a-child
:root-params {:height 120})
(tho/add-simple-component :b :b-root :b-child
:root-params {:constraints-h :right
:layout-item-margin {:m1 9 :m2 9 :m3 9 :m4 9}})
(tho/add-frame :outer-root)
(thc/instantiate-component :a :position :parent-label :outer-root)
(thc/make-component :outer :outer-root)
(thc/instantiate-component :outer :copy-root :children-labels [:swapped])
(tho/swap-component-in-shape :swapped :b))
file (:file (r/sync-cascade file {} (keys (get-in file [:data :components]))))
model {(thi/id :swapped) {:swap-group {}}}]
(t/is (nil? (oracle/check (tm/make-situation file) model)))
(doseq [[attr value] [[:width 101]
[:constraints-h :right]
[:layout-item-margin {:m1 9 :m2 9 :m3 9 :m4 9}]]]
(let [broken (assoc-in file [:data :pages-index (:id (thf/current-page file))
:objects (thi/id :swapped) attr] value)]
(t/is (thrown? #?(:clj Exception :cljs js/Error)
(oracle/check (tm/make-situation broken) model)))))))
(t/deftest detached-rotated-parent-still-places-its-nested-copy
(let [detached (-> (setup/nested-component-with-copy)
(r/step (n/rotate :copy-root 90)) r/settle
(r/step (n/detach :copy-root)) r/settle)]
(t/is (nil? (oracle/check detached {})))
(let [file (tm/file detached)
page (thf/current-page file)
head (tm/role-shape detached :copy-instance)
id (first (:shapes head))
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page)) [id]
#(gsh/move % (gpt/point 10 0)) (:objects page)
{:ignore-touched true})
broken (tm/with-file detached (thf/apply-changes file changes))]
(t/is (thrown? #?(:clj Exception :cljs js/Error) (oracle/check broken {}))))))
(defn- sample
[constraint]
(-> (thf/sample-file :file1)
@ -156,5 +218,129 @@
(:objects (thf/current-page file)) {:ignore-touched true})))
moved (move file #{(thi/id :copy-root) (thi/id :copy-child)} 50)
bad (move moved #{(thi/id :copy-child)} 5)]
(t/is (= (oracle/canonical situation) (oracle/canonical (tm/with-file situation moved))))
(t/is (not= (oracle/canonical situation) (oracle/canonical (tm/with-file situation bad))))))
(t/is (oracle/equivalent? (oracle/canonical situation) (oracle/canonical (tm/with-file situation moved))))
(t/is (not (oracle/equivalent? (oracle/canonical situation) (oracle/canonical (tm/with-file situation bad)))))))
(t/deftest sync-comparison-treats-nil-and-false-flips-as-the-same-orientation
(let [situation (setup/simple-component-with-labeled-copy)
file (tm/file situation)
unflipped (tm/with-file situation (-> file (ths/update-shape :copy-root :flip-x false) (ths/update-shape :copy-root :touched nil)))
flipped (tm/with-file situation (-> file (ths/update-shape :copy-root :flip-x true) (ths/update-shape :copy-root :touched nil)))]
(t/is (oracle/equivalent? (oracle/canonical situation) (oracle/canonical unflipped)))
(t/is (not (oracle/equivalent? (oracle/canonical situation) (oracle/canonical flipped))))))
(t/deftest expected-child-flips-follow-successive-parent-rotations
(let [file (sample :left)
turn (fn [file]
(let [root (ths/get-shape file :copy-root)]
(transform file :copy-root
(ctm/rotation-modifiers root (gsh/shape->center root) 45))))
rotated (-> file turn turn)]
(t/is (nil? (oracle/check (tm/make-situation rotated) (geometry-model rotated))))))
(t/deftest backend-comparison-allows-instance-placement-after-leaving-a-layout
(let [situation (setup/nested-component-with-copy)
file (tm/file situation)
page (thf/current-page file)
head (tm/resolve-shape situation :main2-nested-head)
child (first (:shapes head))
move (fn [ids]
(thf/apply-changes
file (cls/generate-update-shapes
(pcb/empty-changes nil (:id page)) ids
#(gsh/move % (gpt/point 30 0)) (:objects page)
{:ignore-touched true})))
placed (tm/with-file situation (move [(:id head) child]))
broken (tm/with-file situation (move [child]))]
(t/is (oracle/equivalent? (oracle/canonical situation) (oracle/canonical placed)))
(t/is (not (oracle/equivalent? (oracle/canonical situation) (oracle/canonical broken))))))
(t/deftest nested-copy-child-flips-follow-successive-root-rotations
(let [result (cg/run
{:world [2 1 {:text true}]
:ops [[:add-child 0] [:remove-child 0] [:duplicate 0] [:nest 0]
[:delete 0] [:restore 0] [:text 0 0 1] [:text 1 1 1]
[:resize 0 0 2] [:rotate 0 0 0] [:reset 0]
[:edit 0 0 1 1] [:update-main 0] [:sync 1]
[:detach 0] [:undo] [:redo]
[:rotate 4 0 0 0] [:rotate 4 0 5 0]]})]
(t/is (= 3 (get-in result [:executed :rotate])))))
(t/deftest matrix-equivalence-measures-its-effect-in-pixels
(doseq [[size equal?] [[0.01 true] [100 false]]]
(let [file (-> (thf/sample-file :file1)
(tho/add-rect :rect {:width size :height size}))
changed (ths/update-shape file :rect :transform (gmt/matrix 0.8 0 0 1 0 0))]
(t/is (= equal? (oracle/equivalent? (oracle/canonical (tm/make-situation file))
(oracle/canonical (tm/make-situation changed))))))))
(t/deftest authored-child-orientation-follows-the-parent-frame
(let [model {:child {:geometry-group {:width 20 :height 30 :rotation 90}}}
before {:child {:id :child :parent-id :parent}
:parent {:rotation 45}}
after {:child {:id :child :parent-id :parent :rotation 0 :width 20 :height 30}
:parent {:rotation 135 :flip-x true :flip-y true}}
expected (get-in (shadow/parent-orientation model before after) [:child :geometry-group])]
(t/is (= {:width 20 :height 30 :rotation 180 :flip-x true :flip-y true} expected))
(t/is (not (shadow/same-value? (:child after) :geometry-group expected)))))
(t/deftest swapped-head-content-size-keeps-the-position-anchor-after-rotation
(let [file (-> (thf/sample-file :file1)
(tho/add-simple-component :a :a-root :a-child
:root-params {:width 100 :height 100}
:child-params {:width 20 :height 20})
(tho/add-simple-component :b :b-root :b-child
:root-params {:width 100 :height 100}
:child-params {:width 20 :height 20})
(tho/add-frame :outer-root)
(thc/instantiate-component :a :position :parent-label :outer-root))
root (ths/get-shape file :position)
file (transform file :position (ctm/change-size root 100 80))
root (ths/get-shape file :position)
file (-> (transform file :position
(ctm/rotation-modifiers root (gsh/shape->center root) 45))
(thc/make-component :outer :outer-root)
(thc/instantiate-component :outer :copy-root :children-labels [:swapped])
(tho/swap-component-in-shape :swapped :b))
file (:file (r/sync-cascade file {} (keys (get-in file [:data :components]))))
model {(thi/id :position) {:geometry-group (shadow/values (ths/get-shape file :position)
:geometry-group)}
(thi/id :swapped) {:swap-group {}}}
page (thf/current-page file)
moved (thf/apply-changes
file (cls/generate-update-shapes
(pcb/empty-changes nil (:id page)) [(thi/id :swapped)]
#(gsh/move % (gpt/point 5 0)) (:objects page)
{:ignore-touched true}))]
(t/is (nil? (oracle/check (tm/make-situation file) model)))
(t/is (thrown? #?(:clj Exception :cljs js/Error)
(oracle/check (tm/make-situation moved) model)))))
(t/deftest minimum-size-parent-placement-keeps-fixed-child-dimensions
(let [result (cg/run {:world [2 0 {:text true :layout nil}]
:ops [[:add-child 0] [:remove-child 0] [:duplicate 0] [:nest 0]
[:delete 0] [:restore 0] [:text 0 0 1] [:text 1 1 1]
[:resize 0 0 2] [:rotate 0 0 0] [:reset 0]
[:edit 0 0 1 1] [:update-main 0] [:sync 1]
[:detach 0] [:undo] [:redo] [:resize 1 0 0 0]
[:nest 2 0 0 0] [:rotate 0 0 3 0] [:edit 0 0 0 0]]})
situation (:situation result)
file (tm/file situation)
page (thf/current-page file)
objects (:objects page)
child (first (filter (fn [shape]
(let [parent (get objects (:parent-id shape))]
(and (= :frame (:type shape)) (:component-id shape)
(:shape-ref shape) (empty? (:touched shape))
(> (:height shape) 1)
(< (:width parent 1) 0.02)
(< (:height parent 1) 0.02))))
(vals objects)))
changed (thf/apply-changes
file (cls/generate-update-shapes
(pcb/empty-changes nil (:id page)) [(:id child)]
#(gsh/transform-shape % (ctm/change-size % nil 80)) objects
{:ignore-touched true}))]
(t/is (some? child))
(t/is (thrown? #?(:clj Exception :cljs js/Error)
(oracle/check (tm/with-file situation changed) (:model result))))))

View File

@ -9,33 +9,33 @@
:ac
[{:transcript "create-component {:component \"main\", :id sync-main-component}\n make-variant-container {:variant-set \"vset\", :variant-id sync-vset-vcontainer, :count 3}\n make-nested-component-with-variant {:component \"main\", :level 0, :outer sync-main-outercomp-0}\n instantiate-copy {:component \"main\", :copy sync-main-copyhead-0}\n change-property {:target #fn, :property :opacity, :value 0.5}\n switch-variant {:target-component sync-vset-v1-component}\n test\n switch-variant {:target-component sync-vset-v2-component}", :states ["9f9ac61ce3c54295" "5bd9620c6fc0cf6e" "387d74ba1c75e52f" "62a55a068ebd6043"]}]
:ad
[{:transcript "create-component {:component \"main\", :id sync-main-component}\n create-component {:component \"s1\", :id sync-s1-component}\n make-nested-component {:component \"main\", :level 0, :outer sync-main-outercomp-0}\n instantiate-copy {:component \"main\", :copy sync-main-copyhead-0}\n instantiate-copy {:component \"main\", :copy sync-main-copyhead-1}\n swap-shape {:target #fn, :to \"s1\", :keep-touched? false}\n update-main {:target #fn, :updated sync-main-copyhead-0}", :states ["9835a328e680007d" "8cc31cfc15b28449" "40c27ac7ca18d972"]}]
[{:transcript "create-component {:component \"main\", :id sync-main-component}\n create-component {:component \"s1\", :id sync-s1-component}\n make-nested-component {:component \"main\", :level 0, :outer sync-main-outercomp-0}\n instantiate-copy {:component \"main\", :copy sync-main-copyhead-0}\n instantiate-copy {:component \"main\", :copy sync-main-copyhead-1}\n swap-shape {:target #fn, :to \"s1\", :keep-touched? false}\n update-main {:target #fn, :updated sync-main-copyhead-0}", :states ["9835a328e680007d" "cb25044bb21b9f5f" "f1af8a77852eb4e6"]}]
:ae
[{:transcript "create-component {:component \"main\", :id sync-main-component}\n create-component {:component \"s1\", :id sync-s1-component}\n make-nested-component {:component \"main\", :level 0, :outer sync-main-outercomp-0}\n instantiate-copy {:component \"main\", :copy sync-main-copyhead-0}\n swap-shape {:target #fn, :to \"s1\", :keep-touched? false}\n detach {:target #fn, :detached sync-main-copyhead-0}\n change-property {:target #fn, :property :fills, :value \"#0000ff\"}", :states ["a3cb16fb335bc929" "e10b1283627fbeb6" "9be2dfab826240b0" "1fc0e554196dfd2b"]}
{:transcript "create-component {:component \"main\", :id sync-main-component}\n create-component {:component \"s1\", :id sync-s1-component}\n make-nested-component {:component \"main\", :level 0, :outer sync-main-outercomp-0}\n instantiate-copy {:component \"main\", :copy sync-main-copyhead-0}\n swap-shape {:target #fn, :to \"s1\", :keep-touched? false}\n duplicate {:target #fn, :new-head 5}\n change-property {:target #fn, :property :fills, :value \"#0000ff\"}", :states ["a3cb16fb335bc929" "d291c9ca9a187b1b" "99bb6214c05d575b" "0694d4feab3cb633"]}]
:af
[{:transcript "swap-shape {:target :copy-child, :to :other, :keep-touched? false}\n move-to-cell {:board :main-root, :child :main-child, :row 1, :column 3}", :states ["a3bca1c5fd1b9b29" "8a7fa3fde8815d35" "1c786eab89f462da"]}
[{:transcript "swap-shape {:target :copy-child, :to :other, :keep-touched? false}\n move-to-cell {:board :main-root, :child :main-child, :row 1, :column 3}", :states ["0ad5feb2c5e1a843" "18e4f6517178625b" "8247989d981f28b1"]}
{:transcript "one-of -> skip\n move-to-cell {:board :main-root, :child :main-child, :row 1, :column 3}", :states ["7f1f0be7424c543f" "7c9214e430ebb4e8"]}]
:ag
[{:transcript "move-child {:target :sub1, :parent :row-root, :to-index 3}", :states ["7a572aaeec3d11c7" "e6899eae694479cf"]}]
[{:transcript "move-child {:target :sub1, :parent :row-root, :to-index 3}", :states ["b8a611163a069cb1" "7b199a2abaca4eeb"]}]
:ah
[{:transcript "one-of -> recorded-choice\n remove-child {:target :copy-sub1}", :states ["3043434871927bf0" "c5acd77993218759"]}
{:transcript "one-of -> recorded-choice\n remove-child {:target :copy-sub3}", :states ["fdddae12ff7dd7bc" "7efeb83144959be0"]}
[{:transcript "one-of -> recorded-choice\n remove-child {:target :copy-sub1}", :states ["905c9c7c79ff0526" "ba4df35defa20eb6"]}
{:transcript "one-of -> recorded-choice\n remove-child {:target :copy-sub3}", :states ["fd51d05632c0d55f" "53fc16ef85447d0d"]}
{:transcript "one-of -> skip", :states ["4f8193bba8dbfb0d"]}]
:ak
[{:transcript "swap-shape {:target :copy-child, :to :other, :keep-touched? false}\n move-to-cell {:board :copy-root, :child :copy-child, :row 1, :column 3}\n update-main {:target :copy-root, :updated grid-copy}", :states ["a3bca1c5fd1b9b29" "246adb9d062e7222" "725449d340ed4db7" "f7c72fa8b8699165"]}
[{:transcript "swap-shape {:target :copy-child, :to :other, :keep-touched? false}\n move-to-cell {:board :copy-root, :child :copy-child, :row 1, :column 3}\n update-main {:target :copy-root, :updated grid-copy}", :states ["0ad5feb2c5e1a843" "7e8833e5c26e234c" "2508613c0f17bc78" "f3d331b4689dd5b9"]}
{:transcript "one-of -> skip\n move-to-cell {:board :copy-root, :child :copy-child, :row 1, :column 3}\n update-main {:target :copy-root, :updated grid-copy}", :states ["8aad596633c254fd" "7f1f0be7424c543f" "7c9214e430ebb4e8"]}]
:al
[{:transcript "change-property {:target :copy-root, :property :name, :value \"Renamed\"}\n change-property {:target :copy-instance, :property :name, :value \"Renamed child\"}\n update-main {:target :copy-root, :updated copy-root}", :states ["0166c98332ed23a7" "6c0c864aa286702f" "af3a00259b74b965" "6598d0729b91c5b3"]}
{:transcript "change-property {:target :copy-root, :property :name, :value \"Renamed\"}\n one-of -> skip\n update-main {:target :copy-root, :updated copy-root}", :states ["0166c98332ed23a7" "e2849db0c0174bfc" "c307f8e2c66e8760"]}]
:am
[{:transcript "swap-shape {:target :copy-child, :to :other, :keep-touched? false}\n update-main {:target :copy-root, :updated grouped-copy}", :states ["606d0e9ed09ebe9b" "c96014db9eb364e5" "bc48794035d17e6f"]}]
[{:transcript "swap-shape {:target :copy-child, :to :other, :keep-touched? false}\n update-main {:target :copy-root, :updated grouped-copy}", :states ["606d0e9ed09ebe9b" "33ec8f9abaa1b8fb" "e5bd7388fa65520a"]}]
:an
[{:transcript "change-property {:target :copy-instance, :property :margin, :value 8}\n change-property {:target :main-instance, :property :margin, :value 20}", :states ["4087468201a18ebc" "04328fe58e252ee9" "85db26a4ea6a899c"]}
{:transcript "change-property {:target :copy-instance, :property :margin, :value 8}\n reset-overrides {:target :copy-root, :reset copy-root}", :states ["4087468201a18ebc" "cb5b3ab586333d10" "054cd426071de8e3"]}
{:transcript "change-property {:target :copy-instance, :property :margin, :value 8}\n update-main {:target :copy-root, :updated copy-root}", :states ["4087468201a18ebc" "6b82a7e5bb516fc5" "8d389cd790e5cfb3"]}]
:ao
[{:transcript "nest-copy {:copy :loose, :into :main-root, :cell [1 3]}", :states ["6e7144142468b3ea" "16834d9e0ad8afe0"]}]
[{:transcript "nest-copy {:copy :loose, :into :main-root, :cell [1 3]}", :states ["83772bd7270eb3a4" "486bc870af39864a"]}]
:ap
[{:transcript "sync-from-library {:library-id library}", :states ["3550b8df58ff0c95" "3872af2b94d47dea"]}]
:aq
@ -145,12 +145,12 @@
:main-edit-into-1000-copies
[{:transcript "main-edit-into-1000-copies", :states ["b6994497085bc07f"]}]
:n
[{:transcript "rotate {:target :copy-root, :angle 45}\n rotate {:target :main-root, :angle 30}\n change-property {:target :main-instance, :property :fills, :value \"#ff0000\"}", :states ["8208735a98e4c105" "f9a383ec6e3b0bb3" "e1975cf2f47c5763" "9a89ace89a9faec9"]}
{:transcript "rotate {:target :copy-root, :angle 45}\n rotate {:target :main-root, :angle 30}\n change-height {:target :main-instance, :value 80}", :states ["8208735a98e4c105" "f9a383ec6e3b0bb3" "7e9afa9fd32338bd" "15ad444e53bca81b"]}
[{:transcript "rotate {:target :copy-root, :angle 45}\n rotate {:target :main-root, :angle 30}\n change-property {:target :main-instance, :property :fills, :value \"#ff0000\"}", :states ["8208735a98e4c105" "f848dd262a0a07aa" "85f71340f8c9c134" "884bb300d156bcb4"]}
{:transcript "rotate {:target :copy-root, :angle 45}\n rotate {:target :main-root, :angle 30}\n change-height {:target :main-instance, :value 80}", :states ["8208735a98e4c105" "f848dd262a0a07aa" "88df05dbfc4217c8" "6f3c4b7eb0e89f8a"]}
{:transcript "rotate {:target :copy-root, :angle 45}\n one-of -> skip\n change-property {:target :main-instance, :property :fills, :value \"#ff0000\"}", :states ["8208735a98e4c105" "aa9f6c1b6a87ac0f" "38265afaa05640e8"]}
{:transcript "rotate {:target :copy-root, :angle 45}\n one-of -> skip\n change-height {:target :main-instance, :value 80}", :states ["8208735a98e4c105" "e92b0483129edb9c" "afd213908fee2f38"]}
{:transcript "one-of -> skip\n rotate {:target :main-root, :angle 30}\n change-property {:target :main-instance, :property :fills, :value \"#ff0000\"}", :states ["ceecd80f347920c9" "f5c326161b425dca" "049a41b4774ab16d"]}
{:transcript "one-of -> skip\n rotate {:target :main-root, :angle 30}\n change-height {:target :main-instance, :value 80}", :states ["ceecd80f347920c9" "b23260b0f15be83b" "65e4a3e632ab0e48"]}
{:transcript "one-of -> skip\n rotate {:target :main-root, :angle 30}\n change-property {:target :main-instance, :property :fills, :value \"#ff0000\"}", :states ["39e2dadef951dca2" "0cc17d2b4ff02bb9" "b675e496913b617e"]}
{:transcript "one-of -> skip\n rotate {:target :main-root, :angle 30}\n change-height {:target :main-instance, :value 80}", :states ["39e2dadef951dca2" "789103afba436783" "78667a8353113f40"]}
{:transcript "one-of -> skip\n one-of -> skip\n change-property {:target :main-instance, :property :fills, :value \"#ff0000\"}", :states ["17b5ce28aeffa62e" "8f31b06c3ab1921b"]}
{:transcript "one-of -> skip\n one-of -> skip\n change-height {:target :main-instance, :value 80}", :states ["928074db7743f484" "d54aee2c77882202"]}]
:o
@ -173,7 +173,7 @@
{:transcript "create-component {:component \"main\", :id sync-main-component}\n make-nested-component {:component \"main\", :level 1, :outer sync-main-outercomp-1}\n make-nested-component {:component \"main\", :level 1, :outer sync-main-outercomp-1}\n instantiate-copy {:component \"main\", :copy sync-main-copyhead-0}\n instantiate-copy {:component \"main\", :copy sync-main-copyhead-1}\n change-property {:target #fn, :property :fills, :value \"#0000ff\"}\n change-property {:target #fn, :property :fills, :value \"#00ff00\"}\n update-main {:target #fn, :updated sync-main-copyhead-0}", :states ["dd907ef11d9e4851" "481d62dd7e471206" "046525ebd0ffe008" "682e0ba7926cc177"]}
{:transcript "create-component {:component \"main\", :id sync-main-component}\n make-nested-component {:component \"main\", :level 1, :outer sync-main-outercomp-1}\n make-nested-component {:component \"main\", :level 1, :outer sync-main-outercomp-1}\n instantiate-copy {:component \"main\", :copy sync-main-copyhead-0}\n instantiate-copy {:component \"main\", :copy sync-main-copyhead-1}\n one-of -> skip\n change-property {:target #fn, :property :fills, :value \"#00ff00\"}\n update-main {:target #fn, :updated sync-main-copyhead-0}", :states ["af30a8074415ea47" "0292c4761cadebad" "8c3b12b00c20dd24"]}]
:rotation
[{:transcript "rotate {:target :copy-root, :angle 30}\n change-height {:target :copy-instance, :value 60}\n test\n rotate {:target :main-root, :angle 40}", :states ["f54075611f2456da" "dcfc7d5c6f4bec2a" "1f948e0492586197" "46492bbe943d1366"]}]
[{:transcript "rotate {:target :copy-root, :angle 30}\n change-height {:target :copy-instance, :value 60}\n test\n rotate {:target :main-root, :angle 40}", :states ["d12dd9fffaf38df9" "54f0852878f4edfa" "8759a8056147e49e" "3680cabdfe765e60"]}]
:s
[{:transcript "change-property {:target :copy-instance, :property :fills, :value \"#00ff00\"}\n change-property {:target :main-instance, :property :fills, :value \"#ff0000\"}\n delete-component {:target :main-root, :component-id component1}\n test\n restore-component {:component-id component1}\n change-property {:target :main-instance, :property :fills, :value \"#0000ff\"}", :states ["93c77f92c889307b" "0c2ac66ca5452745" "11bd55106f98bebc" "6a0d75c08f6b08b8" "bccb7c6434c8de28" "9da4088c9b6c2b45"]}
{:transcript "one-of -> skip\n change-property {:target :main-instance, :property :fills, :value \"#ff0000\"}\n delete-component {:target :main-root, :component-id component1}\n test\n restore-component {:component-id component1}\n change-property {:target :main-instance, :property :fills, :value \"#0000ff\"}", :states ["8c5be47002e73694" "323f1f1ed8b35e4d" "eb7c91cf5e08a6c3" "86c4d8c4352a04db" "ddbdea68968e3988"]}]
@ -185,12 +185,12 @@
{:transcript "create-component {:component \"main\", :id sync-main-component}\n make-nested-component {:component \"main\", :level 1, :outer sync-main-outercomp-1}\n make-nested-component {:component \"main\", :level 1, :outer sync-main-outercomp-1}\n instantiate-copy {:component \"main\", :copy sync-main-copyhead-0}\n change-property {:target #fn, :property :fills, :value \"#00ff00\"}\n duplicate {:target #fn, :new-head 8}\n change-property {:target #fn, :property :fills, :value \"#0000ff\"}", :states ["083c91e86a50fc3d" "f5f413ad5bea79a4" "0dba6a2c453b5250" "9857e951ca902581"]}
{:transcript "create-component {:component \"main\", :id sync-main-component}\n make-nested-component {:component \"main\", :level 1, :outer sync-main-outercomp-1}\n make-nested-component {:component \"main\", :level 1, :outer sync-main-outercomp-1}\n instantiate-copy {:component \"main\", :copy sync-main-copyhead-0}\n one-of -> skip\n duplicate {:target #fn, :new-head 8}\n change-property {:target #fn, :property :fills, :value \"#0000ff\"}", :states ["ba24e834d0767879" "e97c69b1e46ffe5d" "ec75501d3a176eef"]}]
:u
[{:transcript "create-component {:component \"outer\", :id sync-outer-component}\n create-component {:component \"inner\", :id sync-inner-component}\n instantiate-copy {:component \"outer\", :copy sync-outer-copyhead-0}\n instantiate-copy {:component \"inner\", :copy sync-inner-copyhead-0}\n nest-copy {:copy #fn, :into #fn, :cell nil}\n change-property {:target #fn, :property :fills, :value \"#00ff00\"}\n change-property {:target #fn, :property :fills, :value \"#0000ff\"}", :states ["787b17b227cfdb85" "f6490ae9fe96d3e4" "bd40aa51b1ec2fd1" "53c3ace2c3feb6d6"]}
{:transcript "create-component {:component \"outer\", :id sync-outer-component}\n create-component {:component \"inner\", :id sync-inner-component}\n instantiate-copy {:component \"outer\", :copy sync-outer-copyhead-0}\n instantiate-copy {:component \"inner\", :copy sync-inner-copyhead-0}\n nest-copy {:copy #fn, :into #fn, :cell nil}\n one-of -> skip\n change-property {:target #fn, :property :fills, :value \"#0000ff\"}", :states ["787b17b227cfdb85" "e662ae31e1675fa9" "435dd5416b94f48f"]}]
[{:transcript "create-component {:component \"outer\", :id sync-outer-component}\n create-component {:component \"inner\", :id sync-inner-component}\n instantiate-copy {:component \"outer\", :copy sync-outer-copyhead-0}\n instantiate-copy {:component \"inner\", :copy sync-inner-copyhead-0}\n nest-copy {:copy #fn, :into #fn, :cell nil}\n change-property {:target #fn, :property :fills, :value \"#00ff00\"}\n change-property {:target #fn, :property :fills, :value \"#0000ff\"}", :states ["83fec2239ffc4df8" "2bf9ec1fdeaa05ed" "bd40aa51b1ec2fd1" "53c3ace2c3feb6d6"]}
{:transcript "create-component {:component \"outer\", :id sync-outer-component}\n create-component {:component \"inner\", :id sync-inner-component}\n instantiate-copy {:component \"outer\", :copy sync-outer-copyhead-0}\n instantiate-copy {:component \"inner\", :copy sync-inner-copyhead-0}\n nest-copy {:copy #fn, :into #fn, :cell nil}\n one-of -> skip\n change-property {:target #fn, :property :fills, :value \"#0000ff\"}", :states ["83fec2239ffc4df8" "e662ae31e1675fa9" "435dd5416b94f48f"]}]
:v
[{:transcript "change-property {:target :main-instance, :property :fills, :value \"#ff0000\"}\n undo {:undone 2}\n change-property {:target :main-instance, :property :fills, :value \"#00ff00\"}\n undo {:undone 2}\n redo {:redone 2}\n redo {:redone 0}", :states ["17b5ce28aeffa62e" "e60c9f0fd46b2310" "a30b6f606aaa5f66" "e60c9f0fd46b2310" "a30b6f606aaa5f66" "a30b6f606aaa5f66" "0ba2151cc54b42fa"]}]
:variant-switch-into-copies
[{:transcript "variant-switch-into-copies", :states ["6cdf94ad6bd691c5"]}]
[{:transcript "variant-switch-into-copies", :states ["540f8ef5f8ad96ea"]}]
:w
[{:transcript "create-component {:component \"main\", :id sync-main-component}\n one-of -> skip\n instantiate-copy {:component \"main\", :copy sync-main-copyhead-0}\n change-property {:target #fn, :property :fills, :value \"#0000ff\"}\n change-property {:target #fn, :property :fills, :value \"#00ff00\"}\n change-property {:target #fn, :property :fills, :value \"#aaaaaa\"}\n undo {:undone 5}", :states ["f06eb554db2a667d" "906c1b936c948e9c" "8e29f79b473c1a0b" "0072b33980dae904" "bd57e8070d829bdf"]}
{:transcript "create-component {:component \"main\", :id sync-main-component}\n make-nested-component {:component \"main\", :level 0, :outer sync-main-outercomp-0}\n instantiate-copy {:component \"main\", :copy sync-main-copyhead-0}\n change-property {:target #fn, :property :fills, :value \"#0000ff\"}\n change-property {:target #fn, :property :fills, :value \"#00ff00\"}\n change-property {:target #fn, :property :fills, :value \"#aaaaaa\"}\n undo {:undone 5}", :states ["1de51816cdc0ad11" "ed2117b3a6ae1dd6" "c90876e1b44d00fb" "a5b1e4b00086c891" "f09bed0fa67d711c"]}

View File

@ -11,22 +11,25 @@
[app.common.test-helpers.composable.gen :as cg]
[app.common.test-helpers.composable.oracle :as oracle]
[app.common.test-helpers.normalize :as thn]
[app.main.repo :as rp]
[beicon.v2.core :as rx]
[cljs.test :as t :include-macros true]
[clojure.test.check.generators :as gen]
[frontend-tests.composable-tests.interpreter :as ftm]))
[frontend-tests.composable-tests.interpreter :as ftm]
[frontend-tests.helpers.mock :as mock]))
(t/use-fixtures :each ftm/fixture)
(defn- live-model
[state live]
(let [by-path (into {} (map (juxt :path (comp :id :shape))) (cg/tree live))]
(let [by-path (into {} (map (juxt :path (comp :id :shape))) (cg/forest live))]
(into {}
(keep (fn [{:keys [path shape]}]
(when-let [groups (get (:model state) (:id shape))]
(when-let [id (get by-path path)] [id groups]))))
(cg/tree (:situation state)))))
(cg/forest (:situation state)))))
(defn ^:async replay
(defn- ^:async replay-program
"Replay a JVM-shrunk EDN program against the async frontend. The pure
runner chooses targets independently and both must make the same choices."
[{:keys [world ops] :as program}]
@ -38,7 +41,7 @@
(if-let [tuple (first tuples)]
(let [state (cg/settle-state state)
pure (:situation state)
live (tm/with-file live (ftm/current-file))
live (ftm/refresh-generated live)
expected (cg/resolve-op pure (:model state) tuple)
actual (if (#{:undo :redo} (first tuple))
expected
@ -48,7 +51,7 @@
(await (ftm/run-generated-step live op))
(do
(t/is (= :settled (await (ftm/await-step []))))
(tm/with-file live (ftm/current-file))))
(ftm/refresh-generated live)))
next-state (-> state (cg/step tuple) (cg/settle-state))]
(when-not next-live
(throw (ex-info "Frontend step failed" {:program program :tuple tuple})))
@ -59,7 +62,7 @@
(ex-data error) error))))
(let [expected (oracle/canonical (:situation next-state))
actual (oracle/canonical next-live)]
(t/is (true? (= expected actual))
(t/is (true? (oracle/equivalent? expected actual))
(str "Backend mismatch at " tuple "; replay " (pr-str program)
"; diff " (pr-str (thn/diff expected actual)))))
(t/is (empty? (uc/failures next-live)) (pr-str (uc/failures next-live)))
@ -72,6 +75,12 @@
(t/is false (str (ex-message error) "\nReplay: " (pr-str program)
"\nDetails: " (pr-str (ex-data error)))))))
(defn ^:async replay
[program]
(await (mock/with-mocks* {rp/cmd! mock/rpc-cmd-mock
rx/timer mock/timer-mock}
(await (replay-program program)))))
(t/deftest ^:async seeded-programs-replay-through-the-frontend
(doseq [seed [712 20261022 1792691]]
(await (replay (gen/generate cg/program-gen 20 seed)))))
@ -90,7 +99,67 @@
(t/deftest ^:async nesting-restores-default-constraints-on-undo
(await (replay {:world [2 0] :ops [[:nest 0]]})))
(t/deftest ^:async partial-sync-geometry-boundary-shrunk-regression
(await (replay {:world [2 0 {:text true :layout nil}]
:ops [[:add-child 0] [:remove-child 0] [:duplicate 0] [:nest 0]
[:delete 0] [:restore 0] [:text 0 0 1] [:text 1 1 1]
[:resize 0 0 2] [:rotate 0 0 0] [:reset 0]
[:edit 0 0 1 1] [:update-main 0] [:sync 1]
[:detach 0] [:undo] [:redo] [:undo 0 0 0 0]
[:add-child 17 0 0 0] [:rotate 19 0 0 0] [:undo 0 0 0 0]]})))
(t/deftest ^:async update-main-after-nesting-a-rotated-text-copy-undoes-together
(await (replay {:world [2 0 {:text true}]
:ops [[:duplicate 0] [:nest 0] [:rotate 0 0 0]
[:edit 0 0 1 1] [:update-main 0]]})))
(t/deftest ^:async updating-a-rotated-outer-main-composes-authored-child-orientation
(await (replay {:world [2 0 {:text true :layout nil}]
:ops [[:add-child 0] [:remove-child 0] [:duplicate 0] [:nest 0]
[:delete 0] [:restore 0] [:text 0 0 1] [:text 1 1 1]
[:resize 0 0 2] [:rotate 0 0 0] [:reset 0]
[:edit 0 0 1 1] [:update-main 0] [:sync 1]
[:detach 0] [:undo] [:redo] [:duplicate 0 0 0 0]
[:resize 0 0 0 0] [:nest 0 0 0 0] [:rotate 4 0 0 0]
[:detach 3 0 0 0] [:update-main 4 0 0 0]]})))
(t/deftest ^:async minimum-size-parent-orientation-does-not-amplify-oracle-drift
(await (replay {:world [3 0 {:text true :layout nil}]
:ops [[:add-child 0] [:remove-child 0] [:duplicate 0] [:nest 0]
[:delete 0] [:restore 0] [:text 0 0 1] [:text 1 1 1]
[:resize 0 0 2] [:rotate 0 0 0] [:reset 0]
[:edit 0 0 1 1] [:update-main 0] [:sync 1]
[:detach 0] [:undo] [:redo] [:edit 4 0 0 0]
[:duplicate 3 0 0 0] [:nest 10 0 0 0] [:rotate 9 0 0 0]
[:update-main 7 0 0 0] [:delete 0 0 0 0]]})))
(t/deftest ^:async detaching-a-rotated-parent-inherits-geometry-in-the-copy-frame
(await (replay {:world [2 0 {:text true :layout nil}]
:ops [[:add-child 0] [:remove-child 0] [:duplicate 0] [:nest 0]
[:delete 0] [:restore 0] [:text 0 0 1] [:text 1 1 1]
[:resize 0 0 2] [:rotate 0 0 0] [:reset 0]
[:edit 0 0 1 1] [:update-main 0] [:sync 1]
[:detach 0] [:undo] [:redo] [:resize 0 0 0 0]
[:delete 0 0 0 0] [:rotate 0 0 0 0] [:detach 0 0 0 0]]})))
(t/deftest ^:async resizing-a-copy-of-an-empty-flex-main-keeps-layout-valid
(await (replay {:world [3 0 {:text true :layout :flex}]
:ops [[:add-child 0] [:remove-child 0] [:duplicate 0] [:nest 0]
[:delete 0] [:restore 0] [:text 0 0 1] [:text 1 1 1]
[:resize 0 0 2] [:rotate 0 0 0] [:reset 0]
[:edit 0 0 1 1] [:update-main 0] [:sync 1]
[:detach 0] [:undo] [:redo] [:remove-child 0 0 0 0]
[:reset 0 0 0 0] [:nest 0 0 0 0] [:resize 0 0 0 0]]})))
(t/deftest ^:async updating-a-main-after-successive-rotations-keeps-child-flips
(await (replay {:world [2 1]
:ops [[:rotate 1 0 0 0] [:rotate 1 0 1 0] [:update-main 0 0 0 0]]})))
(t/deftest ^:async minimum-size-parent-placement-keeps-fixed-child-dimensions
(await (replay {:world [2 0 {:text true :layout nil}]
:ops [[:add-child 0] [:remove-child 0] [:duplicate 0] [:nest 0]
[:delete 0] [:restore 0] [:text 0 0 1] [:text 1 1 1]
[:resize 0 0 2] [:rotate 0 0 0] [:reset 0]
[:edit 0 0 1 1] [:update-main 0] [:sync 1]
[:detach 0] [:undo] [:redo] [:resize 1 0 0 0]
[:nest 2 0 0 0] [:rotate 0 0 3 0] [:edit 0 0 0 0]]})))

View File

@ -9,42 +9,42 @@
:case-ac-second-variant-switch-keeps-root-overrides
[{:transcript " {:component \"main\", :id sync-main-component}\n {:variant-set \"vset\", :variant-id sync-vset-vcontainer, :count 3}\n {:component \"main\", :level 0, :outer sync-main-outercomp-0}\n {:component \"main\", :copy sync-main-copyhead-0}\n {:target #fn, :property :opacity, :value 0.5}\n {:target #fn, :value \"b\", :pos 0}\n \n {:target #fn, :value \"c\", :pos 0}", :states ["45b82e3d3433e503" "6aa8b050f6e60b75" "2464ce46aa6d70f8" "83ae3b7689a44277"]}]
:case-ad-update-main-pushes-a-swapped-child
[{:transcript " {:component \"main\", :id sync-main-component}\n {:component \"s1\", :id sync-s1-component}\n {:component \"main\", :level 0, :outer sync-main-outercomp-0}\n {:component \"main\", :copy sync-main-copyhead-0}\n {:component \"main\", :copy sync-main-copyhead-1}\n {:target #fn, :to \"s1\", :keep-touched? false}\n {:target #fn}", :states ["70154c2f90c4d94f" "8276a39e105869aa" "60ae4e382dab157a"]}]
[{:transcript " {:component \"main\", :id sync-main-component}\n {:component \"s1\", :id sync-s1-component}\n {:component \"main\", :level 0, :outer sync-main-outercomp-0}\n {:component \"main\", :copy sync-main-copyhead-0}\n {:component \"main\", :copy sync-main-copyhead-1}\n {:target #fn, :to \"s1\", :keep-touched? false}\n {:target #fn}", :states ["70154c2f90c4d94f" "81f3d357f0dbb315" "c88064e53602169d"]}]
:case-ae-detached-or-duplicated-swapped-copy-follows-its-component
[{:transcript " {:component \"main\", :id sync-main-component}\n {:component \"s1\", :id sync-s1-component}\n {:component \"main\", :level 0, :outer sync-main-outercomp-0}\n {:component \"main\", :copy sync-main-copyhead-0}\n {:target #fn, :to \"s1\", :keep-touched? false}\n {:target #fn}\n {:target #fn, :property :fills, :value \"#0000ff\"}", :states ["d74e87bca3fa2eb9" "ffc66442a5849acd" "ad43d75dcc79bc8a" "6fa02ad66793cf43"]}
{:transcript " {:component \"main\", :id sync-main-component}\n {:component \"s1\", :id sync-s1-component}\n {:component \"main\", :level 0, :outer sync-main-outercomp-0}\n {:component \"main\", :copy sync-main-copyhead-0}\n {:target #fn, :to \"s1\", :keep-touched? false}\n {:target #fn, :new-head 5}\n {:target #fn, :property :fills, :value \"#0000ff\"}", :states ["d74e87bca3fa2eb9" "e2162174653b4d7b" "0bd8763796243e1f" "f612d9fde86170a6"]}]
:case-af-grid-cell-move-reaches-a-swapped-copy-child
[{:transcript " {:target :copy-child, :to :other, :keep-touched? false}\n {:board :main-root, :child :main-child, :row 1, :column 3}", :states ["5a6af6c0b8f86ac3" "ec2294c072f52ae5" "8cd63b096547fb1b"]}
[{:transcript " {:target :copy-child, :to :other, :keep-touched? false}\n {:board :main-root, :child :main-child, :row 1, :column 3}", :states ["66a66d6e7afac79f" "72c68fd299002108" "a8170f02220c56c3"]}
{:transcript "one-of -> \n {:board :main-root, :child :main-child, :row 1, :column 3}", :states ["ab911a9c4ef389ba" "56df09285036aa60"]}]
:case-ag-main-reorder-keeps-copy-sub-heads-aligned
[{:transcript " {:target :sub1, :parent :row-root, :to-index 3}", :states ["8118be0e3d318e30" "e0d17dd4c27d652c"]}]
[{:transcript " {:target :sub1, :parent :row-root, :to-index 3}", :states ["598fcda0ce7604ff" "bf7fbe3947f98e57"]}]
:case-ah-copy-sub-head-delete-keeps-others-aligned
[{:transcript "one-of -> \n {:target :copy-sub1}", :states ["fff37cf722a40e61" "6fd3a8bad999968b"]}
{:transcript "one-of -> \n {:target :copy-sub3}", :states ["efada86db353920f" "3364b0fbfb76067a"]}
[{:transcript "one-of -> \n {:target :copy-sub1}", :states ["8cce96ea845f126a" "9fbdeb6913ed25c7"]}
{:transcript "one-of -> \n {:target :copy-sub3}", :states ["2fb52dd7ebffcfc2" "6daed3c2ff997a9f"]}
{:transcript "one-of -> ", :states ["94318a628db305b6"]}]
:case-ai-copy-sub-head-delete-and-reflow-keeps-others-aligned
[{:transcript "one-of -> \n {:target :copy-sub1}\n {:target :copy-root, :value 500}", :states ["fff37cf722a40e61" "738377a4c5ef9f3c" "f23a64d143d27dba"]}
{:transcript "one-of -> \n {:target :copy-sub1}\n one-of -> ", :states ["fff37cf722a40e61" "6fd3a8bad999968b"]}
{:transcript "one-of -> \n {:target :copy-sub3}\n {:target :copy-root, :value 500}", :states ["efada86db353920f" "701edc575ad9c0c6" "91db7f5306659911"]}
{:transcript "one-of -> \n {:target :copy-sub3}\n one-of -> ", :states ["efada86db353920f" "3364b0fbfb76067a"]}
[{:transcript "one-of -> \n {:target :copy-sub1}\n {:target :copy-root, :value 500}", :states ["8cce96ea845f126a" "259a2b096309de89" "57cee31291838076"]}
{:transcript "one-of -> \n {:target :copy-sub1}\n one-of -> ", :states ["8cce96ea845f126a" "9fbdeb6913ed25c7"]}
{:transcript "one-of -> \n {:target :copy-sub3}\n {:target :copy-root, :value 500}", :states ["2fb52dd7ebffcfc2" "4a9ca18246cd24c1" "d7816b420adf634f"]}
{:transcript "one-of -> \n {:target :copy-sub3}\n one-of -> ", :states ["2fb52dd7ebffcfc2" "6daed3c2ff997a9f"]}
{:transcript "one-of -> \n {:target :copy-root, :value 500}", :states ["000943ecb6c266ee" "209fe67d76a27a9b"]}
{:transcript "one-of -> \n one-of -> ", :states ["94318a628db305b6"]}]
:case-aj-reflow-after-nested-sync-is-its-own-undo-step
[{:transcript " {:component \"main\", :id sync-main-component}\n {:component \"main\", :level 0, :outer sync-main-outercomp-0}\n {:component \"main\", :level 1, :outer sync-main-outercomp-1}\n {:component \"main\", :copy sync-main-copyhead-0}\n {:target #fn, :property :fills, :value \"#0000ff\"}\n {:times 1}", :states ["43ca8b2de80a51fc" "ce4ef0e73faa0894" "ef4de3d8d773582c"]}]
[{:transcript " {:component \"main\", :id sync-main-component}\n {:component \"main\", :level 0, :outer sync-main-outercomp-0}\n {:component \"main\", :level 1, :outer sync-main-outercomp-1}\n {:component \"main\", :copy sync-main-copyhead-0}\n {:target #fn, :property :fills, :value \"#0000ff\"}\n {:times 1}", :states ["43ca8b2de80a51fc" "32b45a5ac7536b08" "7104bfaccd42cd3b"]}]
:case-ak-update-main-pushes-grid-cells
[{:transcript " {:target :copy-child, :to :other, :keep-touched? false}\n {:board :copy-root, :child :copy-child, :row 1, :column 3}\n {:target :copy-root}", :states ["5a6af6c0b8f86ac3" "278e4ff6bf0df746" "322e03c88f4a9dce" "432cfef7e9b05164"]}
[{:transcript " {:target :copy-child, :to :other, :keep-touched? false}\n {:board :copy-root, :child :copy-child, :row 1, :column 3}\n {:target :copy-root}", :states ["66a66d6e7afac79f" "9865d4f8003e91ef" "2d73539548abb75a" "5e5d65dc5416e495"]}
{:transcript "one-of -> \n {:board :copy-root, :child :copy-child, :row 1, :column 3}\n {:target :copy-root}", :states ["9900a7f71dc0cb5f" "de66ab1c3d4cc157" "6b5ff9fd86e2aa87"]}]
:case-al-update-main-renames-the-component
[{:transcript " {:target :copy-root, :property :name, :value \"Renamed\"}\n {:target :copy-instance, :property :name, :value \"Renamed child\"}\n {:target :copy-root}", :states ["d4e11348047fc1e1" "0021aeab497d25e6" "c0aa5a2b915a7542" "ca23729038aec91f"]}
{:transcript " {:target :copy-root, :property :name, :value \"Renamed\"}\n one-of -> \n {:target :copy-root}", :states ["d4e11348047fc1e1" "409795fc3e2bb3a7" "6a882f51aaf16306"]}]
:case-am-update-main-pushes-a-swap-inside-a-group
[{:transcript " {:target :copy-child, :to :other, :keep-touched? false}\n {:target :copy-root}", :states ["b4f808a12f723283" "9b695fc212f26b53" "32b94deb961ac763"]}]
[{:transcript " {:target :copy-child, :to :other, :keep-touched? false}\n {:target :copy-root}", :states ["b4f808a12f723283" "c9440f1c68655424" "1359aa054bd1a482"]}]
:case-an-flex-child-override-follows-sync-reset-and-update-main
[{:transcript " {:target :copy-instance, :property :margin, :value 8}\n {:target :main-instance, :property :margin, :value 20}", :states ["234a21e54bf21bd5" "03664b1e6eb74856" "820db97e4e4b43dc"]}
{:transcript " {:target :copy-instance, :property :margin, :value 8}\n {:target :copy-root}", :states ["234a21e54bf21bd5" "1f92f9b4d2e50598" "a09954797e57930c"]}
{:transcript " {:target :copy-instance, :property :margin, :value 8}\n {:target :copy-root}", :states ["234a21e54bf21bd5" "ace1e76c1da2c443" "8f3452e82544f1ad"]}]
:case-ao-copy-dropped-into-a-grid-cell-reaches-copies
[{:transcript " {:copy :loose, :into :main-root, :cell [1 3]}", :states ["abc6e41a291516a2" "571d314ba8917a08"]}]
[{:transcript " {:copy :loose, :into :main-root, :cell [1 3]}", :states ["85c7e7a4f4289716" "a00c83f43550909b"]}]
:case-ap-library-main-child-removal-reaches-copy
[{:transcript "", :states ["dd877acf0d7f120c" "0a472792b8a73e33"]}]
:case-aq-main-change-reaches-a-copy-nested-in-a-sub-board
@ -56,8 +56,8 @@
{:transcript " {:component \"main\", :id sync-main-component}\n {:variant-set \"vset\", :variant-id sync-vset-vcontainer, :count 2}\n {:component \"main\", :level 0, :outer sync-main-outercomp-0}\n {:component \"main\", :copy sync-main-copyhead-0}\n {:target #fn, :property :fills, :value \"#00ff00\"}\n {:set-name \"vset\", :edit :add, :params {:property-name \"Size\", :property-value \"S\"}}\n {:set-name \"vset\", :edit :reorder, :params {:from 1, :to 0}}\n \n {:target #fn, :value \"b\", :pos #fn}", :states ["df9a042b9d59d699" "c4f5353ee8eb5822" "20e7c6031f77998f" "4b387428cd241ed1" "a79933f2d6da95da"]}
{:transcript " {:component \"main\", :id sync-main-component}\n {:variant-set \"vset\", :variant-id sync-vset-vcontainer, :count 2}\n {:component \"main\", :level 0, :outer sync-main-outercomp-0}\n {:component \"main\", :copy sync-main-copyhead-0}\n {:target #fn, :property :fills, :value \"#00ff00\"}\n {:set-name \"vset\", :edit :add, :params {:property-name \"Size\", :property-value \"S\"}}\n {:set-name \"vset\", :edit :remove, :params {:pos 1}}\n \n {:target #fn, :value \"b\", :pos #fn}", :states ["df9a042b9d59d699" "c4f5353ee8eb5822" "df9a042b9d59d699" "ef6a99bd8579dc7f" "3b1bfc8427b3963d"]}]
:case-as-added-variant-is-a-switch-target
[{:transcript " {:component \"main\", :id sync-main-component}\n {:variant-set \"vset\", :variant-id sync-vset-vcontainer, :count 2}\n {:component \"main\", :level 0, :outer sync-main-outercomp-0}\n {:component \"main\", :copy sync-main-copyhead-0}\n {:target #fn, :property :fills, :value \"#00ff00\"}\n {:set-name \"vset\", :member \"a\", :component-id 7}\n \n {:target #fn, :value \"Value 3\", :pos 0}", :states ["df9a042b9d59d699" "00b4a79bae39f703" "0b85fa1611096bb7" "542884fb9e0b082e"]}
{:transcript " {:component \"main\", :id sync-main-component}\n {:variant-set \"vset\", :variant-id sync-vset-vcontainer, :count 2}\n {:component \"main\", :level 0, :outer sync-main-outercomp-0}\n {:component \"main\", :copy sync-main-copyhead-0}\n {:target #fn, :property :fills, :value \"#00ff00\"}\n {:set-name \"vset\", :member \"b\", :component-id 7}\n \n {:target #fn, :value \"Value 3\", :pos 0}", :states ["df9a042b9d59d699" "73a3942a18403d5e" "3234ee274caa5b9e" "0fd372565af6eae2"]}]
[{:transcript " {:component \"main\", :id sync-main-component}\n {:variant-set \"vset\", :variant-id sync-vset-vcontainer, :count 2}\n {:component \"main\", :level 0, :outer sync-main-outercomp-0}\n {:component \"main\", :copy sync-main-copyhead-0}\n {:target #fn, :property :fills, :value \"#00ff00\"}\n {:set-name \"vset\", :member \"a\", :component-id 7}\n \n {:target #fn, :value \"Value 3\", :pos 0}", :states ["df9a042b9d59d699" "43d53bfb72ea0e0c" "6d809ac935ee93e1" "144104054c2e6b3e"]}
{:transcript " {:component \"main\", :id sync-main-component}\n {:variant-set \"vset\", :variant-id sync-vset-vcontainer, :count 2}\n {:component \"main\", :level 0, :outer sync-main-outercomp-0}\n {:component \"main\", :copy sync-main-copyhead-0}\n {:target #fn, :property :fills, :value \"#00ff00\"}\n {:set-name \"vset\", :member \"b\", :component-id 7}\n \n {:target #fn, :value \"Value 3\", :pos 0}", :states ["df9a042b9d59d699" "06e9af57624cd581" "55609c964d15c784" "c8d9eaf05aa22931"]}]
:case-at-transformed-component-keeps-its-copies
[{:transcript " {:component \"main\", :id sync-main-component}\n {:component \"main\", :copy sync-main-copyhead-0}\n {:target #fn, :property :fills, :value \"#00ff00\"}\n {:target #fn, :property :name, :value \"Button/Primary\"}\n {:action :transform, :params {:target #fn}}\n \n {:target #fn, :value \"Value 2\", :pos 0}", :states ["19168567c3f4c8b1" "865741f5dae8819e" "7f7bd28b4563fabd" "bca3e99455b8e931" "f8be77e905b63d2f"]}
{:transcript " {:component \"main\", :id sync-main-component}\n {:component \"main\", :copy sync-main-copyhead-0}\n {:target #fn, :property :fills, :value \"#00ff00\"}\n {:target #fn, :property :name, :value \"Button/Primary\"}\n {:action :shortcut, :params {:kind :create-component, :targets [#fn]}}\n \n {:target #fn, :value \"Value 2\", :pos 0}", :states ["19168567c3f4c8b1" "865741f5dae8819e" "7f7bd28b4563fabd" "bca3e99455b8e931" "f8be77e905b63d2f"]}
@ -72,10 +72,10 @@
:case-aw-variant-layer-rename-and-cleanup
[{:transcript " {:component \"main\", :id sync-main-component}\n {:variant-set \"vset\", :variant-id sync-vset-vcontainer, :count 2}\n {:component \"main\", :level 0, :outer sync-main-outercomp-0}\n {:component \"main\", :copy sync-main-copyhead-0}\n {:target #fn, :property :fills, :value \"#00ff00\"}\n {:set-name \"vset\", :edit :rename-in-layers, :params {:member \"a\", :name \"Property 1=a2, Size=S\"}}\n \n {:set-name \"vset\", :edit :add, :params {:property-name \"Empty\"}}\n {:set-name \"vset\", :edit :remove-empty, :params {}}\n \n {:set-name \"vset\", :edit :rename-in-layers, :params {:member \"a\", :name \"Property 1=a2\"}}\n \n {:set-name \"vset\", :edit :error, :params {:member \"b\", :value \"Duplicated\"}}\n \n {:set-name \"vset\", :edit :error, :params {:member \"b\", :value nil}}\n \n {:target #fn, :value \"b\", :pos 0}", :states ["df9a042b9d59d699" "4b53486972986d97" "8553f89a9ebbcf54" "4b53486972986d97" "d65ad905aa97d661" "6daf5f692c6595f1" "d65ad905aa97d661" "cc7d2dfd618fc8cb" "6390b4d83995b629"]}]
:case-ax-variant-shortcuts-add-members
[{:transcript " {:component \"main\", :id sync-main-component}\n {:variant-set \"vset\", :variant-id sync-vset-vcontainer, :count 2}\n {:component \"main\", :level 0, :outer sync-main-outercomp-0}\n {:component \"main\", :copy sync-main-copyhead-0}\n {:target #fn, :property :fills, :value \"#00ff00\"}\n {:action :shortcut, :params {:kind :create-component, :targets [#fn]}}", :states ["df9a042b9d59d699" "00b4a79bae39f703" "3c821d5f87436799"]}
{:transcript " {:component \"main\", :id sync-main-component}\n {:variant-set \"vset\", :variant-id sync-vset-vcontainer, :count 2}\n {:component \"main\", :level 0, :outer sync-main-outercomp-0}\n {:component \"main\", :copy sync-main-copyhead-0}\n {:target #fn, :property :fills, :value \"#00ff00\"}\n {:action :shortcut, :params {:kind :duplicate, :targets [#fn]}}", :states ["df9a042b9d59d699" "00b4a79bae39f703" "3c821d5f87436799"]}
{:transcript " {:component \"main\", :id sync-main-component}\n {:variant-set \"vset\", :variant-id sync-vset-vcontainer, :count 2}\n {:component \"main\", :level 0, :outer sync-main-outercomp-0}\n {:component \"main\", :copy sync-main-copyhead-0}\n {:target #fn, :property :fills, :value \"#00ff00\"}\n {:action :shortcut, :params {:kind :create-component, :targets [#fn #fn]}}", :states ["df9a042b9d59d699" "07e820c7c676a7b0" "d3972300b9f78ab6"]}
{:transcript " {:component \"main\", :id sync-main-component}\n {:variant-set \"vset\", :variant-id sync-vset-vcontainer, :count 2}\n {:component \"main\", :level 0, :outer sync-main-outercomp-0}\n {:component \"main\", :copy sync-main-copyhead-0}\n {:target #fn, :property :fills, :value \"#00ff00\"}\n {:action :shortcut, :params {:kind :duplicate, :targets [#fn #fn]}}", :states ["df9a042b9d59d699" "07e820c7c676a7b0" "d3972300b9f78ab6"]}]
[{:transcript " {:component \"main\", :id sync-main-component}\n {:variant-set \"vset\", :variant-id sync-vset-vcontainer, :count 2}\n {:component \"main\", :level 0, :outer sync-main-outercomp-0}\n {:component \"main\", :copy sync-main-copyhead-0}\n {:target #fn, :property :fills, :value \"#00ff00\"}\n {:action :shortcut, :params {:kind :create-component, :targets [#fn]}}", :states ["df9a042b9d59d699" "43d53bfb72ea0e0c" "419f9e2fc8ad2835"]}
{:transcript " {:component \"main\", :id sync-main-component}\n {:variant-set \"vset\", :variant-id sync-vset-vcontainer, :count 2}\n {:component \"main\", :level 0, :outer sync-main-outercomp-0}\n {:component \"main\", :copy sync-main-copyhead-0}\n {:target #fn, :property :fills, :value \"#00ff00\"}\n {:action :shortcut, :params {:kind :duplicate, :targets [#fn]}}", :states ["df9a042b9d59d699" "43d53bfb72ea0e0c" "419f9e2fc8ad2835"]}
{:transcript " {:component \"main\", :id sync-main-component}\n {:variant-set \"vset\", :variant-id sync-vset-vcontainer, :count 2}\n {:component \"main\", :level 0, :outer sync-main-outercomp-0}\n {:component \"main\", :copy sync-main-copyhead-0}\n {:target #fn, :property :fills, :value \"#00ff00\"}\n {:action :shortcut, :params {:kind :create-component, :targets [#fn #fn]}}", :states ["df9a042b9d59d699" "cc394d17823fd4a0" "c3e08163f30218ee"]}
{:transcript " {:component \"main\", :id sync-main-component}\n {:variant-set \"vset\", :variant-id sync-vset-vcontainer, :count 2}\n {:component \"main\", :level 0, :outer sync-main-outercomp-0}\n {:component \"main\", :copy sync-main-copyhead-0}\n {:target #fn, :property :fills, :value \"#00ff00\"}\n {:action :shortcut, :params {:kind :duplicate, :targets [#fn #fn]}}", :states ["df9a042b9d59d699" "cc394d17823fd4a0" "c3e08163f30218ee"]}]
:case-ay-token-on-a-main-reaches-copies
[{:transcript " {:component \"main\", :id sync-main-component}\n {:tokens [[\"radius.s\" 10] [\"radius.l\" 30]]}\n {:component \"main\", :copy sync-main-copyhead-0}\n {:target #fn, :token-name \"radius.s\", :value 10}\n {:target #fn, :token-name \"radius.l\", :value 30}\n \n {:target #fn, :token-name \"radius.l\"}", :states ["04cddffca776d7fb" "24756b13d210f3a5" "8e8c0c21d4a0638b" "f656f9c8188793cf"]}
{:transcript " {:component \"main\", :id sync-main-component}\n {:tokens [[\"radius.s\" 10] [\"radius.l\" 30]]}\n {:component \"main\", :copy sync-main-copyhead-0}\n {:target #fn, :token-name \"radius.s\", :value 10}\n {:target #fn, :token-name \"radius.l\", :value 30}\n \n one-of -> ", :states ["04cddffca776d7fb" "24756b13d210f3a5" "8fb17ce46cb883c8"]}
@ -156,12 +156,12 @@
{:transcript " {:component \"main\", :id sync-main-component}\n {:variant-set \"vset\", :variant-id sync-vset-vcontainer, :count 4}\n {:component \"main\", :level 0, :outer sync-main-outercomp-0}\n {:component \"main\", :level 1, :outer sync-main-outercomp-1}\n {:component \"main\", :level 2, :outer sync-main-outercomp-2}\n one-of -> \n one-of -> \n {:target #fn, :value \"v3\", :pos 0}\n ", :states ["5465856887185ca0" "72519cdcd7188cc4"]}
{:transcript " {:component \"main\", :id sync-main-component}\n {:variant-set \"vset\", :variant-id sync-vset-vcontainer, :count 4}\n {:component \"main\", :level 0, :outer sync-main-outercomp-0}\n {:component \"main\", :level 1, :outer sync-main-outercomp-1}\n {:component \"main\", :level 2, :outer sync-main-outercomp-2}\n one-of -> \n one-of -> \n one-of -> \n ", :states ["0873995f173d4a79"]}]
:case-n-geometry-sync-with-rotated-instances
[{:transcript " {:target :copy-root, :angle 45}\n {:target :main-root, :angle 30}\n {:target :main-instance, :property :fills, :value \"#ff0000\"}", :states ["ba9a6dac13f812ea" "53df1008cfc253d9" "5209d5674c0f3d7d" "25decd78552afde5"]}
{:transcript " {:target :copy-root, :angle 45}\n {:target :main-root, :angle 30}\n {:target :main-instance, :value 80}", :states ["ba9a6dac13f812ea" "53df1008cfc253d9" "3770b3227c55f2e3" "3e932096c59afd2a"]}
[{:transcript " {:target :copy-root, :angle 45}\n {:target :main-root, :angle 30}\n {:target :main-instance, :property :fills, :value \"#ff0000\"}", :states ["ba9a6dac13f812ea" "f3351f6d926b041a" "4a494df6989d018f" "7a9f7377eea04aef"]}
{:transcript " {:target :copy-root, :angle 45}\n {:target :main-root, :angle 30}\n {:target :main-instance, :value 80}", :states ["ba9a6dac13f812ea" "f3351f6d926b041a" "b491da7458174877" "a123c1e4ec60897d"]}
{:transcript " {:target :copy-root, :angle 45}\n one-of -> \n {:target :main-instance, :property :fills, :value \"#ff0000\"}", :states ["ba9a6dac13f812ea" "65ca84670680dd3f" "a920758fa5650950"]}
{:transcript " {:target :copy-root, :angle 45}\n one-of -> \n {:target :main-instance, :value 80}", :states ["ba9a6dac13f812ea" "3f78d8893ed44646" "ca1b4068acd4cf89"]}
{:transcript "one-of -> \n {:target :main-root, :angle 30}\n {:target :main-instance, :property :fills, :value \"#ff0000\"}", :states ["9209eb7b4ee877fa" "594405b1ecce4064" "9db6f380604b2fea"]}
{:transcript "one-of -> \n {:target :main-root, :angle 30}\n {:target :main-instance, :value 80}", :states ["9209eb7b4ee877fa" "1834062b7878fd2f" "85d3c98c7713553c"]}
{:transcript "one-of -> \n {:target :main-root, :angle 30}\n {:target :main-instance, :property :fills, :value \"#ff0000\"}", :states ["483eb80a902eacfa" "e114d8a40d0ce273" "bb70b68365bdacc0"]}
{:transcript "one-of -> \n {:target :main-root, :angle 30}\n {:target :main-instance, :value 80}", :states ["483eb80a902eacfa" "a7012e0e406ede72" "dcc10b7c43621315"]}
{:transcript "one-of -> \n one-of -> \n {:target :main-instance, :property :fills, :value \"#ff0000\"}", :states ["386e940ac63af4b1" "8f48f693ee787fec"]}
{:transcript "one-of -> \n one-of -> \n {:target :main-instance, :value 80}", :states ["a41078de929e10aa" "76237d559c31a37e"]}]
:case-o-variant-switch-keeps-override-only-where-mains-agree
@ -194,10 +194,10 @@
{:transcript " {:component \"main\", :id sync-main-component}\n {:component \"main\", :level 1, :outer sync-main-outercomp-1}\n {:component \"main\", :level 1, :outer sync-main-outercomp-1}\n {:component \"main\", :copy sync-main-copyhead-0}\n {:target #fn, :property :fills, :value \"#00ff00\"}\n {:target #fn, :new-head 8}\n {:target #fn, :property :fills, :value \"#0000ff\"}", :states ["170f05fe60d794fb" "6f95a0e52dbc58f3" "1682e155232c79ff" "7e41824902c17537"]}
{:transcript " {:component \"main\", :id sync-main-component}\n {:component \"main\", :level 1, :outer sync-main-outercomp-1}\n {:component \"main\", :level 1, :outer sync-main-outercomp-1}\n {:component \"main\", :copy sync-main-copyhead-0}\n one-of -> \n {:target #fn, :new-head 8}\n {:target #fn, :property :fills, :value \"#0000ff\"}", :states ["e09ce2b9958878d1" "59617d71e336c70c" "4f37d9c4a6c72163"]}]
:case-touched-copy-child-survives-main-rotation
[{:transcript " {:target :copy-root, :angle 30}\n {:target :copy-instance, :value 60}\n \n {:target :main-root, :angle 40}", :states ["4b1199b00726b9ad" "cfb4d7484a4abd78" "f4cf8a16c5457089" "fa0aab3b284e3448"]}]
[{:transcript " {:target :copy-root, :angle 30}\n {:target :copy-instance, :value 60}\n \n {:target :main-root, :angle 40}", :states ["e5382e7917232740" "b210314af79df750" "81f04c0e4b057fdf" "c54b585f599dc249"]}]
:case-u-nested-copy-reaches-copies-of-the-receiving-main
[{:transcript " {:component \"outer\", :id sync-outer-component}\n {:component \"inner\", :id sync-inner-component}\n {:component \"outer\", :copy sync-outer-copyhead-0}\n {:component \"inner\", :copy sync-inner-copyhead-0}\n {:copy #fn, :into #fn, :cell nil}\n {:target #fn, :property :fills, :value \"#00ff00\"}\n {:target #fn, :property :fills, :value \"#0000ff\"}", :states ["3e741919b165a9b1" "3b36d2a788757ccf" "13cc7bde13f6db61" "d2df2363c6fd87ee"]}
{:transcript " {:component \"outer\", :id sync-outer-component}\n {:component \"inner\", :id sync-inner-component}\n {:component \"outer\", :copy sync-outer-copyhead-0}\n {:component \"inner\", :copy sync-inner-copyhead-0}\n {:copy #fn, :into #fn, :cell nil}\n one-of -> \n {:target #fn, :property :fills, :value \"#0000ff\"}", :states ["3e741919b165a9b1" "20ac8b615e23f0ea" "3b80647668a43256"]}]
[{:transcript " {:component \"outer\", :id sync-outer-component}\n {:component \"inner\", :id sync-inner-component}\n {:component \"outer\", :copy sync-outer-copyhead-0}\n {:component \"inner\", :copy sync-inner-copyhead-0}\n {:copy #fn, :into #fn, :cell nil}\n {:target #fn, :property :fills, :value \"#00ff00\"}\n {:target #fn, :property :fills, :value \"#0000ff\"}", :states ["9e0426e1dc456cde" "eb401f2227844ad7" "13cc7bde13f6db61" "d2df2363c6fd87ee"]}
{:transcript " {:component \"outer\", :id sync-outer-component}\n {:component \"inner\", :id sync-inner-component}\n {:component \"outer\", :copy sync-outer-copyhead-0}\n {:component \"inner\", :copy sync-inner-copyhead-0}\n {:copy #fn, :into #fn, :cell nil}\n one-of -> \n {:target #fn, :property :fills, :value \"#0000ff\"}", :states ["9e0426e1dc456cde" "20ac8b615e23f0ea" "3b80647668a43256"]}]
:case-v-new-edit-after-undo-clears-redo
[{:transcript " {:target :main-instance, :property :fills, :value \"#ff0000\"}\n {:times 1}\n {:target :main-instance, :property :fills, :value \"#00ff00\"}\n {:times 1}\n \n ", :states ["386e940ac63af4b1" "734ec524d88eb6bb" "2249ff7b4eb6cdaa" "734ec524d88eb6bb" "2249ff7b4eb6cdaa" "2249ff7b4eb6cdaa" "fb1a35e87f575e72"]}]
:case-w-undo-several-steps-over-nested-cascades

View File

@ -256,8 +256,9 @@
[(dwl/reset-component (tm/target-shape-id situation (:target op)))]
(instance? n/UpdateMain op)
[(dwl/update-component-sync (tm/target-shape-id situation (:target op))
(:id (tm/file situation)))]
(let [id (tm/target-shape-id situation (:target op))
shape (tm/shape-by-id situation id)]
[(dwl/update-component-sync id (:component-file shape))])
(instance? n/DeleteComponent op)
(let [root (tm/shape-by-id situation (tm/target-shape-id situation (:target op)))]
@ -358,6 +359,17 @@
(let [st @st/state]
(get-in st [:files (:current-file-id st)])))
(defn refresh-generated
"Read every generated file after events, keeping library installation
metadata out of the portable situation. Undo snapshots still keep it."
[situation]
(-> situation
(tm/with-file (current-file))
(tm/with-aux-files
(into {} (map (fn [[id _]]
[id (dissoc (get-in @st/state [:files id]) :library-of)]))
(tm/aux-files situation)))))
(defn fill-of
"The fill color of the shape `label` in the live file."
[label]
@ -440,7 +452,7 @@
[situation op]
(let [before (tm/file situation)
situation (-> situation
(tm/with-file (current-file))
(refresh-generated)
(record-choices op))
op (unwrap op)
record (cond-> (dissoc (into {} op) ::tm/id)
@ -707,7 +719,7 @@
check-step?
;; go on from the redone file, as the pure runner does
(.then (check-step situation (unwrap op) before index-before)
#(run-ops (tm/with-file % (current-file)) (rest ops)))
#(run-ops (refresh-generated %) (rest ops)))
;; never undo past the state a case's own undo went back to
(and (::undo-check? situation)