From 0d73b204e61c40ac72801724e394f7eb4b9589be Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 29 Jun 2026 02:25:59 -0400 Subject: [PATCH] Coalesce continuous annotation bars into one jump point MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A continuous selection across N clips is N marks → N bars, and OTIO's fractional media offsets leave ~1-frame gaps between adjacent clips, so the jump control showed "jump (5)" for a continuous stretch. merge-bars snaps bar endpoints to whole frames and joins touching ranges; only a real >=1-frame gap splits. Co-Authored-By: Claude Opus 4.8 --- tl/src/tl/scene.cljs | 15 +++++++++++++++ tl/src/tl/subs.cljs | 3 ++- tl/test/tl/scene_test.cljs | 9 +++++++++ 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/tl/src/tl/scene.cljs b/tl/src/tl/scene.cljs index b46e4af..62ac014 100644 --- a/tl/src/tl/scene.cljs +++ b/tl/src/tl/scene.cljs @@ -54,6 +54,21 @@ (when (< lo hi) [(+ c (- lo a)) (+ c (- hi a))]))) segs))) +(defn merge-bars + "Coalesce [lo hi) ranges that meet at a frame boundary into single bars, so a + continuous selection spanning several clips reads as one piece. Endpoints snap + to whole frames first, which absorbs the sub-frame gaps OTIO's fractional + media offsets leave between adjacent clips; only a real (≥1 frame) gap splits." + [bars] + (reduce (fn [acc [lo hi]] + (let [lo (js/Math.floor lo) hi (js/Math.ceil hi) + [plo phi] (peek acc)] + (if (and plo (<= lo phi)) + (conj (pop acc) [plo (max phi hi)]) + (conj acc [lo hi])))) + [] + (sort-by first bars))) + (defn slice "Sub-segments of `segs` covering local range [la lb), src + local re-cut." [segs la lb] diff --git a/tl/src/tl/subs.cljs b/tl/src/tl/subs.cljs index 422c3eb..50a7c9f 100644 --- a/tl/src/tl/subs.cljs +++ b/tl/src/tl/subs.cljs @@ -72,7 +72,8 @@ (keep (fn [[gid g]] (when (and (= :annotation (:type g)) (= ctx (:parent g))) (let [src-segs (scene/resolve scene gid) - bars (vec (mapcat (fn [{[a b] :src}] (scene/pieces segs a b)) src-segs)) + bars (scene/merge-bars + (mapcat (fn [{[a b] :src}] (scene/pieces segs a b)) src-segs)) reason (scene/broken-reason scene gid)] {:id gid :name (:name g) :color (or (:color g) "#4e8fc2") :content (:content g) :children (count (:marks g)) diff --git a/tl/test/tl/scene_test.cljs b/tl/test/tl/scene_test.cljs index 808adea..9f3b95c 100644 --- a/tl/test/tl/scene_test.cljs +++ b/tl/test/tl/scene_test.cljs @@ -221,6 +221,15 @@ (is (= 0 (get-in (first marks) [:start :at]))) (is (= 30 (get-in (first marks) [:end :at]))))))) +(deftest merge-bars-coalesces-continuous-run + (testing "sub-frame OTIO gaps collapse to one bar; a real gap stays split" + ;; foobar's real bars (continuous 5-clip selection, ~0.9-frame source gaps) + (is (= [[128 542]] + (s/merge-bars [[128.87 190.87] [191.80 265.80] [266.73 384.73] + [385.61 413.61] [414.58 541.58]]))) + (is (= [[0 50] [200 260]] (s/merge-bars [[0 50] [200 260]]))) ; real gap → two + (is (= [] (s/merge-bars []))))) + (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"