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 <noreply@anthropic.com>
This commit is contained in:
Your Name 2026-06-29 02:10:15 -04:00
parent 0a842c8852
commit ceccec8060
3 changed files with 32 additions and 2 deletions

View file

@ -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)))

View file

@ -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."

View file

@ -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}