Slide rows along time and restack them, at any depth

A node's bar drags along the timeline: one write to its :at, for every node
alike, carried down through the instances above it. The stage and rows show
the slide live through ::render/clip, and it lands as one edit on release.

A row dropped on another's top or bottom edge goes in front of it or behind:
one write to :z, between its new neighbours (symbol/z-between). Onto the edge
of a row in another symbol, it moves there first. Neither needs anything on
screen — nest/down walks a row path by structure, without a frame.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Olive Vaughn 2026-09-30 00:51:34 -04:00
parent 1b8bbc7372
commit 38850a29ba
8 changed files with 306 additions and 32 deletions

View file

@ -192,3 +192,49 @@
(select-keys (get-in moved [:symbols :box :nodes :tri :time]) [:at :rate]))
"half speed inside something at double speed, so it runs as it did")
(is (= (picture c :main fs) (picture moved :main fs)))))
(deftest sliding-a-shape-inside-a-retimed-instance-moves-it-by-frames-of-the-open-symbol
;; `box` runs at double speed from 10, so six frames of main are twelve of box.
(let [c (-> (studio)
(update-in [:symbols :main :nodes] dissoc :tri)
(assoc-in [:symbols :main :nodes a-uuid :time :rate] 2)
(assoc-in [:symbols :box :nodes :inner :channels [:geom :pts]]
(ch/keyed {4 [5 10 15 10 5 40] 20 [5 10 45 10 5 40]})))
{slid :clip :as r} (nest/slide c :main [a-uuid :inner] 6)
fs [13 15 18 20]]
(is (nil? (:refused r)) (:refused r))
(is (= {:mode :map :at 12 :rate 1} (get-in slid [:symbols :box :nodes :inner :time])))
(is (= [4 60] (get-in slid [:symbols :box :nodes :inner :span])) "its span and keys are its own")
(is (= (picture c :main fs) (picture slid :main (map #(+ 6 %) fs)))
"what it did at a frame of main it now does six frames later")
(is (seq (first (picture c :main [14]))))
(is (empty? (first (picture slid :main [14]))) "and it is not there yet where it was")
(testing "sliding it again adds to where it is"
(is (= 2 (get-in (:clip (nest/slide slid :main [a-uuid :inner] -5))
[:symbols :box :nodes :inner :time :at]))))))
(deftest sliding-needs-nothing-on-screen-and-refuses-only-a-loop
(let [c (studio)]
(is (= 3 (get-in (:clip (nest/slide c :main [a-uuid :inner] 3))
[:symbols :box :nodes :inner :time :at]))
"the instance starts at 10, and where the playhead is has nothing to do with it")
(is (:refused (nest/slide (assoc-in c [:symbols :main :nodes a-uuid :time :loop?] true)
:main [a-uuid :inner] 3))
"but through a loop one frame outside is many inside")))
(deftest restacking-is-one-write-to-z
(let [shape (fn [z] {:kind :poly :z z :channels {}})
c (-> (clip/blank)
(assoc-in [:symbols :main :nodes] {:a (shape "a1") :b (shape "a2") :c (shape "a3")}))
order (fn [c] (map key (sort-by (comp :z val) (get-in c [:symbols :main :nodes]))))
front (:clip (nest/restack c :main [:a] [:c] true))
back (:clip (nest/restack c :main [:c] [:a] false))
mid (:clip (nest/restack c :main [:a] [:b] true))]
(is (= [:b :c :a] (order front)) "in front of the front one")
(is (= [:c :a :b] (order back)) "behind the back one")
(is (= [:b :a :c] (order mid)) "between two")
(is (= (dissoc (get-in c [:symbols :main :nodes]) :a)
(dissoc (get-in mid [:symbols :main :nodes]) :a))
"and nothing else is renumbered")
(is (:refused (nest/restack c :main [:a] [a-uuid :inner] true))
"only among the things it is beside")))

View file

@ -107,6 +107,21 @@
(is (= [:a :b :c] (ids-at with 0)))
(is (= (get-in base [:nodes :a]) (get-in with [:nodes :a])) "and :a is untouched")))
(deftest z-between-always-finds-room
(let [between? (fn [a b z] (and (or (nil? a) (neg? (compare a z)))
(or (nil? b) (neg? (compare z b)))))]
(testing "on the keys scenes already have"
(doseq [[a b] [[nil "a1"] ["a1" nil] ["a1" "a2"] ["a1" "a3"] ["a" "a1"]
["a1" "z1727000000000-shape"] [nil "0"] [nil "-"] ["c0000" "c0001"]]]
(is (between? a b (symbol/z-between a b)) (pr-str [a b (symbol/z-between a b)]))))
(testing "and again and again: to the back, to the front, and into one gap from either side"
(doseq [[label start step past?] [["back" "a1" #(symbol/z-between nil %) #(between? nil %1 %2)]
["front" "a1" #(symbol/z-between % nil) #(between? %1 nil %2)]
["under a2" "a1" #(symbol/z-between % "a2") #(between? %1 "a2" %2)]
["over a1" "a2" #(symbol/z-between "a1" %) #(between? "a1" %1 %2)]]]
(is (every? (fn [[x y]] (past? x y)) (partition 2 1 (take 300 (iterate step start))))
label)))))
;; ---- transform composition through the tree ----
(deftest geometry-lands-in-the-parents-space