Constant, ramp, and return offsets now append ordinary channel layers to a lane or cel in explicit owner frames. The inspector exposes the commands as one undoable transaction, shows conflicts, and offers removal or retry while preserving generated bases through regeneration. Ordered-stack compatibility is shared by validation, conflict reporting, and regeneration, including adjacent replacement coverage. Cel-sheet gaps and headers select their lane, so commands cannot fall through to another column's stale selection. 437 tests, 5,804 assertions; both browser flows; 56 Django tests; optimized frontend build.
452 lines
24 KiB
Clojure
452 lines
24 KiB
Clojure
(ns arthur.domain.channel-test
|
|
"The channel is the load-bearing claim of the whole model: analysis and a hand
|
|
produce the SAME data, and the only difference between them is a flag nothing
|
|
in the renderer reads. These assert the parts of that claim that could silently
|
|
stop being true."
|
|
(:require [cljs.test :refer [deftest is testing]]
|
|
[arthur.domain.channel :as ch]))
|
|
|
|
;; ---- the three shapes read the same way ----
|
|
|
|
(deftest framed-is-the-same-value-at-every-frame
|
|
(let [c (ch/framed :skin-dark)]
|
|
(is (= :framed (ch/describe c)))
|
|
(is (every? #(= :skin-dark (ch/value-at c % nil)) (range -5 20)))))
|
|
|
|
(deftest keyed-holds-until-the-next-key
|
|
;; Hold is the DEFAULT, not a special case: docs/design.md requires it of every
|
|
;; cut part, and a tweened mouth reads as puppet software.
|
|
(let [c (ch/keyed {0 :a, 4 :b, 12 :c} :hold)]
|
|
(is (= :keyed (ch/describe c)))
|
|
(is (= [:a :a :a :a :b :b :b :b :b :b :b :b :c :c]
|
|
(mapv #(ch/value-at c % nil) (range 0 14))))))
|
|
|
|
(deftest linear-vector-keys-interpolate-each-component
|
|
(let [c (ch/keyed {0 [0.4 0.6], 10 [0.6 0.4]} :linear)
|
|
frames [0 5 10 5 2]
|
|
cursor (ch/cursor c nil)]
|
|
(is (empty? (ch/problems c)))
|
|
(is (= [0.5 0.5] (ch/value-at c 5 nil)))
|
|
(is (= (mapv #(ch/value-at c % nil) frames)
|
|
(mapv #(ch/sample! cursor %) frames)))))
|
|
|
|
(deftest one-channel-can-cut-then-tween
|
|
(let [c (assoc (ch/keyed {0 [0 0], 4 [4 0], 8 [8 0]} :hold)
|
|
:segments {4 :linear})
|
|
cursor (ch/cursor c nil)]
|
|
(is (= [0 0] (ch/value-at c 2 nil)))
|
|
(is (= [4 0] (ch/value-at c 4 nil)))
|
|
(is (= [6 0] (ch/value-at c 6 nil)))
|
|
(is (= (mapv #(ch/value-at c % nil) [0 2 4 6 8 3 7])
|
|
(mapv #(ch/sample! cursor %) [0 2 4 6 8 3 7])))))
|
|
|
|
(deftest a-frame-before-the-first-key-reads-the-first-key
|
|
;; The JS activeKey clamps low, and that is kept: a channel's first key is the
|
|
;; pose the part starts in. Having NO value is a different question — it is a
|
|
;; state bit, not an empty region of the key map.
|
|
(let [c (ch/keyed {10 :a, 20 :b} :hold)]
|
|
(is (= :a (ch/value-at c 0 nil)))
|
|
(is (= :a (ch/value-at c 9 nil)))
|
|
(is (= :b (ch/value-at c 999 nil)) "and clamps high by holding the last key")))
|
|
|
|
(deftest keys-are-a-map-so-frame-order-in-the-literal-cannot-matter
|
|
;; Transit and JSON both lose sortedness, so the sorted index is built at read
|
|
;; time. A resolver that trusted insertion order would work in the REPL and
|
|
;; fail after a round trip through the server, which is the worst possible way
|
|
;; to find out.
|
|
(let [forward (ch/keyed (array-map 0 :a, 4 :b, 12 :c) :hold)
|
|
backward (ch/keyed (array-map 12 :c, 4 :b, 0 :a) :hold)
|
|
shuffled (ch/keyed (array-map 4 :b, 12 :c, 0 :a) :hold)]
|
|
(doseq [c [backward shuffled]]
|
|
(is (= (mapv #(ch/value-at forward % nil) (range 0 16))
|
|
(mapv #(ch/value-at c % nil) (range 0 16)))))))
|
|
|
|
(deftest dense-reads-one-value-per-frame-out-of-a-typed-array
|
|
(let [store {"blk" {:data (js/Int16Array. #js [0 0, 10 20, 30 40, 50 60]) :state nil}}
|
|
c {:animated? true :interp :hold
|
|
:dense {:store "blk" :offset 0 :stride 2 :frames 4}
|
|
:generated {:by :roto/lips-outer :analysis "sha256:test"}}]
|
|
(is (= :dense (ch/describe c)))
|
|
(is (= [[0 0] [10 20] [30 40] [50 60]]
|
|
(mapv (fn [f] (let [v (ch/value-at c f store)]
|
|
[(ch/component v 0) (ch/component v 1)]))
|
|
(range 4))))))
|
|
|
|
(deftest a-dense-value-is-a-view-not-a-copy
|
|
;; Fixed topology is what makes this possible — the frame's data is a
|
|
;; rectangular slice at a known offset — and a copy per node per frame is
|
|
;; exactly the allocation the model exists to avoid.
|
|
(let [data (js/Int16Array. #js [1 2 3 4])
|
|
store {"blk" {:data data :state nil}}
|
|
c {:animated? true :dense {:store "blk" :offset 0 :stride 2 :frames 2}}
|
|
v (ch/value-at c 1 store)]
|
|
(is (= (.-buffer data) (.-buffer v)) "shares the block's buffer")))
|
|
|
|
(deftest a-dense-read-clamps-rather-than-running-off-the-end
|
|
;; A time map with an offset deliberately reads the future — mouth lead is the
|
|
;; entire reason :offset exists — so the last frames of a leading track ask for
|
|
;; frames past the end on every take. Clamping holds the final pose; the
|
|
;; alternative blanks the mouth at the end of every clip.
|
|
(let [store {"blk" {:data (js/Int16Array. #js [7 8 9]) :state nil}}
|
|
c {:animated? true :dense {:store "blk" :offset 0 :stride 1 :frames 3}}]
|
|
(is (= 7 (ch/value-at c -4 store)))
|
|
(is (= 9 (ch/value-at c 99 store)))))
|
|
|
|
(deftest a-dense-channel-whose-store-is-missing-says-so
|
|
(let [c {:animated? true :dense {:store "gone" :offset 0 :stride 1 :frames 1}}]
|
|
(is (thrown-with-msg? ExceptionInfo #"store key is not in the store"
|
|
(ch/value-at c 0 {})))))
|
|
|
|
;; ---- presence is not visibility ----
|
|
|
|
(deftest the-mask-carries-absence-and-vis-carries-hiding
|
|
;; An occluded subject has NO VALUE on a frame. A part being switched off is a
|
|
;; different question and it is `[:vis]`, a channel like any other. Two
|
|
;; mechanisms for one question is how you get a part hidden by one and shown by
|
|
;; the other, so the mask carries absence only.
|
|
(let [state (js/Uint8Array. #js [ch/present ch/absent-bit ch/present])
|
|
store {"blk" {:data (js/Int16Array. #js [1 2 3]) :state state}}
|
|
c {:animated? true :dense {:store "blk" :offset 0 :stride 1 :frames 3}}]
|
|
(is (= 1 (ch/value-at c 0 store)))
|
|
(is (= ch/absent (ch/value-at c 1 store)))
|
|
(is (= 3 (ch/value-at c 2 store)))
|
|
(is (ch/nothing? ch/absent))
|
|
(is (not (ch/nothing? 0)) "zero is a value, not an absence")
|
|
(is (not (ch/nothing? false)) "and so is false")))
|
|
|
|
(deftest the-mask-belongs-to-the-block-s-slice-and-not-to-frame-zero
|
|
;; A block is NODE-MAJOR, so one block holds several nodes' tracks and therefore
|
|
;; several mask regions. Indexing the mask by the frame alone reads the FIRST
|
|
;; track's absence for every track in the block — which is not a subtly wrong
|
|
;; pose, it is every part in the block vanishing on the frames where one of them
|
|
;; was occluded, and `demo/swarm` drew nothing at all for its first thirty-four
|
|
;; frames on account of it.
|
|
(let [nf 3
|
|
;; Track 0 absent on frame 1, track 1 absent on frame 2.
|
|
state (js/Uint8Array. #js [ch/present ch/absent-bit ch/present
|
|
ch/present ch/present ch/absent-bit])
|
|
store {"blk" {:data (js/Int16Array. #js [10 11, 20 21, 30 31,
|
|
40 41, 50 51, 60 61])
|
|
:state state}}
|
|
track (fn [i] {:animated? true
|
|
:dense {:store "blk" :offset (* i nf 2) :stride 2 :frames nf}})
|
|
read (fn [i f] (let [v (ch/value-at (track i) f store)]
|
|
(if (ch/nothing? v) :absent [(ch/component v 0) (ch/component v 1)])))]
|
|
(is (= [[10 11] :absent [30 31]] (mapv #(read 0 %) (range nf))))
|
|
(is (= [[40 41] [50 51] :absent] (mapv #(read 1 %) (range nf))))))
|
|
|
|
(deftest a-block-with-no-mask-is-present-throughout
|
|
;; The mask is optional: a generator that cannot fail to detect has nothing to
|
|
;; say, and allocating a zeroed byte per frame to say it would be noise.
|
|
(let [store {"blk" {:data (js/Int16Array. #js [1 2 3]) :state nil}}
|
|
c {:animated? true :dense {:store "blk" :offset 0 :stride 1 :frames 3}}]
|
|
(is (= [1 2 3] (mapv #(ch/value-at c % store) (range 3))))))
|
|
|
|
;; ---- the cursor is the playback path and must agree exactly ----
|
|
|
|
(defn- via-cursor
|
|
"Sample one cursor at each of `fs` in the order given, which is the point: a
|
|
cursor carries state between calls."
|
|
[c fs]
|
|
(let [cur (ch/cursor c nil)]
|
|
(mapv #(ch/sample! cur %) fs)))
|
|
|
|
(deftest the-cursor-agrees-with-the-specification-in-any-frame-order
|
|
;; This is the assertion the cursor exists for. A cursor that drifts produces
|
|
;; the WRONG POSE rather than an error, so nothing would report it: the mouth
|
|
;; would simply be a beat behind on some frames and not others, which reads as
|
|
;; a bad take.
|
|
(doseq [[label c] [["sparse" (ch/keyed {0 :a, 4 :b, 12 :c, 13 :d, 40 :e} :hold)]
|
|
["one key" (ch/keyed {7 :only} :hold)]
|
|
["dense-ish" (ch/keyed (into {} (map (juxt identity #(* 10 %))) (range 40)) :hold)]
|
|
["framed" (ch/framed :static)]
|
|
;; A stacked channel has a reading head per key map, and a
|
|
;; layer's head is asked for nothing at all across the long
|
|
;; stretches outside its support — then asked again. That is
|
|
;; the drift this test exists to catch, now squared.
|
|
["corrected"
|
|
(assoc (ch/keyed {0 0, 20 200, 44 440} :linear)
|
|
:over [(ch/layer :a [10 20] :offset
|
|
(ch/keyed {10 1, 19 90} :linear))])]
|
|
["corrected twice, and over a constant"
|
|
(assoc (ch/keyed {0 0, 30 300} :linear)
|
|
:over [(ch/layer :a [5 15] :offset (ch/framed 7))
|
|
(ch/layer :b [12 25] :replace
|
|
(ch/keyed {12 -1, 24 -12} :linear))
|
|
(ch/layer :c [20 44] :offset
|
|
(ch/keyed {20 0, 43 23} :linear))])]
|
|
["a corrected framed base"
|
|
(assoc (ch/framed 3)
|
|
:over [(ch/layer :a [8 36] :offset
|
|
(ch/keyed {8 0, 35 27} :linear))])]]]
|
|
(let [spec #(ch/value-at c % nil)
|
|
forward (range 0 45)
|
|
back (reverse forward)
|
|
jumpy [0 44 1 43 12 12 13 3 40 7 0 22 22 21 44]]
|
|
(testing label
|
|
(doseq [[order-name fs] [["forward" forward] ["backward" back] ["random access" jumpy]]]
|
|
(is (= (mapv spec fs) (via-cursor c fs))
|
|
(str label " / " order-name)))))))
|
|
|
|
(deftest a-fixed-point-block-decodes-through-its-own-scale
|
|
;; `:scale` is in the block HEADER and not agreed by convention, because a block
|
|
;; in image-height units and a block in stage pixels need different ones to fill
|
|
;; an Int16 usefully. It is what lets the block in memory be byte for byte the
|
|
;; block on the wire, which a handle naming a sha256 requires.
|
|
(let [store {"blk" {:data (js/Int16Array. #js [16384 -8192, 4096 32767]) :state nil}}
|
|
wide {:animated? true :dense {:store "blk" :offset 0 :stride 2 :frames 2
|
|
:scale 16384}}
|
|
thin {:animated? true :dense {:store "blk" :offset 0 :stride 1 :frames 4
|
|
:scale 16384}}]
|
|
(is (= [[1.0 -0.5] [0.25 (/ 32767 16384)]]
|
|
(mapv (fn [f] (let [v (ch/value-at wide f store)]
|
|
[(ch/component v 0) (ch/component v 1)]))
|
|
(range 2))))
|
|
(is (= [1.0 -0.5 0.25] (mapv #(ch/value-at thin % store) (range 3)))
|
|
"a stride-1 block decodes to a number and needs no buffer")
|
|
(is (seq (ch/problems (assoc-in wide [:dense :scale] 0)))
|
|
"a scale of zero divides every value in the block by zero")
|
|
(is (seq (ch/problems (assoc-in wide [:dense :scale] -16384))))))
|
|
|
|
(deftest the-cursor-decodes-into-a-buffer-it-owns-and-agrees-with-the-spec
|
|
;; Decoding costs the subarray view, so the reader owns one destination per
|
|
;; channel — the same bargain the resolver makes with its point buffers, and it
|
|
;; carries the same contract: a value has to be CONSUMED before the next frame
|
|
;; is asked for, because the next read overwrites it.
|
|
(let [store {"blk" {:data (js/Int16Array. #js [16384 0, 0 16384, -16384 0]) :state nil}}
|
|
c {:animated? true :dense {:store "blk" :offset 0 :stride 2 :frames 3
|
|
:scale 16384}}
|
|
cur (ch/cursor c store)
|
|
pair (fn [v] [(ch/component v 0) (ch/component v 1)])]
|
|
(is (= (mapv #(pair (ch/value-at c % store)) (range 3))
|
|
(mapv #(pair (ch/sample! cur %)) (range 3))))
|
|
(let [held (ch/sample! cur 0)]
|
|
(ch/sample! cur 2)
|
|
(is (= [-1.0 0.0] (pair held))
|
|
"the buffer is reused, which is the contract and not a bug"))))
|
|
|
|
(deftest the-cursor-reads-a-dense-block-too
|
|
(let [store {"blk" {:data (js/Float32Array. #js [1 2 3 4 5]) :state nil}}
|
|
c {:animated? true :dense {:store "blk" :offset 0 :stride 1 :frames 5}}
|
|
cur (ch/cursor c store)]
|
|
(is (= [1 2 3 4 5] (mapv #(ch/sample! cur %) (range 5))))
|
|
(is (= [5 1] (mapv #(ch/sample! cur %) [4 0])) "and seeks")))
|
|
|
|
;; ---- correction layers ----
|
|
|
|
(defn- corrected
|
|
"A base channel with layers over it."
|
|
[base & layers]
|
|
(assoc base :over (vec layers)))
|
|
|
|
(deftest a-correction-applies-only-over-its-support
|
|
;; THE property. A range says where an edit applies, so outside it the
|
|
;; underlying animation must evaluate exactly as it did before — which is what
|
|
;; makes a bounded correction different from inserting boundary keys, and the
|
|
;; reason the stack exists at all.
|
|
(let [base (ch/keyed {0 0, 10 100} :linear)
|
|
c (corrected base (ch/layer :nudge [3 6] :offset (ch/framed 5)))
|
|
plain (mapv #(ch/value-at base % nil) (range 11))
|
|
with (mapv #(ch/value-at c % nil) (range 11))]
|
|
(is (= (concat (take 3 plain) [35 45 55] (drop 6 plain)) with))
|
|
(is (= (assoc plain 3 35 4 45 5 55) with) "and nothing else moved at all")))
|
|
|
|
(deftest a-return-motion-is-a-keyed-layer-and-a-constant-is-a-framed-one
|
|
;; The three commands the lane model asks for over one selected range, and
|
|
;; none of them needs a new way to say what a value is over time.
|
|
(let [base (ch/keyed {0 0} :hold)
|
|
at (fn [c] (mapv #(ch/value-at c % nil) (range 6)))]
|
|
(is (= [0 0 10 10 10 0]
|
|
(at (corrected base (ch/layer :flat [2 5] :offset (ch/framed 10)))))
|
|
"a constant adjustment")
|
|
(is (= [0 0 0 5 10 0]
|
|
(at (corrected base (ch/layer :ramp [2 5] :offset
|
|
(ch/keyed {2 0, 4 10} :linear)))))
|
|
"a ramp")
|
|
(is (= [0 0 0 10 0 0]
|
|
(at (corrected base (ch/layer :return [2 5] :offset
|
|
(ch/keyed {2 0, 3 10, 4 0} :linear)))))
|
|
"and a return motion, which is three samples and no new mechanism")))
|
|
|
|
(deftest the-stack-is-ordered-and-replace-wins-where-it-covers
|
|
(let [base (ch/keyed {0 1} :hold)
|
|
two (fn [a b] (mapv #(ch/value-at (corrected base a b) % nil) (range 4)))
|
|
add2 (ch/layer :a [0 4] :offset (ch/framed 2))
|
|
put9 (ch/layer :b [1 3] :replace (ch/framed 9))]
|
|
(is (= [3 9 9 3] (two add2 put9)) "the later layer sees the earlier one's result")
|
|
(is (= [3 11 11 3] (two put9 add2)) "and order therefore matters")))
|
|
|
|
(deftest validation-follows-the-shape-produced-by-an-ordered-stack
|
|
(let [base (ch/framed [0 0])
|
|
put3 (ch/layer :put3 [0 2] :replace (ch/framed [1 2 3]))
|
|
add2 (ch/layer :add2 [0 2] :offset (ch/framed [1 1]))
|
|
add3 (ch/layer :add3 [0 2] :offset (ch/framed [1 1 1]))]
|
|
(is (seq (ch/problems (corrected base put3 add2)))
|
|
"validation rejects a stack that would throw while reading")
|
|
(is (empty? (ch/problems (corrected base put3 add3)))
|
|
"a covering replacement establishes the shape seen by later layers")
|
|
(is (= [2 3 4] (ch/value-at (corrected base put3 add3) 0 nil)))))
|
|
|
|
(deftest stack-compatibility-follows-adjacent-replacements-and-skips-conflicts
|
|
(let [base (ch/framed [0 0])
|
|
left (ch/layer :left [0 2] :replace (ch/framed [1 2 3]))
|
|
right (ch/layer :right [2 4] :replace (ch/framed [4 5 6]))
|
|
add3 (ch/layer :add [0 4] :offset (ch/framed [1 1 1]))
|
|
valid (corrected base left right add3)
|
|
broken (corrected base (assoc left :conflict "skip it") right add3)]
|
|
(is (empty? (ch/problems valid))
|
|
"adjacent replacements jointly prevent the base shape reaching the offset")
|
|
(is (seq (ch/problems broken))
|
|
"a conflicted replacement is absent from the effective stack")
|
|
(is (empty? (ch/conflicts valid)))
|
|
(is (= [:left :add] (mapv :id (ch/conflicts broken))))))
|
|
|
|
(deftest reconciliation-recomputes-the-complete-stack-in-order
|
|
(let [old (assoc (ch/framed [0 0]) :over
|
|
[(assoc (ch/layer :put [0 3] :replace (ch/framed [1 2 3]))
|
|
:conflict "old")
|
|
(ch/layer :add [0 3] :offset (ch/framed [1 1 1]))])
|
|
layers (ch/reconcile old)]
|
|
(is (nil? (:conflict (first layers))) "a stale mark is cleared")
|
|
(is (nil? (:conflict (second layers))) "the later offset sees that replacement")
|
|
(is (= [2 3 4] (ch/value-at (assoc old :over layers) 1 nil)))))
|
|
|
|
(deftest generated-base-time-and-authored-correction-time-can-differ
|
|
(let [c (corrected (ch/keyed {0 0, 2 20} :hold)
|
|
(ch/layer :nudge [1 2] :offset (ch/framed 3)))
|
|
cursor (ch/cursor c nil)]
|
|
(is (= 3 (ch/value-at c 0 1 nil)))
|
|
(is (= 3 (ch/sample! cursor 0 1)))
|
|
(is (= 23 (ch/value-at c 2 1 nil)))
|
|
(is (= 20 (ch/sample! cursor 2 0)))))
|
|
|
|
(deftest replace-can-supply-a-value-where-offset-has-nothing-to-add-to
|
|
;; An absent value is not zero. `replace` states a pose; `offset` cannot
|
|
;; offset a pose that was never measured, and must not invent one.
|
|
(let [gone (ch/keyed {} :hold)]
|
|
(is (ch/nothing? (ch/value-at gone 0 nil)))
|
|
(is (ch/nothing? (ch/value-at (corrected gone (ch/layer :o [0 2] :offset (ch/framed 5))) 0 nil)))
|
|
(is (= 5 (ch/value-at (corrected gone (ch/layer :r [0 2] :replace (ch/framed 5))) 0 nil)))
|
|
;; And outside the support it is still absent, not the layer's value.
|
|
(is (ch/nothing? (ch/value-at (corrected gone (ch/layer :r [0 2] :replace (ch/framed 5))) 7 nil)))))
|
|
|
|
(deftest a-correction-offsets-geometry-component-wise-over-a-dense-base
|
|
;; The base a correction matters most for is generated, and a dense value is a
|
|
;; VIEW onto the block: offsetting must not write into it.
|
|
(let [data (js/Int16Array. #js [10 20, 30 40])
|
|
store {"blk" {:data data :state nil}}
|
|
base {:animated? true :dense {:store "blk" :offset 0 :stride 2 :frames 2}}
|
|
c (corrected base (ch/layer :nudge [1 2] :offset (ch/framed [5 -5])))]
|
|
(is (= [[10 20] [35 35]]
|
|
(mapv (fn [f] (let [v (ch/value-at c f store)]
|
|
[(ch/component v 0) (ch/component v 1)]))
|
|
(range 2))))
|
|
(is (= [10 20 30 40] (vec data)) "the block itself is untouched")
|
|
(is (thrown-with-msg?
|
|
ExceptionInfo #"different shape"
|
|
(ch/value-at (corrected base (ch/layer :bad [0 2] :offset (ch/framed 1))) 0 store))
|
|
"a correction of the wrong shape is loud, not silently dropped")))
|
|
|
|
(deftest a-malformed-correction-is-reported-rather-than-read
|
|
(let [ok (ch/layer :a [0 2] :offset (ch/framed 1))
|
|
base (ch/keyed {0 1} :hold)]
|
|
(is (empty? (ch/problems (corrected base ok))))
|
|
(is (seq (ch/problems (assoc base :over (list ok)))) "an ordered stack is a vector")
|
|
(doseq [[label bad] [["no id" (dissoc ok :id)]
|
|
["backwards support" (assoc ok :support [5 2])]
|
|
["open-ended support" (assoc ok :support [0 ##Inf])]
|
|
["a support that is not a pair" (assoc ok :support 3)]
|
|
["an unknown op" (assoc ok :op :multiply)]
|
|
["no values" (dissoc ok :values)]
|
|
["values that are not a channel" (assoc ok :values {:keys {0 1}})]
|
|
["layers under a layer"
|
|
(assoc ok :values (corrected base ok))]]]
|
|
(is (seq (ch/problems (corrected base bad))) label))))
|
|
|
|
(deftest an-empty-over-is-fine-and-is-what-scenes-carry
|
|
(is (empty? (ch/problems (ch/keyed {0 1} :hold))))
|
|
(is (= 1 (ch/value-at (ch/keyed {0 1} :hold) 0 nil))))
|
|
|
|
;; ---- shape validation ----
|
|
|
|
(deftest problems-names-the-ways-a-channel-is-malformed
|
|
(is (empty? (ch/problems (ch/framed 1))))
|
|
(is (empty? (ch/problems (ch/keyed {0 1} :hold))))
|
|
(testing "keys as a vector is the mistake most worth catching"
|
|
(is (seq (ch/problems {:animated? true :keys [[0 1]]}))))
|
|
(is (seq (ch/problems {:value 1})) "no :animated?")
|
|
(is (seq (ch/problems {:animated? true})) "animated with nothing to read")
|
|
(is (seq (ch/problems {:animated? true :keys {0 1}
|
|
:dense {:store "x" :offset 0 :stride 1 :frames 1}}))
|
|
"one shape at a time")
|
|
(is (empty? (ch/problems (ch/keyed {0 0.0, 10 1.0} :linear)))
|
|
"scalar controls can interpolate")
|
|
(is (seq (ch/problems (ch/keyed {0 :a, 10 :b} :linear)))
|
|
"a cut or tone cannot interpolate"))
|
|
|
|
(deftest numeric-channels-can-ramp-between-keys
|
|
(let [c (ch/keyed {0 0.0, 10 1.0} :linear)
|
|
cursor (ch/cursor c nil)]
|
|
(is (= [0.0 0.5 1.0 1.0]
|
|
(mapv #(ch/value-at c % nil) [0 5 10 15])))
|
|
(is (= [0.0 0.5 1.0 0.2]
|
|
(mapv #(ch/sample! cursor %) [0 5 10 2])))))
|
|
|
|
(deftest component-reads-vectors-and-typed-arrays-the-same-way
|
|
(is (= 3 (ch/component [3 4] 0)))
|
|
(is (= 4 (ch/component [3 4] 1)))
|
|
(is (= 3 (ch/component (js/Int16Array. #js [3 4]) 0)))
|
|
(is (= 4 (ch/component (js/Int16Array. #js [3 4]) 1))))
|
|
|
|
(deftest generated-is-provenance-and-nothing-reads-it
|
|
;; The flag lives on the CHANNEL, not the node, because a mouth wants a
|
|
;; rotoscoped [:geom :pts] and a hand-animated [:xform :pos] at the same time.
|
|
;; What this asserts is that sampling does not depend on it: strip :generated
|
|
;; and every frame is identical.
|
|
(let [store {"blk" {:data (js/Int16Array. #js [1 2 3]) :state nil}}
|
|
base {:animated? true :dense {:store "blk" :offset 0 :stride 1 :frames 3}}
|
|
with (assoc base :generated {:by :roto/lips-outer :params {:verts 8}})]
|
|
(is (= (mapv #(ch/value-at base % store) (range 3))
|
|
(mapv #(ch/value-at with % store) (range 3))))))
|
|
|
|
(deftest a-correction-the-base-outgrew-is-a-conflict-and-not-a-problem
|
|
;; What a topology change does: a re-freeze gives the mouth a different number
|
|
;; of points, and a correction that was right when it was made can no longer
|
|
;; be added component by component. That is not a broken document — it is a
|
|
;; decision waiting for a person.
|
|
(let [pts (fn [n] {:animated? true :interp :hold :keys {0 (vec (repeat n 1))}})
|
|
nudge (ch/layer :nudge [0 4] :offset (ch/framed [1 1 1 1]))
|
|
fits (assoc (pts 4) :over [nudge])
|
|
outgrown (assoc (pts 6) :over [nudge])]
|
|
(is (nil? (ch/conflict-with (pts 4) nudge)))
|
|
(is (empty? (ch/conflicts fits)))
|
|
(is (empty? (ch/problems fits)))
|
|
(is (re-find #"different shape" (ch/conflict-with (pts 6) nudge)))
|
|
;; Unrecorded, it is an authoring bug and says so.
|
|
(is (seq (ch/problems outgrown)))
|
|
(is (= [:nudge] (mapv :id (ch/conflicts outgrown))))
|
|
;; Recorded, the document is sound and the correction is simply not applied.
|
|
(let [marked (assoc (pts 6) :over [(assoc nudge :conflict "outgrown")])]
|
|
(is (empty? (ch/problems marked)) "a recorded conflict is not a reason not to load")
|
|
(is (= [:nudge] (mapv :id (ch/conflicts marked))))
|
|
(is (= [1 1 1 1 1 1] (vec (ch/value-at marked 0 nil)))
|
|
"the base alone — neither misapplied nor silently dropped")
|
|
(is (= (ch/value-at marked 0 nil) (first (via-cursor marked [0])))
|
|
"and the cursor skips it too"))))
|
|
|
|
(deftest replace-never-conflicts-and-an-opaque-value-cannot-be-offset
|
|
(let [flag (ch/keyed {0 true} :hold)]
|
|
(is (nil? (ch/conflict-with flag (ch/layer :r [0 2] :replace (ch/framed false))))
|
|
"replace states a whole value, so it has nothing to agree with")
|
|
(is (re-find #"not a number" (ch/conflict-with flag (ch/layer :o [0 2] :offset (ch/framed 1)))))
|
|
(is (nil? (ch/conflict-with (ch/keyed {} :hold) (ch/layer :o [0 2] :offset (ch/framed 1))))
|
|
"an empty key map is not a disagreement")))
|
|
|
|
(deftest the-shape-of-a-channels-values-is-readable-without-sampling-it
|
|
(is (= :scalar (ch/value-shape (ch/framed 3))))
|
|
(is (= 2 (ch/value-shape (ch/framed [1 2]))))
|
|
(is (= :scalar (ch/value-shape {:animated? true :dense {:stride 1}})))
|
|
(is (= 40 (ch/value-shape {:animated? true :dense {:stride 40}})))
|
|
(is (= 2 (ch/value-shape (ch/keyed {0 [1 2], 4 [3 4]} :linear))))
|
|
(is (= :opaque (ch/value-shape (ch/keyed {0 :a} :hold))))
|
|
(is (nil? (ch/value-shape (ch/keyed {} :hold))) "nothing to read it off"))
|