claud erefactor checkpoint
This commit is contained in:
parent
06dd2b597c
commit
7ac8f27b57
3 changed files with 155 additions and 45 deletions
|
|
@ -34,3 +34,9 @@ decided: a clip/subclip-ref mark never crosses a clip boundary, so the "range wh
|
|||
this brings us to playing. since right now there is only one source video file, we need to be able to seek to arbitrary frames. each context (mark-group) keeps its own local playhead, used when it's the top of the timeline stack. when we hit play, in the example above of annotation X, we find the clip under the playhead, compute the source frame, seek there, and start playing. one correction though: the local playhead has to be the master clock, not the video. you can't derive local position from currentTime -- once an annotation repeats or reorders clips, one source frame maps to several local frames, it's not invertible. so the local playhead advances on its own (wall-clock x fps while playing), and every frame we compute expected = group->media(local) and seek the video there only if round(currentTime*fps) != expected. within a clip, expected tracks the video's natural playback so no seek fires; at a mark boundary it jumps once and we seek. and right -- no recursion at play time: we resolve the current context once into flat ordered spans, and group->media is just the flat lookup the renderer already does.
|
||||
# how do we determine which tracks are included when we zoom into each annotation? for now it should just be if a clip is within the ranges of the mark-group, its track is included in the annotation.
|
||||
# automatically scroll to bottom-most track in mark group range when we hit the start mark? but what if it's massively spread out. maybe not then. scrolling should be an option turned on. thats ok. make it explicit.
|
||||
|
||||
|
||||
* update!
|
||||
- ok so the idea is this. you hit the new annotation button. it does not auto-select a mark for you. you can either click the clip, click the frame button, or drag a range. after you select a range, you are automatically in drawing mode. your drawings are connected to the mark, not the annotation. a mark should only ever appear in one annotation. instead of creating a new annotation mark group by default, this mode also allows you to either create new or associate with an existing annotation. associate with existing gives you our dropdown with only other annotations available. when you pick one, you effectively go into "edit" mode on that annotation with the new marks suddenly added. so this is basically our "transclusion": we can have annotations with marks embedded in other timelines. this is great for if we have subdivided our analysis into "chapters" but want to annotate shared concepts across them while keeping the main annotation pane clear. it's organized. so the big thing is that you don't create the annotation, you create the mark(s) first, then either create or assoc the annotation.
|
||||
- another crucial thing: if we click and drag and it spans multiple clips, the range we have in our create/add annotation UI in the annotation pane should only show the start and end points relative to the clips at start and end. so the way this will work is we will create a mark group that's not an annotation as our "proxy" marks so that they don't appear in the ui that spans the full range and contains the full sequential clips, and then the mark group on the annotation that contains that mark group will just use that mark group as start and end as if it had been clicked. so, for example: there's clips A, B, C and D contiguous. user drags region from clip A to clip D. in the UI, we should see that our mark starts at clip A frame 0 and ends at clip D last frame, so we need to "pass through" the synthetic unnamed non-annotation mark group to the underlying clips, the synthetic unnamed non-annotation mark group is a proxy.. so that proxy mark group has marks that go from clip A start-clip A end, clip B start - clip B end, clip C start to clip C end, and clip D start to clip D end. makes sense? so what do we do if the user wants to drag adjust endpoint in UI? let's say there's another clip before clip A called clip 0. we move start point BACK to clip 0 frame 50. well, our main mark group just shows the range as we would expect: clip 0 frame 50 TO clip D last frame. but the proxy mark group? it has a new mark range with new mark id at the beginning, but the other mark ids are stable. and the same is true of rolling the end point forward: new mark id, new range, rest are stsable. what if we roll the endpoints inward? same principle but we kill off mark ranges instead of adding new ones. should be clean. so this means we needs we need to change how draft/edit marks look/act in the lane. when in draft/edit mode, clicking on the mark brings up drawing mode for that mark (there can still be a button next to the mark in the edit pane). you can drag the whole mark left to right. and you can also grab handles on the edges of the annotation left to right. and since we consider whatever last created or last touched mark to be the "active" one for associating drawings to, we need to have that visually represented in the timeline, and in the pane where the draft marks or edit annotation is. and these need to share the same look for the mark range,s its only the stuff above it that iwll change. make sense?
|
||||
- note: when we talk about rolling the whole clip around, we know that the mark ids are going to change if we highlight one clip, unhighlight, then return back. this means that if we annotated a range defined w/r/t that annotation, the underlying gids are broken forever, even if they're rolled back. so that they exist still, right? since the underlying clips will never change, i wonder if we could just give each clip a stable identifier and define our root-most ranges in terms of those stable identifiers? or is that worse? idk
|
||||
|
|
|
|||
|
|
@ -390,9 +390,18 @@
|
|||
:else t)
|
||||
note {:type :script-note :name nm :color "#c2864e"
|
||||
:regions [(merge {:id rid :kind :text :content ""} region)]}
|
||||
target (get-in db [:view :note-target])
|
||||
db (-> db (assoc-in [:scene :groups gid] note)
|
||||
(assoc-in [:view :active-note] gid)
|
||||
(assoc :save-error nil))]
|
||||
(assoc :save-error nil))
|
||||
;; created for a mark → bind it and hop back to the annotation
|
||||
db (if target
|
||||
(-> db (update-in [:scene :groups (:gid target) :marks]
|
||||
(fn [ms] (mapv (fn [m] (if (= (:mark-id target) (:id m))
|
||||
(update m :notes add-in gid) m)) ms)))
|
||||
(assoc-in [:view :note-target] nil)
|
||||
(assoc-in [:view :pane] :annotations))
|
||||
db)]
|
||||
(merge {:db db} (persist-note-fx db gid)))))
|
||||
|
||||
;; commentary edits update the note locally (on-change); persist on blur so we
|
||||
|
|
@ -433,6 +442,21 @@
|
|||
(fn [db [_ gid mark-id note-gid]]
|
||||
(update-mark db gid mark-id #(update % :notes rm-in note-gid))))
|
||||
|
||||
;; click a mark row to make it the active one (drawings/edits target it)
|
||||
(rf/reg-event-db ::set-active-mark
|
||||
(fn [db [_ mark-id]] (assoc-in db [:view :active-mark] mark-id)))
|
||||
|
||||
;; "add a new script note for this mark": remember the target mark + hop to the
|
||||
;; Script pane. When a note is created there (highlight-into-new-note) it binds to
|
||||
;; the target and returns to the annotation; ::cancel-note-target backs out.
|
||||
(rf/reg-event-db ::new-note-for-mark
|
||||
(fn [db [_ gid mark-id]]
|
||||
(-> db (assoc-in [:view :note-target] {:gid gid :mark-id mark-id})
|
||||
(assoc-in [:view :pane] :script))))
|
||||
(rf/reg-event-db ::cancel-note-target
|
||||
(fn [db _] (-> db (assoc-in [:view :note-target] nil)
|
||||
(assoc-in [:view :pane] :annotations))))
|
||||
|
||||
;; --- drawings -------------------------------------------------------------
|
||||
;; A drawing is a first-class entity (:type :drawing) — a bag of normalized
|
||||
;; strokes + a seed for its wiggle boil — bound to a mark via mark :drawings
|
||||
|
|
@ -704,6 +728,18 @@
|
|||
(into (subvec marks 0 i) (subvec marks (inc i))))
|
||||
(assoc-in [:view :pt] {:seg (:ref keep) :f (:at keep) :mark mark :i i})))))
|
||||
|
||||
;; clear ONE endpoint of a proxy mark to re-pick it: remember the OPPOSITE
|
||||
;; endpoint's ctx-local position (kept fixed) so the next clip-click only rerolls
|
||||
;; the cleared side — the kept end never turns into the start (the old swap bug).
|
||||
(rf/reg-event-db
|
||||
::unset-proxy-endpoint
|
||||
(fn [db [_ gid mark-id pid which]]
|
||||
(let [scene (:scene db)
|
||||
ctx (:parent (get-in scene [:groups gid]))
|
||||
segs (scene/content-segments scene ctx)
|
||||
[lo hi] (scene/mark-extent scene ctx gid mark-id segs)]
|
||||
(assoc-in db [:view :pt] {:proxy pid :which which :keep (if (= which :start) hi lo)}))))
|
||||
|
||||
;; complete a selection on the active draft: wrap the run [lo hi) in a proxy (a
|
||||
;; synthetic clip), give the annotation one mark referencing it, make it active,
|
||||
;; seek to its start, and drop into drawing mode ("select a range → you're drawing").
|
||||
|
|
@ -737,7 +773,21 @@
|
|||
[gid g] (some (fn [[gid g]] (when (:draft g) [gid g])) (:groups scene))
|
||||
segs (scene/content-segments scene (:parent g))
|
||||
pt (get-in db [:view :pt])]
|
||||
(if (map? pt)
|
||||
(cond
|
||||
;; re-picking one endpoint of a proxy: roll only that side, keep the other
|
||||
(:proxy pt)
|
||||
(let [proxy (get-in scene [:groups (:proxy pt)])
|
||||
new-local (if (= (:which pt) :start)
|
||||
(scene/seg-local segs seg-id (or frame 0))
|
||||
(scene/seg-local segs seg-id (or frame (scene/seg-length segs seg-id))))
|
||||
keep (:keep pt)
|
||||
lo (js/Math.round (min new-local keep))
|
||||
hi (js/Math.round (max new-local keep))]
|
||||
{:db (-> db (assoc-in [:scene :groups (:proxy pt)]
|
||||
(scene/roll-proxy scene (:parent g) proxy lo (max (inc lo) hi)))
|
||||
(assoc-in [:view :pt] :new))})
|
||||
|
||||
(map? pt)
|
||||
(let [a (scene/seg-local segs (:seg pt) (:f pt))
|
||||
b (scene/seg-local segs seg-id (or frame (scene/seg-length segs seg-id)))
|
||||
lo (min a b) hi (max a b)
|
||||
|
|
@ -758,6 +808,8 @@
|
|||
(assoc-in [:view :pt] :new))})
|
||||
;; new selection (two-click): same completion as a timeline drag.
|
||||
(select-range-fx db gid g lo hi)))
|
||||
|
||||
:else
|
||||
{:db (assoc-in db [:view :pt] {:seg seg-id :f (or frame 0)})}))))
|
||||
|
||||
;; remove mark `i` from `gid`; if it referenced a proxy, drop the now-orphaned
|
||||
|
|
@ -796,6 +848,16 @@
|
|||
(assoc-in db [:scene :groups pid] (scene/roll-proxy scene ctx proxy la* lb*))
|
||||
db))))
|
||||
|
||||
;; numeric endpoint edit from the pane: set a proxy's boundary internal mark's
|
||||
;; :at directly (the collapsed row's start = first mark's start, end = last mark's
|
||||
;; end). In-place within one clip — no boundary crossing (that's the lane handles).
|
||||
(rf/reg-event-db
|
||||
::set-proxy-frame
|
||||
(fn [db [_ pid which frame]]
|
||||
(let [marks (get-in db [:scene :groups pid :marks])
|
||||
idx (if (= which :start) 0 (dec (count marks)))]
|
||||
(assoc-in db [:scene :groups pid :marks idx which :at] frame))))
|
||||
|
||||
;; transclusion: instead of creating a new annotation, append the draft's marks
|
||||
;; to an EXISTING one and open it in edit mode ("the new marks suddenly added").
|
||||
;; We persist the attachment now (the marks + their proxies) and discard the draft
|
||||
|
|
|
|||
|
|
@ -493,9 +493,10 @@
|
|||
|
||||
(defn- region-select!
|
||||
"On the timeline while authoring: DRAG to select a region [lo hi) → one
|
||||
selection (a proxy); a plain CLICK (no drag) just moves the playhead, so you
|
||||
can still scrub in marking mode. `content` is the coord ref."
|
||||
[content fps zoom ev]
|
||||
selection (a proxy). A plain CLICK (no drag) calls `on-click` — clicking a clip
|
||||
picks it (draft-click-seg), clicking empty timeline just moves the playhead —
|
||||
so both still work in marking mode. `content` is the coord ref."
|
||||
[content fps zoom on-click ev]
|
||||
(.preventDefault ev)
|
||||
(let [rect (.getBoundingClientRect content)
|
||||
sx (.-clientX ev)
|
||||
|
|
@ -513,7 +514,7 @@
|
|||
(reset! region-sel nil)
|
||||
(if (and was (> (- hi lo) 0.5))
|
||||
(rf/dispatch [::events/draft-select-range (js/Math.round lo) (js/Math.round hi)])
|
||||
(goto! a))))] ; click → move the playhead
|
||||
(on-click a))))] ; click (no drag)
|
||||
(.addEventListener js/document "mousemove" mv)
|
||||
(.addEventListener js/document "mouseup" up)))
|
||||
|
||||
|
|
@ -755,7 +756,8 @@
|
|||
[:div.content.track-content {:ref (fn [n] (reset! content n))
|
||||
;; while authoring, drag empty space to select a region
|
||||
:on-mouse-down (if authoring?
|
||||
#(region-select! @content fps zoom %)
|
||||
;; empty timeline: drag = region, click = move playhead
|
||||
#(region-select! @content fps zoom goto! %)
|
||||
#(scrub! @content fps zoom %))
|
||||
:style {:width width :height tracks-h}}
|
||||
(for [[i t] (map-indexed vector tracks)]
|
||||
|
|
@ -772,7 +774,10 @@
|
|||
(if linking
|
||||
(commit-link! (scene/seg-point scene seg 0)
|
||||
(clip-label scene segs (:mark seg)))
|
||||
(region-select! @content fps zoom e))))
|
||||
;; drag = region; click = pick this clip (two-click flow)
|
||||
(region-select! @content fps zoom
|
||||
(fn [_] (rf/dispatch [::events/draft-click-seg (:mark seg)]))
|
||||
e))))
|
||||
:style {:left (px c fps zoom) :width (max 1 (px (- d c) fps zoom))
|
||||
:top (* (get track-y track 0) row-h) :height (- row-h 2)
|
||||
:line-height (str (- row-h 2) "px") :position "absolute"}}
|
||||
|
|
@ -937,7 +942,9 @@
|
|||
idx (r/atom 0)
|
||||
off (r/atom (or (:offset value) 0))
|
||||
picked (r/atom value)
|
||||
open? (r/atom true)]
|
||||
;; start CLOSED; only opens on focus (an auto-focused picker opens
|
||||
;; itself via :on-focus). Was defaulting open even when unfocused.
|
||||
open? (r/atom (boolean auto-focus?))]
|
||||
(let [cands (point-candidates scene ctx @text timelines?)
|
||||
i (min @idx (max 0 (dec (count cands))))
|
||||
cur (or (:candidate @picked) (get cands i))
|
||||
|
|
@ -966,7 +973,12 @@
|
|||
(reset! open? true)
|
||||
(reset! off 0)
|
||||
(preview! (get cands j) 0))]
|
||||
[:div.pt-input {:class class}
|
||||
[:div.pt-input {:class class
|
||||
;; close the dropdown once focus leaves the picker entirely
|
||||
;; (but not when it just moves between the text/frame inputs)
|
||||
:on-blur (fn [e]
|
||||
(when-not (.contains (.-currentTarget e) (.-relatedTarget e))
|
||||
(reset! open? false)))}
|
||||
[:input.pt-text
|
||||
{:auto-focus auto-focus?
|
||||
:placeholder (or placeholder "clip / annotation / frame...")
|
||||
|
|
@ -1274,6 +1286,23 @@
|
|||
[:button.pt-chip-x {:type "button" :title "Re-pick this end"
|
||||
:on-click #(rf/dispatch [::events/unset-endpoint (:gid d) i k])} "✕"]]))
|
||||
|
||||
(defn- proxy-frame-chip
|
||||
"Editable endpoint for a proxy (synthetic-clip) mark: clip name + frame input
|
||||
that edits the proxy's OWN boundary internal mark's :at in place (`which` =
|
||||
:start on its first mark, :end on its last). Crossing a clip boundary is the
|
||||
lane handles' job; this is the whole-frame numeric nudge within a clip. The ✕
|
||||
clears just this end to re-pick it (the other end stays put)."
|
||||
[scene segs gid mark-id pid which {:keys [seg f]}]
|
||||
(let [len (scene/seg-length segs seg)]
|
||||
[:div.pt-chip
|
||||
[:span.pt-chip-name (clip-label scene segs seg)]
|
||||
[:input.pt-frame {:type "number" :min 0 :max len :value f
|
||||
:on-change #(rf/dispatch [::events/set-proxy-frame pid which
|
||||
(to-frame (.. % -target -value) len)])}]
|
||||
[:span.pt-dur (str "/" len)]
|
||||
[:button.pt-chip-x {:type "button" :title "Re-pick this end"
|
||||
:on-click #(rf/dispatch [::events/unset-proxy-endpoint gid mark-id pid which])} "✕"]]))
|
||||
|
||||
;; --- script-note bindings (annotation form) ------------------------------
|
||||
;; A note is bound by dragging its chip from the source list onto a drop target
|
||||
;; (a mark row, or the annotation-level target). The pointer lives on the referrer
|
||||
|
|
@ -1347,10 +1376,11 @@
|
|||
gid (:gid d)
|
||||
new? (= :new (:draft d))
|
||||
root? (nil? (:parent d))
|
||||
;; a fresh draft starts in the "choosing" stage: just marks + a title
|
||||
;; autocomplete (pick an existing annotation → associate; type a new
|
||||
;; title → create). Everything else appears once you've committed.
|
||||
choosing? (and new? (not root?) (= :choosing @(rf/subscribe [::subs/draft-stage]))) ; the root timeline: content only
|
||||
;; ONE shared form for new + edit. In draft/new we HIDE the lower fields
|
||||
;; (content, notes, save) until a title is picked — pick a new title to
|
||||
;; "create new", or an existing annotation to "edit existing". The title
|
||||
;; sits at the top (above the marks), same place the name does in edit.
|
||||
choosing? (and new? (not root?) (= :choosing @(rf/subscribe [::subs/draft-stage])))
|
||||
put (fn [g] (rf/dispatch [::events/put-group gid (dissoc g :gid)]))
|
||||
notes @(rf/subscribe [::subs/notes])
|
||||
nmap (into {} (map (juxt :id identity)) notes)
|
||||
|
|
@ -1403,6 +1433,24 @@
|
|||
[:div.form-hint "Click a clip or the frame-readout to link it, or pick above."])])
|
||||
(when-not root?
|
||||
[:<>
|
||||
;; the title IS the create/associate control: type a new title to make a
|
||||
;; fresh annotation with these marks, or pick an existing annotation to
|
||||
;; add them to it (transclusion). Above the marks + autofocused so you can
|
||||
;; name it first thing.
|
||||
(when choosing?
|
||||
(let [targets @(rf/subscribe [::subs/associate-targets])
|
||||
by-name (into {} (map (juxt :name :gid)) targets)
|
||||
ctx-of (into {} (map (juxt :name :in)) targets)]
|
||||
[:div.form-associate
|
||||
[:div.form-marks-label "Title"]
|
||||
[autocomplete {:items (mapv :name targets) :allow-new? true :auto-focus? true
|
||||
:placeholder "new title, or pick an annotation to add to…"
|
||||
:item-suffix (fn [n] (when-let [c (ctx-of n)]
|
||||
[:span.cand-group (str " · " c)]))
|
||||
:on-choose (fn [choice]
|
||||
(if-let [g (by-name choice)]
|
||||
(rf/dispatch [::events/associate-marks gid g])
|
||||
(rf/dispatch [::events/create-named gid choice])))}]]))
|
||||
[:div.form-marks-label "Marks"]
|
||||
(doall
|
||||
(for [[i row] (map-indexed vector rows)
|
||||
|
|
@ -1424,16 +1472,30 @@
|
|||
(reset! mark-drag {:src i}))} "⠿"]
|
||||
(when (contains? broken mark-id)
|
||||
[:span.ann-warn {:title "This mark's clip/reference no longer resolves"} "△ "])
|
||||
;; a proxy collapses its cross-clip run to one read-only span
|
||||
;; (first-clip start → last-clip end); endpoint editing is via the
|
||||
;; lane handles (a later chunk). A plain clip mark stays editable.
|
||||
(if (:proxy row)
|
||||
;; a proxy collapses its cross-clip run to first-clip start →
|
||||
;; last-clip end; each endpoint edits the proxy's own boundary
|
||||
;; mark's frame (crossing a clip boundary is the lane handles).
|
||||
;; A plain clip mark edits its own :at directly.
|
||||
(if-let [pid (:proxy row)]
|
||||
;; while re-picking one end (after its ✕) that end becomes a live
|
||||
;; clip picker in the row; the other end stays a normal chip.
|
||||
(let [pick (when (and (map? pt) (= (:proxy pt) pid)) (:which pt))]
|
||||
[:<>
|
||||
[:span.pt-chip.proxy [:span.pt-chip-name
|
||||
(str (clip-label scene segs (:seg (:s row))) " @" (:f (:s row)) "f")]]
|
||||
(if (= pick :start)
|
||||
[point-picker {:scene scene :ctx ctx :class "active" :auto-focus? true
|
||||
:placeholder "click a clip for start…"
|
||||
:on-cancel #(rf/dispatch [::events/draft-focus :new])
|
||||
:on-pick #(when-let [p (local->draft-point segs (:local %))]
|
||||
(rf/dispatch [::events/draft-click-seg (:seg p) (:f p)]))}]
|
||||
[proxy-frame-chip scene segs gid mark-id pid :start (:s row)])
|
||||
[:span.mark-arrow "→"]
|
||||
[:span.pt-chip.proxy [:span.pt-chip-name
|
||||
(str (clip-label scene segs (:seg (:e row))) " @" (:f (:e row)) "f")]]]
|
||||
(if (= pick :end)
|
||||
[point-picker {:scene scene :ctx ctx :class "active" :auto-focus? true
|
||||
:placeholder "click a clip for end…"
|
||||
:on-cancel #(rf/dispatch [::events/draft-focus :new])
|
||||
:on-pick #(when-let [p (local->draft-end-point segs (:local %))]
|
||||
(rf/dispatch [::events/draft-click-seg (:seg p) (:f p)]))}]
|
||||
[proxy-frame-chip scene segs gid mark-id pid :end (:e row)])])
|
||||
[:<>
|
||||
[frame-chip scene segs put d i :start (:s row)]
|
||||
[:span.mark-arrow "→"]
|
||||
|
|
@ -1452,7 +1514,7 @@
|
|||
[note-drop nil (:notes mark) nmap live
|
||||
#(rf/dispatch [::events/bind-note-mark gid mark-id %])
|
||||
#(rf/dispatch [::events/unbind-note-mark gid mark-id %])])]))
|
||||
(when (map? pt)
|
||||
(when (and (map? pt) (not (:proxy pt)))
|
||||
[:div.mark-row
|
||||
[:div.pt-chip [:span.pt-chip-name (str (clip-label scene segs (:seg pt))
|
||||
" @" (js/Math.round (:f pt)) "f")]]
|
||||
|
|
@ -1472,26 +1534,6 @@
|
|||
:on-pick #(when-let [p (local->draft-point segs (:local %))]
|
||||
(rf/dispatch [::events/draft-click-seg (:seg p) (:f p)]))}]])
|
||||
[:div.form-hint "Drag across the timeline to select a range (click to move the playhead)."]
|
||||
;; the title IS the create/associate control: type a new title to make a
|
||||
;; fresh annotation with these marks, or pick an existing annotation to
|
||||
;; add them to it (dropping straight into its edit form). Transclusion.
|
||||
(when choosing?
|
||||
(let [targets @(rf/subscribe [::subs/associate-targets])
|
||||
;; match on the plain NAME (round-trips cleanly); the home
|
||||
;; context is a dropdown-only hint via :item-suffix, so it never
|
||||
;; leaks into a newly-created annotation's name.
|
||||
by-name (into {} (map (juxt :name :gid)) targets)
|
||||
ctx-of (into {} (map (juxt :name :in)) targets)]
|
||||
[:div.form-associate
|
||||
[:div.form-marks-label "Title"]
|
||||
[autocomplete {:items (mapv :name targets) :allow-new? true
|
||||
:placeholder "new title, or pick an annotation to add to…"
|
||||
:item-suffix (fn [n] (when-let [c (ctx-of n)]
|
||||
[:span.cand-group (str " · " c)]))
|
||||
:on-choose (fn [choice]
|
||||
(if-let [g (by-name choice)]
|
||||
(rf/dispatch [::events/associate-marks gid g])
|
||||
(rf/dispatch [::events/create-named gid choice])))}]]))
|
||||
(when-not choosing?
|
||||
[:<>
|
||||
[:div.form-marks-label "Script notes"]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue