fixed creating in

This commit is contained in:
Your Name 2026-10-03 02:15:27 -04:00
parent 2697401aad
commit a90e0cfb61
5 changed files with 99 additions and 17 deletions

View file

@ -6,20 +6,39 @@
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 selection]
(let [[kind sid id selected-path] selection
[document open selection]
(let [[kind _sid id selected-path] selection
path (when (= :node kind)
(vec (or (seq selected-path) (when id [id]))))]
(vec (or (seq selected-path) (when id [id]))))
selected (path-node document open path)]
(cond
(empty? path) []
(= :instance (get-in document [:symbols sid :nodes id :kind])) path
(= :instance (:kind selected)) path
:else (vec (butlast path)))))
(defn target
@ -33,7 +52,7 @@
Returns `{:kind :lane|:symbol :sid :path :frame :matrix :time}`."
[document store open selection frame]
(let [preferred (preferred-path document selection)]
(let [preferred (preferred-path document open selection)]
(some (fn [path]
(when-let [inside (nest/inside document store open path frame)]
(when-let [sid (:sid inside)]

View file

@ -345,9 +345,16 @@
(rf/reg-event-db
::toggle-row
(fn [db [_ path]]
(update-in db [:ui :expanded] #(if (contains? % path) (disj % path) (conj % path)))))
::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)))))
(rf/reg-event-db
::solo
@ -800,8 +807,11 @@
;; 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 [_ frame target]]
(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))

View file

@ -656,11 +656,14 @@
: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/toggle-row path]))
:class (println expandable?)}
(rf/dispatch [::ui/set-row-expanded path (not expanded?)]))}
(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

View file

@ -72,26 +72,30 @@
(vals (get-in saved [:symbols :main :nodes]))))
"claiming the frame leaves no overlapping cel"))))
(deftest double-click-creation-uses-the-lane-and-pointer-frame
(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 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 (zero? (get-in @rf-db/app-db [:playback :frame]))
"the pointer frame, not the playhead, chooses the new cel's time"))))
(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 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))
@ -213,6 +217,36 @@
(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
@ -360,6 +394,16 @@
(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