Split clips from timelines
This commit is contained in:
parent
27bfe18bee
commit
9778b9023b
31 changed files with 794 additions and 444 deletions
|
|
@ -10,9 +10,10 @@
|
|||
here would fail on a loaded CI box and teach everyone to ignore it."
|
||||
(:require [cljs.test :refer [deftest is]]
|
||||
[arthur.demo.swarm :as swarm]
|
||||
[arthur.domain.clip :as clip]
|
||||
[arthur.domain.palette :as pal]
|
||||
[arthur.domain.raster :as raster]
|
||||
[arthur.domain.scene :as scene]))
|
||||
[arthur.domain.timeline :as timeline]))
|
||||
|
||||
(defn- ms [label n f]
|
||||
(let [t0 (js/Date.now)]
|
||||
|
|
@ -23,11 +24,11 @@
|
|||
(/ dt n))))
|
||||
|
||||
(deftest bench
|
||||
(let [res (scene/resolver @swarm/scene @swarm/store pal/index-of)
|
||||
(let [res (timeline/resolver (clip/root @swarm/clip) @swarm/store pal/index-of)
|
||||
ras (raster/make 320 200)
|
||||
dest (js/Uint8ClampedArray. (* 320 200 4))
|
||||
n 120]
|
||||
(println "\nswarm:" (count (:nodes @swarm/scene)) "nodes")
|
||||
(println "\nswarm:" (count (clip/nodes @swarm/clip)) "nodes")
|
||||
(let [a (ms "resolve " n (fn [i] (res (mod i 229))))
|
||||
b (ms "resolve+draw " n (fn [i]
|
||||
(raster/clear! ras 0)
|
||||
|
|
|
|||
|
|
@ -4,74 +4,97 @@
|
|||
subtly smaller, and the loss is discovered later, by somebody whose work is
|
||||
already gone.
|
||||
|
||||
So the assertion is exact equality on the real scenes — the frozen take in both
|
||||
So the assertion is exact equality on the real clips — the frozen take in both
|
||||
head modes, the hand-written demo, the swarm — rather than on a fixture, and
|
||||
`scene-keys` makes a field added without a leaf fail loudly instead."
|
||||
`clip/clip-keys` plus `timeline/timeline-keys` make a field added without a leaf
|
||||
fail loudly instead."
|
||||
(:require [cljs.test :refer [deftest is testing]]
|
||||
[arthur.demo :as demo]
|
||||
[arthur.demo.swarm :as swarm]
|
||||
[arthur.demo.take :as take]
|
||||
[arthur.domain.channel :as ch]
|
||||
[arthur.domain.clip :as clip]
|
||||
[arthur.domain.leaf :as leaf]))
|
||||
|
||||
(deftest every-real-scene-survives-the-split-exactly
|
||||
(doseq [[label scene] [["the frozen take" @take/scene]
|
||||
["the locked take" @take/locked]
|
||||
["the hand-written demo" demo/scene]
|
||||
["the swarm" @swarm/scene]]]
|
||||
(defn- one-timeline
|
||||
"A minimal clip holding one timeline of these nodes, for the cases that are
|
||||
about a path rather than about a take."
|
||||
[nodes]
|
||||
{:timelines {:main {:id :main :frames 1 :nodes nodes}}})
|
||||
|
||||
(deftest every-real-clip-survives-the-split-exactly
|
||||
(doseq [[label c] [["the frozen take" @take/clip]
|
||||
["the locked take" @take/locked]
|
||||
["the hand-written demo" demo/clip]
|
||||
["the swarm" @swarm/clip]]]
|
||||
(testing label
|
||||
(is (= scene (leaf/scene :c1 (leaf/leaves :c1 scene)))))))
|
||||
(is (= c (leaf/clip :c1 (leaf/leaves :c1 c)))))))
|
||||
|
||||
(deftest the-leaves-are-the-paths-the-sync-design-names
|
||||
(let [ls (leaf/leaves :c7 @take/scene)]
|
||||
(let [ls (leaf/leaves :c7 @take/clip)]
|
||||
(is (contains? ls "clip/c7/timing"))
|
||||
(is (contains? ls "clip/c7/stage"))
|
||||
(is (contains? ls "clip/c7/source"))
|
||||
(is (contains? ls "clip/c7/node/mouth"))
|
||||
(is (contains? ls "clip/c7/channel/mouth/geom.pts"))
|
||||
(is (contains? ls "clip/c7/channel/mouth-in/vis"))
|
||||
;; A TIMELINE ID IS A SEGMENT, which is what lets a symbol's nodes be
|
||||
;; addressed by the same path shape as the clip's own. `main` is the root.
|
||||
(is (contains? ls "clip/c7/timeline/main"))
|
||||
(is (= {:frames 229} (get ls "clip/c7/timeline/main"))
|
||||
"a timeline's leaf is its frame space; :fps is the clip's")
|
||||
(is (contains? ls "clip/c7/timeline/main/node/mouth"))
|
||||
(is (contains? ls "clip/c7/timeline/main/channel/mouth/geom.pts"))
|
||||
(is (contains? ls "clip/c7/timeline/main/channel/mouth-in/vis"))
|
||||
(is (contains? ls "clip/c7/feature/eye-r"))
|
||||
(is (contains? ls "clip/c7/group/eyes-1"))
|
||||
(is (contains? ls "clip/c7/subject/face-1"))
|
||||
;; `:head`'s measured channels are written together by a freeze and replaced
|
||||
;; together by a re-freeze, so they are one leaf and not three.
|
||||
(is (contains? ls "clip/c7/measured/head"))
|
||||
(is (= 3 (count (get ls "clip/c7/measured/head"))))))
|
||||
(is (contains? ls "clip/c7/timeline/main/measured/head"))
|
||||
(is (= 3 (count (get ls "clip/c7/timeline/main/measured/head"))))
|
||||
;; :frames is NOT in `timing` any more. A timeline is a frame space and a clip
|
||||
;; is a rate, so the one leaf that held both was the persistence half of the
|
||||
;; conflation `domain/clip` exists to undo.
|
||||
(is (= {:fps 30} (get ls "clip/c7/timing")))))
|
||||
|
||||
(deftest a-node-and-its-channels-are-different-leaves
|
||||
;; The boundary that lets two people key different parts without meeting. A node
|
||||
;; leaf carries structure and no geometry.
|
||||
(let [ls (leaf/leaves :c1 @take/scene)
|
||||
n (get ls "clip/c1/node/mouth")]
|
||||
(let [ls (leaf/leaves :c1 @take/clip)
|
||||
n (get ls "clip/c1/timeline/main/node/mouth")]
|
||||
(is (= {:id :mouth :name "mouth" :kind :poly :parent :head :z "a1"} n))
|
||||
(is (nil? (:channels n)))
|
||||
(is (:animated? (get ls "clip/c1/channel/mouth/geom.pts")))))
|
||||
(is (:animated? (get ls "clip/c1/timeline/main/channel/mouth/geom.pts")))))
|
||||
|
||||
(deftest a-field-with-no-leaf-is-refused-rather-than-dropped
|
||||
;; The invariant that keeps the round trip exact as the model grows: a scene
|
||||
;; field nobody gave a leaf to would save silently and come back missing.
|
||||
;; The invariant that keeps the round trip exact as the model grows: a field
|
||||
;; nobody gave a leaf to would save silently and come back missing. Asserted at
|
||||
;; BOTH levels now, because there are two types that can grow one.
|
||||
(is (thrown-with-msg? ExceptionInfo #"no leaf to save it in"
|
||||
(leaf/leaves :c1 (assoc @take/scene :sequences []))))
|
||||
(is (= leaf/scene-keys (set (keys (assoc @take/scene :name "x"))))
|
||||
"scene-keys has drifted from what a frozen scene actually holds"))
|
||||
(leaf/leaves :c1 (assoc @take/clip :sequences []))))
|
||||
(is (thrown-with-msg? ExceptionInfo #"no leaf to save it in"
|
||||
(leaf/leaves :c1 (assoc-in @take/clip
|
||||
[:timelines :main :markers] []))))
|
||||
(is (= clip/clip-keys (set (keys (assoc @take/clip :name "x"))))
|
||||
"clip-keys has drifted from what a frozen clip actually holds"))
|
||||
|
||||
(deftest an-absent-field-stays-absent
|
||||
;; A scene with no fps must not come back with `:fps nil`. `=` is the test, and
|
||||
;; the demo scene is the case: it has no analysis record and its root has no
|
||||
;; A clip with no analysis must not come back with `:analysis nil`. `=` is the
|
||||
;; test, and the demo is the case: no analysis record, and its root node has no
|
||||
;; channels.
|
||||
(let [ls (leaf/leaves :c1 demo/scene)]
|
||||
(let [ls (leaf/leaves :c1 demo/clip)]
|
||||
(is (not (contains? ls "clip/c1/source")))
|
||||
(is (not (contains? (leaf/scene :c1 ls) :analysis)))
|
||||
(is (not (contains? (get-in (leaf/scene :c1 ls) [:nodes :root]) :channels)))))
|
||||
(is (not (contains? (leaf/clip :c1 ls) :analysis)))
|
||||
(is (not (contains? (get-in (leaf/clip :c1 ls)
|
||||
[:timelines :main :nodes :root])
|
||||
:channels)))))
|
||||
|
||||
(deftest a-namespaced-id-is-one-path-segment
|
||||
;; docs/architecture.md draws a node as `:eye-r/iris`, and a leaf path is
|
||||
;; "/"-delimited, so the two have to be reconciled somewhere.
|
||||
(let [scene {:nodes {:eye-r/iris {:id :eye-r/iris :kind :disc :parent nil :z "a1"
|
||||
:channels {[:geom :radius] (ch/framed 2)}}}}
|
||||
ls (leaf/leaves :c1 scene)]
|
||||
(is (contains? ls "clip/c1/node/eye-r~iris"))
|
||||
(is (= scene (leaf/scene :c1 ls))))
|
||||
(let [c (one-timeline {:eye-r/iris {:id :eye-r/iris :kind :disc :parent nil :z "a1"
|
||||
:channels {[:geom :radius] (ch/framed 2)}}})
|
||||
ls (leaf/leaves :c1 c)]
|
||||
(is (contains? ls "clip/c1/timeline/main/node/eye-r~iris"))
|
||||
(is (= c (leaf/clip :c1 ls))))
|
||||
;; `(keyword "a~b")` rather than a literal: ~ is unquote in CLJS source.
|
||||
(is (thrown-with-msg? ExceptionInfo #"cannot contain ~"
|
||||
(leaf/segment (keyword "a~b")))))
|
||||
|
|
@ -79,10 +102,10 @@
|
|||
(deftest another-clips-leaves-are-ignored-rather-than-merged
|
||||
;; A project's whole leaf map can be handed in for one clip, which is what makes
|
||||
;; a two-clip project one fetch.
|
||||
(let [a (leaf/leaves :a @take/scene)
|
||||
b (leaf/leaves :b demo/scene)]
|
||||
(is (= @take/scene (leaf/scene :a (merge a b))))
|
||||
(is (= demo/scene (leaf/scene :b (merge a b))))))
|
||||
(let [a (leaf/leaves :a @take/clip)
|
||||
b (leaf/leaves :b demo/clip)]
|
||||
(is (= @take/clip (leaf/clip :a (merge a b))))
|
||||
(is (= demo/clip (leaf/clip :b (merge a b))))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; what a document may not contain
|
||||
|
|
@ -91,20 +114,33 @@
|
|||
;; `demo/swarm` names its blocks "swarm/pos", which is exactly the descriptive
|
||||
;; key content addressing replaced: a handle that only means something on the
|
||||
;; machine that made it. It is a fine load test and not a document.
|
||||
(let [ps (leaf/problems (leaf/leaves :c1 @swarm/scene))]
|
||||
(let [ps (leaf/problems (leaf/leaves :c1 @swarm/clip))]
|
||||
(is (seq ps))
|
||||
(is (some #(re-find #"names tier 2 as \"swarm/pos\"" %) ps) (pr-str (first ps)))))
|
||||
|
||||
(deftest a-frozen-clip-has-no-problems
|
||||
(is (empty? (leaf/problems (leaf/leaves :c1 @take/scene))))
|
||||
(is (empty? (leaf/problems (leaf/leaves :c1 @take/clip))))
|
||||
(is (empty? (leaf/problems (leaf/leaves :c1 @take/locked)))))
|
||||
|
||||
(deftest a-channel-leaf-for-a-node-that-is-not-there-is-named
|
||||
(let [ls (dissoc (leaf/leaves :c1 @take/scene) "clip/c1/node/mouth")]
|
||||
(let [ls (dissoc (leaf/leaves :c1 @take/clip) "clip/c1/timeline/main/node/mouth")]
|
||||
(is (some #(re-find #"node with no node leaf" %) (leaf/problems ls)))))
|
||||
|
||||
(deftest a-node-leaf-is-scoped-to-its-own-timeline
|
||||
;; The reason the node index in `problems` is keyed by (clip, timeline, node)
|
||||
;; rather than by node alone: two timelines may each hold a `:mouth`, and a
|
||||
;; channel of one is not a channel of the other. Keyed by node alone, deleting
|
||||
;; the root's node leaf would have been excused by the symbol's.
|
||||
(let [ls (-> (leaf/leaves :c1 @take/clip)
|
||||
(assoc "clip/c1/timeline/sym~blink" {:frames 3}
|
||||
"clip/c1/timeline/sym~blink/node/mouth"
|
||||
{:id :mouth :kind :poly :parent nil :z "a1"})
|
||||
(dissoc "clip/c1/timeline/main/node/mouth"))]
|
||||
(is (some #(re-find #"node with no node leaf" %) (leaf/problems ls)))))
|
||||
|
||||
(deftest a-property-with-path-punctuation-in-it-is-refused
|
||||
(is (thrown-with-msg?
|
||||
ExceptionInfo #"cannot contain . or /"
|
||||
(leaf/leaves :c1 {:nodes {:a {:id :a :kind :poly :parent nil :z "a1"
|
||||
:channels {[:geom :pts.x] (ch/framed [0 0])}}}}))))
|
||||
(leaf/leaves :c1 (one-timeline
|
||||
{:a {:id :a :kind :poly :parent nil :z "a1"
|
||||
:channels {[:geom :pts.x] (ch/framed [0 0])}}})))))
|
||||
|
|
|
|||
|
|
@ -9,9 +9,9 @@
|
|||
back as a slightly wrong performance rather than as an error.
|
||||
|
||||
So what is compared is the OPS, frame for frame, through both evaluators, in
|
||||
every frame order — the same machinery scene-test uses to hold `eval-frame` and
|
||||
`resolver` to each other, which is the strictest statement available about two
|
||||
scenes being the same scene.
|
||||
every frame order — the same machinery timeline-test uses to hold `eval-frame`
|
||||
and `resolver` to each other, which is the strictest statement available about
|
||||
two documents being the same document.
|
||||
|
||||
This runs the conversion the network runs — `JSON.parse(JSON.stringify(...))` —
|
||||
and not the network. `clips/tests.py` puts the same document through Django, and
|
||||
|
|
@ -20,8 +20,9 @@
|
|||
(:require [cljs.test :refer [deftest is testing]]
|
||||
[arthur.demo.take :as take]
|
||||
[arthur.domain.channel :as ch]
|
||||
[arthur.domain.clip :as clip]
|
||||
[arthur.domain.project :as project]
|
||||
[arthur.domain.scene :as scene]
|
||||
[arthur.domain.timeline :as timeline]
|
||||
[arthur.flow.freeze :as freeze]
|
||||
[arthur.support.ops :as ops]))
|
||||
|
||||
|
|
@ -33,28 +34,31 @@
|
|||
(def ^:private before (delay @take/frozen))
|
||||
(def ^:private after (delay (wired :c1 @before)))
|
||||
|
||||
(deftest what-comes-back-is-a-valid-scene
|
||||
(let [ps (scene/problems (:scene @after))]
|
||||
(deftest what-comes-back-is-a-valid-clip
|
||||
;; `clip/problems` and not `timeline/problems`: the round trip has to preserve
|
||||
;; the tracking identities and the timeline map as well as the nodes, and only
|
||||
;; the clip-level check looks at those.
|
||||
(let [ps (clip/problems (:clip @after))]
|
||||
(is (empty? ps) (pr-str ps))))
|
||||
|
||||
(deftest the-document-comes-back-equal
|
||||
;; Stronger than it needs to be and worth having: not merely equivalent, EQUAL.
|
||||
;; Any drift here is a field the codec is rewriting, and a field that is
|
||||
;; rewritten once is rewritten again on every save.
|
||||
(is (= (:scene @before) (:scene @after))))
|
||||
(is (= (:clip @before) (:clip @after))))
|
||||
|
||||
(deftest every-frame-resolves-to-the-same-ops-before-and-after
|
||||
;; The assertion. Both evaluators, both scenes, every frame order — so a block
|
||||
;; that came back with its offsets shifted, or a cursor that seeks differently
|
||||
;; over a rebuilt key map, has nowhere to hide.
|
||||
(let [n (:frames (:scene @before))
|
||||
(let [n (clip/frames (:clip @before))
|
||||
paths {"specification" [ops/specified ops/specified]
|
||||
"playback" [ops/resolved ops/resolved]
|
||||
"spec vs playback, after" [ops/specified ops/resolved]}]
|
||||
(doseq [[label [f g]] paths
|
||||
[order fs] (ops/orders n)]
|
||||
(let [a (f (:scene @before) (:store @before))
|
||||
b (g (:scene @after) (:store @after))]
|
||||
(let [a (f (clip/root (:clip @before)) (:store @before))
|
||||
b (g (clip/root (:clip @after)) (:store @after))]
|
||||
(testing (str label ", " order)
|
||||
(doseq [frame fs]
|
||||
(is (= (a frame) (b frame))
|
||||
|
|
@ -76,11 +80,11 @@
|
|||
;; The other head mode, because it is the one whose `:head` channels are FRAMED
|
||||
;; rather than dense: a codec that only handled dense channels would pass
|
||||
;; everything above and lose the locked take's identity transform.
|
||||
(let [locked {:scene @take/locked :store @take/store}
|
||||
(let [locked {:clip @take/locked :store @take/store}
|
||||
back (wired :c1 locked)
|
||||
a (ops/resolved (:scene locked) (:store locked))
|
||||
b (ops/resolved (:scene back) (:store back))]
|
||||
(is (= (:scene locked) (:scene back)))
|
||||
a (ops/resolved (clip/root (:clip locked)) (:store locked))
|
||||
b (ops/resolved (clip/root (:clip back)) (:store back))]
|
||||
(is (= (:clip locked) (:clip back)))
|
||||
(doseq [frame (range 0 take/frames 7)]
|
||||
(is (= (a frame) (b frame)) (str "frame " frame)))))
|
||||
|
||||
|
|
@ -109,9 +113,9 @@
|
|||
|
||||
(deftest an-absence-mask-survives-the-wire
|
||||
(let [back (wired :c1 @gappy)
|
||||
at (fn [clip id path f]
|
||||
(ch/value-at (get-in (:scene clip) [:nodes id :channels path])
|
||||
f (:store clip)))
|
||||
at (fn [entry id path f]
|
||||
(ch/value-at (get-in (clip/nodes (:clip entry)) [id :channels path])
|
||||
f (:store entry)))
|
||||
;; Every dense track of the eye, iris, brow and brow-position blocks, and
|
||||
;; the feature whose gap it must follow — the same table
|
||||
;; `each-dense-track-follows-its-own-features-presence` pins.
|
||||
|
|
@ -140,7 +144,8 @@
|
|||
;; Presence is not visibility, on the far side too: the node is dropped from the
|
||||
;; frame rather than hidden, and its partner is not.
|
||||
(let [back (wired :c1 @gappy)
|
||||
drawn (into #{} (map :node) ((scene/resolver (:scene back) (:store back)) 12))]
|
||||
drawn (into #{} (map :node)
|
||||
((timeline/resolver (clip/root (:clip back)) (:store back)) 12))]
|
||||
(is (not (contains? drawn :eye-r)))
|
||||
(is (contains? drawn :eye-l))
|
||||
(is (contains? drawn :mouth))))
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
(ns arthur.domain.scene-test
|
||||
(ns arthur.domain.timeline-test
|
||||
"Frame evaluation, and the hand-written scene.
|
||||
|
||||
port-plan step 2 exists to find out whether the data model works BEFORE nine
|
||||
|
|
@ -13,7 +13,8 @@
|
|||
[arthur.domain.node :as node]
|
||||
[arthur.domain.palette :as pal]
|
||||
[arthur.domain.raster :as raster]
|
||||
[arthur.domain.scene :as scene]
|
||||
[arthur.domain.clip :as clip]
|
||||
[arthur.domain.timeline :as timeline]
|
||||
[arthur.support.ops :as ops]))
|
||||
|
||||
(defn- poly [id parent z pts color & [extra]]
|
||||
|
|
@ -26,7 +27,7 @@
|
|||
{:nodes (into {} (map (juxt :id identity)) nodes)})
|
||||
|
||||
(defn- ids-at [scene f]
|
||||
(mapv :node (scene/eval-frame scene f)))
|
||||
(mapv :node (timeline/eval-frame scene f)))
|
||||
|
||||
(def ^:private pts-of ops/points)
|
||||
|
||||
|
|
@ -37,9 +38,9 @@
|
|||
{:id :b :kind :group :parent :a :z "a1"}
|
||||
{:id :c :kind :group :parent :b :z "a1"}
|
||||
{:id :d :kind :group :parent :a :z "a2"})
|
||||
ord (scene/order (:nodes s))]
|
||||
(is (= 0 (scene/depth (:nodes s) :a)))
|
||||
(is (= 2 (scene/depth (:nodes s) :c)))
|
||||
ord (timeline/order (:nodes s))]
|
||||
(is (= 0 (timeline/depth (:nodes s) :a)))
|
||||
(is (= 2 (timeline/depth (:nodes s) :c)))
|
||||
(let [pos (into {} (map-indexed (fn [i id] [id i])) ord)]
|
||||
(doseq [[id p] [[:b :a] [:c :b] [:d :a]]]
|
||||
(is (< (get pos p) (get pos id)) (str p " must come before " id))))))
|
||||
|
|
@ -49,12 +50,12 @@
|
|||
;; diagnostic than a stack trace naming the nodes.
|
||||
(let [s (sc {:id :a :kind :group :parent :b :z "a1"}
|
||||
{:id :b :kind :group :parent :a :z "a1"})]
|
||||
(is (thrown-with-msg? ExceptionInfo #"cycle" (scene/order (:nodes s))))
|
||||
(is (seq (scene/problems s)))))
|
||||
(is (thrown-with-msg? ExceptionInfo #"cycle" (timeline/order (:nodes s))))
|
||||
(is (seq (timeline/problems s)))))
|
||||
|
||||
(deftest a-missing-parent-is-named-rather-than-silently-orphaning
|
||||
(let [s (sc {:id :a :kind :group :parent :nope :z "a1"})]
|
||||
(is (seq (scene/problems s)))))
|
||||
(is (seq (timeline/problems s)))))
|
||||
|
||||
(deftest reparenting-is-one-field-and-does-not-move-a-subtree
|
||||
;; The flat-with-pointers claim, asserted as the thing it buys: a reparent is an
|
||||
|
|
@ -69,9 +70,9 @@
|
|||
(is (identical? (get-in s [:nodes :b]) (get-in s' [:nodes :b]))
|
||||
"and so is the new one")
|
||||
(is (= [[0 0] [10 0] [10 10]]
|
||||
(pts-of (first (filter #(= :c (:node %)) (scene/eval-frame s 0))))))
|
||||
(pts-of (first (filter #(= :c (:node %)) (timeline/eval-frame s 0))))))
|
||||
(is (= [[100 0] [110 0] [110 10]]
|
||||
(pts-of (first (filter #(= :c (:node %)) (scene/eval-frame s' 0))))))))
|
||||
(pts-of (first (filter #(= :c (:node %)) (timeline/eval-frame s' 0))))))))
|
||||
|
||||
;; ---- draw order ----
|
||||
|
||||
|
|
@ -113,7 +114,7 @@
|
|||
:channels {[:xform :pos] (ch/framed [100 50])
|
||||
[:xform :scale] (ch/framed [2 2])}}
|
||||
(poly :p :g "a1" [0 0 10 0 10 10 0 10] :skin-base))
|
||||
op (first (scene/eval-frame s 0))]
|
||||
op (first (timeline/eval-frame s 0))]
|
||||
(is (= [[100 50] [120 50] [120 70] [100 70]] (pts-of op)))))
|
||||
|
||||
(deftest a-keyed-group-position-moves-its-children-and-holds-between-keys
|
||||
|
|
@ -123,7 +124,7 @@
|
|||
:channels {[:xform :pos]
|
||||
(ch/keyed {0 [0 0], 4 [10 0], 8 [10 10], 12 [0 10]})}}
|
||||
(poly :p :g "a1" [0 0 2 0 2 2] :skin-base))
|
||||
at #(first (pts-of (first (scene/eval-frame s %))))]
|
||||
at #(first (pts-of (first (timeline/eval-frame s %))))]
|
||||
(is (= [0 0] (at 0)))
|
||||
(is (= [0 0] (at 3)) "held")
|
||||
(is (= [10 0] (at 4)))
|
||||
|
|
@ -140,7 +141,7 @@
|
|||
{:id :g :kind :group :parent :root :z "a1"
|
||||
:channels {[:xform :pos] (ch/keyed (into {} (map (juxt identity #(vector % 0))) (range 12)))}}
|
||||
(poly :p :g "a1" [0 0 1 0 1 1] :skin-base))
|
||||
x-at #(first (first (pts-of (first (scene/eval-frame s %)))))]
|
||||
x-at #(first (first (pts-of (first (timeline/eval-frame s %)))))]
|
||||
(is (= [0 0 0 3 3 3 6 6 6 9 9 9] (mapv x-at (range 12)))))
|
||||
|
||||
(testing "and a node may set its own grid, which the model permits deliberately"
|
||||
|
|
@ -148,7 +149,7 @@
|
|||
{:id :g :kind :group :parent :root :z "a1" :time {:mode :map :expose 4}
|
||||
:channels {[:xform :pos] (ch/keyed (into {} (map (juxt identity #(vector % 0))) (range 12)))}}
|
||||
(poly :p :g "a1" [0 0 1 0 1 1] :skin-base))
|
||||
x-at #(first (first (pts-of (first (scene/eval-frame s %)))))]
|
||||
x-at #(first (first (pts-of (first (timeline/eval-frame s %)))))]
|
||||
(is (= [0 0 0 0 4 4 4 4 8 8 8 8] (mapv x-at (range 12)))))))
|
||||
|
||||
(deftest offset-is-per-node-which-is-the-entire-point-of-mouth-lead
|
||||
|
|
@ -162,7 +163,7 @@
|
|||
{:id :mouth :kind :group :parent :root :z "a2" :time {:mode :map :offset 2}
|
||||
:channels {[:xform :pos] (ch/keyed keys)}}
|
||||
(poly :mouth-p :mouth "a1" [0 0 1 0 1 1] :mouth-dark))
|
||||
x-of (fn [f id] (->> (scene/eval-frame s f)
|
||||
x-of (fn [f id] (->> (timeline/eval-frame s f)
|
||||
(filter #(= id (:node %))) first pts-of first first))]
|
||||
(is (= [0 1 2 3] (mapv #(x-of % :plate-p) (range 4))))
|
||||
(is (= [2 3 4 5] (mapv #(x-of % :mouth-p) (range 4))) "the mouth reads ahead")))
|
||||
|
|
@ -205,11 +206,11 @@
|
|||
:dense {:store "pts" :offset 0 :stride 6 :frames 2}}
|
||||
[:style :color] (ch/framed :mouth-dark)}}
|
||||
(poly :teeth :m "a2" [0 0 1 0 1 1] :teeth))]
|
||||
(is (= [:child] (mapv :node (scene/eval-frame absent-pos 0 store))))
|
||||
(is (= [] (mapv :node (scene/eval-frame absent-pos 1 store)))
|
||||
(is (= [:child] (mapv :node (timeline/eval-frame absent-pos 0 store))))
|
||||
(is (= [] (mapv :node (timeline/eval-frame absent-pos 1 store)))
|
||||
"an absent transform gives the children nowhere to be")
|
||||
(is (= [:m :teeth] (mapv :node (scene/eval-frame absent-pts 0 store))))
|
||||
(is (= [:teeth] (mapv :node (scene/eval-frame absent-pts 1 store)))
|
||||
(is (= [:m :teeth] (mapv :node (timeline/eval-frame absent-pts 0 store))))
|
||||
(is (= [:teeth] (mapv :node (timeline/eval-frame absent-pts 1 store)))
|
||||
"an absent outline removes only itself")))
|
||||
|
||||
;; ---- stencils ----
|
||||
|
|
@ -223,7 +224,7 @@
|
|||
{:id :iris :kind :disc :parent :root :stencil :sclera :z "a2"
|
||||
:channels {[:geom :radius] (ch/framed 4)
|
||||
[:style :color] (ch/framed :iris)}})
|
||||
ops (scene/eval-frame s 0)]
|
||||
ops (timeline/eval-frame s 0)]
|
||||
(is (= [:sclera :iris] (mapv :node ops)))
|
||||
(is (= (:eye-white pal/index-of) (:stencil (second ops))))))
|
||||
|
||||
|
|
@ -250,7 +251,7 @@
|
|||
:channels {[:geom :radius] (ch/framed 3) [:style :color] (ch/framed :iris)}}
|
||||
{:id :r :kind :rect :parent :g :z "a2"
|
||||
:channels {[:geom :size] (ch/framed 1.7) [:style :color] (ch/framed :pupil)}})
|
||||
[d r] (scene/eval-frame s 0)]
|
||||
[d r] (timeline/eval-frame s 0)]
|
||||
(is (= [50 60 6] [(:cx d) (:cy d) (:r d)]))
|
||||
;; 1.7 x 2 is 3.4, and a block 3.4px wide would be 3px on one frame and 4 on
|
||||
;; the next, which reads as the pupil breathing.
|
||||
|
|
@ -266,7 +267,7 @@
|
|||
;; The frame orders and the snapshot live in `arthur.support.ops`, because the
|
||||
;; same comparison is what proves a scene survived the server — see
|
||||
;; flow/project-test.
|
||||
(let [s demo/scene
|
||||
(let [s demo/timeline
|
||||
spec (ops/specified s nil)
|
||||
fast (ops/resolved s nil)]
|
||||
(doseq [[label fs] (ops/orders (:frames s))]
|
||||
|
|
@ -277,28 +278,38 @@
|
|||
(deftest the-resolver-reuses-one-buffer-per-node
|
||||
;; At 30fps per-frame allocation is the only thing that will make this stutter,
|
||||
;; and fixed topology is what makes the buffer size knowable at all.
|
||||
(let [res (scene/resolver demo/scene)
|
||||
(let [res (timeline/resolver demo/timeline)
|
||||
buf-of (fn [f id] (->> (res f) (filter #(= id (:node %))) first :pts))]
|
||||
(is (identical? (buf-of 0 :card) (buf-of 30 :card)))))
|
||||
|
||||
;; ---- the hand-written scene, end to end ----
|
||||
|
||||
(deftest the-hand-written-scene-is-valid
|
||||
(let [ps (scene/problems demo/scene)]
|
||||
(deftest the-hand-written-clip-is-valid
|
||||
;; `clip/problems` rather than `timeline/problems`: it checks the clip's fields,
|
||||
;; the timeline map and the tracking identities as well as the nodes, so it is
|
||||
;; the check a save would make.
|
||||
(let [ps (clip/problems demo/clip)]
|
||||
(is (empty? ps) (pr-str ps)))
|
||||
(is (pos? (:frames demo/scene))))
|
||||
(is (pos? demo/frames))
|
||||
(testing "a clip is not a timeline, and handing one over fails loudly"
|
||||
;; The mistake this split makes easy: both are maps with an :id, and the wrong
|
||||
;; one resolves to no ops rather than to an error.
|
||||
(is (thrown-with-msg? ExceptionInfo #"not a timeline"
|
||||
(timeline/resolver demo/clip)))
|
||||
(is (thrown-with-msg? ExceptionInfo #"not a timeline"
|
||||
(timeline/eval-frame demo/clip 0)))))
|
||||
|
||||
(deftest the-hand-written-scene-renders-and-moves
|
||||
(deftest the-hand-written-clip-renders-and-moves
|
||||
;; port-plan step 2's done condition, as an assertion rather than a look: the
|
||||
;; scene rasterises, it writes only palette indices, and the pixels are not the
|
||||
;; same on every frame.
|
||||
(let [res (scene/resolver demo/scene)
|
||||
(let [res (timeline/resolver demo/timeline)
|
||||
render (fn [f]
|
||||
(let [r (raster/make (:width demo/scene) (:height demo/scene))]
|
||||
(let [r (raster/make (:width demo/clip) (:height demo/clip))]
|
||||
(raster/clear! r (:bg pal/index-of))
|
||||
(raster/draw-ops! r (res f))
|
||||
r))
|
||||
frames (mapv render (range 0 (:frames demo/scene) 6))
|
||||
frames (mapv render (range 0 demo/frames 6))
|
||||
sig (fn [r] (vec (array-seq (:buf r))))]
|
||||
(is (every? (fn [r] (every? #(< % (count pal/rgb)) (array-seq (:buf r)))) frames)
|
||||
"every byte written is a real palette index")
|
||||
|
|
@ -306,17 +317,17 @@
|
|||
(testing "the mark actually covers pixels"
|
||||
(is (pos? (count (remove zero? (sig (first frames)))))))))
|
||||
|
||||
(deftest the-hand-written-scene-steps-on-the-exposure-grid
|
||||
(deftest the-hand-written-clip-steps-on-the-exposure-grid
|
||||
;; Exposure 2 on the clip root, inherited, so odd frames are identical to the
|
||||
;; even frame before them. If this fails, exposure is being applied somewhere
|
||||
;; other than the frame the channels are sampled at.
|
||||
(let [res (scene/resolver demo/scene)
|
||||
(let [res (timeline/resolver demo/timeline)
|
||||
render (fn [f]
|
||||
(let [r (raster/make (:width demo/scene) (:height demo/scene))]
|
||||
(let [r (raster/make (:width demo/clip) (:height demo/clip))]
|
||||
(raster/clear! r (:bg pal/index-of))
|
||||
(raster/draw-ops! r (res f))
|
||||
(vec (array-seq (:buf r)))))]
|
||||
(doseq [f (range 0 (:frames demo/scene) 2)]
|
||||
(doseq [f (range 0 demo/frames 2)]
|
||||
(is (= (render f) (render (inc f))) (str "frame " (inc f) " must hold frame " f)))
|
||||
;; Two grid slots that straddle a key, not two adjacent ones: between keys
|
||||
;; nothing changes, because that is what hold MEANS. The scene's second key
|
||||
|
|
@ -324,13 +335,13 @@
|
|||
;; expose-before-anything-else rule showing up in pixels.
|
||||
(is (not= (render 56) (render 58)) "and a key on the grid is seen")))
|
||||
|
||||
(deftest the-hand-written-scene-keeps-the-iris-and-pupil-inside-the-card
|
||||
(deftest the-hand-written-clip-keeps-the-iris-and-pupil-inside-the-card
|
||||
;; The stencil chain, on real pixels: the iris is clipped by the card and the
|
||||
;; pupil by the iris, and neither is expressed anywhere as a chain.
|
||||
(let [res (scene/resolver demo/scene)]
|
||||
(doseq [f (range 0 (:frames demo/scene) 4)]
|
||||
(let [before (raster/make (:width demo/scene) (:height demo/scene))
|
||||
after (raster/make (:width demo/scene) (:height demo/scene))
|
||||
(let [res (timeline/resolver demo/timeline)]
|
||||
(doseq [f (range 0 demo/frames 4)]
|
||||
(let [before (raster/make (:width demo/clip) (:height demo/clip))
|
||||
after (raster/make (:width demo/clip) (:height demo/clip))
|
||||
ops (res f)
|
||||
card? (fn [op] (= :card (:node op)))]
|
||||
(raster/clear! before (:bg pal/index-of))
|
||||
|
|
@ -360,9 +371,9 @@
|
|||
(poly :p :root "a1" [0 0 10 0 10 10] :skin-base))
|
||||
day {:skin-base 1}
|
||||
night {:skin-base 17}]
|
||||
(is (= 1 (:color (first (scene/eval-frame s 0 nil day)))))
|
||||
(is (= 17 (:color (first (scene/eval-frame s 0 nil night)))))
|
||||
(is (= 17 (:color (first ((scene/resolver s nil night) 0))))
|
||||
(is (= 1 (:color (first (timeline/eval-frame s 0 nil day)))))
|
||||
(is (= 17 (:color (first (timeline/eval-frame s 0 nil night)))))
|
||||
(is (= 17 (:color (first ((timeline/resolver s nil night) 0))))
|
||||
"and the playback path agrees")))
|
||||
|
||||
(deftest a-tone-the-ramp-does-not-define-is-loudly-wrong
|
||||
|
|
@ -370,7 +381,7 @@
|
|||
;; authored data and should be impossible to miss.
|
||||
(let [s (sc {:id :root :kind :group :z "a1"}
|
||||
(poly :p :root "a1" [0 0 10 0 10 10] :skin-base))]
|
||||
(is (= 255 (:color (first (scene/eval-frame s 0 nil {})))))))
|
||||
(is (= 255 (:color (first (timeline/eval-frame s 0 nil {})))))))
|
||||
|
||||
(deftest partitioning-the-index-space-stops-two-palettes-colliding-on-a-stencil
|
||||
;; A stencil is a colour key, so two nodes sharing a tone share a stencil —
|
||||
|
|
@ -383,6 +394,6 @@
|
|||
[:style :color] (ch/framed :iris)}})
|
||||
;; :night's tones sit above :day's in one concatenated space
|
||||
night {:eye-white 14 :iris 15}
|
||||
ops (scene/eval-frame s 0 nil night)]
|
||||
ops (timeline/eval-frame s 0 nil night)]
|
||||
(is (= 14 (:stencil (second ops)))
|
||||
"the stencil resolves to the index the stencil node actually drew in")))
|
||||
|
|
@ -54,7 +54,7 @@
|
|||
|
||||
(deftest a-whole-leaf-map-round-trips-through-parsed-json
|
||||
;; What a save actually does: transit, then parsed so the column holds JSON.
|
||||
(let [ls (leaf/leaves :c1 @take/scene)]
|
||||
(let [ls (leaf/leaves :c1 @take/clip)]
|
||||
(is (= ls (into {} (map (fn [[p v]] [p (round-json v)])) ls)))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -2,9 +2,10 @@
|
|||
(:require [cljs.test :refer [deftest is]]
|
||||
[arthur.demo.take :as take]
|
||||
[arthur.domain.channel :as ch]
|
||||
[arthur.domain.clip :as clip]
|
||||
[arthur.domain.landmarks :as lm]
|
||||
[arthur.domain.palette :as pal]
|
||||
[arthur.domain.scene :as scene]
|
||||
[arthur.domain.timeline :as timeline]
|
||||
[arthur.flow.condition.eyes :as condition-eyes]
|
||||
[arthur.flow.ingest :as ingest]
|
||||
[arthur.flow.measure.eyes :as eyes]
|
||||
|
|
@ -45,20 +46,20 @@
|
|||
|
||||
(deftest the-full-take-hides-only-the-annotated-eye-and-returns-to-the-same-id
|
||||
(let [presence (ingest/feature-presence take/frames {:eye-r [[10 14]]})
|
||||
{:keys [scene store]} (flow-take/build
|
||||
{:keys [clip store]} (flow-take/build
|
||||
(assoc take/params :aspect 1 :name "observed-gap")
|
||||
{:dense @take/analysis :presence presence})
|
||||
sample (fn [id frame]
|
||||
(ch/value-at (get-in scene [:nodes id :channels [:geom :pts]])
|
||||
(ch/value-at (get-in (clip/nodes clip) [id :channels [:geom :pts]])
|
||||
frame store))]
|
||||
(is (empty? (scene/problems scene)))
|
||||
(is (= [:eye-r :eye-l] (get-in scene [:groups :eyes-1 :members])))
|
||||
(is (empty? (clip/problems clip)))
|
||||
(is (= [:eye-r :eye-l] (get-in clip [:groups :eyes-1 :members])))
|
||||
(doseq [f (range 9 14)]
|
||||
(is (ch/nothing? (sample :eye-r f)))
|
||||
(is (not (ch/nothing? (sample :eye-l f))))
|
||||
(is (not (ch/nothing? (sample :mouth f)))))
|
||||
(let [drawn (into #{} (map :node)
|
||||
((scene/resolver scene store pal/index-of) 11))]
|
||||
((timeline/resolver (clip/root clip) store pal/index-of) 11))]
|
||||
(is (not (contains? drawn :eye-r)))
|
||||
(is (not (contains? drawn :iris-r)))
|
||||
(is (contains? drawn :eye-l))
|
||||
|
|
|
|||
|
|
@ -11,12 +11,13 @@
|
|||
(:require [cljs.test :refer [deftest is testing]]
|
||||
[arthur.demo.take :as take]
|
||||
[arthur.domain.channel :as ch]
|
||||
[arthur.domain.clip :as clip]
|
||||
[arthur.domain.geom :as geom]
|
||||
[arthur.domain.node :as node]
|
||||
[arthur.domain.palette :as pal]
|
||||
[arthur.domain.raster :as raster]
|
||||
[arthur.domain.ring :as ring]
|
||||
[arthur.domain.scene :as scene]
|
||||
[arthur.domain.timeline :as timeline]
|
||||
[arthur.flow.freeze :as freeze]))
|
||||
|
||||
(def ^:private W 320)
|
||||
|
|
@ -24,13 +25,17 @@
|
|||
|
||||
;; The whole vertical slice, exactly as the page builds it. Asserting against the
|
||||
;; page's own clip rather than against a fixture built here is deliberate: a
|
||||
;; fixture is a second scene nobody looks at, and the one that renders is the one
|
||||
;; fixture is a second document nobody looks at, and the one that renders is the one
|
||||
;; that has to be right.
|
||||
(def clip (delay @take/frozen))
|
||||
(def scene* (delay (:scene @clip)))
|
||||
(def store (delay (:store @clip)))
|
||||
(def frozen (delay @take/frozen))
|
||||
(def clip* (delay (:clip @frozen)))
|
||||
;; The ROOT TIMELINE, which is what an evaluator takes. `clip*` is the document —
|
||||
;; `:fps`, the stage, the analysis, the tracking identities — and `tl*` is the bag
|
||||
;; of nodes in its frame space. `timeline/resolver` refuses the wrong one loudly.
|
||||
(def tl* (delay (clip/root @clip*)))
|
||||
(def store (delay (:store @frozen)))
|
||||
|
||||
(defn- node [id] (get-in @scene* [:nodes id]))
|
||||
(defn- node [id] (get-in @tl* [:nodes id]))
|
||||
(defn- chan [id path] (get-in (node id) [:channels path]))
|
||||
|
||||
(defn- block-of
|
||||
|
|
@ -44,16 +49,19 @@
|
|||
(let [v (ch/value-at (chan id [:geom :pts]) f @store)]
|
||||
(mapv #(ch/component v %) (range (.-length v)))))
|
||||
|
||||
(defn- ops-at [sc f]
|
||||
((scene/resolver sc @store pal/index-of) f))
|
||||
(defn- ops-at
|
||||
"Ops for one frame of a TIMELINE."
|
||||
[tl f]
|
||||
((timeline/resolver tl @store pal/index-of) f))
|
||||
|
||||
(defn- render
|
||||
"One frame of a scene into a byte buffer. The stage's size comes off the clip,
|
||||
because project dimensions are the project's and not the footage's."
|
||||
[sc f]
|
||||
(let [r (raster/make (:width sc) (:height sc))]
|
||||
"One frame of a CLIP into a byte buffer. The stage's size comes off the clip and
|
||||
the ops off its root timeline, because project dimensions are the project's and
|
||||
not the footage's — and not a timeline's either."
|
||||
[c f]
|
||||
(let [r (raster/make (:width c) (:height c))]
|
||||
(raster/clear! r (get pal/index-of :bg))
|
||||
(raster/draw-ops! r (ops-at sc f))
|
||||
(raster/draw-ops! r (ops-at (clip/root c) f))
|
||||
(vec (array-seq (:buf r)))))
|
||||
|
||||
(defn- drawn
|
||||
|
|
@ -64,25 +72,27 @@
|
|||
;; ---------------------------------------------------------------------------
|
||||
;; the shape of what came out
|
||||
|
||||
(deftest the-frozen-take-is-a-valid-scene-in-every-head-mode
|
||||
;; `scene/problems` is total by construction, so this is safe to run over data
|
||||
;; before the data is trusted — which is what it is for.
|
||||
(deftest the-frozen-take-is-a-valid-clip-in-every-head-mode
|
||||
;; `clip/problems` is total by construction, so this is safe to run over data
|
||||
;; before the data is trusted — which is what it is for. It checks the clip's
|
||||
;; fields, every timeline in it and the tracking identities, so it is the whole
|
||||
;; of what a save would refuse.
|
||||
(doseq [mode [:as-filmed :locked]]
|
||||
(let [sc (freeze/head-mode {:mode mode} @clip)]
|
||||
(is (empty? (scene/problems sc)) (str mode ": " (pr-str (scene/problems sc))))))
|
||||
(let [sc (freeze/head-mode {:mode :per-plate :kept #{0 12 40 88 150}} @clip)]
|
||||
(is (empty? (scene/problems sc)) (pr-str (scene/problems sc)))))
|
||||
(let [c (freeze/head-mode {:mode mode} @frozen)]
|
||||
(is (empty? (clip/problems c)) (str mode ": " (pr-str (clip/problems c))))))
|
||||
(let [c (freeze/head-mode {:mode :per-plate :kept #{0 12 40 88 150}} @frozen)]
|
||||
(is (empty? (clip/problems c)) (pr-str (clip/problems c)))))
|
||||
|
||||
(deftest the-tree-is-the-one-the-model-specifies
|
||||
;; :face is AUTHORED and :head is MEASURED, and they are two nodes because two
|
||||
;; different things want that transform. A group node is free; keeping the
|
||||
;; hand-placed and the measured transform apart is the whole reason the
|
||||
;; transform is decomposed in the first place.
|
||||
(is (= [:face :root] (scene/lineage (:nodes @scene*) :face)))
|
||||
(is (= [:head :face :root] (scene/lineage (:nodes @scene*) :head)))
|
||||
(is (= [:mouth :head :face :root] (scene/lineage (:nodes @scene*) :mouth)))
|
||||
(is (= [:face :root] (timeline/lineage (:nodes @tl*) :face)))
|
||||
(is (= [:head :face :root] (timeline/lineage (:nodes @tl*) :head)))
|
||||
(is (= [:mouth :head :face :root] (timeline/lineage (:nodes @tl*) :mouth)))
|
||||
(is (= [:mouth-in :mouth :head :face :root]
|
||||
(scene/lineage (:nodes @scene*) :mouth-in)))
|
||||
(timeline/lineage (:nodes @tl*) :mouth-in)))
|
||||
;; Exposure lives on the clip root and inherits strictly.
|
||||
(is (= {:mode :map :expose 2} (:time (node :root))))
|
||||
(is (every? #(nil? (:time (node %))) [:face :head :mouth :mouth-in])))
|
||||
|
|
@ -186,8 +196,8 @@
|
|||
;; of the two says it is. A test that recomputed the chain would only be
|
||||
;; checking arithmetic against itself; this checks `node/local!`, `node/world!`
|
||||
;; and `emit` as well.
|
||||
(let [sc (freeze/head-mode {:mode :as-filmed} @clip)
|
||||
res (scene/resolver sc @store pal/index-of)
|
||||
(let [c (freeze/head-mode {:mode :as-filmed} @frozen)
|
||||
res (timeline/resolver (clip/root c) @store pal/index-of)
|
||||
k (first (:value (chan :face [:xform :scale])))
|
||||
anc (:value (chan :face [:xform :anchor]))
|
||||
pos (:value (chan :face [:xform :pos]))
|
||||
|
|
@ -228,9 +238,9 @@
|
|||
|
||||
(deftest the-three-head-modes-are-the-three-channel-shapes
|
||||
(let [kept #{0 12 40 88 150}
|
||||
of (fn [sc path] (get-in sc [:nodes :head :channels path]))]
|
||||
of (fn [c path] (get-in (clip/nodes c) [:head :channels path]))]
|
||||
(testing "locked is framed identity"
|
||||
(let [sc (freeze/head-mode {:mode :locked} @clip)]
|
||||
(let [sc (freeze/head-mode {:mode :locked} @frozen)]
|
||||
(is (= [:framed :framed :framed]
|
||||
(mapv #(ch/describe (of sc %))
|
||||
[[:xform :pos] [:xform :rot] [:xform :scale]])))
|
||||
|
|
@ -238,12 +248,12 @@
|
|||
(is (= 0.0 (:value (of sc [:xform :rot]))))
|
||||
(is (= [1.0 1.0] (:value (of sc [:xform :scale]))))))
|
||||
(testing "as filmed is dense"
|
||||
(let [sc (freeze/head-mode {:mode :as-filmed} @clip)]
|
||||
(let [sc (freeze/head-mode {:mode :as-filmed} @frozen)]
|
||||
(is (= [:dense :dense :dense]
|
||||
(mapv #(ch/describe (of sc %))
|
||||
[[:xform :pos] [:xform :rot] [:xform :scale]])))))
|
||||
(testing "per plate is keyed at exactly the kept frames"
|
||||
(let [sc (freeze/head-mode {:mode :per-plate :kept kept} @clip)]
|
||||
(let [sc (freeze/head-mode {:mode :per-plate :kept kept} @frozen)]
|
||||
(doseq [path [[:xform :pos] [:xform :rot] [:xform :scale]]]
|
||||
(is (= :keyed (ch/describe (of sc path))))
|
||||
(is (= (sort kept) (sort (keys (:keys (of sc path))))))
|
||||
|
|
@ -262,25 +272,29 @@
|
|||
;; It has to be impossible for the toggle to move something a hand placed, and
|
||||
;; it has to be a DOCUMENT edit: tier 1, undoable, syncable, instant, and not a
|
||||
;; reason to re-analyse.
|
||||
(let [a (freeze/head-mode {:mode :as-filmed} @clip)
|
||||
b (freeze/head-mode {:mode :locked} @clip)
|
||||
c (freeze/head-mode {:mode :per-plate :kept #{0 40}} @clip)]
|
||||
(doseq [sc [b c]]
|
||||
(is (= (get-in a [:nodes :face]) (get-in sc [:nodes :face]))
|
||||
(let [a (freeze/head-mode {:mode :as-filmed} @frozen)
|
||||
b (freeze/head-mode {:mode :locked} @frozen)
|
||||
c (freeze/head-mode {:mode :per-plate :kept #{0 40}} @frozen)]
|
||||
(doseq [x [b c]]
|
||||
(is (= (get (clip/nodes a) :face) (get (clip/nodes x) :face))
|
||||
":face moved")
|
||||
(is (= (dissoc (:nodes a) :head) (dissoc (:nodes sc) :head))
|
||||
(is (= (dissoc (clip/nodes a) :head) (dissoc (clip/nodes x) :head))
|
||||
"a node other than :head changed")
|
||||
;; The measurement does not go away when the head is locked: always measure,
|
||||
;; always store factored, toggle the parent.
|
||||
(is (= (get-in a [:nodes :head :measured]) (get-in sc [:nodes :head :measured]))))))
|
||||
(is (= (get-in (clip/nodes a) [:head :measured])
|
||||
(get-in (clip/nodes x) [:head :measured])))
|
||||
;; And nothing above the timeline moved either: the toggle is one node's
|
||||
;; channels, so the clip's own fields and its other timelines are untouched.
|
||||
(is (= (dissoc a :timelines) (dissoc x :timelines))))))
|
||||
|
||||
(deftest a-head-mode-that-is-not-one-of-the-three-is-refused
|
||||
(is (thrown-with-msg? ExceptionInfo #"not one of the three channel shapes"
|
||||
(freeze/head-mode {:mode :stabilised} @clip)))
|
||||
(freeze/head-mode {:mode :stabilised} @frozen)))
|
||||
;; The kept-frame set belongs to the plate strip, not to measurement, so freeze
|
||||
;; cannot invent one.
|
||||
(is (thrown-with-msg? ExceptionInfo #"kept-frame set"
|
||||
(freeze/head-mode {:mode :per-plate} @clip))))
|
||||
(freeze/head-mode {:mode :per-plate} @frozen))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; the face: authored, and what makes makeXform deletable
|
||||
|
|
@ -290,7 +304,7 @@
|
|||
;; transform on a node, which a hand can revise, and it claims no generator that
|
||||
;; would offer to overwrite it.
|
||||
(doseq [path [[:xform :pos] [:xform :rot] [:xform :scale] [:xform :anchor]]]
|
||||
(let [c (get (node/channels (get-in @scene* [:nodes :face])) path)]
|
||||
(let [c (get (node/channels (node :face)) path)]
|
||||
(is (= :framed (ch/describe c)) (str path " is not framed"))
|
||||
(is (nil? (:generated c)) (str path " claims provenance")))))
|
||||
|
||||
|
|
@ -314,18 +328,20 @@
|
|||
;; re-measuring anything.
|
||||
(let [big (freeze/clip (assoc take/params :stage [640 480] :name "big")
|
||||
@take/measured)
|
||||
key-of (fn [sc] (:store (:dense (get-in sc [:nodes :mouth :channels [:geom :pts]]))))]
|
||||
(is (= [640 480] [(:width (:scene big)) (:height (:scene big))]))
|
||||
key-of (fn [c] (:store (:dense (get-in (clip/nodes c)
|
||||
[:mouth :channels [:geom :pts]]))))]
|
||||
(is (= [640 480] [(:width (:clip big)) (:height (:clip big))]))
|
||||
;; Stronger than it was, and for free: the stage is not an input to tier 2, so
|
||||
;; the two clips do not merely hold equal bytes — they name the SAME BLOCK, and
|
||||
;; a stage change cannot invalidate a bake. The clip's name is not an input
|
||||
;; either, which is why "big" and "take" still agree.
|
||||
(is (= (key-of @scene*) (key-of (:scene big)))
|
||||
(is (= (key-of @clip*) (key-of (:clip big)))
|
||||
"a different stage is a different document over the same tier 2")
|
||||
(is (= (vec (array-seq (:data (get @store (key-of @scene*)))))
|
||||
(vec (array-seq (:data (get (:store big) (key-of (:scene big)))))))
|
||||
(is (= (vec (array-seq (:data (get @store (key-of @clip*)))))
|
||||
(vec (array-seq (:data (get (:store big) (key-of (:clip big)))))))
|
||||
"the geometry is the same numbers at either stage size")
|
||||
(is (not= (:value (get-in (:scene big) [:nodes :face :channels [:xform :scale]]))
|
||||
(is (not= (:value (get-in (clip/nodes (:clip big))
|
||||
[:face :channels [:xform :scale]]))
|
||||
(:value (chan :face [:xform :scale]))))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
|
@ -355,7 +371,7 @@
|
|||
;; interior comes and goes.
|
||||
(is (nil? (chan :mouth [:vis])))
|
||||
(doseq [f (range 0 take/frames 9)]
|
||||
(is (some #(= :mouth (:node %)) (ops-at @scene* f))
|
||||
(is (some #(= :mouth (:node %)) (ops-at @tl* f))
|
||||
(str "frame " f " drew no mouth outline"))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
|
@ -384,13 +400,15 @@
|
|||
;; `:generated` out of the document and the frame is the same bytes. If this
|
||||
;; ever fails, a rotoscoped part and a hand-drawn one have stopped being the
|
||||
;; same data.
|
||||
(let [stripped (update @scene* :nodes
|
||||
(fn [ns] (into {} (map (fn [[id n]]
|
||||
[id (update n :channels
|
||||
#(into {} (map (fn [[p c]] [p (dissoc c :generated)])) %))]))
|
||||
ns)))]
|
||||
(let [strip-node (fn [n]
|
||||
(update n :channels
|
||||
#(into {} (map (fn [[p c]] [p (dissoc c :generated)])) %)))
|
||||
stripped (clip/update-root
|
||||
@clip*
|
||||
update :nodes
|
||||
#(into {} (map (fn [[id n]] [id (strip-node n)])) %))]
|
||||
(doseq [f (range 0 take/frames 17)]
|
||||
(is (= (render @scene* f) (render stripped f))
|
||||
(is (= (render @clip* f) (render stripped f))
|
||||
(str "frame " f " differs with provenance removed")))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
|
@ -401,21 +419,23 @@
|
|||
presence {:eye-r (mapv #(not (contains? gap %)) (range take/frames))}
|
||||
c (freeze/clip (assoc take/params :name "one-eye-gappy")
|
||||
(assoc @take/measured :presence presence))
|
||||
sc (:scene c)
|
||||
tl (clip/root (:clip c))
|
||||
at (fn [id f]
|
||||
(ch/value-at (get-in sc [:nodes id :channels [:geom :pts]]) f (:store c)))]
|
||||
(is (= [:eye-r :eye-l] (get-in sc [:groups :eyes-1 :members])))
|
||||
(is (= :eye-r (get-in sc [:features :eye-r :id])))
|
||||
(ch/value-at (get-in (:nodes tl) [id :channels [:geom :pts]]) f (:store c)))]
|
||||
;; The identities are the CLIP's, and a gap does not touch them: a feature keeps
|
||||
;; its id and its pair membership across the frames it was not observed on.
|
||||
(is (= [:eye-r :eye-l] (get-in (:clip c) [:groups :eyes-1 :members])))
|
||||
(is (= :eye-r (get-in (:clip c) [:features :eye-r :id])))
|
||||
(is (not (ch/nothing? (at :eye-r 39))))
|
||||
(is (ch/nothing? (at :eye-r 50)))
|
||||
(is (not (ch/nothing? (at :eye-r 60))))
|
||||
(is (not (ch/nothing? (at :eye-l 50))))
|
||||
(is (not (ch/nothing? (at :mouth 50))))
|
||||
(let [drawn-nodes (into #{} (map :node) ((scene/resolver sc (:store c) pal/index-of) 50))]
|
||||
(let [drawn-nodes (into #{} (map :node) ((timeline/resolver tl (:store c) pal/index-of) 50))]
|
||||
(is (not (contains? drawn-nodes :eye-r)))
|
||||
(is (contains? drawn-nodes :eye-l))
|
||||
(is (contains? drawn-nodes :mouth)))
|
||||
(is (empty? (scene/problems sc)))))
|
||||
(is (empty? (clip/problems (:clip c))))))
|
||||
|
||||
(deftest each-dense-track-follows-its-own-features-presence
|
||||
;; Every feature is given a DIFFERENT gap, so a track wired to the wrong one
|
||||
|
|
@ -437,9 +457,9 @@
|
|||
windows)
|
||||
c (freeze/clip (assoc take/params :name "per-track-gaps")
|
||||
(assoc @take/measured :presence presence))
|
||||
sc (:scene c)
|
||||
tl (clip/root (:clip c))
|
||||
at (fn [id path f]
|
||||
(ch/value-at (get-in sc [:nodes id :channels path]) f (:store c)))
|
||||
(ch/value-at (get-in (:nodes tl) [id :channels path]) f (:store c)))
|
||||
;; Node, channel, and the feature whose gap it must follow. Every dense
|
||||
;; track of the eye, iris, brow and brow-position blocks appears once.
|
||||
tracks [[:eye-r [:geom :pts] :eye-r]
|
||||
|
|
@ -452,7 +472,7 @@
|
|||
[:brow-l [:geom :pts] :brow-l]
|
||||
[:brow-r [:xform :pos] :brow-r]
|
||||
[:brow-l [:xform :pos] :brow-l]]]
|
||||
(is (empty? (scene/problems sc)))
|
||||
(is (empty? (clip/problems (:clip c))))
|
||||
(doseq [[id path owner] tracks
|
||||
[feature gap] windows
|
||||
f gap]
|
||||
|
|
@ -465,10 +485,10 @@
|
|||
(deftest teeth-follow-the-mouth-through-their-stencil-and-not-through-a-mask
|
||||
;; Teeth are their own feature, so that they can carry their own :area :teeth
|
||||
;; parameters — which means an occluded MOUTH sets no absence bit on them. They
|
||||
;; do not need one: they are stencilled by :mouth-in, and scene/finish drops a
|
||||
;; do not need one: they are stencilled by :mouth-in, and timeline/finish drops a
|
||||
;; node whose stencil drew nothing. So the coupling is real, and it is the
|
||||
;; stencil rule rather than the mask that enforces it. The rule itself is
|
||||
;; asserted in scene-test; what is pinned here is the wiring that relies on it.
|
||||
;; asserted in timeline-test; what is pinned here is the wiring that relies on it.
|
||||
(let [gap (set (range 40 60))
|
||||
teeth-in {;; The mouth's own inner ring standing in for a pixel-derived
|
||||
;; contour: real geometry and no nils, so every frame has a
|
||||
|
|
@ -485,7 +505,7 @@
|
|||
;; Hoisted: the resolver caches its order and reuses its buffers, so the
|
||||
;; node ids come out before the next frame is asked for.
|
||||
nodes-at (fn [c]
|
||||
(let [r (scene/resolver (:scene c) (:store c) pal/index-of)]
|
||||
(let [r (timeline/resolver (clip/root (:clip c)) (:store c) pal/index-of)]
|
||||
(fn [f] (into #{} (map :node) (r f)))))
|
||||
ref-at (nodes-at ref)
|
||||
occ-at (nodes-at occ)
|
||||
|
|
@ -495,7 +515,7 @@
|
|||
open (filter #(contains? (ref-at %) :teeth) (range take/frames))
|
||||
inside (first (filter gap open))
|
||||
outside (first (remove gap open))]
|
||||
(is (= :mouth-in (get-in (:scene occ) [:nodes :teeth :stencil]))
|
||||
(is (= :mouth-in (get-in (clip/nodes (:clip occ)) [:teeth :stencil]))
|
||||
"teeth stop inheriting the mouth's absence if this stops being their stencil")
|
||||
(is (some? inside) "no open-mouth frame inside the gap to test with")
|
||||
(is (some? outside) "no open-mouth frame outside the gap to test with")
|
||||
|
|
@ -503,14 +523,14 @@
|
|||
;; Annotating the MOUTH sets no bit on the teeth: they are a feature of
|
||||
;; their own and nothing named them.
|
||||
(is (not (ch/nothing?
|
||||
(ch/value-at (get-in (:scene occ) [:nodes :teeth :channels [:geom :pts]])
|
||||
(ch/value-at (get-in (clip/nodes (:clip occ)) [:teeth :channels [:geom :pts]])
|
||||
inside (:store occ)))))
|
||||
;; They are dropped anyway — :mouth-in drew nothing to clip them against.
|
||||
(is (not (contains? (occ-at inside) :mouth-in)))
|
||||
(is (not (contains? (occ-at inside) :teeth)))
|
||||
;; And the same articulation outside the gap still draws them.
|
||||
(is (contains? (occ-at outside) :teeth)))
|
||||
(is (empty? (scene/problems (:scene occ))))))
|
||||
(is (empty? (clip/problems (:clip occ))))))
|
||||
|
||||
(deftest an-undetected-frame-has-no-pose-at-all
|
||||
;; A subject that was not on the frame has NO VALUE, which is different from a
|
||||
|
|
@ -521,8 +541,8 @@
|
|||
det (mapv #(not (contains? gap %)) (range take/frames))
|
||||
c (freeze/clip (assoc take/params :name "gappy")
|
||||
(assoc @take/measured :detected det))
|
||||
sc (:scene c)
|
||||
res (scene/resolver sc (:store c) pal/index-of)]
|
||||
tl (clip/root (:clip c))
|
||||
res (timeline/resolver tl (:store c) pal/index-of)]
|
||||
(doseq [f [39 40 50 59 60]]
|
||||
(let [ops (res f)]
|
||||
(if (contains? gap f)
|
||||
|
|
@ -531,7 +551,7 @@
|
|||
;; And it is the MASK doing it, not a hidden flag: `[:vis]` on :mouth-in is
|
||||
;; unchanged across the gap, because hiding and absence are different
|
||||
;; questions with different answers.
|
||||
(is (= (mapv #(ch/value-at (get-in sc [:nodes :mouth-in :channels [:vis]]) %)
|
||||
(is (= (mapv #(ch/value-at (get-in (:nodes tl) [:mouth-in :channels [:vis]]) %)
|
||||
(range take/frames))
|
||||
(mapv #(ch/value-at (chan :mouth-in [:vis]) %) (range take/frames))))))
|
||||
|
||||
|
|
@ -569,7 +589,7 @@
|
|||
|
||||
(deftest the-take-draws-something-on-every-frame
|
||||
(doseq [f (range 0 take/frames 5)]
|
||||
(let [n (drawn (render @scene* f))]
|
||||
(let [n (drawn (render @clip* f))]
|
||||
(is (> n 200) (str "frame " f " drew only " n " pixels")))))
|
||||
|
||||
(deftest the-mouth-moves
|
||||
|
|
@ -577,10 +597,10 @@
|
|||
;; from different beats are genuinely different mouths and frames inside one
|
||||
;; beat are not. This is the numeric half of step 5's done-criterion; the other
|
||||
;; half is a picture and lives in test/browser/take.mjs.
|
||||
(let [locked (freeze/head-mode {:mode :locked} @clip)
|
||||
(let [locked (freeze/head-mode {:mode :locked} @frozen)
|
||||
shot (fn [f]
|
||||
(let [r (raster/make W H)
|
||||
mouth (filter #(= :mouth (:node %)) (ops-at locked f))]
|
||||
mouth (filter #(= :mouth (:node %)) (ops-at (clip/root locked) f))]
|
||||
(raster/clear! r (get pal/index-of :bg))
|
||||
(raster/draw-ops! r mouth)
|
||||
(vec (array-seq (:buf r)))))
|
||||
|
|
@ -594,6 +614,6 @@
|
|||
(is (< (differ (shot 10) (shot 12)) 200)
|
||||
"a held pose is moving more than the detector noise it should have lost"))
|
||||
;; As filmed, the head carries it around the stage as well.
|
||||
(let [filmed (freeze/head-mode {:mode :as-filmed} @clip)]
|
||||
(let [filmed (freeze/head-mode {:mode :as-filmed} @frozen)]
|
||||
(is (> (count (remove true? (map = (render filmed 10) (render filmed 120)))) 300)
|
||||
"the head does not move across the take")))
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
(ns arthur.support.ops
|
||||
"Comparing two evaluations of a scene, frame for frame.
|
||||
"Comparing two evaluations of a timeline, frame for frame.
|
||||
|
||||
`domain/scene` has two evaluators on purpose — `eval-frame` is the
|
||||
specification and `resolver` is what playback uses — and scene-test's central
|
||||
`domain/timeline` has two evaluators on purpose — `eval-frame` is the
|
||||
specification and `resolver` is what playback uses — and timeline-test's central
|
||||
assertion is that they agree in forward, backward and random frame order.
|
||||
Step 9 needs the same comparison for a different question: that a scene which
|
||||
Step 9 needs the same comparison for a different question: that a document which
|
||||
has been through the server produces the same ops as the one that went in.
|
||||
|
||||
Shared rather than copied, because the interesting part is not the equality —
|
||||
|
|
@ -12,12 +12,15 @@
|
|||
point buffer per node, so it can agree on a forward pass and disagree on a
|
||||
scrub, and a copy of this list that forgot 'backward' would test the easy half.
|
||||
|
||||
Every entry point takes a TIMELINE, not a clip: what produces ops is a bag of
|
||||
nodes in a frame space, and a caller with a clip says `(clip/root c)`.
|
||||
|
||||
`snapshot` is what makes ops comparable at all: a resolved op carries `:pts` as
|
||||
a VIEW into a reused buffer, so two ops from different frames can be `=` while
|
||||
naming the same array, and holding one and then asking for the next frame
|
||||
changes what the first one says. Reading the points out is what pins the frame."
|
||||
(:require [arthur.domain.palette :as pal]
|
||||
[arthur.domain.scene :as scene]))
|
||||
[arthur.domain.timeline :as timeline]))
|
||||
|
||||
(defn points
|
||||
"An op's points as a vector of [x y], read out of its buffer."
|
||||
|
|
@ -27,7 +30,7 @@
|
|||
|
||||
(defn snapshot
|
||||
"Ops -> comparable data. `:i` goes too: it is the draw-order index, and it is a
|
||||
function of the scene rather than of the frame."
|
||||
function of the timeline rather than of the frame."
|
||||
[ops]
|
||||
(mapv (fn [op]
|
||||
(cond-> (dissoc op :pts :i)
|
||||
|
|
@ -47,13 +50,13 @@
|
|||
|
||||
(defn specified
|
||||
"(fn [f] -> snapshot) through `eval-frame`, the specification."
|
||||
([scene store] (specified scene store pal/index-of))
|
||||
([scene store palette]
|
||||
(fn [f] (snapshot (scene/eval-frame scene f store palette)))))
|
||||
([tl store] (specified tl store pal/index-of))
|
||||
([tl store palette]
|
||||
(fn [f] (snapshot (timeline/eval-frame tl f store palette)))))
|
||||
|
||||
(defn resolved
|
||||
"(fn [f] -> snapshot) through `resolver`, the playback path."
|
||||
([scene store] (resolved scene store pal/index-of))
|
||||
([scene store palette]
|
||||
(let [res (scene/resolver scene store palette)]
|
||||
([tl store] (resolved tl store pal/index-of))
|
||||
([tl store palette]
|
||||
(let [res (timeline/resolver tl store palette)]
|
||||
(fn [f] (snapshot (res f))))))
|
||||
|
|
|
|||
|
|
@ -186,7 +186,7 @@ const PROBE = `(() => {
|
|||
cx: drawn ? cx / drawn : null, cy: drawn ? cy / drawn : null,
|
||||
hash: h >>> 0,
|
||||
frame: document.querySelector('.readout span')?.textContent ?? '',
|
||||
scene: [...document.querySelectorAll('.transport .row button')]
|
||||
selectedClip: [...document.querySelectorAll('.transport .row button')]
|
||||
.filter((b) => b.classList.contains('on')).map((b) => b.textContent),
|
||||
};
|
||||
})()`;
|
||||
|
|
@ -235,9 +235,9 @@ async function main() {
|
|||
}
|
||||
if (!probe) throw new Error('no canvas.stage on the page — is `shadow-cljs watch app` running?');
|
||||
|
||||
console.log(`\ncanvas ${probe.w}x${probe.h}, scene ${JSON.stringify(probe.scene)}`);
|
||||
console.log(`\ncanvas ${probe.w}x${probe.h}, clip ${JSON.stringify(probe.selectedClip)}`);
|
||||
|
||||
check(probe.scene.includes('take'), 'the take is the scene that opens');
|
||||
check(probe.selectedClip.includes('take'), 'the take is the clip that opens');
|
||||
check(probe.w === 320 && probe.h === 200, 'the canvas is the stage size',
|
||||
`${probe.w}x${probe.h}`);
|
||||
check(probe.drawn > 200, 'the first frame is not blank', `${probe.drawn} px drawn`);
|
||||
|
|
@ -384,8 +384,8 @@ async function main() {
|
|||
'and the open mouth still has an interior on the far side');
|
||||
// The reopened clip is not one of the built-ins: this is the document that came
|
||||
// back from the server, not the one that was in the page all along.
|
||||
check(!back[0].scene.includes('take'), 'the picture is the reopened document',
|
||||
JSON.stringify(back[0].scene));
|
||||
check(!back[0].selectedClip.includes('take'), 'the picture is the reopened document',
|
||||
JSON.stringify(back[0].selectedClip));
|
||||
|
||||
check(page.logs.length === 0, 'no errors on the console',
|
||||
page.logs.slice(0, 3).join(' | '));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue