From ceccec8060ab2a80ff6afcf46afff86fff9a0b2b Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 29 Jun 2026 02:10:15 -0400 Subject: [PATCH] Fix annotations not persisting across reload (keyword round-trip) Keyword *values* don't survive JSON: clj->js stringifies :annotation/:root/clip refs and js->clj only re-keywordizes keys, so a reloaded annotation had string :type/:parent/:ref and was filtered out of the scene. Re-keywordize those fields on load (scene/restore-annotations) and give marks string ids (random-uuid was serializing as a UUID object). Co-Authored-By: Claude Opus 4.8 --- tl/src/tl/events.cljs | 2 +- tl/src/tl/scene.cljs | 20 +++++++++++++++++++- tl/test/tl/scene_test.cljs | 12 ++++++++++++ 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/tl/src/tl/events.cljs b/tl/src/tl/events.cljs index 90702ed..1390cea 100644 --- a/tl/src/tl/events.cljs +++ b/tl/src/tl/events.cljs @@ -47,7 +47,7 @@ (fn [db [_ detail otio-raw scene-resp]] (let [parsed (otio/parse otio-raw) base (scene/from-otio parsed) - anns (get-in scene-resp [:scene :groups] {})] ; saved annotation layer + anns (scene/restore-annotations (get-in scene-resp [:scene :groups] {}))] (-> db (assoc :project detail :page :editor) (assoc :fps (or (:fps scene-resp) (:fps parsed))) diff --git a/tl/src/tl/scene.cljs b/tl/src/tl/scene.cljs index a22746e..b46e4af 100644 --- a/tl/src/tl/scene.cljs +++ b/tl/src/tl/scene.cljs @@ -177,7 +177,7 @@ (mapv (fn [{:keys [mark src]}] (let [{:keys [xs]} (target-range scene mark) [a b] src] - {:id (random-uuid) + {:id (str (random-uuid)) ; string so it survives JSON :start {:ref mark :at (- a xs)} :end {:ref mark :at (- b xs)}})) (slice (content-segments scene ctx) la lb))) @@ -219,6 +219,24 @@ (let [{:keys [xs xe]} (target-range scene ref)] (+ (- xe xs) at 1)) at)) +(defn- restore-mark [m] + (cond-> m + (get-in m [:start :ref]) (update-in [:start :ref] keyword) + (get-in m [:end :ref]) (update-in [:end :ref] keyword) + (:track m) (update :track keyword))) + +(defn restore-annotations + "Re-keywordize the fields that lose their keyword-ness through JSON: a saved + annotation round-trips with string :type/:parent and string mark :refs, so + put them back before merging into the (keyword-keyed) scene." + [anns] + (into {} (map (fn [[gid g]] + [gid (-> g + (update :type keyword) + (update :parent keyword) + (update :marks #(mapv restore-mark (or % []))))])) + anns)) + (defn marks->rows "Render single-clip `marks` as editor rows (one {:s … :e …} per mark, frames normalised to mark time). 1:1 with `marks`, so row i pairs with mark i." diff --git a/tl/test/tl/scene_test.cljs b/tl/test/tl/scene_test.cljs index d6ce5bf..808adea 100644 --- a/tl/test/tl/scene_test.cljs +++ b/tl/test/tl/scene_test.cljs @@ -221,6 +221,18 @@ (is (= 0 (get-in (first marks) [:start :at]))) (is (= 30 (get-in (first marks) [:end :at]))))))) +(deftest restore-annotations-rekeywordizes-json + (testing "a JSON-roundtripped annotation (string :type/:parent/:ref) is restored + resolves" + (let [json-like {:ann-1 {:type "annotation" :parent "root" :name "x" + :marks [{:id "m1" :start {:ref "clip-a" :at 0} + :end {:ref "clip-a" :at -1}}]}} + g (:ann-1 (s/restore-annotations json-like))] + (is (= :annotation (:type g))) + (is (= :root (:parent g))) + (is (= :clip-a (get-in g [:marks 0 :start :ref]))) + (let [scene (assoc-in base [:groups :ann-1] g)] + (is (= [0 100] (:src (first (s/resolve scene :ann-1))))))))) + (deftest marks-to-rows-normalises-frames (testing "marks->rows is 1:1 with marks and normalises :at -1 to the length" (let [rows (s/marks->rows base [{:id :m :start {:ref :clip-a :at 0}