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)
|
||||
(when-let [{:keys [xs xe track]} (target-range scene (:ref point))]
|
||||
(let [n (:at point)]
|
||||
{:frame (if (neg? n) (+ xe n 1) (+ xs n)) :track track}))))
|
||||
(let [n (:at point)
|
||||
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
|
||||
"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))))
|
||||
(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 --------------------------------
|
||||
|
||||
(defn content-segments
|
||||
|
|
|
|||
|
|
@ -62,13 +62,15 @@
|
|||
(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 (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")
|
||||
:content (:content g) :children (count (:marks g))
|
||||
:nested (get nested gid 0)
|
||||
:draft (boolean (:draft g))
|
||||
:broken (boolean reason) :reason reason
|
||||
:start (or (ffirst bars) 0) :bars bars}))))
|
||||
(sort-by :start)
|
||||
(sort-by (juxt :broken :start)) ; broken annotations sink to the bottom
|
||||
vec))))
|
||||
|
||||
;; the annotation the playhead is currently inside (or the latest one passed) —
|
||||
|
|
|
|||
|
|
@ -228,7 +228,7 @@
|
|||
^{:key (:id a)}
|
||||
[:div.ann {:id (str "ann-" (name (:id a))) :class (when (= (:id a) active) "active")}
|
||||
[:div.ann-head
|
||||
[:div.ann-title (:name a)]
|
||||
[:div.ann-title (when (:broken a) [:span {:title (:reason a)} "⚠ "]) (:name a)]
|
||||
[:div.ann-actions
|
||||
[jump-control open 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
|
||||
: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 []
|
||||
(let [status @(rf/subscribe [::subs/status])
|
||||
draft? (some? @(rf/subscribe [::subs/draft-group]))]
|
||||
|
|
@ -331,6 +342,7 @@
|
|||
[:div.top
|
||||
[:div.video-pane [video-monitor] [frame-readout]]
|
||||
[:div.annot-pane (if draft? [annotation-form] [commentary])]]
|
||||
[:div.divider-h {:on-pointer-down drag-top!}]
|
||||
[:div.timeline-pane
|
||||
[breadcrumbs]
|
||||
[toolbar]
|
||||
|
|
|
|||
|
|
@ -229,6 +229,34 @@
|
|||
(is (= {:seg :clip-a :f 0} (:s (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
|
||||
(testing "two instances of A share a source frame but distinct locals (local is master)"
|
||||
(let [scene (with-group base :ann
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue