Detect broken child mark-refs; restore the timeline/top divider
- scene: point-frame rejects out-of-range offsets so broken-marks catches boundary shifts and cascades, not just deleted refs; add broken-reason - subs: annotations carry :broken/:reason and sort broken ones to the bottom - views: ⚠ on broken annotations (hover = reason); drag-resize the top/timeline split via --top-h (vh) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
c03756d3e1
commit
7a960afb5c
4 changed files with 55 additions and 5 deletions
|
|
@ -97,8 +97,9 @@
|
||||||
|
|
||||||
(map? point)
|
(map? point)
|
||||||
(when-let [{:keys [xs xe track]} (target-range scene (:ref point))]
|
(when-let [{:keys [xs xe track]} (target-range scene (:ref point))]
|
||||||
(let [n (:at point)]
|
(let [n (:at point)
|
||||||
{:frame (if (neg? n) (+ xe n 1) (+ xs n)) :track track}))))
|
f (if (neg? n) (+ xe n 1) (+ xs n))]
|
||||||
|
(when (<= xs f xe) {:frame f :track track}))))) ; nil if trimmed out of range
|
||||||
|
|
||||||
(defn resolve-mark
|
(defn resolve-mark
|
||||||
"One mark → its segment(s) (1 for a ref mark, 1+ for an absolute mark), with
|
"One mark → its segment(s) (1 for a ref mark, 1+ for an absolute mark), with
|
||||||
|
|
@ -141,6 +142,13 @@
|
||||||
(filterv (fn [m] (empty? (resolve-mark scene gid m))))
|
(filterv (fn [m] (empty? (resolve-mark scene gid m))))
|
||||||
(mapv :id)))
|
(mapv :id)))
|
||||||
|
|
||||||
|
(defn broken-reason
|
||||||
|
"A short why for `gid`'s first broken mark, or nil if nothing's broken."
|
||||||
|
[scene gid]
|
||||||
|
(when-let [mid (first (broken-marks scene gid))]
|
||||||
|
(let [ref (->> (:marks (grp scene gid)) (some #(when (= mid (:id %)) %)) :start :ref)]
|
||||||
|
(if (target-range scene ref) "reference trimmed away" "referenced clip deleted"))))
|
||||||
|
|
||||||
;; --- what the current timeline is made of --------------------------------
|
;; --- what the current timeline is made of --------------------------------
|
||||||
|
|
||||||
(defn content-segments
|
(defn content-segments
|
||||||
|
|
|
||||||
|
|
@ -62,13 +62,15 @@
|
||||||
(keep (fn [[gid g]]
|
(keep (fn [[gid g]]
|
||||||
(when (and (= :annotation (:type g)) (= ctx (:parent g)))
|
(when (and (= :annotation (:type g)) (= ctx (:parent g)))
|
||||||
(let [src-segs (scene/resolve scene gid)
|
(let [src-segs (scene/resolve scene gid)
|
||||||
bars (vec (mapcat (fn [{[a b] :src}] (scene/pieces segs a b)) src-segs))]
|
bars (vec (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")
|
{:id gid :name (:name g) :color (or (:color g) "#4e8fc2")
|
||||||
:content (:content g) :children (count (:marks g))
|
:content (:content g) :children (count (:marks g))
|
||||||
:nested (get nested gid 0)
|
:nested (get nested gid 0)
|
||||||
:draft (boolean (:draft g))
|
:draft (boolean (:draft g))
|
||||||
|
:broken (boolean reason) :reason reason
|
||||||
:start (or (ffirst bars) 0) :bars bars}))))
|
:start (or (ffirst bars) 0) :bars bars}))))
|
||||||
(sort-by :start)
|
(sort-by (juxt :broken :start)) ; broken annotations sink to the bottom
|
||||||
vec))))
|
vec))))
|
||||||
|
|
||||||
;; the annotation the playhead is currently inside (or the latest one passed) —
|
;; the annotation the playhead is currently inside (or the latest one passed) —
|
||||||
|
|
|
||||||
|
|
@ -228,7 +228,7 @@
|
||||||
^{:key (:id a)}
|
^{:key (:id a)}
|
||||||
[:div.ann {:id (str "ann-" (name (:id a))) :class (when (= (:id a) active) "active")}
|
[:div.ann {:id (str "ann-" (name (:id a))) :class (when (= (:id a) active) "active")}
|
||||||
[:div.ann-head
|
[:div.ann-head
|
||||||
[:div.ann-title (:name a)]
|
[:div.ann-title (when (:broken a) [:span {:title (:reason a)} "⚠ "]) (:name a)]
|
||||||
[:div.ann-actions
|
[:div.ann-actions
|
||||||
[jump-control open a]
|
[jump-control open a]
|
||||||
[:button.expand-btn {:title "Expand" :on-click #(rf/dispatch [::events/expand (:id a)])}
|
[:button.expand-btn {:title "Expand" :on-click #(rf/dispatch [::events/expand (:id a)])}
|
||||||
|
|
@ -324,6 +324,17 @@
|
||||||
[:input {:type "range" :min 8 :max 120 :value row-h
|
[:input {:type "range" :min 8 :max 120 :value row-h
|
||||||
:on-change #(rf/dispatch [::events/set-row-h (js/parseFloat (.. % -target -value))])}]]]))
|
:on-change #(rf/dispatch [::events/set-row-h (js/parseFloat (.. % -target -value))])}]]]))
|
||||||
|
|
||||||
|
(defn- drag-top!
|
||||||
|
"Drag the horizontal divider: set --top-h (in vh) from the pointer's Y."
|
||||||
|
[ev]
|
||||||
|
(.preventDefault ev)
|
||||||
|
(letfn [(move [e] (let [vh (-> (.-clientY e) (/ (.-innerHeight js/window)) (* 100) (max 10) (min 85))]
|
||||||
|
(.setProperty (.. js/document -documentElement -style) "--top-h" (str vh "vh"))))
|
||||||
|
(up [_] (.removeEventListener js/document "pointermove" move)
|
||||||
|
(.removeEventListener js/document "pointerup" up))]
|
||||||
|
(.addEventListener js/document "pointermove" move)
|
||||||
|
(.addEventListener js/document "pointerup" up)))
|
||||||
|
|
||||||
(defn main-panel []
|
(defn main-panel []
|
||||||
(let [status @(rf/subscribe [::subs/status])
|
(let [status @(rf/subscribe [::subs/status])
|
||||||
draft? (some? @(rf/subscribe [::subs/draft-group]))]
|
draft? (some? @(rf/subscribe [::subs/draft-group]))]
|
||||||
|
|
@ -331,6 +342,7 @@
|
||||||
[:div.top
|
[:div.top
|
||||||
[:div.video-pane [video-monitor] [frame-readout]]
|
[:div.video-pane [video-monitor] [frame-readout]]
|
||||||
[:div.annot-pane (if draft? [annotation-form] [commentary])]]
|
[:div.annot-pane (if draft? [annotation-form] [commentary])]]
|
||||||
|
[:div.divider-h {:on-pointer-down drag-top!}]
|
||||||
[:div.timeline-pane
|
[:div.timeline-pane
|
||||||
[breadcrumbs]
|
[breadcrumbs]
|
||||||
[toolbar]
|
[toolbar]
|
||||||
|
|
|
||||||
|
|
@ -229,6 +229,34 @@
|
||||||
(is (= {:seg :clip-a :f 0} (:s (first rows))))
|
(is (= {:seg :clip-a :f 0} (:s (first rows))))
|
||||||
(is (= {:seg :clip-a :f 100} (:e (first rows)))))))
|
(is (= {:seg :clip-a :f 100} (:e (first rows)))))))
|
||||||
|
|
||||||
|
;; =========================================================================
|
||||||
|
;; Suite 5 — a parent breaking a child's mark references
|
||||||
|
;; =========================================================================
|
||||||
|
|
||||||
|
(deftest broken-when-referenced-mark-deleted
|
||||||
|
(testing "deleting the referenced subclip dangles the child's mark"
|
||||||
|
(let [del (update-in x+y [:groups :ann-x :marks] #(vec (rest %)))] ; drop s0
|
||||||
|
(is (= [:m/y0] (s/broken-marks del :ann-y)))
|
||||||
|
(is (= "referenced clip deleted" (s/broken-reason del :ann-y))))))
|
||||||
|
|
||||||
|
(deftest broken-when-boundaries-shift-out-of-range
|
||||||
|
(testing "shrinking s0 below the child's :at makes the offset unreferenceable"
|
||||||
|
;; y0 refs s0 at 10..20; trim s0 to length 15 so 20 is past its end
|
||||||
|
(let [trim (assoc-in x+y [:groups :ann-x :marks 0 :end] {:ref :clip-b :at 65})]
|
||||||
|
(is (= 15 (s/length (s/resolve-mark trim :ann-x (get-in trim [:groups :ann-x :marks 0])))))
|
||||||
|
(is (= [:m/y0] (s/broken-marks trim :ann-y)))
|
||||||
|
(is (= "reference trimmed away" (s/broken-reason trim :ann-y))))))
|
||||||
|
|
||||||
|
(deftest broken-cascades-through-a-subclip
|
||||||
|
(testing "if the subclip's own ref breaks, the child referencing it breaks too"
|
||||||
|
(let [gone (update x+y :groups dissoc :clip-b)] ; s0 refs clip-b, y0 refs s0
|
||||||
|
(is (= [:m/y0] (s/broken-marks gone :ann-y)))
|
||||||
|
(is (= "referenced clip deleted" (s/broken-reason gone :ann-y))))))
|
||||||
|
|
||||||
|
(deftest healthy-annotation-has-no-reason
|
||||||
|
(testing "broken-reason is nil when every mark resolves"
|
||||||
|
(is (nil? (s/broken-reason x+y :ann-y)))))
|
||||||
|
|
||||||
(deftest repeats-are-unambiguous
|
(deftest repeats-are-unambiguous
|
||||||
(testing "two instances of A share a source frame but distinct locals (local is master)"
|
(testing "two instances of A share a source frame but distinct locals (local is master)"
|
||||||
(let [scene (with-group base :ann
|
(let [scene (with-group base :ann
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue