Edit shapes nested in other symbols, from any symbol above them

`nest/inside` walked a row path only through instances. Every node has
the same two maps to its parent, so the walk now steps into any node:
inside an instance is the symbol it places, inside a shape is where its
points and keys are. The selected shape at any depth is `inside` over
its full path, which gives the stage editor its handles (through the
matrix, drags back through the inverse) and the inspector the shape's
own frame to key at, and the time map back for jumping to a key.

`inside` resolves only the node's lineage: where a node is depends on
its parents and nothing else, and resolving the whole symbol cost more
than a stage frame (7.9ms against 3ms on the swarm; now 0.09ms).
Checked equal to the whole-symbol resolve on every node and frame of
the swarm, two instances deep.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Olive Vaughn 2026-09-30 00:12:21 -04:00
parent 6a53adb5e0
commit 1b8bbc7372
6 changed files with 130 additions and 49 deletions

View file

@ -47,6 +47,42 @@
(map - drawn (take 6 (array-seq (:pts op)))))
"resolved back out through the instance, it is exactly what was drawn")))
(deftest a-shape-two-instances-down-is-edited-where-it-is-seen
(let [u #uuid "00000000-0000-4000-8000-0000000000d1"
v #uuid "00000000-0000-4000-8000-0000000000d2"
turn (fn [c host id pos rot k]
(update-in c [:symbols host :nodes id :channels] merge
{[:xform :pos] (ch/framed pos)
[:xform :rot] (ch/framed rot)
[:xform :scale] (ch/framed [k k])}))
c (-> (clip/blank)
(assoc-in [:symbols :mid] {:id :mid :frames 40 :nodes {}})
(assoc-in [:symbols :box] {:id :box :frames 30 :nodes {}})
(clip/place-symbol nil :main :mid 10 u nil)
(clip/place-symbol nil :mid :box 2 v nil)
(turn :main u [40 20] (/ js/Math.PI 2) 2)
(turn :mid v [5 -3] 0.3 1.5)
(paint/new-shape :box :shape 4 [0 0 10 0 5 10] :brow))
draw #(take 6 (array-seq (:pts (first (filter (fn [op] (= [u [v :shape]] (:node op)))
((clip/resolver % nil pal/index-of :main) 16))))))
{:keys [frame matrix time]} (nest/inside c nil :main [u v :shape] 16)
out (js/Float64Array. 2)
seen (mapcat (fn [[x y]] (vec (array-seq (node/apply-pt! out 0 matrix x y))))
(partition 2 [0 0 10 0 5 10]))
[x y] (array-seq (node/apply-pt! out 0 (nest/invert matrix) 7 3))
moved (paint/set-vertex c :box :shape frame 0 [x y])
keyed (paint/add-key c :box :shape (:frame (nest/inside c nil :main [u v :shape] 20)))]
(is (= 4 frame) "16 of main is 6 of mid, which is 4 of box and of the shape in it")
(is (= {:at 12 :rate 1} time))
(is (= #{4 8} (set (keys (get-in keyed [:symbols :box :nodes :shape :channels paint/geometry :keys]))))
"a key added at 20 of main lands at 8, the shape's own time")
(is (nil? (nest/inside c nil :main [u v :shape] 13))
"and where the shape is not on screen there is nothing to edit")
(is (every? #(< (js/Math.abs %) 1e-9) (map - (draw c) seen))
"the handles sit on what the stage draws")
(is (every? #(< (js/Math.abs %) 1e-9) (map - [7 3] (take 2 (draw moved))))
"and a vertex dragged to (7, 3) is drawn at (7, 3)")))
(deftest a-placed-symbols-sound-is-heard-where-it-is-placed
(let [voice {:id :v :kind :audio :source {:footage "f"} :z "a1"
:span [10 40] :time {:mode :map :at -10 :rate 1}