diff --git a/tl/src/tl/scene.cljs b/tl/src/tl/scene.cljs index 0cc61eb..a22746e 100644 --- a/tl/src/tl/scene.cljs +++ b/tl/src/tl/scene.cljs @@ -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 diff --git a/tl/src/tl/subs.cljs b/tl/src/tl/subs.cljs index 900d829..2eb709b 100644 --- a/tl/src/tl/subs.cljs +++ b/tl/src/tl/subs.cljs @@ -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) — diff --git a/tl/src/tl/views.cljs b/tl/src/tl/views.cljs index bea9189..229fa2e 100644 --- a/tl/src/tl/views.cljs +++ b/tl/src/tl/views.cljs @@ -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] diff --git a/tl/test/tl/scene_test.cljs b/tl/test/tl/scene_test.cljs index 521c304..d6ce5bf 100644 --- a/tl/test/tl/scene_test.cljs +++ b/tl/test/tl/scene_test.cljs @@ -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