multi fce stuff
This commit is contained in:
parent
49ece8dee6
commit
ddabfbeaa8
32 changed files with 1309 additions and 700 deletions
|
|
@ -1,6 +1,5 @@
|
|||
(ns arthur.domain.feature-test
|
||||
(:require [cljs.test :refer [deftest is]]
|
||||
[arthur.domain.clip :as clip]
|
||||
[arthur.domain.feature :as feature]
|
||||
[arthur.domain.params :as params]))
|
||||
|
||||
|
|
@ -12,16 +11,22 @@
|
|||
{:subjects (into {} (map (fn [id] [id {:id id :params {}}]) people))
|
||||
:features (into {} (map-indexed (fn [i id]
|
||||
[id {:id id :subject (nth people (quot i 2))
|
||||
:timeline (nth people (quot i 2))
|
||||
:area :eye :nodes [] :params {}}]) eyes))
|
||||
:groups (into {} (map-indexed (fn [i ids]
|
||||
(let [id (keyword (str "pair-" i))]
|
||||
[id {:id id :kind :eye-pair
|
||||
:subject (nth people i)
|
||||
:members ids :params {}}])) members))
|
||||
:timelines {:main {:id :main :frames 1 :nodes {}}}}))
|
||||
:timelines (into {} (map (fn [id]
|
||||
[id {:id id :frames 1
|
||||
:nodes {:head {:id :head :kind :group :z "a1"
|
||||
:measured {[:xform :rot]
|
||||
{:animated? false :value 0.0}}}}}]))
|
||||
people)}))
|
||||
|
||||
(defn- problems [c]
|
||||
(feature/problems c (clip/nodes c)))
|
||||
(feature/problems c))
|
||||
|
||||
(deftest five-people-can-have-nine-identified-eyes
|
||||
(let [scene (nine-eyes)]
|
||||
|
|
|
|||
|
|
@ -40,16 +40,17 @@
|
|||
(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"))
|
||||
;; Nodes are local to the face timeline; feature and group ids are clip-wide.
|
||||
(is (contains? ls "clip/c7/timeline/face-1/node/mouth"))
|
||||
(is (contains? ls "clip/c7/timeline/face-1/channel/mouth/geom.pts"))
|
||||
(is (contains? ls "clip/c7/timeline/face-1/channel/mouth-in/vis"))
|
||||
(is (contains? ls "clip/c7/feature/face-1~eye-r"))
|
||||
(is (contains? ls "clip/c7/group/face-1~eyes"))
|
||||
(is (contains? ls "clip/c7/subject/face-1"))
|
||||
;; `:head`'s measured channels are written together by a freeze and replaced
|
||||
;; A 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/timeline/main/measured/head"))
|
||||
(is (= 3 (count (get ls "clip/c7/timeline/main/measured/head"))))
|
||||
(is (contains? ls "clip/c7/timeline/face-1/measured/head"))
|
||||
(is (= 3 (count (get ls "clip/c7/timeline/face-1/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.
|
||||
|
|
@ -59,11 +60,11 @@
|
|||
;; 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/clip)
|
||||
n (get ls "clip/c1/timeline/main/node/mouth")]
|
||||
(is (= {:id :mouth :name "mouth" :kind :poly :parent :head :z "a1"
|
||||
:pose-group :mouth} n))
|
||||
n (get ls "clip/c1/timeline/face-1/node/mouth")]
|
||||
(is (= {:id :mouth :name "mouth" :kind :poly :parent :head
|
||||
:z "a1" :pose-group :mouth} n))
|
||||
(is (nil? (:channels n)))
|
||||
(is (:animated? (get ls "clip/c1/timeline/main/channel/mouth/geom.pts")))))
|
||||
(is (:animated? (get ls "clip/c1/timeline/face-1/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 field
|
||||
|
|
@ -154,7 +155,7 @@
|
|||
(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/clip) "clip/c1/timeline/main/node/mouth")]
|
||||
(let [ls (dissoc (leaf/leaves :c1 @take/clip) "clip/c1/timeline/face-1/node/mouth")]
|
||||
(is (some #(re-find #"node with no node leaf" %) (leaf/problems ls)))))
|
||||
|
||||
(deftest a-node-leaf-is-scoped-to-its-own-timeline
|
||||
|
|
@ -166,7 +167,7 @@
|
|||
(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"))]
|
||||
(dissoc "clip/c1/timeline/face-1/node/mouth"))]
|
||||
(is (some #(re-find #"node with no node leaf" %) (leaf/problems ls)))))
|
||||
|
||||
(deftest a-property-with-path-punctuation-in-it-is-refused
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@
|
|||
[arthur.flow.freeze :as freeze]
|
||||
[arthur.support.ops :as ops]))
|
||||
|
||||
(defn- face-timeline [c] (clip/timeline c :face-1))
|
||||
|
||||
(defn- wired
|
||||
"A clip out and back, over a wire that is really only JSON."
|
||||
[cid clip]
|
||||
|
|
@ -57,8 +59,8 @@
|
|||
"spec vs playback, after" [ops/specified ops/resolved]}]
|
||||
(doseq [[label [f g]] paths
|
||||
[order fs] (ops/orders n)]
|
||||
(let [a (f (clip/root (:clip @before)) (:store @before))
|
||||
b (g (clip/root (:clip @after)) (:store @after))]
|
||||
(let [a (f (face-timeline (:clip @before)) (:store @before))
|
||||
b (g (face-timeline (:clip @after)) (:store @after))]
|
||||
(testing (str label ", " order)
|
||||
(doseq [frame fs]
|
||||
(is (= (a frame) (b frame))
|
||||
|
|
@ -82,8 +84,8 @@
|
|||
;; everything above and lose the locked take's identity transform.
|
||||
(let [locked {:clip @take/locked :store @take/store}
|
||||
back (wired :c1 locked)
|
||||
a (ops/resolved (clip/root (:clip locked)) (:store locked))
|
||||
b (ops/resolved (clip/root (:clip back)) (:store back))]
|
||||
a (ops/resolved (face-timeline (:clip locked)) (:store locked))
|
||||
b (ops/resolved (face-timeline (:clip back)) (:store back))]
|
||||
(is (= (:clip locked) (:clip back)))
|
||||
(doseq [frame (range 0 take/frames 7)]
|
||||
(is (= (a frame) (b frame)) (str "frame " frame)))))
|
||||
|
|
@ -104,17 +106,17 @@
|
|||
|
||||
(def ^:private gappy
|
||||
(delay (freeze/clip (assoc take/params :name "gappy")
|
||||
(assoc @take/measured
|
||||
:presence
|
||||
(into {} (map (fn [[id gap]]
|
||||
[id (mapv #(not (contains? gap %))
|
||||
(range take/frames))]))
|
||||
windows)))))
|
||||
{:face-1 (assoc @take/measured
|
||||
:presence
|
||||
(into {} (map (fn [[id gap]]
|
||||
[id (mapv #(not (contains? gap %))
|
||||
(range take/frames))]))
|
||||
windows))})))
|
||||
|
||||
(deftest an-absence-mask-survives-the-wire
|
||||
(let [back (wired :c1 @gappy)
|
||||
at (fn [entry id path f]
|
||||
(ch/value-at (get-in (clip/nodes (:clip entry)) [id :channels path])
|
||||
(ch/value-at (get-in (:nodes (face-timeline (: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
|
||||
|
|
@ -145,7 +147,7 @@
|
|||
;; frame rather than hidden, and its partner is not.
|
||||
(let [back (wired :c1 @gappy)
|
||||
drawn (into #{} (map :node)
|
||||
((timeline/resolver (clip/root (:clip back)) (:store back)) 12))]
|
||||
((timeline/resolver (face-timeline (:clip back)) (:store back)) 12))]
|
||||
(is (not (contains? drawn :eye-r)))
|
||||
(is (contains? drawn :eye-l))
|
||||
(is (contains? drawn :mouth))))
|
||||
|
|
|
|||
|
|
@ -109,9 +109,10 @@
|
|||
(deftest a-three-pixel-pupil-is-three-by-three-at-every-centre
|
||||
;; A square pupil is only worth having if it is the SAME square every frame:
|
||||
;; exactly its nominal size at any centre, or it breathes as the gaze moves.
|
||||
(doseq [[cx cy] [[20 20] [20.5 20.5] [20.49 19.51] [21 20] [20.9 20.1]]]
|
||||
(doseq [[cx cy] [[20 20] [20.5 20.5] [20.49 19.51] [21 20] [20.9 20.1]]
|
||||
size [2.6 3 3.4]]
|
||||
(let [ras (-> (r/make 40 40) (r/clear! 0))]
|
||||
(r/fill-rect! ras cx cy 3 1)
|
||||
(r/fill-rect! ras cx cy size 1)
|
||||
(let [[x0 x1 y0 y1 n] (bbox ras 1)]
|
||||
(is (= [3 3 9] [(inc (- x1 x0)) (inc (- y1 y0)) n])
|
||||
(str "centre " cx "," cy " gave " (inc (- x1 x0)) "x" (inc (- y1 y0)) ":" n))))))
|
||||
|
|
|
|||
|
|
@ -244,7 +244,7 @@
|
|||
|
||||
;; ---- discs and rects ----
|
||||
|
||||
(deftest a-discs-radius-takes-the-mean-scale-and-a-rects-size-is-rounded
|
||||
(deftest disc-and-rect-extents-retain-precision-for-enclosing-instances
|
||||
(let [s (sc {:id :g :kind :group :z "a1"
|
||||
:channels {[:xform :pos] (ch/framed [50 60]) [:xform :scale] (ch/framed [2 2])}}
|
||||
{:id :d :kind :disc :parent :g :z "a1"
|
||||
|
|
@ -253,9 +253,7 @@
|
|||
:channels {[:geom :size] (ch/framed 1.7) [:style :color] (ch/framed :pupil)}})
|
||||
[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.
|
||||
(is (= 3 (:size r)))))
|
||||
(is (= 3.4 (:size r)))))
|
||||
|
||||
;; ---- the fast path and the specification agree ----
|
||||
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@
|
|||
:seed 3 :frames frames :fps (:fps p)
|
||||
:aspect (:aspect p)}
|
||||
(:detector overrides))))]
|
||||
(freeze/clip p (take/measure p {:dense @dense-track}))))
|
||||
(freeze/clip p {:face-1 (take/measure p {:dense @dense-track})})))
|
||||
|
||||
(defn- bytes-of [{:keys [data state]}]
|
||||
(str (sha/of-bytes (js/Uint8Array. (.-buffer data)))
|
||||
|
|
@ -228,9 +228,10 @@
|
|||
(assoc (params-at {}) :analysis
|
||||
(address/analysis {:detector "synth" :version "mulberry32"
|
||||
:seed 3 :frames frames :fps fps :aspect 1}))
|
||||
(assoc (take/measure (params-at {}) {:dense @dense-track})
|
||||
:presence {:brow-r (mapv #(not (contains? gap %))
|
||||
(range frames))})))]
|
||||
{:face-1 (assoc (take/measure (params-at {}) {:dense @dense-track})
|
||||
:presence
|
||||
{:brow-r (mapv #(not (contains? gap %))
|
||||
(range frames))})}))]
|
||||
(is (not= (:key (get @base "brows")) (:key (get with "brows"))))
|
||||
(is (not= (:key (get @base "brow-pos")) (:key (get with "brow-pos"))))
|
||||
(doseq [role ["geom" "eyes" "iris-pos" "head-pos" "head-rot" "head-scale"]]
|
||||
|
|
|
|||
|
|
@ -48,18 +48,18 @@
|
|||
(let [presence (ingest/feature-presence take/frames {:eye-r [[10 14]]})
|
||||
{:keys [clip store]} (flow-take/build
|
||||
(assoc take/params :aspect 1 :name "observed-gap")
|
||||
{:dense @take/analysis :presence presence})
|
||||
{:face-1 {:dense @take/analysis :presence presence}})
|
||||
sample (fn [id frame]
|
||||
(ch/value-at (get-in (clip/nodes clip) [id :channels [:geom :pts]])
|
||||
(ch/value-at (get-in (:nodes (clip/timeline clip :face-1)) [id :channels [:geom :pts]])
|
||||
frame store))]
|
||||
(is (empty? (clip/problems clip)))
|
||||
(is (= [:eye-r :eye-l] (get-in clip [:groups :eyes-1 :members])))
|
||||
(is (= [:face-1/eye-r :face-1/eye-l] (get-in clip [:groups :face-1/eyes :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)
|
||||
((timeline/resolver (clip/root clip) store pal/index-of) 11))]
|
||||
((timeline/resolver (clip/timeline clip :face-1) store pal/index-of) 11))]
|
||||
(is (not (contains? drawn :eye-r)))
|
||||
(is (not (contains? drawn :iris-r)))
|
||||
(is (contains? drawn :eye-l))
|
||||
|
|
|
|||
|
|
@ -30,13 +30,14 @@
|
|||
;; that has to be right.
|
||||
(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*)))
|
||||
;; Geometry assertions read the face timeline. Rendering assertions resolve
|
||||
;; the whole clip, including placement and inherited exposure.
|
||||
(defn- face-timeline [c] (clip/timeline c :face-1))
|
||||
(defn- nodes [c] (merge (clip/nodes c) (:nodes (face-timeline c))))
|
||||
(def tl* (delay (face-timeline @clip*)))
|
||||
(def store (delay (:store @frozen)))
|
||||
|
||||
(defn- node [id] (get-in @tl* [:nodes id]))
|
||||
(defn- node [id] (get (nodes @clip*) id))
|
||||
(defn- chan [id path] (get-in (node id) [:channels path]))
|
||||
|
||||
(defn- block-of
|
||||
|
|
@ -57,12 +58,11 @@
|
|||
|
||||
(defn- render
|
||||
"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."
|
||||
the ops from the complete clip, including nested face timelines."
|
||||
[c f]
|
||||
(let [r (raster/make (:width c) (:height c))]
|
||||
(raster/clear! r (get pal/index-of :bg))
|
||||
(raster/draw-ops! r (ops-at (clip/root c) f))
|
||||
(raster/draw-ops! r ((clip/resolver c @store pal/index-of) f))
|
||||
(vec (array-seq (:buf r)))))
|
||||
|
||||
(defn- drawn
|
||||
|
|
@ -86,16 +86,11 @@
|
|||
(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] (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]
|
||||
(timeline/lineage (:nodes @tl*) :mouth-in)))
|
||||
;; Exposure lives on the clip root and inherits strictly.
|
||||
(is (= [:face :root] (timeline/lineage (clip/nodes @clip*) :face)))
|
||||
(is (= :face-1 (get-in @clip* [:timelines :main :nodes :face-1 :of])))
|
||||
(is (= [:head] (timeline/lineage (:nodes @tl*) :head)))
|
||||
(is (= [:mouth :head] (timeline/lineage (:nodes @tl*) :mouth)))
|
||||
(is (= [:mouth-in :mouth :head] (timeline/lineage (:nodes @tl*) :mouth-in)))
|
||||
(is (= {:mode :map :expose 2} (:time (node :root))))
|
||||
(is (every? #(nil? (:time (node %))) [:face :head :mouth :mouth-in])))
|
||||
|
||||
|
|
@ -158,8 +153,9 @@
|
|||
(is (thrown-with-msg?
|
||||
ExceptionInfo #"does not fit the block's fixed point"
|
||||
(freeze/clip (assoc take/params :name "huge")
|
||||
(update @take/measured :outer
|
||||
(fn [rings] (mapv (fn [r] (mapv #(update % :x + 3) r)) rings)))))))
|
||||
{:face-1 (update @take/measured :outer
|
||||
(fn [rings]
|
||||
(mapv (fn [r] (mapv #(update % :x + 3) r)) rings)))}))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; the anchor: three channels, and the inverse
|
||||
|
|
@ -199,14 +195,14 @@
|
|||
;; checking arithmetic against itself; this checks `node/local!`, `node/world!`
|
||||
;; and `emit` as well.
|
||||
(let [c (freeze/head-mode {:mode :free} @frozen)
|
||||
res (timeline/resolver (clip/root c) @store pal/index-of)
|
||||
res (clip/resolver c @store pal/index-of)
|
||||
k (first (:value (chan :face [:xform :scale])))
|
||||
anc (:value (chan :face [:xform :anchor]))
|
||||
pos (:value (chan :face [:xform :pos]))
|
||||
tfs (:transforms @take/measured)]
|
||||
(doseq [f (range 0 take/frames 13)]
|
||||
(let [ops (res f)
|
||||
op (first (filter #(= :mouth (:node %)) ops))
|
||||
op (first (filter #(= [:face-1 :mouth] (:node %)) ops))
|
||||
;; EXPOSURE FIRST. The clip root is on 2s and exposure inherits
|
||||
;; strictly, so frame 13 shows frame 12's pose — which is also the
|
||||
;; cheapest place to assert that the grid is actually being applied,
|
||||
|
|
@ -243,23 +239,23 @@
|
|||
one (freeze/head-mode {:mode :anchored :anchors {0 12}} @frozen)
|
||||
keyed (freeze/head-mode {:mode :anchored
|
||||
:anchors {0 12, 40 88, 150 150}} @frozen)
|
||||
of (fn [c path] (get-in (clip/nodes c) [:head :channels path]))]
|
||||
of (fn [c path] (get-in (nodes c) [:head :channels path]))]
|
||||
(doseq [c [free one keyed]
|
||||
path [[:xform :pos] [:xform :rot] [:xform :scale]]]
|
||||
(is (= :dense (ch/describe (of c path))))
|
||||
(is (= (of free path) (of c path)) "anchor edits do not copy measurements"))
|
||||
(is (nil? (get-in (clip/nodes free) [:head :anchors])))
|
||||
(is (= {0 12} (get-in (clip/nodes one) [:head :anchors])))
|
||||
(is (nil? (get-in (nodes free) [:head :anchors])))
|
||||
(is (= {0 12} (get-in (nodes one) [:head :anchors])))
|
||||
(is (= {0 12, 40 88, 150 150}
|
||||
(get-in (clip/nodes keyed) [:head :anchors])))
|
||||
(get-in (nodes keyed) [:head :anchors])))
|
||||
(is (= keyed (leaf/clip "head" (leaf/leaves "head" keyed)))
|
||||
"anchor source addresses survive the document round trip")))
|
||||
|
||||
(deftest head-anchor-keys-hold-the-whole-measured-transform
|
||||
(let [free (timeline/resolver (clip/root
|
||||
(let [free (timeline/resolver (face-timeline
|
||||
(freeze/head-mode {:mode :free} @frozen))
|
||||
@store pal/index-of)
|
||||
held (timeline/resolver (clip/root
|
||||
held (timeline/resolver (face-timeline
|
||||
(freeze/head-mode {:mode :anchored
|
||||
:anchors {0 12, 40 88}} @frozen))
|
||||
@store pal/index-of)
|
||||
|
|
@ -279,14 +275,14 @@
|
|||
b (freeze/head-mode {:mode :anchored :anchors {0 0}} @frozen)
|
||||
c (freeze/head-mode {:mode :anchored :anchors {0 0, 40 40}} @frozen)]
|
||||
(doseq [x [b c]]
|
||||
(is (= (get (clip/nodes a) :face) (get (clip/nodes x) :face))
|
||||
(is (= (get (nodes a) :face) (get (nodes x) :face))
|
||||
":face moved")
|
||||
(is (= (dissoc (clip/nodes a) :head) (dissoc (clip/nodes x) :head))
|
||||
(is (= (dissoc (nodes a) :head) (dissoc (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 (clip/nodes a) [:head :measured])
|
||||
(get-in (clip/nodes x) [:head :measured])))
|
||||
(is (= (get-in (nodes a) [:head :measured])
|
||||
(get-in (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))))))
|
||||
|
|
@ -331,8 +327,8 @@
|
|||
;; asking for a different stage moves and rescales the same geometry rather than
|
||||
;; re-measuring anything.
|
||||
(let [big (freeze/clip (assoc take/params :stage [640 480] :name "big")
|
||||
@take/measured)
|
||||
key-of (fn [c] (:store (:dense (get-in (clip/nodes c)
|
||||
{:face-1 @take/measured})
|
||||
key-of (fn [c] (:store (:dense (get-in (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
|
||||
|
|
@ -344,7 +340,7 @@
|
|||
(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 (clip/nodes (:clip big))
|
||||
(is (not= (:value (get-in (nodes (:clip big))
|
||||
[:face :channels [:xform :scale]]))
|
||||
(:value (chan :face [:xform :scale]))))))
|
||||
|
||||
|
|
@ -422,14 +418,14 @@
|
|||
(let [gap (set (range 40 60))
|
||||
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))
|
||||
tl (clip/root (:clip c))
|
||||
{:face-1 (assoc @take/measured :presence presence)})
|
||||
tl (face-timeline (:clip c))
|
||||
at (fn [id f]
|
||||
(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 (= [:face-1/eye-r :face-1/eye-l] (get-in (:clip c) [:groups :face-1/eyes :members])))
|
||||
(is (= :face-1/eye-r (get-in (:clip c) [:features :face-1/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))))
|
||||
|
|
@ -460,8 +456,8 @@
|
|||
(range take/frames))]))
|
||||
windows)
|
||||
c (freeze/clip (assoc take/params :name "per-track-gaps")
|
||||
(assoc @take/measured :presence presence))
|
||||
tl (clip/root (:clip c))
|
||||
{:face-1 (assoc @take/measured :presence presence)})
|
||||
tl (face-timeline (:clip c))
|
||||
at (fn [id path f]
|
||||
(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
|
||||
|
|
@ -501,7 +497,8 @@
|
|||
:shown (vec (repeat take/frames true))}
|
||||
clip (fn [nm extra]
|
||||
(freeze/clip (assoc take/params :name nm)
|
||||
(merge (assoc @take/measured :teeth teeth-in) extra)))
|
||||
{:face-1 (merge (assoc @take/measured :teeth teeth-in)
|
||||
extra)}))
|
||||
ref (clip "teeth-reference" nil)
|
||||
occ (clip "teeth-mouth-gap"
|
||||
{:presence {:mouth (mapv #(not (contains? gap %))
|
||||
|
|
@ -509,7 +506,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 (timeline/resolver (clip/root (:clip c)) (:store c) pal/index-of)]
|
||||
(let [r (timeline/resolver (face-timeline (:clip c)) (:store c) pal/index-of)]
|
||||
(fn [f] (into #{} (map :node) (r f)))))
|
||||
ref-at (nodes-at ref)
|
||||
occ-at (nodes-at occ)
|
||||
|
|
@ -519,7 +516,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 (clip/nodes (:clip occ)) [:teeth :stencil]))
|
||||
(is (= :mouth-in (get-in (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")
|
||||
|
|
@ -527,7 +524,7 @@
|
|||
;; 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 (clip/nodes (:clip occ)) [:teeth :channels [:geom :pts]])
|
||||
(ch/value-at (get-in (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)))
|
||||
|
|
@ -544,8 +541,8 @@
|
|||
(let [gap (set (range 40 60))
|
||||
det (mapv #(not (contains? gap %)) (range take/frames))
|
||||
c (freeze/clip (assoc take/params :name "gappy")
|
||||
(assoc @take/measured :detected det))
|
||||
tl (clip/root (:clip c))
|
||||
{:face-1 (assoc @take/measured :detected det)})
|
||||
tl (face-timeline (:clip c))
|
||||
res (timeline/resolver tl (:store c) pal/index-of)]
|
||||
(doseq [f [39 40 50 59 60]]
|
||||
(let [ops (res f)]
|
||||
|
|
@ -604,7 +601,8 @@
|
|||
(let [locked (freeze/head-mode {:mode :anchored :anchors {0 0}} @frozen)
|
||||
shot (fn [f]
|
||||
(let [r (raster/make W H)
|
||||
mouth (filter #(= :mouth (:node %)) (ops-at (clip/root locked) f))]
|
||||
mouth (filter #(= [:face-1 :mouth] (:node %))
|
||||
((clip/resolver locked @store pal/index-of) f))]
|
||||
(raster/clear! r (get pal/index-of :bg))
|
||||
(raster/draw-ops! r mouth)
|
||||
(vec (array-seq (:buf r)))))
|
||||
|
|
|
|||
190
frontend/test/arthur/flow/multi_face_test.cljs
Normal file
190
frontend/test/arthur/flow/multi_face_test.cljs
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
(ns arthur.flow.multi-face-test
|
||||
(:require [cljs.test :refer [deftest is testing]]
|
||||
[arthur.demo.stage :as stage]
|
||||
[arthur.domain.channel :as ch]
|
||||
[arthur.domain.clip :as clip]
|
||||
[arthur.domain.feature :as feature]
|
||||
[arthur.domain.pose :as pose]
|
||||
[arthur.domain.project :as project]
|
||||
[arthur.events.footage :as footage]
|
||||
[arthur.flow.address :as address]
|
||||
[arthur.flow.detect :as detect]
|
||||
[arthur.flow.freeze :as freeze]
|
||||
[arthur.flow.regenerate :as regenerate]
|
||||
[arthur.flow.source :as source]
|
||||
[arthur.flow.take :as take]
|
||||
[arthur.support.ops :as ops]
|
||||
[arthur.synth :as synth]))
|
||||
|
||||
(def frames 40)
|
||||
(def settings
|
||||
(merge take/knobs {:name "two faces" :fps 30 :aspect 1 :stage [320 200]
|
||||
:fit-motion? true :expose 1 :head :anchored :anchors {0 0}
|
||||
:analysis (address/analysis {:detector "synth" :version "two-faces-v1"
|
||||
:seed 9 :frames frames :fps 30 :aspect 1})}))
|
||||
(def inputs
|
||||
(delay
|
||||
(into {} (for [[id seed dx] [[:face-1 9 -0.3] [:face-2 12 0.3]]]
|
||||
[id {:dense (mapv (fn [face] (mapv #(update % :x + dx) face))
|
||||
(synth/synth-dense frames {:seed seed}))
|
||||
:detected (vec (repeat frames true))
|
||||
:crops (vec (repeat frames nil))}]))))
|
||||
(def initial
|
||||
(delay (assoc (take/build settings @inputs) :source-inputs {:subjects @inputs})))
|
||||
|
||||
(defn channel [entry subject node path]
|
||||
(get-in entry [:clip :timelines subject :nodes node :channels path]))
|
||||
(defn snapshot [c store f] (ops/snapshot ((clip/resolver c store) f)))
|
||||
(defn by-node [c store f] (into {} (map (juxt :node identity)) (snapshot c store f)))
|
||||
|
||||
(deftest subjects-share-local-names-without-sharing-blocks
|
||||
(let [{:keys [clip store]} @initial
|
||||
a (get-in clip [:timelines :face-1 :nodes])
|
||||
b (get-in clip [:timelines :face-2 :nodes])]
|
||||
(is (empty? (clip/problems clip)))
|
||||
(is (= (set (keys a)) (set (keys b))))
|
||||
(is (= :head (get-in b [:mouth :parent])))
|
||||
(is (= :eye-r-in (get-in b [:iris-r :stencil])))
|
||||
(is (= :mouth (get-in b [:mouth :pose-group])))
|
||||
(is (= :face-2 (get-in clip [:features :face-2/mouth :timeline])))
|
||||
(doseq [path [[:xform :pos] [:xform :rot] [:xform :scale]]]
|
||||
(is (not= (:store (:dense (get-in a [:head :measured path])))
|
||||
(:store (:dense (get-in b [:head :measured path]))))))
|
||||
(let [drawn (by-node clip store 12)
|
||||
left (get drawn [:face-1 :mouth])
|
||||
right (get drawn [:face-2 :mouth])]
|
||||
(is (seq (:points left)))
|
||||
(is (< (ffirst (:points left)) (ffirst (:points right)))
|
||||
"one shared placement preserves the filmed separation"))
|
||||
(is (some #(re-find #"more than one feature" %)
|
||||
(feature/problems
|
||||
(assoc-in clip [:features :duplicate]
|
||||
(assoc (get-in clip [:features :face-2/mouth]) :id :duplicate)))))))
|
||||
|
||||
(deftest one-subjects-edit-and-anchor-do-not-change-its-neighbor
|
||||
(let [before @initial
|
||||
at [:clip :timelines :face-2 :nodes :iris-r :channels [:style :color]]
|
||||
before (assoc-in before at (ch/framed :brow))
|
||||
after (regenerate/change before {:scope :feature :id :face-2/eye-r
|
||||
:knob :gaze-gain :value 2})
|
||||
anchored (freeze/head-mode {:subject :face-2 :mode :anchored :anchors {0 12}} after)]
|
||||
(is (= (get-in before at) (get-in after at)) "authored channels survive regeneration")
|
||||
(is (= (get-in before [:clip :timelines :face-1])
|
||||
(get-in after [:clip :timelines :face-1])
|
||||
(get-in anchored [:timelines :face-1])))
|
||||
(is (not= (channel before :face-2 :iris-r [:xform :pos])
|
||||
(channel after :face-2 :iris-r [:xform :pos])))
|
||||
(is (= (channel before :face-2 :iris-l [:xform :pos])
|
||||
(channel after :face-2 :iris-l [:xform :pos])))
|
||||
(is (= {0 12} (get-in anchored [:timelines :face-2 :nodes :head :anchors])))
|
||||
(is (empty? (clip/problems anchored)))))
|
||||
|
||||
(deftest the-second-subject-regenerates-inside-a-composed-stage
|
||||
(let [before (update @initial :clip stage/compose)
|
||||
after (regenerate/change before {:scope :subject :id :face-2
|
||||
:knob :anchor-avg :value 4})
|
||||
full (take/build (assoc settings :anchor-avg 4) @inputs)]
|
||||
(is (= (get-in full [:clip :timelines :face-2 :nodes :head :measured])
|
||||
(get-in after [:clip :timelines :face-2 :nodes :head :measured])))
|
||||
(doseq [tid [:main :sym/face-8625 :face-1]]
|
||||
(is (= (get-in before [:clip :timelines tid])
|
||||
(get-in after [:clip :timelines tid]))))
|
||||
(is (empty? (clip/problems (:clip after))))
|
||||
(is (thrown? ExceptionInfo
|
||||
(freeze/head-mode {:subject :face-2 :mode :anchored :anchors {0 frames}}
|
||||
after))
|
||||
"anchors are bounded by the source timeline, even on a longer stage")))
|
||||
|
||||
(deftest an-instance-pose-cut-only-holds-that-faces-mouth
|
||||
(let [{:keys [clip store]} @initial
|
||||
cut (pose/put-cut clip :face-2 :mouth 0 20)
|
||||
original (by-node clip store 4)
|
||||
held (by-node cut store 4)
|
||||
source (by-node clip store 20)]
|
||||
(is (empty? (clip/problems cut)))
|
||||
(is (= (get held [:face-2 :mouth]) (get source [:face-2 :mouth])))
|
||||
(is (not= (get held [:face-2 :mouth]) (get original [:face-2 :mouth])))
|
||||
(doseq [id [[:face-1 :mouth] [:face-1 :eye-r] [:face-2 :eye-r]]]
|
||||
(is (= (get held id) (get original id))))))
|
||||
|
||||
(deftest detection-and-feature-gaps-are-per-subject
|
||||
(let [inputs (-> @inputs
|
||||
(assoc-in [:face-2 :detected 8] false)
|
||||
(assoc-in [:face-2 :presence :eye-r]
|
||||
(assoc (vec (repeat frames true)) 12 false)))
|
||||
{:keys [clip store]} (take/build settings inputs)
|
||||
clip (freeze/head-mode {:mode :free} {:clip clip})
|
||||
at8 (by-node clip store 8)
|
||||
at12 (by-node clip store 12)]
|
||||
(is (some? (get at8 [:face-1 :mouth])))
|
||||
(is (nil? (get at8 [:face-2 :mouth])))
|
||||
(is (nil? (get at12 [:face-2 :eye-r])))
|
||||
(is (nil? (get at12 [:face-2 :iris-r])))
|
||||
(is (some? (get at12 [:face-1 :eye-r])))
|
||||
(is (some? (get at12 [:face-2 :eye-l])))
|
||||
(is (some? (get (by-node clip store 13) [:face-2 :eye-r])))))
|
||||
|
||||
(deftest nested-faces-survive-save-and-resolve-through-the-stage
|
||||
(let [entry (update @initial :clip stage/compose)
|
||||
back (project/load "two" (js/JSON.parse (js/JSON.stringify (project/save "two" entry))))]
|
||||
(is (= (:clip entry) (:clip back)))
|
||||
(is (empty? (clip/problems (:clip back))))
|
||||
(doseq [f [0 12 30 10]]
|
||||
(let [before (snapshot (:clip entry) (:store entry) f)
|
||||
after (snapshot (:clip back) (:store back) f)]
|
||||
(is (seq before))
|
||||
(is (some #(= [:face-2 :mouth] (second (:node %))) before)))
|
||||
(is (= (snapshot (:clip entry) (:store entry) f)
|
||||
(snapshot (:clip back) (:store back) f))))))
|
||||
|
||||
(deftest source-round-trip-keeps-two-faces-with-identical-detection-masks
|
||||
(let [blocks (source/pack-subjects (get-in settings [:analysis :id]) @inputs)
|
||||
wire (source/wire-blocks blocks)
|
||||
back (:subjects (source/unpack (.reverse wire) [320 200]))]
|
||||
(is (= 6 (count (set (source/block-keys blocks)))) )
|
||||
(doseq [subject [:face-1 :face-2]]
|
||||
(is (= (select-keys (get @inputs subject) [:dense :detected])
|
||||
(select-keys (get back subject) [:dense :detected]))))))
|
||||
|
||||
(deftest reordered-detections-and-gaps-retain-their-track
|
||||
(let [a [{:x 0.1 :y 0.5}] b [{:x 0.8 :y 0.5}]
|
||||
frames [[a] [b a] [] [a b]]
|
||||
tracks (detect/tracks frames)]
|
||||
(is (= {:face-1 [0 1 nil 0] :face-2 [nil 0 nil 1]} tracks))
|
||||
(is (= [false true false true]
|
||||
(:detected (detect/fill-gaps (detect/pick frames (:face-2 tracks))))))
|
||||
(is (thrown? ExceptionInfo (detect/tracks [[] []])))))
|
||||
|
||||
(deftest manifest-masks-enter-measurement-with-local-feature-names
|
||||
(let [manifest {:presence {:eye-l [false true]
|
||||
:face-1/eye-r [true false]
|
||||
:face-2/eye-r [false true]}}]
|
||||
(is (= {:eye-l [false true] :eye-r [true false]}
|
||||
(footage/presence-for manifest :face-1)))
|
||||
(is (= {:eye-r [false true]} (footage/presence-for manifest :face-2)))))
|
||||
|
||||
(deftest tracking-settings-participate-in-the-analysis-address
|
||||
(let [manifest {:width 320 :height 200 :frames 40 :fps 30 :source "source"}
|
||||
analysis (take/analysis-for manifest {:version "test"})]
|
||||
(is (= detect/settings (:tracking analysis)))
|
||||
(is (not= (:id analysis) (:id (address/analysis (dissoc analysis :tracking)))))
|
||||
(is (not= (:id analysis)
|
||||
(:id (address/analysis (assoc-in analysis [:tracking :gate] 0.3)))))))
|
||||
|
||||
(deftest the-instance-boundary-preserves-the-single-face-geometry
|
||||
(let [{:keys [clip store]} (take/build settings (select-keys @inputs [:face-1]))
|
||||
flat (assoc-in clip [:timelines :main :nodes]
|
||||
(merge (dissoc (clip/nodes clip) :face-1)
|
||||
(assoc-in (get-in clip [:timelines :face-1 :nodes])
|
||||
[:head :parent] :face)))
|
||||
nested (clip/resolver clip store)
|
||||
reference (clip/resolver flat store)]
|
||||
(doseq [f [0 1 7 20 39]]
|
||||
(let [a (ops/snapshot (nested f)) b (ops/snapshot (reference f))]
|
||||
(is (= (mapv (comp second :node) a) (mapv :node b)))
|
||||
(doseq [[x y] (map vector a b)]
|
||||
(is (= (:kind x) (:kind y)))
|
||||
(doseq [[u v] (map vector
|
||||
(concat (mapcat identity (:points x)) (keep x [:cx :cy :r :size]))
|
||||
(concat (mapcat identity (:points y)) (keep y [:cx :cy :r :size])))]
|
||||
(is (< (abs (- u v)) 1e-8))))))))
|
||||
|
|
@ -21,7 +21,7 @@
|
|||
:analysis (address/analysis
|
||||
{:detector "synth" :version "mulberry32"
|
||||
:seed 9 :frames frames :fps 30 :aspect 1})})]
|
||||
(assoc (take/build p @inputs) :source-inputs @inputs))))
|
||||
(assoc (take/build p {:face-1 @inputs}) :source-inputs {:subjects {:face-1 @inputs}}))))
|
||||
|
||||
(defn- full-at [change]
|
||||
(take/build
|
||||
|
|
@ -30,15 +30,15 @@
|
|||
:expose 1 :head :free
|
||||
:analysis (get-in @initial [:clip :analysis])}
|
||||
change)
|
||||
@inputs))
|
||||
{:face-1 @inputs}))
|
||||
|
||||
(defn- channel [entry node path]
|
||||
(get-in entry [:clip :timelines :main :nodes node :channels path]))
|
||||
(get-in entry [:clip :timelines :face-1 :nodes node :channels path]))
|
||||
|
||||
(deftest eye-rebuild-is-confined-to-the-edited-feature
|
||||
(let [before @initial
|
||||
after (regenerate/change before
|
||||
{:scope :feature :id :eye-r :knob :gaze-gain :value 2})]
|
||||
{:scope :feature :id :face-1/eye-r :knob :gaze-gain :value 2})]
|
||||
(is (not= (channel before :iris-r [:xform :pos])
|
||||
(channel after :iris-r [:xform :pos])))
|
||||
(is (= (channel before :iris-l [:xform :pos])
|
||||
|
|
@ -53,7 +53,7 @@
|
|||
(deftest framed-eye-size-needs-no-new-dense-block
|
||||
(let [before @initial
|
||||
after (regenerate/change before
|
||||
{:scope :feature :id :eye-r :knob :iris-size :value 0.6})]
|
||||
{:scope :feature :id :face-1/eye-r :knob :iris-size :value 0.6})]
|
||||
(is (not= (channel before :iris-r [:geom :radius])
|
||||
(channel after :iris-r [:geom :radius])))
|
||||
(is (= (channel before :iris-l [:geom :radius])
|
||||
|
|
@ -65,11 +65,11 @@
|
|||
(deftest mouth-edit-leaves-the-head-and-eye-alone
|
||||
(let [before @initial
|
||||
after (regenerate/change before
|
||||
{:scope :feature :id :mouth :knob :verts :value 10})]
|
||||
{:scope :feature :id :face-1/mouth :knob :verts :value 10})]
|
||||
(is (not= (channel before :mouth [:geom :pts])
|
||||
(channel after :mouth [:geom :pts])))
|
||||
(is (= (get-in before [:clip :timelines :main :nodes :head])
|
||||
(get-in after [:clip :timelines :main :nodes :head])))
|
||||
(is (= (get-in before [:clip :timelines :face-1 :nodes :head])
|
||||
(get-in after [:clip :timelines :face-1 :nodes :head])))
|
||||
(is (= (channel before :eye-r [:geom :pts])
|
||||
(channel after :eye-r [:geom :pts])))
|
||||
(is (= (channel (full-at {:verts 10}) :mouth [:geom :pts])
|
||||
|
|
@ -80,10 +80,10 @@
|
|||
after (regenerate/change before
|
||||
{:scope :subject :id :face-1 :knob :anchor-avg :value 4})
|
||||
full (full-at {:anchor-avg 4})]
|
||||
(is (not= (get-in before [:clip :timelines :main :nodes :head :measured])
|
||||
(get-in after [:clip :timelines :main :nodes :head :measured])))
|
||||
(is (= (get-in full [:clip :timelines :main :nodes :head :measured])
|
||||
(get-in after [:clip :timelines :main :nodes :head :measured])))
|
||||
(is (not= (get-in before [:clip :timelines :face-1 :nodes :head :measured])
|
||||
(get-in after [:clip :timelines :face-1 :nodes :head :measured])))
|
||||
(is (= (get-in full [:clip :timelines :face-1 :nodes :head :measured])
|
||||
(get-in after [:clip :timelines :face-1 :nodes :head :measured])))
|
||||
(is (= (get-in before [:clip :timelines :main :nodes :face])
|
||||
(get-in after [:clip :timelines :main :nodes :face])))))
|
||||
|
||||
|
|
@ -91,14 +91,14 @@
|
|||
(let [before @initial
|
||||
after (regenerate/change before
|
||||
{:scope :subject :id :face-1 :knob :contour-avg :value 3})]
|
||||
(is (= (get-in before [:clip :timelines :main :nodes :head])
|
||||
(get-in after [:clip :timelines :main :nodes :head])))
|
||||
(is (= (get-in before [:clip :timelines :face-1 :nodes :head])
|
||||
(get-in after [:clip :timelines :face-1 :nodes :head])))
|
||||
(is (= (channel before :teeth [:geom :pts])
|
||||
(channel after :teeth [:geom :pts])))))
|
||||
|
||||
(deftest edited-take-still-round-trips-through-normal-save
|
||||
(let [after (regenerate/change @initial
|
||||
{:scope :group :id :eyes-1 :knob :iris-size :value 0.6})
|
||||
{:scope :group :id :face-1/eyes :knob :iris-size :value 0.6})
|
||||
wire (project/save "regen" after)
|
||||
loaded (project/load "regen" (js/JSON.parse (js/JSON.stringify wire)))]
|
||||
(is (= (:clip after) (:clip loaded)))
|
||||
|
|
@ -172,7 +172,7 @@
|
|||
{:fps 30 :aspect 1 :analysis (get-in @initial [:clip :analysis])}
|
||||
overrides)
|
||||
inputs (assoc @inputs :interior @interior-track)]
|
||||
(freeze/part area p (take/measure-part area p inputs (take/anchor-base p inputs)))))
|
||||
(freeze/part :face-1 area p (take/measure-part area p inputs (take/anchor-base p inputs)))))
|
||||
|
||||
(def ^:private swept
|
||||
"Per area, the knobs whose bytes or channels that area's freeze could plausibly
|
||||
|
|
@ -202,22 +202,9 @@
|
|||
|
||||
;; ---- the stage: a shared symbol behind many placements ----
|
||||
|
||||
(defn- staged
|
||||
"The take composed onto the 8625 stage: its timeline becomes the shared symbol,
|
||||
and seven uuid-keyed placements sit on `:main`.
|
||||
(defn- staged [] (update @initial :clip stage/compose))
|
||||
|
||||
This path had no coverage, and it is the one that differs structurally: `change`
|
||||
re-roots the SYMBOL as the take, edits that, and puts it back, so every
|
||||
assumption `change-take` makes about `:main` holding the tracked nodes is only
|
||||
true of the re-rooted document and not of the stage itself."
|
||||
[]
|
||||
;; The ENTRY with its clip composed, not the composed clip: `change` takes an
|
||||
;; entry (`:clip`, `:store`, `:source-inputs`) and the store is what the
|
||||
;; regenerated blocks merge into.
|
||||
(update @initial :clip stage/compose))
|
||||
|
||||
(defn- sym-channel [entry node path]
|
||||
(get-in entry [:clip :timelines :sym/face-8625 :nodes node :channels path]))
|
||||
(defn- sym-channel [entry node path] (channel entry node path))
|
||||
|
||||
(deftest a-stage-edit-is-previewable-at-all
|
||||
;; The guard `events/project/::preview-settings` bails on, stated here so a
|
||||
|
|
@ -228,11 +215,11 @@
|
|||
"the composed stage keeps the analysis the edit needs")
|
||||
(is (some? (:source-inputs entry)))
|
||||
(testing "and the features still say which timeline they live in"
|
||||
(is (every? #(= :sym/face-8625 (:timeline %))
|
||||
(is (every? #(= :face-1 (:timeline %))
|
||||
(vals (:features (:clip entry))))))))
|
||||
|
||||
(deftest a-stage-edit-plans-the-same-features-as-a-take-edit
|
||||
(let [edit {:scope :feature :id :eye-r :knob :iris-size :value 0.6}]
|
||||
(let [edit {:scope :feature :id :face-1/eye-r :knob :iris-size :value 0.6}]
|
||||
(is (= (:features (regenerate/plan (:clip @initial) edit))
|
||||
(:features (regenerate/plan (:clip (staged)) edit)))
|
||||
"the same knob dirties the same features on a stage as on a take")))
|
||||
|
|
@ -243,7 +230,7 @@
|
|||
;; and `:main` — which holds only placements — must come back untouched.
|
||||
(let [before (staged)
|
||||
after (regenerate/change before
|
||||
{:scope :feature :id :eye-r :knob :iris-size :value 0.6})]
|
||||
{:scope :feature :id :face-1/eye-r :knob :iris-size :value 0.6})]
|
||||
(is (not= (sym-channel before :iris-r [:geom :radius])
|
||||
(sym-channel after :iris-r [:geom :radius]))
|
||||
"the shared drawing is what changed")
|
||||
|
|
@ -261,7 +248,7 @@
|
|||
;; faces off the stage, or dangle the voice links, while looking like a
|
||||
;; successful edit of the drawing.
|
||||
(let [after (regenerate/change (staged)
|
||||
{:scope :feature :id :eye-r :knob :iris-size :value 0.6})
|
||||
{:scope :feature :id :face-1/eye-r :knob :iris-size :value 0.6})
|
||||
nodes (get-in after [:clip :timelines :main :nodes])
|
||||
syms (filter (comp #{:symbol} :kind val) nodes)]
|
||||
(is (= 7 (count syms)))
|
||||
|
|
|
|||
|
|
@ -10,14 +10,14 @@
|
|||
(range (:teeth-verts settings)))
|
||||
measures [{:contour ring :contrast 0.7 :area 42}
|
||||
{:contour nil :contrast 0 :area 0}]
|
||||
block (source/interior-block "sha256:analysis" settings measures)
|
||||
block (source/interior-block "sha256:analysis" :face-1 settings measures)
|
||||
response #js {:descriptor (:descriptor block)
|
||||
:data (wire/base64 (:data block))}]
|
||||
(is (= (:key block)
|
||||
(source/interior-key "sha256:analysis" settings 2)))
|
||||
(source/interior-key "sha256:analysis" :face-1 settings 2)))
|
||||
(is (= measures (source/unpack-interior response settings 2)))
|
||||
(is (not= (:key block)
|
||||
(source/interior-key "sha256:analysis"
|
||||
(source/interior-key "sha256:analysis" :face-1
|
||||
(assoc settings :cavity-erode 0.2) 2)))))
|
||||
|
||||
(deftest source-blocks-round-trip-without-source-images
|
||||
|
|
@ -25,8 +25,9 @@
|
|||
pixels (js/Uint8ClampedArray. #js [12 24 36 255])
|
||||
input {:dense [face face] :detected [true false]
|
||||
:crops [{:box {:x 3 :y 4 :w 1 :h 1} :data pixels} nil]}
|
||||
blocks (source/pack "sha256:analysis" input)
|
||||
out (source/unpack (source/wire-blocks blocks) [100 80])]
|
||||
blocks (source/pack "sha256:analysis" :face-1 input)
|
||||
out (get-in (source/unpack (source/wire-blocks {:face-1 blocks}) [100 80])
|
||||
[:subjects :face-1])]
|
||||
(is (= (set source/roles) (set (keys blocks))))
|
||||
(is (= (:dense input) (:dense out)))
|
||||
(is (= (:detected input) (:detected out)))
|
||||
|
|
@ -36,20 +37,20 @@
|
|||
(vec (array-seq (:data (first (:crops out)))))))
|
||||
(is (nil? (second (:crops out))))
|
||||
(is (= (mapv :key (vals blocks))
|
||||
(mapv :key (vals (source/pack "sha256:analysis" out)))))))
|
||||
(mapv :key (vals (source/pack "sha256:analysis" :face-1 out)))))))
|
||||
|
||||
(deftest fresh-source-includes-its-pixel-measurement-block
|
||||
(let [face (vec (repeat 478 {:x 0.25 :y 0.5 :z -0.125}))
|
||||
input {:dense [face] :detected [false] :crops [nil]
|
||||
:interior [{:contour nil :contrast 0 :area 0}]
|
||||
:interior-settings params/defaults}
|
||||
blocks (source/pack "sha256:analysis" input)]
|
||||
blocks (source/pack "sha256:analysis" :face-1 input)]
|
||||
(is (contains? blocks "source/interior"))
|
||||
(is (= 4 (.-length (source/upload-blocks blocks))))
|
||||
(is (= 4 (.-length (source/upload-blocks {:face-1 blocks}))))
|
||||
(is (= 3 (count source/roles)))
|
||||
(testing "and cannot be packed without the settings it was measured at"
|
||||
;; The block is addressed BY those knobs. Defaulting them would name it
|
||||
;; after settings its bytes did not come from, which is a key that lies.
|
||||
(is (thrown-with-msg?
|
||||
ExceptionInfo #"needs the settings"
|
||||
(source/pack "sha256:analysis" (dissoc input :interior-settings)))))))
|
||||
(source/pack "sha256:analysis" :face-1 (dissoc input :interior-settings)))))))
|
||||
|
|
|
|||
|
|
@ -254,6 +254,7 @@ async function main() {
|
|||
await sleep(120);
|
||||
const open = await page.eval(PROBE);
|
||||
await page.shot('take-open');
|
||||
check(open.toneSet.includes(0x171a22), 'nested face pupils reach the rasterizer');
|
||||
check(open.toneSet.includes(MOUTH_DARK),
|
||||
'an open mouth draws an outline and an interior',
|
||||
`${open.tones} tones, interior ${open.toneSet.includes(MOUTH_DARK) ? 'present' : 'MISSING'}`);
|
||||
|
|
@ -493,13 +494,9 @@ async function main() {
|
|||
check(stageLoaded !== null, 'the 8625 stage is ready', stageLoaded ?? (await page.eval(STATUS)));
|
||||
const eyeSelected = await page.eval(`(() => {
|
||||
const select = document.querySelector('.controls select');
|
||||
// The instance prefix is the placement's NAME ("8625 left"), not its id:
|
||||
// a placement is keyed by a uuid now, and a uuid is not something to show
|
||||
// anyone. So this matches the feature and requires SOME instance prefix,
|
||||
// rather than pinning the label a rename is free to change.
|
||||
// Settings belong to tracked features shared by the stage placements.
|
||||
const option = [...select.options]
|
||||
.find(o => o.textContent.trim().endsWith('/ feature · eye-r')
|
||||
&& o.textContent.includes(' / '));
|
||||
.find(o => o.textContent.trim().endsWith('feature · face-1/eye-r'));
|
||||
if (!option) return false;
|
||||
select.value = option.value;
|
||||
select.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
|
|
@ -533,7 +530,7 @@ async function main() {
|
|||
const preview = await statusMatching(/preview · unsaved/, 160);
|
||||
const debug = await page.eval(`document.querySelector('.regeneration-debug')?.textContent ?? ''`);
|
||||
check(preview !== null, 'the slider updates the stage preview', preview ?? debug);
|
||||
check(debug.includes(':eye-r') && debug.includes('tier 1 only'),
|
||||
check(debug.includes(':face-1/eye-r') && debug.includes('tier 1 only'),
|
||||
'the panel reports the affected feature and tier', debug);
|
||||
const endFrame = await page.eval(`Number(document.querySelector('.readout span').textContent.match(/\\d+/)[0])`);
|
||||
check(endFrame > startFrame, 'playback continues during tuning', `${startFrame} -> ${endFrame}`);
|
||||
|
|
@ -581,7 +578,7 @@ async function main() {
|
|||
if (debug.includes('head-pos')) break;
|
||||
await sleep(250);
|
||||
}
|
||||
check(debug.includes('head-pos') && debug.includes(':teeth'),
|
||||
check(debug.includes('head-pos') && debug.includes(':face-1/teeth'),
|
||||
'anchor invalidates the head and teeth', debug);
|
||||
const preview = await statusMatching(/preview · unsaved/, 160);
|
||||
check(preview !== null, 'the anchor edit finishes previewing',
|
||||
|
|
@ -591,8 +588,7 @@ async function main() {
|
|||
const teethSelected = await page.eval(`(() => {
|
||||
const select = document.querySelector('.controls select');
|
||||
const option = [...select.options]
|
||||
.find(o => o.textContent.trim().endsWith('/ feature · teeth')
|
||||
&& o.textContent.includes(' / '));
|
||||
.find(o => o.textContent.trim().endsWith('feature · face-1/teeth'));
|
||||
if (!option) return false;
|
||||
select.value = option.value;
|
||||
select.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
|
|
@ -627,10 +623,10 @@ async function main() {
|
|||
let debug = '';
|
||||
for (let i = 0; i < 160; i++) {
|
||||
debug = await page.eval(`document.querySelector('.regeneration-debug')?.textContent ?? ''`);
|
||||
if (debug.includes('dirty features: [:teeth]')) break;
|
||||
if (debug.includes('dirty features: [:face-1/teeth]')) break;
|
||||
await sleep(250);
|
||||
}
|
||||
check(debug.includes('dirty features: [:teeth]'),
|
||||
check(debug.includes('dirty features: [:face-1/teeth]'),
|
||||
'the pixel setting invalidates only teeth', debug);
|
||||
const preview = await statusMatching(/preview · unsaved/, 160);
|
||||
check(preview !== null, 'the teeth edit finishes previewing',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue