diff --git a/clips/admin.py b/clips/admin.py index 8b7add7..9e73270 100644 --- a/clips/admin.py +++ b/clips/admin.py @@ -18,7 +18,7 @@ class ProjectAdmin(admin.ModelAdmin): @admin.register(Clip) class ClipAdmin(admin.ModelAdmin): - list_display = ("cid", "project", "name") + list_display = ("cid", "project", "name", "footage", "analysis") list_filter = ("project",) diff --git a/clips/migrations/0014_multiple_analyses.py b/clips/migrations/0014_multiple_analyses.py deleted file mode 100644 index 97fe72b..0000000 --- a/clips/migrations/0014_multiple_analyses.py +++ /dev/null @@ -1,15 +0,0 @@ -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [("clips", "0013_palette_track")] - - operations = [ - migrations.RemoveField(model_name="clip", name="analysis"), - migrations.RemoveField(model_name="clip", name="footage"), - migrations.AlterField( - model_name="project", - name="schema_version", - field=models.PositiveIntegerField(default=5), - ), - ] diff --git a/clips/models.py b/clips/models.py index 9eeb4b3..f304b71 100644 --- a/clips/models.py +++ b/clips/models.py @@ -235,7 +235,7 @@ class Project(models.Model): settings.AUTH_USER_MODEL, blank=True, related_name="shared_projects", ) name = models.CharField(max_length=200, default="untitled") - schema_version = models.PositiveIntegerField(default=5) + schema_version = models.PositiveIntegerField(default=4) seq = models.PositiveBigIntegerField(default=0) palette = models.CharField(max_length=64, default="arthur/default") created = models.DateTimeField(auto_now_add=True) @@ -273,6 +273,12 @@ class Clip(models.Model): cid = models.SlugField(max_length=64) name = models.CharField(max_length=200, blank=True) order = models.IntegerField(default=0) + footage = models.ForeignKey( + Footage, null=True, blank=True, on_delete=models.SET_NULL, related_name="clips" + ) + analysis = models.ForeignKey( + Analysis, null=True, blank=True, on_delete=models.SET_NULL, related_name="clips" + ) blocks = models.ManyToManyField( Block, blank=True, related_name="clips", help_text="the tier-2 blocks this clip's channels name", diff --git a/clips/tests/test_api.py b/clips/tests/test_api.py index 26e8e73..68f474f 100644 --- a/clips/tests/test_api.py +++ b/clips/tests/test_api.py @@ -396,25 +396,14 @@ class DocumentTests(TestCase): ], } - def save(self, leaves=None, blocks=None, analyses=None): + def save(self, leaves=None, blocks=None): return self.put(f"/api/projects/{self.project.id}", { "name": "a project", - "clips": [{"cid": "c1", "name": "take", - "analyses": [self.analysis] if analyses is None else analyses, + "clips": [{"cid": "c1", "name": "take", "analysis": self.analysis, "leaves": leaves if leaves is not None else self.leaves(), "blocks": blocks if blocks is not None else [self.block]}], }) - def test_a_clip_declares_the_registered_analyses_its_blocks_name(self): - undeclared = self.save(analyses=[]) - self.assertEqual(409, undeclared.status_code) - self.assertIn("every block", undeclared.json()["error"]) - - unknown = "sha256:" + "f" * 64 - missing = self.save(analyses=[self.analysis, unknown]) - self.assertEqual(409, missing.status_code) - self.assertEqual([unknown], missing.json()["missing"]) - def test_every_saved_symbol_is_listed_across_projects(self): leaves = self.leaves() leaves["clip/c1/symbol/sym~face"] = ["^ ", "~:name", "face", "~:frames", 12] @@ -446,12 +435,12 @@ class DocumentTests(TestCase): self.assertEqual(5, len(response.json()["written"])) loaded = self.client.get(f"/api/projects/{self.project.id}").json() - self.assertEqual(5, loaded["schema_version"]) + self.assertEqual(4, loaded["schema_version"]) self.assertEqual(1, len(loaded["clips"])) clip = loaded["clips"][0] self.assertEqual("c1", clip["cid"]) self.assertEqual([self.block], clip["blocks"]) - self.assertNotIn("analysis", clip) + self.assertEqual(self.analysis, clip["analysis"]) # The whole point: byte-identical values, including the integer frame keys # transit writes as "~i0". A JSON round trip that stringified them would # come back "0" and the part would hold its first pose forever. @@ -553,7 +542,7 @@ class DocumentTests(TestCase): def patch(self, base, leaves, removed=()): return self.put(f"/api/projects/{self.project.id}", { "base": base, - "clips": [{"cid": "c1", "analyses": [self.analysis], "leaves": leaves, + "clips": [{"cid": "c1", "analysis": self.analysis, "leaves": leaves, "removed": list(removed), "blocks": [self.block]}], }) diff --git a/clips/views.py b/clips/views.py index e88dff8..4092a0e 100644 --- a/clips/views.py +++ b/clips/views.py @@ -718,6 +718,8 @@ def _project_json(project: Project, user): { "cid": clip.cid, "name": clip.name, + "footage": str(clip.footage_id) if clip.footage_id else None, + "analysis": clip.analysis_id, "blocks": sorted(clip.blocks.values_list("key", flat=True)), "leaves": {leaf.path: leaf.value for leaf in leaves if leaf.path.startswith(prefix)}, } @@ -907,19 +909,7 @@ def _save(project: Project, data, user): ) keys = spec.get("blocks") or [] - analyses = spec.get("analyses") or [] - if (not isinstance(analyses, list) - or not all(isinstance(key, str) for key in analyses) - or len(analyses) != len(set(analyses))): - raise Bad("a clip's analyses must be a list of distinct analysis ids") - registered = set(Analysis.objects.filter(key__in=analyses) - .values_list("key", flat=True)) - if unknown := [key for key in analyses if key not in registered]: - raise Bad("this clip names analyses the server does not know; register them first", - status=409, missing=unknown) - - block_rows = list(Block.objects.filter(key__in=keys)) - have = {block.key for block in block_rows} + have = set(Block.objects.filter(key__in=keys).values_list("key", flat=True)) if missing := [k for k in keys if k not in have]: # Referential integrity across the tiers, enforced where it can be: # a document that names blocks the server does not hold would load @@ -929,9 +919,6 @@ def _save(project: Project, data, user): "before saving the document that points at them", status=409, missing=missing, ) - if undeclared := sorted({block.analysis_id for block in block_rows} - registered): - raise Bad("every block in a clip must name one of that clip's analyses", - status=409, missing=undeclared) existing = {leaf.path: leaf for leaf in project.leaves.filter(path__startswith=prefix)} if base is not None: @@ -943,12 +930,16 @@ def _save(project: Project, data, user): if conflicts: continue + analysis = Analysis.objects.filter(key=spec.get("analysis")).first() + footage = None + if spec.get("footage"): + footage = Footage.objects.filter(id=spec["footage"]).first() clip, _ = Clip.objects.update_or_create( project=project, cid=cid, - defaults={"name": spec.get("name") or ""}, + defaults={"name": spec.get("name") or "", "analysis": analysis, "footage": footage}, ) - blocks = block_rows + blocks = Block.objects.filter(key__in=keys) if base is None: clip.blocks.set(blocks) gone = [path for path in existing if path not in leaves] diff --git a/docs/creating-in.md b/docs/creating-in.md index 8d5a8dd..ad03dca 100644 --- a/docs/creating-in.md +++ b/docs/creating-in.md @@ -72,12 +72,6 @@ Once resolved, every creation command follows the destination kind. - Selecting an existing cel changes the destination from the lane to the symbol placed by that cel; subsequent symbols and shapes become children there. -Double-clicking a lane creates an empty cel at the playhead; beginning a drawing -creates a drawing cel there. These are the same lane-creation operation with -different payloads. The pointer chooses the lane, never a second creation time. -The resulting cel is selected, so it immediately becomes the preferred target: -drawing again enters that cel's symbol instead of replacing it. - Thus no separate "new cel" versus "add inside" mode is needed. Selecting the lane header says new cel; selecting a cel says add inside. diff --git a/frontend/src/arthur/domain/bring.cljs b/frontend/src/arthur/domain/bring.cljs index 97a18b5..166ca51 100644 --- a/frontend/src/arthur/domain/bring.cljs +++ b/frontend/src/arthur/domain/bring.cljs @@ -11,7 +11,6 @@ so the events that fetch them are only fetching." (:refer-clojure :exclude [take]) (:require [arthur.domain.clip :as clip] - [arthur.domain.feature :as feature] [arthur.domain.node :as node] [clojure.string :as string])) @@ -60,8 +59,8 @@ (keyword (if (seq slug) slug "symbol")))) (defn take - "Put `frozen`, a take, into `clip` as ONE symbol called `label`. Returns the - changed clip, the imported symbol id and the old-to-scoped subject ids. + "Put `frozen`, a take, into `clip` as ONE symbol called `label`. Returns + `{:clip :sid :tracked?}`. `frozen` is what `flow/freeze/clip` makes: a `:main` that places one symbol per tracked face. `:main` becomes the named symbol — it is what holds the faces in @@ -76,22 +75,18 @@ symbols. `nest/audio-tracks` collapses simultaneous copies carrying the same `:media-link`; placing those faces at different times still schedules each one. - The unique imported symbol id scopes every subject id. Thus two takes may both - arrive with detector subject `:face-1` without colliding in the document." + The tracking identities, and the analysis they were measured by, come along + only when `clip` has no analysis of its own and no face had to be renamed. A + document holds one analysis, and regeneration finds a face's symbol by its + subject id, so either condition failing means the take comes in as drawings + that play but cannot be re-tuned — `:tracked? false` says so." [clip frozen label footage-id range] - (let [scope (fn [root subject] (keyword (str (name root) "." (name subject)))) - root (clip/free-id - (fn [candidate] - (or (contains? (:symbols clip) candidate) - (some #(contains? (:symbols clip) (scope candidate %)) - (keys (:subjects frozen))))) - (symbol-id label)) - scoped (partial scope root) - wanted (into {:main root} (map (fn [subject] [subject (scoped subject)])) - (keys (:subjects frozen))) - {c :clip ids :ids} (symbols clip frozen [:main] wanted) + (let [{c :clip ids :ids} (symbols clip frozen [:main] {:main (symbol-id label)}) sid (ids :main) faces (mapv ids (sort-by str (keys (:subjects frozen)))) + ;; Older/plain imported clips have no subject table. They are still a + ;; take, so their wrapper remains the only honest owner of the sound. + sound-hosts (if (seq faces) faces [sid]) sound {:id :sound :name "sound" :kind :audio :parent nil :z "z-sound" :source {:footage footage-id} ;; All automatic copies name one recording. The audio walk @@ -101,42 +96,18 @@ ;; Source frame `start` plays on the symbol's 0. :span range :time {:mode :map :at (- (first range)) :rate 1}} - feature-ids (into {} - (map (fn [[id f]] - [id (feature/owned (ids (:subject f)) (keyword (name id)))])) - (:features frozen)) - subjects (into {} - (map (fn [[id subject]] - (let [new-id (ids id)] - [new-id (assoc subject :id new-id - :footage footage-id)]))) - (:subjects frozen)) - features (into {} - (map (fn [[id f]] - (let [new-id (feature-ids id)] - [new-id (-> f - (assoc :id new-id - :subject (ids (:subject f)) - :symbol (ids (:symbol f))))]))) - (:features frozen)) - groups (into {} - (map (fn [[id g]] - (let [new-subject (ids (:subject g)) - new-id (feature/owned new-subject (keyword (name id)))] - [new-id (-> g - (assoc :id new-id :subject new-subject) - (update :members #(mapv feature-ids %)))]))) - (:groups frozen))] + tracked? (and (nil? (:analysis clip)) + (every? #(= % (ids %)) (keys (:subjects frozen))))] {:sid sid - :subject-ids (select-keys ids (keys (:subjects frozen))) - :clip (-> (reduce (fn [document face] - (assoc-in document [:symbols face :nodes :sound] sound)) - c faces) - (assoc-in [:symbols sid :name] (str label)) - (update :analyses merge (:analyses frozen)) - (update :subjects merge subjects) - (update :features merge features) - (update :groups merge groups))})) + :tracked? tracked? + :clip (cond-> (-> (reduce (fn [document face] + (assoc-in document [:symbols face :nodes :sound] sound)) + c sound-hosts) + (assoc-in [:symbols sid :name] (str label))) + tracked? (-> (assoc :analysis (:analysis frozen)) + (update :subjects merge (:subjects frozen)) + (update :features merge (:features frozen)) + (update :groups merge (:groups frozen))))})) (defn placed diff --git a/frontend/src/arthur/domain/channel.cljs b/frontend/src/arthur/domain/channel.cljs index 6ce70f8..9864b41 100644 --- a/frontend/src/arthur/domain/channel.cljs +++ b/frontend/src/arthur/domain/channel.cljs @@ -364,11 +364,7 @@ ;; Palette-choice channels interpolate identities into a blend ;; descriptor. The renderer keeps indexed geometry in the left ;; palette's bank and blends that bank's ramp toward the right one. - ;; Palette ids are opaque identities. Built-ins happen to use - ;; keywords, while palettes made in the editor use UUIDs; treating - ;; the latter as numbers produces NaN and therefore the renderer's - ;; pink bad-data sentinel. - (and (= :palette (:semantic ch)) (some? a) (some? b)) + (and (= :palette (:semantic ch)) (keyword? a) (keyword? b)) {:from a :to b :t t} (vector? a) (mapv (fn [x y] (+ x (* t (- y x)))) a b) @@ -524,7 +520,7 @@ (let [values (when (map? (:keys ch)) (vals (:keys ch))) first-value (first values) linear-values? (or (every? number? values) - (and (= :palette (:semantic ch)) (every? some? values)) + (and (= :palette (:semantic ch)) (every? keyword? values)) (and (vector? first-value) (pos? (count first-value)) (every? (fn [v] (and (vector? v) diff --git a/frontend/src/arthur/domain/clip.cljs b/frontend/src/arthur/domain/clip.cljs index 39d15a2..6dd8cfe 100644 --- a/frontend/src/arthur/domain/clip.cljs +++ b/frontend/src/arthur/domain/clip.cljs @@ -4,7 +4,7 @@ {:name \"take\" :fps 30 :width 320 :height 200 - :analyses {analysis-id {...}} + :analysis {...} :subjects {...} :features {...} :groups {...} :symbols {:main {:id :main :frames 229 :nodes {...}}}} @@ -51,7 +51,7 @@ that loses something on every round trip, which is the one bug a persistence layer must not be able to have. Add the field here and to `leaf/leaves` and `leaf/clip` in the same commit." - #{:name :fps :analyses :subjects :features :groups :width :height :symbols + #{:name :fps :analysis :subjects :features :groups :width :height :symbols :palettes :default-palette}) (defn symbol @@ -264,16 +264,7 @@ ;; A palette track has meaningful uncovered time. Ordinary held ;; channels clamp to their first key before it, but doing that ;; here would erase the gap before the first palette segment. - (let [fallback (or (channel-value (:palette owner) frame) - inherited (:default palette)) - materialize (fn [choice] - (cond - (= pal/inherit choice) fallback - (map? choice) (-> choice - (update :from #(if (= pal/inherit %) fallback %)) - (update :to #(if (= pal/inherit %) fallback %))) - :else choice)) - track (:palette-channel owner) + (let [track (:palette-channel owner) track-value (if-let [ks (:keys track)] (some->> (keys ks) (filter #(<= % frame)) @@ -295,10 +286,12 @@ choice (get-in palette-clip [:channels [:palette]]) start (some-> palette-clip node/placed-span first)] (or (when (and choice start) - (materialize (channel-value choice (- frame start)))) + (channel-value choice (- frame start))) fallback))) track-value - fallback))) + (channel-value (:palette owner) frame) + inherited + (:default palette)))) (selection-at [owner frame inherited] (let [selection (:palette owner) chosen (cond @@ -581,18 +574,6 @@ (str "clip has a field with no leaf to save it in: " (pr-str k))) (when-not (map? (:symbols clip)) [":symbols must be a map of id -> symbol"]) - (when (and (contains? clip :analyses) (not (map? (:analyses clip)))) - [":analyses must be a map of analysis id -> analysis"]) - (for [[id analysis] (:analyses clip) - :when (not= id (:id analysis))] - (str "analysis under key " (pr-str id) " has :id " (pr-str (:id analysis)))) - (for [[id subject] (:subjects clip) - :when (not (contains? (:analyses clip) (:analysis subject)))] - (str "subject " (pr-str id) " names missing analysis " - (pr-str (:analysis subject)))) - (for [[id subject] (:subjects clip) - :when (not (keyword? (:source-subject subject)))] - (str "subject " (pr-str id) " has no source subject")) (when (and (contains? clip :palettes) (not (map? (:palettes clip)))) [":palettes must be a map of id -> palette"]) (when (and (contains? clip :default-palette) (map? (:palettes clip)) diff --git a/frontend/src/arthur/domain/creation.cljs b/frontend/src/arthur/domain/creation.cljs index 49660b0..ec1f3ca 100644 --- a/frontend/src/arthur/domain/creation.cljs +++ b/frontend/src/arthur/domain/creation.cljs @@ -6,39 +6,20 @@ is actually available. See `docs/creating-in.md`." (:require [arthur.domain.clip :as clip] [arthur.domain.nest :as nest] - [arthur.domain.node :as node] [arthur.domain.symbol :as symbol])) -(defn- path-node - "The node at the end of an occurrence `path`, walked from `open`. - - Selection also carries an owner sid and node id for commands that edit the - node directly. Those are deliberately not used here: the path is the address - of the row as seen from the open symbol, and is the only part that describes - every enclosing occurrence at arbitrary depth." - [document open path] - (loop [sid open [id & more] (seq path) found nil] - (if-not id - found - (when-let [n (get-in document [:symbols sid :nodes id])] - (if (seq more) - (when-let [inner (and (= :instance (:kind n)) (node/source n))] - (recur inner more n)) - n))))) - (defn preferred-path "The container path structurally implied by `selection`. Selecting an instance means inside it. Selecting any other node means its containing symbol. An empty or non-node selection means the open symbol." - [document open selection] - (let [[kind _sid id selected-path] selection + [document selection] + (let [[kind sid id selected-path] selection path (when (= :node kind) - (vec (or (seq selected-path) (when id [id])))) - selected (path-node document open path)] + (vec (or (seq selected-path) (when id [id]))))] (cond (empty? path) [] - (= :instance (:kind selected)) path + (= :instance (get-in document [:symbols sid :nodes id :kind])) path :else (vec (butlast path))))) (defn target @@ -52,7 +33,7 @@ Returns `{:kind :lane|:symbol :sid :path :frame :matrix :time}`." [document store open selection frame] - (let [preferred (preferred-path document open selection)] + (let [preferred (preferred-path document selection)] (some (fn [path] (when-let [inside (nest/inside document store open path frame)] (when-let [sid (:sid inside)] diff --git a/frontend/src/arthur/domain/leaf.cljs b/frontend/src/arthur/domain/leaf.cljs index e47accf..714d12c 100644 --- a/frontend/src/arthur/domain/leaf.cljs +++ b/frontend/src/arthur/domain/leaf.cljs @@ -137,7 +137,7 @@ (some-leaf (at "timing") (select-keys clip [:fps])) (some-leaf (at "stage") (select-keys clip [:width :height])) (some-leaf (at "palette-default") (select-keys clip [:default-palette])) - (some-leaf (at "analyses") (:analyses clip)) + (some-leaf (at "source") (:analysis clip)) (concat (for [[id v] (:subjects clip)] {(at "subject" (segment id)) v}) (for [[id v] (:features clip)] {(at "feature" (segment id)) v}) @@ -194,7 +194,7 @@ "name" (merge acc v) "timing" (merge acc v) "stage" (merge acc v) - "analyses" (assoc acc :analyses v) + "source" (assoc acc :analysis v) "palette-default" (merge acc v) "palette" (assoc-in acc [:palettes (unsegment a)] v) "subject" (assoc-in acc [:subjects (unsegment a)] v) @@ -237,7 +237,7 @@ false) (case (count p) ;; The clip's own facts carry no id. - 3 (#{"name" "timing" "stage" "analyses" "palette-default"} (nth p 2)) + 3 (#{"name" "timing" "stage" "source" "palette-default"} (nth p 2)) 4 (#{"subject" "feature" "group" "palette"} (nth p 2)) false))))] (vec diff --git a/frontend/src/arthur/domain/node.cljs b/frontend/src/arthur/domain/node.cljs index 34d4775..0f2a2fb 100644 --- a/frontend/src/arthur/domain/node.cljs +++ b/frontend/src/arthur/domain/node.cljs @@ -99,30 +99,15 @@ (or (:dense c) (:generated c))) [:pos :rot :scale]))) -(defn hold-only? - "Channels whose values are choices, not quantities. Their keys may change at - a frame boundary but there is no meaningful value between two keys." - [path value] - (or (= path [:style :color]) - (boolean? value) - (and (keyword? value) (not= path [:palette])))) - -(defn- enforce-hold [path c] - (if (and (= path [:style :color]) (:keys c)) - (-> c (assoc :interp :hold) (dissoc :segments)) - c)) - (defn set-channel "Write `v` into channel `path`: a key on the node's own frame `f` when the channel is keyed, its one value when it is not." [n path f v] (let [c (get (channels n) path)] (assoc-in n [:channels path] - (enforce-hold - path - (if (:keys c) - (assoc-in c [:keys f] v) - (merge (select-keys c [:semantic]) (ch/framed v))))))) + (if (:keys c) + (assoc-in c [:keys f] v) + (merge (select-keys c [:semantic]) (ch/framed v)))))) (defn set-keyed-channel "Write `v` as a key at `f`, starting an animated channel when needed. This is @@ -131,12 +116,10 @@ [n path f v] (let [c (get (channels n) path)] (assoc-in n [:channels path] - (enforce-hold - path - (if (:keys c) - (assoc-in c [:keys f] v) - (merge (select-keys c [:semantic]) - (ch/keyed {f v} (if (hold-only? path v) :hold :linear)))))))) + (if (:keys c) + (assoc-in c [:keys f] v) + (merge (select-keys c [:semantic]) + (ch/keyed {f v} (if (or (boolean? v) (keyword? v)) :hold :linear))))))) (defn toggle-key "Key channel `path` on the node's own frame `f` with the value it has there, or @@ -144,22 +127,19 @@ the last one off leaves it that one value. A boolean holds; anything else tweens. `store` because the value it keys is read out of the channel, and a measured - channel's values live in tier 2. Colour is always held even though current - documents store palette choices as numeric slot indices." + channel's values live in tier 2." [n path f store] (let [c (get (channels n) path) v (ch/value-at c f store) ks (dissoc (:keys c) f)] (assoc-in n [:channels path] - (enforce-hold - path - (cond - (not (:keys c)) (merge (select-keys c [:semantic]) - (ch/keyed {f v} (if (hold-only? path v) :hold :linear))) - (not (contains? (:keys c) f)) (assoc-in c [:keys f] v) - (seq ks) (cond-> (assoc c :keys ks) - (:segments c) (update :segments dissoc f)) - :else (ch/framed v)))))) + (cond + (not (:keys c)) (merge (select-keys c [:semantic]) + (ch/keyed {f v} (if (or (boolean? v) (keyword? v)) :hold :linear))) + (not (contains? (:keys c) f)) (assoc-in c [:keys f] v) + (seq ks) (cond-> (assoc c :keys ks) + (:segments c) (update :segments dissoc f)) + :else (ch/framed v))))) (defn set-segment-interp "Choose how channel `path`'s key at `left` leads to the next one: `:hold` cuts @@ -167,9 +147,7 @@ only a gap that exists, between a key and a later one, can be chosen." [n path left interp] (let [ks (:keys (get (channels n) path))] - (if (and (contains? ks left) (some #(< left %) (keys ks)) - (#{:hold :linear} interp) - (or (= :hold interp) (not (hold-only? path nil)))) + (if (and (contains? ks left) (some #(< left %) (keys ks)) (#{:hold :linear} interp)) (assoc-in n [:channels path :segments left] interp) n))) diff --git a/frontend/src/arthur/domain/palette.cljs b/frontend/src/arthur/domain/palette.cljs index ca9e6b8..2669fdb 100644 --- a/frontend/src/arthur/domain/palette.cljs +++ b/frontend/src/arthur/domain/palette.cljs @@ -7,7 +7,6 @@ are derived, never persisted: adding a palette never rewrites drawing data.") (def default-id :arthur/default) -(def inherit :arthur.palette/inherit) (def entries [{:name :bg :hex "#12141c"} diff --git a/frontend/src/arthur/domain/project.cljs b/frontend/src/arthur/domain/project.cljs index 38920a7..5c5afc1 100644 --- a/frontend/src/arthur/domain/project.cljs +++ b/frontend/src/arthur/domain/project.cljs @@ -29,7 +29,9 @@ (:require [arthur.domain.leaf :as leaf] [arthur.domain.wire :as wire])) -(def schema-version 5) +(def schema-version + "4 separates a symbol's static authoring palette from its palette track." + 4) (defn block-keys "Every tier-2 key a leaf map names, in a stable order." diff --git a/frontend/src/arthur/domain/symbol.cljs b/frontend/src/arthur/domain/symbol.cljs index c407451..0ba6622 100644 --- a/frontend/src/arthur/domain/symbol.cljs +++ b/frontend/src/arthur/domain/symbol.cljs @@ -730,7 +730,9 @@ (-> [] (cond-> (and (= :palette (:type sym)) (seq nodes)) - (conj "a palette symbol cannot contain nodes")) + (conj "a palette symbol cannot contain nodes") + (and (= :palette (:type sym)) (nil? (:palette-ref sym))) + (conj "a palette symbol must point at a palette")) (into (for [[id n] nodes :when (not= id (:id n))] (str "node under key " (pr-str id) " has :id " (pr-str (:id n))))) diff --git a/frontend/src/arthur/events/footage.cljs b/frontend/src/arthur/events/footage.cljs index 3a7670c..bc2e80b 100644 --- a/frontend/src/arthur/events/footage.cljs +++ b/frontend/src/arthur/events/footage.cljs @@ -151,8 +151,7 @@ :detector detector}) _ (mark! "build-clip: freeze") built (:clip frozen) - analysis-id (-> built :analyses keys first) - source-blocks (source/pack-subjects analysis-id subjects) + source-blocks (source/pack-subjects (:id (:analysis built)) subjects) _ (mark! "build-clip: pack source blocks")] (assoc (select-keys built [:fps :width :height]) @@ -395,10 +394,7 @@ (rf/reg-event-db ::choose - (fn [db [_ id]] - (-> db - (assoc-in [:footage :chosen] id) - (ui/selected [:footage id])))) + (fn [db [_ id]] (assoc-in db [:footage :chosen] id))) ;; --------------------------------------------------------------------------- ;; renaming an asset @@ -508,7 +504,7 @@ (let [entry (store/entry (:clip/current db)) uuid (random-uuid) fps (get-in db [:clip :fps]) - {:keys [clip sid subject-ids]} + {:keys [clip sid tracked?]} (bring/take (:clip entry) (:clip built) name footage-id range) st (merge (:store entry) (:store built)) imported-frames (clip/output-frames clip sid) @@ -529,22 +525,9 @@ (update :footage merge {:loading? false :status why}))} (let [db (edit/edit-entry db - #(let [analysis-id (-> built :clip :analyses keys first) - remap-subjects - (fn [inputs] - (update inputs :subjects - (fn [subjects] - (into {} (map (fn [[old new]] [new (get subjects old)])) - subject-ids))))] - (-> (assoc % :clip (:clip result) :store st) - (update-in [:sources analysis-id] - (fn [source] - {:footage-id (:footage-id built) - :source-blocks (:source-blocks built) - :source-inputs - (update (remap-subjects (:source-inputs built)) - :subjects merge - (get-in source [:source-inputs :subjects]))})))))] + #(cond-> (assoc % :clip (:clip result) :store st) + tracked? (merge (select-keys built [:footage-id :source-blocks + :source-inputs]))))] {:db (-> db (update :ui dissoc :convert) (ui/selected @@ -553,5 +536,7 @@ {:loading? false :status (str "made " name " · " imported-frames " frames at " fps " fps" (when (not= fps source-fps) - (str " · sampled from " source-fps " fps")))})) + (str " · sampled from " source-fps " fps")) + (when-not tracked? + " · as drawings: this project already tracks other footage"))})) :dispatch [::pb/refresh-clock]}))))) diff --git a/frontend/src/arthur/events/project.cljs b/frontend/src/arthur/events/project.cljs index 6838371..295ccfe 100644 --- a/frontend/src/arthur/events/project.cljs +++ b/frontend/src/arthur/events/project.cljs @@ -102,10 +102,15 @@ (.then (fn [^js created] (.-id created)))))) (defn- opened-entry! [^js clip-json] - (-> (js/Promise.all - (into-array (map #(http/GET (str "/api/blocks/" %)) - (array-seq (.-blocks clip-json))))) - (.then (fn [blocks] + (let [footage-id (.-footage clip-json)] + (-> (js/Promise.all + #js [(js/Promise.all + (into-array (map #(http/GET (str "/api/blocks/" %)) + (array-seq (.-blocks clip-json))))) + (if footage-id + (http/GET (str "/api/footage/" footage-id)) + (js/Promise.resolve nil))]) + (.then (fn [[blocks ^js footage]] (let [cid (.-cid clip-json) loaded (project/load cid #js {:leaves (.-leaves clip-json) @@ -124,9 +129,11 @@ ;; write lands on what does not. :synced (project/tier1 (.-leaves clip-json)) :clip built :store (:store loaded) - :audio "/static/arthur/audio.wav"})] + :footage-id footage-id + :audio (if footage (.-audio footage) + "/static/arthur/audio.wav")})] (-> (mix/mix! built (clip/opens-on built) (:audio entry) (:store entry)) - (.then (fn [audio] (assoc entry :audio audio)))))))))) + (.then (fn [audio] (assoc entry :audio audio))))))))))) (defn- saved-clip! "Promise of clip `cid` of saved project `pid`, as `{:clip :store}`: its @@ -233,60 +240,46 @@ :removed (into-array (remove #(contains? local %) (keys synced)))}) #js {:leaves (.-leaves doc)})) -(defonce ^:private uploaded-blocks - ;; Block keys this page has already put on the server. Content addressed, so - ;; once there they are there: a save of a moved vertex asks for none again. +(defonce ^:private on-server + ;; Analyses and block keys this page has already put on the server. Content + ;; addressed, so once there they are there: a save of a moved vertex asks for + ;; none of it again, and is one request. (atom #{})) -(defonce ^:private linked-analyses (atom #{})) - (defn- upload-new! [^js doc] (let [keys (array-seq (block-keys doc))] - (if (every? @uploaded-blocks keys) + (if (every? @on-server keys) (js/Promise.resolve 0) - (.then (upload-missing! doc) (fn [n] (swap! uploaded-blocks into keys) n))))) - -(defn- analyses-of [entry] - (vals (get-in entry [:clip :analyses]))) - -(defn- upload-sources! [sources] - (reduce - (fn [chain [analysis-id {:keys [source-blocks]}]] - (.then chain - (fn [_] - (when (and (seq source-blocks) - (not (@linked-analyses analysis-id))) - (-> (upload-missing! #js {:blocks (source/upload-blocks source-blocks)}) - (.then #(http/PUT - (str "/api/analyses/" analysis-id) - #js {:source_blocks - (into-array (source/block-keys source-blocks))})) - (.then (fn [answer] - (swap! linked-analyses conj analysis-id) - answer))))))) - (js/Promise.resolve nil) - sources)) + (.then (upload-missing! doc) (fn [n] (swap! on-server into keys) n))))) (rf/reg-fx ::save! (fn [{:keys [id cid label clip base]}] - (let [entry clip - analyses (analyses-of entry) - doc (project/save cid entry) - local (leaf/leaves cid (:clip entry)) - base (when (and id (:synced entry)) base) - sources (:sources entry)] + (let [analysis (:analysis (:clip clip)) + doc (project/save cid clip) + local (leaf/leaves cid (:clip clip)) + base (when (and id (:synced clip)) base) + source-blocks (:source-blocks clip)] (-> (ensure-project! id label) (.then (fn [pid] - (-> (reduce (fn [chain one] - (.then chain - #(http/POST "/api/analyses" - (analysis-payload one)))) - (js/Promise.resolve nil) - analyses) + (-> (if (and analysis (not (@on-server (:id analysis)))) + (http/POST "/api/analyses" (analysis-payload analysis)) + (js/Promise.resolve nil)) (.then (fn [_] - (upload-sources! sources))) + (when (and (seq source-blocks) (not (@on-server (:id analysis)))) + (-> (upload-missing! + #js {:blocks (source/upload-blocks source-blocks)}) + (.then (fn [_] + (http/PUT + (str "/api/analyses/" (:id analysis)) + ;; One set per tracked subject, in + ;; the order `source/unpack` does + ;; not depend on. + #js {:source_blocks + (into-array + (source/block-keys source-blocks))}))))))) (.then (fn [_] + (when analysis (swap! on-server conj (:id analysis))) (upload-new! doc))) (.then (fn [uploaded] (-> (http/PUT (str "/api/projects/" pid) @@ -295,11 +288,11 @@ :clips #js [(js/Object.assign #js {:cid cid :name label - :analyses (into-array - (keys (get-in entry [:clip :analyses]))) + :analysis (:id analysis) + :footage (:footage-id clip) :blocks (block-keys doc)} (clip-payload doc base local - (:synced entry)))]}) + (:synced clip)))]}) (.then (fn [^js saved] (rf/dispatch [::saved pid cid label (.-seq saved) @@ -446,38 +439,39 @@ (assoc measured :interior-key block-key)))))))) (throw error))))))) -(defn- source-for! [entry edit] - (let [subject (:subject (regenerate/plan (:clip entry) edit)) - subject-record (get-in entry [:clip :subjects subject]) - analysis-id (:analysis subject-record) - analysis (get-in entry [:clip :analyses analysis-id]) - source-subject (:source-subject subject-record) - local (get-in entry [:sources analysis-id :source-inputs :subjects subject])] - (if local - (js/Promise.resolve {:subjects {subject local}}) - (if (= [analysis-id subject] (:key @retained-source)) +(defn- source-for! [entry] + (if-let [inputs (:source-inputs entry)] + (js/Promise.resolve inputs) + (let [analysis (get-in entry [:clip :analysis])] + (if (= (:id analysis) (:id @retained-source)) (:promise @retained-source) (let [promise (if (= "synth" (:detector analysis)) + ;; The synthetic take tracks one face and regenerating it reads + ;; that face's landmarks, so it arrives in the same shape real + ;; footage does rather than in a flat one only this branch uses. (js/Promise.resolve {:subjects - {subject {:dense (synth/synth-dense (:frames analysis) - {:seed (:seed analysis)})}}}) - (.then (ingest/manifest! (:footage subject-record)) - (fn [manifest] - (.then (footage/saved-source! - analysis-id [(:width manifest) (:height manifest)]) - (fn [inputs] - (when-not inputs - (throw (ex-info "saved analysis has no source blocks" {}))) - {:subjects - {subject - (assoc (get-in inputs [:subjects source-subject]) - :presence - (footage/presence-for manifest - source-subject))}})))))] + {:face-1 {:dense (synth/synth-dense (:frames analysis) + {:seed (:seed analysis)})}}}) + (-> (ingest/manifest! (:footage-id entry)) + (.then (fn [manifest] + (-> (footage/saved-source! (:id analysis) + [(:width manifest) + (:height manifest)]) + (.then (fn [inputs] + (when-not inputs + (throw (ex-info "saved analysis has no source blocks" {}))) + (update inputs :subjects + (fn [subjects] + (into {} + (map (fn [[id one]] + [id (assoc one :presence + (footage/presence-for + manifest id))])) + subjects))))))))))] (do - (reset! retained-source {:key [analysis-id subject] :promise promise}) + (reset! retained-source {:id (:id analysis) :promise promise}) promise)))))) (defn- inputs-for-edit! @@ -490,19 +484,18 @@ one (get-in inputs [:subjects subject])] (if (and teeth (:crops one)) (let [settings (merge take/knobs (feature/effective-params clip teeth)) - analysis (get-in clip [:subjects subject :analysis]) - source-subject (get-in clip [:subjects subject :source-subject]) + analysis (get-in clip [:analysis :id]) frames (count (:crops one)) - key (source/interior-key analysis source-subject settings frames) + key (source/interior-key analysis subject settings frames) done (fn [measured] (assoc-in inputs [:subjects subject] measured))] (if (and (:interior one) (or (= (:interior-key one) key) (and (nil? (:interior-key one)) - (= key (source/interior-key analysis source-subject take/knobs frames))))) + (= key (source/interior-key analysis subject take/knobs frames))))) (js/Promise.resolve inputs) (.then (if (= key (:key @retained-interior)) (:promise @retained-interior) - (let [promise (retained-interior! analysis source-subject settings one)] + (let [promise (retained-interior! analysis subject settings one)] (reset! retained-interior {:key key :promise promise}) promise)) done))) @@ -511,7 +504,7 @@ (rf/reg-fx ::preview-settings! (fn [{:keys [id entry edit request]}] - (-> (source-for! entry edit) + (-> (source-for! entry) (.then (fn [inputs] (inputs-for-edit! entry edit inputs))) (.then (fn [inputs] (regenerate/change (assoc entry :source-inputs inputs) edit))) @@ -526,7 +519,7 @@ (fn [{:keys [db]} [_ edit]] (let [id (:clip/current db) entry (store/entry id)] - (if (or (get-in db [:project :busy?]) (empty? (:analyses (:clip entry)))) + (if (or (get-in db [:project :busy?]) (nil? (:analysis (:clip entry)))) {} (let [plan (regenerate/plan (:clip entry) edit) report (select-keys plan [:features :roles]) @@ -729,6 +722,50 @@ (assoc-in % [:symbols sid :palette] id) (update-in % [:symbols sid] dissoc :palette))))) +(rf/reg-event-db + ::drop-palette + (fn [db [_ root-sid frame palette-id]] + (let [{document :clip st :store} (store/entry (:clip/current db)) + root (clip/symbol document root-sid) + old-track (:palette-track root) + track-id (if (= :palette-track (get-in document [:symbols old-track :type])) + old-track (clip/fresh-id document)) + palette-sid (or (some (fn [[sid sym]] + (when (and (= :palette (:type sym)) + (= palette-id (:palette-ref sym))) sid)) + (:symbols document)) + (clip/fresh-id (cond-> document + (not= track-id old-track) + (assoc-in [:symbols track-id] {})))) + document (cond-> (if (= track-id old-track) + document + (-> document + (assoc-in [:symbols root-sid :palette-track] track-id) + (assoc-in [:symbols track-id] + {:id track-id :name "palette" :type :palette-track + :display :lane :frames (:frames root) + :fps (clip/fps document root-sid) :nodes {}}))) + (nil? (clip/symbol document palette-sid)) + (assoc-in [:symbols palette-sid] + {:id palette-sid + :name (or (get-in document [:palettes palette-id :name]) + (name palette-id)) + :type :palette :palette-ref palette-id + :frames 1 :fps (clip/fps document root-sid) :nodes {}})) + id (random-uuid) + result (span/place-symbol document st track-id id palette-sid frame + {:extent :grow-symbol :remainder-id (random-uuid)}) + result (if-let [placed (:clip result)] + (assoc result :clip + (assoc-in placed [:symbols track-id :nodes id :channels [:palette]] + (assoc (ch/framed palette-id) :semantic :palette))) + result)] + (if-let [why (:refused result)] + (update db :project merge {:status why}) + (-> db + (edit/edit (constantly (:clip result))) + (assoc-in [:ui :selection] [:node track-id id [id]])))))) + (rf/reg-event-db ::set-channel ;; `frame` is the node's own, as for a drawing key. @@ -740,7 +777,7 @@ (if (get-in document [:symbols sid :nodes id :channels [:palette]]) document (let [source (get-in document [:symbols sid :nodes id :source :symbol]) - palette-id (or (get-in document [:symbols source :palette-ref]) pal/inherit)] + palette-id (get-in document [:symbols source :palette-ref])] (assoc-in document [:symbols sid :nodes id :channels [:palette]] (assoc (ch/framed palette-id) :semantic :palette))))) diff --git a/frontend/src/arthur/events/ui.cljs b/frontend/src/arthur/events/ui.cljs index b61f2d2..724c56b 100644 --- a/frontend/src/arthur/events/ui.cljs +++ b/frontend/src/arthur/events/ui.cljs @@ -17,14 +17,12 @@ app to change and the most expensive to have two copies of." (:require [clojure.string :as str] [arthur.domain.clip :as clip] - [arthur.domain.channel :as ch] [arthur.domain.clipboard :as clipboard] [arthur.domain.correction :as correction] [arthur.domain.creation :as creation] [arthur.domain.gesture :as gesture] [arthur.domain.nest :as nest] [arthur.domain.node :as node] - [arthur.domain.palette :as pal] [arthur.domain.span :as span] [arthur.events.edit :as edit] [arthur.domain.paint :as paint] @@ -345,16 +343,9 @@ (rf/reg-event-db - ::set-row-expanded - (fn [db [_ path expanded?]] - ;; The disclosure control says what the next state is instead of asking us - ;; to invert whatever happens to be in app-db by the time its event runs. - ;; A structural drop may reveal this path between pointer-down and click; - ;; blindly toggling then immediately closed the row whose triangle was still - ;; painted as closed. - (update-in db [:ui :expanded] - (fn [paths] - ((if expanded? conj disj) (set paths) path))))) + ::toggle-row + (fn [db [_ path]] + (update-in db [:ui :expanded] #(if (contains? % path) (disj % path) (conj % path))))) (rf/reg-event-db ::solo @@ -802,55 +793,6 @@ (edit/transaction (constantly (:clip result))) (selected [:node sid uuid (conj (vec path) uuid)])))) -(rf/reg-event-db - ::new-symbol-at - ;; One gesture and one creation path for every lane. The destination decides - ;; the kind: an ordinary lane gets a blank symbol; the synthetic palette row - ;; gets a blank palette symbol whose placement starts by inheriting. - (fn [db [_ _pointer-frame target]] - (let [{document :clip st :store} (store/entry (:clip/current db)) - ;; Creation always happens at the playhead. The double-click only - ;; names the lane; it is not a second, pointer-based time cursor. - frame (editing-frame db document) - palette? (= :arthur.ui.timeline/palette-track (first target)) - root-sid (second target) - root (when palette? (clip/symbol document root-sid)) - old-track (:palette-track root) - track-id (when palette? - (if (= :palette-track (get-in document [:symbols old-track :type])) - old-track (clip/fresh-id document))) - document (if (and palette? (not= track-id old-track)) - (-> document - (assoc-in [:symbols root-sid :palette-track] track-id) - (assoc-in [:symbols track-id] - {:id track-id :name "palette" :type :palette-track - :display :lane :frames (:frames root) - :fps (clip/fps document root-sid) :nodes {}})) - document) - where (if palette? - {:clip document :sid track-id :at frame :path []} - (drop-destination db document st frame target)) - sid (clip/fresh-id document) - uuid (random-uuid)] - (if (:refused where) - (update db :project merge {:status (:refused where)}) - (let [seeded (assoc-in document [:symbols sid] - (cond-> {:id sid - :name (if palette? "palette transition" (name sid)) - :fps (clip/fps document (:sid where)) - :frames 1 :nodes {}} - palette? (assoc :type :palette))) - result (span/place-symbol seeded st (:sid where) uuid sid (:at where) - {:extent :grow-symbol - :remainder-id (random-uuid)}) - result (if-let [placed (and palette? (:clip result))] - (assoc result :clip - (assoc-in placed [:symbols (:sid where) :nodes uuid - :channels [:palette]] - (assoc (ch/framed pal/inherit) :semantic :palette))) - result)] - (landed db where uuid result)))))) - (rf/reg-event-db ::drop-symbol ;; A symbol dropped on a row becomes a naturally playing clip in the symbol that diff --git a/frontend/src/arthur/flow/freeze.cljs b/frontend/src/arthur/flow/freeze.cljs index 91d057a..c83efa5 100644 --- a/frontend/src/arthur/flow/freeze.cljs +++ b/frontend/src/arthur/flow/freeze.cljs @@ -762,15 +762,11 @@ {:frames (vec lengths)}))) nf (first lengths) merged (fn [k] (into {} (mapcat (comp k second)) parts)) - analysis (:analysis params) - built {:name name :fps fps :analyses {(:id analysis) analysis} + built {:name name :fps fps :analysis (:analysis params) :width (first stage) :height (second stage) :palettes {pal/default-id pal/default-palette} :default-palette pal/default-id - :subjects (into {} (map (fn [[id _]] - [id {:id id :params {} - :analysis (:id analysis) - :source-subject id}])) ordered) + :subjects (into {} (map (fn [[id _]] [id {:id id :params {}}])) ordered) :features (merged :features) :groups (merged :groups) :symbols (into {:main diff --git a/frontend/src/arthur/flow/regenerate.cljs b/frontend/src/arthur/flow/regenerate.cljs index 7148df0..e228d3a 100644 --- a/frontend/src/arthur/flow/regenerate.cljs +++ b/frontend/src/arthur/flow/regenerate.cljs @@ -142,11 +142,6 @@ teeth are dirty, retained pixel measurements. No IO or app-db here." [{:keys [clip source-inputs] :as entry} edit] (let [{:keys [changed subject features]} (plan clip edit) - analysis-id (get-in changed [:subjects subject :analysis]) - analysis (get-in changed [:analyses analysis-id]) - _ (when-not analysis - (throw (ex-info "subject names a missing analysis" - {:subject subject :analysis analysis-id}))) ;; THE EDITED SUBJECT'S OWN LANDMARKS. Retained source is per subject — ;; one video, one dense track per tracked face — so re-measuring the ;; second face through the first one's anchor is the mistake this lookup @@ -157,8 +152,8 @@ {:subject subject}))) base-params (merge take/knobs {:fps (or (get-in changed [:symbols subject :fps]) (:fps changed)) - :aspect (:aspect analysis) - :analysis analysis}) + :aspect (get-in changed [:analysis :aspect]) + :analysis (:analysis changed)}) ;; One conditioned anchor for the whole edit, and it is the SUBJECT's. ;; `:anchor-avg` is a subject setting, so the shared upstream measurement ;; is not read off whichever dirty feature happened to sort first — and diff --git a/frontend/src/arthur/subs/render.cljs b/frontend/src/arthur/subs/render.cljs index 8b09c31..d225b09 100644 --- a/frontend/src/arthur/subs/render.cljs +++ b/frontend/src/arthur/subs/render.cljs @@ -212,6 +212,7 @@ ;; row is not on stage, so neither is its footage. (let [solo (filter #(placed? document open %) solo)] {:document document :store store + :footage-id (:footage-id (footage/entry id)) :opacity (or opacity trace/opacity-default) :traces (cond->> (when document (trace/shown document open faces)) (seq solo) (filterv (fn [{:keys [path]}] diff --git a/frontend/src/arthur/subs/ui.cljs b/frontend/src/arthur/subs/ui.cljs index 763b876..3ffc7b7 100644 --- a/frontend/src/arthur/subs/ui.cljs +++ b/frontend/src/arthur/subs/ui.cljs @@ -156,8 +156,7 @@ ;; than kept beside it, so a sound placed from footage makes that footage the ;; project's without anything else being told. (let [entry (store/entry id) - used (into (set uploaded) - (keep :footage (vals (get-in entry [:clip :subjects])))) + used (into (set (keep identity [(:footage-id entry)])) uploaded) used (into used (for [[_ sym] (get-in entry [:clip :symbols]) [_ n] (:nodes sym) :let [f (get-in n [:source :footage])] diff --git a/frontend/src/arthur/ui/drag.cljs b/frontend/src/arthur/ui/drag.cljs index 1b50f0f..530c4eb 100644 --- a/frontend/src/arthur/ui/drag.cljs +++ b/frontend/src/arthur/ui/drag.cljs @@ -47,6 +47,13 @@ :center (clip/center document st sid) :shapes (outline document st sid)})))) +(defn palette! + "Start carrying a project palette toward the open symbol's palette track." + [id label] + (reset! carrying {:kind :palette :palette id :label label :frames 1})) + +(defn palette? [] (= :palette (:kind @carrying))) + (defn accepts? "Whether the stage or the tracks should accept what is being carried: things out of the pool, and not one that would make a cycle." @@ -120,3 +127,10 @@ :sound (rf/dispatch [::ui/drop-sound c frame target]) nil)) (done!))) + +(defn land-palette! + "Put the carried palette on `sid`'s palette track at `frame`." + [sid frame] + (when-let [{:keys [palette]} (when (palette?) @carrying)] + (rf/dispatch [::project/drop-palette sid frame palette])) + (done!)) diff --git a/frontend/src/arthur/ui/params.cljs b/frontend/src/arthur/ui/params.cljs index 60f5140..a1770d7 100644 --- a/frontend/src/arthur/ui/params.cljs +++ b/frontend/src/arthur/ui/params.cljs @@ -2,9 +2,10 @@ "The right pane: what the selection is, and what can be changed about it. Sections rather than a mode switch. The clip's facts are always true, so the - clip section is always there; node, footage, face and tracking sections follow - the thing actually selected. Nothing here computes — every control dispatches - an intent and every readout comes off a subscription." + clip section is always there; the node and symbol sections appear when + something of that kind is selected; the tracking section appears when the clip + has analysis in it. Nothing here computes — every control dispatches an intent + and every readout comes off a subscription." (:require [clojure.string :as str] [arthur.domain.clip :as clip-domain] [arthur.domain.channel :as channel] @@ -207,42 +208,12 @@ v))) (when gap? [segment-select sid id path ch left])])) -(defn- color-control [sid id ch frame auto-key? palette] - (let [keyed? (some? (:keys ch)) - value (channel/value-at ch (or frame 0) nil) - value (if (integer? value) - value - (or (first (keep-indexed #(when (= value (:name %2)) %1) - (:slots palette))) 0)) - off? (and keyed? (nil? frame))] - [:dd.channel {:class (when auto-key? "live")} - [:button.key {:class (cond (contains? (:keys ch) frame) "on" keyed? "keyed") - :disabled (nil? frame) - :title (if keyed? (str (count (:keys ch)) " held keys") "key color here") - :on-click #(rf/dispatch [::project/toggle-key sid id [:style :color] frame])} - "◆"] - [:div.channel-swatches - (doall - (for [[i {:keys [name hex]}] (map-indexed vector (:slots palette))] - ^{:key i} - [:button.swatch - {:class (when (= i value) "on") - :style {:background hex} - :disabled off? - :aria-label (str "color " i (when name (str " " (clojure.core/name name)))) - :aria-pressed (= i value) - :title (str i (when name (str " · " (clojure.core/name name))) " · " hex) - :on-click #(rf/dispatch [::project/set-channel sid id [:style :color] frame i])}]))] - (when keyed? [:span.dim "hold"])])) - (defn- node-section [[sid id n]] (let [[start end] (:span n) auto-key? @(rf/subscribe [::sub/auto-key?]) clip @(rf/subscribe [::render/clip]) local @(rf/subscribe [::sub/selected-local]) - frame (:frame local) - palette-id (or (get-in clip [:symbols sid :palette]) (pal/default-palette-id clip)) - palette (get (pal/palettes clip) palette-id)] + frame (:frame local)] [section (str (name (:kind n)) " · in " (name sid)) [facts "id" (brief id) @@ -261,24 +232,13 @@ (let [{:keys [frame]} local] [:dl.facts (doall - (for [[path ch] (sort-by (fn [[path]] - [(cond - (= :xform (first path)) 0 - (= path [:style :color]) 1 - :else 2) - (str path)]) - (node/channels n))] + (for [[path ch] (sort-by (comp str key) (node/channels n))] ^{:key (str path)} [:<> [:dt (str/join " " (map name path))] - (cond - (and (= path [:style :color]) (not (:dense ch))) - [color-control sid id ch frame auto-key? palette] - - (and (contains? (node/defaults-of n) path) (not (:dense ch))) + (if (and (contains? (node/defaults-of n) path) (not (:dense ch))) [channel-control sid id path ch frame auto-key?] - - :else [:dd (channel-state ch)])]))])])) + [:dd (channel-state ch)])]))])])) (defn- palette-placement-section [[sid id n]] (let [clip @(rf/subscribe [::render/clip]) @@ -308,15 +268,12 @@ :title (if (:keys ch) (str (count (:keys ch)) " keys") "key this here") :on-click #(rf/dispatch [::project/toggle-palette-key sid id frame])} "◆"] - [:select {:value (if (= pal/inherit chosen) "" (str chosen)) + [:select {:value (str chosen) :on-change (fn [e] (let [v (.. e -target -value) - palette-id (or (first (filter #(= v (str %)) - (keys (pal/palettes clip)))) - pal/inherit)] + palette-id (first (filter #(= v (str %)) (keys (pal/palettes clip))))] (rf/dispatch [::project/set-palette-choice sid id frame palette-id])))} - [:option {:value ""} "inherit"] (for [[palette-id p] palettes] ^{:key (str palette-id)} [:option {:value (str palette-id)} (:name p)])]]] @@ -471,16 +428,19 @@ ;; the footage showing under the picture (defn- footage-section - "The footage under the explicitly selected face or footage asset, on or off - and how strongly. `here` is exactly those faces. + "The footage under the faces the open symbol has, on or off and how strongly. + `here` is those faces. ONE SWITCH FOR THE FACES THAT ARE HERE. A face's footage is the face's, not a placement's, so there is nothing to inherit and nothing to set twice; with several faces in a take the box says how many are showing and switches the rest on, and one face alone is switched from its own timeline row. - This is editor state, but the inspector still obeys selection scope: selecting - an unrelated shape must neither expose nor mutate a face's viewing aid." + THE OPEN SYMBOL'S FACES AND NOT THE SELECTION'S, which is what lets this live in + the inspector at all: a viewing aid that appeared only once the right row had + been found would make the way to see the footage you are tracing depend on what + you had clicked. So the section is there whenever the picture on the stage has + any footage behind it, wherever the selection happens to be." [here] (let [{:keys [faces opacity]} @(rf/subscribe [::render/tracing]) on (filterv (set faces) here)] @@ -694,45 +654,13 @@ (max 1 (* 2 default))) :step (cond even? 2 (= type :integer) 1 :else 0.01)}) -(defn- subject-of-owner [clip [scope id]] - (case scope - :subject id - :feature (get-in clip [:features id :subject]) - :group (get-in clip [:groups id :subject]) - nil)) - -(defn- face-selection - "The one face explicitly named by a face symbol, its placement, or a tracking - owner. A shape merely living inside an open face is deliberately not one." - [clip selection selected-node] - (let [[kind id] selection - placed (node/source (peek selected-node)) - candidate (cond - (= :symbol kind) id - (#{:subject :feature :group} kind) (subject-of-owner clip selection) - (= :node kind) placed)] - (when (and candidate (trace/traceable? clip candidate)) candidate))) - -(defn- footage-faces [clip selection selected-node] - (if (= :footage (first selection)) - (let [footage-id (second selection)] - (into [] (comp (filter #(= footage-id (:footage (val %)))) (map key)) - (:subjects clip))) - (some-> (face-selection clip selection selected-node) vector))) - -(defn- tracking-owners [clip selection selected-node] - (let [faces (if (= :footage (first selection)) - (set (footage-faces clip selection selected-node)) - (some-> (face-selection clip selection selected-node) hash-set))] - (when (seq faces) - (filterv #(contains? faces (subject-of-owner clip %)) (owners clip))))) - -(defn- tracking-section [all] +(defn- tracking-section [] (let [clip @(rf/subscribe [::render/clip]) selection @(rf/subscribe [::sub/selection]) knobs @(rf/subscribe [::sub/knobs]) busy? (:busy? @(rf/subscribe [::playback/project])) report @(rf/subscribe [::project/regeneration]) + all (owners clip) [scope id :as owner] (if (some #{selection} all) selection (first all)) area (case scope :subject :subject @@ -796,6 +724,7 @@ open @(rf/subscribe [::render/open]) selection @(rf/subscribe [::sub/selection]) node @(rf/subscribe [::sub/selected-node]) + tracked? (seq (owners clip)) ;; The face the tracing section is about: the SELECTED PLACEMENT's symbol, ;; or, when the selection is not an instance or there is none, the OPEN ;; symbol — which is the face itself when a face is open to be drawn over. @@ -805,15 +734,16 @@ ;; lane traces is not a question with one answer, and naming the drawing ;; showing now would move the section under the playhead. placed (node/source (peek node)) - footage-faces (footage-faces clip selection node) - tracking-owners (tracking-owners clip selection node) palette-placement? (and node - (= :palette-track - (get-in clip [:symbols (first node) :type]))) + (= :palette (get-in clip [:symbols placed :type]))) palette-symbol? (and (= :symbol (first selection)) (= :palette (get-in clip [:symbols (second selection) :type]))) face (or placed (when (trace/traceable? clip open) open)) faces (when face (trace/faces clip face)) + ;; Every face the open symbol has, which is what the footage switch is + ;; about: the stage either has footage behind it or it has none, and that + ;; does not depend on what is selected. + here (trace/traceable-faces clip open) ;; Where that face sits, as a row path from the open symbol, so the faces ;; inside it can be selected by their own rows. A selection made on the ;; stage has no path and names a node directly in the open symbol; the @@ -829,13 +759,12 @@ (when (and placed (not palette-placement?)) [symbol-section placed "source symbol"]) (when (and node (not palette-placement?)) ^{:key (str (first node) "/" (second node))} [correction-section node]) - (when (and (not palette-placement?) (seq footage-faces)) - [footage-section footage-faces]) + (when (and (not palette-placement?) (seq here)) [footage-section here]) (when (and face (or (trace/traceable? clip face) (seq faces))) [tracing-section face faces path]) (when face ^{:key (str "perf/" face)} [performance-section face]) (when palette-symbol? [palette-symbol-section (second selection)]) (when (and (= :symbol (first selection)) (not palette-symbol?)) [symbol-section (second selection)]) - (when (and (seq tracking-owners) (not palette-placement?) (not palette-symbol?)) - [tracking-section tracking-owners])]])) + (when (and tracked? (not palette-placement?) (not palette-symbol?)) + [tracking-section])]])) diff --git a/frontend/src/arthur/ui/pool.cljs b/frontend/src/arthur/ui/pool.cljs index 5c93ba0..16b047e 100644 --- a/frontend/src/arthur/ui/pool.cljs +++ b/frontend/src/arthur/ui/pool.cljs @@ -271,6 +271,25 @@ (carrying (str "symbol:" (subs (str sid) 1)) #(drag/symbol! clip-id sid open)))])) +(defn- palette-row [id p default-id chosen rename] + ^{:key (str id)} + [row (merge {:label (:name p) + :sub (str (count (:slots p)) " colors") + :title (str (:name p) " · " (count (:slots p)) + " indexed colors — drag onto the palette track") + :thumb [:span.thumb {:style {:display "grid" + :grid-template-columns "repeat(4,1fr)"}} + (for [[i s] (map-indexed vector (take 16 (:slots p)))] + ^{:key i} [:i {:style {:background (:hex s)}}])] + :on? (= id chosen) + :rename (assoc rename :key [:palette id] :value (:name p) + :commit! (fn [value] + ((:begin! rename) nil) + (rf/dispatch [::project/palette-name id value]))) + :on-click #(rf/dispatch [:arthur.ui.palette/select id]) + :on-double-click #(rf/dispatch [::project/default-palette id])} + (carrying (str "palette:" id) #(drag/palette! id (:name p))))]) + (defn- palette-transition-row [document sid selection rename] (let [sym (clip/symbol document sid) p (get (pal/palettes document) (:palette-ref sym))] @@ -404,12 +423,7 @@ (let [named? #(hit? query (clip/symbol-name document %)) top (when (and main (named? main)) main) symbol-ids (sort-by str (keys (:symbols document))) - transition-ids (into #{} (mapcat (fn [sym] - (when (= :palette-track (:type sym)) - (keep (comp :symbol :source val) (:nodes sym))))) - (vals (:symbols document))) - transition? #(or (= :palette (:type (clip/symbol document %))) - (contains? transition-ids %)) + transition? #(= :palette (:type (clip/symbol document %))) transitions (filterv #(and (named? %) (transition? %)) symbol-ids) rest (filterv #(and (named? %) (not= main %) (not (#{:palette :palette-track} @@ -417,10 +431,11 @@ symbol-ids) media (filterv #(hit? query (:label %)) media) sounds (filterv #(hit? query (:label %)) sounds) - ] + palettes (filterv #(hit? query (:name (val %))) + (sort-by (comp str :name val) (pal/palettes document)))] [sections searching? (+ (if top 1 0) (count rest) (count transitions) - (count media) (count sounds)) + (count media) (count sounds) (count palettes)) [{:title "project" :searching? searching? :blank "nothing to open yet" :rows (when top [(symbol-row document top ctx)])} @@ -430,6 +445,11 @@ {:title "palette transitions" :searching? searching? :blank "no palette transitions" :rows (mapv #(palette-transition-row document % selection rename) transitions)} + {:title "palettes" :searching? searching? + :blank "no palettes" + :rows (mapv (fn [[id p]] (palette-row id p (:default-palette document) + (or palette-choice (pal/default-palette-id document)) + rename)) palettes)} {:title "media" :searching? searching? :blank "drop a video here" :rows (mapv #(footage-row % chosen rename) media)} diff --git a/frontend/src/arthur/ui/timeline.cljs b/frontend/src/arthur/ui/timeline.cljs index 3772771..5a4a22d 100644 --- a/frontend/src/arthur/ui/timeline.cljs +++ b/frontend/src/arthur/ui/timeline.cljs @@ -656,14 +656,11 @@ :else [::ui/group [from path]])))))})) [:button.tl-twist {:disabled (not expandable?) - ;; This button lives inside a draggable row label. Do not let a tiny - ;; pointer movement turn disclosure into a native row drag (whose click - ;; is then suppressed by the browser). - :draggable false - :on-pointer-down (fn [^js e] (.stopPropagation e)) :on-click (fn [^js e] + (println "heyyy") (.stopPropagation e) - (rf/dispatch [::ui/set-row-expanded path (not expanded?)]))} + (rf/dispatch [::ui/toggle-row path])) + :class (println expandable?)} (when expandable? (if expanded? "▾" "▸"))] ;; A LANE IS NOT AN INSTANCE WEARING A DIFFERENT HAT, to read. Its one row ;; holds blocks that follow one another in time, where every other row @@ -890,25 +887,30 @@ ;; one, else this row's own instance. It opens as a tab, which is what ;; double-clicking the same symbol in the pool does. :on-double-click (fn [^js e] - (let [under (clip-under e)] - (cond - (and lane? (nil? under)) - (do (.stopPropagation e) - (rf/dispatch [::ui/new-symbol-at - (frame-at e frames) select])) - - :else - (when-let [source (or (:source under) of)] - (.stopPropagation e) - (rf/dispatch [::pb/open-symbol source]))))) + (when-let [source (and (not= :palette kind) + (or (:source (clip-under e)) of))] + (.stopPropagation e) + (rf/dispatch [::pb/open-symbol source]))) :ref (when lane? (fn [el] (when el (aset el "arthurLane" select)))) :on-drag-enter (fn [^js e] + ;; A palette hover belongs only to the palette row. If + ;; the pointer leaves it for an ordinary row, clear its + ;; last accepted preview even though this row correctly + ;; declines the native drop. + (when (and (drag/palette?) (not= :palette kind)) + (rf/dispatch [::ui/drop-clear])) (cond + (and (= :palette kind) (drag/palette?)) + (do (.preventDefault e) (.stopPropagation e)) (and (not= :palette kind) lane? (or (drag/accepts?) (drag/row))) (do (.preventDefault e) (.stopPropagation e)))) :on-drag-over (fn [^js e] (cond + (and (= :palette kind) (drag/palette?)) + (do (.preventDefault e) (.stopPropagation e) + (set! (.. e -dataTransfer -dropEffect) "copy") + (drag/hover! :timeline (frame-at e frames) nil select)) (and (not= :palette kind) lane? (or (drag/accepts?) (drag/row))) (do (.preventDefault e) (.stopPropagation e) @@ -918,6 +920,10 @@ (drag/hover! :timeline (frame-at e frames) nil select))))) :on-drop (fn [^js e] (cond + (and (= :palette kind) (drag/palette?)) + (let [at (frame-at e frames)] + (.preventDefault e) (.stopPropagation e) + (drag/land-palette! open at)) (and (not= :palette kind) lane? (or (drag/accepts?) (drag/row))) (do (.preventDefault e) diff --git a/frontend/src/arthur/ui/underlay.cljs b/frontend/src/arthur/ui/underlay.cljs index 7daf387..73c6525 100644 --- a/frontend/src/arthur/ui/underlay.cljs +++ b/frontend/src/arthur/ui/underlay.cljs @@ -129,7 +129,7 @@ says where each face's head went and which of its frames it was on, so this reads the frame rather than resolving it again. `on-ready` is called when a still or a manifest that was missing arrives, to paint again." - [{:keys [document store traces opacity width playing?]} resolver on-ready] + [{:keys [document store footage-id traces opacity width playing?]} resolver on-ready] (when-let [^js canvas (:canvas @state)] (let [ctx (.getContext canvas "2d") zoom (/ (.-width canvas) width)] @@ -138,20 +138,17 @@ ;; What each face is showing, forgotten for the faces switched off. Before ;; the early exits, so switching them all off forgets all of them. (swap! state update :last select-keys (map :path traces)) - (when (seq traces) - (let [;; Read once: each face writes only its own entry below, and what it - ;; was showing is what it falls back to. - was (:last @state)] + (when-let [us (and (seq traces) footage-id (urls footage-id on-ready))] + (let [start (first (get-in document [:analysis :range] [0])) + ;; Read once: each face writes only its own entry below, and what it + ;; was showing is what it falls back to. A face that has been off + ;; stage for a few frames still has the still it went away with. + was (:last @state)] ;; ONE OPACITY for all of them, set once: how strongly the reference ;; draws is a property of looking at the stage, not of a face. (set! (.-globalAlpha ctx) opacity) - (doseq [{:keys [path face]} traces - :let [subject (get-in document [:subjects face]) - analysis (get-in document [:analyses (:analysis subject)]) - us (urls (:footage subject) on-ready)] - :when us] - (let [start (first (get analysis :range [0])) - at (conj path :head) + (doseq [{:keys [path face]} traces] + (let [at (conj path :head) world (symbol/world-of resolver at) frame (symbol/frame-of resolver at)] (when (and world (number? frame)) diff --git a/frontend/test/arthur/domain/bring_test.cljs b/frontend/test/arthur/domain/bring_test.cljs index 468d754..50cb809 100644 --- a/frontend/test/arthur/domain/bring_test.cljs +++ b/frontend/test/arthur/domain/bring_test.cljs @@ -26,34 +26,3 @@ (vals (get-in clip [:symbols :take :nodes])))))) "and the copy's instance follows its renamed symbol") (is (empty? (clip/problems clip))))) - -(defn- tracked-take [analysis-id] - {:analyses {analysis-id {:id analysis-id :detector "test" :version "1"}} - :subjects {:face-1 {:id :face-1 :analysis analysis-id - :source-subject :face-1 :params {}}} - :features {:face-1/mouth {:id :face-1/mouth :subject :face-1 - :symbol :face-1 :area :mouth :nodes [:mouth] :params {}}} - :groups {} - :symbols {:main {:id :main :fps 30 :frames 10 - :nodes {:face-1 {:id :face-1 :kind :instance :z "a1" - :source {:symbol :face-1}}}} - :face-1 {:id :face-1 :fps 30 :frames 10 - :nodes {:head {:id :head :kind :group :z "a1" - :measured {[:xform :pos] - {:animated? false :value [0 0]}}} - :mouth {:id :mouth :kind :poly :z "a2"}}}}}) - -(deftest every-import-is-tracked-under-its-unique-symbol-scope - (let [first (bring/take (clip/blank) (tracked-take "analysis-a") - "close up" "footage-a" [0 10]) - second (bring/take (:clip first) (tracked-take "analysis-b") - "close up" "footage-b" [0 10]) - c (:clip second)] - (is (= #{:close-up.face-1 :close-up-2.face-1} (set (keys (:subjects c))))) - (is (= #{"analysis-a" "analysis-b"} (set (keys (:analyses c))))) - (is (= "analysis-a" (get-in c [:subjects :close-up.face-1 :analysis]))) - (is (= "analysis-b" (get-in c [:subjects :close-up-2.face-1 :analysis]))) - (is (= :face-1 (get-in c [:subjects :close-up-2.face-1 :source-subject]))) - (is (= #{:close-up.face-1/mouth :close-up-2.face-1/mouth} - (set (keys (:features c))))) - (is (empty? (clip/problems c))))) diff --git a/frontend/test/arthur/domain/cadence_test.cljs b/frontend/test/arthur/domain/cadence_test.cljs index 9e8c30a..c32002b 100644 --- a/frontend/test/arthur/domain/cadence_test.cljs +++ b/frontend/test/arthur/domain/cadence_test.cljs @@ -90,17 +90,8 @@ (is (or (zero? f) (< (clip/shown-frame doc :main (dec f)) n)))))))) (deftest imported-footage-uses-selection-for-picture-and-real-speed-for-audio - (let [tracked (-> footage - (assoc :analyses {"a" {:id "a"}} - :subjects {:face-1 {:id :face-1 :analysis "a" - :source-subject :face-1}} - :features {} :groups {} - :symbols {:main {:id :main :fps 30 :frames 60 - :nodes {:face-1 {:id :face-1 :kind :instance - :z "a" :source {:symbol :face-1}}}} - :face-1 (get-in footage [:symbols :main])})) - {doc :clip sid :sid} (bring/take (clip/set-fps (clip/blank) 12) - tracked "take" "video" [15 75]) + (let [{doc :clip sid :sid} (bring/take (clip/set-fps (clip/blank) 12) + footage "take" "video" [15 75]) ;; A host authored at 12, with a 30fps source starting half a second in. doc (-> doc (assoc-in [:symbols :main :fps] 12) (clip/place-symbol store :main sid 6 :insert nil)) @@ -108,16 +99,21 @@ draw (clip/resolver doc :main store pal/index-of nil) [sound] (nest/audio-tracks doc :main)] (is (= 60 (clip/frames doc sid))) - (is (= 1 (get-in doc [:symbols :take.face-1 :nodes :sound :time :rate]))) + (is (= 1 (get-in doc [:symbols sid :nodes :sound :time :rate]))) (is (= [0 24] (:span n))) (is (empty? (draw 5))) (is (= 8 (:size (first (draw 9))))) - (is (= 7 (:frame (nest/inside doc store :main [:insert :face-1 :mark] 9)))) + (is (= 7 (:frame (nest/inside doc store :main [:insert :mark] 9)))) (is (= [-4 -4 4 4] ((pick/bounds-of doc store :main n) 3))) (is (= [6 30] (node/placed-span sound))) (is (= 15 (node/local-frame sound 6))) (is (= 1 (* (get-in sound [:time :rate]) (/ (:fps doc) (:fps sound))))) (is (= [6 30] (:span (first (timeline/rows doc :main #{[:insert]}))))) + (testing "an off-grid closed mouth can win without modifying the dense source" + (let [pinned (pose/put-cut doc :main :insert :mouth 7 6) + r (clip/resolver pinned :main store pal/index-of nil)] + (is (= 7 (:size (first (r 9))))) + (is (= (get-in doc [:symbols sid]) (get-in pinned [:symbols sid]))))) (testing "a deliberate half speed still retimes audio" (let [slow (assoc-in doc [:symbols :main :nodes :insert :playback :speed] 0.5) [track] (nest/audio-tracks slow :main)] @@ -125,9 +121,7 @@ (deftest generated-faces-own-their-footage-sound (let [face (fn [id] {:id id :fps 30 :frames 20 :nodes {}}) - frozen {:fps 30 :analyses {"a" {:id "a"}} - :subjects {:face-1 {:id :face-1 :analysis "a" :source-subject :face-1} - :face-2 {:id :face-2 :analysis "a" :source-subject :face-2}} + frozen {:fps 30 :subjects {:face-1 {} :face-2 {}} :symbols {:main {:id :main :fps 30 :frames 20 :nodes {:face-1 {:id :face-1 :kind :instance :z "a1" :source {:symbol :face-1}} @@ -136,11 +130,11 @@ :face-1 (face :face-1) :face-2 (face :face-2)}} {doc :clip take :sid} (bring/take (clip/blank) frozen "take" "video" [3 13]) - alone (clip/place-symbol doc nil :main :take.face-1 4 :placed-face nil)] + alone (clip/place-symbol doc nil :main :face-1 4 :placed-face nil)] (is (nil? (get-in doc [:symbols take :nodes :sound])) "the wrapper does not own a sound the face would lose") (is (= {:footage "video"} - (get-in doc [:symbols :take.face-1 :nodes :sound :source]))) + (get-in doc [:symbols :face-1 :nodes :sound :source]))) (is (= 1 (count (nest/audio-tracks doc take))) "the same take recording is not mixed once per detected face") (is (= 1 (count (nest/audio-tracks alone :main))) diff --git a/frontend/test/arthur/domain/channel_test.cljs b/frontend/test/arthur/domain/channel_test.cljs index fd3fc9d..be640bb 100644 --- a/frontend/test/arthur/domain/channel_test.cljs +++ b/frontend/test/arthur/domain/channel_test.cljs @@ -4,8 +4,7 @@ in the renderer reads. These assert the parts of that claim that could silently stop being true." (:require [cljs.test :refer [deftest is testing]] - [arthur.domain.channel :as ch] - [arthur.domain.palette :as pal])) + [arthur.domain.channel :as ch])) ;; ---- the three shapes read the same way ---- @@ -393,21 +392,6 @@ (is (= {:from :day :to :night :t 0.5} (ch/value-at blend 5 nil))))) -(deftest palette-choice-channels-blend-editor-uuid-identities - (let [day (random-uuid) - night (random-uuid) - blend (assoc (ch/keyed {0 day 10 night} :linear) :semantic :palette)] - (is (empty? (ch/problems blend))) - (is (= {:from day :to night :t 0.5} - (ch/value-at blend 5 nil))))) - -(deftest palette-choice-channels-can-blend-from-inherit - (let [night (random-uuid) - blend (assoc (ch/keyed {0 pal/inherit 10 night} :linear) :semantic :palette)] - (is (empty? (ch/problems blend))) - (is (= {:from pal/inherit :to night :t 0.5} - (ch/value-at blend 5 nil))))) - (deftest numeric-channels-can-ramp-between-keys (let [c (ch/keyed {0 0.0, 10 1.0} :linear) cursor (ch/cursor c nil)] diff --git a/frontend/test/arthur/domain/instance_test.cljs b/frontend/test/arthur/domain/instance_test.cljs index 7323f9e..aec3c64 100644 --- a/frontend/test/arthur/domain/instance_test.cljs +++ b/frontend/test/arthur/domain/instance_test.cljs @@ -406,32 +406,3 @@ (is (nil? (:refused extended)) (:refused extended)) (is (= [0 6] (node/placed-span (get-in extended [:clip :symbols :palettes :nodes :change])))))) - -(deftest a-blank-palette-symbol-can-tween-from-inherit-to-an-editor-palette - (let [day (random-uuid) - night (random-uuid) - palette (fn [id color] - {:id id :name (str id) :slots [{:hex color} {:hex color}]}) - choice (assoc (ch/keyed {0 pal/inherit 4 night} :linear) :semantic :palette) - document {:fps 30 :width 20 :height 20 - :palettes {day (palette day "#000000") - night (palette night "#ffffff")} - :default-palette day - :symbols - {:main {:id :main :frames 5 :palette day :palette-track :palettes - :nodes {}} - :palettes {:id :palettes :type :palette-track :display :lane :frames 5 - :nodes {:change {:id :change :kind :instance :z "a1" - :source {:symbol :transition} - :span [0 5] :time {:at 0} - :channels {[:palette] choice}}}} - :transition {:id :transition :name "palette transition" - :type :palette :frames 1 :nodes {}}}} - context (pal/compile document) - resolve (clip/resolver document :main nil context nil)] - (is (empty? (clip/problems document))) - (resolve 2) - (is (= {:from day :to night :t 0.5} (clip/active-palette resolve))) - (is (= [128 128 128] - (nth (pal/effective-ramp context (clip/active-palette resolve)) - (pal/render-index context day 1)))))) diff --git a/frontend/test/arthur/domain/leaf_test.cljs b/frontend/test/arthur/domain/leaf_test.cljs index 0978d65..cae7576 100644 --- a/frontend/test/arthur/domain/leaf_test.cljs +++ b/frontend/test/arthur/domain/leaf_test.cljs @@ -45,7 +45,7 @@ (let [ls (leaf/leaves :c7 @take/clip)] (is (contains? ls "clip/c7/timing")) (is (contains? ls "clip/c7/stage")) - (is (contains? ls "clip/c7/analyses")) + (is (contains? ls "clip/c7/source")) ;; 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/symbol/main")) diff --git a/frontend/test/arthur/domain/node_test.cljs b/frontend/test/arthur/domain/node_test.cljs index 73d96dc..5c19862 100644 --- a/frontend/test/arthur/domain/node_test.cljs +++ b/frontend/test/arthur/domain/node_test.cljs @@ -226,20 +226,3 @@ (is (= :linear (get-in moved [:channels [:xform :rot] :interp]))) (is (= :hold (get-in visible [:channels [:vis] :interp])) "boolean parameters do not tween"))) - -(deftest colour-keys-are-discrete - (let [n {:id :x :kind :poly - :channels {[:style :color] (ch/framed 2)}} - keyed (node/set-keyed-channel n [:style :color] 3 4) - two (node/set-keyed-channel keyed [:style :color] 8 7)] - (is (= :hold (get-in two [:channels [:style :color] :interp])) - "numeric palette slots still hold") - (is (= two (node/set-segment-interp two [:style :color] 3 :linear)) - "a caller cannot introduce a colour tween") - (is (= :hold (get-in (node/set-channel - (assoc-in two [:channels [:style :color] :interp] :linear) - [:style :color] 8 6) - [:channels [:style :color] :interp])) - "editing repairs an older tweened colour channel") - (is (= :hold (get-in (node/toggle-key n [:style :color] 3 nil) - [:channels [:style :color] :interp]))))) diff --git a/frontend/test/arthur/events/lane_test.cljs b/frontend/test/arthur/events/lane_test.cljs index b4b3783..42b576d 100644 --- a/frontend/test/arthur/events/lane_test.cljs +++ b/frontend/test/arthur/events/lane_test.cljs @@ -5,7 +5,6 @@ [arthur.domain.history :as history] [arthur.domain.leaf :as leaf] [arthur.domain.node :as node] - [arthur.domain.palette :as pal] [arthur.domain.sequence-test :as fixture] [arthur.domain.span :as span] [arthur.events.ui :as ui] @@ -72,44 +71,6 @@ (vals (get-in saved [:symbols :main :nodes])))) "claiming the frame leaves no overlapping cel")))) -(deftest double-click-creation-uses-the-lane-and-playhead - (let [doc (fixture/document) - id (store/install! {:clip doc :store {}} "double-click-new-symbol")] - (reset! rf-db/app-db {:clip/current id :paint/revision 0 - :ui {:open :main} :playback {:frame 5}}) - (rf/dispatch-sync [::ui/new-symbol-at 5 [:node :main nil []]]) - (let [saved (:clip (store/entry id)) - [_ sid instance-id] (get-in @rf-db/app-db [:ui :selection]) - selection (get-in @rf-db/app-db [:ui :selection]) - instance (get-in saved [:symbols sid :nodes instance-id])] - (is (= :main sid)) - (is (= [5 6] (node/placed-span instance))) - (is (= 1 (clip/frames saved (node/source instance)))) - (is (= (node/source instance) - (:sid (creation/target saved {} :main selection 5))) - "the new cel can immediately be selected as the creation target") - (is (= 5 (get-in @rf-db/app-db [:playback :frame])) - "the playhead chooses the new cel's time")))) - -(deftest the-same-double-click-command-creates-a-palette-symbol-on-the-palette-row - (let [doc (clip/blank) - id (store/install! {:clip doc :store {}} "double-click-palette-symbol")] - (reset! rf-db/app-db {:clip/current id :paint/revision 0 - :ui {:open :main} :playback {:frame 5}}) - (rf/dispatch-sync [::ui/new-symbol-at 5 - [:arthur.ui.timeline/palette-track :main]]) - (let [saved (:clip (store/entry id)) - track-id (get-in saved [:symbols :main :palette-track]) - [_ sid instance-id] (get-in @rf-db/app-db [:ui :selection]) - instance (get-in saved [:symbols sid :nodes instance-id]) - source (node/source instance)] - (is (= track-id sid)) - (is (= :palette-track (get-in saved [:symbols track-id :type]))) - (is (= :palette (get-in saved [:symbols source :type]))) - (is (= [5 6] (node/placed-span instance))) - (is (= pal/inherit - (get-in instance [:channels [:palette] :value])))))) - (deftest a-new-lane-uses-the-symbol-selected-at-the-playhead (let [doc (clip/blank) id (store/install! {:clip doc :store {}} "nested-new-lane")] @@ -217,36 +178,6 @@ (at 25)) "a lane behind an inactive parent occurrence is not valid"))) -(deftest creation-uses-the-occurrence-path-at-arbitrary-depth - (let [instance (fn [id source span] - {:id id :kind :instance :z "a" :span span - :time {:mode :map :at 0 :rate 1} - :source {:symbol source} - :playback {:in 0 :speed 1 :end :stop}}) - doc {:fps 24 :width 20 :height 20 - :symbols - {:main {:id :main :frames 20 - :nodes {:outer (instance :outer :ordinary [0 20])}} - :ordinary {:id :ordinary :frames 20 - :nodes {:lane (instance :lane :symbol-7 [0 20])}} - :symbol-7 {:id :symbol-7 :name "symbol-7" :display :lane :frames 20 - :nodes {:cel (instance :cel :symbol-8 [4 10])}} - :symbol-8 {:id :symbol-8 :name "symbol-8" :frames 6 :nodes {}}}} - lane-selection [:node :ordinary :lane [:outer :lane]] - ;; The path, rather than these redundant owner fields, is authoritative - ;; for creation. A row address may name the occurrence from an outer - ;; view; resolution must still enter the terminal cel. - cel-selection [:node :ordinary :lane [:outer :lane :cel]] - pick #(select-keys (creation/target doc nil :main % 5) - [:kind :sid :path :frame])] - (is (= {:kind :lane :sid :symbol-7 :path [:outer :lane] :frame 5} - (pick lane-selection)) - "the nested lane header remains the lane insertion surface") - (is (= {:kind :symbol :sid :symbol-8 - :path [:outer :lane :cel] :frame 5} - (pick cel-selection)) - "the cel enters its symbol no matter how deeply the lane is nested"))) - (deftest creation-walks-out-of-an-instance-past-its-window (let [doc {:fps 24 :width 20 :height 20 :symbols @@ -394,16 +325,6 @@ (is (= #{[:already-open]} (get-in after [:ui :expanded])) "disclosure is changed only by the twist control"))) -(deftest disclosure-sets-the-state-painted-by-the-button - ;; A move can reveal a destination after the old, closed button has already - ;; received pointer-down. Its eventual click must keep the row open instead - ;; of toggling the newer state closed again. - (reset! rf-db/app-db {:ui {:expanded #{[:destination]}}}) - (rf/dispatch-sync [::ui/set-row-expanded [:destination] true]) - (is (= #{[:destination]} (get-in @rf-db/app-db [:ui :expanded]))) - (rf/dispatch-sync [::ui/set-row-expanded [:destination] false]) - (is (empty? (get-in @rf-db/app-db [:ui :expanded])))) - (deftest finishing-a-polygon-opens-no-rows ;; Expansion is the twist triangle's business. Finishing a shape used to open ;; every row down to it, which inside a lane meant tearing its one row into a diff --git a/frontend/test/arthur/flow/regenerate_test.cljs b/frontend/test/arthur/flow/regenerate_test.cljs index 12b9ab0..232e7a6 100644 --- a/frontend/test/arthur/flow/regenerate_test.cljs +++ b/frontend/test/arthur/flow/regenerate_test.cljs @@ -2,7 +2,6 @@ (:require [cljs.test :refer [deftest is testing]] [clojure.walk :as walk] [arthur.demo.stage :as stage] - [arthur.domain.bring :as bring] [arthur.domain.channel :as ch] [arthur.domain.clip :as clip] [arthur.domain.params :as params] @@ -30,41 +29,13 @@ (merge take/knobs {:name "regen" :fps 30 :aspect 1 :stage [320 200] :expose 1 :head :free - :analysis (-> @initial :clip :analyses vals first)} + :analysis (get-in @initial [:clip :analysis])} change) {:face-1 @inputs})) (defn- channel [entry node path] (get-in entry [:clip :symbols :face-1 :nodes node :channels path])) -(deftest a-second-import-regenerates-through-its-own-analysis - (let [built (fn [seed] - (let [input {:dense (synth/synth-dense frames {:seed seed})} - analysis (address/analysis - {:detector "synth" :version "mulberry32" - :seed seed :frames frames :fps 30 :aspect 1})] - [(take/build (merge take/knobs - {:name "take" :fps 30 :aspect 1 :stage [320 200] - :expose 1 :head :free :analysis analysis}) - {:face-1 input}) - input])) - [a input-a] (built 3) - [b input-b] (built 7) - first (bring/take (clip/blank) (:clip a) "shot" "footage-a" [0 frames]) - second (bring/take (:clip first) (:clip b) "shot" "footage-b" [0 frames]) - entry {:clip (:clip second) :store (merge (:store a) (:store b)) - :source-inputs {:subjects {:shot.face-1 input-a - :shot-2.face-1 input-b}}} - fid :shot-2.face-1/mouth - changed (regenerate/change entry - {:scope :feature :id fid :knob :verts :value 10}) - analysis-id (get-in changed [:clip :subjects :shot-2.face-1 :analysis]) - generated (get-in changed [:clip :symbols :shot-2.face-1 :nodes :mouth - :channels [:geom :pts] :generated])] - (is (= (get-in b [:clip :subjects :face-1 :analysis]) analysis-id)) - (is (= analysis-id (:analysis generated))) - (is (= 10 (get-in generated [:params :verts]))))) - (deftest eye-rebuild-is-confined-to-the-edited-feature (let [before @initial after (regenerate/change before @@ -199,7 +170,7 @@ (defn- part-at [area overrides] (let [p (merge take/knobs - {:fps 30 :aspect 1 :analysis (-> @initial :clip :analyses vals first)} + {:fps 30 :aspect 1 :analysis (get-in @initial [:clip :analysis])} overrides) inputs (assoc @inputs :interior @interior-track)] (freeze/part :face-1 area p (take/measure-part area p inputs (take/anchor-base p inputs))))) @@ -241,8 +212,8 @@ ;; document that cannot be previewed fails in the suite rather than as a slider ;; that silently does nothing in the browser. (let [entry (staged)] - (is (seq (:analyses (:clip entry))) - "the composed stage keeps the analyses its edits need") + (is (some? (:analysis (:clip entry))) + "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? #(= :face-1 (:symbol %)) diff --git a/static/arthur/app.css b/static/arthur/app.css index c3c82c2..5951bc7 100644 --- a/static/arthur/app.css +++ b/static/arthur/app.css @@ -1384,14 +1384,6 @@ button.share-button:hover, button.share-button.on { filter: brightness(1.1); } .facts dd.channel .key { padding: 0 3px; color: var(--dim); } .facts dd.channel .key.keyed { color: var(--fg); } .facts dd.channel .key.on { color: var(--sel); } -.facts dd.channel .channel-swatches { - display: flex; - flex: 1; - flex-wrap: wrap; - gap: 3px; - min-width: 0; -} -.facts dd.channel .channel-swatches .swatch { flex: 0 0 15px; } .facts dd.channel.live { margin: -2px; padding: 2px;