feat: transclusion completion by chat
This commit is contained in:
parent
40dd3b0d59
commit
d2d803e38b
10 changed files with 259 additions and 101 deletions
|
|
@ -75,7 +75,7 @@
|
||||||
(let [stack (vec (valid-stack scene stack))
|
(let [stack (vec (valid-stack scene stack))
|
||||||
ctx (peek stack)]
|
ctx (peek stack)]
|
||||||
(cond-> (assoc view :stack stack)
|
(cond-> (assoc view :stack stack)
|
||||||
playhead (assoc-in [:playheads ctx] playhead))))
|
playhead (assoc-in [:playheads ctx] (scene/assert-frame "route playhead" playhead)))))
|
||||||
|
|
||||||
(defn- route-state [db]
|
(defn- route-state [db]
|
||||||
(let [ctx (peek (get-in db [:view :stack]))]
|
(let [ctx (peek (get-in db [:view :stack]))]
|
||||||
|
|
@ -601,7 +601,8 @@
|
||||||
;; moved the stack, so snap back to the editing context before seeking the frame.
|
;; moved the stack, so snap back to the editing context before seeking the frame.
|
||||||
(rf/reg-event-fx ::preview-frame
|
(rf/reg-event-fx ::preview-frame
|
||||||
(fn [{:keys [db]} [_ local]]
|
(fn [{:keys [db]} [_ local]]
|
||||||
(let [stack (get-in db [:view :linking :stack])
|
(let [local (scene/assert-frame "preview frame" local)
|
||||||
|
stack (get-in db [:view :linking :stack])
|
||||||
ctx (peek stack)
|
ctx (peek stack)
|
||||||
db (-> db (assoc-in [:view :stack] stack)
|
db (-> db (assoc-in [:view :stack] stack)
|
||||||
(assoc-in [:view :playheads ctx] local))
|
(assoc-in [:view :playheads ctx] local))
|
||||||
|
|
@ -611,7 +612,8 @@
|
||||||
|
|
||||||
(rf/reg-event-fx ::set-playhead
|
(rf/reg-event-fx ::set-playhead
|
||||||
(fn [{:keys [db]} [_ ctx lf]]
|
(fn [{:keys [db]} [_ ctx lf]]
|
||||||
(let [next-db (-> db
|
(let [lf (scene/assert-frame "playhead" lf)
|
||||||
|
next-db (-> db
|
||||||
(assoc-in [:view :playheads ctx] lf)
|
(assoc-in [:view :playheads ctx] lf)
|
||||||
(sync-draft-mark-for-playhead ctx lf))]
|
(sync-draft-mark-for-playhead ctx lf))]
|
||||||
(sync-route {:db next-db} next-db))))
|
(sync-route {:db next-db} next-db))))
|
||||||
|
|
@ -841,8 +843,8 @@
|
||||||
(scene/seg-local segs seg-id (or frame 0))
|
(scene/seg-local segs seg-id (or frame 0))
|
||||||
(scene/seg-local segs seg-id (or frame (scene/seg-length segs seg-id))))
|
(scene/seg-local segs seg-id (or frame (scene/seg-length segs seg-id))))
|
||||||
keep (:keep pt)
|
keep (:keep pt)
|
||||||
lo (js/Math.round (min new-local keep))
|
lo (scene/assert-frame "proxy range start" (min new-local keep))
|
||||||
hi (js/Math.round (max new-local keep))]
|
hi (scene/assert-frame "proxy range end" (max new-local keep))]
|
||||||
{:db (-> db (assoc-in [:scene :groups (:proxy pt)]
|
{:db (-> db (assoc-in [:scene :groups (:proxy pt)]
|
||||||
(scene/roll-proxy scene (:parent g) proxy lo (max (inc lo) hi)))
|
(scene/roll-proxy scene (:parent g) proxy lo (max (inc lo) hi)))
|
||||||
(assoc-in [:view :pt] :new))})
|
(assoc-in [:view :pt] :new))})
|
||||||
|
|
@ -903,9 +905,8 @@
|
||||||
;; For a normal (non-transcluded) mark the two are the same.
|
;; For a normal (non-transcluded) mark the two are the same.
|
||||||
ctx (peek (get-in db [:view :stack]))
|
ctx (peek (get-in db [:view :stack]))
|
||||||
len (scene/length (scene/content-segments scene ctx))
|
len (scene/length (scene/content-segments scene ctx))
|
||||||
;; the dragged endpoint is a whole frame; the FIXED endpoint arrives
|
la (scene/assert-frame "proxy roll start" la)
|
||||||
;; EXACT (fractional) so its mark re-derives identically. Only the fixed
|
lb (scene/assert-frame "proxy roll end" lb)
|
||||||
;; end must avoid rounding here — the clamp keeps [la lb) valid half-open.
|
|
||||||
la* (max 0 (min la (dec len)))
|
la* (max 0 (min la (dec len)))
|
||||||
lb* (max (inc la*) (min lb len))]
|
lb* (max (inc la*) (min lb len))]
|
||||||
(if (= :proxy (:type proxy))
|
(if (= :proxy (:type proxy))
|
||||||
|
|
@ -918,7 +919,8 @@
|
||||||
(rf/reg-event-db
|
(rf/reg-event-db
|
||||||
::set-proxy-frame
|
::set-proxy-frame
|
||||||
(fn [db [_ pid which frame]]
|
(fn [db [_ pid which frame]]
|
||||||
(let [marks (get-in db [:scene :groups pid :marks])
|
(let [frame (scene/assert-frame "proxy endpoint frame" frame)
|
||||||
|
marks (get-in db [:scene :groups pid :marks])
|
||||||
idx (if (= which :start) 0 (dec (count marks)))]
|
idx (if (= which :start) 0 (dec (count marks)))]
|
||||||
(assoc-in db [:scene :groups pid :marks idx which :at] frame))))
|
(assoc-in db [:scene :groups pid :marks idx which :at] frame))))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,14 +13,20 @@
|
||||||
(defn- clip? [item]
|
(defn- clip? [item]
|
||||||
(str/starts-with? (:OTIO_SCHEMA item "") "Clip"))
|
(str/starts-with? (:OTIO_SCHEMA item "") "Clip"))
|
||||||
|
|
||||||
(defn- frames
|
(defn- timeline-frames
|
||||||
"Frame number of a RationalTime, as a WHOLE frame. This is the ONE place a
|
"Frame count/position for timeline math. These must already be integer frames."
|
||||||
fraction can enter: a rate conform (e.g. 23.976 NTSC) gives fractional source
|
|
||||||
positions (start_time). A frame is absolute and integer — you can't seek to half
|
|
||||||
a frame — so we snap here, at the boundary. Everything downstream is integer and
|
|
||||||
nothing else rounds (durations are already whole, so timeline tiling is exact)."
|
|
||||||
[rational-time]
|
[rational-time]
|
||||||
(js/Math.round (:value rational-time)))
|
(let [v (:value rational-time)]
|
||||||
|
(when-not (integer? v)
|
||||||
|
(throw (js/Error. (str "OTIO RationalTime value must be an integer frame, got " v))))
|
||||||
|
v))
|
||||||
|
|
||||||
|
(defn- media-frame
|
||||||
|
"Source media position as an integer frame index. Some OTIO exports carry
|
||||||
|
rate-conformed source starts as sub-frame RationalTime values; the app model is
|
||||||
|
frame-index based, so source starts are normalized once at this import boundary."
|
||||||
|
[rational-time]
|
||||||
|
(int (:value rational-time)))
|
||||||
|
|
||||||
(defn- clip-starts
|
(defn- clip-starts
|
||||||
"Source start_time (frames) of every clip across all tracks."
|
"Source start_time (frames) of every clip across all tracks."
|
||||||
|
|
@ -28,15 +34,15 @@
|
||||||
(for [t tracks
|
(for [t tracks
|
||||||
c (:children t)
|
c (:children t)
|
||||||
:when (clip? c)]
|
:when (clip? c)]
|
||||||
(frames (get-in c [:source_range :start_time]))))
|
(media-frame (get-in c [:source_range :start_time]))))
|
||||||
|
|
||||||
(defn- parse-track [media-offset idx track]
|
(defn- parse-track [media-offset idx track]
|
||||||
(loop [pos 0.0
|
(loop [pos 0
|
||||||
items (:children track)
|
items (:children track)
|
||||||
ci 0
|
ci 0
|
||||||
clips (transient [])]
|
clips (transient [])]
|
||||||
(if-let [item (first items)]
|
(if-let [item (first items)]
|
||||||
(let [dur (frames (get-in item [:source_range :duration]))
|
(let [dur (timeline-frames (get-in item [:source_range :duration]))
|
||||||
is-clip (clip? item)]
|
is-clip (clip? item)]
|
||||||
(recur (+ pos dur)
|
(recur (+ pos dur)
|
||||||
(rest items)
|
(rest items)
|
||||||
|
|
@ -46,7 +52,7 @@
|
||||||
:name (:name item)
|
:name (:name item)
|
||||||
:start pos ; timeline frame, 0-based
|
:start pos ; timeline frame, 0-based
|
||||||
:duration dur
|
:duration dur
|
||||||
:media-in (- (frames (get-in item [:source_range :start_time]))
|
:media-in (- (media-frame (get-in item [:source_range :start_time]))
|
||||||
media-offset)}) ; frame into local .mov
|
media-offset)}) ; frame into local .mov
|
||||||
clips)))
|
clips)))
|
||||||
{:id idx
|
{:id idx
|
||||||
|
|
@ -62,11 +68,11 @@
|
||||||
(let [tracks (get-in otio [:tracks :children])
|
(let [tracks (get-in otio [:tracks :children])
|
||||||
fps (get-in otio [:global_start_time :rate] 24)
|
fps (get-in otio [:global_start_time :rate] 24)
|
||||||
starts (clip-starts tracks)
|
starts (clip-starts tracks)
|
||||||
media-offset (if (seq starts) (apply min starts) 0.0)
|
media-offset (if (seq starts) (apply min starts) 0)
|
||||||
parsed (vec (map-indexed (partial parse-track media-offset) tracks))]
|
parsed (vec (map-indexed (partial parse-track media-offset) tracks))]
|
||||||
{:fps fps
|
{:fps fps
|
||||||
:media-offset media-offset
|
:media-offset media-offset
|
||||||
:duration (reduce max 0.0 (for [t parsed
|
:duration (reduce max 0 (for [t parsed
|
||||||
c (:clips t)]
|
c (:clips t)]
|
||||||
(+ (:media-in c) (:duration c))))
|
(+ (:media-in c) (:duration c))))
|
||||||
:tracks parsed}))
|
:tracks parsed}))
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,8 @@
|
||||||
(:require
|
(:require
|
||||||
[clojure.string :as str]
|
[clojure.string :as str]
|
||||||
[reitit.frontend :as reitit]
|
[reitit.frontend :as reitit]
|
||||||
[reitit.frontend.easy :as rfe]))
|
[reitit.frontend.easy :as rfe]
|
||||||
|
[tl.scene :as scene]))
|
||||||
|
|
||||||
(def routes
|
(def routes
|
||||||
[["/" {:name :projects}]
|
[["/" {:name :projects}]
|
||||||
|
|
@ -54,15 +55,18 @@
|
||||||
(into {} (map (fn [[k v]] [(keyword k) v])
|
(into {} (map (fn [[k v]] [(keyword k) v])
|
||||||
(:query-params match))))
|
(:query-params match))))
|
||||||
stack (some-> (:stack qp) (str/split #","))
|
stack (some-> (:stack qp) (str/split #","))
|
||||||
f (some-> (:f qp) js/parseFloat)]
|
fstr (some-> (:f qp) str)
|
||||||
|
f (when (and fstr (re-matches #"\d+" fstr))
|
||||||
|
(scene/assert-frame "route playhead" (js/parseInt fstr 10)))]
|
||||||
(cond-> {}
|
(cond-> {}
|
||||||
(seq stack) (assoc :stack (mapv keyword stack))
|
(seq stack) (assoc :stack (mapv keyword stack))
|
||||||
(and f (not (js/isNaN f))) (assoc :playhead f))))
|
f (assoc :playhead f))))
|
||||||
|
|
||||||
(defn project-url [id stack playhead]
|
(defn project-url [id stack playhead]
|
||||||
(let [stack-param (->> (rest stack) (map name) (str/join ","))
|
(let [stack-param (->> (rest stack) (map name) (str/join ","))
|
||||||
query (query-string {:stack stack-param
|
query (query-string {:stack stack-param
|
||||||
:f (some-> playhead js/Math.round)})]
|
:f (when (some? playhead)
|
||||||
|
(scene/assert-frame "route playhead" playhead))})]
|
||||||
(str (href :project/show {:id id}) query)))
|
(str (href :project/show {:id id}) query)))
|
||||||
|
|
||||||
(defonce ^:private last-replaced (atom nil))
|
(defonce ^:private last-replaced (atom nil))
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,28 @@
|
||||||
|
|
||||||
(defn- grp [scene gid] (get-in scene [:groups gid]))
|
(defn- grp [scene gid] (get-in scene [:groups gid]))
|
||||||
|
|
||||||
|
(defn frame?
|
||||||
|
"True when `n` is a concrete integer frame coordinate."
|
||||||
|
[n]
|
||||||
|
(and (number? n) (integer? n) (not (js/isNaN n))))
|
||||||
|
|
||||||
|
(defn assert-frame
|
||||||
|
"Return `n` after asserting it is an integer frame. This is intentionally a
|
||||||
|
runtime check, not cljs.core/assert, so production builds keep the invariant."
|
||||||
|
[label n]
|
||||||
|
(when-not (frame? n)
|
||||||
|
(throw (js/Error. (str label " must be an integer frame, got " (pr-str n)))))
|
||||||
|
n)
|
||||||
|
|
||||||
|
(defn assert-range
|
||||||
|
"Return `[lo hi]` after asserting a valid integer half-open frame range."
|
||||||
|
[label [lo hi]]
|
||||||
|
(assert-frame (str label " start") lo)
|
||||||
|
(assert-frame (str label " end") hi)
|
||||||
|
(when (> lo hi)
|
||||||
|
(throw (js/Error. (str label " must be ordered, got " (pr-str [lo hi])))))
|
||||||
|
[lo hi])
|
||||||
|
|
||||||
(defn- find-mark
|
(defn- find-mark
|
||||||
"[owning-gid mark] for a mark id anywhere in the scene, or nil."
|
"[owning-gid mark] for a mark id anywhere in the scene, or nil."
|
||||||
[scene mid]
|
[scene mid]
|
||||||
|
|
@ -31,7 +53,10 @@
|
||||||
(defn local->source
|
(defn local->source
|
||||||
"The source frame shown at local frame `lf` (clamped to the end)."
|
"The source frame shown at local frame `lf` (clamped to the end)."
|
||||||
[segs lf]
|
[segs lf]
|
||||||
|
(assert-frame "local frame" lf)
|
||||||
(or (some (fn [{:keys [src local]}]
|
(or (some (fn [{:keys [src local]}]
|
||||||
|
(assert-range "segment source" src)
|
||||||
|
(assert-range "segment local" local)
|
||||||
(let [[c d] local [a _] src]
|
(let [[c d] local [a _] src]
|
||||||
(when (and (<= c lf) (< lf d)) (+ a (- lf c)))))
|
(when (and (<= c lf) (< lf d)) (+ a (- lf c)))))
|
||||||
segs)
|
segs)
|
||||||
|
|
@ -40,7 +65,10 @@
|
||||||
(defn source->local
|
(defn source->local
|
||||||
"Local frame for source frame `sf` (first segment containing it), or nil."
|
"Local frame for source frame `sf` (first segment containing it), or nil."
|
||||||
[segs sf]
|
[segs sf]
|
||||||
|
(assert-frame "source frame" sf)
|
||||||
(some (fn [{:keys [src local]}]
|
(some (fn [{:keys [src local]}]
|
||||||
|
(assert-range "segment source" src)
|
||||||
|
(assert-range "segment local" local)
|
||||||
(let [[a b] src [c _] local]
|
(let [[a b] src [c _] local]
|
||||||
(when (and (<= a sf) (< sf b)) (+ c (- sf a)))))
|
(when (and (<= a sf) (< sf b)) (+ c (- sf a)))))
|
||||||
segs))
|
segs))
|
||||||
|
|
@ -48,7 +76,10 @@
|
||||||
(defn pieces
|
(defn pieces
|
||||||
"Where source range [sa sb) lands in local coords: a list of [lo hi)."
|
"Where source range [sa sb) lands in local coords: a list of [lo hi)."
|
||||||
[segs sa sb]
|
[segs sa sb]
|
||||||
|
(assert-range "source range" [sa sb])
|
||||||
(vec (keep (fn [{:keys [src local]}]
|
(vec (keep (fn [{:keys [src local]}]
|
||||||
|
(assert-range "segment source" src)
|
||||||
|
(assert-range "segment local" local)
|
||||||
(let [[a b] src [c _] local
|
(let [[a b] src [c _] local
|
||||||
lo (max sa a) hi (min sb b)]
|
lo (max sa a) hi (min sb b)]
|
||||||
(when (< lo hi) [(+ c (- lo a)) (+ c (- hi a))])))
|
(when (< lo hi) [(+ c (- lo a)) (+ c (- hi a))])))
|
||||||
|
|
@ -58,13 +89,11 @@
|
||||||
"Coalesce [lo hi) ranges that meet at a boundary into single bars, so a
|
"Coalesce [lo hi) ranges that meet at a boundary into single bars, so a
|
||||||
continuous run spanning several clips reads as one piece. Ranges are half-open,
|
continuous run spanning several clips reads as one piece. Ranges are half-open,
|
||||||
so adjacent clips share a boundary (A.hi == B.lo) and merge exactly — no gap to
|
so adjacent clips share a boundary (A.hi == B.lo) and merge exactly — no gap to
|
||||||
fudge. Endpoints snap to whole frames first (a bar covers the frames it
|
fudge. Called per-mark, so it never fuses two distinct marks."
|
||||||
touches); only a real (≥1 frame) gap splits. Called per-mark, so it never fuses
|
|
||||||
two distinct marks."
|
|
||||||
[bars]
|
[bars]
|
||||||
(reduce (fn [acc [lo hi]]
|
(reduce (fn [acc [lo hi]]
|
||||||
(let [lo (js/Math.floor lo) hi (js/Math.ceil hi)
|
(assert-range "bar" [lo hi])
|
||||||
[plo phi] (peek acc)]
|
(let [[plo phi] (peek acc)]
|
||||||
(if (and plo (<= lo phi))
|
(if (and plo (<= lo phi))
|
||||||
(conj (pop acc) [plo (max phi hi)])
|
(conj (pop acc) [plo (max phi hi)])
|
||||||
(conj acc [lo hi]))))
|
(conj acc [lo hi]))))
|
||||||
|
|
@ -74,8 +103,11 @@
|
||||||
(defn slice
|
(defn slice
|
||||||
"Sub-segments of `segs` covering local range [la lb), src + local re-cut."
|
"Sub-segments of `segs` covering local range [la lb), src + local re-cut."
|
||||||
[segs la lb]
|
[segs la lb]
|
||||||
|
(assert-range "slice" [la lb])
|
||||||
(vec
|
(vec
|
||||||
(keep (fn [{:keys [src local] :as seg}]
|
(keep (fn [{:keys [src local] :as seg}]
|
||||||
|
(assert-range "segment source" src)
|
||||||
|
(assert-range "segment local" local)
|
||||||
(let [[a _] src [c d] local
|
(let [[a _] src [c d] local
|
||||||
lo (max la c) hi (min lb d)]
|
lo (max la c) hi (min lb d)]
|
||||||
(when (< lo hi)
|
(when (< lo hi)
|
||||||
|
|
@ -323,9 +355,9 @@
|
||||||
|
|
||||||
(defn mark-extent
|
(defn mark-extent
|
||||||
"EXACT context-local [lo hi] of mark `mark-id` of annotation `gid` — the true
|
"EXACT context-local [lo hi] of mark `mark-id` of annotation `gid` — the true
|
||||||
min piece-start / max piece-end, WITHOUT merge-bars' floor/ceil. Endpoint
|
min piece-start / max piece-end, without merge-bars. Endpoint editing must use
|
||||||
editing must use this (not a rounded lane bar) or the fixed end drifts a frame
|
this exact lane extent or the fixed end drifts a frame per edit. `ctx-segs` =
|
||||||
per re-roll. `ctx-segs` = content-segments of ctx."
|
content-segments of ctx."
|
||||||
[scene gid mark-id ctx-segs]
|
[scene gid mark-id ctx-segs]
|
||||||
(let [pcs (->> (resolve scene gid)
|
(let [pcs (->> (resolve scene gid)
|
||||||
(filter #(= mark-id (:mark %)))
|
(filter #(= mark-id (:mark %)))
|
||||||
|
|
@ -402,10 +434,10 @@
|
||||||
(defn selection->marks
|
(defn selection->marks
|
||||||
"Split local range [la lb) of context `ctx` into a run of single-clip ref
|
"Split local range [la lb) of context `ctx` into a run of single-clip ref
|
||||||
marks, one per content segment it crosses (the 'no cross-clip marks' rule).
|
marks, one per content segment it crosses (the 'no cross-clip marks' rule).
|
||||||
Each references the segment's source id with the right offsets. Offsets are
|
Each references the segment's source id with the right offsets. Inputs and
|
||||||
ROUNDED to whole frames here — this is where user frame-accuracy is applied
|
derived offsets must already be integer frames."
|
||||||
(clips stay exact for tiling; the mark snaps to a whole frame of its clip)."
|
|
||||||
[scene ctx la lb]
|
[scene ctx la lb]
|
||||||
|
(assert-range "selection" [la lb])
|
||||||
(mapv (fn [{:keys [mark src]}]
|
(mapv (fn [{:keys [mark src]}]
|
||||||
;; :at is the target's OWN local frame (src->at), matching resolve-mark's
|
;; :at is the target's OWN local frame (src->at), matching resolve-mark's
|
||||||
;; slice — correct even when the target is a scattered multi-clip proxy.
|
;; slice — correct even when the target is a scattered multi-clip proxy.
|
||||||
|
|
@ -413,8 +445,8 @@
|
||||||
(let [[a b] src
|
(let [[a b] src
|
||||||
a' (src->at scene mark a)]
|
a' (src->at scene mark a)]
|
||||||
{:id (str (random-uuid)) ; string so it survives JSON
|
{:id (str (random-uuid)) ; string so it survives JSON
|
||||||
:start {:ref mark :at (js/Math.round a')}
|
:start {:ref mark :at (assert-frame "selection start offset" a')}
|
||||||
:end {:ref mark :at (js/Math.round (+ a' (- b a)))}}))
|
:end {:ref mark :at (assert-frame "selection end offset" (+ a' (- b a)))}}))
|
||||||
(slice (content-segments scene ctx) la lb)))
|
(slice (content-segments scene ctx) la lb)))
|
||||||
|
|
||||||
(defn reconcile-run
|
(defn reconcile-run
|
||||||
|
|
@ -542,7 +574,7 @@
|
||||||
shared basis for both the annotation editor rows and the jump popover, so they
|
shared basis for both the annotation editor rows and the jump popover, so they
|
||||||
can never disagree on how a point reads."
|
can never disagree on how a point reads."
|
||||||
[scene {:keys [ref at]}]
|
[scene {:keys [ref at]}]
|
||||||
{:seg ref :f (js/Math.round (at->local scene ref at))})
|
{:seg ref :f (assert-frame "display point" (at->local scene ref at))})
|
||||||
|
|
||||||
(defn- clip-row
|
(defn- clip-row
|
||||||
"Editor row {:s … :e …} for a single-clip/subclip ref mark, in mark time."
|
"Editor row {:s … :e …} for a single-clip/subclip ref mark, in mark time."
|
||||||
|
|
@ -580,22 +612,23 @@
|
||||||
clip mark-group per clip (source range + track), and the root timeline. The
|
clip mark-group per clip (source range + track), and the root timeline. The
|
||||||
otio is only a seed — nothing here reads it again.
|
otio is only a seed — nothing here reads it again.
|
||||||
|
|
||||||
Clip ranges are kept EXACT (OTIO's fractional RationalTime), NOT rounded:
|
Clip ranges are integer frame ranges. Fractional OTIO input is rejected before
|
||||||
adjacent clips must share their half-open boundary exactly to tile without a
|
this point; from here on, frame math asserts instead of snapping."
|
||||||
gap, and independently rounding :start vs length breaks that. Frame-accuracy is
|
|
||||||
applied where it belongs — at the mark the user creates (selection->marks
|
|
||||||
rounds :at) — and at display, never by rounding clips or bars."
|
|
||||||
[{:keys [duration tracks]}]
|
[{:keys [duration tracks]}]
|
||||||
|
(assert-frame "OTIO duration" duration)
|
||||||
(let [vtracks (filter #(= :video (:kind %)) tracks)
|
(let [vtracks (filter #(= :video (:kind %)) tracks)
|
||||||
track-map (into {} (map (fn [t] [(keyword (str "t" (:index t))) {:name (:name t)}])) vtracks)
|
track-map (into {} (map (fn [t] [(keyword (str "t" (:index t))) {:name (:name t)}])) vtracks)
|
||||||
clips (into {} (for [t vtracks c (:clips t)]
|
clips (into {} (for [t vtracks c (:clips t)]
|
||||||
[(keyword (:id c))
|
(let [start (assert-frame "clip timeline start" (:start c))
|
||||||
{:type :clip :parent nil :name (:name c)
|
media-in (assert-frame "clip media-in" (:media-in c))
|
||||||
:start (:start c) ; timeline position (frames)
|
duration (assert-frame "clip duration" (:duration c))]
|
||||||
:marks [{:id (keyword (str (:id c) "-m"))
|
[(keyword (:id c))
|
||||||
:start (:media-in c)
|
{:type :clip :parent nil :name (:name c)
|
||||||
:end (+ (:media-in c) (:duration c))
|
:start start ; timeline position (frames)
|
||||||
:track (keyword (str "t" (:index t)))}]}]))]
|
:marks [{:id (keyword (str (:id c) "-m"))
|
||||||
|
:start media-in
|
||||||
|
:end (+ media-in duration)
|
||||||
|
:track (keyword (str "t" (:index t)))}]}])))]
|
||||||
{:tracks track-map
|
{:tracks track-map
|
||||||
:groups (assoc clips :root {:type :timeline :parent nil
|
:groups (assoc clips :root {:type :timeline :parent nil
|
||||||
:marks [{:id :root-m :start 0 :end duration}]})}))
|
:marks [{:id :root-m :start 0 :end duration}]})}))
|
||||||
|
|
@ -684,7 +717,7 @@
|
||||||
(last csegs))]
|
(last csegs))]
|
||||||
{:local lo
|
{:local lo
|
||||||
:seg (:mark seg)
|
:seg (:mark seg)
|
||||||
:f (js/Math.round (- lo (first (:local seg [0 0]))))}))
|
:f (assert-frame "jump target frame" (- lo (first (:local seg [0 0]))))}))
|
||||||
(runs scene ctx gid))))
|
(runs scene ctx gid))))
|
||||||
|
|
||||||
(defn linkables
|
(defn linkables
|
||||||
|
|
|
||||||
|
|
@ -183,7 +183,7 @@
|
||||||
:hidden (boolean hidden)
|
:hidden (boolean hidden)
|
||||||
:tags (vec (get-in g [:meta :tags]))
|
:tags (vec (get-in g [:meta :tags]))
|
||||||
;; jump targets labelled from the marks' clip refs (same as
|
;; jump targets labelled from the marks' clip refs (same as
|
||||||
;; the editor) — not re-derived from a floored bar frame
|
;; the editor) — not re-derived from a display bar frame
|
||||||
:jumps jumps
|
:jumps jumps
|
||||||
:start (or (ffirst bars) 0) :bars bars}))))))
|
:start (or (ffirst bars) 0) :bars bars}))))))
|
||||||
(sort-by (juxt :broken :start)) ; broken annotations sink to the bottom
|
(sort-by (juxt :broken :start)) ; broken annotations sink to the bottom
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,14 @@
|
||||||
|
|
||||||
(defn- px [local fps zoom] (* (/ local fps) zoom))
|
(defn- px [local fps zoom] (* (/ local fps) zoom))
|
||||||
|
|
||||||
|
(defn- frame-index
|
||||||
|
"Convert a continuous UI/video coordinate to the integer frame index it is over.
|
||||||
|
After this edge conversion, frame values are asserted, not snapped."
|
||||||
|
[label n]
|
||||||
|
(when-not (and (number? n) (not (js/isNaN n)))
|
||||||
|
(throw (js/Error. (str label " must be numeric, got " (pr-str n)))))
|
||||||
|
(scene/assert-frame label (int n)))
|
||||||
|
|
||||||
(defn- clip-thumbnail-layer [thumbs fps zoom row-h {:keys [mark thumb thumb-start local]}]
|
(defn- clip-thumbnail-layer [thumbs fps zoom row-h {:keys [mark thumb thumb-start local]}]
|
||||||
(when-let [tiles (seq (get-in thumbs [:clips (or thumb mark)]))]
|
(when-let [tiles (seq (get-in thumbs [:clips (or thumb mark)]))]
|
||||||
(let [[c d] local
|
(let [[c d] local
|
||||||
|
|
@ -125,7 +133,7 @@
|
||||||
(defn- play-tick []
|
(defn- play-tick []
|
||||||
(when-let [{:keys [ctx segs fps idx]} @play]
|
(when-let [{:keys [ctx segs fps idx]} @play]
|
||||||
(let [v @video-el
|
(let [v @video-el
|
||||||
sf (* (.-currentTime v) fps)
|
sf (frame-index "video source frame" (* (.-currentTime v) fps))
|
||||||
{:keys [pending-ns]} @play
|
{:keys [pending-ns]} @play
|
||||||
{[ss se] :src [ls _] :local} (nth segs idx)]
|
{[ss se] :src [ls _] :local} (nth segs idx)]
|
||||||
(cond
|
(cond
|
||||||
|
|
@ -206,7 +214,8 @@
|
||||||
clip under the landing frame into view vertically — jumps always do both."
|
clip under the landing frame into view vertically — jumps always do both."
|
||||||
([local] (goto! local false))
|
([local] (goto! local false))
|
||||||
([local follow?]
|
([local follow?]
|
||||||
(let [ctx @(rf/subscribe [::subs/context])
|
(let [local (scene/assert-frame "goto frame" local)
|
||||||
|
ctx @(rf/subscribe [::subs/context])
|
||||||
segs @(rf/subscribe [::subs/segments])
|
segs @(rf/subscribe [::subs/segments])
|
||||||
fps @(rf/subscribe [::subs/fps])
|
fps @(rf/subscribe [::subs/fps])
|
||||||
zoom @(rf/subscribe [::subs/zoom])]
|
zoom @(rf/subscribe [::subs/zoom])]
|
||||||
|
|
@ -399,10 +408,10 @@
|
||||||
linking @(rf/subscribe [::subs/linking])
|
linking @(rf/subscribe [::subs/linking])
|
||||||
auth? (some? @(rf/subscribe [::subs/draft-group]))
|
auth? (some? @(rf/subscribe [::subs/draft-group]))
|
||||||
seg (some (fn [{[c d] :local :as s}] (when (and (<= c ph) (< ph d)) s)) segs)
|
seg (some (fn [{[c d] :local :as s}] (when (and (<= c ph) (< ph d)) s)) segs)
|
||||||
cf (when seg (js/Math.round (- ph (first (:local seg)))))
|
cf (when seg (scene/assert-frame "clip frame" (- ph (first (:local seg)))))
|
||||||
click? (and auth? seg)]
|
click? (and auth? seg)]
|
||||||
[:div.frame-readout
|
[:div.frame-readout
|
||||||
[:span.fr-abs (str (js/Math.round ph) "f")]
|
[:span.fr-abs (str (scene/assert-frame "playhead" ph) "f")]
|
||||||
(when seg
|
(when seg
|
||||||
[:span.fr-clip {:class (when click? "clickable")
|
[:span.fr-clip {:class (when click? "clickable")
|
||||||
:title (when click? (if linking "Click to link this frame"
|
:title (when click? (if linking "Click to link this frame"
|
||||||
|
|
@ -449,7 +458,7 @@
|
||||||
(.preventDefault ev)
|
(.preventDefault ev)
|
||||||
(let [to (fn [clientX]
|
(let [to (fn [clientX]
|
||||||
(let [x (- clientX (.-left (.getBoundingClientRect content)))]
|
(let [x (- clientX (.-left (.getBoundingClientRect content)))]
|
||||||
(goto! (max 0 (* (/ x zoom) fps)))))
|
(goto! (frame-index "scrub frame" (max 0 (* (/ x zoom) fps))))))
|
||||||
move (fn [e] (to (.-clientX e)))
|
move (fn [e] (to (.-clientX e)))
|
||||||
up (fn up [_] (.removeEventListener js/document "mousemove" move)
|
up (fn up [_] (.removeEventListener js/document "mousemove" move)
|
||||||
(.removeEventListener js/document "mouseup" up))]
|
(.removeEventListener js/document "mouseup" up))]
|
||||||
|
|
@ -471,12 +480,11 @@
|
||||||
(let [d (d-of e)]
|
(let [d (d-of e)]
|
||||||
(when (or @moved? (> (js/Math.abs d) 3))
|
(when (or @moved? (> (js/Math.abs d) 3))
|
||||||
(reset! moved? true)
|
(reset! moved? true)
|
||||||
;; round ONLY the dragged endpoint; the fixed endpoint stays
|
|
||||||
;; exact so selection->marks re-derives its mark identically.
|
|
||||||
(let [[la lb] (case mode
|
(let [[la lb] (case mode
|
||||||
:move [(js/Math.round (+ lo d)) (js/Math.round (+ hi d))]
|
:move [(frame-index "drag start frame" (+ lo d))
|
||||||
:start [(js/Math.round (+ lo d)) hi]
|
(frame-index "drag end frame" (+ hi d))]
|
||||||
:end [lo (js/Math.round (+ hi d))])]
|
:start [(frame-index "drag start frame" (+ lo d)) hi]
|
||||||
|
:end [lo (frame-index "drag end frame" (+ hi d))])]
|
||||||
(rf/dispatch [::events/reroll-proxy ann mark-id la lb])))))
|
(rf/dispatch [::events/reroll-proxy ann mark-id la lb])))))
|
||||||
up (fn up [_]
|
up (fn up [_]
|
||||||
(.removeEventListener js/document "mousemove" move)
|
(.removeEventListener js/document "mousemove" move)
|
||||||
|
|
@ -500,7 +508,7 @@
|
||||||
(.preventDefault ev)
|
(.preventDefault ev)
|
||||||
(let [rect (.getBoundingClientRect content)
|
(let [rect (.getBoundingClientRect content)
|
||||||
sx (.-clientX ev)
|
sx (.-clientX ev)
|
||||||
to (fn [cx] (max 0 (* (/ (- cx (.-left rect)) zoom) fps)))
|
to (fn [cx] (frame-index "selection frame" (max 0 (* (/ (- cx (.-left rect)) zoom) fps))))
|
||||||
a (to sx)
|
a (to sx)
|
||||||
moved? (atom false)
|
moved? (atom false)
|
||||||
mv (fn [e]
|
mv (fn [e]
|
||||||
|
|
@ -513,7 +521,7 @@
|
||||||
(let [was @moved? b (to (.-clientX e)) lo (min a b) hi (max a b)]
|
(let [was @moved? b (to (.-clientX e)) lo (min a b) hi (max a b)]
|
||||||
(reset! region-sel nil)
|
(reset! region-sel nil)
|
||||||
(if (and was (> (- hi lo) 0.5))
|
(if (and was (> (- hi lo) 0.5))
|
||||||
(rf/dispatch [::events/draft-select-range (js/Math.round lo) (js/Math.round hi)])
|
(rf/dispatch [::events/draft-select-range lo hi])
|
||||||
(on-click a))))] ; click (no drag)
|
(on-click a))))] ; click (no drag)
|
||||||
(.addEventListener js/document "mousemove" mv)
|
(.addEventListener js/document "mousemove" mv)
|
||||||
(.addEventListener js/document "mouseup" up)))
|
(.addEventListener js/document "mouseup" up)))
|
||||||
|
|
@ -693,9 +701,8 @@
|
||||||
;; visual pieces the mark has, it gets one handle pair, at its ends.
|
;; visual pieces the mark has, it gets one handle pair, at its ends.
|
||||||
(for [a visible :when (:draft a)
|
(for [a visible :when (:draft a)
|
||||||
mid (distinct (map #(nth % 2) (:bars a)))
|
mid (distinct (map #(nth % 2) (:bars a)))
|
||||||
;; EXACT context-local extent (never the rounded bar). The drag
|
;; Exact context-local extent. The drag keeps the fixed endpoint
|
||||||
;; keeps the FIXED endpoint at this exact value so its mark
|
;; at this value so its mark re-derives identically.
|
||||||
;; re-derives identically — that's what stops the other end drifting.
|
|
||||||
:let [ext (scene/mark-extent scene (:id a) mid segs)]
|
:let [ext (scene/mark-extent scene (:id a) mid segs)]
|
||||||
:when ext
|
:when ext
|
||||||
:let [[lo hi] ext
|
:let [[lo hi] ext
|
||||||
|
|
@ -833,7 +840,7 @@
|
||||||
:title (when-not lf "linked clip no longer in this timeline")
|
:title (when-not lf "linked clip no longer in this timeline")
|
||||||
:on-click #(goto-link! lf)}
|
:on-click #(goto-link! lf)}
|
||||||
(when-not lf "△ ") label
|
(when-not lf "△ ") label
|
||||||
(when lf [:span.link-f (str " " (js/Math.round lf) "f")])])))))
|
(when lf [:span.link-f (str " " (scene/assert-frame "link frame" lf) "f")])])))))
|
||||||
|
|
||||||
(defn- build-chip [{:keys [label ref at kind]}]
|
(defn- build-chip [{:keys [label ref at kind]}]
|
||||||
(let [span (js/document.createElement "span")
|
(let [span (js/document.createElement "span")
|
||||||
|
|
@ -865,7 +872,7 @@
|
||||||
(when lf
|
(when lf
|
||||||
(let [f (js/document.createElement "span")]
|
(let [f (js/document.createElement "span")]
|
||||||
(set! (.-className f) "link-f")
|
(set! (.-className f) "link-f")
|
||||||
(set! (.-textContent f) (str " " (js/Math.round lf) "f"))
|
(set! (.-textContent f) (str " " (scene/assert-frame "link frame" lf) "f"))
|
||||||
(.appendChild span f)))
|
(.appendChild span f)))
|
||||||
span))
|
span))
|
||||||
|
|
||||||
|
|
@ -953,7 +960,7 @@
|
||||||
:point {:kind :script-note :ref id}})))))))))
|
:point {:kind :script-note :ref id}})))))))))
|
||||||
|
|
||||||
(defn- point-label [cand off local]
|
(defn- point-label [cand off local]
|
||||||
(str (:label cand) " @" (js/Math.round local) "f"
|
(str (:label cand) " @" (scene/assert-frame "point label frame" local) "f"
|
||||||
(when (and (not (:abs cand)) (pos? off))
|
(when (and (not (:abs cand)) (pos? off))
|
||||||
(str " +" off))))
|
(str " +" off))))
|
||||||
|
|
||||||
|
|
@ -1066,18 +1073,18 @@
|
||||||
[:span.cand-label (:label c)]
|
[:span.cand-label (:label c)]
|
||||||
(when (:group c) [:span.cand-group (str " " (:group c))])
|
(when (:group c) [:span.cand-group (str " " (:group c))])
|
||||||
(when-not (contains? #{:timeline :script-note} (:kind c))
|
(when-not (contains? #{:timeline :script-note} (:kind c))
|
||||||
[:span.link-f (str " " (js/Math.round (:local c)) "f")])])))])))
|
[:span.link-f (str " " (scene/assert-frame "candidate frame" (:local c)) "f")])])))])))
|
||||||
|
|
||||||
(defn- local->draft-point [segs local]
|
(defn- local->draft-point [segs local]
|
||||||
(some (fn [{m :mark [c d] :local}]
|
(some (fn [{m :mark [c d] :local}]
|
||||||
(when (and (<= c local) (< local d))
|
(when (and (<= c local) (< local d))
|
||||||
{:seg m :f (js/Math.round (- local c))}))
|
{:seg m :f (scene/assert-frame "draft point frame" (- local c))}))
|
||||||
segs))
|
segs))
|
||||||
|
|
||||||
(defn- local->draft-end-point [segs local]
|
(defn- local->draft-end-point [segs local]
|
||||||
(some (fn [{m :mark [c d] :local}]
|
(some (fn [{m :mark [c d] :local}]
|
||||||
(when (and (<= c local) (<= local d))
|
(when (and (<= c local) (<= local d))
|
||||||
{:seg m :f (js/Math.round (- local c))}))
|
{:seg m :f (scene/assert-frame "draft endpoint frame" (- local c))}))
|
||||||
segs))
|
segs))
|
||||||
|
|
||||||
;; --- shared string autocomplete ------------------------------------------
|
;; --- shared string autocomplete ------------------------------------------
|
||||||
|
|
@ -1369,7 +1376,7 @@
|
||||||
(let [len (pt-len scene segs seg)]
|
(let [len (pt-len scene segs seg)]
|
||||||
[:div.pt-chip.pending
|
[:div.pt-chip.pending
|
||||||
[:span.pt-chip-name (pt-name scene segs seg)]
|
[:span.pt-chip-name (pt-name scene segs seg)]
|
||||||
[:input.pt-frame {:type "number" :value (js/Math.round f) :disabled true}]
|
[:input.pt-frame {:type "number" :value (scene/assert-frame "pending frame" f) :disabled true}]
|
||||||
[:span.pt-dur (str "/" len)]]))
|
[:span.pt-dur (str "/" len)]]))
|
||||||
|
|
||||||
(defn- empty-frame-chip [label]
|
(defn- empty-frame-chip [label]
|
||||||
|
|
@ -1741,7 +1748,7 @@
|
||||||
len @(rf/subscribe [::subs/length])
|
len @(rf/subscribe [::subs/length])
|
||||||
authed? @(rf/subscribe [::subs/authed?])
|
authed? @(rf/subscribe [::subs/authed?])
|
||||||
authoring? (some? @(rf/subscribe [::subs/draft-group]))
|
authoring? (some? @(rf/subscribe [::subs/draft-group]))
|
||||||
ph-now #(js/Math.round @(rf/subscribe [::subs/playhead]))]
|
ph-now #(scene/assert-frame "playhead" @(rf/subscribe [::subs/playhead]))]
|
||||||
[:div.toolbar
|
[:div.toolbar
|
||||||
[:div.transport
|
[:div.transport
|
||||||
[:button.step-btn {:title "Previous frame" :disabled start?
|
[:button.step-btn {:title "Previous frame" :disabled start?
|
||||||
|
|
@ -2083,7 +2090,7 @@
|
||||||
(when (and url (seq @pages))
|
(when (and url (seq @pages))
|
||||||
[:div.script-controls
|
[:div.script-controls
|
||||||
[:button {:title "Zoom out" :on-click #(bump (fn [z] (* z 0.9)))} "−"]
|
[:button {:title "Zoom out" :on-click #(bump (fn [z] (* z 0.9)))} "−"]
|
||||||
[:span.zoom-read (str (js/Math.round (* scale 100)) "%")]
|
[:span.zoom-read (str (int (* scale 100)) "%")]
|
||||||
[:button {:title "Zoom in" :on-click #(bump (fn [z] (* z 1.1)))} "+"]
|
[:button {:title "Zoom in" :on-click #(bump (fn [z] (* z 1.1)))} "+"]
|
||||||
[:button {:title "Fit width" :on-click fit!} "Fit"]])]))))
|
[:button {:title "Fit width" :on-click fit!} "Fit"]])]))))
|
||||||
|
|
||||||
|
|
@ -2271,7 +2278,7 @@
|
||||||
(when uploading?
|
(when uploading?
|
||||||
[:div.upload-progress
|
[:div.upload-progress
|
||||||
[:div.upload-track [:div.upload-fill {:style {:width (str (* 100 prog) "%")}}]]
|
[:div.upload-track [:div.upload-fill {:style {:width (str (* 100 prog) "%")}}]]
|
||||||
[:span.upload-pct (str (js/Math.round (* 100 prog)) "%")]])
|
[:span.upload-pct (str (int (* 100 prog)) "%")]])
|
||||||
[:div.form-actions
|
[:div.form-actions
|
||||||
[:button.save {:disabled (or uploading? (str/blank? @pname) (not @otio) (not @clip))
|
[:button.save {:disabled (or uploading? (str/blank? @pname) (not @otio) (not @clip))
|
||||||
:on-click #(rf/dispatch [::events/create-project @pname @otio @clip])}
|
:on-click #(rf/dispatch [::events/create-project @pname @otio @clip])}
|
||||||
|
|
|
||||||
|
|
@ -127,3 +127,35 @@
|
||||||
(let [bars (bars-in :annB :annC)]
|
(let [bars (bars-in :annB :annC)]
|
||||||
(is (some #(= bid (nth % 2)) bars) "after the drag the B-mark still resolves in annB (no vanish)")
|
(is (some #(= bid (nth % 2)) bars) "after the drag the B-mark still resolves in annB (no vanish)")
|
||||||
(is (= [50 140] (some (fn [[lo hi m]] (when (= m bid) [lo hi])) bars)) "handle moved end to 140"))))))
|
(is (= [50 140] (some (fn [[lo hi m]] (when (= m bid) [lo hi])) bars)) "handle moved end to 140"))))))
|
||||||
|
|
||||||
|
(deftest draft-range-events-keep-integer-frames
|
||||||
|
(testing "basic drag selection through the real event path stores only integer frames"
|
||||||
|
(setup! clips-scene [:root])
|
||||||
|
(rf/dispatch-sync [::ev/open-draft])
|
||||||
|
(rf/dispatch-sync [::ev/draft-select-range 25 175])
|
||||||
|
(let [[gid g] (some (fn [[gid g]] (when (:draft g) [gid g])) (:groups (scene*)))
|
||||||
|
mark (first (:marks g))
|
||||||
|
pid (get-in mark [:start :ref])
|
||||||
|
proxy (get-in (scene*) [:groups pid])
|
||||||
|
ats (mapcat (fn [m] [(get-in m [:start :at]) (get-in m [:end :at])]) (:marks proxy))]
|
||||||
|
(is gid "draft exists")
|
||||||
|
(is (= :proxy (:type proxy)))
|
||||||
|
(is (every? integer? ats))
|
||||||
|
(is (= [[25 175 (:id mark)]] (bars-in :root gid)))))
|
||||||
|
(testing "fractional drag selection fails instead of being changed into nearby frames"
|
||||||
|
(setup! clips-scene [:root])
|
||||||
|
(rf/dispatch-sync [::ev/open-draft])
|
||||||
|
(is (thrown-with-msg? js/Error #"integer frame"
|
||||||
|
(rf/dispatch-sync [::ev/draft-select-range 25.5 175])))))
|
||||||
|
|
||||||
|
(deftest reroll-proxy-rejects-fractional-frames
|
||||||
|
(testing "live proxy reroll accepts integer handle positions and rejects fractional ones"
|
||||||
|
(setup! clips-scene [:root])
|
||||||
|
(rf/dispatch-sync [::ev/open-draft])
|
||||||
|
(rf/dispatch-sync [::ev/draft-select-range 25 175])
|
||||||
|
(let [[gid g] (some (fn [[gid g]] (when (:draft g) [gid g])) (:groups (scene*)))
|
||||||
|
mark-id (:id (first (:marks g)))]
|
||||||
|
(rf/dispatch-sync [::ev/reroll-proxy gid mark-id 30 170])
|
||||||
|
(is (= [[30 170 mark-id]] (bars-in :root gid)))
|
||||||
|
(is (thrown-with-msg? js/Error #"integer frame"
|
||||||
|
(rf/dispatch-sync [::ev/reroll-proxy gid mark-id 30.25 170]))))))
|
||||||
|
|
|
||||||
27
tl/test/tl/frame_policy_test.cljs
Normal file
27
tl/test/tl/frame_policy_test.cljs
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
(ns tl.frame-policy-test
|
||||||
|
(:require [cljs.test :refer-macros [deftest is testing]]
|
||||||
|
[clojure.string :as str]))
|
||||||
|
|
||||||
|
(def fs (js/require "fs"))
|
||||||
|
(def path (js/require "path"))
|
||||||
|
|
||||||
|
(defn- cljs-files [dir]
|
||||||
|
(mapcat (fn [name]
|
||||||
|
(let [p (.join path dir name)
|
||||||
|
st (.statSync fs p)]
|
||||||
|
(cond
|
||||||
|
(.isDirectory st) (cljs-files p)
|
||||||
|
(str/ends-with? name ".cljs") [p]
|
||||||
|
:else [])))
|
||||||
|
(array-seq (.readdirSync fs dir))))
|
||||||
|
|
||||||
|
(deftest no-math-round-in-frame-code
|
||||||
|
(testing "frame code must not use the JS rounding API"
|
||||||
|
(let [needle (str "Math" "." "round")
|
||||||
|
hits (->> (concat (cljs-files "src") (cljs-files "test"))
|
||||||
|
(remove #(str/ends-with? % "frame_policy_test.cljs"))
|
||||||
|
(keep (fn [p]
|
||||||
|
(when (str/includes? (.readFileSync fs p "utf8") needle)
|
||||||
|
p)))
|
||||||
|
vec)]
|
||||||
|
(is (= [] hits)))))
|
||||||
15
tl/test/tl/routes_test.cljs
Normal file
15
tl/test/tl/routes_test.cljs
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
(ns tl.routes-test
|
||||||
|
(:require [cljs.test :refer-macros [deftest is testing]]
|
||||||
|
[tl.routes :as routes]))
|
||||||
|
|
||||||
|
(deftest view-state-parses-integer-frame-query
|
||||||
|
(testing "route playhead accepts string and numeric integer query params"
|
||||||
|
(set! (.. js/window -location -hash) "")
|
||||||
|
(is (= {:playhead 42}
|
||||||
|
(routes/view-state {:query-params {"f" "42"}})))
|
||||||
|
(is (= {:playhead 42}
|
||||||
|
(routes/view-state {:query-params {"f" 42}}))))
|
||||||
|
(testing "fractional route playhead is ignored instead of changed"
|
||||||
|
(set! (.. js/window -location -hash) "")
|
||||||
|
(is (= {}
|
||||||
|
(routes/view-state {:query-params {"f" "42.5"}})))))
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
(ns tl.scene-test
|
(ns tl.scene-test
|
||||||
(:require [cljs.test :refer-macros [deftest is testing]]
|
(:require [cljs.test :refer-macros [deftest is testing]]
|
||||||
[tl.md :as md]
|
[tl.md :as md]
|
||||||
|
[tl.otio :as otio]
|
||||||
[tl.scene :as s]))
|
[tl.scene :as s]))
|
||||||
|
|
||||||
;; --- shared fixture -------------------------------------------------------
|
;; --- shared fixture -------------------------------------------------------
|
||||||
|
|
@ -16,6 +17,8 @@
|
||||||
|
|
||||||
(defn with-group [scene gid g] (assoc-in scene [:groups gid] g))
|
(defn with-group [scene gid g] (assoc-in scene [:groups gid] g))
|
||||||
(defn refm [id clip a b] {:id id :start {:ref clip :at a} :end {:ref clip :at b}})
|
(defn refm [id clip a b] {:id id :start {:ref clip :at a} :end {:ref clip :at b}})
|
||||||
|
(defn err-msg [f]
|
||||||
|
(try (f) nil (catch js/Error e (.-message e))))
|
||||||
|
|
||||||
;; X = inter-x: B[50,100), A[0,50), B[0,50), A[50,100) — four 50-frame subclips.
|
;; X = inter-x: B[50,100), A[0,50), B[0,50), A[50,100) — four 50-frame subclips.
|
||||||
(def inter-x
|
(def inter-x
|
||||||
|
|
@ -408,26 +411,55 @@
|
||||||
(is (= #{:t0 :t1} (s/tracks segs)))
|
(is (= #{:t0 :t1} (s/tracks segs)))
|
||||||
(is (= 200 (s/length segs)))))))
|
(is (= 200 (s/length segs)))))))
|
||||||
|
|
||||||
(deftest from-otio-keeps-clip-ranges-exact
|
(deftest otio-normalizes-source-starts-and-rejects-fractional-durations
|
||||||
(testing "clips keep OTIO's exact fractional ranges (so adjacent clips tile);
|
(testing "fractional OTIO source starts are normalized at import"
|
||||||
frame-accuracy is applied at mark creation, not here"
|
(let [clip {:OTIO_SCHEMA "Clip.2"
|
||||||
(let [parsed {:fps 24 :duration 199.6
|
:name "a"
|
||||||
:tracks [{:index 0 :kind :video :name "W"
|
:source_range {:start_time {:value 10.5 :rate 24}
|
||||||
:clips [{:id "t0-c0" :name "a" :start 0.2 :media-in 188.87 :duration 100.4}]}]}
|
:duration {:value 5 :rate 24}}}
|
||||||
mark (first (get-in (s/from-otio parsed) [:groups :t0-c0 :marks]))]
|
otio {:global_start_time {:rate 24}
|
||||||
(is (= 188.87 (:start mark))) ; exact, not rounded
|
:tracks {:children [{:name "V" :kind "Video" :children [clip]}]}}
|
||||||
(is (= 289.27 (:end mark))))))
|
parsed (otio/parse otio)]
|
||||||
|
(is (= 10 (:media-offset parsed)))
|
||||||
|
(is (= 0 (get-in parsed [:tracks 0 :clips 0 :media-in])))
|
||||||
|
(is (integer? (get-in parsed [:tracks 0 :clips 0 :media-in])))))
|
||||||
|
(testing "fractional OTIO durations still fail because timeline math is integer"
|
||||||
|
(let [clip {:OTIO_SCHEMA "Clip.2"
|
||||||
|
:name "a"
|
||||||
|
:source_range {:start_time {:value 10 :rate 24}
|
||||||
|
:duration {:value 5.5 :rate 24}}}
|
||||||
|
otio {:global_start_time {:rate 24}
|
||||||
|
:tracks {:children [{:name "V" :kind "Video" :children [clip]}]}}
|
||||||
|
msg (err-msg #(otio/parse otio))]
|
||||||
|
(is (re-find #"integer frame" msg))))
|
||||||
|
(testing "from-otio also asserts parsed frame fields are integers"
|
||||||
|
(let [clip {:id "t0-c0" :name "a" :start 0.2 :media-in 188.87 :duration 100.4}
|
||||||
|
parsed {:fps 24 :duration 199.6
|
||||||
|
:tracks [{:index 0 :kind :video :name "W" :clips [clip]}]}
|
||||||
|
msg (err-msg #(s/from-otio parsed))]
|
||||||
|
(is (re-find #"integer frame" msg)))))
|
||||||
|
|
||||||
(deftest selection-rounds-at-to-whole-frames
|
(deftest real-otio-source-starts-import-as-integer-media-frames
|
||||||
(testing "a selection over a clip with fractional layout still yields whole-frame
|
(testing "the bundled Challengers OTIO has fractional source starts but imports into integer model frames"
|
||||||
:at offsets (clip stays exact, the mark snaps)"
|
(let [raw (js->clj (js/JSON.parse (.readFileSync (js/require "fs") "resources/public/one_two_three.otio" "utf8"))
|
||||||
|
:keywordize-keys true)
|
||||||
|
parsed (otio/parse raw)
|
||||||
|
scene (s/from-otio parsed)
|
||||||
|
media-ins (for [t (:tracks parsed) c (:clips t)] (:media-in c))]
|
||||||
|
(is (seq media-ins))
|
||||||
|
(is (every? integer? media-ins))
|
||||||
|
(is (every? integer? (mapcat (fn [[_ g]]
|
||||||
|
(mapcat (juxt :start :end) (:marks g)))
|
||||||
|
(:groups scene)))))))
|
||||||
|
|
||||||
|
(deftest selection-rejects-fractional-frames
|
||||||
|
(testing "a selection over fractional clip layout fails instead of changing frames"
|
||||||
(let [scene {:tracks {:t0 {:name "W"}}
|
(let [scene {:tracks {:t0 {:name "W"}}
|
||||||
:groups {:root {:type :timeline :parent nil :marks [{:id :m/r :start 0 :end 100}]}
|
:groups {:root {:type :timeline :parent nil :marks [{:id :m/r :start 0 :end 100}]}
|
||||||
:fr {:type :clip :parent nil :start 0.3 ; fractional timeline pos
|
:fr {:type :clip :parent nil :start 0.3 ; fractional timeline pos
|
||||||
:marks [{:id :m/fr :start 10.4 :end 110.4 :track :t0}]}}}
|
:marks [{:id :m/fr :start 10.4 :end 110.4 :track :t0}]}}}
|
||||||
run (s/selection->marks scene :root 20 60)]
|
msg (err-msg #(s/selection->marks scene :root 20 60))]
|
||||||
(is (seq run))
|
(is (re-find #"integer frame" msg)))))
|
||||||
(is (every? integer? (mapcat (juxt #(get-in % [:start :at]) #(get-in % [:end :at])) run))))))
|
|
||||||
|
|
||||||
;; =========================================================================
|
;; =========================================================================
|
||||||
;; Suite 4 — draft rows <-> marks (the two-input editor)
|
;; Suite 4 — draft rows <-> marks (the two-input editor)
|
||||||
|
|
@ -460,13 +492,13 @@
|
||||||
(is (= 0 (get-in (first marks) [:start :at])))
|
(is (= 0 (get-in (first marks) [:start :at])))
|
||||||
(is (= 30 (get-in (first marks) [:end :at])))))))
|
(is (= 30 (get-in (first marks) [:end :at])))))))
|
||||||
|
|
||||||
(deftest merge-bars-coalesces-continuous-run
|
(deftest merge-bars-coalesces-continuous-integer-runs
|
||||||
(testing "sub-frame OTIO gaps collapse to one bar; a real gap stays split"
|
(testing "integer-adjacent bars merge; fractional bars fail"
|
||||||
;; foobar's real bars (continuous 5-clip selection, ~0.9-frame source gaps)
|
|
||||||
(is (= [[128 542]]
|
(is (= [[128 542]]
|
||||||
(s/merge-bars [[128.87 190.87] [191.80 265.80] [266.73 384.73]
|
(s/merge-bars [[128 191] [191 266] [266 386]
|
||||||
[385.61 413.61] [414.58 541.58]])))
|
[386 414] [414 542]])))
|
||||||
(is (= [[0 50] [200 260]] (s/merge-bars [[0 50] [200 260]]))) ; real gap → two
|
(is (= [[0 50] [200 260]] (s/merge-bars [[0 50] [200 260]]))) ; real gap → two
|
||||||
|
(is (re-find #"integer frame" (err-msg #(s/merge-bars [[0.2 50]]))))
|
||||||
(is (= [] (s/merge-bars [])))))
|
(is (= [] (s/merge-bars [])))))
|
||||||
|
|
||||||
(deftest restore-annotations-rekeywordizes-json
|
(deftest restore-annotations-rekeywordizes-json
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue