Model palette tracks as typed symbol lanes
This commit is contained in:
parent
dff23d7994
commit
26fab9392e
20 changed files with 371 additions and 123 deletions
45
clips/migrations/0013_palette_track.py
Normal file
45
clips/migrations/0013_palette_track.py
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
"""Schema 4: split animated symbol palettes from the authoring palette.
|
||||
|
||||
Before schema 4 a symbol's ``:palette`` leaf value was always a channel. It now
|
||||
names the static palette used when that symbol is the viewed root, while the
|
||||
old channel is retained as ``:palette-channel`` compatibility data. New edits
|
||||
use a real lane symbol referenced by ``:palette-track``.
|
||||
"""
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def forwards(apps, schema_editor):
|
||||
Leaf = apps.get_model("clips", "Leaf")
|
||||
Project = apps.get_model("clips", "Project")
|
||||
for leaf in Leaf.objects.filter(path__contains="/symbol/"):
|
||||
parts = leaf.path.split("/")
|
||||
if len(parts) != 4 or parts[2] != "symbol":
|
||||
continue
|
||||
value = leaf.value
|
||||
if not (isinstance(value, list) and value[:1] == ["^ "]):
|
||||
continue
|
||||
out = list(value)
|
||||
changed = False
|
||||
for i in range(1, len(out) - 1, 2):
|
||||
if out[i] == "~:palette" and isinstance(out[i + 1], list):
|
||||
out[i] = "~:palette-channel"
|
||||
changed = True
|
||||
if changed:
|
||||
leaf.value = out
|
||||
leaf.version += 1
|
||||
leaf.save(update_fields=["value", "version"])
|
||||
Project.objects.update(schema_version=4)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [("clips", "0012_sound_label")]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="project",
|
||||
name="schema_version",
|
||||
field=models.PositiveIntegerField(default=4),
|
||||
),
|
||||
migrations.RunPython(forwards, migrations.RunPython.noop),
|
||||
]
|
||||
|
|
@ -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=3)
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -435,7 +435,7 @@ class DocumentTests(TestCase):
|
|||
self.assertEqual(5, len(response.json()["written"]))
|
||||
|
||||
loaded = self.client.get(f"/api/projects/{self.project.id}").json()
|
||||
self.assertEqual(3, loaded["schema_version"])
|
||||
self.assertEqual(4, loaded["schema_version"])
|
||||
self.assertEqual(1, len(loaded["clips"]))
|
||||
clip = loaded["clips"][0]
|
||||
self.assertEqual("c1", clip["cid"])
|
||||
|
|
|
|||
|
|
@ -718,28 +718,33 @@ scope does not define resolves to the loud magenta, like any other missing index
|
|||
|
||||
### The scope rule
|
||||
|
||||
`:palette` on a timeline is a channel like any other:
|
||||
`:palette-track` points to an ordinary lane symbol. Its clips are instances of
|
||||
restricted palette symbols: a palette symbol owns no nodes and points at exactly
|
||||
one project palette.
|
||||
|
||||
```clojure
|
||||
{:frames 91
|
||||
:palette {:animated? true :interp :hold :keys {0 :day, 48 :dusk, 72 :night}}
|
||||
:nodes {...}}
|
||||
{:id :shot :frames 91 :palette :day :palette-track :shot-palettes :nodes {...}}
|
||||
|
||||
{:id :shot-palettes :type :palette-track :display :lane :frames 91
|
||||
:nodes {:day-clip {:kind :instance :source {:symbol :day-palette} ...}
|
||||
:dusk-clip {:kind :instance :source {:symbol :dusk-palette} ...}}}
|
||||
|
||||
{:id :day-palette :type :palette :palette-ref :day :frames 1 :nodes {}}
|
||||
```
|
||||
|
||||
**Absent means inherit** from the instancing context. **Present means this
|
||||
timeline's content is read in that ramp, and it travels with the timeline** — a
|
||||
symbol authored against `:night` stays night wherever it is placed. That is
|
||||
lexical scope, and deliberately: a character with their own palette is a
|
||||
character, not a decoration of whichever scene they were dropped into.
|
||||
`:palette` is the symbol's authoring/preview palette. It seeds evaluation only
|
||||
when that symbol is the viewed root; nested symbols do not replace the root's
|
||||
choice merely because they were authored under another ramp. When absent, the
|
||||
project default seeds evaluation.
|
||||
|
||||
Composition is the same walk as `:time` — down the instance chain, **innermost
|
||||
set palette wins**. An enclosing timeline's palette therefore applies to
|
||||
everything inside it that does not set its own, which is adjustment-layer
|
||||
behaviour with no adjustment layer in it. It is just scope.
|
||||
|
||||
And because it is an ordinary channel, a project switches palette over time with
|
||||
keys on the root timeline, a child timeline switches on its own, and neither
|
||||
knows about the other.
|
||||
Covered clips of the viewed root's palette track override that seed. An
|
||||
uncovered lane interval is a genuine gap, restoring the authoring palette or
|
||||
project default. Palette clips use the same trim, roll, slide, claim-time and
|
||||
undo commands as visual clips; palette code does not duplicate those edits.
|
||||
Thus palette-track coverage, authoring preview, and project fallback are
|
||||
separate facts rather than three accidental meanings of one field. There is no
|
||||
second keyed palette control on symbols or instances: time-varying palette
|
||||
changes are authored only as clips in the palette lane.
|
||||
|
||||
### One index space, partitioned by palette
|
||||
|
||||
|
|
|
|||
|
|
@ -249,7 +249,40 @@
|
|||
native node frames and world matrices for the last rendered output frame."
|
||||
[clip sid store palette opts]
|
||||
(let [context? (and (map? palette) (:palettes palette) (:offsets palette))]
|
||||
(letfn [(selection-at [owner frame inherited]
|
||||
(letfn [(channel-value [selection frame]
|
||||
(cond
|
||||
(nil? selection) nil
|
||||
(and (map? selection) (contains? selection :animated?))
|
||||
(ch/value-at selection frame store)
|
||||
:else selection))
|
||||
(root-selection-at [owner frame inherited]
|
||||
;; 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 [track (:palette-channel owner)
|
||||
track-value (if-let [ks (:keys track)]
|
||||
(some->> (keys ks)
|
||||
(filter #(<= % frame))
|
||||
sort last
|
||||
(get ks))
|
||||
(channel-value track frame))]
|
||||
(or (when-let [track-sid (and (keyword? (:palette-track owner))
|
||||
(:palette-track owner))]
|
||||
(let [track-symbol (symbol clip track-sid)
|
||||
palette-clip (first
|
||||
(filter (fn [n]
|
||||
(let [[a b] (node/placed-span n)]
|
||||
(and a (<= a frame) (< frame b))))
|
||||
(symbol/children (:nodes track-symbol))))
|
||||
palette-symbol (some-> palette-clip node/source
|
||||
(#(symbol clip %)))]
|
||||
(when (= :palette (:type palette-symbol))
|
||||
(:palette-ref palette-symbol))))
|
||||
track-value
|
||||
(channel-value (:palette owner) frame)
|
||||
inherited
|
||||
(:default palette))))
|
||||
(selection-at [owner frame inherited]
|
||||
(let [selection (:palette owner)
|
||||
chosen (cond
|
||||
(nil? selection) nil
|
||||
|
|
@ -257,7 +290,7 @@
|
|||
(ch/value-at selection frame store)
|
||||
:else selection)]
|
||||
(or chosen inherited (:default palette))))
|
||||
(build [sid chain pose-tracks]
|
||||
(build [sid chain pose-tracks root?]
|
||||
(when (some #{sid} chain)
|
||||
(throw (ex-info "symbol cycle" {:chain (conj chain sid)})))
|
||||
(let [sym (or (symbol clip sid)
|
||||
|
|
@ -276,7 +309,7 @@
|
|||
:when (= :instance (:kind n))
|
||||
child (sort-by str (node/sources n))]
|
||||
[[id child] (build child (conj chain sid)
|
||||
(get-in n [:playback :tracks]))]))
|
||||
(get-in n [:playback :tracks]) false)]))
|
||||
;; The instances that were on the last frame, and WHICH
|
||||
;; drawing each was showing — a row path is read back through
|
||||
;; the child that was actually resolved, not the only one
|
||||
|
|
@ -286,7 +319,10 @@
|
|||
step (fn [f pre inherited forced]
|
||||
(when context?
|
||||
(vreset! active (or forced
|
||||
(selection-at sym (js/Math.floor f) inherited))))
|
||||
(if root?
|
||||
(root-selection-at sym (js/Math.floor f) inherited)
|
||||
inherited)
|
||||
(:default palette))))
|
||||
(vreset! entered {})
|
||||
(let [by-id (into {} (map (juxt :node identity))
|
||||
(own (js/Math.floor f) (js/Math.floor pre)))]
|
||||
|
|
@ -349,7 +385,7 @@
|
|||
(when (contains? @entered id)
|
||||
(symbol/pre-frame-of (get children [id (get @entered id)]) (vec more)))
|
||||
(symbol/pre-frame-of own id))))))]
|
||||
(let [r (build sid [] nil)
|
||||
(let [r (build sid [] nil true)
|
||||
grid (or (:grid-fps opts) (:fps clip))
|
||||
native (fps clip sid)]
|
||||
(reify
|
||||
|
|
|
|||
|
|
@ -148,7 +148,8 @@
|
|||
;; disagree with itself about; `clip` puts it back.
|
||||
(for [[sid sym] (:symbols clip)]
|
||||
{(at "symbol" (segment sid))
|
||||
(select-keys sym [:name :frames :fps :width :height :palette :display])})
|
||||
(select-keys sym [:name :frames :fps :width :height :palette
|
||||
:palette-track :palette-channel :type :palette-ref :display])})
|
||||
(for [[sid sym] (:symbols clip)
|
||||
[id n] (:nodes sym)]
|
||||
{(at "symbol" (segment sid) "node" (segment id))
|
||||
|
|
|
|||
|
|
@ -319,6 +319,8 @@
|
|||
(nil? n) "nothing to move"
|
||||
(or (nil? here) (nil? there)) "both have to be on screen at this frame"
|
||||
(nil? (:sid there)) "only a symbol can take it"
|
||||
(span/placement-refusal clip (:sid there) n)
|
||||
(span/placement-refusal clip (:sid there) n)
|
||||
(not (and (:time here) (:time there)))
|
||||
"a held or looping clip has no clock to move through"
|
||||
(nil? (some-> there :matrix node/invert)) "the target is scaled to nothing"
|
||||
|
|
|
|||
|
|
@ -79,6 +79,10 @@
|
|||
used (into #{(default-palette-id clip)}
|
||||
(mapcat selection-values)
|
||||
(concat (map :palette (vals (:symbols clip)))
|
||||
(map :palette-channel (vals (:symbols clip)))
|
||||
(map (fn [sym]
|
||||
(when (= :palette (:type sym)) (:palette-ref sym)))
|
||||
(vals (:symbols clip)))
|
||||
(for [sym (vals (:symbols clip))
|
||||
n (vals (:nodes sym))]
|
||||
(:palette n))))
|
||||
|
|
|
|||
|
|
@ -30,9 +30,8 @@
|
|||
[arthur.domain.wire :as wire]))
|
||||
|
||||
(def schema-version
|
||||
"3 stores cel source references and explicit playback clocks. Older
|
||||
source-channel documents are unsupported; there is no compatibility conversion."
|
||||
3)
|
||||
"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."
|
||||
|
|
|
|||
|
|
@ -501,6 +501,25 @@
|
|||
nodes later)]
|
||||
(finish clip sid nodes id extent)))))
|
||||
|
||||
(defn placement-refusal
|
||||
"Why direct child `n` may not be placed in symbol `sid`, or nil.
|
||||
|
||||
This is the type-containment boundary for every placement path. Specialized
|
||||
symbols are confined here rather than by drag affordances, so pool drops,
|
||||
transfers, structural nesting and future commands cannot disagree."
|
||||
[clip sid n]
|
||||
(let [destination (clip/symbol clip sid)
|
||||
source (when (= :instance (:kind n)) (clip/symbol clip (node/source n)))
|
||||
destination-type (:type destination)
|
||||
source-type (:type source)]
|
||||
(cond
|
||||
(= :palette destination-type) "a palette symbol cannot contain nodes"
|
||||
(and (= :palette-track destination-type) (not= :palette source-type))
|
||||
"a palette track accepts only palette symbols"
|
||||
(and (= :palette source-type) (not= :palette-track destination-type))
|
||||
"a palette symbol can be placed only in a palette track"
|
||||
:else nil)))
|
||||
|
||||
(defn place-node
|
||||
"Place the already-materialized, direct child `n` into symbol `sid` at `at`.
|
||||
|
||||
|
|
@ -519,10 +538,12 @@
|
|||
nodes (:nodes sym)
|
||||
id (:id n)
|
||||
[lo hi] (when n (node/placed-span n))
|
||||
duration (when (and lo hi) (- hi lo))]
|
||||
duration (when (and lo hi) (- hi lo))
|
||||
incompatible (when (and sym n) (placement-refusal clip sid n))]
|
||||
(cond
|
||||
(nil? sym) {:refused "there is no destination symbol"}
|
||||
(nil? n) {:refused "there is no clip to place"}
|
||||
incompatible {:refused incompatible}
|
||||
(contains? nodes id) {:refused "the destination already uses that clip ID"}
|
||||
(some? (:parent n)) {:refused "only a direct child can be placed in a symbol"}
|
||||
(not (and (integer? at) (not (neg? at))))
|
||||
|
|
|
|||
|
|
@ -709,7 +709,8 @@
|
|||
`:display` is how the TIMELINE draws the symbol — `:lane` for its clips as
|
||||
blocks on one row — and is saved because two people editing one document must
|
||||
play by the same editing rules. See `lane?`."
|
||||
#{:id :name :frames :fps :width :height :nodes :palette :display})
|
||||
#{:id :name :frames :fps :width :height :nodes :palette :palette-track
|
||||
:palette-channel :type :palette-ref :display})
|
||||
|
||||
(defn problems
|
||||
"Human-readable reasons this symbol will not evaluate. Empty means it will.
|
||||
|
|
@ -727,6 +728,11 @@
|
|||
(if-not (map? nodes)
|
||||
[":nodes must be a map of id -> node"]
|
||||
(-> []
|
||||
(cond->
|
||||
(and (= :palette (:type sym)) (seq 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)))))
|
||||
|
|
|
|||
|
|
@ -714,39 +714,49 @@
|
|||
|
||||
(rf/reg-event-db
|
||||
::symbol-palette
|
||||
(fn [db [_ sid id frame]]
|
||||
(fn [db [_ sid id]]
|
||||
(edit/edit db #(if id
|
||||
(let [old (get-in % [:symbols sid :palette])]
|
||||
(assoc-in % [:symbols sid :palette]
|
||||
(if (:keys old)
|
||||
(assoc-in old [:keys frame] id)
|
||||
(ch/framed id))))
|
||||
(assoc-in % [:symbols sid :palette] id)
|
||||
(update-in % [:symbols sid] dissoc :palette)))))
|
||||
|
||||
(rf/reg-event-db
|
||||
::key-symbol-palette
|
||||
(fn [db [_ sid frame id]]
|
||||
(edit/edit db
|
||||
(fn [c]
|
||||
(let [old (get-in c [:symbols sid :palette])
|
||||
base (or (when old (ch/value-at old frame nil))
|
||||
(pal/default-palette-id c))
|
||||
keyed (if (:keys old) old (ch/keyed {0 base} :hold))]
|
||||
(assoc-in c [:symbols sid :palette]
|
||||
(assoc-in keyed [:keys frame] id)))))))
|
||||
|
||||
(rf/reg-event-db
|
||||
::instance-palette
|
||||
(fn [db [_ sid node-id frame id]]
|
||||
(edit/edit db
|
||||
(fn [c]
|
||||
(if-not id
|
||||
(update-in c [:symbols sid :nodes node-id] dissoc :palette)
|
||||
(let [old (get-in c [:symbols sid :nodes node-id :palette])]
|
||||
(assoc-in c [:symbols sid :nodes node-id :palette]
|
||||
(if (:keys old)
|
||||
(assoc-in old [:keys frame] id)
|
||||
(ch/framed id)))))))))
|
||||
::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)})]
|
||||
(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
|
||||
|
|
|
|||
|
|
@ -855,18 +855,18 @@
|
|||
::sliding
|
||||
;; A bar in the middle of a slide, drawn by `::render/clip`; nil path when the
|
||||
;; drag is abandoned.
|
||||
(fn [db [_ path df kind ripple? other]]
|
||||
(fn [db [_ path df kind ripple? other owner]]
|
||||
(if path
|
||||
(assoc-in db [:ui :sliding] {:path path :df df :kind (or kind :slide)
|
||||
:ripple? (boolean ripple?) :other other})
|
||||
:ripple? (boolean ripple?) :other other :owner owner})
|
||||
(update db :ui dissoc :sliding))))
|
||||
|
||||
(rf/reg-event-db
|
||||
::slide
|
||||
(fn [db [_ path df kind ripple? other]]
|
||||
(fn [db [_ path df kind ripple? other owner]]
|
||||
(let [db (update db :ui dissoc :sliding)
|
||||
clip (:clip (store/entry (:clip/current db)))
|
||||
open (get-in db [:ui :open])
|
||||
open (or owner (get-in db [:ui :open]))
|
||||
r (case kind
|
||||
:out (nest/resize-out clip open path df ripple?)
|
||||
:in (nest/resize-in clip open path df)
|
||||
|
|
|
|||
|
|
@ -44,12 +44,13 @@
|
|||
;; lets go, so the stage and the rows follow the pointer. Nothing is written
|
||||
;; until then: one drag is one undo step and one write to collaborators.
|
||||
(let [{c :clip st :store} (footage/entry id)]
|
||||
(or (when-let [{:keys [path df kind ripple? other]} sliding]
|
||||
(:clip (case kind
|
||||
:out (nest/resize-out c open path df ripple?)
|
||||
:in (nest/resize-in c open path df)
|
||||
:roll (nest/roll c open other path df)
|
||||
(nest/slide c open path df))))
|
||||
(or (when-let [{:keys [path df kind ripple? other owner]} sliding]
|
||||
(let [root (or owner open)]
|
||||
(:clip (case kind
|
||||
:out (nest/resize-out c root path df ripple?)
|
||||
:in (nest/resize-in c root path df)
|
||||
:roll (nest/roll c root other path df)
|
||||
(nest/slide c root path df)))))
|
||||
(when-let [{:keys [sid id frame values edits]} gesture]
|
||||
;; A held stage control owns the touched parameters completely. Make
|
||||
;; them temporary static channels for the preview, so their existing
|
||||
|
|
|
|||
|
|
@ -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!))
|
||||
|
|
|
|||
|
|
@ -218,16 +218,6 @@
|
|||
"span" (when start (str start " … " end))
|
||||
"at" (when (node/mapped-time? n) (str (get-in n [:time :at] 0)))]
|
||||
(when (:paint? n) [drawing-keys sid id n @(rf/subscribe [::sub/selected-local])])
|
||||
(when (= :instance (:kind n))
|
||||
[:label.inspector-field "palette override"
|
||||
[:select {:value (str (or (some-> (:palette n) (channel/value-at frame nil)) ""))
|
||||
:on-change (fn [e]
|
||||
(let [v (.. e -target -value)
|
||||
pid (first (filter #(= v (str %)) (keys (pal/palettes clip))))]
|
||||
(rf/dispatch [::project/instance-palette sid id frame pid])))}
|
||||
[:option {:value ""} "inherit"]
|
||||
(for [[pid p] (sort-by (comp str :name val) (pal/palettes clip))]
|
||||
^{:key (str pid)} [:option {:value (str pid)} (:name p)])]])
|
||||
[:div.row {:style {:margin-top "6px"}} [:span.dim "channels"]]
|
||||
(let [{:keys [frame]} local]
|
||||
[:dl.facts
|
||||
|
|
@ -538,14 +528,12 @@
|
|||
;; ---------------------------------------------------------------------------
|
||||
;; a symbol
|
||||
|
||||
(defn- symbol-section [sid]
|
||||
(defn- symbol-section [sid & [title]]
|
||||
(let [clip @(rf/subscribe [::render/clip])
|
||||
sym (get-in clip [:symbols sid])
|
||||
frame @(rf/subscribe [::render/open-frame])
|
||||
palette-value (or (some-> (:palette sym) (channel/value-at frame nil))
|
||||
(pal/default-palette-id clip))
|
||||
busy? (:busy? @(rf/subscribe [::playback/project]))]
|
||||
[section "symbol"
|
||||
[section (or title "symbol")
|
||||
[facts
|
||||
"id" (str sid)
|
||||
"length" (str (:frames sym) " frames at " (clip-domain/fps clip sid) " fps")
|
||||
|
|
@ -563,23 +551,15 @@
|
|||
#(rf/dispatch [::project/symbol-setting sid :width %]) "project default" busy?]
|
||||
[number-field "height" (:height sym)
|
||||
#(rf/dispatch [::project/symbol-setting sid :height %]) "project default" busy?]
|
||||
[:label.inspector-field "palette"
|
||||
[:select {:value (str palette-value) :disabled busy?
|
||||
[:label.inspector-field "authoring palette"
|
||||
[:select {:value (str (or (:palette sym) "")) :disabled busy?
|
||||
:on-change (fn [e]
|
||||
(let [v (.. e -target -value)
|
||||
id (first (filter #(= v (str %)) (keys (pal/palettes clip))))]
|
||||
(rf/dispatch [::project/symbol-palette sid id frame])))}
|
||||
(rf/dispatch [::project/symbol-palette sid id])))}
|
||||
[:option {:value ""} "inherit"]
|
||||
(for [[id p] (sort-by (comp str :name val) (pal/palettes clip))]
|
||||
^{:key (str id)} [:option {:value (str id)} (:name p)])]]
|
||||
[:div.row
|
||||
[:button {:disabled busy?
|
||||
:title "hold this palette from this frame"
|
||||
:on-click #(rf/dispatch [::project/key-symbol-palette sid frame palette-value])}
|
||||
"◆ key palette"]
|
||||
[:button {:disabled (or busy? (nil? (:palette sym)))
|
||||
:on-click #(rf/dispatch [::project/symbol-palette sid nil])}
|
||||
"inherit"]]
|
||||
(when (or (:width sym) (:height sym))
|
||||
[:div.row
|
||||
[:button {:disabled busy? :on-click #(do
|
||||
|
|
@ -587,6 +567,28 @@
|
|||
(rf/dispatch [::project/symbol-setting sid :height nil]))}
|
||||
"use project stage"]])]]))
|
||||
|
||||
(defn- symbol-parameters-section [sid frame title]
|
||||
(let [clip @(rf/subscribe [::render/clip])
|
||||
auto-key? @(rf/subscribe [::sub/auto-key?])
|
||||
nodes (get-in clip [:symbols sid :nodes])]
|
||||
(when (and sid (number? frame) (seq nodes))
|
||||
(into
|
||||
[section title]
|
||||
(for [[id n] (sort-by (comp str key) nodes)
|
||||
:let [editable (filter (fn [[path ch]]
|
||||
(and (contains? (node/defaults-of n) path)
|
||||
(not (:dense ch))))
|
||||
(node/channels n))]
|
||||
:when (seq editable)]
|
||||
^{:key (str id)}
|
||||
(into
|
||||
[:div.inspector-form
|
||||
[:div.row [:span.dim (or (:name n) (brief id))]]]
|
||||
(for [[path ch] editable]
|
||||
^{:key (str path)}
|
||||
[:label.inspector-field (str/join " " (map name path))
|
||||
[channel-control sid id path ch frame auto-key?]])))))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; tracked objects
|
||||
;;
|
||||
|
|
@ -706,6 +708,11 @@
|
|||
[:div {:style {:min-height 0}}
|
||||
[clip-section]
|
||||
(when node [node-section node])
|
||||
(when placed [symbol-section placed "source symbol"])
|
||||
(when placed
|
||||
(let [{source-sid :sid source-frame :frame} @(rf/subscribe [::sub/selected-local])]
|
||||
(when (= placed source-sid)
|
||||
[symbol-parameters-section placed source-frame "source parameters"])))
|
||||
(when node ^{:key (str (first node) "/" (second node))}
|
||||
[correction-section node])
|
||||
(when (seq here) [footage-section here])
|
||||
|
|
@ -713,4 +720,9 @@
|
|||
[tracing-section face faces path])
|
||||
(when face ^{:key (str "perf/" face)} [performance-section face])
|
||||
(when (= :symbol (first selection)) [symbol-section (second selection)])
|
||||
(when (= :symbol (first selection))
|
||||
(let [sid (second selection)
|
||||
frame (min (max 0 @(rf/subscribe [::render/open-frame]))
|
||||
(max 0 (dec (or (clip-domain/frames clip sid) 1))))]
|
||||
[symbol-parameters-section sid frame "symbol parameters"]))
|
||||
(when tracked? [tracking-section])]]))
|
||||
|
|
|
|||
|
|
@ -272,9 +272,10 @@
|
|||
|
||||
(defn- palette-row [id p default-id chosen rename]
|
||||
^{:key (str id)}
|
||||
[row {:label (:name p)
|
||||
[row (merge {:label (:name p)
|
||||
:sub (str (count (:slots p)) " colors")
|
||||
:title (str (:name p) " · " (count (:slots p)) " indexed 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)))]
|
||||
|
|
@ -285,7 +286,8 @@
|
|||
((: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])}])
|
||||
:on-double-click #(rf/dispatch [::project/default-palette id])}
|
||||
(carrying (str "palette:" id) #(drag/palette! id (:name p))))])
|
||||
|
||||
(defn- footage-row [{:keys [id label frames fps video] :as f} chosen rename]
|
||||
^{:key id}
|
||||
|
|
|
|||
|
|
@ -332,6 +332,18 @@
|
|||
(walk sid [] 0 identity)
|
||||
[])))))
|
||||
|
||||
(defn- palette-track-row [clip sid frames expanded chosen]
|
||||
(let [track-id (get-in clip [:symbols sid :palette-track])
|
||||
track (get-in clip [:symbols track-id])]
|
||||
(if (= :palette-track (:type track))
|
||||
(-> (first (rows clip track-id expanded chosen))
|
||||
(assoc :path [::palette-track] :owner track-id
|
||||
:label "palette" :kind :palette
|
||||
:select [::palette-track sid]))
|
||||
{:path [::palette-track] :depth 0 :label "palette" :kind :palette
|
||||
:owner nil :select [::palette-track sid]
|
||||
:span [0 frames] :keys [] :cels []})))
|
||||
|
||||
(defn sound-rows
|
||||
"Audio rows use the same flattened intervals as the mixer, including source
|
||||
in-points, cel speeds, parent timing, and silence beneath visual holds.
|
||||
|
|
@ -705,7 +717,7 @@
|
|||
`:from` is the frame the press was on and `:df` how many frames the pointer has
|
||||
moved since. What it looks like mid-drag is `[:ui :sliding]`, which the clip
|
||||
every row and the stage are drawn from already has in it."
|
||||
[{:keys [path span keys dense? kind node-kind select slides cels lane? of unmapped?]}
|
||||
[{:keys [path span keys dense? kind node-kind select slides cels lane? of unmapped? owner]}
|
||||
frames sliding hint {:keys [clip store open frame selection]}]
|
||||
(let [active-row (:row @sliding)
|
||||
slide (fn [^js e]
|
||||
|
|
@ -784,7 +796,7 @@
|
|||
(swap! sliding assoc :df df)
|
||||
(rf/dispatch [::ui/sliding (:path @sliding) df
|
||||
(:kind @sliding) (:ripple? @sliding)
|
||||
(:other @sliding)]))))))))
|
||||
(:other @sliding) owner]))))))))
|
||||
done (fn [commit?]
|
||||
(when (= path (:row @sliding))
|
||||
(let [{:keys [path df kind ripple? other target-lane target-frame
|
||||
|
|
@ -808,7 +820,7 @@
|
|||
;; selected. The two structural cases above select what
|
||||
;; they landed, so neither needs this.
|
||||
(do (when on-click (rf/dispatch [::ui/select on-click]))
|
||||
(rf/dispatch [::ui/slide path df kind ripple? other]))
|
||||
(rf/dispatch [::ui/slide path df kind ripple? other owner]))
|
||||
:else (rf/dispatch [::ui/sliding nil])))))
|
||||
begin! (fn [^js e actual-path gesture-kind actual-select other drag]
|
||||
(let [track (.closest (.-currentTarget e) ".tl-track")]
|
||||
|
|
@ -839,7 +851,7 @@
|
|||
[:div.tl-track
|
||||
;; The track, not the bar, holds the pointer while a bar slides, so the drag
|
||||
;; goes on when the bar has slid off the ruler and is no longer drawn.
|
||||
{:class (when lane? "lane")
|
||||
{:class (str (when lane? "lane") (when (= :palette kind) " palette"))
|
||||
:on-pointer-move slide
|
||||
:on-pointer-up (fn [e] (slide e) (done true))
|
||||
:on-pointer-cancel (fn [_] (done false))
|
||||
|
|
@ -854,28 +866,40 @@
|
|||
(rf/dispatch [::pb/open-symbol source])))
|
||||
:ref (when lane? (fn [el] (when el (aset el "arthurLane" select))))
|
||||
:on-drag-enter (fn [^js e]
|
||||
(when (and lane? (or (drag/accepts?) (drag/row)))
|
||||
(.preventDefault e)
|
||||
(.stopPropagation e)
|
||||
(when (drag/accepts?)
|
||||
(drag/hover! :timeline (frame-at e frames) nil select))))
|
||||
(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]
|
||||
(when (and lane? (or (drag/accepts?) (drag/row)))
|
||||
(.preventDefault e)
|
||||
(.stopPropagation e)
|
||||
(set! (.. e -dataTransfer -dropEffect)
|
||||
(if (drag/row) "move" "copy"))
|
||||
(when (drag/accepts?)
|
||||
(drag/hover! :timeline (frame-at e frames) nil select))))
|
||||
(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)
|
||||
(set! (.. e -dataTransfer -dropEffect)
|
||||
(if (drag/row) "move" "copy"))
|
||||
(when (drag/accepts?)
|
||||
(drag/hover! :timeline (frame-at e frames) nil select)))))
|
||||
:on-drop (fn [^js e]
|
||||
(when (and lane? (or (drag/accepts?) (drag/row)))
|
||||
(.preventDefault e)
|
||||
(.stopPropagation e)
|
||||
(if-let [from (drag/row-selection)]
|
||||
(do (drag/done!)
|
||||
(rf/dispatch [::ui/drop-clip from select
|
||||
(frame-at e frames)]))
|
||||
(drag/land! (frame-at e frames) nil select))))}
|
||||
(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)
|
||||
(.stopPropagation e)
|
||||
(if-let [from (drag/row-selection)]
|
||||
(do (drag/done!)
|
||||
(rf/dispatch [::ui/drop-clip from select
|
||||
(frame-at e frames)]))
|
||||
(drag/land! (frame-at e frames) nil select)))))}
|
||||
;; Clipped to the ruler: an instance longer than the room left in its
|
||||
;; symbol still plays its own frames from 0, it is just cut off at the end.
|
||||
(when-let [[in out] (when (and span (nil? cels)) [(max 0 (first span)) (min frames (second span))])]
|
||||
|
|
@ -1052,13 +1076,19 @@
|
|||
{:id ::drop :label (str "+ " (:label drop)) :ghost? true
|
||||
:span drop-span})
|
||||
chosen (when (= :node (first selection)) (nth selection 3))
|
||||
picture (cond->> (cond->> (rows clip open expanded chosen)
|
||||
picture (cond->> (cond->> (into [(palette-track-row clip open frames expanded chosen)]
|
||||
(rows clip open expanded chosen))
|
||||
lane-ghost
|
||||
(mapv (fn [row]
|
||||
(if (= drop-lane (:select row))
|
||||
(update row :cels (fnil conj []) lane-ghost)
|
||||
row))))
|
||||
(and ghost (not (:sound? drop))) (cons ghost))
|
||||
;; The palette track is a permanent master row. An
|
||||
;; untargeted visual drop becomes a new ordinary row and
|
||||
;; therefore previews immediately BELOW it, where the
|
||||
;; committed row will appear—not above the master row.
|
||||
(and ghost (not (:sound? drop)))
|
||||
((fn [rows] (into [(first rows) ghost] (rest rows)))))
|
||||
sounds (cond->> (into (vec (filter :sound? picture))
|
||||
(sound-rows clip open expanded))
|
||||
(and ghost (:sound? drop)) (cons ghost))
|
||||
|
|
|
|||
|
|
@ -318,3 +318,40 @@
|
|||
"the inheriting child follows the lightning cut; an instance override wins")
|
||||
(is (= [17 34 51] (nth (:ramp context) 1)))
|
||||
(is (= [170 187 204] (nth (:ramp context) 3)))))
|
||||
|
||||
(deftest symbol-authoring-palettes-seed-only-the-view-root
|
||||
(let [palette (fn [id name color]
|
||||
{:id id :name name :slots [{:hex color} {:hex color}]})
|
||||
mark {:id :mark :kind :rect :z "a1"
|
||||
:channels {[:geom :size] (ch/framed 2)
|
||||
[:style :color] (ch/framed 1)}}
|
||||
document {:fps 30 :width 20 :height 20
|
||||
:palettes {:day (palette :day "Day" "#112233")
|
||||
:night (palette :night "Night" "#aabbcc")}
|
||||
:default-palette :day
|
||||
:symbols
|
||||
{:main {:id :main :frames 4 :palette :day :palette-track :palette-lane
|
||||
:nodes {:child {:id :child :kind :instance :z "a1"
|
||||
:source {:symbol :drawing}}}}
|
||||
:palette-lane {:id :palette-lane :name "palette"
|
||||
:type :palette-track :display :lane :frames 4
|
||||
:nodes {:night {:id :night :kind :instance :z "a1"
|
||||
:source {:symbol :night-palette}
|
||||
:span [0 1] :time {:at 1}}}}
|
||||
:night-palette {:id :night-palette :name "Night"
|
||||
:type :palette :palette-ref :night
|
||||
:frames 1 :nodes {}}
|
||||
:drawing {:id :drawing :frames 4 :palette :night
|
||||
:nodes {:mark mark}}}}
|
||||
context (pal/compile document)
|
||||
main (clip/resolver document :main nil context nil)
|
||||
drawing (clip/resolver document :drawing nil context nil)
|
||||
color #(-> (%1 %2) first :color)]
|
||||
(is (= (pal/render-index context :day 1) (color main 0))
|
||||
"a nested symbol's authoring palette does not override the viewed root")
|
||||
(is (= (pal/render-index context :night 1) (color main 1))
|
||||
"a covered palette-track interval overrides the root fallback")
|
||||
(is (= (pal/render-index context :day 1) (color main 2))
|
||||
"a nil track interval is a real gap and restores the authoring palette")
|
||||
(is (= (pal/render-index context :night 1) (color drawing 0))
|
||||
"the same nested symbol uses its authoring palette when viewed directly")))
|
||||
|
|
|
|||
|
|
@ -90,3 +90,26 @@
|
|||
(deftest host-frame-of-a-parentless-clip-is-the-symbol-frame
|
||||
(is (= 6 (span/host-frame (document) :main :b 6)))
|
||||
(is (= 6 (span/host-frame (ordinary-document) :main :badge 6))))
|
||||
|
||||
(deftest typed-symbol-containment-applies-to-every-transfer
|
||||
(let [palette-node {:id :p :kind :instance :z "a1"
|
||||
:source {:symbol :night-palette}
|
||||
:span [0 1] :time {:at 0}}
|
||||
visual-node {:id :v :kind :instance :z "a1"
|
||||
:source {:symbol :drawing}
|
||||
:span [0 1] :time {:at 0}}
|
||||
doc {:fps 30 :width 20 :height 20
|
||||
:symbols
|
||||
{:root {:id :root :frames 8 :nodes {}}
|
||||
:ordinary-lane {:id :ordinary-lane :frames 8 :display :lane :nodes {}}
|
||||
:palette-lane {:id :palette-lane :frames 8 :display :lane
|
||||
:type :palette-track :nodes {:p palette-node}}
|
||||
:night-palette {:id :night-palette :frames 1 :type :palette
|
||||
:palette-ref :night :nodes {}}
|
||||
:drawing {:id :drawing :frames 1 :nodes {}}}}]
|
||||
(is (:refused (span/transfer doc :palette-lane :p :root 0 {}))
|
||||
"a palette clip cannot escape to a composition root")
|
||||
(is (:refused (span/transfer doc :palette-lane :p :ordinary-lane 0 {}))
|
||||
"a palette clip cannot escape to an ordinary lane")
|
||||
(is (:refused (span/place-node doc :palette-lane visual-node 0 {}))
|
||||
"a palette lane refuses ordinary visual clips")))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue