fix: label jump-popover targets from the mark's clip ref

The jump dropdown described each target with clip-at, which re-derived the clip
by looking up the (floored) bar-edge frame in the content segments — at a clip
boundary that lands one clip early, so a mark on clip B could read "clip A <last
frame>" even though the jump seeks correctly. The annotation editor labels each
mark straight from its :ref via clip-label, so the two disagreed.

Add scene/jump-targets (built on runs, keyed by the owning mark) and label the
popover with clip-label + the mark-time frame — the same basis as the editor.
Drops the now-unused clip-at.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Your Name 2026-07-02 10:29:58 -04:00
parent 1c6283059e
commit ab16763f8d
3 changed files with 23 additions and 17 deletions

View file

@ -394,6 +394,17 @@
[{:id id :lo lo :hi hi :len (inc (- hi lo))}])) [{:id id :lo lo :hi hi :len (inc (- hi lo))}]))
[] pts))) [] pts)))
(defn jump-targets
"One target per discontinuity for an annotation's jump popover — labelled from
the owning mark's clip ref + frame, the SAME basis the annotation editor uses
(clip-label of :ref), so the jump label can never disagree with the mark row.
Each: {:local <ctx frame to seek> :seg <clip ref> :f <mark-time frame>}."
[scene ctx gid]
(mapv (fn [{:keys [id lo]}]
(let [st (:start (some #(when (= id (:id %)) %) (:marks (grp scene gid))))]
{:local lo :seg (:ref st) :f (at->frame scene (:ref st) (:at st))}))
(runs scene ctx gid)))
(defn linkables (defn linkables
"Pickable link targets within `ctx`, grouped for the autocomplete: one group "Pickable link targets within `ctx`, grouped for the autocomplete: one group
per video track (its clips, by start) and per child annotation (its run per video track (its clips, by start) and per child annotation (its run

View file

@ -112,6 +112,9 @@
(filterv #(= :script-note (get-in scene [:groups % :type])))) (filterv #(= :script-note (get-in scene [:groups % :type]))))
:broken (boolean reason) :reason reason :broken (boolean reason) :reason reason
:hidden (boolean hidden) :hidden (boolean hidden)
;; jump targets labelled from the marks' clip refs (same as
;; the editor) — not re-derived from a floored bar frame
:jumps (scene/jump-targets scene ctx gid)
:start (or (ffirst bars) 0) :bars bars})))) :start (or (ffirst bars) 0) :bars bars}))))
(sort-by (juxt :broken :start)) ; broken annotations sink to the bottom (sort-by (juxt :broken :start)) ; broken annotations sink to the bottom
vec)))) vec))))

View file

@ -384,15 +384,6 @@
(:map (reset! label-cache {:segs segs :map (build-label-map scene segs)})))] (:map (reset! label-cache {:segs segs :map (build-label-map scene segs)})))]
(get m mid "clip"))) (get m mid "clip")))
(defn- clip-at
"Describe local frame `lf` the way the annotation editor does: the underlying
clip name plus its crop-adjusted mark-time frame, e.g. \"CU Tashi (1) 45f\"."
[scene segs lf]
(if-let [seg (some (fn [{[c d] :local :as s}] (when (and (<= c lf) (< lf d)) s)) segs)]
(str (clip-label scene segs (:mark seg))
" " (:at (scene/seg-point scene seg (js/Math.round (- lf (first (:local seg)))))) "f")
(str (js/Math.round lf) "f")))
(defn frame-readout (defn frame-readout
"Bottom-right of the video: the timeline (context-absolute) frame, and the "Bottom-right of the video: the timeline (context-absolute) frame, and the
track + clip + mark-time frame under the playhead. While authoring, the clip track + clip + mark-time frame under the playhead. While authoring, the clip
@ -881,21 +872,22 @@
(defn- jump-control (defn- jump-control
"One jump button, or — when the annotation's marks have discontinuities (more "One jump button, or — when the annotation's marks have discontinuities (more
than one bar) — a button that opens a popover of one target per piece, each than one piece) — a button that opens a popover of one target per piece. Each
labelled with the clip + frame it lands on (as the annotation editor shows it)." is labelled straight from its mark's clip ref via clip-label, exactly like the
annotation editor's mark rows, so the two never disagree."
[open a scene segs] [open a scene segs]
(let [bars (:bars a)] (let [jumps (:jumps a)]
(if (< (count bars) 2) (if (< (count jumps) 2)
[:button.jump-btn {:on-click #(goto! (:start a) true)} "↪ jump"] [:button.jump-btn {:on-click #(goto! (:start a) true)} "↪ jump"]
[:span {:style {:position "relative"}} [:span {:style {:position "relative"}}
[:button.jump-btn {:on-click #(swap! open (fn [x] (when (not= x (:id a)) (:id a))))} [:button.jump-btn {:on-click #(swap! open (fn [x] (when (not= x (:id a)) (:id a))))}
(str "↪ jump (" (count bars) ") ▾")] (str "↪ jump (" (count jumps) ") ▾")]
(when (= @open (:id a)) (when (= @open (:id a))
[:div.jump-pop [:div.jump-pop
(for [[i [lo _]] (map-indexed vector bars)] (for [[i {:keys [local seg f]}] (map-indexed vector jumps)]
^{:key i} ^{:key i}
[:button {:on-click #(do (goto! lo true) (reset! open nil))} [:button {:on-click #(do (goto! local true) (reset! open nil))}
(str "▸ " (clip-at scene segs lo))])])]))) (str "▸ " (clip-label scene segs seg) " " f "f")])])])))
;; A bound-note chip on an annotation card. Owns its own »»» state so a bar ;; A bound-note chip on an annotation card. Owns its own »»» state so a bar
;; crossing only re-renders the chip that flipped, not the whole card list. ;; crossing only re-renders the chip that flipped, not the whole card list.