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]
|
||||
(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
|
||||
"A symbol's length. Read off the symbol, never copied beside it."
|
||||
[clip sid]
|
||||
|
|
@ -117,7 +122,7 @@
|
|||
:symbols {:main {:id :main :frames blank-frames :nodes {}}}})
|
||||
|
||||
(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
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
Refused, returning the clip unchanged, when it would make a cycle: a symbol
|
||||
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)
|
||||
end (frames clip into)]
|
||||
end (frames clip host)]
|
||||
(if (or (nil? target) (nil? end) (nil? frame) (neg? frame) (>= frame end)
|
||||
(contains-symbol? clip sid into))
|
||||
(contains-symbol? clip sid host))
|
||||
clip
|
||||
(update-symbol
|
||||
clip into assoc-in [:nodes uuid]
|
||||
clip host assoc-in [:nodes uuid]
|
||||
{:id uuid
|
||||
:name (name sid)
|
||||
:name (symbol-name clip sid)
|
||||
:kind :instance
|
||||
:of sid
|
||||
:parent nil
|
||||
|
|
@ -150,6 +155,39 @@
|
|||
:time {:mode :map :at frame :in 0 :rate 1}
|
||||
: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
|
||||
"Put a symbol's already resolved mark into its instance's parent space."
|
||||
[op m path]
|
||||
|
|
|
|||
|
|
@ -145,7 +145,7 @@
|
|||
;; disagree with itself about; `clip` puts it back.
|
||||
(for [[sid sym] (:symbols clip)]
|
||||
{(at "symbol" (segment sid))
|
||||
(select-keys sym [:frames :palette])})
|
||||
(select-keys sym [:name :frames :palette])})
|
||||
(for [[sid sym] (:symbols clip)
|
||||
[id n] (:nodes sym)]
|
||||
{(at "symbol" (segment sid) "node" (segment id))
|
||||
|
|
|
|||
|
|
@ -514,8 +514,11 @@
|
|||
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 —
|
||||
and leaving the field out would make the first one a migration instead of a
|
||||
write."
|
||||
#{:id :frames :nodes :palette})
|
||||
write.
|
||||
|
||||
`: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
|
||||
"Human-readable reasons this symbol will not evaluate. Empty means it will.
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
(:require [arthur.domain.clip :as clip]
|
||||
[arthur.events.edit :as edit]
|
||||
[arthur.events.paint :as paint]
|
||||
[arthur.footage.store :as store]
|
||||
[re-frame.core :as rf]))
|
||||
|
||||
(rf/reg-event-db
|
||||
|
|
@ -75,7 +76,39 @@
|
|||
::place-symbol
|
||||
(fn [db [_ sid [x y]]]
|
||||
(let [uuid (random-uuid)
|
||||
into (get-in db [:ui :open])]
|
||||
host (get-in db [:ui :open])]
|
||||
(-> db
|
||||
(edit/edit #(clip/place-symbol % into sid (get-in db [:playback :frame]) uuid [x y]))
|
||||
(assoc-in [:ui :selection] [:node into uuid])))))
|
||||
(edit/edit #(clip/place-symbol % host sid (get-in db [:playback :frame]) uuid [x y]))
|
||||
(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)
|
||||
:kind :node
|
||||
:node-kind (:kind n)
|
||||
:select [:node sid id]
|
||||
:select [:node sid id rpath]
|
||||
:expandable? true
|
||||
:expanded? open?
|
||||
:span span
|
||||
|
|
@ -176,6 +176,9 @@
|
|||
[:button {:class (when (== r rate) "on")
|
||||
: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))]))
|
||||
[: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.dim (str frame " / " frames)]
|
||||
;; Measured in the loop, not derived from the clock — the whole question
|
||||
|
|
|
|||
|
|
@ -234,3 +234,26 @@
|
|||
(is (empty? (clip/problems c)))
|
||||
(is (= (get-in 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