Solo an instance from its timeline row, shift-click for more than one

A nested mark is now named by the flat path its row has, [a b mark] rather
than [a [b mark]], so a soloed row is a prefix of what it shows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Olive Vaughn 2026-09-30 01:44:24 -04:00
parent 7aaf0a15bd
commit da7e293814
8 changed files with 63 additions and 7 deletions

View file

@ -135,12 +135,14 @@
:symbols {:main {:id :main :frames blank-frames :nodes {}}}})
(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. Its
name becomes its path of instances down to it, the path its timeline row has."
[op m path]
(let [at (fn [x y] [(+ (* (aget m 0) x) (* (aget m 2) y) (aget m 4))
(+ (* (aget m 1) x) (* (aget m 3) y) (aget m 5))])
scale (node/mean-scale m)
op (assoc op :node (conj path (:node op)))]
n (:node op)
op (assoc op :node (if (vector? n) (into path n) (conj path n)))]
(case (:kind op)
:poly (let [out (js/Float64Array. (.-length (:pts op)))]
(dotimes [i (:n op)]

View file

@ -25,6 +25,18 @@
(fn [db [_ path]]
(update-in db [:ui :expanded] #(if (contains? % path) (disj % path) (conj % path)))))
(rf/reg-event-db
::solo
(fn [db [_ path more?]]
;; Per open symbol, because a row path only means something from the symbol
;; it was walked from. A click solos that row alone, or un-solos it if it
;; already was; a shift-click adds it to or takes it out of the ones soloed.
(update-in db [:ui :solo (get-in db [:ui :open])]
(fn [on]
(cond more? (if (contains? on path) (disj on path) (conj (set on) path))
(= on #{path}) #{}
:else #{path})))))
(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

View file

@ -19,6 +19,7 @@
(rf/reg-sub ::open (fn [db _] (get-in db [:ui :open])))
(rf/reg-sub ::sliding (fn [db _] (get-in db [:ui :sliding])))
(rf/reg-sub ::solo (fn [db _] (get-in db [:ui :solo (get-in db [:ui :open])])))
(rf/reg-sub
::clip
@ -91,6 +92,12 @@
;; new blocks, the resolver cannot find them, and the frame throws.
(:store (footage/entry id))))
(defn- placed?
"Does row `path` from symbol `sid` still name an instance, all the way down?"
[clip sid path]
(reduce (fn [sid id] (or (get-in clip [:symbols sid :nodes id :of]) (reduced nil)))
sid path))
(rf/reg-sub
::resolver
:<- [::clip]
@ -101,3 +108,26 @@
(fn [[document sid store palette picture-fps] _]
(when (and document (clip/symbol document sid))
(clip/resolver document store palette sid {:picture-fps picture-fps}))))
(rf/reg-sub
::shown
:<- [::resolver]
:<- [::clip]
:<- [::open]
:<- [::solo]
(fn [[resolve document sid solo] _]
;; What the stage draws: `::resolver`, less whatever is not under a soloed
;; row. Its own layer, so soloing does not rebuild the resolver.
;;
;; A soloed row that has since been deleted or moved would otherwise leave
;; the stage blank, with no row left to un-solo it from.
(let [solo (filter #(placed? document sid %) solo)]
(if (or (nil? resolve) (empty? solo))
resolve
;; An op inside an instance is named by the path its row has; one at the
;; top by its bare id.
(fn [f]
(filterv (fn [{n :node}]
(let [p (if (vector? n) n [n])]
(some #(= % (take (count %) p)) solo)))
(resolve f)))))))

View file

@ -100,7 +100,7 @@
(reset! tracker
(ratom/run!
(let [was (:resolver @snapshot)
now @(rf/subscribe [::render/resolver])]
now @(rf/subscribe [::render/shown])]
(reset! snapshot
{:resolver now
:palette @(rf/subscribe [::render/palette])

View file

@ -204,7 +204,7 @@
(cond (< y 0.3) :front (> y 0.7) :back :else :into)))
(defn- label-cell [{:keys [path depth label kind node-kind select expandable? expanded? of]}
selection over]
selection over solo]
(let [node? (= :node kind)
[over-path where] @over]
[:div (cond-> {:class (str "tl-label" (when (and select (= select selection)) " on")
@ -260,6 +260,13 @@
(when expandable? (if expanded? "▾" "▸"))]
[:span.name label]
(when node? [:span.kind (str "·" (name node-kind))])
(when (= :instance node-kind)
[:button {:class (str "tl-solo" (when (contains? solo path) " on"))
:title "show only this on the stage (⇧ for more than one)"
:on-click (fn [^js e]
(.stopPropagation e)
(rf/dispatch [::ui/solo path (.-shiftKey e)]))}
"S"])
(when (and node? (= select selection))
[:button.tl-delete {:title "delete, with everything in it (⌫)"
:on-click (fn [^js e]
@ -329,6 +336,7 @@
selection @(rf/subscribe [::sub/selection])
expanded @(rf/subscribe [::sub/expanded])
drop @(rf/subscribe [::sub/drop])
solo (set @(rf/subscribe [::render/solo]))
;; Where a drag out of the pool would land, as a row of its own at the
;; top: its own length, starting on the frame it would start on. The
;; stage's drop shows it too, at the playhead.
@ -357,7 +365,7 @@
(rf/dispatch [::ui/move-node from []]))))}
[:div.tl-corner]
(doall (for [row visible]
^{:key (str (:path row))} [label-cell row selection over]))]
^{:key (str (:path row))} [label-cell row selection over solo]))]
[:div.tl-tracks
{:on-drag-enter (fn [^js e] (when (drag/accepts?) (.preventDefault e)))
:on-drag-over (fn [^js e]

View file

@ -63,7 +63,7 @@
(turn :main u [40 20] (/ js/Math.PI 2) 2)
(turn :mid v [5 -3] 0.3 1.5)
(paint/new-shape :box :shape 4 [0 0 10 0 5 10] :brow))
draw #(take 6 (array-seq (:pts (first (filter (fn [op] (= [u [v :shape]] (:node op)))
draw #(take 6 (array-seq (:pts (first (filter (fn [op] (= [u v :shape] (:node op)))
((clip/resolver % nil pal/index-of :main) 16))))))
{:keys [frame matrix time]} (nest/inside c nil :main [u v :shape] 16)
out (js/Float64Array. 2)

View file

@ -134,7 +134,7 @@
(let [before (snapshot (:clip entry) (:store entry) f)
after (snapshot (:clip back) (:store back) f)]
(is (seq before))
(is (some #(= [:face-2 :mouth] (second (:node %))) before)))
(is (some #(= [:face-2 :mouth] (rest (:node %))) before)))
(is (= (snapshot (:clip entry) (:store entry) f)
(snapshot (:clip back) (:store back) f))))))