One time map for every node; move and group nodes between symbols
Every node now has the same time map into its parent, local = rate·(parent − at), with its span and keys in its own frames: what instances had, made the rule. A shape without a time map reads as it always did, so no data changes. The per-kind branches, the span-start term and the rate refusal are gone; node/time-of, then-time and invert-time compose it like the matrix. clip/move-node puts a node into another symbol without changing the picture or the timing — its matrix becomes a :pinv, its time a new :at and :rate, and its channels, keys and span are untouched — and clip/group makes a new symbol around side-by-side nodes. Generated parts, split stencils, cycles and looping instances are refused with the reason. Nested sounds use the same map. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
6bec121108
commit
eafbe6c4d2
7 changed files with 360 additions and 91 deletions
|
|
@ -257,9 +257,9 @@
|
|||
(deftest an-instance-span-is-in-its-own-frames
|
||||
(let [n {:id :i :kind :instance :of :x :z "a1" :span [3 13]
|
||||
:time {:mode :map :at 40 :rate 2}}]
|
||||
(is (= [40 45] (node/placed-span n)) "ten own frames at double rate is five")
|
||||
(is (= 3 (node/local-frame n 40)) "the parent's :at is the span's first frame")
|
||||
(is (= 11 (node/local-frame n 44)))
|
||||
(is (= [41.5 46.5] (node/placed-span n)) "own frames 3 to 13, at double rate, from 40")
|
||||
(is (= 0 (node/local-frame n 40)) "the parent's :at is where its own frame 0 lands")
|
||||
(is (= 8 (node/local-frame n 44)))
|
||||
(is (= [2 9] (node/placed-span {:kind :poly :span [2 9]}))
|
||||
"a shape has no time of its own, so its span is already the parent's")
|
||||
(is (seq (node/problems (assoc-in n [:time :in] 3)))
|
||||
|
|
@ -311,7 +311,7 @@
|
|||
|
||||
(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 0 :rate 1}
|
||||
:span [10 40] :time {:mode :map :at -10 :rate 1}
|
||||
:channels {[:audio :gain] (ch/keyed {0 0.0 5 1.0})}}
|
||||
c (-> (clip/blank)
|
||||
(assoc-in [:symbols :talk] {:id :talk :frames 30 :nodes {:v voice}})
|
||||
|
|
@ -351,3 +351,95 @@
|
|||
(is (= [55 35] (clip/center grown nil :box)) "the symbol's middle moved")
|
||||
(is (= [25 35] (get-in grown [:symbols :main :nodes u :channels [:xform :anchor] :value]))
|
||||
"the instance's did not")))))
|
||||
|
||||
(defn- picture
|
||||
"What `sid` draws at each of `fs`, without the node paths a move changes:
|
||||
per frame, the sorted marks with their points rounded to a thousandth."
|
||||
[c sid fs]
|
||||
(let [resolve (clip/resolver c nil pal/index-of sid)
|
||||
round #(/ (js/Math.round (* 1000 %)) 1000)]
|
||||
(mapv (fn [f]
|
||||
(sort-by str (map (fn [op]
|
||||
[(:color op) (mapv round (take (* 2 (:n op)) (array-seq (:pts op))))])
|
||||
(resolve f))))
|
||||
fs)))
|
||||
|
||||
(def ^:private a-uuid #uuid "00000000-0000-4000-8000-0000000000e1")
|
||||
(def ^:private b-uuid #uuid "00000000-0000-4000-8000-0000000000e2")
|
||||
|
||||
(defn- studio
|
||||
"`main` holds a keyed shape and a moved, turned, doubled instance of `box`,
|
||||
placed at frame 10; `box` holds a shape of its own."
|
||||
[]
|
||||
(let [tri (fn [id x keyed]
|
||||
{:id id :kind :poly :z "a1" :paint? true :span [4 60]
|
||||
:channels {[:geom :pts] (ch/keyed (into {} (map (fn [[f dx]] [f [x 10 (+ x dx) 10 x 40]])) keyed))
|
||||
[:style :color] (ch/framed :brow)}})]
|
||||
(-> (clip/blank)
|
||||
(assoc-in [:symbols :main :nodes :tri] (tri :tri 100 {4 20 30 40}))
|
||||
(assoc-in [:symbols :box] {:id :box :frames 50 :nodes {:inner (tri :inner 5 {0 10})}})
|
||||
(clip/place-symbol nil :main :box 10 a-uuid nil)
|
||||
(update-in [:symbols :main :nodes a-uuid :channels] merge
|
||||
{[:xform :pos] (ch/framed [30 20])
|
||||
[:xform :rot] (ch/framed 0.5)
|
||||
[:xform :scale] (ch/framed [2 2])}))))
|
||||
|
||||
(deftest moving-a-node-into-a-symbol-changes-nothing-on-screen
|
||||
;; From frame 10, where the instance starts: inside a symbol a node exists only
|
||||
;; while that symbol is on screen, so moving the shape in cuts off 4–9.
|
||||
(let [c (studio)
|
||||
fs [10 16 29 30 45 59 60 70]
|
||||
{moved :clip :as r} (clip/move-node c nil :main [:tri] [a-uuid] 16)]
|
||||
(is (nil? (:refused r)) (:refused r))
|
||||
(is (contains? (get-in moved [:symbols :box :nodes]) :tri) "it is in the symbol now")
|
||||
(is (not (contains? (get-in moved [:symbols :main :nodes]) :tri)) "and not beside it")
|
||||
(is (= [4 60] (get-in moved [:symbols :box :nodes :tri :span]))
|
||||
"its span and keys are its own and do not change")
|
||||
(is (= -10 (get-in moved [:symbols :box :nodes :tri :time :at]))
|
||||
"its time map takes up the ten frames the instance starts late")
|
||||
(is (= (picture c :main fs) (picture moved :main fs)) "and the picture is the same, frame for frame")
|
||||
(testing "moving it back out is also invisible"
|
||||
(let [{back :clip} (clip/move-node moved nil :main [a-uuid :tri] [] 16)]
|
||||
(is (= (picture c :main fs) (picture back :main fs)))))))
|
||||
|
||||
(deftest moving-an-instance-moves-only-its-at
|
||||
(let [c (-> (studio)
|
||||
(assoc-in [:symbols :holder] {:id :holder :frames 100 :nodes {}})
|
||||
(clip/place-symbol nil :main :holder 3 b-uuid nil))
|
||||
fs [0 10 16 40 59]
|
||||
{moved :clip :as r} (clip/move-node c nil :main [a-uuid] [b-uuid] 16)
|
||||
n (get-in moved [:symbols :holder :nodes a-uuid])]
|
||||
(is (nil? (:refused r)) (:refused r))
|
||||
(is (= 7 (get-in n [:time :at])) "placed at 10 in main is at 7 inside something placed at 3")
|
||||
(is (= [0 50] (:span n)) "its own frames do not move")
|
||||
(is (= (picture c :main fs) (picture moved :main fs)))))
|
||||
|
||||
(deftest grouping-makes-a-symbol-around-them-and-changes-nothing-on-screen
|
||||
(let [c (studio)
|
||||
fs [0 4 10 16 30 59 60]
|
||||
{grouped :clip :as r} (clip/group c nil :main [[:tri] [a-uuid]] :group-1 b-uuid 16)
|
||||
inst (get-in grouped [:symbols :main :nodes b-uuid])]
|
||||
(is (nil? (:refused r)) (:refused r))
|
||||
(is (= #{:tri a-uuid} (set (keys (get-in grouped [:symbols :group-1 :nodes])))))
|
||||
(is (= [b-uuid] (keys (get-in grouped [:symbols :main :nodes]))) "one instance where they were")
|
||||
(is (= 4 (get-in inst [:time :at])) "starting where the earliest of them starts")
|
||||
(is (= 56 (clip/frames grouped :group-1)) "and lasting until the last one ends")
|
||||
(is (= (picture c :main fs) (picture grouped :main fs)))
|
||||
(is (empty? (clip/problems grouped)))))
|
||||
|
||||
(deftest what-cannot-move-says-why
|
||||
(let [c (studio)]
|
||||
(is (:refused (clip/move-node c nil :main [a-uuid] [a-uuid] 16)) "not into itself")
|
||||
(is (:refused (clip/move-node c nil :main [:tri] [a-uuid] 2)) "not while the target is off screen")
|
||||
(let [roto (assoc-in c [:symbols :main :nodes :tri :channels [:geom :pts] :generated] {:by :roto})]
|
||||
(is (re-find #"generated" (:refused (clip/move-node roto nil :main [:tri] [a-uuid] 16)))))))
|
||||
|
||||
(deftest moving-into-a-retimed-instance-keeps-the-timing
|
||||
(let [c (assoc-in (studio) [:symbols :main :nodes a-uuid :time :rate] 2)
|
||||
fs [10 12 17 20 29 34]
|
||||
{moved :clip :as r} (clip/move-node c nil :main [:tri] [a-uuid] 16)]
|
||||
(is (nil? (:refused r)) (:refused r))
|
||||
(is (= {:at -20 :rate 0.5}
|
||||
(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)))))
|
||||
|
|
|
|||
|
|
@ -132,16 +132,16 @@
|
|||
(is (= (vec (range 8)) (mapv #(node/local-frame {:id :x} %) (range 8))))
|
||||
(is (= (vec (range 8)) (mapv #(node/local-frame {:id :x :time {:mode :inherit}} %) (range 8)))))
|
||||
|
||||
(deftest a-retimed-instance-is-refused-rather-than-ignored
|
||||
;; :rate is a symbol instance's timing and symbols are out of scope. Silently
|
||||
;; dropping it would be a retimed blink playing at the wrong speed, which looks
|
||||
;; like a bad blink and not like a missing feature.
|
||||
(is (thrown-with-msg? ExceptionInfo #":rate"
|
||||
(node/local-frame {:id :x :time {:mode :map :rate 0.5}} 0)))
|
||||
(deftest every-node-has-the-same-time-map
|
||||
;; One rule, whatever the node is: `local = rate · (parent − at)`.
|
||||
(is (= 5 (node/local-frame {:id :x :time {:mode :map :rate 0.5}} 10)) "a shape, at half speed")
|
||||
(is (= 5 (node/local-frame {:id :x :kind :instance :time {:mode :map :rate 0.5}} 10)) "an instance, the same")
|
||||
(is (= 3 (node/local-frame {:id :x :time {:mode :map :at 7}} 10)) "and :at is where its frame 0 lands")
|
||||
(is (= 4 (node/local-frame {:id :x :time {:mode :map :expose 2 :rate 1.0}} 5))
|
||||
"rate 1.0 is the identity and is allowed, because it appears in the spec's example"))
|
||||
|
||||
;; ---- the shape ----
|
||||
"exposure still floors on its own clock")
|
||||
(let [m {:at 3 :rate 2}]
|
||||
(is (= {:at 0 :rate 1} (node/then-time m (node/invert-time m)))
|
||||
"a map followed by its inverse is nothing")))
|
||||
|
||||
(deftest transform-channels-default-to-the-identity
|
||||
(let [chs (node/channels {:id :x :kind :group})]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue