fix: live-sync drawings/notes; cut per-frame sub cost; pen color+size

- backend: the scene PUT only broadcast to peers when annotation counts
  changed, so drawing/script-note edits saved but never synced live (only
  after a reload). Broadcast any gid that actually moved, regardless of type.
- perf: active-drawings/active-note-set recomputed fully on every playhead
  tick (active-drawings even re-resolved each annotation per mark). Split the
  bar computation into memoized ::drawing-bars/::note-bars (scene/ctx/segs
  only) and reduce per-frame work to cheap interval tests.
- perf: only subscribe drawing-layer to the boil clock when something is
  actually animating (no more 7fps idle re-renders).
- drawings: pen colour picker + stroke-size slider, still defaulting to the
  annotation colour per session.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Your Name 2026-07-01 19:29:32 -04:00
parent 9f06b51f76
commit 40ff2ecf52
4 changed files with 80 additions and 54 deletions

View file

@ -95,6 +95,9 @@ body { overflow: hidden; background: var(--desktop); background-size: 4px 4px;
.draw-tools button.active { background: var(--ink); }
.draw-tools button:disabled { opacity: .4; cursor: default; }
.draw-tools .draw-spacer { width: 8px; }
.draw-tools .draw-color { width: 22px; height: 20px; padding: 0; border: 1px solid var(--ink);
background: none; cursor: pointer; }
.draw-tools .draw-wid { width: 56px; }
.draw-done { font-weight: bold; }
.mark-draw { background: none; border: 1px solid transparent; border-radius: 0;
font-size: 12px; cursor: pointer; padding: 0 3px; line-height: 1; }

View file

@ -151,49 +151,56 @@
(rf/reg-sub ::draw (fn [db] (get-in db [:view :draw]))) ; draw-mode state or nil
(rf/reg-sub ::drawing :<- [::scene] (fn [scene [_ gid]] (get-in scene [:groups gid])))
;; drawings to render over the video now: those bound to a mark whose span the
;; playhead is inside — the same playhead-in-mark hook that lights an annotation.
;; Bar precompute — the expensive part (resolving annotations into local bars)
;; depends only on scene/context/segments, NOT the playhead, so it's memoized here
;; and stays put while you scrub or play. The playhead-driven `active-*` subs below
;; then just do cheap interval tests. `bound` picks :notes or :drawings.
(defn- binding-bars [scene ctx segs bound]
(into []
(mapcat
(fn [[gid g]]
;; child annotations of the context OR the context annotation itself
;; (pushing the owner onto the stack makes ctx that annotation)
(when (and (= :annotation (:type g)) (or (= ctx (:parent g)) (= ctx gid)))
(let [src-segs (scene/resolve scene gid)
by-mark (group-by :mark src-segs)
->bars (fn [ss] (scene/merge-bars
(mapcat (fn [{[a b] :src}] (scene/pieces segs a b)) ss)))]
(concat
;; annotation-level bindings (notes only; drawings bind per-mark)
(when-let [gs (seq (bound g))] [{:gids gs :bars (->bars src-segs)}])
;; per-mark bindings
(for [m (:marks g) :when (seq (bound m))]
{:gids (bound m) :bars (->bars (get by-mark (:id m)))}))))))
(:groups scene)))
(rf/reg-sub ::drawing-bars
:<- [::scene] :<- [::context] :<- [::segments]
(fn [[scene ctx segs] _] (binding-bars scene ctx segs :drawings)))
(rf/reg-sub ::note-bars
:<- [::scene] :<- [::context] :<- [::segments]
(fn [[scene ctx segs] _] (binding-bars scene ctx segs :notes)))
(defn- gids-at [entries ph]
(persistent!
(reduce (fn [acc {:keys [gids bars]}]
(if (some (fn [[lo hi]] (<= lo ph hi)) bars) (reduce conj! acc gids) acc))
(transient #{}) entries)))
;; drawings to render over the video now (playhead in a bound mark's span).
(rf/reg-sub
::active-drawings
:<- [::scene] :<- [::context] :<- [::segments] :<- [::playhead]
(fn [[scene ctx segs ph] _]
(let [in? (fn [bars] (some (fn [[lo hi]] (<= lo ph hi)) bars))]
(->> (:groups scene)
;; child annotations of the context OR the context annotation itself
;; (when you push the owner onto the stack, ctx IS that annotation)
(mapcat (fn [[gid g]]
(when (and (= :annotation (:type g)) (or (= ctx (:parent g)) (= ctx gid)))
(mapcat (fn [m]
(when (and (seq (:drawings m))
(in? (scene/mark-bars scene gid (:id m) segs)))
(:drawings m)))
(:marks g)))))
distinct
(keep (fn [dg] (when-let [d (get-in scene [:groups dg])]
(when (= :drawing (:type d)) (assoc d :id dg)))))
vec))))
:<- [::scene] :<- [::drawing-bars] :<- [::playhead]
(fn [[scene dbars ph] _]
(->> (gids-at dbars ph)
(keep (fn [dg] (when-let [d (get-in scene [:groups dg])]
(when (= :drawing (:type d)) (assoc d :id dg)))))
vec)))
;; the set of note gids whose bound span currently contains the playhead — drives
;; the >>> "we're in range" indicator. Annotation-level bindings light while the
;; playhead is in ANY of the annotation's bars; a mark-level binding lights only
;; while the playhead is in that specific mark's bars.
;; note gids whose bound span currently contains the playhead — drives the >>>
;; indicator (annotation-level lights on any bar; mark-level on that mark's bars).
(rf/reg-sub
::active-note-set
:<- [::scene] :<- [::context] :<- [::segments] :<- [::playhead]
(fn [[scene ctx segs ph] _]
(let [in? (fn [bars] (some (fn [[lo hi]] (<= lo ph hi)) bars))
bars (fn [src-segs] (scene/merge-bars
(mapcat (fn [{[a b] :src}] (scene/pieces segs a b)) src-segs)))]
(reduce
(fn [acc [gid g]]
(if (and (= :annotation (:type g)) (or (= ctx (:parent g)) (= ctx gid)))
(let [src-segs (scene/resolve scene gid)
acc (if (and (seq (:notes g)) (in? (bars src-segs)))
(into acc (:notes g)) acc)]
(reduce (fn [acc m]
(if (and (seq (:notes m))
(in? (bars (filter #(= (:id m) (:mark %)) src-segs))))
(into acc (:notes m)) acc))
acc (:marks g)))
acc))
#{} (:groups scene)))))
:<- [::note-bars] :<- [::playhead]
(fn [[nbars ph] _] (gids-at nbars ph)))

View file

@ -261,17 +261,21 @@
Editing state is local; only the committed strokes persist."
[]
(r/with-let [strokes (r/atom []) undo (r/atom []) cur (r/atom nil)
tool (r/atom :pencil) svg (atom nil) sess (atom nil)]
tool (r/atom :pencil) svg (atom nil) sess (atom nil)
pen-color (r/atom nil) pen-w (r/atom draw-width)]
(let [draw @(rf/subscribe [::subs/draw])
drawing (when draw @(rf/subscribe [::subs/drawing (:gid draw)]))
color (when draw (or (get-in @(rf/subscribe [::subs/scene]) [:groups (:ann draw) :color]) "#111"))
actives @(rf/subscribe [::subs/active-drawings])
tick @boil
variant (mod tick wiggle-variants)]
;; seed local editing state at the start of a draw session
;; only subscribe to the boil clock when there's actually something to
;; animate — otherwise this component re-renders 7×/s for nothing
variant (if (or draw (seq actives)) (mod @boil wiggle-variants) 0)]
;; seed local editing state at the start of a draw session — pen defaults to
;; the annotation's colour (user can override for this session)
(when (and draw (not= @sess (:gid draw)))
(reset! sess (:gid draw)) (reset! strokes (vec (:strokes drawing)))
(reset! undo []) (reset! cur nil) (reset! tool :pencil))
(reset! undo []) (reset! cur nil) (reset! tool :pencil)
(reset! pen-color color) (reset! pen-w draw-width))
(when (and (nil? draw) @sess) (reset! sess nil))
(let [push! #(swap! undo conj @strokes)
erase-at (fn [xy] (when-let [i (first (keep-indexed (fn [i s] (when (near-stroke? xy s) i)) @strokes))]
@ -283,7 +287,7 @@
(let [xy (pointer-xy @svg e)]
(.setPointerCapture (.-target e) (.-pointerId e))
(if (= @tool :eraser) (erase-at xy)
(reset! cur {:pts [xy] :color color :w draw-width})))))
(reset! cur {:pts [xy] :color (or @pen-color color) :w @pen-w})))))
:on-pointer-move
(when draw (fn [^js e]
(let [xy (pointer-xy @svg e)]
@ -302,6 +306,11 @@
[:div.draw-tools
[:button {:class (when (= @tool :pencil) "active") :title "Pencil" :on-click #(reset! tool :pencil)} "✏️"]
[:button {:class (when (= @tool :eraser) "active") :title "Eraser (deletes a stroke)" :on-click #(reset! tool :eraser)} "🧽"]
[:input.draw-color {:type "color" :title "Pen colour (defaults to the annotation colour)"
:value (or @pen-color color "#111111")
:on-change #(reset! pen-color (.. % -target -value))}]
[:input.draw-wid {:type "range" :min 1 :max 8 :step 0.5 :title "Stroke size"
:value @pen-w :on-change #(reset! pen-w (js/parseFloat (.. % -target -value)))}]
[:button {:title "Undo" :disabled (empty? @undo)
:on-click #(when (seq @undo) (reset! strokes (peek @undo)) (swap! undo pop))} "↶"]
[:span.draw-spacer]