New empty symbol, nested in the selected instance or the open symbol
'+ symbol' in the timeline makes an empty symbol and places it at the playhead: inside the selected instance, beside any other selected node, or in the open symbol. Timeline selections carry their row path so a symbol placed twice nests into the row that was clicked. Symbols gain a :name. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
5dff490162
commit
2d2eb0fc9f
6 changed files with 114 additions and 14 deletions
|
|
@ -53,6 +53,11 @@
|
||||||
[clip sid]
|
[clip sid]
|
||||||
(get-in clip [:symbols sid]))
|
(get-in clip [:symbols sid]))
|
||||||
|
|
||||||
|
(defn symbol-name
|
||||||
|
"What to call a symbol: its `:name`, or its id when it has none."
|
||||||
|
[clip sid]
|
||||||
|
(or (:name (symbol clip sid)) (name sid)))
|
||||||
|
|
||||||
(defn frames
|
(defn frames
|
||||||
"A symbol's length. Read off the symbol, never copied beside it."
|
"A symbol's length. Read off the symbol, never copied beside it."
|
||||||
[clip sid]
|
[clip sid]
|
||||||
|
|
@ -117,7 +122,7 @@
|
||||||
:symbols {:main {:id :main :frames blank-frames :nodes {}}}})
|
:symbols {:main {:id :main :frames blank-frames :nodes {}}}})
|
||||||
|
|
||||||
(defn place-symbol
|
(defn place-symbol
|
||||||
"An instance of symbol `sid`, inside symbol `into`, at `frame` of `into`.
|
"An instance of symbol `sid`, inside symbol `host`, at `frame` of `host`.
|
||||||
|
|
||||||
THE UUID IS AN ARGUMENT. A placement's identity is the key it has in the node
|
THE UUID IS AN ARGUMENT. A placement's identity is the key it has in the node
|
||||||
map — it is what `:linked-to`, an export target and a saved leaf all name — so
|
map — it is what `:linked-to`, an export target and a saved leaf all name — so
|
||||||
|
|
@ -125,21 +130,21 @@
|
||||||
called, and this namespace is the pure one.
|
called, and this namespace is the pure one.
|
||||||
|
|
||||||
The instance's own time starts where it was dropped: `:at frame` with `:in 0`
|
The instance's own time starts where it was dropped: `:at frame` with `:in 0`
|
||||||
means local frame 0 of the symbol plays on `frame` of `into`, which is what
|
means local frame 0 of the symbol plays on `frame` of `host`, which is what
|
||||||
dragging something onto a playhead is asking for.
|
dragging something onto a playhead is asking for.
|
||||||
|
|
||||||
Refused, returning the clip unchanged, when it would make a cycle: a symbol
|
Refused, returning the clip unchanged, when it would make a cycle: a symbol
|
||||||
cannot be placed inside itself or inside anything it places."
|
cannot be placed inside itself or inside anything it places."
|
||||||
[clip into sid frame uuid [x y]]
|
[clip host sid frame uuid [x y]]
|
||||||
(let [target (symbol clip sid)
|
(let [target (symbol clip sid)
|
||||||
end (frames clip into)]
|
end (frames clip host)]
|
||||||
(if (or (nil? target) (nil? end) (nil? frame) (neg? frame) (>= frame end)
|
(if (or (nil? target) (nil? end) (nil? frame) (neg? frame) (>= frame end)
|
||||||
(contains-symbol? clip sid into))
|
(contains-symbol? clip sid host))
|
||||||
clip
|
clip
|
||||||
(update-symbol
|
(update-symbol
|
||||||
clip into assoc-in [:nodes uuid]
|
clip host assoc-in [:nodes uuid]
|
||||||
{:id uuid
|
{:id uuid
|
||||||
:name (name sid)
|
:name (symbol-name clip sid)
|
||||||
:kind :instance
|
:kind :instance
|
||||||
:of sid
|
:of sid
|
||||||
:parent nil
|
:parent nil
|
||||||
|
|
@ -150,6 +155,39 @@
|
||||||
:time {:mode :map :at frame :in 0 :rate 1}
|
:time {:mode :map :at frame :in 0 :rate 1}
|
||||||
:channels {[:xform :pos] {:animated? false :value [x y]}}}))))
|
:channels {[:xform :pos] {:animated? false :value [x y]}}}))))
|
||||||
|
|
||||||
|
(defn fresh-id
|
||||||
|
"The first `:symbol-N` the clip does not already hold. Readable because an id
|
||||||
|
shows up in saved leaf paths, and deterministic because this namespace is pure."
|
||||||
|
[clip]
|
||||||
|
(first (remove (:symbols clip) (map #(keyword (str "symbol-" %)) (iterate inc 1)))))
|
||||||
|
|
||||||
|
(defn new-symbol
|
||||||
|
"A new, empty symbol `sid`, placed inside `host` at `frame` and running to the
|
||||||
|
end of it. Placed at the origin, so whatever is drawn into it lands where it was
|
||||||
|
drawn until the instance is moved."
|
||||||
|
[clip host sid frame uuid]
|
||||||
|
(let [end (frames clip host)]
|
||||||
|
(if (or (nil? end) (symbol clip sid) (nil? frame) (neg? frame) (>= frame end))
|
||||||
|
clip
|
||||||
|
(-> clip
|
||||||
|
(assoc-in [:symbols sid] {:id sid :name (name sid) :frames (- end frame) :nodes {}})
|
||||||
|
(place-symbol host sid frame uuid [0 0])))))
|
||||||
|
|
||||||
|
(defn frame-inside
|
||||||
|
"Carry frame `f` of symbol `sid` down through the nodes named by `path`, one
|
||||||
|
per level, the way a timeline row's path names them. Returns `[symbol frame]`:
|
||||||
|
the symbol the last node places, and the frame it is showing.
|
||||||
|
|
||||||
|
Each step applies the node's own time map and its ancestors' in that symbol,
|
||||||
|
outermost first, which is the order `symbol/eval-frame` composes them in."
|
||||||
|
[clip sid path f]
|
||||||
|
(reduce (fn [[sid f] id]
|
||||||
|
(let [nodes (:nodes (symbol clip sid))
|
||||||
|
f (reduce #(node/local-frame (get nodes %2) %1)
|
||||||
|
f (rseq (symbol/lineage nodes id)))]
|
||||||
|
[(:of (get nodes id)) (js/Math.floor f)]))
|
||||||
|
[sid f] path))
|
||||||
|
|
||||||
(defn- transform-op
|
(defn- transform-op
|
||||||
"Put a symbol's already resolved mark into its instance's parent space."
|
"Put a symbol's already resolved mark into its instance's parent space."
|
||||||
[op m path]
|
[op m path]
|
||||||
|
|
|
||||||
|
|
@ -145,7 +145,7 @@
|
||||||
;; disagree with itself about; `clip` puts it back.
|
;; disagree with itself about; `clip` puts it back.
|
||||||
(for [[sid sym] (:symbols clip)]
|
(for [[sid sym] (:symbols clip)]
|
||||||
{(at "symbol" (segment sid))
|
{(at "symbol" (segment sid))
|
||||||
(select-keys sym [:frames :palette])})
|
(select-keys sym [:name :frames :palette])})
|
||||||
(for [[sid sym] (:symbols clip)
|
(for [[sid sym] (:symbols clip)
|
||||||
[id n] (:nodes sym)]
|
[id n] (:nodes sym)]
|
||||||
{(at "symbol" (segment sid) "node" (segment id))
|
{(at "symbol" (segment sid) "node" (segment id))
|
||||||
|
|
|
||||||
|
|
@ -514,8 +514,11 @@
|
||||||
a ramp belongs — `domain/symbol` takes the palette as a PARAMETER rather than
|
a ramp belongs — `domain/symbol` takes the palette as a PARAMETER rather than
|
||||||
reaching for a global precisely so that a nested symbol can carry its own —
|
reaching for a global precisely so that a nested symbol can carry its own —
|
||||||
and leaving the field out would make the first one a migration instead of a
|
and leaving the field out would make the first one a migration instead of a
|
||||||
write."
|
write.
|
||||||
#{:id :frames :nodes :palette})
|
|
||||||
|
`:name` is what a person calls it, and is not its id: an id is what instances
|
||||||
|
and saved leaves point at, so renaming a symbol must not change it."
|
||||||
|
#{:id :name :frames :nodes :palette})
|
||||||
|
|
||||||
(defn problems
|
(defn problems
|
||||||
"Human-readable reasons this symbol will not evaluate. Empty means it will.
|
"Human-readable reasons this symbol will not evaluate. Empty means it will.
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
(:require [arthur.domain.clip :as clip]
|
(:require [arthur.domain.clip :as clip]
|
||||||
[arthur.events.edit :as edit]
|
[arthur.events.edit :as edit]
|
||||||
[arthur.events.paint :as paint]
|
[arthur.events.paint :as paint]
|
||||||
|
[arthur.footage.store :as store]
|
||||||
[re-frame.core :as rf]))
|
[re-frame.core :as rf]))
|
||||||
|
|
||||||
(rf/reg-event-db
|
(rf/reg-event-db
|
||||||
|
|
@ -75,7 +76,39 @@
|
||||||
::place-symbol
|
::place-symbol
|
||||||
(fn [db [_ sid [x y]]]
|
(fn [db [_ sid [x y]]]
|
||||||
(let [uuid (random-uuid)
|
(let [uuid (random-uuid)
|
||||||
into (get-in db [:ui :open])]
|
host (get-in db [:ui :open])]
|
||||||
(-> db
|
(-> db
|
||||||
(edit/edit #(clip/place-symbol % into sid (get-in db [:playback :frame]) uuid [x y]))
|
(edit/edit #(clip/place-symbol % host sid (get-in db [:playback :frame]) uuid [x y]))
|
||||||
(assoc-in [:ui :selection] [:node into uuid])))))
|
(assoc-in [:ui :selection] [:node host uuid])))))
|
||||||
|
|
||||||
|
(defn- where-new-goes
|
||||||
|
"The row path, from the open symbol down, of the symbol a new thing goes into:
|
||||||
|
INSIDE the selected instance, or BESIDE any other selected node, or at the top
|
||||||
|
of the open symbol when nothing is selected.
|
||||||
|
|
||||||
|
A selection from a timeline row carries that row's path, because one symbol
|
||||||
|
placed twice is two rows and only the path says which was clicked. One made on
|
||||||
|
the stage does not, and names a node directly in the open symbol."
|
||||||
|
[clip db]
|
||||||
|
(let [[kind sid id path] (get-in db [:ui :selection])
|
||||||
|
path (when (= :node kind) (or path [id]))]
|
||||||
|
(cond
|
||||||
|
(nil? path) []
|
||||||
|
(= :instance (get-in clip [:symbols sid :nodes id :kind])) path
|
||||||
|
:else (pop path))))
|
||||||
|
|
||||||
|
(rf/reg-event-db
|
||||||
|
::new-symbol
|
||||||
|
(fn [db _]
|
||||||
|
(let [clip (:clip (store/entry (:clip/current db)))
|
||||||
|
down (where-new-goes clip db)
|
||||||
|
[host frame] (clip/frame-inside clip (get-in db [:ui :open]) down
|
||||||
|
(get-in db [:playback :frame]))
|
||||||
|
sid (clip/fresh-id clip)
|
||||||
|
uuid (random-uuid)]
|
||||||
|
(-> db
|
||||||
|
(edit/edit #(clip/new-symbol % host sid frame uuid))
|
||||||
|
(assoc-in [:ui :selection] [:node host uuid (conj down uuid)])
|
||||||
|
;; Open every row down to it, or the new row is inside a closed one and
|
||||||
|
;; the button looks like it did nothing.
|
||||||
|
(update-in [:ui :expanded] into (rest (reductions conj [] down)))))))
|
||||||
|
|
|
||||||
|
|
@ -113,7 +113,7 @@
|
||||||
:label (node-label id n)
|
:label (node-label id n)
|
||||||
:kind :node
|
:kind :node
|
||||||
:node-kind (:kind n)
|
:node-kind (:kind n)
|
||||||
:select [:node sid id]
|
:select [:node sid id rpath]
|
||||||
:expandable? true
|
:expandable? true
|
||||||
:expanded? open?
|
:expanded? open?
|
||||||
:span span
|
:span span
|
||||||
|
|
@ -176,6 +176,9 @@
|
||||||
[:button {:class (when (== r rate) "on")
|
[:button {:class (when (== r rate) "on")
|
||||||
:on-click #(rf/dispatch [::pb/set-rate r])}
|
:on-click #(rf/dispatch [::pb/set-rate r])}
|
||||||
(case r 1.0 "1x" 0.5 "½" 0.25 "¼" 2.0 "2x" 4.0 "4x" (str r))]))
|
(case r 1.0 "1x" 0.5 "½" 0.25 "¼" 2.0 "2x" 4.0 "4x" (str r))]))
|
||||||
|
[:button {:title "a new empty symbol inside the selected instance, or beside the selected node, or in the open symbol"
|
||||||
|
:on-click #(rf/dispatch [::ui/new-symbol])}
|
||||||
|
"+ symbol"]
|
||||||
[:span.spacer]
|
[:span.spacer]
|
||||||
[:span.dim (str frame " / " frames)]
|
[:span.dim (str frame " / " frames)]
|
||||||
;; Measured in the loop, not derived from the clock — the whole question
|
;; Measured in the loop, not derived from the clock — the whole question
|
||||||
|
|
|
||||||
|
|
@ -234,3 +234,26 @@
|
||||||
(is (empty? (clip/problems c)))
|
(is (empty? (clip/problems c)))
|
||||||
(is (= (get-in c [:symbols :outer :nodes])
|
(is (= (get-in c [:symbols :outer :nodes])
|
||||||
(get-in (leaf/clip "c" (leaf/leaves "c" c)) [:symbols :outer :nodes]))))))
|
(get-in (leaf/clip "c" (leaf/leaves "c" c)) [:symbols :outer :nodes]))))))
|
||||||
|
|
||||||
|
(deftest a-new-symbol-is-empty-and-placed-where-it-was-asked-for
|
||||||
|
(let [c (nested)
|
||||||
|
u #uuid "00000000-0000-4000-8000-000000000001"
|
||||||
|
id (clip/fresh-id c)
|
||||||
|
made (clip/new-symbol c :outer id 20 u)]
|
||||||
|
(is (= :symbol-1 id))
|
||||||
|
(is (= :symbol-2 (clip/fresh-id made)) "the next one does not collide")
|
||||||
|
(is (= {:id :symbol-1 :name "symbol-1" :frames 180 :nodes {}}
|
||||||
|
(clip/symbol made :symbol-1))
|
||||||
|
"empty, and as long as the rest of what it was placed in")
|
||||||
|
(is (= {:of :symbol-1 :span [20 200] :time {:mode :map :at 20 :in 0 :rate 1}}
|
||||||
|
(select-keys (get-in made [:symbols :outer :nodes u]) [:of :span :time])))
|
||||||
|
(is (empty? (clip/problems made)))
|
||||||
|
(is (= c (clip/new-symbol c :outer :inner 0 u)) "an id already in use is refused")
|
||||||
|
(is (= c (clip/new-symbol c :outer id 200 u)) "past the end is refused")))
|
||||||
|
|
||||||
|
(deftest a-frame-is-carried-down-through-the-instances-a-row-path-names
|
||||||
|
(let [c (nested)
|
||||||
|
[id] (keys (get-in c [:symbols :outer :nodes]))]
|
||||||
|
(is (= [:outer 12] (clip/frame-inside c :outer [] 12)))
|
||||||
|
(is (= [:inner 7] (clip/frame-inside c :outer [id] 12))
|
||||||
|
"the instance starts at 5, so frame 12 outside is frame 7 inside")))
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue