Compare commits
No commits in common. "a90e0cfb61ee32e03ae0d1b9ff6bf3e7b7349eaa" and "5bcf22e4581a6ca930f943e12422a12c78405434" have entirely different histories.
a90e0cfb61
...
5bcf22e458
36 changed files with 342 additions and 761 deletions
|
|
@ -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",)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
]
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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]}],
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)))
|
||||
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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)))))
|
||||
|
|
|
|||
|
|
@ -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]})))))
|
||||
|
|
|
|||
|
|
@ -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)))))
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]}]
|
||||
|
|
|
|||
|
|
@ -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])]
|
||||
|
|
|
|||
|
|
@ -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!))
|
||||
|
|
|
|||
|
|
@ -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])]]))
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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)))))
|
||||
|
|
|
|||
|
|
@ -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)))
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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))))))
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
|
|
|
|||
|
|
@ -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])))))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 %))
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue