From 1c520e6a683859b90b2e4b69cd40f867282b986d Mon Sep 17 00:00:00 2001 From: Olive Vaughn Date: Sun, 4 Oct 2026 02:12:06 -0400 Subject: [PATCH] Add instance playback controls to the inspector --- frontend/src/arthur/events/project.cljs | 26 +++++++++++++ frontend/src/arthur/ui/params.cljs | 29 ++++++++++++++ .../arthur/events/instance_playback_test.cljs | 39 +++++++++++++++++++ 3 files changed, 94 insertions(+) create mode 100644 frontend/test/arthur/events/instance_playback_test.cljs diff --git a/frontend/src/arthur/events/project.cljs b/frontend/src/arthur/events/project.cljs index 34554a1..e6f133f 100644 --- a/frontend/src/arthur/events/project.cljs +++ b/frontend/src/arthur/events/project.cljs @@ -757,6 +757,32 @@ (let [put (if (get-in db [:ui :auto-key?]) node/set-keyed-channel node/set-channel)] (edit/edit db #(update-in % [:symbols sid :nodes id] put path frame value))))) +(rf/reg-event-db + ::instance-playback + (fn [db [_ sid id field value]] + (let [valid? (case field + :mode (#{:once :loop :frame} value) + (:in :speed) (and (node/finite-number? value) (<= 0 value)) + false)] + (if-not valid? + db + (edit/edit + db + (fn [document] + (let [path [:symbols sid :nodes id] + n (get-in document path)] + (if-not (= :instance (:kind n)) + document + (let [{:keys [speed]} (node/playback-of n) + updated (if (= :mode field) + (-> n + (update :time dissoc :loop?) + (assoc-in [:playback :end] (if (= :loop value) :loop :stop)) + (assoc-in [:playback :speed] + (if (= :frame value) 0 (if (pos? speed) speed 1)))) + (assoc-in n [:playback field] value))] + (assoc-in document path updated)))))))))) + (defn- seed-palette-choice [document sid id] (if (get-in document [:symbols sid :nodes id :channels [:palette]]) document diff --git a/frontend/src/arthur/ui/params.cljs b/frontend/src/arthur/ui/params.cljs index e20e3da..5e56c99 100644 --- a/frontend/src/arthur/ui/params.cljs +++ b/frontend/src/arthur/ui/params.cljs @@ -236,6 +236,34 @@ :on-click #(rf/dispatch [::project/set-channel sid id [:style :color] frame i])}]))] (when keyed? [:span.dim "hold"])])) +(defn- instance-playback [[sid id n]] + (let [{:keys [in speed end]} (node/playback-of n) + mode (cond (zero? speed) :frame + (or (= :loop end) (get-in n [:time :loop?])) :loop + :else :once) + change! (fn [field value] + (rf/dispatch [::project/instance-playback sid id field value]))] + [section "playback" + [:div.inspector-form + [:label.inspector-field "mode" + [:select {:value (name mode) + :aria-label "instance playback mode" + :on-change #(change! :mode (keyword (.. % -target -value)))} + [:option {:value "once"} "play once"] + [:option {:value "loop"} "loop"] + [:option {:value "frame"} "hold frame"]]] + [:label.inspector-field "start frame" + [number-input {:min 0 :step 1 :value in + :aria-label "instance start frame" + :parse #(js/parseInt % 10) + :on-number #(change! :in %)}]] + [:label.inspector-field "speed" + [number-input {:min 0 :step 0.1 :value speed + :disabled (= :frame mode) + :aria-label "instance playback speed" + :parse js/parseFloat + :on-number #(change! :speed %)}]]]])) + (defn- node-section [[sid id n]] (let [[start end] (:span n) auto-key? @(rf/subscribe [::sub/auto-key?]) @@ -787,6 +815,7 @@ [palette-placement-section node] [clip-section]) (when (and node (not palette-placement?)) [node-section node]) + (when (and placed (not palette-placement?)) [instance-playback node]) (when (and node (not palette-placement?)) [palette/remap-section]) (when (and placed (not palette-placement?)) [symbol-section placed "source symbol"]) (when (and node (not palette-placement?)) ^{:key (str (first node) "/" (second node))} diff --git a/frontend/test/arthur/events/instance_playback_test.cljs b/frontend/test/arthur/events/instance_playback_test.cljs new file mode 100644 index 0000000..1ed25e3 --- /dev/null +++ b/frontend/test/arthur/events/instance_playback_test.cljs @@ -0,0 +1,39 @@ +(ns arthur.events.instance-playback-test + (:require [cljs.test :refer [deftest is]] + [arthur.domain.clip :as clip] + [arthur.events.project :as project] + [arthur.footage.store :as store] + [re-frame.core :as rf] + [re-frame.db :as rf-db])) + +(deftest playback-controls-change-only-the-selected-instance + (let [n {:id :wheel :kind :instance :source {:symbol :spin} + :span [0 240] :playback {:in 0 :speed 2 :end :stop + :tracks {:mouth []}}} + document (-> (clip/blank) + (assoc-in [:symbols :spin] {:frames 24 :fps 30 :nodes {}}) + (assoc-in [:symbols :main :nodes :wheel] n) + (assoc-in [:symbols :main :nodes :other] (assoc n :id :other))) + id (store/install! {:clip document :store {}} "instance-playback-controls") + current #(get-in (:clip (store/entry (:clip/current @rf-db/app-db))) + [:symbols :main :nodes :wheel])] + (reset! rf-db/app-db {:clip/current id :paint/revision 0}) + (rf/dispatch-sync [::project/instance-playback :main :wheel :mode :loop]) + (is (= 0 (:frame (clip/placed-frame document :main (current) 24)))) + (is (= :loop (get-in (current) [:playback :end]))) + (is (= 2 (get-in (current) [:playback :speed]))) + (rf/dispatch-sync [::project/instance-playback :main :wheel :in 5]) + (rf/dispatch-sync [::project/instance-playback :main :wheel :mode :frame]) + (is (= 5 (:frame (clip/placed-frame document :main (current) 100)))) + (rf/dispatch-sync [::project/instance-playback :main :wheel :mode :once]) + (is (nil? (clip/placed-frame document :main (current) 100))) + (is (= [0 240] (:span (current)))) + (is (= {:mouth []} (get-in (current) [:playback :tracks]))) + (let [before (current)] + (doseq [[field value] [[:in -1] [:speed js/Infinity] [:mode :invalid]]] + (rf/dispatch-sync [::project/instance-playback :main :wheel field value])) + (is (= before (current)))) + (is (= n (assoc (get-in (:clip (store/entry (:clip/current @rf-db/app-db))) + [:symbols :main :nodes :other]) :id :wheel))) + (is (= 24 (get-in (:clip (store/entry (:clip/current @rf-db/app-db))) + [:symbols :spin :frames])))))