Create in a lane at the frame double-clicked, palette lanes included
The gesture was handed the pointer's frame and threw it away, creating at the playhead instead; and the palette row carried :lane? only once its track existed, so the first double-click on a palette lane -- the one that has to make the track -- dispatched nothing at all. Both are one rule now. ::new-symbol-at uses the frame it is given and resolves every row through drop-destination, with the destination deciding what is created: a clip of a palette track is a palette symbol, a clip of any other lane is blank. A row with no path of its own resolves to the symbol it names, which is what lets a palette track -- hanging off its owner by :palette-track rather than placed in it -- be reached without a special case; it also stops a palette cel's slide resolving against the open symbol and looking like a transfer out of the track. The one thing left that knows about palettes is materializing the lane a palette row names before anything asks where the row leads. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
09b74de162
commit
90b1fbe2f8
3 changed files with 89 additions and 36 deletions
|
|
@ -760,8 +760,13 @@
|
||||||
(nil? target) []
|
(nil? target) []
|
||||||
(= :instance (get-in document [:symbols sid :nodes id :kind])) path0
|
(= :instance (get-in document [:symbols sid :nodes id :kind])) path0
|
||||||
:else (vec (butlast path0)))
|
:else (vec (butlast path0)))
|
||||||
|
;; A ROW WITH NO PATH IS THE SYMBOL IT NAMES, on this very frame: the
|
||||||
|
;; open symbol's own row, and a palette track's, which hangs off its
|
||||||
|
;; owner by `:palette-track` instead of being placed in it and so is
|
||||||
|
;; reached by naming it rather than by walking to it.
|
||||||
|
from (if (seq path) open (or sid open))
|
||||||
{destination :sid at :frame matrix :matrix}
|
{destination :sid at :frame matrix :matrix}
|
||||||
(nest/inside document st open path frame)]
|
(nest/inside document st from path frame)]
|
||||||
(cond
|
(cond
|
||||||
(nil? destination) {:refused "what you are dropping into is not on screen at this frame"}
|
(nil? destination) {:refused "what you are dropping into is not on screen at this frame"}
|
||||||
(not (integer? at)) {:refused "the drop is not on one frame of that symbol"}
|
(not (integer? at)) {:refused "the drop is not on one frame of that symbol"}
|
||||||
|
|
@ -802,34 +807,45 @@
|
||||||
(edit/transaction (constantly (:clip result)))
|
(edit/transaction (constantly (:clip result)))
|
||||||
(selected [:node sid uuid (conj (vec path) uuid)]))))
|
(selected [:node sid uuid (conj (vec path) uuid)]))))
|
||||||
|
|
||||||
|
(defn- lane-of-palette-row
|
||||||
|
"`document` and `target` with the palette row of a symbol that has no palette
|
||||||
|
track yet replaced by that track, created here.
|
||||||
|
|
||||||
|
THE ONE ROW THAT MAY NAME A LANE THAT DOES NOT EXIST, and the only thing
|
||||||
|
anything downstream needs to know about palettes: materializing it turns that
|
||||||
|
row into an ordinary lane row, so one gesture resolves and places through one
|
||||||
|
rule. The track borrows its owner's clock because it is read frame for frame
|
||||||
|
against it."
|
||||||
|
[document target]
|
||||||
|
(if-let [owner (when (= :arthur.ui.timeline/palette-track (first target))
|
||||||
|
(second target))]
|
||||||
|
(let [track (or (get-in document [:symbols owner :palette-track])
|
||||||
|
(clip/fresh-id document))]
|
||||||
|
[(-> document
|
||||||
|
(assoc-in [:symbols owner :palette-track] track)
|
||||||
|
;; What is there already wins, so this fills a track in and can never
|
||||||
|
;; empty one.
|
||||||
|
(update-in [:symbols track]
|
||||||
|
#(merge {:id track :name "palette" :type :palette-track
|
||||||
|
:display :lane :frames (clip/frames document owner)
|
||||||
|
:fps (clip/fps document owner) :nodes {}}
|
||||||
|
%)))
|
||||||
|
[:node track nil []]])
|
||||||
|
[document target]))
|
||||||
|
|
||||||
(rf/reg-event-db
|
(rf/reg-event-db
|
||||||
::new-symbol-at
|
::new-symbol-at
|
||||||
;; One gesture and one creation path for every lane. The destination decides
|
;; ONE GESTURE AND ONE CREATION PATH FOR EVERY LANE. Empty space in a lane
|
||||||
;; the kind: an ordinary lane gets a blank symbol; the synthetic palette row
|
;; takes a new one-frame symbol at the frame double-clicked -- the pointer
|
||||||
;; gets a blank palette symbol whose placement starts by inheriting.
|
;; names a place in time as well as a lane, which is the whole of the gesture
|
||||||
(fn [db [_ _pointer-frame target]]
|
;; and the reason it is not the playhead's. The DESTINATION decides what is
|
||||||
|
;; created there: a clip of a palette track is a palette symbol whose
|
||||||
|
;; placement starts by inheriting, a clip of any other lane is a blank symbol.
|
||||||
|
(fn [db [_ frame target]]
|
||||||
(let [{document :clip st :store} (store/entry (:clip/current db))
|
(let [{document :clip st :store} (store/entry (:clip/current db))
|
||||||
;; Creation always happens at the playhead. The double-click only
|
[document target] (lane-of-palette-row document target)
|
||||||
;; names the lane; it is not a second, pointer-based time cursor.
|
where (drop-destination db document st frame target)
|
||||||
frame (editing-frame db document)
|
palette? (= :palette-track (get-in document [:symbols (:sid where) :type]))
|
||||||
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)
|
sid (clip/fresh-id document)
|
||||||
uuid (random-uuid)]
|
uuid (random-uuid)]
|
||||||
(if (:refused where)
|
(if (:refused where)
|
||||||
|
|
|
||||||
|
|
@ -336,17 +336,25 @@
|
||||||
(walk sid [] 0 identity)
|
(walk sid [] 0 identity)
|
||||||
[])))))
|
[])))))
|
||||||
|
|
||||||
(defn- palette-track-row [clip sid frames expanded chosen]
|
(defn palette-track-row
|
||||||
|
"`sid`'s palette row: ONE LANE ROW WHETHER OR NOT THE LANE EXISTS YET, because
|
||||||
|
a person putting a palette transition somewhere is doing the same thing in
|
||||||
|
either case. Empty, it names the symbol whose palette it would be; filled, it
|
||||||
|
is the lane symbol's own row under this pane's name for it."
|
||||||
|
[clip sid frames expanded chosen]
|
||||||
(let [track-id (get-in clip [:symbols sid :palette-track])
|
(let [track-id (get-in clip [:symbols sid :palette-track])
|
||||||
track (get-in clip [:symbols track-id])]
|
track (get-in clip [:symbols track-id])]
|
||||||
(if (= :palette-track (:type track))
|
(if (= :palette-track (:type track))
|
||||||
|
;; A MATERIALIZED TRACK IS AN ORDINARY LANE ROW, selection and all: it is
|
||||||
|
;; the row `rows` already draws for a lane symbol, wearing this pane's
|
||||||
|
;; name for it. Only the row of a track that does not exist yet has to say
|
||||||
|
;; whose palette it would be, which is what creating in it needs.
|
||||||
(-> (first (rows clip track-id expanded chosen))
|
(-> (first (rows clip track-id expanded chosen))
|
||||||
(assoc :path [::palette-track] :owner track-id
|
(assoc :path [::palette-track] :owner track-id
|
||||||
:label "palette" :kind :palette
|
:label "palette" :kind :palette
|
||||||
:expandable? false :expanded? false
|
:expandable? false :expanded? false))
|
||||||
:select [::palette-track sid]))
|
|
||||||
{:path [::palette-track] :depth 0 :label "palette" :kind :palette
|
{:path [::palette-track] :depth 0 :label "palette" :kind :palette
|
||||||
:owner nil :select [::palette-track sid]
|
:lane? true :owner nil :select [::palette-track sid]
|
||||||
:span [0 frames] :keys [] :cels []})))
|
:span [0 frames] :keys [] :cels []})))
|
||||||
|
|
||||||
(defn sound-rows
|
(defn sound-rows
|
||||||
|
|
|
||||||
|
|
@ -72,11 +72,14 @@
|
||||||
(vals (get-in saved [:symbols :main :nodes]))))
|
(vals (get-in saved [:symbols :main :nodes]))))
|
||||||
"claiming the frame leaves no overlapping cel"))))
|
"claiming the frame leaves no overlapping cel"))))
|
||||||
|
|
||||||
(deftest double-click-creation-uses-the-lane-and-playhead
|
(deftest double-click-creation-uses-the-lane-and-the-frame-double-clicked
|
||||||
(let [doc (fixture/document)
|
(let [doc (fixture/document)
|
||||||
id (store/install! {:clip doc :store {}} "double-click-new-symbol")]
|
id (store/install! {:clip doc :store {}} "double-click-new-symbol")]
|
||||||
|
;; THE PLAYHEAD IS SOMEWHERE ELSE, deliberately: the gesture names a frame
|
||||||
|
;; as well as a lane, and the empty space double-clicked is where the new
|
||||||
|
;; symbol goes.
|
||||||
(reset! rf-db/app-db {:clip/current id :paint/revision 0
|
(reset! rf-db/app-db {:clip/current id :paint/revision 0
|
||||||
:ui {:open :main} :playback {:frame 5}})
|
:ui {:open :main} :playback {:frame 9}})
|
||||||
(rf/dispatch-sync [::ui/new-symbol-at 5 [:node :main nil []]])
|
(rf/dispatch-sync [::ui/new-symbol-at 5 [:node :main nil []]])
|
||||||
(let [saved (:clip (store/entry id))
|
(let [saved (:clip (store/entry id))
|
||||||
[_ sid instance-id] (get-in @rf-db/app-db [:ui :selection])
|
[_ sid instance-id] (get-in @rf-db/app-db [:ui :selection])
|
||||||
|
|
@ -88,14 +91,28 @@
|
||||||
(is (= (node/source instance)
|
(is (= (node/source instance)
|
||||||
(:sid (creation/target saved {} :main selection 5)))
|
(:sid (creation/target saved {} :main selection 5)))
|
||||||
"the new cel can immediately be selected as the creation target")
|
"the new cel can immediately be selected as the creation target")
|
||||||
(is (= 5 (get-in @rf-db/app-db [:playback :frame]))
|
(is (= 9 (get-in @rf-db/app-db [:playback :frame]))
|
||||||
"the playhead chooses the new cel's time"))))
|
"creating in a lane does not move the playhead"))))
|
||||||
|
|
||||||
|
(deftest the-palette-row-is-a-lane-row-whether-or-not-its-track-exists
|
||||||
|
(let [empty-row (timeline/palette-track-row (clip/blank) :main 120 #{} nil)
|
||||||
|
filled (-> (clip/blank)
|
||||||
|
(assoc-in [:symbols :main :palette-track] :track)
|
||||||
|
(assoc-in [:symbols :track]
|
||||||
|
{:id :track :type :palette-track :display :lane
|
||||||
|
:frames 120 :nodes {}}))
|
||||||
|
filled-row (timeline/palette-track-row filled :main 120 #{} nil)]
|
||||||
|
;; The double-click that creates in a lane asks nothing else of a row.
|
||||||
|
(is (:lane? empty-row))
|
||||||
|
(is (:lane? filled-row))
|
||||||
|
(is (= [:node :track nil []] (:select filled-row))
|
||||||
|
"a track that exists is addressed as the lane it is")))
|
||||||
|
|
||||||
(deftest the-same-double-click-command-creates-a-palette-symbol-on-the-palette-row
|
(deftest the-same-double-click-command-creates-a-palette-symbol-on-the-palette-row
|
||||||
(let [doc (clip/blank)
|
(let [doc (clip/blank)
|
||||||
id (store/install! {:clip doc :store {}} "double-click-palette-symbol")]
|
id (store/install! {:clip doc :store {}} "double-click-palette-symbol")]
|
||||||
(reset! rf-db/app-db {:clip/current id :paint/revision 0
|
(reset! rf-db/app-db {:clip/current id :paint/revision 0
|
||||||
:ui {:open :main} :playback {:frame 5}})
|
:ui {:open :main} :playback {:frame 9}})
|
||||||
(rf/dispatch-sync [::ui/new-symbol-at 5
|
(rf/dispatch-sync [::ui/new-symbol-at 5
|
||||||
[:arthur.ui.timeline/palette-track :main]])
|
[:arthur.ui.timeline/palette-track :main]])
|
||||||
(let [saved (:clip (store/entry id))
|
(let [saved (:clip (store/entry id))
|
||||||
|
|
@ -108,7 +125,19 @@
|
||||||
(is (= :palette (get-in saved [:symbols source :type])))
|
(is (= :palette (get-in saved [:symbols source :type])))
|
||||||
(is (= [5 6] (node/placed-span instance)))
|
(is (= [5 6] (node/placed-span instance)))
|
||||||
(is (= pal/inherit
|
(is (= pal/inherit
|
||||||
(get-in instance [:channels [:palette] :value]))))))
|
(get-in instance [:channels [:palette] :value])))
|
||||||
|
;; ONCE THE TRACK EXISTS ITS ROW IS AN ORDINARY LANE ROW, which is the
|
||||||
|
;; address the pane then hands back: the second transition is created by
|
||||||
|
;; the same command resolving the same way as any other lane's.
|
||||||
|
(rf/dispatch-sync [::ui/new-symbol-at 20 [:node track-id nil []]])
|
||||||
|
(let [saved (:clip (store/entry id))
|
||||||
|
[_ sid2 id2] (get-in @rf-db/app-db [:ui :selection])
|
||||||
|
second-clip (get-in saved [:symbols sid2 :nodes id2])]
|
||||||
|
(is (= track-id sid2) "it lands in the same palette track")
|
||||||
|
(is (= :palette (get-in saved [:symbols (node/source second-clip) :type])))
|
||||||
|
(is (= [20 21] (node/placed-span second-clip)))
|
||||||
|
(is (= pal/inherit
|
||||||
|
(get-in second-clip [:channels [:palette] :value])))))))
|
||||||
|
|
||||||
(deftest a-new-lane-uses-the-symbol-selected-at-the-playhead
|
(deftest a-new-lane-uses-the-symbol-selected-at-the-playhead
|
||||||
(let [doc (clip/blank)
|
(let [doc (clip/blank)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue