✨ Generate shadow, blur and corner edits

Add drop/inner shadows, layer and background blur, and the three remaining
corner radii to generated style edits. Preserve the existing input order
and describe their authored groups independently of production metadata.

Check distinct inputs, main propagation, preserved overrides, corrupt
values and lost touched groups. Run component update, reset, detach and
exact undo/redo histories in common and the async frontend.

The input test fails six assertions before the addition. Focused common
checks pass 4 tests / 90 assertions; the six frontend histories pass
204 assertions. Full common JVM/JS, frontend, backend and exporter suites
and lint/format checks pass, including six 40-program properties at seed
712. F227's parity correction is covered separately. Golden data and
production behavior are unchanged.

Wider attribute/layout and source-copy generation, pending-work coverage
and the five fresh 5,000-program gates remain open.

AI-assisted-by: GPT-6
This commit is contained in:
alonso.torres 2026-10-28 01:10:31 +01:00
parent c79b09af24
commit 9483d9f737
5 changed files with 104 additions and 3 deletions

View File

@ -91,7 +91,8 @@
;; ---------------------------------------------------------------------------
(def scalar-properties
#{:strokes :hidden :blocked :r1 :blend-mode :hide-fill-on-export :show-content
#{:strokes :shadow :blur :background-blur :hidden :blocked :r1 :r2 :r3 :r4
:blend-mode :hide-fill-on-export :show-content
:fixed-scroll :constraints-h :constraints-v :layout-item-margin
:layout-gap :layout-padding :layout-align-items})

View File

@ -373,7 +373,28 @@
[:layout-item-margin [{:m1 0 :m2 0 :m3 0 :m4 0} {:m1 4 :m2 4 :m3 4 :m4 4}]]
[:layout-gap [{:row-gap 0 :column-gap 0} {:row-gap 8 :column-gap 8}]]
[:layout-padding [{:p1 0 :p2 0 :p3 0 :p4 0} {:p1 8 :p2 8 :p3 8 :p4 8}]]
[:layout-align-items [:start :center :end]]])
[:layout-align-items [:start :center :end]]
[:shadow [[] [{:id #uuid "10000000-0000-0000-0000-000000000001"
:style :drop-shadow :color {:color "#000000" :opacity 0.5}
:offset-x 2 :offset-y 3 :blur 4 :spread 1 :hidden false}]
[{:id #uuid "10000000-0000-0000-0000-000000000001"
:style :inner-shadow :color {:color "#ff0000" :opacity 0.75}
:offset-x -2 :offset-y 4 :blur 8 :spread 2 :hidden false}]]]
[:blur [{:id #uuid "10000000-0000-0000-0000-000000000002"
:type :layer-blur :value 0 :hidden true}
{:id #uuid "10000000-0000-0000-0000-000000000002"
:type :layer-blur :value 4 :hidden false}
{:id #uuid "10000000-0000-0000-0000-000000000002"
:type :layer-blur :value 12 :hidden false}]]
[:background-blur [{:id #uuid "10000000-0000-0000-0000-000000000003"
:type :background-blur :value 0 :hidden true}
{:id #uuid "10000000-0000-0000-0000-000000000003"
:type :background-blur :value 6 :hidden false}
{:id #uuid "10000000-0000-0000-0000-000000000003"
:type :background-blur :value 16 :hidden false}]]
[:r2 [0 8 16]]
[:r3 [0 8 16]]
[:r4 [0 8 16]]])
(def text-style-edits
"All twelve style units, independent of the production text group table."

View File

@ -19,7 +19,9 @@
(def groups
{:fills :fill-group :opacity :layer-effects-group :name :name-group
:strokes :stroke-group :hidden :visibility-group :blocked :modifiable-group
:r1 :radius-group :blend-mode :layer-effects-group :hide-fill-on-export :fill-group
:r1 :radius-group :r2 :radius-group :r3 :radius-group :r4 :radius-group
:shadow :shadow-group :blur :blur-group :background-blur :blur-group
:blend-mode :layer-effects-group :hide-fill-on-export :fill-group
:show-content :show-content :fixed-scroll :constraints-group
:constraints-h :constraints-group :constraints-v :constraints-group
:layout-item-margin :layout-item-margin :layout-gap :layout-gap

View File

@ -85,6 +85,74 @@
(t/is (= :frame (get-in resolved [:shape :type]))
(str "property " property ", scope " scope))))))
(t/deftest generated-effect-and-corner-edits-have-distinct-inputs
(let [situation (cg/world [2 1 {}])]
(doseq [[index attr] [[14 :shadow] [15 :blur] [16 :background-blur]
[17 :r2] [18 :r3] [19 :r4]]]
(let [resolved (cg/resolve-op situation {} [:style 0 index 1 1])]
(t/is (= attr (:property resolved)))
(t/is (some? (:op resolved)))
(when (= attr (:property resolved))
(t/is (not= (get (:shape resolved) attr) (:value resolved))))))))
(t/deftest effect-and-corner-overrides-reject-lost-values-or-groups
(doseq [[index attr group] [[14 :shadow :shadow-group] [15 :blur :blur-group]
[16 :background-blur :blur-group] [17 :r2 :radius-group]
[18 :r3 :radius-group] [19 :r4 :radius-group]]]
(let [initial (cg/initial-state [1 0 {}])
resolved (cg/resolve-op (:situation initial) {} [:style 0 index 1 1])
shape (:shape resolved)
expected (shadow/values (assoc shape attr (:value resolved)) group)
edited (-> initial (cg/step [:style 0 index 1 1]) cg/settle-state)
situation (:situation edited)
model (:model edited)
file (tm/file situation)
path [:data :pages-index (:id (thf/current-page file)) :objects (:id shape)]
invariant (fn [file]
(try (oracle/check (tm/with-file situation file) model) nil
(catch #?(:clj Exception :cljs js/Error) error
(:invariant (ex-data error)))))]
(t/is (= expected (get-in model [(:id shape) group])))
(t/is (= :I9 (invariant (update-in file path
#(merge (dissoc % attr) (select-keys shape [attr]))))))
(t/is (= :I3 (invariant (update-in file (conj path :touched) disj group)))))))
(t/deftest main-effect-and-corner-edits-reach-clean-copies-and-spare-overrides
(doseq [property (range 14 20)]
(let [initial (cg/initial-state [1 0 {}])
situation (:situation initial)
resolved (cg/resolve-op situation {} [:style 0 property 1 1])
copy (:shape resolved)
attr (:property resolved)
main-index (first (keep-indexed (fn [index entry]
(when (= (:shape-ref copy) (get-in entry [:shape :id])) index))
(cg/candidates situation {} :edit 0)))
main-edit [:style main-index property 0 0]
copies (filter #(= (:shape-ref copy) (get-in % [:shape :shape-ref])) (cg/tree situation))
inherited (-> initial (cg/step main-edit) cg/settle-state)
overridden (-> inherited (cg/step [:style 0 property 1 1]) cg/settle-state)
updated (-> overridden (cg/step (assoc main-edit 3 2)) cg/settle-state)
requested (:value (cg/resolve-op situation {} (assoc main-edit 3 2)))]
(t/is (some? main-index))
(t/is (= 2 (count copies)))
(t/is (not= requested (:value resolved)))
(doseq [{:keys [shape]} copies]
(let [actual (tm/shape-by-id (:situation updated) (:id shape))]
(t/is (= (if (= (:id copy) (:id shape)) (:value resolved) requested) (get actual attr)))
(t/is (= (= (:id copy) (:id shape)) (boolean (seq (:touched actual)))))))
(t/is (map? (cg/run {:world [1 0 {}]
:ops [main-edit [:style 0 property 1 1] (assoc main-edit 3 2)
[:undo] [:redo]]}))))))
(t/deftest generated-effects-and-corners-survive-component-operations
(doseq [property (range 14 20)]
(t/is (map? (cg/run {:world [2 1 {:variants true :rich-variants true}]
:ops [[:style 0 property 1 1] [:style 1 property 1 0]
[:undo] [:redo] [:update-main 0]
[:style 0 property 0 1] [:reset 0]
[:style 0 property 1 1] [:detach 0]
[:undo] [:redo]]})))))
(t/deftest text-style-edits-cover-every-style-unit
(doseq [[unit group] (map-indexed vector
[:text-content-family :text-content-weight :text-content-size

View File

@ -93,6 +93,15 @@
:ops [[:delete 9 9 1 2] [:text 5 9 1 3] [:style 3 1 1]
[:restore 5 8 1 6] [:nest 5 6 0 2] [:undo] [:redo]]})))
(t/deftest ^:async generated-effects-and-corners-survive-component-operations
(doseq [property (range 14 20)]
(await (replay {:world [2 1 {:variants true :rich-variants true}]
:ops [[:style 0 property 1 1] [:style 1 property 1 0]
[:undo] [:redo] [:update-main 0]
[:style 0 property 0 1] [:reset 0]
[:style 0 property 1 1] [:detach 0]
[:undo] [:redo]]}))))
(t/deftest ^:async swapping-a-copy-with-authored-position-constraints-stays-settled
(await (replay {:world [2 1 {:library true :variants true :rich-variants true
:text true :text-styles true :layout :flex :layout-copies true}]