Make lanes explicit symbol views
This commit is contained in:
parent
fb38990090
commit
e459307a4a
22 changed files with 2183 additions and 2330 deletions
72
docs/lane-is-a-view-notes.md
Normal file
72
docs/lane-is-a-view-notes.md
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
# Implementation notes — "A lane is a view"
|
||||
|
||||
Running log for `docs/lane-is-a-view-plan.md`. `[ ]` not started, `[~]` in
|
||||
progress, `[x]` done with `npm test` green.
|
||||
|
||||
Baseline at `fb38990`: 475 tests, 9621 assertions, 0 failures.
|
||||
|
||||
## Order of work
|
||||
|
||||
The plan's seven steps, re-grouped — see *Deviation from the plan's order* below.
|
||||
|
||||
- [x] A. Domain: `symbol/children`, `symbol/lane?`, `symbol/overlaps`,
|
||||
`lane.cljs` → `span.cljs`, the overlap check in `span/finish`
|
||||
(plan steps 3, 4, and the domain half of 5)
|
||||
- [x] B. Events: re-base callers and remove lane-node-specific commands; keep
|
||||
explicit `::new-lane` and cross-lane adoption (plan step 5)
|
||||
- [x] C. UI: row per symbol, explicit lane creation, and drag handling
|
||||
(plan steps 1, 2, and the UI half of 5)
|
||||
- [x] D. Audio: delete `holds-other?`, the mixed-lane refusal, the `in-lane`
|
||||
filter in `sound-rows` (plan step 6)
|
||||
- [x] E. Tests: `domain/sequence_test`, `events/lane_test`, `browser/lane.mjs`
|
||||
- [x] F. Shift-to-reparent still works, untouched (plan step 7)
|
||||
|
||||
Not in this pass — see *Left for a second pass*: tearing out the held cel, the
|
||||
instance-playback control, drawing a loop's repeats, the audio period guard.
|
||||
|
||||
## Deviation from the plan's order
|
||||
|
||||
The plan's steps 1 and 2 are display work that keys off "a symbol's children",
|
||||
and step 3 is what MAKES the cels a symbol's children. Until then a cel's
|
||||
`:parent` is the lane node, so there is nothing for the display to read: step 2
|
||||
cannot draw "a symbol's children as blocks" while the children belong to a
|
||||
group. So the data model moves first (A) and the display follows (C). The
|
||||
content of each step is unchanged; only the order is.
|
||||
|
||||
The one thing this gives up is the plan's promise that every step leaves the
|
||||
editor usable — between A and C the timeline draws the new shape with the old
|
||||
code. `npm test` is green at each step either way.
|
||||
|
||||
## Decisions taken
|
||||
|
||||
1. **Lane mode is `:display :lane` on the SYMBOL** — plan's recommendation 2,
|
||||
and open question 1 answered "the symbol, not the instance". A symbol placed
|
||||
twice is drawn as a lane in both places. Added to `symbol/symbol-keys` and to
|
||||
`leaf/leaves`' `select-keys` so it saves like `:frames`.
|
||||
2. **A symbol's children are its parent-less nodes** that have a placed span.
|
||||
The plan's step 6 settles it: "an audio node is already a parent-less child
|
||||
of a symbol, which is exactly the new shape". Span-less nodes — a shape on
|
||||
screen for the whole shot — are not in the sequence and are skipped, which is
|
||||
also what stops the commands destructuring a nil span.
|
||||
3. **A symbol holds at most one sequence.** It follows from 1 and 2: the
|
||||
container is the symbol. Two lanes of picture is now two symbols placed in a
|
||||
third, which is what compositing already was.
|
||||
4. **The open symbol gets a row of its own in lane mode**, and only then. The
|
||||
blocks have to sit on a row and the open symbol had none; expanding it turns
|
||||
its children into ordinary rows. Not a row always, which would shift every
|
||||
row in the pane for no gain.
|
||||
5. **Open question 2** — a lane row's edge drag trims the PLACING INSTANCE's
|
||||
span, via `span/resize-out`, like the handle on every other row. Rippling
|
||||
the children is what the cel blocks' own edges already do, and giving one
|
||||
handle two meanings is what the plan refuses elsewhere.
|
||||
6. **Open question 3** — the lane work first, the held cel after. The plan says
|
||||
they are independent, and the held cel is joined to a loop control that does
|
||||
not exist yet; doing it second costs one more pass over `lane_test`'s
|
||||
fixtures and risks nothing.
|
||||
7. **Lane creation stays explicit.** A blank document and `new symbol` create
|
||||
ordinary symbols. The separate `new → lane` command creates and places a
|
||||
symbol with `:display :lane`, aims it for immediate drawing or dropping, and
|
||||
always adds it at the top of the open symbol rather than nesting it in the
|
||||
previously aimed lane.
|
||||
|
||||
## Notes
|
||||
|
|
@ -149,20 +149,13 @@
|
|||
which is long enough to key something into and short enough to scrub by hand."
|
||||
120)
|
||||
|
||||
(def lane-node
|
||||
"The lane every symbol is born with.
|
||||
|
||||
EVERY SYMBOL HAS AT LEAST ONE LANE, because a lane is the only place
|
||||
temporal content goes and a symbol with none has nowhere to drop a thing —
|
||||
which made the first drop into any symbol a special case that had to invent
|
||||
a lane before it could do what the second drop does. An empty lane is a true
|
||||
statement about a symbol nobody has put anything in yet. The id is a keyword
|
||||
rather than a uuid because this namespace is pure, and `:lane` reads in a
|
||||
path; commands that add FURTHER lanes bring their own uuids."
|
||||
{:id :lane :name "lane" :kind :group :layout :sequence :z "z-lane"})
|
||||
|
||||
(defn blank
|
||||
"A new, empty document: one symbol, holding one empty lane.
|
||||
"A new, empty document: one symbol, and nothing in it.
|
||||
|
||||
A SYMBOL IS BORN EMPTY. It used to be born holding a lane, because a lane was
|
||||
the only place temporal content could go; now the symbol itself is the
|
||||
container — see `symbol/children` — so there is nothing to invent and the
|
||||
first drop into a symbol is the same operation as the second.
|
||||
|
||||
The tracking maps are ABSENT rather than empty, because `leaf/leaves` writes no
|
||||
leaf for an empty one and so cannot bring it back: a blank document that opened
|
||||
|
|
@ -174,8 +167,7 @@
|
|||
{:name "untitled"
|
||||
:fps 30
|
||||
:width 320 :height 200
|
||||
:symbols {:main {:id :main :fps 30 :frames blank-frames
|
||||
:nodes {:lane lane-node}}}})
|
||||
:symbols {:main {:id :main :fps 30 :frames blank-frames :nodes {}}}})
|
||||
|
||||
(defn- transform-op
|
||||
"Put a symbol's already resolved mark into its instance's parent space. Its
|
||||
|
|
@ -421,7 +413,7 @@
|
|||
clip
|
||||
(-> clip
|
||||
(assoc-in [:symbols sid] {:id sid :name (name sid) :fps (fps clip host)
|
||||
:frames (- end frame) :nodes {:lane lane-node}})
|
||||
:frames (- end frame) :nodes {}})
|
||||
(place-symbol nil host sid frame uuid nil)))))
|
||||
|
||||
(defn free-id
|
||||
|
|
|
|||
|
|
@ -1,494 +0,0 @@
|
|||
(ns arthur.domain.lane
|
||||
"The commands that need a SEQUENCE: make a lane, place symbol clips in it,
|
||||
change how long they are exposed, empty part of it, and decide which held
|
||||
drawing clips share content.
|
||||
|
||||
WHAT A LANE IS lives in `arthur.domain.symbol`, beside the other rules about a
|
||||
node map: a group with `:layout :sequence`, whose children are non-overlapping
|
||||
visual symbol clips. This namespace only changes them.
|
||||
|
||||
WHAT IS NOT HERE: split, trim and move. Each of those is one write to one
|
||||
node's span or position, which is a fact every node has, so they live in
|
||||
`arthur.domain.span` and work on a symbol placed straight into a shot as
|
||||
readily as on a cel. What stays here is everything that cannot be said about
|
||||
one node alone — a ripple needs later siblings, a gap needs a row to be a hole
|
||||
in, and appending needs to know where the row stops.
|
||||
|
||||
EVERY COMMAND IS ONE STEP AND ALL OF IT. Each returns `{:clip :selection}` or
|
||||
`{:refused reason}` — never a half-applied edit, and never a document that
|
||||
`clip/problems` would reject. A command that cannot say what the person meant
|
||||
refuses and says why, rather than picking for them: the overflow policy is a
|
||||
caller's `:extent`, and decoupling shared content is its own command instead
|
||||
of something an ordinary edit does silently.
|
||||
|
||||
IDS FOR CELS COME FROM THE CALLER, because a cel's identity is
|
||||
a uuid and this namespace is pure. Ids for new CONTENT are derived from the
|
||||
drawing being copied — `clip/free-id` is pure too, and `drawing-a-2` says what
|
||||
it came from in a way `symbol-7` does not."
|
||||
(:require [arthur.domain.bring :as bring]
|
||||
[arthur.domain.clip :as clip]
|
||||
[arthur.domain.node :as node]
|
||||
[arthur.domain.span :as span]
|
||||
[arthur.domain.symbol :as symbol]))
|
||||
|
||||
(defn extend-hold
|
||||
"Change one held cel's duration by `delta` lane frames and ripple its
|
||||
later siblings. Lane channels, cel channels and source clocks stay put.
|
||||
Returns {:clip :selection} or {:refused :required-frames?}; never partially edits."
|
||||
[clip sid id delta {:keys [extent] :or {extent :keep}}]
|
||||
(let [nodes (get-in clip [:symbols sid :nodes])
|
||||
n (get nodes id)
|
||||
lane (get nodes (:parent n))
|
||||
rate (:rate (node/time-of n))
|
||||
span (:span n)
|
||||
m (when lane (symbol/frame-map nodes (:id lane)))
|
||||
;; The LANE's own shape, not the whole symbol's: refusing a cel
|
||||
;; edit over some unrelated defect elsewhere in the symbol would be
|
||||
;; this command answering for a part of the document it never touches.
|
||||
broken (first (symbol/lane-problems nodes))]
|
||||
(cond
|
||||
(not (node/lane? lane)) {:refused "select a cel in a lane"}
|
||||
broken {:refused broken}
|
||||
(not (and (integer? delta) (not (zero? delta)))) {:refused "hold change must be a nonzero whole number of lane frames"}
|
||||
(not (zero? (:speed (node/playback-of n)))) {:refused "hold length applies to a held drawing"}
|
||||
(nil? m) {:refused "cel timing through a stepped or looping lane is not supported"}
|
||||
(<= (+ (second span) (* rate delta)) (first span)) {:refused "a drawing must keep a positive cel"}
|
||||
:else
|
||||
(let [[_ boundary] (node/placed-span n)
|
||||
later (filter #(>= (first (node/placed-span %)) boundary)
|
||||
(symbol/lane-clips nodes (:id lane)))
|
||||
nodes (assoc-in nodes [id :span 1] (+ (second span) (* rate delta)))
|
||||
nodes (reduce (fn [ns sibling]
|
||||
(update-in ns [(:id sibling) :time :at] (fnil + 0) delta))
|
||||
nodes later)]
|
||||
(span/finish clip sid nodes id extent)))))
|
||||
|
||||
(defn resize-out
|
||||
"Put cel `id`'s right edge at lane frame `to`.
|
||||
|
||||
Without `ripple?`, growing the cel consumes the starts of the cels it reaches:
|
||||
wholly covered cels disappear and the last partially covered one is trimmed.
|
||||
Shrinking leaves a gap. With `ripple?`, every cel beginning at or after the
|
||||
old edge moves by the same delta, in either direction, so their contents are
|
||||
preserved. A lane never contains an overlap in either mode."
|
||||
[clip sid id to {:keys [extent ripple?] :or {extent :keep ripple? false}}]
|
||||
(let [nodes (get-in clip [:symbols sid :nodes])
|
||||
n (get nodes id)
|
||||
lane (get nodes (:parent n))
|
||||
[lo old-out] (when n (node/placed-span n))
|
||||
members (when (node/lane? lane) (symbol/lane-clips nodes (:id lane)))
|
||||
later (when members
|
||||
(remove #(= id (:id %))
|
||||
(filter #(>= (first (node/placed-span %)) old-out) members)))
|
||||
broken (first (symbol/lane-problems nodes))]
|
||||
(cond
|
||||
(not (node/lane? lane)) {:refused "select a cel in a lane"}
|
||||
broken {:refused broken}
|
||||
(not (integer? to)) {:refused "a cel edge goes to a whole lane frame"}
|
||||
(not (< lo to)) {:refused "a drawing must keep at least one frame"}
|
||||
(= to old-out) {:clip clip :selection id}
|
||||
:else
|
||||
(let [delta (- to old-out)
|
||||
resized (assoc nodes id (span/edged n :out to))
|
||||
changed
|
||||
(if ripple?
|
||||
(reduce (fn [ns sibling]
|
||||
(update-in ns [(:id sibling) :time :at] (fnil + 0) delta))
|
||||
resized later)
|
||||
(if (pos? delta)
|
||||
(reduce
|
||||
(fn [ns sibling]
|
||||
(let [[s e] (node/placed-span sibling)]
|
||||
(cond
|
||||
(>= s to) ns
|
||||
(<= e to) (dissoc ns (:id sibling))
|
||||
:else (assoc ns (:id sibling) (span/edged sibling :in to)))))
|
||||
resized later)
|
||||
resized))]
|
||||
(span/finish clip sid changed id extent)))))
|
||||
|
||||
(defn resize-in
|
||||
"Put cel `id`'s left edge at lane frame `to`. Shrinking leaves a gap;
|
||||
growing left consumes earlier cels symmetrically with `resize-out`."
|
||||
[clip sid id to]
|
||||
(let [nodes (get-in clip [:symbols sid :nodes])
|
||||
n (get nodes id)
|
||||
lane (get nodes (:parent n))
|
||||
[old-in hi] (when n (node/placed-span n))
|
||||
earlier (when (node/lane? lane)
|
||||
(remove #(= id (:id %))
|
||||
(filter #(<= (second (node/placed-span %)) old-in)
|
||||
(symbol/lane-clips nodes (:id lane)))))
|
||||
broken (first (symbol/lane-problems nodes))]
|
||||
(cond
|
||||
(not (node/lane? lane)) {:refused "select a cel in a lane"}
|
||||
broken {:refused broken}
|
||||
(not (integer? to)) {:refused "a cel edge goes to a whole lane frame"}
|
||||
(not (< to hi)) {:refused "a drawing must keep at least one frame"}
|
||||
(neg? to) {:refused "a cel cannot begin before the lane"}
|
||||
(= to old-in) {:clip clip :selection id}
|
||||
:else
|
||||
(let [resized (assoc nodes id (span/edged n :in to))
|
||||
changed (if (< to old-in)
|
||||
(reduce
|
||||
(fn [ns sibling]
|
||||
(let [[s e] (node/placed-span sibling)]
|
||||
(cond
|
||||
(<= e to) ns
|
||||
(>= s to) (dissoc ns (:id sibling))
|
||||
:else (assoc ns (:id sibling) (span/edged sibling :out to)))))
|
||||
resized earlier)
|
||||
resized)]
|
||||
(span/finish clip sid changed id :keep)))))
|
||||
|
||||
(defn roll
|
||||
"Move the shared boundary between adjacent cels `left-id` and `right-id`.
|
||||
This is deliberately only the composition of the two ordinary edge edits."
|
||||
[clip sid left-id right-id to]
|
||||
(let [nodes (get-in clip [:symbols sid :nodes])
|
||||
left (get nodes left-id)
|
||||
right (get nodes right-id)
|
||||
lane (get nodes (:parent left))
|
||||
[llo lhi] (when left (node/placed-span left))
|
||||
[rlo rhi] (when right (node/placed-span right))]
|
||||
(cond
|
||||
(or (not (node/lane? lane)) (not= (:parent left) (:parent right)))
|
||||
{:refused "a rolling edit needs two cels in one lane"}
|
||||
(not= lhi rlo) {:refused "a rolling edit needs one shared boundary"}
|
||||
(not (integer? to)) {:refused "a cel edge goes to a whole lane frame"}
|
||||
(not (< llo to rhi)) {:refused "both drawings must keep at least one frame"}
|
||||
:else
|
||||
(let [left-result (resize-out clip sid left-id to {})]
|
||||
(if (:refused left-result)
|
||||
left-result
|
||||
(resize-in (:clip left-result) sid right-id to))))))
|
||||
|
||||
(defn blank
|
||||
"Clear lane frames `[a b)` of lane `lane-id`, leaving a GAP.
|
||||
|
||||
A gap is not a drawing. Nothing is invented to cover those frames and nothing
|
||||
closes the hole — the cels after it stay where they are, because
|
||||
emptying frames and re-timing a performance are different intentions.
|
||||
|
||||
What it does to each cel it meets is `span/edged`, applied three ways: one wholly
|
||||
inside is removed, one overlapping an end is trimmed to it, and the one that
|
||||
spans the whole range is split, which is the only case that needs `id`. Their
|
||||
drawings stay in the library — a lane does not own its content, and a drawing
|
||||
whose last cel is gone is still a drawing somebody made."
|
||||
[clip sid lane-id [a b] {:keys [id]}]
|
||||
(let [nodes (get-in clip [:symbols sid :nodes])
|
||||
lane (get nodes lane-id)
|
||||
members (when (node/lane? lane) (symbol/lane-clips nodes lane-id))
|
||||
spanning (when members
|
||||
(first (filter #(let [[lo hi] (node/placed-span %)] (and (< lo a) (> hi b)))
|
||||
members)))]
|
||||
(cond
|
||||
(not (node/lane? lane)) {:refused "select a lane"}
|
||||
(not (and (integer? a) (integer? b) (< a b)))
|
||||
{:refused "a range to blank is whole lane frames, and not empty"}
|
||||
(and spanning (or (nil? id) (contains? nodes id)))
|
||||
{:refused "blanking inside one cel splits it, which needs a free ID for the remainder"}
|
||||
:else
|
||||
(let [nodes (reduce
|
||||
(fn [ns n]
|
||||
(let [[lo hi] (node/placed-span n)]
|
||||
(cond
|
||||
(or (<= hi a) (>= lo b)) ns
|
||||
(and (< lo a) (> hi b))
|
||||
(-> ns
|
||||
(assoc (:id n) (span/edged n :out a))
|
||||
(assoc id (assoc (span/edged n :in b) :id id :z (str "a-" id))))
|
||||
(and (>= lo a) (<= hi b)) (dissoc ns (:id n))
|
||||
(< lo a) (assoc ns (:id n) (span/edged n :out a))
|
||||
:else (assoc ns (:id n) (span/edged n :in b)))))
|
||||
nodes members)]
|
||||
(span/finish clip sid nodes (or (when spanning id) lane-id) :keep)))))
|
||||
|
||||
(defn add-lane [clip sid id]
|
||||
(let [nodes (get-in clip [:symbols sid :nodes])
|
||||
front (last (sort (keep (fn [[_ n]] (when (nil? (:parent n)) (:z n))) nodes)))]
|
||||
(if (or (nil? (clip/symbol clip sid)) (get nodes id))
|
||||
{:refused "the symbol is missing or the lane ID is already used"}
|
||||
{:clip (assoc-in clip [:symbols sid :nodes id]
|
||||
{:id id :name "lane" :kind :group :layout :sequence
|
||||
:z (symbol/z-between front nil)})
|
||||
:selection id})))
|
||||
|
||||
(defn- holds-other?
|
||||
"Whether `lane-id` already holds clips that are not of `kind`.
|
||||
|
||||
A lane holds picture or sound and not both, and the check belongs HERE
|
||||
rather than only in validation: placement claims time, so a picture dropped
|
||||
on a lane of sound would not be caught as a mixture — `blank` would have
|
||||
deleted the sound to make room for it first, and the document would be
|
||||
valid and the sound gone."
|
||||
[nodes lane-id kind]
|
||||
(boolean (some #(not= kind (:kind %)) (symbol/lane-clips nodes lane-id))))
|
||||
|
||||
(defn place-symbol
|
||||
"Place arbitrary symbol `source-id` as a naturally playing clip in a lane.
|
||||
|
||||
The new clip claims its interval: existing clips under that interval are
|
||||
trimmed, removed, or split by `blank`, so the lane remains a partition rather
|
||||
than storing an overlap. This is the generic operation behind dropping a
|
||||
library symbol into a lane; one-frame held drawing creation remains a policy
|
||||
of `append-drawing`/`overwrite-drawing`, not a different lane type."
|
||||
[clip store sid lane-id id source-id at
|
||||
{:keys [extent point remainder-id] :or {extent :keep}}]
|
||||
(let [nodes (get-in clip [:symbols sid :nodes])
|
||||
lane (get nodes lane-id)
|
||||
seeded (clip/place-symbol clip store sid source-id 0 id point)
|
||||
n (get-in seeded [:symbols sid :nodes id])
|
||||
duration (when n (- (second (node/placed-span n))
|
||||
(first (node/placed-span n))))]
|
||||
(cond
|
||||
(not (node/lane? lane)) {:refused "select a lane"}
|
||||
(holds-other? nodes lane-id :instance)
|
||||
{:refused "that lane holds sound; a lane holds picture or sound, not both"}
|
||||
(contains? nodes id) {:refused "the new clip ID is already used"}
|
||||
(not (and (integer? at) (not (neg? at))))
|
||||
{:refused "a position is a nonnegative whole lane frame"}
|
||||
(nil? (clip/symbol clip source-id)) {:refused "there is no such symbol to place"}
|
||||
(nil? n) {:refused "a symbol cannot go inside itself"}
|
||||
(not (pos? duration)) {:refused "the symbol has no frames to place"}
|
||||
(and remainder-id (contains? nodes remainder-id))
|
||||
{:refused "the remainder clip needs a free ID"}
|
||||
:else
|
||||
(let [cleared (blank clip sid lane-id [at (+ at duration)]
|
||||
{:id remainder-id})]
|
||||
(if (:refused cleared)
|
||||
cleared
|
||||
(let [nodes (assoc (get-in (:clip cleared) [:symbols sid :nodes]) id
|
||||
(-> n
|
||||
(assoc :parent lane-id)
|
||||
(assoc-in [:time :at] at)))]
|
||||
(span/finish (:clip cleared) sid nodes id extent)))))))
|
||||
|
||||
(defn adopt
|
||||
"Move an existing clip — a symbol instance or a sound — into `lane-id` at
|
||||
lane frame `at`. Its source, span, transforms, corrections, and identity come
|
||||
with it; the destination interval is claimed with the same overwrite trimming
|
||||
as a pool drop. What a lane may not do is mix the two kinds, which
|
||||
`symbol/lane-problems` is the judge of and `finish` enforces."
|
||||
[clip sid lane-id id at {:keys [extent remainder-id] :or {extent :keep}}]
|
||||
(let [nodes (get-in clip [:symbols sid :nodes])
|
||||
lane (get nodes lane-id)
|
||||
n (get nodes id)
|
||||
[lo hi] (when n (node/placed-span n))
|
||||
duration (when (and lo hi) (- hi lo))]
|
||||
(cond
|
||||
(not (node/lane? lane)) {:refused "select a lane"}
|
||||
(not (contains? #{:instance :audio} (:kind n)))
|
||||
{:refused "only a symbol or sound clip goes in a lane"}
|
||||
(holds-other? nodes lane-id (:kind n))
|
||||
{:refused "a lane holds picture or sound, not both"}
|
||||
(= lane-id (:parent n)) {:refused "this clip is already in that lane"}
|
||||
(not (and (integer? at) (not (neg? at))))
|
||||
{:refused "a position is a nonnegative whole lane frame"}
|
||||
(not (pos? duration)) {:refused "the clip has no frames to place"}
|
||||
(and remainder-id (contains? nodes remainder-id))
|
||||
{:refused "the remainder clip needs a free ID"}
|
||||
:else
|
||||
(let [cleared (blank clip sid lane-id [at (+ at duration)] {:id remainder-id})]
|
||||
(if (:refused cleared)
|
||||
cleared
|
||||
(let [moved (-> n
|
||||
(assoc :parent lane-id)
|
||||
(update-in [:time :at] (fnil + 0) (- at lo)))
|
||||
nodes (assoc (get-in (:clip cleared) [:symbols sid :nodes]) id moved)]
|
||||
(span/finish (:clip cleared) sid nodes id extent)))))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; putting drawings in a lane
|
||||
|
||||
(defn- held
|
||||
"A one-frame held cel of `drawing-id`, starting at lane frame `at`.
|
||||
|
||||
Held rather than playing, and one frame rather than the length of what it
|
||||
places: a cel's duration is the lane's business — `extend-hold` is how
|
||||
it changes — and reading it off the content would make placing a ten-frame
|
||||
animation and holding its first drawing the same gesture."
|
||||
[id lane-id drawing-id at]
|
||||
{:id id :kind :instance :parent lane-id :z (str "a-" id)
|
||||
:span [0 1] :time {:at at :rate 1}
|
||||
:source {:symbol drawing-id} :playback {:in 0 :speed 0 :end :stop}})
|
||||
|
||||
(defn lane-frame
|
||||
"Symbol frame `f` as a frame of lane `lane-id`'s OWN time, or nil through a
|
||||
stepped or looping lane, where one frame of the symbol is not one frame of the
|
||||
lane and there is no single answer to give a command."
|
||||
[clip sid lane-id f]
|
||||
(when-let [{:keys [at rate]} (symbol/frame-map (get-in clip [:symbols sid :nodes]) lane-id)]
|
||||
(* rate (- f at))))
|
||||
|
||||
(defn- lane-end
|
||||
"Where lane `lane-id`'s occupied frames stop, in its own time."
|
||||
[nodes lane-id]
|
||||
(apply max 0 (map #(second (node/placed-span %))
|
||||
(symbol/lane-clips nodes lane-id))))
|
||||
|
||||
(defn- place
|
||||
"Put a held cel of `drawing-id` into `lane-id` at lane frame `at`, and
|
||||
RIPPLE: everything starting at or after it moves later by its duration.
|
||||
|
||||
There is one placement function and `:end` is a position like any other, so
|
||||
appending is not a different operation from inserting — the end is just where
|
||||
nothing has to move. Overwriting is the other policy and is NOT this: taking
|
||||
frames away from the cel already there is trimming, which is its own
|
||||
command and not something placing a drawing should do on the quiet.
|
||||
|
||||
`:frame` in the result is where it landed, in the open symbol's time, for a
|
||||
caller that wants to look at what it just made."
|
||||
[clip sid lane-id id drawing-id at extent ripple?]
|
||||
(let [nodes (get-in clip [:symbols sid :nodes])
|
||||
at (if (= :end at) (lane-end nodes lane-id) at)
|
||||
n (held id lane-id drawing-id at)
|
||||
[lo hi] (node/placed-span n)
|
||||
later (when ripple?
|
||||
(filter #(>= (first (node/placed-span %)) lo)
|
||||
(symbol/lane-clips nodes lane-id)))
|
||||
nodes (reduce (fn [ns sibling]
|
||||
(update-in ns [(:id sibling) :time :at] (fnil + 0) (- hi lo)))
|
||||
(assoc nodes id n) later)
|
||||
result (span/finish clip sid nodes id extent)
|
||||
m (symbol/frame-map nodes lane-id)]
|
||||
(cond-> result
|
||||
(:clip result) (assoc :frame (+ (:at m) (/ at (:rate m)))))))
|
||||
|
||||
(defn- placeable
|
||||
"Why a held cel cannot go into `lane-id` at `at`, or nil."
|
||||
[clip sid lane-id id at]
|
||||
(let [nodes (get-in clip [:symbols sid :nodes])
|
||||
lane (get nodes lane-id)
|
||||
;; INSIDE a cel is not a position for another one. Splitting that
|
||||
;; cel is what makes it two, and doing it here would be one command
|
||||
;; quietly performing two: the caller asks for `split` and then places.
|
||||
inside (when (number? at)
|
||||
(some (fn [n] (let [[lo hi] (node/placed-span n)]
|
||||
(when (< lo at hi) n)))
|
||||
(symbol/lane-clips nodes lane-id)))]
|
||||
(cond
|
||||
(not (node/lane? lane)) "select a lane"
|
||||
(contains? nodes id) "the new cel ID is already used"
|
||||
(not (or (= :end at) (and (integer? at) (not (neg? at)))))
|
||||
"a position is :end or a whole lane frame"
|
||||
inside (str "frame " at " is inside a cel; split it first")
|
||||
(nil? (symbol/frame-map nodes lane-id)) "drawing creation through a stepped or looping lane is not supported"
|
||||
:else (first (symbol/lane-problems nodes)))))
|
||||
|
||||
(defn append-drawing
|
||||
"Append fresh empty content and a held cel of it. IDs come from the
|
||||
caller so a command is deterministic and replayable.
|
||||
|
||||
Fresh content, not a blank range: a lane with no cel over a frame shows
|
||||
nothing there already, and a drawing nobody has drawn in is a different thing
|
||||
from a gap."
|
||||
[clip sid lane-id id drawing-id {:keys [at extent] :or {extent :keep at :end}}]
|
||||
(if-let [why (or (placeable clip sid lane-id id at)
|
||||
(when (clip/symbol clip drawing-id) "the new drawing ID is already used"))]
|
||||
{:refused why}
|
||||
(place (assoc-in clip [:symbols drawing-id]
|
||||
{:id drawing-id :name (name drawing-id) :fps (clip/fps clip sid) :frames 1 :nodes {}})
|
||||
sid lane-id id drawing-id at extent true)))
|
||||
|
||||
(defn reuse-drawing
|
||||
"Append a held cel of content the document ALREADY has, so the same
|
||||
drawing is exposed twice and editing it changes both cels.
|
||||
|
||||
This is the command `make-unique` is the undo of, and the reason they are two
|
||||
commands: reuse is a decision to share, and sharing is not something to
|
||||
discover later when an edit turns up somewhere else."
|
||||
[clip sid lane-id id drawing-id {:keys [at extent] :or {extent :keep at :end}}]
|
||||
(if-let [why (or (placeable clip sid lane-id id at)
|
||||
(when-not (clip/symbol clip drawing-id) "there is no such drawing to reuse")
|
||||
;; Placing something that contains this symbol would close a
|
||||
;; loop, and a lane is no different from any other placement.
|
||||
(when (clip/contains-symbol? clip drawing-id sid)
|
||||
"a symbol cannot go inside itself"))]
|
||||
{:refused why}
|
||||
(place clip sid lane-id id drawing-id at extent true)))
|
||||
|
||||
(defn- copied
|
||||
"A copy of symbol `from`, as `{:clip :id}`.
|
||||
|
||||
SHALLOW by default: its own nodes and channels are copied, and its references
|
||||
to other symbols are kept, so a head built out of reusable eyes still uses
|
||||
those eyes. `deep?` copies everything it places as well, with new ids
|
||||
throughout, for a drawing that must share nothing — the distinction the
|
||||
shallow copy cannot make on its own, and a promise of independence that only
|
||||
the deep one keeps."
|
||||
[clip from deep?]
|
||||
(if deep?
|
||||
(let [{c :clip ids :ids} (bring/symbols clip clip [from] {})]
|
||||
{:clip c :id (ids from)})
|
||||
(let [id (clip/free-id (:symbols clip) from)]
|
||||
{:clip (assoc-in clip [:symbols id] (assoc (clip/symbol clip from) :id id))
|
||||
:id id})))
|
||||
|
||||
(defn duplicate-drawing
|
||||
"Append a held cel of a COPY of what cel `id` places, for when
|
||||
the drawing on screen is the starting point for the next one.
|
||||
|
||||
The copy is of the content only. The new cel is a plain one-frame hold
|
||||
rather than a copy of `id`'s own transform or corrections: those belong to
|
||||
that cel, and carrying them over would make duplicating a drawing quietly
|
||||
duplicate the treatment of one use of it."
|
||||
[clip sid id new-id {:keys [at extent deep?] :or {extent :keep at :end}}]
|
||||
(let [n (get-in clip [:symbols sid :nodes id])
|
||||
from (node/source n)]
|
||||
(if-let [why (or (when-not from "select a cel to duplicate")
|
||||
(when-not (clip/symbol clip from) "the drawing it places is missing")
|
||||
(placeable clip sid (:parent n) new-id at))]
|
||||
{:refused why}
|
||||
(let [{c :clip copy :id} (copied clip from deep?)]
|
||||
(place c sid (:parent n) new-id copy at extent true)))))
|
||||
|
||||
(defn overwrite-drawing
|
||||
"Put a fresh one-frame drawing at lane frame `at`, replacing whatever was
|
||||
there and leaving every other cel where it was.
|
||||
|
||||
This is `blank` and placement composed in ONE command and therefore one undo
|
||||
step. `remainder-id` is used only when clearing the frame cuts one cel into
|
||||
two; ids still come from the caller because this namespace is pure."
|
||||
[clip sid lane-id id drawing-id at {:keys [extent remainder-id]
|
||||
:or {extent :keep}}]
|
||||
(let [nodes (get-in clip [:symbols sid :nodes])]
|
||||
(if-let [why (cond
|
||||
(not (and (integer? at) (not (neg? at))))
|
||||
"a position is a nonnegative whole lane frame"
|
||||
(contains? nodes id) "the new cel ID is already used"
|
||||
(or (= id remainder-id) (contains? nodes remainder-id))
|
||||
"the remainder cel needs a free ID different from the new cel"
|
||||
(clip/symbol clip drawing-id) "the new drawing ID is already used"
|
||||
(nil? (symbol/frame-map nodes lane-id))
|
||||
"drawing creation through a stepped or looping lane is not supported")]
|
||||
{:refused why}
|
||||
(let [cleared (blank clip sid lane-id [at (inc at)] {:id remainder-id})]
|
||||
(if (:refused cleared)
|
||||
cleared
|
||||
(place (assoc-in (:clip cleared) [:symbols drawing-id]
|
||||
{:id drawing-id :name (name drawing-id) :fps (clip/fps clip sid) :frames 1 :nodes {}})
|
||||
sid lane-id id drawing-id at extent false))))))
|
||||
|
||||
(defn make-unique
|
||||
"Point cel `id` at a private copy of its content, leaving every other
|
||||
cel of that drawing sharing the original.
|
||||
|
||||
Refused when nothing else uses it: a drawing with one cel is already
|
||||
unique, and answering with a silent copy would leave a second identical symbol
|
||||
in the library for no reason a person could see."
|
||||
[clip sid id {:keys [deep?]}]
|
||||
(let [n (get-in clip [:symbols sid :nodes id])
|
||||
from (node/source n)
|
||||
elsewhere (for [[osid osym] (:symbols clip)
|
||||
[oid on] (:nodes osym)
|
||||
:when (and (= from (node/source on)) (not= [sid id] [osid oid]))]
|
||||
[osid oid])]
|
||||
(if-let [why (or (when-not from "select a cel to make unique")
|
||||
(when-not (clip/symbol clip from) "the drawing it places is missing")
|
||||
(when (empty? elsewhere) "nothing else uses this drawing"))]
|
||||
{:refused why}
|
||||
(let [{c :clip copy :id} (copied clip from deep?)
|
||||
c (assoc-in c [:symbols sid :nodes id :source :symbol] copy)
|
||||
ps (clip/problems c)]
|
||||
(if (seq ps) {:refused (first ps)} {:clip c :selection id})))))
|
||||
|
|
@ -144,7 +144,7 @@
|
|||
;; disagree with itself about; `clip` puts it back.
|
||||
(for [[sid sym] (:symbols clip)]
|
||||
{(at "symbol" (segment sid))
|
||||
(select-keys sym [:name :frames :fps :width :height :palette])})
|
||||
(select-keys sym [:name :frames :fps :width :height :palette :display])})
|
||||
(for [[sid sym] (:symbols clip)
|
||||
[id n] (:nodes sym)]
|
||||
{(at "symbol" (segment sid) "node" (segment id))
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@
|
|||
matrix becomes a `:pinv`, Blender's parent-inverse, and the time a new `:at`
|
||||
and `:rate`. Its channels, keys and span are untouched."
|
||||
(:require [arthur.domain.clip :as clip]
|
||||
[arthur.domain.lane :as lane]
|
||||
[arthur.domain.node :as node]
|
||||
[arthur.domain.palette :as pal]
|
||||
[arthur.domain.span :as span]
|
||||
|
|
@ -73,7 +72,7 @@
|
|||
inner (:symbol shown)
|
||||
lf (if shown (:frame shown) local)]
|
||||
(if (and m (number? local)
|
||||
;; A lane over a gap has no inside to be in.
|
||||
;; A clip over a gap has no inside to be in.
|
||||
(or (not inst?) shown)
|
||||
(or (nil? inner) (< -1 lf (clip/frames clip inner))))
|
||||
{:sid inner :frame (js/Math.floor lf)
|
||||
|
|
@ -284,8 +283,8 @@
|
|||
|
||||
The one that surprises: BOTH have to be on screen at this frame, because the
|
||||
move keeps the picture and there is no common frame to keep it at otherwise.
|
||||
Two clips in one lane never overlap, so nesting one into another there can
|
||||
never be done — it is a thing to do between lanes, with the playhead
|
||||
Two clips of one lane never overlap, so nesting one into another there can
|
||||
never be done — it is a thing to do between symbols, with the playhead
|
||||
somewhere both of them are showing.
|
||||
|
||||
The deeper refusals — generated parts, a stencil parted from what it clips —
|
||||
|
|
@ -335,8 +334,8 @@
|
|||
only a move that keeps the PICTURE needs a frame, for the matrix."
|
||||
[clip sid path]
|
||||
(let [;; Structurally, a row leads into a symbol only where it names one:
|
||||
;; a cel does, and the lane holding it does not, so the walk
|
||||
;; stops at a lane rather than picking the drawing showing now — which
|
||||
;; a clip does, and a group holding one does not, so the walk stops at
|
||||
;; the group rather than picking the drawing showing now — which
|
||||
;; would make where a row lives depend on the playhead.
|
||||
only (fn [sid id]
|
||||
(when sid (node/source (get-in clip [:symbols sid :nodes id]))))
|
||||
|
|
@ -394,8 +393,10 @@
|
|||
|
||||
(defn resize-out
|
||||
"Move the right edge of the node at `path` by `df` frames of `open`.
|
||||
Lane cels use the lane's collision/ripple rules; ordinary clips, including
|
||||
audio, resize independently."
|
||||
|
||||
One command either way: in a symbol drawn as a lane `span/resize-out` claims
|
||||
the time it grows into, and in a composition it is one write to one span. The
|
||||
mode says which, and nothing here has to ask."
|
||||
[clip open path df ripple?]
|
||||
(let [here (down clip open (pop path))
|
||||
sid (:sid here)
|
||||
|
|
@ -409,12 +410,10 @@
|
|||
(let [chain (map #(get nodes %) (reverse (rest (symbol/lineage nodes id))))
|
||||
d (* df (:rate (reduce node/then-time (:time here) (map node/time-of chain))))
|
||||
to (+ (second (node/placed-span n)) d)]
|
||||
(if (node/lane? (get nodes (:parent n)))
|
||||
(lane/resize-out clip sid id to {:ripple? ripple? :extent :grow-symbol})
|
||||
(span/resize-out clip sid id to))))))
|
||||
(span/resize-out clip sid id to {:ripple? ripple? :extent :grow-symbol})))))
|
||||
|
||||
(defn resize-in
|
||||
"Move a lane cel's left edge by `df` frames of `open`."
|
||||
"Move the left edge of the node at `path` by `df` frames of `open`."
|
||||
[clip open path df]
|
||||
(let [here (down clip open (pop path))
|
||||
sid (:sid here)
|
||||
|
|
@ -424,13 +423,11 @@
|
|||
(cond
|
||||
(nil? n) {:refused "nothing to resize"}
|
||||
(nil? (:time here)) {:refused "a looping instance is in the way"}
|
||||
(not (node/lane? (get nodes (:parent n))))
|
||||
{:refused "only a lane cel has a collision-free left edge"}
|
||||
:else
|
||||
(let [chain (map #(get nodes %) (reverse (rest (symbol/lineage nodes id))))
|
||||
d (* df (:rate (reduce node/then-time (:time here) (map node/time-of chain))))
|
||||
to (+ (first (node/placed-span n)) d)]
|
||||
(lane/resize-in clip sid id to)))))
|
||||
(span/resize-in clip sid id to)))))
|
||||
|
||||
(defn roll
|
||||
"Move the shared boundary at `right-path` and the adjacent `left-path`."
|
||||
|
|
@ -443,13 +440,13 @@
|
|||
right (get nodes right-id)]
|
||||
(cond
|
||||
(or (not= (pop left-path) (pop right-path)) (nil? right))
|
||||
{:refused "a rolling edit needs adjacent clips in one lane"}
|
||||
{:refused "a rolling edit needs adjacent clips in one sequence"}
|
||||
(nil? (:time here)) {:refused "a looping instance is in the way"}
|
||||
:else
|
||||
(let [chain (map #(get nodes %) (reverse (rest (symbol/lineage nodes right-id))))
|
||||
d (* df (:rate (reduce node/then-time (:time here) (map node/time-of chain))))
|
||||
to (+ (first (node/placed-span right)) d)]
|
||||
(lane/roll clip sid left-id right-id to)))))
|
||||
(span/roll clip sid left-id right-id to)))))
|
||||
|
||||
(defn restack
|
||||
"Put the node at row path `from` just in front of the one at `to` when
|
||||
|
|
|
|||
|
|
@ -244,16 +244,6 @@
|
|||
(when (and (<= 0 frame) (< frame length))
|
||||
{:symbol sid :frame frame})))))
|
||||
|
||||
(defn lane?
|
||||
"Is this group a LANE — a succession of cels rather than a composition?
|
||||
|
||||
`:layout :sequence` is the field because it names the RULE: children follow
|
||||
one another and may not overlap. A group carrying it is called a lane, which
|
||||
is the one place two words are kept for one thing, and they are kept apart on
|
||||
purpose — the layout says what the rule is, the noun says what the thing is."
|
||||
[n]
|
||||
(and (= :group (:kind n)) (= :sequence (:layout n))))
|
||||
|
||||
(defn finite-number? [v] (and (number? v) (js/Number.isFinite v)))
|
||||
|
||||
(defn local-frame
|
||||
|
|
@ -415,8 +405,8 @@
|
|||
(finite-number? speed) (<= 0 speed)
|
||||
(#{:stop :hold :loop} end)))))
|
||||
(conj "playback needs a nonnegative finite :in and :speed, and :end :stop, :hold or :loop")
|
||||
(and (:layout n) (not (lane? n)))
|
||||
(conj ":layout :sequence belongs to a group")
|
||||
(:layout n)
|
||||
(conj ":layout is not a node field — a lane is how the timeline DRAWS a symbol, not a thing in the document")
|
||||
(and (= k :audio) (not (some (:source n) [:footage :sound])))
|
||||
(conj "an audio node needs a :source :footage or :sound")
|
||||
(and (some? (get-in n [:time :rate]))
|
||||
|
|
|
|||
|
|
@ -1,28 +1,51 @@
|
|||
(ns arthur.domain.span
|
||||
"The commands over ONE node's place in time: split it, trim an edge, move it.
|
||||
"The commands over a node's place in time: split it, trim an edge, move it —
|
||||
and, where the symbol holding it is drawn as a lane, re-span its siblings to
|
||||
make room.
|
||||
|
||||
A `:span` is in the node's OWN frames and its `:time` says where those land in
|
||||
its parent, and that is true of EVERY node — which is why these three are not
|
||||
lane commands, though a lane of cels is where they were first needed. A cel in
|
||||
a lane, a symbol placed straight into a shot, a shape that exists for part of
|
||||
one: each is a span in a parent's frame space, and a span in a parent's frame
|
||||
space is the whole of what these commands touch. They were gated on a lane for
|
||||
as long as a lane was the only thing anybody had timed.
|
||||
its parent, and that is true of EVERY node. A clip in a sequence, a symbol
|
||||
placed straight into a shot, a shape that exists for part of one: each is a
|
||||
span in a parent's frame space, and a span in a parent's frame space is the
|
||||
whole of what these commands touch.
|
||||
|
||||
THE COORDINATE IS ALWAYS THE PARENT'S. For a cel the parent is its lane, so
|
||||
`host-frame` reads lane time exactly as the lane commands always did; for a
|
||||
node sitting straight in the symbol it reads the symbol's own frames. One rule,
|
||||
so a caller holding a node does not branch on what it sits in.
|
||||
THE COORDINATE IS ALWAYS THE PARENT'S. `host-frame` reads the frame space the
|
||||
node is positioned in, whatever that is, so a caller holding a node does not
|
||||
branch on what it sits in.
|
||||
|
||||
A GROUP IS REFUSED. Dividing a group means deciding what becomes of its
|
||||
children, and nothing in a span says: the right half of a split lane would
|
||||
reference none of its cels, and a span that narrows past a child hides it
|
||||
without saying so. `domain/lane` holds the commands for a sequence, which are
|
||||
the ones that ripple siblings or leave a gap.
|
||||
A GROUP IS REFUSED where one node is being divided. Dividing a group means
|
||||
deciding what becomes of its children, and nothing in a span says: a span that
|
||||
narrows past a child hides it without saying so.
|
||||
|
||||
`finish` lives here because every command in this namespace and every one in
|
||||
`domain/lane` commits through it."
|
||||
(:require [arthur.domain.clip :as clip]
|
||||
WHY THE SEQUENCE COMMANDS ARE HERE TOO. They used to be `domain/lane`, gated on
|
||||
a group with `:layout :sequence`, because a lane was the only thing anybody had
|
||||
timed. There is no such group any more — the SYMBOL is the container and a lane
|
||||
is how the timeline draws one, `symbol/lane?` — so their subject is a symbol and
|
||||
its children, which is the same subject as everything else here: one write to
|
||||
one node's span, with the siblings re-spanned around it. See
|
||||
`docs/lane-is-a-view-plan.md`.
|
||||
|
||||
THE CLAIM-TIME RULE FOLLOWS THE MODE. In lane mode, placing, moving or growing
|
||||
over occupied frames TRIMS what it lands on — trimming the incumbent, removing
|
||||
one wholly covered, splitting one it lands inside — so the result has no overlap
|
||||
because the operation that could have made one did not. Outside lane mode
|
||||
nothing is enforced, because overlapping children are what compositing IS.
|
||||
|
||||
EVERY COMMAND IS ONE STEP AND ALL OF IT. Each returns `{:clip :selection}` or
|
||||
`{:refused reason}` — never a half-applied edit, and never a document that
|
||||
`clip/problems` would reject. A command that cannot say what the person meant
|
||||
refuses and says why, rather than picking for them: the overflow policy is a
|
||||
caller's `:extent`, and decoupling shared content is its own command instead of
|
||||
something an ordinary edit does silently.
|
||||
|
||||
IDS COME FROM THE CALLER, because a clip's identity is a uuid and this namespace
|
||||
is pure. Ids for new CONTENT are derived from the drawing being copied —
|
||||
`clip/free-id` is pure too, and `drawing-a-2` says what it came from in a way
|
||||
`symbol-7` does not.
|
||||
|
||||
`finish` lives here because every command in this namespace commits through it."
|
||||
(:require [arthur.domain.bring :as bring]
|
||||
[arthur.domain.clip :as clip]
|
||||
[arthur.domain.node :as node]
|
||||
[arthur.domain.symbol :as symbol]))
|
||||
|
||||
|
|
@ -30,32 +53,43 @@
|
|||
"Commit `nodes` as symbol `sid`'s, or refuse.
|
||||
|
||||
THE SHOT LENGTH IS AUTHORED. `:frames` is the symbol's window — how long the
|
||||
shot IS — and the occupied extent of its lanes is a different fact derived
|
||||
from the cels. A command may GROW the window when the caller says
|
||||
`:grow-symbol`, and never shrinks it: emptying the end of a shot leaves a shot
|
||||
with empty frames at the end, which is a true statement about what somebody
|
||||
authored. Deriving the window from the extent instead would make deleting the
|
||||
last drawing silently shorten the film.
|
||||
shot IS — and where its clips reach is a different fact derived from them. A
|
||||
command may GROW the window when the caller says `:grow-symbol`, and never
|
||||
shrinks it: emptying the end of a shot leaves a shot with empty frames at the
|
||||
end, which is a true statement about what somebody authored. Deriving the window
|
||||
from the reach instead would make deleting the last drawing silently shorten the
|
||||
film.
|
||||
|
||||
So there are two numbers and this function keeps them apart: `needed` is where
|
||||
the cels reach, `:frames` is what was authored, and the only way the
|
||||
second follows the first is a caller asking.
|
||||
the clips reach, `:frames` is what was authored, and the only way the second
|
||||
follows the first is a caller asking.
|
||||
|
||||
Only LANES are measured for reach. A node placed straight in a shot may hang
|
||||
off the end of it — that is an ordinary thing to author and the window is
|
||||
what crops it — whereas a lane's cels are a sequence whose length is the
|
||||
thing being edited."
|
||||
Only a symbol drawn AS A LANE is measured for reach. A node placed into a
|
||||
composition may hang off the end of it — that is an ordinary thing to author and
|
||||
the window is what crops it — whereas a lane's clips are a sequence whose length
|
||||
is the thing being edited.
|
||||
|
||||
THE OVERLAP INVARIANT IS ENFORCED HERE, and here is the only place it needs to
|
||||
be: this is the single commit path for every sequence command, it validates
|
||||
before it returns, and it refuses rather than half-applying. So no command can
|
||||
commit an overlap in lane mode, and `symbol/overlaps` turning up anything is a
|
||||
bug in a command rather than a state to design around."
|
||||
[clip sid nodes selection extent]
|
||||
(let [sym (clip/symbol clip sid)
|
||||
reach (for [[id n] nodes :when (node/lane? n)
|
||||
child (symbol/lane-clips nodes id)
|
||||
:let [m (symbol/frame-map nodes id)
|
||||
end (second (node/placed-span child))]]
|
||||
(when m (+ (:at m) (/ end (:rate m)))))
|
||||
needed (js/Math.ceil (apply max 0 (keep identity reach)))
|
||||
ps (symbol/problems (assoc sym :nodes nodes))]
|
||||
lane? (symbol/lane? sym)
|
||||
after (assoc sym :nodes nodes)
|
||||
needed (if-not lane?
|
||||
0
|
||||
(js/Math.ceil (apply max 0 (keep #(second (node/placed-span %))
|
||||
(symbol/children nodes)))))
|
||||
ps (symbol/problems after)
|
||||
clashing (when lane? (symbol/overlaps after))]
|
||||
(cond
|
||||
(seq ps) {:refused (first ps)}
|
||||
(seq clashing)
|
||||
{:refused (str "that would put " (pr-str (ffirst clashing)) " and "
|
||||
(pr-str (second (first clashing)))
|
||||
" on screen over the same frames of a lane")}
|
||||
(not (#{:keep :grow-symbol} extent)) {:refused "choose an explicit shot-length policy"}
|
||||
(and (> needed (:frames sym)) (= :keep extent))
|
||||
{:refused (str "the edit needs " needed " frames; extend the shot to continue")
|
||||
|
|
@ -73,7 +107,7 @@
|
|||
;; and `:playback` are untouched — which is why trimming the front of a playing
|
||||
;; insert starts it later in its source instead of resetting it, and why the two
|
||||
;; halves of a split go on meaning what the one node meant.
|
||||
;; Trim, split and `lane/blank` are all this one operation, applied differently.
|
||||
;; Trim, split and `blank` are all this one operation, applied differently.
|
||||
|
||||
(defn local
|
||||
"Parent frame `f` as one of `n`'s own frames."
|
||||
|
|
@ -89,8 +123,10 @@
|
|||
(defn host-frame
|
||||
"Symbol frame `f` as a frame of the space node `id` is POSITIONED in — its
|
||||
parent's — which is the frame space every command here takes its coordinate
|
||||
in. Nil through a stepped or looping ancestor, where one frame of the symbol
|
||||
is not one frame of the parent and there is no single answer to give."
|
||||
in. For a clip of a lane that is the symbol's own frames, because the symbol is
|
||||
the container and the clip has no parent. Nil through a stepped or looping
|
||||
ancestor, where one frame of the symbol is not one frame of the parent and there
|
||||
is no single answer to give."
|
||||
[clip sid id f]
|
||||
(let [nodes (get-in clip [:symbols sid :nodes])]
|
||||
(when-let [{:keys [at rate]} (symbol/frame-map nodes (:parent (get nodes id)))]
|
||||
|
|
@ -98,7 +134,7 @@
|
|||
|
||||
(defn- subject
|
||||
"The node `id` names, as `{:node n}`, or `{:refused why}` where these commands
|
||||
have nothing to act on. The one guard all three share."
|
||||
have nothing to act on. The one guard they share."
|
||||
[nodes id]
|
||||
(let [n (get nodes id)]
|
||||
(cond
|
||||
|
|
@ -109,6 +145,13 @@
|
|||
{:refused "this is on screen for the whole shot, so it has no edges to cut"}
|
||||
:else {:node n})))
|
||||
|
||||
(defn- siblings
|
||||
"The other clips of `sid`'s sequence, or nil where `sid` is not drawn as a lane
|
||||
and so has no sequence to re-span."
|
||||
[clip sid nodes id]
|
||||
(when (symbol/lane? (clip/symbol clip sid))
|
||||
(remove #(= id (:id %)) (symbol/children nodes))))
|
||||
|
||||
(defn split
|
||||
"Cut node `id` in two at parent frame `cut`. The left piece keeps its
|
||||
identity; the right gets `new-id`.
|
||||
|
|
@ -125,8 +168,8 @@
|
|||
|
||||
THE RIGHT PIECE KEEPS THE ORIGINAL'S `:z`. Two halves of one thing draw at
|
||||
one depth; nothing orders them against each other, because they are never on
|
||||
screen on the same frame. Cels in a lane do not consult `:z` at all —
|
||||
`symbol/lane-clips` sorts them by where they start.
|
||||
screen on the same frame. A lane's clips do not consult `:z` at all —
|
||||
`symbol/children` sorts them by where they start.
|
||||
|
||||
The right piece is the selection, because it is the piece that was made."
|
||||
[clip sid id cut new-id]
|
||||
|
|
@ -148,10 +191,10 @@
|
|||
"Move one edge of node `id` to parent frame `to`, without disturbing anything
|
||||
else at all.
|
||||
|
||||
TRIM NARROWS. Lengthening a cel is `lane/extend-hold`, which carries a ripple
|
||||
policy and a shot-length policy because it needs them; letting trim grow as
|
||||
well would give one gesture two sets of rules and a way to overlap its
|
||||
neighbour. `edge` is `:in` or `:out`.
|
||||
TRIM NARROWS. Lengthening is `resize-out`, which carries a ripple policy and a
|
||||
shot-length policy because it needs them; letting trim grow as well would give
|
||||
one gesture two sets of rules and a way to overlap its neighbour. `edge` is
|
||||
`:in` or `:out`.
|
||||
|
||||
The source clock is untouched, so trimming the front of a playing insert
|
||||
starts it later INTO its animation rather than restarting it — which is the
|
||||
|
|
@ -168,32 +211,18 @@
|
|||
{:refused (str "frame " to " is not inside this; trim narrows it")}
|
||||
:else (finish clip sid (assoc nodes id (edged node edge to)) id :keep))))
|
||||
|
||||
(defn resize-out
|
||||
"Put one node's right edge at parent frame `to`, allowing it to grow.
|
||||
|
||||
This is for an ordinary timeline clip, including audio. Lane cels use
|
||||
`lane/resize-out`, because only a lane has neighbours to trim or ripple."
|
||||
[clip sid id to]
|
||||
(let [nodes (get-in clip [:symbols sid :nodes])
|
||||
{:keys [node refused]} (subject nodes id)
|
||||
[lo _] (when node (node/placed-span node))]
|
||||
(cond
|
||||
refused {:refused refused}
|
||||
(not (integer? to)) {:refused "an edge goes to a whole frame"}
|
||||
(not (< lo to)) {:refused "a clip must keep at least one frame"}
|
||||
:else (finish clip sid (assoc nodes id (edged node :out to)) id :keep))))
|
||||
|
||||
(defn move
|
||||
"Put node `id` at parent frame `to`, leaving its own length, source and
|
||||
corrections alone — and, in a lane, every other cel.
|
||||
corrections alone — and, in a lane, every other clip.
|
||||
|
||||
One write to `:time :at`. A destination that would overlap a neighbour IN A
|
||||
LANE is refused rather than rippled or overwritten: moving a drawing and
|
||||
re-timing the ones around it are different intentions, and a move that
|
||||
silently pushed the rest would be the second one wearing the first one's name.
|
||||
Clear the room first — `lane/blank` makes a gap, `trim` shortens a neighbour.
|
||||
Outside a lane there is no such rule to break: things placed in a composition
|
||||
are allowed to be on screen together, so the move simply happens."
|
||||
Clear the room first — `blank` makes a gap, `trim` shortens a neighbour — or
|
||||
say you meant to claim it, which is `adopt`, what a body drag does.
|
||||
Outside lane mode there is no such rule to break: things placed in a
|
||||
composition are allowed to be on screen together, so the move simply happens."
|
||||
[clip sid id to]
|
||||
(let [nodes (get-in clip [:symbols sid :nodes])
|
||||
{:keys [node refused]} (subject nodes id)]
|
||||
|
|
@ -205,3 +234,488 @@
|
|||
(if (not= to (first (node/placed-span moved)))
|
||||
{:refused "timing through a stepped or looping parent is not supported"}
|
||||
(finish clip sid (assoc nodes id moved) id :keep))))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; the sequence: one edge edit, with the siblings re-spanned around it
|
||||
|
||||
(defn- trimmed-into-a-sequence
|
||||
"`nodes` with every clip's right edge pulled back to where the next one starts,
|
||||
and any clip the next one wholly covers removed.
|
||||
|
||||
THE SAME CLAIM-TIME RULE, APPLIED ALL AT ONCE. Later claims from earlier
|
||||
everywhere it has to, which is the rule every other command here follows one
|
||||
edit at a time; doing it as a pass is only what makes the answer to \"make this
|
||||
a lane\" one undo step."
|
||||
[nodes]
|
||||
(reduce
|
||||
(fn [ns [earlier later]]
|
||||
(let [[lo hi] (node/placed-span (get ns (:id earlier)))
|
||||
[next-lo _] (node/placed-span later)]
|
||||
(cond
|
||||
(nil? lo) ns
|
||||
(<= hi next-lo) ns
|
||||
(<= next-lo lo) (dissoc ns (:id earlier))
|
||||
:else (assoc ns (:id earlier) (edged (get ns (:id earlier)) :out next-lo)))))
|
||||
nodes
|
||||
(partition 2 1 (symbol/children nodes))))
|
||||
|
||||
(defn draw-as-lane
|
||||
"Turn symbol `sid`'s lane mode on or off. `{:clip c}` or `{:refused why}`.
|
||||
|
||||
OFF IS ALWAYS POSSIBLE: a composition has no invariant to break, so dropping
|
||||
the hint drops the rules with it and nothing in the document moves.
|
||||
|
||||
ON IS THE ONE PLACE A PERSON CAN ASK FOR THE IMPOSSIBLE. Every other command
|
||||
maintains the sequence; this one asks a symbol whose clips may already be on
|
||||
screen together to start being one, and there is no answer that does not throw
|
||||
frames away. So it refuses and says how many clips it would have to trim,
|
||||
carrying `:required-trim` for the retry the UI offers as one button — the same
|
||||
shape as `finish`'s `:required-frames`. With `trim?` it does it: later claims
|
||||
from earlier, which is the rule everything else here already follows."
|
||||
[clip sid on? {:keys [trim?]}]
|
||||
(let [sym (clip/symbol clip sid)]
|
||||
(cond
|
||||
(nil? sym) {:refused "there is no such symbol"}
|
||||
(not on?) {:clip (update-in clip [:symbols sid] dissoc :display)}
|
||||
:else
|
||||
(let [clashing (symbol/overlaps sym)]
|
||||
(cond
|
||||
(empty? clashing) {:clip (assoc-in clip [:symbols sid :display] :lane)}
|
||||
(not trim?)
|
||||
{:refused (str "drawing this as a lane means trimming "
|
||||
(count clashing) " clip"
|
||||
(when (< 1 (count clashing)) "s")
|
||||
" that overlap a neighbour")
|
||||
:required-trim (count clashing)}
|
||||
:else
|
||||
(let [nodes (trimmed-into-a-sequence (:nodes sym))
|
||||
after (assoc sym :nodes nodes :display :lane)]
|
||||
(if (seq (symbol/overlaps after))
|
||||
{:refused "these clips cannot be trimmed into a sequence"}
|
||||
{:clip (assoc-in clip [:symbols sid] after)})))))))
|
||||
|
||||
(defn resize-out
|
||||
"Put clip `id`'s right edge at parent frame `to`, allowing it to grow.
|
||||
|
||||
IN A LANE this claims time. Without `ripple?`, growing consumes the starts of
|
||||
the clips it reaches: wholly covered clips disappear and the last partially
|
||||
covered one is trimmed. Shrinking leaves a gap. With `ripple?`, every clip
|
||||
beginning at or after the old edge moves by the same delta, in either
|
||||
direction, so their contents are preserved. Either way there is no overlap,
|
||||
because the operation that could have made one did not.
|
||||
|
||||
OUTSIDE A LANE it is one write to one span and nothing else moves, because
|
||||
things placed in a composition are allowed to be on screen together. One
|
||||
gesture, and the mode says which rule it plays by."
|
||||
[clip sid id to {:keys [extent ripple?] :or {extent :keep ripple? false}}]
|
||||
(let [nodes (get-in clip [:symbols sid :nodes])
|
||||
{:keys [node refused]} (subject nodes id)
|
||||
[lo old-out] (when node (node/placed-span node))
|
||||
later (when node
|
||||
(filter #(>= (first (node/placed-span %)) old-out)
|
||||
(siblings clip sid nodes id)))]
|
||||
(cond
|
||||
refused {:refused refused}
|
||||
(not (integer? to)) {:refused "an edge goes to a whole frame"}
|
||||
(not (< lo to)) {:refused "a clip must keep at least one frame"}
|
||||
(= to old-out) {:clip clip :selection id}
|
||||
:else
|
||||
(let [delta (- to old-out)
|
||||
resized (assoc nodes id (edged node :out to))
|
||||
changed
|
||||
(if ripple?
|
||||
(reduce (fn [ns sibling]
|
||||
(update-in ns [(:id sibling) :time :at] (fnil + 0) delta))
|
||||
resized later)
|
||||
(if (pos? delta)
|
||||
(reduce
|
||||
(fn [ns sibling]
|
||||
(let [[s e] (node/placed-span sibling)]
|
||||
(cond
|
||||
(>= s to) ns
|
||||
(<= e to) (dissoc ns (:id sibling))
|
||||
:else (assoc ns (:id sibling) (edged sibling :in to)))))
|
||||
resized later)
|
||||
resized))]
|
||||
(finish clip sid changed id extent)))))
|
||||
|
||||
(defn resize-in
|
||||
"Put clip `id`'s left edge at parent frame `to`. Shrinking leaves a gap;
|
||||
in a lane, growing left consumes earlier clips symmetrically with `resize-out`."
|
||||
[clip sid id to]
|
||||
(let [nodes (get-in clip [:symbols sid :nodes])
|
||||
{:keys [node refused]} (subject nodes id)
|
||||
[old-in hi] (when node (node/placed-span node))
|
||||
earlier (when node
|
||||
(filter #(<= (second (node/placed-span %)) old-in)
|
||||
(siblings clip sid nodes id)))]
|
||||
(cond
|
||||
refused {:refused refused}
|
||||
(not (integer? to)) {:refused "an edge goes to a whole frame"}
|
||||
(not (< to hi)) {:refused "a clip must keep at least one frame"}
|
||||
(neg? to) {:refused "a clip cannot begin before the shot"}
|
||||
(= to old-in) {:clip clip :selection id}
|
||||
:else
|
||||
(let [resized (assoc nodes id (edged node :in to))
|
||||
changed (if (< to old-in)
|
||||
(reduce
|
||||
(fn [ns sibling]
|
||||
(let [[s e] (node/placed-span sibling)]
|
||||
(cond
|
||||
(<= e to) ns
|
||||
(>= s to) (dissoc ns (:id sibling))
|
||||
:else (assoc ns (:id sibling) (edged sibling :out to)))))
|
||||
resized earlier)
|
||||
resized)]
|
||||
(finish clip sid changed id :keep)))))
|
||||
|
||||
(defn roll
|
||||
"Move the shared boundary between adjacent clips `left-id` and `right-id`.
|
||||
This is deliberately only the composition of the two ordinary edge edits."
|
||||
[clip sid left-id right-id to]
|
||||
(let [nodes (get-in clip [:symbols sid :nodes])
|
||||
left (get nodes left-id)
|
||||
right (get nodes right-id)
|
||||
[llo lhi] (when left (node/placed-span left))
|
||||
[rlo rhi] (when right (node/placed-span right))]
|
||||
(cond
|
||||
(not (symbol/lane? (clip/symbol clip sid)))
|
||||
{:refused "a rolling edit needs a symbol drawn as a lane"}
|
||||
(not (and llo rlo (nil? (:parent left)) (nil? (:parent right))))
|
||||
{:refused "a rolling edit needs two clips of one sequence"}
|
||||
(not= lhi rlo) {:refused "a rolling edit needs one shared boundary"}
|
||||
(not (integer? to)) {:refused "a clip edge goes to a whole frame"}
|
||||
(not (< llo to rhi)) {:refused "both clips must keep at least one frame"}
|
||||
:else
|
||||
(let [left-result (resize-out clip sid left-id to {})]
|
||||
(if (:refused left-result)
|
||||
left-result
|
||||
(resize-in (:clip left-result) sid right-id to))))))
|
||||
|
||||
(defn blank
|
||||
"Clear frames `[a b)` of symbol `sid`, leaving a GAP.
|
||||
|
||||
A gap is not a drawing. Nothing is invented to cover those frames and nothing
|
||||
closes the hole — the clips after it stay where they are, because emptying
|
||||
frames and re-timing a performance are different intentions.
|
||||
|
||||
What it does to each clip it meets is `edged`, applied three ways: one wholly
|
||||
inside is removed, one overlapping an end is trimmed to it, and the one that
|
||||
spans the whole range is split, which is the only case that needs `id`. Their
|
||||
drawings stay in the library — a symbol does not own its content, and a drawing
|
||||
whose last clip is gone is still a drawing somebody made."
|
||||
[clip sid [a b] {:keys [id]}]
|
||||
(let [nodes (get-in clip [:symbols sid :nodes])
|
||||
lane? (symbol/lane? (clip/symbol clip sid))
|
||||
members (when lane? (symbol/children nodes))
|
||||
spanning (when members
|
||||
(first (filter #(let [[lo hi] (node/placed-span %)] (and (< lo a) (> hi b)))
|
||||
members)))]
|
||||
(cond
|
||||
(not lane?) {:refused "clearing a range of frames needs a symbol drawn as a lane"}
|
||||
(not (and (integer? a) (integer? b) (< a b)))
|
||||
{:refused "a range to blank is whole frames, and not empty"}
|
||||
(and spanning (or (nil? id) (contains? nodes id)))
|
||||
{:refused "blanking inside one clip splits it, which needs a free ID for the remainder"}
|
||||
:else
|
||||
(let [nodes (reduce
|
||||
(fn [ns n]
|
||||
(let [[lo hi] (node/placed-span n)]
|
||||
(cond
|
||||
(or (<= hi a) (>= lo b)) ns
|
||||
(and (< lo a) (> hi b))
|
||||
(-> ns
|
||||
(assoc (:id n) (edged n :out a))
|
||||
(assoc id (assoc (edged n :in b) :id id :z (str "a-" id))))
|
||||
(and (>= lo a) (<= hi b)) (dissoc ns (:id n))
|
||||
(< lo a) (assoc ns (:id n) (edged n :out a))
|
||||
:else (assoc ns (:id n) (edged n :in b)))))
|
||||
nodes members)]
|
||||
;; NOTHING SENSIBLE IS SELECTED by emptying frames, so nothing is: a
|
||||
;; split names its remainder, and otherwise the caller keeps whatever was
|
||||
;; selected rather than being handed a clip it did not ask for.
|
||||
(finish clip sid nodes (when spanning id) :keep)))))
|
||||
|
||||
(defn- cleared
|
||||
"`clip` with frames `[at (+ at duration))` of `sid` emptied where `sid` is drawn
|
||||
as a lane, and untouched where it is not: outside lane mode a placement does not
|
||||
claim time, because being on screen together is what compositing IS.
|
||||
|
||||
`{:clip c}` or `{:refused why}`, so one `if-let` covers both."
|
||||
[clip sid at duration remainder-id]
|
||||
(if-not (symbol/lane? (clip/symbol clip sid))
|
||||
{:clip clip}
|
||||
(blank clip sid [at (+ at duration)] {:id remainder-id})))
|
||||
|
||||
(defn extend-hold
|
||||
"Change one held clip's duration by `delta` frames and ripple its later
|
||||
siblings. Keys, source clocks and the clips' own channels stay put.
|
||||
Returns {:clip :selection} or {:refused :required-frames?}; never partially edits."
|
||||
[clip sid id delta {:keys [extent] :or {extent :keep}}]
|
||||
(let [nodes (get-in clip [:symbols sid :nodes])
|
||||
n (get nodes id)
|
||||
rate (:rate (node/time-of n))
|
||||
span (:span n)]
|
||||
(cond
|
||||
(not (symbol/lane? (clip/symbol clip sid)))
|
||||
{:refused "a hold is lengthened in a symbol drawn as a lane"}
|
||||
(nil? (node/placed-span n)) {:refused "select a clip in the lane"}
|
||||
(some? (:parent n)) {:refused "select a clip of the lane itself"}
|
||||
(not (and (integer? delta) (not (zero? delta)))) {:refused "hold change must be a nonzero whole number of frames"}
|
||||
(not (zero? (:speed (node/playback-of n)))) {:refused "hold length applies to a held drawing"}
|
||||
(<= (+ (second span) (* rate delta)) (first span)) {:refused "a drawing must keep a positive exposure"}
|
||||
:else
|
||||
(let [[_ boundary] (node/placed-span n)
|
||||
later (filter #(>= (first (node/placed-span %)) boundary)
|
||||
(siblings clip sid nodes id))
|
||||
nodes (assoc-in nodes [id :span 1] (+ (second span) (* rate delta)))
|
||||
nodes (reduce (fn [ns sibling]
|
||||
(update-in ns [(:id sibling) :time :at] (fnil + 0) delta))
|
||||
nodes later)]
|
||||
(finish clip sid nodes id extent)))))
|
||||
|
||||
(defn place-symbol
|
||||
"Place arbitrary symbol `source-id` into `sid` as a naturally playing clip at
|
||||
frame `at`.
|
||||
|
||||
IN A LANE the new clip claims its interval: existing clips under it are
|
||||
trimmed, removed, or split, so the sequence stays a partition rather than
|
||||
storing an overlap. Outside one it is simply placed. This is the generic
|
||||
operation behind dropping a library symbol into the timeline; one-frame held
|
||||
drawing creation remains a policy of `append-drawing`/`overwrite-drawing`,
|
||||
not a different kind of container."
|
||||
[clip store sid id source-id at
|
||||
{:keys [extent point remainder-id] :or {extent :keep}}]
|
||||
(let [nodes (get-in clip [:symbols sid :nodes])
|
||||
seeded (clip/place-symbol clip store sid source-id 0 id point)
|
||||
n (get-in seeded [:symbols sid :nodes id])
|
||||
duration (when n (- (second (node/placed-span n))
|
||||
(first (node/placed-span n))))]
|
||||
(cond
|
||||
(contains? nodes id) {:refused "the new clip ID is already used"}
|
||||
(not (and (integer? at) (not (neg? at))))
|
||||
{:refused "a position is a nonnegative whole frame"}
|
||||
(nil? (clip/symbol clip source-id)) {:refused "there is no such symbol to place"}
|
||||
(nil? n) {:refused "a symbol cannot go inside itself"}
|
||||
(not (pos? duration)) {:refused "the symbol has no frames to place"}
|
||||
(and remainder-id (contains? nodes remainder-id))
|
||||
{:refused "the remainder clip needs a free ID"}
|
||||
:else
|
||||
(let [room (cleared clip sid at duration remainder-id)]
|
||||
(if (:refused room)
|
||||
room
|
||||
(let [nodes (assoc (get-in (:clip room) [:symbols sid :nodes]) id
|
||||
(assoc-in n [:time :at] at))]
|
||||
(finish (:clip room) sid nodes id extent)))))))
|
||||
|
||||
(defn adopt
|
||||
"Move an existing clip of `sid` to frame `at`, CLAIMING the time it lands on.
|
||||
|
||||
Its source, span, transforms, corrections, and identity come with it; in a lane
|
||||
the destination interval is claimed with the same trimming as a pool drop. This
|
||||
is what a body drag does, and it is why `move` and this are two commands:
|
||||
`move` refuses to disturb a neighbour, and a drag onto occupied time has
|
||||
already said it means to."
|
||||
[clip sid id at {:keys [extent remainder-id] :or {extent :keep}}]
|
||||
(let [nodes (get-in clip [:symbols sid :nodes])
|
||||
n (get nodes id)
|
||||
[lo hi] (when n (node/placed-span n))
|
||||
duration (when (and lo hi) (- hi lo))]
|
||||
(cond
|
||||
(nil? n) {:refused "select a clip to move"}
|
||||
(some? (:parent n)) {:refused "only a clip of the symbol itself is placed in its sequence"}
|
||||
(not (and (integer? at) (not (neg? at))))
|
||||
{:refused "a position is a nonnegative whole frame"}
|
||||
(not (pos? duration)) {:refused "the clip has no frames to place"}
|
||||
(and remainder-id (contains? nodes remainder-id))
|
||||
{:refused "the remainder clip needs a free ID"}
|
||||
:else
|
||||
(let [room (cleared (assoc-in clip [:symbols sid :nodes]
|
||||
(dissoc nodes id))
|
||||
sid at duration remainder-id)]
|
||||
(if (:refused room)
|
||||
room
|
||||
(let [moved (update-in n [:time :at] (fnil + 0) (- at lo))
|
||||
nodes (assoc (get-in (:clip room) [:symbols sid :nodes]) id moved)]
|
||||
(finish (:clip room) sid nodes id extent)))))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; putting drawings in a sequence
|
||||
|
||||
(defn- held
|
||||
"A one-frame held clip of `drawing-id`, starting at frame `at`.
|
||||
|
||||
Held rather than playing, and one frame rather than the length of what it
|
||||
places: a clip's duration is the sequence's business — `extend-hold` and
|
||||
`resize-out` are how it changes — and reading it off the content would make
|
||||
placing a ten-frame animation and holding its first drawing the same gesture."
|
||||
[id drawing-id at]
|
||||
{:id id :kind :instance :z (str "a-" id)
|
||||
:span [0 1] :time {:at at :rate 1}
|
||||
:source {:symbol drawing-id} :playback {:in 0 :speed 0 :end :stop}})
|
||||
|
||||
(defn- sequence-end
|
||||
"Where `sid`'s occupied frames stop."
|
||||
[nodes]
|
||||
(apply max 0 (map #(second (node/placed-span %)) (symbol/children nodes))))
|
||||
|
||||
(defn- place
|
||||
"Put a held clip of `drawing-id` into `sid` at frame `at`, and RIPPLE:
|
||||
everything starting at or after it moves later by its duration.
|
||||
|
||||
There is one placement function and `:end` is a position like any other, so
|
||||
appending is not a different operation from inserting — the end is just where
|
||||
nothing has to move. Overwriting is the other policy and is NOT this: taking
|
||||
frames away from the clip already there is trimming, which is its own command
|
||||
and not something placing a drawing should do on the quiet.
|
||||
|
||||
`:frame` in the result is where it landed, in the symbol's own frames, for a
|
||||
caller that wants to look at what it just made."
|
||||
[clip sid id drawing-id at extent ripple?]
|
||||
(let [nodes (get-in clip [:symbols sid :nodes])
|
||||
at (if (= :end at) (sequence-end nodes) at)
|
||||
n (held id drawing-id at)
|
||||
[lo hi] (node/placed-span n)
|
||||
;; RIPPLE FOLLOWS THE MODE. Pushing later siblings is what inserting
|
||||
;; into a sequence means; in a composition there is no "later sibling"
|
||||
;; to push, because being on screen together is the point.
|
||||
later (when (and ripple? (symbol/lane? (clip/symbol clip sid)))
|
||||
(filter #(>= (first (node/placed-span %)) lo) (symbol/children nodes)))
|
||||
nodes (reduce (fn [ns sibling]
|
||||
(update-in ns [(:id sibling) :time :at] (fnil + 0) (- hi lo)))
|
||||
(assoc nodes id n) later)
|
||||
result (finish clip sid nodes id extent)]
|
||||
(cond-> result
|
||||
(:clip result) (assoc :frame at))))
|
||||
|
||||
(defn- placeable
|
||||
"Why a held clip cannot go into `sid` at `at`, or nil."
|
||||
[clip sid id at]
|
||||
(let [nodes (get-in clip [:symbols sid :nodes])
|
||||
;; INSIDE a clip is not a position for another one, IN A LANE. Splitting
|
||||
;; that clip is what makes it two, and doing it here would be one command
|
||||
;; quietly performing two: the caller asks for `split` and then places.
|
||||
;; In a composition landing inside something is not a collision at all.
|
||||
inside (when (and (number? at) (symbol/lane? (clip/symbol clip sid)))
|
||||
(some (fn [n] (let [[lo hi] (node/placed-span n)]
|
||||
(when (< lo at hi) n)))
|
||||
(symbol/children nodes)))]
|
||||
(cond
|
||||
(contains? nodes id) "the new clip ID is already used"
|
||||
(not (or (= :end at) (and (integer? at) (not (neg? at)))))
|
||||
"a position is :end or a whole frame"
|
||||
inside (str "frame " at " is inside a clip; split it first"))))
|
||||
|
||||
(defn append-drawing
|
||||
"Append fresh empty content and a held clip of it. IDs come from the
|
||||
caller so a command is deterministic and replayable.
|
||||
|
||||
Fresh content, not a blank range: a sequence with no clip over a frame shows
|
||||
nothing there already, and a drawing nobody has drawn in is a different thing
|
||||
from a gap."
|
||||
[clip sid id drawing-id {:keys [at extent] :or {extent :keep at :end}}]
|
||||
(if-let [why (or (placeable clip sid id at)
|
||||
(when (clip/symbol clip drawing-id) "the new drawing ID is already used"))]
|
||||
{:refused why}
|
||||
(place (assoc-in clip [:symbols drawing-id]
|
||||
{:id drawing-id :name (name drawing-id) :fps (clip/fps clip sid) :frames 1 :nodes {}})
|
||||
sid id drawing-id at extent true)))
|
||||
|
||||
(defn reuse-drawing
|
||||
"Append a held clip of content the document ALREADY has, so the same
|
||||
drawing is exposed twice and editing it changes both clips.
|
||||
|
||||
This is the command `make-unique` is the undo of, and the reason they are two
|
||||
commands: reuse is a decision to share, and sharing is not something to
|
||||
discover later when an edit turns up somewhere else."
|
||||
[clip sid id drawing-id {:keys [at extent] :or {extent :keep at :end}}]
|
||||
(if-let [why (or (placeable clip sid id at)
|
||||
(when-not (clip/symbol clip drawing-id) "there is no such drawing to reuse")
|
||||
;; Placing something that contains this symbol would close a
|
||||
;; loop, and a sequence is no different from any other placement.
|
||||
(when (clip/contains-symbol? clip drawing-id sid)
|
||||
"a symbol cannot go inside itself"))]
|
||||
{:refused why}
|
||||
(place clip sid id drawing-id at extent true)))
|
||||
|
||||
(defn- copied
|
||||
"A copy of symbol `from`, as `{:clip :id}`.
|
||||
|
||||
SHALLOW by default: its own nodes and channels are copied, and its references
|
||||
to other symbols are kept, so a head built out of reusable eyes still uses
|
||||
those eyes. `deep?` copies everything it places as well, with new ids
|
||||
throughout, for a drawing that must share nothing — the distinction the
|
||||
shallow copy cannot make on its own, and a promise of independence that only
|
||||
the deep one keeps."
|
||||
[clip from deep?]
|
||||
(if deep?
|
||||
(let [{c :clip ids :ids} (bring/symbols clip clip [from] {})]
|
||||
{:clip c :id (ids from)})
|
||||
(let [id (clip/free-id (:symbols clip) from)]
|
||||
{:clip (assoc-in clip [:symbols id] (assoc (clip/symbol clip from) :id id))
|
||||
:id id})))
|
||||
|
||||
(defn duplicate-drawing
|
||||
"Append a held clip of a COPY of what clip `id` places, for when
|
||||
the drawing on screen is the starting point for the next one.
|
||||
|
||||
The copy is of the content only. The new clip is a plain one-frame hold
|
||||
rather than a copy of `id`'s own transform or corrections: those belong to
|
||||
that clip, and carrying them over would make duplicating a drawing quietly
|
||||
duplicate the treatment of one use of it."
|
||||
[clip sid id new-id {:keys [at extent deep?] :or {extent :keep at :end}}]
|
||||
(let [n (get-in clip [:symbols sid :nodes id])
|
||||
from (node/source n)]
|
||||
(if-let [why (or (when-not from "select a clip to duplicate")
|
||||
(when-not (clip/symbol clip from) "the drawing it places is missing")
|
||||
(placeable clip sid new-id at))]
|
||||
{:refused why}
|
||||
(let [{c :clip copy :id} (copied clip from deep?)]
|
||||
(place c sid new-id copy at extent true)))))
|
||||
|
||||
(defn overwrite-drawing
|
||||
"Put a fresh one-frame drawing at frame `at`, replacing whatever was there and
|
||||
leaving every other clip where it was.
|
||||
|
||||
This is `blank` and placement composed in ONE command and therefore one undo
|
||||
step. `remainder-id` is used only when clearing the frame cuts one clip into
|
||||
two; ids still come from the caller because this namespace is pure."
|
||||
[clip sid id drawing-id at {:keys [extent remainder-id] :or {extent :keep}}]
|
||||
(let [nodes (get-in clip [:symbols sid :nodes])]
|
||||
(if-let [why (cond
|
||||
(not (and (integer? at) (not (neg? at))))
|
||||
"a position is a nonnegative whole frame"
|
||||
(contains? nodes id) "the new clip ID is already used"
|
||||
(or (= id remainder-id) (contains? nodes remainder-id))
|
||||
"the remainder clip needs a free ID different from the new clip"
|
||||
(clip/symbol clip drawing-id) "the new drawing ID is already used")]
|
||||
{:refused why}
|
||||
(let [room (cleared clip sid at 1 remainder-id)]
|
||||
(if (:refused room)
|
||||
room
|
||||
(place (assoc-in (:clip room) [:symbols drawing-id]
|
||||
{:id drawing-id :name (name drawing-id) :fps (clip/fps clip sid) :frames 1 :nodes {}})
|
||||
sid id drawing-id at extent false))))))
|
||||
|
||||
(defn make-unique
|
||||
"Point clip `id` at a private copy of its content, leaving every other
|
||||
clip of that drawing sharing the original.
|
||||
|
||||
Refused when nothing else uses it: a drawing with one clip is already
|
||||
unique, and answering with a silent copy would leave a second identical symbol
|
||||
in the library for no reason a person could see."
|
||||
[clip sid id {:keys [deep?]}]
|
||||
(let [n (get-in clip [:symbols sid :nodes id])
|
||||
from (node/source n)
|
||||
elsewhere (for [[osid osym] (:symbols clip)
|
||||
[oid on] (:nodes osym)
|
||||
:when (and (= from (node/source on)) (not= [sid id] [osid oid]))]
|
||||
[osid oid])]
|
||||
(if-let [why (or (when-not from "select a clip to make unique")
|
||||
(when-not (clip/symbol clip from) "the drawing it places is missing")
|
||||
(when (empty? elsewhere) "nothing else uses this drawing"))]
|
||||
{:refused why}
|
||||
(let [{c :clip copy :id} (copied clip from deep?)
|
||||
c (assoc-in c [:symbols sid :nodes id :source :symbol] copy)
|
||||
ps (clip/problems c)]
|
||||
(if (seq ps) {:refused (first ps)} {:clip c :selection id})))))
|
||||
|
|
|
|||
|
|
@ -96,16 +96,25 @@
|
|||
[nodes id]
|
||||
(dec (count (lineage nodes id))))
|
||||
|
||||
(defn lane-clips
|
||||
"The symbol clips of lane `lane`, in timeline order.
|
||||
(defn children
|
||||
"The symbol's own clips — the nodes placed directly in it — in timeline order.
|
||||
|
||||
Sorted by where they START, not by `:z`: a lane's blocks follow one another in
|
||||
time, and two of them cannot be in the same place for `:z` to decide between.
|
||||
Ties go to the id so the order is the same on every run."
|
||||
[nodes lane]
|
||||
THE SYMBOL IS THE CONTAINER. There is no lane node to ask for its members: a
|
||||
symbol drawn as a lane draws THESE, and the sequence commands re-span THESE.
|
||||
See `docs/lane-is-a-view-plan.md`.
|
||||
|
||||
Sorted by where they START, not by `:z`: blocks in a sequence follow one
|
||||
another in time, and two of them cannot be in the same place for `:z` to
|
||||
decide between. Ties go to the id so the order is the same on every run.
|
||||
|
||||
A NODE WITH NO SPAN IS NOT IN THE SEQUENCE. A shape on screen for the whole
|
||||
shot has no `[in out)` to follow anything else, so it is not something an edge
|
||||
edit can trim or ripple, and a command that destructured its nil span would
|
||||
fail on the most ordinary node there is."
|
||||
[nodes]
|
||||
(->> (vals nodes)
|
||||
(filter #(= lane (:parent %)))
|
||||
(sort-by (juxt #(or (first (node/placed-span %)) 0) #(str (:id %))))
|
||||
(filter #(and (nil? (:parent %)) (node/placed-span %)))
|
||||
(sort-by (juxt #(first (node/placed-span %)) #(str (:id %))))
|
||||
vec))
|
||||
|
||||
(defn frame-map
|
||||
|
|
@ -131,61 +140,47 @@
|
|||
(<= (or (:expose t) 1) 1))
|
||||
(recur (:parent n) (conj seen id) (conj chain n)))))))
|
||||
|
||||
(defn lanes
|
||||
"The symbol's lanes, front-most first.
|
||||
(defn lane?
|
||||
"Whether symbol `sym` is DRAWN as a lane: its clips as blocks on one row,
|
||||
following one another in time and claiming it from each other.
|
||||
|
||||
SAME ORDER THE TIMELINE DRAWS ITS ROWS IN — `:z` descending, the id breaking
|
||||
ties so it is stable across runs, so \"the first lane\" means the same thing
|
||||
to the view that shows it and to commands that inspect the lane order.
|
||||
A DISPLAY HINT AND NOT A TYPE. Nothing in evaluation reads it, `problems` does
|
||||
not check it, and a symbol carrying it behaves identically on the stage — it
|
||||
says how the timeline draws the symbol and, because the editing rules follow
|
||||
the mode, which rules an edge drag inside it plays by. It is on the symbol
|
||||
rather than in editor state so that those rules are reproducible between two
|
||||
people looking at one document, and it is a field rather than something derived
|
||||
from \"the clips do not currently overlap\" because a symbol must not stop being
|
||||
a lane the moment something overlaps — that is when the rules are needed.
|
||||
|
||||
Only this symbol's own: a lane inside a nested instance belongs to that
|
||||
symbol, and aiming a drawing at it is entering it first."
|
||||
[nodes]
|
||||
(->> (vals nodes)
|
||||
(filter node/lane?)
|
||||
(sort-by (fn [n] [(or (:z n) "") (str (:id n))]))
|
||||
reverse
|
||||
vec))
|
||||
The only things allowed to read it are the timeline and `span/finish`'s
|
||||
overlap check. See `docs/lane-is-a-view-plan.md`."
|
||||
[sym]
|
||||
(= :lane (:display sym)))
|
||||
|
||||
(defn lane-problems
|
||||
"What makes a lane not a lane. A SEQUENCE is the one composition rule the node
|
||||
map carries — ordinary groups compose freely — so it is checked here, beside
|
||||
the parent and stencil references, rather than wherever a command happens to
|
||||
build one.
|
||||
(defn overlaps
|
||||
"The pairs of `sym`'s clips that are on screen over the same frames, as
|
||||
`[[a b] ...]` of ids. Empty for a symbol whose clips form a sequence.
|
||||
|
||||
Clips must be finite and non-overlapping. An accidental overlap
|
||||
is refused rather than resolved by draw order: two drawings exposed on one
|
||||
frame of one lane is a document nobody meant to write, and picking a winner
|
||||
would hide it. Empty lanes are valid — a lane is made before it is filled.
|
||||
A BUG REPORT, NOT A CONDITION TO DESIGN AROUND. In lane mode this cannot
|
||||
happen: placing claims time, so anything placed, moved or grown over occupied
|
||||
frames TRIMS what it lands on, and `span/finish` — the one commit path for
|
||||
every sequence command — refuses rather than committing one. So an overlap that
|
||||
appears anyway is a defect in a command.
|
||||
|
||||
A SOUND IS A CLIP TOO. A lane is the one temporal container, so audio sits in
|
||||
one on the same terms as picture: its own frames in `:span`, where they land
|
||||
in `:time`, and no overlap with its neighbours. What a lane may NOT hold is a
|
||||
mixture, and that is the explicit capability the model wanted rather than a
|
||||
per-frame guess: all picture or all sound, so what the lane does with the
|
||||
frame it owns is answered by the lane and not by the clip that happens to be
|
||||
under the playhead."
|
||||
[nodes]
|
||||
(vec
|
||||
(mapcat
|
||||
(fn [[id lane]]
|
||||
(when (node/lane? lane)
|
||||
(let [children (filter #(= id (:parent %)) (vals nodes))
|
||||
valid? (fn [n]
|
||||
(and (contains? #{:instance :audio} (:kind n))
|
||||
(empty? (node/problems n))
|
||||
(:span n)
|
||||
(every? node/finite-number? (node/placed-span n))))
|
||||
kept (filter valid? children)
|
||||
intervals (sort-by first (map node/placed-span kept))]
|
||||
(concat
|
||||
(for [n children :when (not (valid? n))]
|
||||
(str "sequence " id " needs finite symbol or sound clips: " (:id n)))
|
||||
(when (< 1 (count (into #{} (map :kind) kept)))
|
||||
[(str "lane " id " holds picture or sound, not both")])
|
||||
(when (some (fn [[[_ b] [c _]]] (> b c)) (partition 2 1 intervals))
|
||||
[(str "lane " id " has overlapping clips")])))))
|
||||
nodes)))
|
||||
Which is why it is NOT `clip/problems`, which means the document will not load:
|
||||
a display hint must never be able to stop a document loading, and a document
|
||||
that somehow arrives holding an overlap still opens and is drawn visibly wrong.
|
||||
Nor `clip/conflicts`, which means a person has a decision to make. This is
|
||||
neither."
|
||||
[sym]
|
||||
(let [spans (for [n (children (:nodes sym))
|
||||
:let [[lo hi] (node/placed-span n)]
|
||||
:when (and (node/finite-number? lo) (node/finite-number? hi))]
|
||||
[lo hi (:id n)])]
|
||||
(vec (for [[[_ b x] [c _ y]] (partition 2 1 (sort-by (juxt first second str) spans))
|
||||
:when (> b c)]
|
||||
[x y]))))
|
||||
|
||||
(defn order
|
||||
"Node ids in topological order: every node after its parent.
|
||||
|
|
@ -708,8 +703,12 @@
|
|||
and saved leaves point at, so renaming a symbol must not change it.
|
||||
|
||||
`:width` and `:height` are the symbol's own stage, and are absent until someone
|
||||
sets them: a symbol without them uses the clip's — see `clip/stage`."
|
||||
#{:id :name :frames :fps :width :height :nodes :palette})
|
||||
sets them: a symbol without them uses the clip's — see `clip/stage`.
|
||||
|
||||
`:display` is how the TIMELINE draws the symbol — `:lane` for its clips as
|
||||
blocks on one row — and is saved because two people editing one document must
|
||||
play by the same editing rules. See `lane?`."
|
||||
#{:id :name :frames :fps :width :height :nodes :palette :display})
|
||||
|
||||
(defn problems
|
||||
"Human-readable reasons this symbol will not evaluate. Empty means it will.
|
||||
|
|
@ -727,7 +726,6 @@
|
|||
(if-not (map? nodes)
|
||||
[":nodes must be a map of id -> node"]
|
||||
(-> []
|
||||
(into (lane-problems nodes))
|
||||
(into (for [[id n] nodes
|
||||
:when (not= id (:id n))]
|
||||
(str "node under key " (pr-str id) " has :id " (pr-str (:id n)))))
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
address of every block this produces."
|
||||
(:require [arthur.domain.bring :as bring]
|
||||
[arthur.domain.clip :as clip]
|
||||
[arthur.domain.lane :as lane]
|
||||
[arthur.domain.span :as span]
|
||||
[arthur.events.edit :as edit]
|
||||
[arthur.events.ui :as ui]
|
||||
[arthur.events.playback :as pb]
|
||||
|
|
@ -497,9 +497,8 @@
|
|||
|
||||
(rf/reg-event-fx
|
||||
::converted
|
||||
;; A TAKE IS A CLIP IN A LANE, like everything else that enters the timeline.
|
||||
;; It used to be placed straight into the open symbol as a row of its own,
|
||||
;; which was the one way to get temporal content that no lane owned.
|
||||
;; A TAKE IS A CLIP LIKE ANY OTHER, placed into whichever symbol the drop names
|
||||
;; — see `ui/drop-destination` — rather than into a container invented for it.
|
||||
(fn [{:keys [db]} [_ {{:keys [name frame point range target]} :request footage-id :footage-id}
|
||||
built]]
|
||||
(let [entry (store/entry (:clip/current db))
|
||||
|
|
@ -510,10 +509,10 @@
|
|||
st (merge (:store entry) (:store built))
|
||||
imported-frames (clip/output-frames clip sid)
|
||||
source-fps (get-in built [:clip :fps])
|
||||
where (ui/lane-destination db clip st frame target :picture)
|
||||
where (ui/drop-destination db clip st frame target)
|
||||
result (if (:refused where)
|
||||
where
|
||||
(lane/place-symbol (:clip where) st (:sid where) (:lane-id where)
|
||||
(span/place-symbol (:clip where) st (:sid where)
|
||||
uuid sid (:at where)
|
||||
{:extent :grow-symbol :point point
|
||||
:remainder-id (random-uuid)}))]
|
||||
|
|
@ -528,9 +527,6 @@
|
|||
:source-inputs]))))]
|
||||
{:db (-> db
|
||||
(update :ui dissoc :convert)
|
||||
(cond-> (:made? where)
|
||||
(assoc-in [:ui :target] {:sid (:sid where) :id (:lane-id where)
|
||||
:path [(:lane-id where)]}))
|
||||
(assoc-in [:ui :selection]
|
||||
[:node (:sid where) uuid (conj (vec (:path where)) uuid)])
|
||||
(update :footage merge
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@
|
|||
(:require [arthur.db :as db]
|
||||
[arthur.domain.bring :as bring]
|
||||
[arthur.domain.clip :as clip]
|
||||
[arthur.domain.span :as span]
|
||||
[arthur.domain.leaf :as leaf]
|
||||
[arthur.domain.node :as node]
|
||||
[arthur.events.edit :as edit]
|
||||
|
|
@ -35,7 +36,6 @@
|
|||
[arthur.events.footage :as footage]
|
||||
[arthur.events.playback :as pb]
|
||||
[arthur.events.ui :as ui]
|
||||
[arthur.domain.lane :as lane]
|
||||
[arthur.footage.store :as store]
|
||||
[arthur.flow.address :as address]
|
||||
[arthur.flow.ingest :as ingest]
|
||||
|
|
@ -171,21 +171,18 @@
|
|||
uuid (random-uuid)
|
||||
{:keys [clip ids]} (bring/symbols (:clip entry) (:clip other) [sid] {})
|
||||
st (merge (:store entry) (:store other))
|
||||
;; A symbol from another project arrives as a clip in a lane, the same
|
||||
;; A symbol from another project arrives as an ordinary clip, the same
|
||||
;; as one from this project's pool.
|
||||
where (ui/lane-destination db clip st frame target :picture)
|
||||
where (ui/drop-destination db clip st frame target)
|
||||
result (if (:refused where)
|
||||
where
|
||||
(lane/place-symbol (:clip where) st (:sid where) (:lane-id where)
|
||||
(span/place-symbol (:clip where) st (:sid where)
|
||||
uuid (ids sid) (:at where)
|
||||
{:extent :grow-symbol :point point
|
||||
:remainder-id (random-uuid)}))]
|
||||
(if-let [why (or (:refused where) (:refused result))]
|
||||
{:db (update db :project merge {:status why})}
|
||||
{:db (-> (edit/edit-entry db #(assoc % :clip (:clip result) :store st))
|
||||
(cond-> (:made? where)
|
||||
(assoc-in [:ui :target] {:sid (:sid where) :id (:lane-id where)
|
||||
:path [(:lane-id where)]}))
|
||||
(assoc-in [:ui :selection]
|
||||
[:node (:sid where) uuid (conj (vec (:path where)) uuid)])
|
||||
(update :project merge {:status (str "brought in " label)}))
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@
|
|||
[arthur.domain.gesture :as gesture]
|
||||
[arthur.domain.nest :as nest]
|
||||
[arthur.domain.node :as node]
|
||||
[arthur.domain.lane :as lane]
|
||||
[arthur.domain.span :as span]
|
||||
[arthur.domain.symbol :as symbol]
|
||||
[arthur.events.edit :as edit]
|
||||
|
|
@ -56,7 +55,7 @@
|
|||
[:clip :symbols sid :nodes id :kind]))]
|
||||
(cond-> (-> db
|
||||
(assoc-in [:ui :selection] selection)
|
||||
(update :ui dissoc :points :lane-retry))
|
||||
(update :ui dissoc :points :retry))
|
||||
(and (= :node kind) path (not sound?))
|
||||
(update-in [:ui :expanded] (fnil into #{}) (rest (reductions conj [] (pop path)))))))
|
||||
|
||||
|
|
@ -97,46 +96,26 @@
|
|||
::set-tone
|
||||
(fn [db [_ tone]] (assoc-in db [:ui :tone] tone)))
|
||||
|
||||
(defn- aimed-lane
|
||||
"The lane the target names, or nil — including nil for a target aimed at a cel
|
||||
INSIDE a lane, which is the lane's business and not a lane itself."
|
||||
[clip db]
|
||||
(let [{:keys [sid id path]} (get-in db [:ui :target])
|
||||
n (when id (get-in clip [:symbols sid :nodes id]))]
|
||||
(when (and n (node/lane? n) (= 1 (count path))
|
||||
(= sid (get-in db [:ui :open])))
|
||||
n)))
|
||||
|
||||
|
||||
(defn empty-lane
|
||||
"The id of a lane of `sid` that has nothing in it, or nil.
|
||||
|
||||
EVERY SYMBOL IS BORN WITH A LANE, so the first thing put into one goes
|
||||
there instead of beside it. An OCCUPIED lane is never chosen this way:
|
||||
placing claims time, so taking a lane nobody pointed at would trim or delete
|
||||
what was already in it. That is a fine thing to ask for and not a fine thing
|
||||
to assume."
|
||||
[document sid]
|
||||
(let [nodes (get-in document [:symbols sid :nodes])]
|
||||
(first (keep (fn [lane]
|
||||
(when (empty? (symbol/lane-clips nodes (:id lane))) (:id lane)))
|
||||
(symbol/lanes nodes)))))
|
||||
|
||||
(defn apply-lane-command
|
||||
(defn apply-command
|
||||
"Commit a successful domain command as one history step. A refused command
|
||||
leaves the document and history untouched; an overflow offers an explicit retry."
|
||||
leaves the document and history untouched; an overflow offers an explicit retry.
|
||||
|
||||
A COMMAND NEED NOT NAME A SELECTION. Emptying frames deletes what was selected
|
||||
and has nothing sensible to put in its place, so a nil `:selection` leaves the
|
||||
selection alone rather than pointing it at a clip nobody asked for."
|
||||
[db sid result retry]
|
||||
(if-let [why (:refused result)]
|
||||
(-> db
|
||||
(assoc-in [:project :status] why)
|
||||
(assoc-in [:ui :lane-retry]
|
||||
(when (:required-frames result) retry)))
|
||||
(assoc-in [:ui :retry] (when (:required-frames result) retry)))
|
||||
(let [[_ selected-sid _ path] (get-in db [:ui :selection])
|
||||
prefix (if (and (= sid selected-sid) (seq path)) (pop path) [])]
|
||||
(-> db
|
||||
(cond-> (-> db
|
||||
(edit/transaction (constantly (:clip result)))
|
||||
(assoc-in [:ui :selection] [:node sid (:selection result) (conj prefix (:selection result))])
|
||||
(update :ui dissoc :lane-retry)))))
|
||||
(update :ui dissoc :retry))
|
||||
(:selection result)
|
||||
(assoc-in [:ui :selection]
|
||||
[:node sid (:selection result) (conj prefix (:selection result))])))))
|
||||
|
||||
(defn apply-correction-command
|
||||
"Commit one correction command while keeping the complete row address that
|
||||
|
|
@ -170,17 +149,19 @@
|
|||
db (correction/retry-layer clip sid id path layer-id)))))
|
||||
|
||||
(rf/reg-event-db
|
||||
::new-lane
|
||||
;; AND AIMED AT, because the reason to make a lane is to draw in it: `add-lane`
|
||||
;; returns the new lane as its selection, and `apply-lane-command` has already
|
||||
;; worked out the row path that addresses it.
|
||||
(fn [db _]
|
||||
::draw-as-lane
|
||||
;; THE ONE PLACE A PERSON CAN ASK FOR THE IMPOSSIBLE. Everything else maintains
|
||||
;; the sequence; this asks a symbol whose clips may already overlap to start
|
||||
;; being one. `span/draw-as-lane` refuses and says what it would have to trim,
|
||||
;; and `::retry` is the one button that says do it.
|
||||
(fn [db [_ sid on? trim?]]
|
||||
(let [clip (:clip (store/entry (:clip/current db)))
|
||||
sid (get-in db [:ui :open])
|
||||
after (apply-lane-command db sid (lane/add-lane clip sid (random-uuid)) nil)]
|
||||
(cond-> after
|
||||
(not= (get-in after [:ui :selection]) (get-in db [:ui :selection]))
|
||||
(assoc-in [:ui :target] (target-of (get-in after [:ui :selection])))))))
|
||||
result (span/draw-as-lane clip sid on? {:trim? trim?})]
|
||||
(if-let [why (:refused result)]
|
||||
(-> db (assoc-in [:project :status] why)
|
||||
(assoc-in [:ui :retry] (when (:required-trim result) [::draw-as-lane sid on? true])))
|
||||
(-> db (edit/transaction (constantly (:clip result)))
|
||||
(update :ui dissoc :retry))))))
|
||||
|
||||
(defn- committed
|
||||
"One appending command, as effects: commit it, and look at what it made.
|
||||
|
|
@ -195,23 +176,17 @@
|
|||
{:keys [at rate]} (:time (nest/inside clip st (get-in db [:ui :open])
|
||||
(if (seq path) (pop path) [])
|
||||
(get-in db [:playback :frame])))]
|
||||
(cond-> {:db (apply-lane-command db sid result retry)}
|
||||
(cond-> {:db (apply-command db sid result retry)}
|
||||
(and (:clip result) (:frame result) rate)
|
||||
(assoc :dispatch [::playback/seek (+ at (/ (:frame result) rate))]))))
|
||||
|
||||
(defn- selected-lane
|
||||
"The lane a command should act in: the selected lane itself, or the one
|
||||
holding the selected cel."
|
||||
[clip sid id]
|
||||
(let [n (get-in clip [:symbols sid :nodes id])]
|
||||
(if (node/lane? n) id (:parent n))))
|
||||
|
||||
(defn selection-frame
|
||||
"The playhead as a frame of the symbol that owns `selection`.
|
||||
|
||||
A timeline selection carries its path from the open symbol. Walking to the
|
||||
parent of the selected node crosses every enclosing instance clock before a
|
||||
lane command converts that owning-symbol frame into lane time."
|
||||
parent of the selected node crosses every enclosing instance clock, and that
|
||||
is the whole of it: a clip of a lane has no parent, so the symbol's own frames
|
||||
ARE the sequence's. One clock where there used to be two."
|
||||
[clip st open selection frame]
|
||||
(let [[_ sid _ path] selection]
|
||||
(if (or (= sid open) (not (seq path)))
|
||||
|
|
@ -223,8 +198,7 @@
|
|||
(fn [{:keys [db]} [_ extent]]
|
||||
(let [clip (:clip (store/entry (:clip/current db)))
|
||||
[_ sid id] (get-in db [:ui :selection])
|
||||
result (lane/append-drawing clip sid (selected-lane clip sid id)
|
||||
(random-uuid) (clip/fresh-id clip)
|
||||
result (span/append-drawing clip sid (random-uuid) (clip/fresh-id clip)
|
||||
{:extent (or extent :keep)})]
|
||||
(committed db sid result [::append-drawing :grow-symbol]))))
|
||||
|
||||
|
|
@ -233,7 +207,7 @@
|
|||
(fn [{:keys [db]} [_ extent]]
|
||||
(let [clip (:clip (store/entry (:clip/current db)))
|
||||
[_ sid id] (get-in db [:ui :selection])
|
||||
result (lane/reuse-drawing clip sid (selected-lane clip sid id) (random-uuid)
|
||||
result (span/reuse-drawing clip sid (random-uuid)
|
||||
(node/source (get-in clip [:symbols sid :nodes id]))
|
||||
{:extent (or extent :keep)})]
|
||||
(committed db sid result [::reuse-drawing :grow-symbol]))))
|
||||
|
|
@ -243,27 +217,26 @@
|
|||
(fn [{:keys [db]} [_ extent deep?]]
|
||||
(let [clip (:clip (store/entry (:clip/current db)))
|
||||
[_ sid id] (get-in db [:ui :selection])
|
||||
result (lane/duplicate-drawing clip sid id (random-uuid)
|
||||
result (span/duplicate-drawing clip sid id (random-uuid)
|
||||
{:extent (or extent :keep) :deep? deep?})]
|
||||
(committed db sid result [::duplicate-drawing :grow-symbol deep?]))))
|
||||
|
||||
(rf/reg-event-fx
|
||||
::insert-drawing
|
||||
;; The playhead is the position: you scrub to where the drawing goes. A lane
|
||||
;; that is stepped or retimed off whole frames has no single lane frame for a
|
||||
;; symbol frame, and `lane-frame` says so rather than snapping to one.
|
||||
;; The playhead is the position: you scrub to where the drawing goes. The
|
||||
;; symbol's own frames are the sequence's, so there is nothing to convert —
|
||||
;; only an enclosing stepped or looping instance can leave the playhead on no
|
||||
;; single frame of it, which `selection-frame` says by answering nil.
|
||||
(fn [{:keys [db]} [_ extent]]
|
||||
(let [{clip :clip st :store} (store/entry (:clip/current db))
|
||||
selection (get-in db [:ui :selection])
|
||||
[_ sid id] selection
|
||||
lane (selected-lane clip sid id)
|
||||
owner-frame (selection-frame clip st (get-in db [:ui :open]) selection
|
||||
[_ sid] selection
|
||||
at (selection-frame clip st (get-in db [:ui :open]) selection
|
||||
(get-in db [:playback :frame]))
|
||||
at (when (number? owner-frame) (lane/lane-frame clip sid lane owner-frame))
|
||||
result (if at
|
||||
(lane/append-drawing clip sid lane (random-uuid) (clip/fresh-id clip)
|
||||
result (if (integer? at)
|
||||
(span/append-drawing clip sid (random-uuid) (clip/fresh-id clip)
|
||||
{:at at :extent (or extent :keep)})
|
||||
{:refused "this lane's frames are not the open symbol's"})]
|
||||
{:refused "the playhead is not on one frame of this symbol"})]
|
||||
(committed db sid result [::insert-drawing :grow-symbol]))))
|
||||
|
||||
(defn- at-playhead
|
||||
|
|
@ -292,7 +265,7 @@
|
|||
::split
|
||||
(fn [db _]
|
||||
(let [{:keys [clip sid id at]} (at-playhead db)]
|
||||
(apply-lane-command
|
||||
(apply-command
|
||||
db sid (if at (span/split clip sid id at (random-uuid)) no-frame) nil))))
|
||||
|
||||
(rf/reg-event-fx
|
||||
|
|
@ -300,30 +273,28 @@
|
|||
(fn [{:keys [db]} [_ extent]]
|
||||
(let [{clip :clip st :store} (store/entry (:clip/current db))
|
||||
selection (get-in db [:ui :selection])
|
||||
[_ sid id] selection
|
||||
lane-id (selected-lane clip sid id)
|
||||
owner-frame (selection-frame clip st (get-in db [:ui :open]) selection
|
||||
[_ sid] selection
|
||||
at (selection-frame clip st (get-in db [:ui :open]) selection
|
||||
(get-in db [:playback :frame]))
|
||||
at (when (number? owner-frame) (lane/lane-frame clip sid lane-id owner-frame))
|
||||
result (if (integer? at)
|
||||
(lane/overwrite-drawing clip sid lane-id (random-uuid) (clip/fresh-id clip) at
|
||||
(span/overwrite-drawing clip sid (random-uuid) (clip/fresh-id clip) at
|
||||
{:extent (or extent :keep)
|
||||
:remainder-id (random-uuid)})
|
||||
{:refused "this lane's frames are not the open symbol's"})]
|
||||
{:refused "the playhead is not on one frame of this symbol"})]
|
||||
(committed db sid result [::overwrite-drawing :grow-symbol]))))
|
||||
|
||||
(rf/reg-event-db
|
||||
::trim
|
||||
(fn [db [_ edge]]
|
||||
(let [{:keys [clip sid id at]} (at-playhead db)]
|
||||
(apply-lane-command
|
||||
(apply-command
|
||||
db sid (if at (span/trim clip sid id edge at) no-frame) nil))))
|
||||
|
||||
(rf/reg-event-db
|
||||
::move
|
||||
(fn [db _]
|
||||
(let [{:keys [clip sid id at]} (at-playhead db)]
|
||||
(apply-lane-command
|
||||
(apply-command
|
||||
db sid (if at (span/move clip sid id at) no-frame) nil))))
|
||||
|
||||
(rf/reg-event-db
|
||||
|
|
@ -333,10 +304,10 @@
|
|||
(fn [db _]
|
||||
(let [{:keys [clip sid node]} (at-playhead db)
|
||||
span (node/placed-span node)]
|
||||
(apply-lane-command
|
||||
(apply-command
|
||||
db sid (if (and span (every? integer? span))
|
||||
(lane/blank clip sid (:parent node) span {})
|
||||
{:refused "select a cel that starts and ends on whole lane frames"})
|
||||
(span/blank clip sid span {})
|
||||
{:refused "select a clip that starts and ends on whole frames"})
|
||||
nil))))
|
||||
|
||||
(rf/reg-event-db
|
||||
|
|
@ -344,21 +315,21 @@
|
|||
(fn [db [_ deep?]]
|
||||
(let [clip (:clip (store/entry (:clip/current db)))
|
||||
[_ sid id] (get-in db [:ui :selection])]
|
||||
(apply-lane-command db sid (lane/make-unique clip sid id {:deep? deep?}) nil))))
|
||||
(apply-command db sid (span/make-unique clip sid id {:deep? deep?}) nil))))
|
||||
|
||||
(rf/reg-event-db
|
||||
::extend-hold
|
||||
(fn [db [_ delta extent]]
|
||||
(let [clip (:clip (store/entry (:clip/current db)))
|
||||
[_ sid id] (get-in db [:ui :selection])
|
||||
result (lane/extend-hold clip sid id delta {:extent (or extent :keep)})]
|
||||
(apply-lane-command db sid result [::extend-hold delta :grow-symbol]))))
|
||||
result (span/extend-hold clip sid id delta {:extent (or extent :keep)})]
|
||||
(apply-command db sid result [::extend-hold delta :grow-symbol]))))
|
||||
|
||||
(rf/reg-event-fx
|
||||
::lane-retry
|
||||
::retry
|
||||
(fn [{:keys [db]} _]
|
||||
(if-let [event (get-in db [:ui :lane-retry])]
|
||||
{:db (update db :ui dissoc :lane-retry) :dispatch event}
|
||||
(if-let [event (get-in db [:ui :retry])]
|
||||
{:db (update db :ui dissoc :retry) :dispatch event}
|
||||
{})))
|
||||
|
||||
|
||||
|
|
@ -436,6 +407,19 @@
|
|||
(= :instance (get-in clip [:symbols sid :nodes id :kind])) path
|
||||
:else (vec (butlast path)))))
|
||||
|
||||
(defn aimed-symbol
|
||||
"The symbol a command acts in and the path of rows down to it: `{:sid :path}`.
|
||||
|
||||
INSIDE the aimed instance, beside an aimed node of any other kind, the open
|
||||
symbol when nothing is aimed — which is `where-new-goes`, resolved. There is
|
||||
no lane to aim at any more: a lane is how a symbol is DRAWN, so what a gesture
|
||||
names is a symbol, and whether that symbol is drawn as a lane is a separate
|
||||
question `symbol/lane?` answers."
|
||||
[clip st db frame]
|
||||
(let [open (get-in db [:ui :open])
|
||||
down (where-new-goes clip db)]
|
||||
(assoc (nest/inside clip st open down frame) :path down)))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; drawing a polygon
|
||||
;;
|
||||
|
|
@ -456,10 +440,10 @@
|
|||
(update-in db [:ui :draft] into [x y])
|
||||
db)))
|
||||
|
||||
(defn into-the-lane
|
||||
"Where a polygon drawn with a lane aimed goes: the path of the held clip the
|
||||
lane exposes at the playhead, and the document containing it. `{:clip :path}`,
|
||||
or `{:refused why}`.
|
||||
(defn into-the-sequence
|
||||
"Where a polygon drawn into a symbol DRAWN AS A LANE goes: the path of the held
|
||||
clip that symbol exposes at the playhead, and the document containing it.
|
||||
`{:clip :path}`, or `{:refused why}`.
|
||||
|
||||
A GAP IS NOT A REFUSAL, IT IS A NEW DRAWING. The frame under the playhead is
|
||||
where the drawing belongs, so drawing on an empty frame makes the one-frame
|
||||
|
|
@ -467,47 +451,46 @@
|
|||
controls used to say.
|
||||
|
||||
`overwrite-drawing` rather than `append-drawing`, because a gap already has
|
||||
the room. Appending RIPPLES everything after it later by the new cel's
|
||||
the room. Appending RIPPLES everything after it later by the new clip's
|
||||
duration — that is what `insert` means, and it is a different intention.
|
||||
Placed into a gap, overwrite clears nothing and moves nobody.
|
||||
|
||||
A CEL'S PATH IS ONE STEP. `[cel]` and not `[lane cel]`: within one symbol the
|
||||
rows are flat and a cel is not a row at all, which is the same addressing
|
||||
`rows` hands the timeline and `trail` reads back."
|
||||
[clip st db lane]
|
||||
(let [open (get-in db [:ui :open])
|
||||
lane-id (:id lane)
|
||||
owner (:frame (nest/inside clip st open [] (get-in db [:playback :frame])))
|
||||
at (when (number? owner) (lane/lane-frame clip open lane-id owner))
|
||||
cel (when (integer? at)
|
||||
THE CLIP'S PATH IS THE SYMBOL'S PATH PLUS ONE STEP, because a clip of a lane
|
||||
is an ordinary row of the symbol holding it — which is the addressing `rows`
|
||||
hands the timeline and `trail` reads back."
|
||||
[clip db {:keys [sid frame path]}]
|
||||
(let [exposed (when (integer? frame)
|
||||
(some (fn [c] (let [[lo hi] (node/placed-span c)]
|
||||
(when (and (<= lo at) (< at hi)) c)))
|
||||
(symbol/lane-clips (get-in clip [:symbols open :nodes]) lane-id)))]
|
||||
(when (and (<= lo frame) (< frame hi)) c)))
|
||||
(symbol/children (get-in clip [:symbols sid :nodes]))))]
|
||||
(cond
|
||||
(not (integer? at))
|
||||
{:refused "the playhead is not on one frame of this lane"}
|
||||
cel {:clip clip :path [(:id cel)]}
|
||||
(not (integer? frame))
|
||||
{:refused "the playhead is not on one frame of this symbol"}
|
||||
exposed {:clip clip :path (conj (vec path) (:id exposed))}
|
||||
:else
|
||||
(let [cel-id (random-uuid)
|
||||
made (lane/overwrite-drawing clip open lane-id cel-id (clip/fresh-id clip) at
|
||||
made (span/overwrite-drawing clip sid cel-id (clip/fresh-id clip) frame
|
||||
{:extent :grow-symbol
|
||||
:remainder-id (random-uuid)})]
|
||||
(if (:refused made) made {:clip (:clip made) :path [cel-id]})))))
|
||||
(if (:refused made) made {:clip (:clip made) :path (conj (vec path) cel-id)})))))
|
||||
|
||||
(defn polygon-landing
|
||||
"Choose the document and row path a finished polygon is drawn into.
|
||||
|
||||
An aimed lane wins regardless of what is selected on the stage: an
|
||||
occupied frame lands in its cel and a gap first becomes a one-frame drawing.
|
||||
With no lane aimed, ordinary target-based drawing is unchanged."
|
||||
WHERE IT GOES IS A SYMBOL, and what happens there follows how that symbol is
|
||||
DRAWN: in one drawn as a lane an occupied frame lands in its clip and a gap
|
||||
first becomes a one-frame drawing, because the sequence is what says which
|
||||
drawing the playhead is on. In an ordinary composition the polygon goes
|
||||
straight into the symbol, as it always did."
|
||||
[clip st db]
|
||||
(if-let [lane (aimed-lane clip db)]
|
||||
(assoc (into-the-lane clip st db lane) :lane? true)
|
||||
{:clip clip :path (where-new-goes clip db) :lane? false}))
|
||||
(let [into (aimed-symbol clip st db (get-in db [:playback :frame]))]
|
||||
(if (and (:sid into) (symbol/lane? (clip/symbol clip (:sid into))))
|
||||
(assoc (into-the-sequence clip db into) :lane? true)
|
||||
{:clip clip :path (:path into) :lane? false})))
|
||||
|
||||
(defn beginning-polygon
|
||||
"Enter polygon mode, first materializing a drawing at the playhead when a
|
||||
lane is aimed and that frame is empty.
|
||||
"Enter polygon mode, first materializing a drawing at the playhead when the
|
||||
symbol it lands in is drawn as a lane and that frame is empty.
|
||||
|
||||
Creating here, rather than when the polygon is finished, means the drawing is
|
||||
already the current cel while points are being placed. Cancelling the polygon
|
||||
|
|
@ -558,7 +541,7 @@
|
|||
(nil? sid)
|
||||
(update db :project merge
|
||||
{:status (if (:lane? landing)
|
||||
"this lane is not on screen at this frame"
|
||||
"this sequence is not on screen at this frame"
|
||||
"what you are drawing into is not on screen at this frame")})
|
||||
;; `random-uuid` is the one impurity in this namespace, and it is here
|
||||
;; rather than in `domain/paint` for the reason `clip/place-symbol` spells
|
||||
|
|
@ -588,57 +571,61 @@
|
|||
(declare record-auto-frame)
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; a new symbol
|
||||
;; a new symbol or lane
|
||||
|
||||
(defn- create-container
|
||||
"Create and place one empty symbol. Lane creation is explicit: `lane?` marks
|
||||
the new symbol for linear timeline display; ordinary symbol creation never
|
||||
does. Both operations place the new symbol through the same span command, so
|
||||
an explicitly aimed lane still applies its claim-time rule."
|
||||
[db where lane?]
|
||||
(let [{document :clip st :store} (store/entry (:clip/current db))
|
||||
open (get-in db [:ui :open])
|
||||
frame (get-in db [:playback :frame])
|
||||
into (if (= :top where)
|
||||
(assoc (nest/inside document st open [] frame) :path [])
|
||||
(aimed-symbol document st db frame))
|
||||
{host :sid at :frame down :path} into
|
||||
sid (clip/fresh-id document)
|
||||
instance-id (random-uuid)
|
||||
end (clip/frames document host)
|
||||
result (cond
|
||||
(nil? host)
|
||||
{:refused "what you are adding to is not on screen at this frame"}
|
||||
(not (integer? at))
|
||||
{:refused "the playhead is not on one frame of this symbol"}
|
||||
(or (nil? end) (>= at end))
|
||||
{:refused "the playhead is past the end of this symbol"}
|
||||
:else
|
||||
(let [new-symbol (cond-> {:id sid :name (name sid)
|
||||
:fps (clip/fps document host)
|
||||
:frames (- end at) :nodes {}}
|
||||
lane? (assoc :display :lane))
|
||||
seeded (assoc-in document [:symbols sid] new-symbol)]
|
||||
(span/place-symbol seeded st host instance-id sid at
|
||||
{:extent :grow-symbol
|
||||
:remainder-id (random-uuid)})))]
|
||||
(if-let [why (:refused result)]
|
||||
(update db :project merge {:status why})
|
||||
(let [selection [:node host instance-id (conj (vec down) instance-id)]]
|
||||
(cond-> (-> db
|
||||
(edit/transaction (constantly (:clip result)))
|
||||
(assoc-in [:ui :selection] selection)
|
||||
(update-in [:ui :expanded] (fnil into #{})
|
||||
(rest (reductions conj [] down))))
|
||||
lane? (assoc-in [:ui :target] (target-of selection)))))))
|
||||
|
||||
(rf/reg-event-db
|
||||
::new-symbol
|
||||
;; `where` is `:inside` — in whatever the target names — or `:top`, in the open
|
||||
;; symbol regardless of it. A lane is the temporal form of this same operation:
|
||||
;; inside it, a new empty symbol is a one-frame held clip at the playhead.
|
||||
(fn [db [_ where]]
|
||||
(let [{clip :clip st :store} (store/entry (:clip/current db))
|
||||
target-lane (when (= :inside where) (aimed-lane clip db))
|
||||
open (get-in db [:ui :open])
|
||||
owner (:frame (nest/inside clip st open [] (get-in db [:playback :frame])))
|
||||
at (when (and target-lane (number? owner))
|
||||
(lane/lane-frame clip open (:id target-lane) owner))
|
||||
drawing-id (clip/fresh-id clip)
|
||||
cel-id (random-uuid)
|
||||
down (if (= :top where) [] (where-new-goes clip db))
|
||||
{host :sid frame :frame} (nest/inside clip st open down
|
||||
(get-in db [:playback :frame]))
|
||||
lane-id (random-uuid)]
|
||||
(if target-lane
|
||||
(let [result (if (integer? at)
|
||||
(lane/overwrite-drawing clip open (:id target-lane) cel-id drawing-id at
|
||||
{:extent :grow-symbol
|
||||
:remainder-id (random-uuid)})
|
||||
{:refused "the playhead is not on one frame of this lane"})]
|
||||
(if-let [why (:refused result)]
|
||||
(update db :project merge {:status why})
|
||||
(-> db
|
||||
(edit/transaction (constantly (:clip result)))
|
||||
(assoc-in [:ui :selection] [:node open cel-id [cel-id]]))))
|
||||
(if-not host
|
||||
(update db :project merge
|
||||
{:status "what you are adding to is not on screen at this frame"})
|
||||
(let [free (empty-lane clip host)
|
||||
lane-id (or free lane-id)
|
||||
prepared (if free {:clip clip} (lane/add-lane clip host lane-id))
|
||||
result (if (:refused prepared) prepared
|
||||
(lane/overwrite-drawing (:clip prepared) host lane-id
|
||||
cel-id drawing-id frame
|
||||
{:extent :grow-symbol
|
||||
:remainder-id (random-uuid)}))]
|
||||
(if-let [why (:refused result)]
|
||||
(update db :project merge {:status why})
|
||||
(-> db
|
||||
(edit/transaction (constantly (:clip result)))
|
||||
(assoc-in [:ui :selection] [:node host cel-id (conj down cel-id)])
|
||||
(assoc-in [:ui :target]
|
||||
{:sid host :id lane-id :path (conj down lane-id)})
|
||||
(update-in [:ui :expanded] (fnil into #{})
|
||||
(rest (reductions conj [] down)))))))))))
|
||||
(create-container db where false)))
|
||||
|
||||
(rf/reg-event-db
|
||||
::new-lane
|
||||
(fn [db _]
|
||||
;; A lane is an explicit top-level track of the open symbol. It must not
|
||||
;; become nested merely because the previously created lane is still aimed.
|
||||
(create-container db :top true)))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; a drop in flight
|
||||
|
|
@ -659,85 +646,74 @@
|
|||
::drop-clear
|
||||
(fn [db _] (update db :ui dissoc :drop)))
|
||||
|
||||
(defn lane-destination
|
||||
"Where a drop lands: the lane it was aimed at, the selected one, or a new one.
|
||||
(defn drop-destination
|
||||
"Which symbol a drop lands in and on which of its frames: `{:clip :sid :at
|
||||
:path}`, or `{:refused why}`.
|
||||
|
||||
EVERYTHING IN THE TIMELINE IS A LANE, so this is the one rule and every drop
|
||||
asks it — a symbol from the pool, a sound, and a video brought in as a take
|
||||
alike. It answers `{:clip :sid :lane-id :at :made?}` with the lane already
|
||||
created in `:clip` when it had to make one, or `{:refused why}`.
|
||||
ONE RULE AND EVERY DROP ASKS IT — a symbol from the pool, a sound, and a video
|
||||
brought in as a take alike. The pointer names a ROW, `target`, and a row leads
|
||||
into a symbol exactly where it names an instance, which is what `nest/inside`
|
||||
already answers; with nothing under the pointer it is the open symbol. Nothing
|
||||
is created to receive the drop: the symbol IS the container, so the first drop
|
||||
into one is the same operation as the second.
|
||||
|
||||
`kind` is what is about to go in, `:picture` or `:sound`: a lane holds one or
|
||||
the other, so an aimed lane that holds the other kind is not the destination
|
||||
and a new one is made beside it."
|
||||
[db document st frame target kind]
|
||||
(let [open (get-in db [:ui :open])
|
||||
selected (get-in db [:ui :selection])
|
||||
[_ sel-sid sel-id] selected
|
||||
holds (fn [[_ sid id]]
|
||||
(let [nodes (get-in document [:symbols sid :nodes])]
|
||||
(when (node/lane? (get nodes id))
|
||||
(let [kinds (into #{} (map :kind) (symbol/lane-clips nodes id))]
|
||||
(or (empty? kinds)
|
||||
(= kinds #{(if (= :sound kind) :audio :instance)}))))))
|
||||
;; With nothing aimed, an empty lane that is already there — see
|
||||
;; `empty-lane` — and only then a new one.
|
||||
free (when-let [id (empty-lane document open)]
|
||||
(when (holds [:node open id]) [:node open id [id]]))
|
||||
aimed (or (when (and target (holds target)) target)
|
||||
(when (and (= :node (first selected)) (holds selected)) selected)
|
||||
free)
|
||||
[_ aimed-sid aimed-id aimed-path] aimed
|
||||
sid (if aimed aimed-sid open)
|
||||
lane-id (if aimed aimed-id (random-uuid))
|
||||
prepared (if aimed {:clip document} (lane/add-lane document sid lane-id))
|
||||
lane-sel [:node sid lane-id (if aimed aimed-path [lane-id])]
|
||||
owner (when-not (:refused prepared)
|
||||
(selection-frame (:clip prepared) st open lane-sel frame))
|
||||
at (when (number? owner)
|
||||
(lane/lane-frame (:clip prepared) sid lane-id owner))]
|
||||
`:clip` is handed back unchanged and is in the result only so the callers that
|
||||
used to be given a document with a freshly made lane in it go on reading one
|
||||
thing."
|
||||
[db document st frame target]
|
||||
(let [target (if (vector? target)
|
||||
(let [[_ sid id path] target]
|
||||
{:sid sid :id id :path path})
|
||||
target)
|
||||
open (get-in db [:ui :open])
|
||||
path (cond
|
||||
(nil? target) []
|
||||
(= :instance (get-in document [:symbols (:sid target)
|
||||
:nodes (:id target) :kind]))
|
||||
(vec (:path target))
|
||||
:else (vec (butlast (:path target))))
|
||||
{:keys [sid] at :frame} (nest/inside document st open path frame)]
|
||||
(cond
|
||||
(:refused prepared) prepared
|
||||
(not (integer? at)) {:refused "the drop is not on one frame of this lane"}
|
||||
:else {:clip (:clip prepared) :sid sid :lane-id lane-id :at at
|
||||
:path (vec (butlast (nth lane-sel 3))) :made? (nil? aimed)})))
|
||||
(nil? sid) {:refused "what you are dropping into is not on screen at this frame"}
|
||||
(not (integer? at)) {:refused "the drop is not on one frame of that symbol"}
|
||||
:else {:clip document :sid sid :at at :path path})))
|
||||
|
||||
(defn landed
|
||||
"`db` after a drop that produced `result`, with `uuid` selected in `lane`."
|
||||
[db {:keys [sid lane-id path made?]} uuid result]
|
||||
"`db` after a drop that produced `result`, with `uuid` selected."
|
||||
[db {:keys [sid path]} uuid result]
|
||||
(if-let [why (:refused result)]
|
||||
(-> db (update :ui dissoc :drop) (update :project merge {:status why}))
|
||||
(cond-> (-> db
|
||||
(-> db
|
||||
(update :ui dissoc :drop)
|
||||
(edit/transaction (constantly (:clip result)))
|
||||
(assoc-in [:ui :selection] [:node sid uuid (conj (vec path) uuid)]))
|
||||
made? (assoc-in [:ui :target] {:sid sid :id lane-id :path [lane-id]}))))
|
||||
(assoc-in [:ui :selection] [:node sid uuid (conj (vec path) uuid)]))))
|
||||
|
||||
(rf/reg-event-db
|
||||
::drop-symbol
|
||||
;; A symbol dropped on a lane becomes a naturally playing clip in that lane.
|
||||
;; With no lane under it, make one: new timeline/stage placement therefore
|
||||
;; never invents another permanent row-per-symbol track.
|
||||
;; A symbol dropped on a row becomes a naturally playing clip in the symbol that
|
||||
;; row leads into. Whether it claims the time it lands on is `span/place-symbol`'s
|
||||
;; question and it answers it from the destination's display mode, so a drop onto
|
||||
;; a lane trims its neighbour and a drop into a composition does not.
|
||||
(fn [db [_ source-id frame point target]]
|
||||
(let [{document :clip st :store} (store/entry (:clip/current db))
|
||||
where (lane-destination db document st frame target :picture)
|
||||
where (drop-destination db document st frame target)
|
||||
uuid (random-uuid)]
|
||||
(if (:refused where)
|
||||
(-> db (update :ui dissoc :drop) (update :project merge {:status (:refused where)}))
|
||||
(landed db where uuid
|
||||
(lane/place-symbol (:clip where) st (:sid where) (:lane-id where)
|
||||
(span/place-symbol (:clip where) st (:sid where)
|
||||
uuid source-id (:at where)
|
||||
{:extent :grow-symbol :point point
|
||||
:remainder-id (random-uuid)}))))))
|
||||
|
||||
(rf/reg-event-db
|
||||
::drop-sound
|
||||
;; A SOUND IS A CLIP IN A LANE TOO. It is placed and then adopted rather than
|
||||
;; written straight into the lane, so one command owns where a sound's frames
|
||||
;; are — `clip/place-sound` — and one owns what claiming lane time means.
|
||||
;; A SOUND IS A CLIP TOO. It is placed and then adopted rather than written
|
||||
;; straight in, so one command owns where a sound's frames are —
|
||||
;; `clip/place-sound` — and one owns what claiming time means.
|
||||
(fn [db [_ {:keys [source label length rate]} frame target]]
|
||||
(let [{document :clip st :store} (store/entry (:clip/current db))
|
||||
where (lane-destination db document st frame target :sound)
|
||||
where (drop-destination db document st frame target)
|
||||
uuid (random-uuid)]
|
||||
(if (:refused where)
|
||||
(-> db (update :ui dissoc :drop) (update :project merge {:status (:refused where)}))
|
||||
|
|
@ -747,31 +723,41 @@
|
|||
(:rate (clip/grid-time (:clip where) sid)))
|
||||
uuid)]
|
||||
(landed db where uuid
|
||||
(lane/adopt seeded sid (:lane-id where) uuid (:at where)
|
||||
(span/adopt seeded sid uuid (:at where)
|
||||
{:extent :grow-symbol :remainder-id (random-uuid)})))))))
|
||||
|
||||
(rf/reg-event-db
|
||||
::adopt-in-lane
|
||||
(fn [db [_ [_ from-sid id _] [_ lane-sid lane-id lane-path :as lane-selection]
|
||||
frame]]
|
||||
::drop-clip
|
||||
;; A CLIP BODY DRAGGED ONTO A ROW. Within its own symbol this is `span/adopt`,
|
||||
;; which claims the time it lands on — a drop onto occupied frames has already
|
||||
;; said it means to. Onto a row leading into ANOTHER symbol it is that, after
|
||||
;; `nest/move-node` carries the clip across keeping its world transform; the two
|
||||
;; commit as one transaction, so it is still one undo step.
|
||||
(fn [db [_ [_ from-sid id from-path] target-path frame]]
|
||||
(let [{document :clip st :store} (store/entry (:clip/current db))
|
||||
open (get-in db [:ui :open])
|
||||
owner-frame (selection-frame document st open lane-selection frame)
|
||||
at (when (number? owner-frame)
|
||||
(lane/lane-frame document lane-sid lane-id owner-frame))
|
||||
into (nest/inside document st open (vec target-path) frame)
|
||||
crossing? (not= from-sid (:sid into))
|
||||
carried (when (and (:sid into) crossing?)
|
||||
(nest/move-node document st open (vec from-path) (vec target-path) frame))
|
||||
doc (if crossing? (:clip carried) document)
|
||||
landed-id (if crossing? (:id carried) id)
|
||||
result (cond
|
||||
(not= from-sid lane-sid)
|
||||
{:refused "a clip and its destination lane must be in the same symbol"}
|
||||
(not (integer? at)) {:refused "the drop is not on one frame of this lane"}
|
||||
:else (lane/adopt document lane-sid lane-id id at
|
||||
(nil? (:sid into))
|
||||
{:refused "what you are dropping into is not on screen at this frame"}
|
||||
(not (integer? (:frame into)))
|
||||
{:refused "the drop is not on one frame of that symbol"}
|
||||
(and crossing? (:refused carried)) carried
|
||||
:else (span/adopt doc (:sid into) landed-id (:frame into)
|
||||
{:extent :grow-symbol
|
||||
:remainder-id (random-uuid)}))
|
||||
path (conj (vec (butlast lane-path)) id)]
|
||||
:remainder-id (random-uuid)}))]
|
||||
(if-let [why (:refused result)]
|
||||
(update db :project merge {:status why})
|
||||
(-> db
|
||||
(edit/transaction (constantly (:clip result)))
|
||||
(assoc-in [:ui :selection] [:node lane-sid id path]))))))
|
||||
(assoc-in [:ui :selection]
|
||||
[:node (:sid into) landed-id
|
||||
(conj (vec target-path) landed-id)]))))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; moving rows between symbols
|
||||
|
|
@ -783,6 +769,48 @@
|
|||
(defn- refused [db why]
|
||||
(update db :project merge {:status (str "can't: " why)}))
|
||||
|
||||
(rf/reg-event-db
|
||||
::adopt-in-lane
|
||||
;; Plain body-drag is temporal. The destination row names either an instance
|
||||
;; of an explicit lane symbol, or the open lane symbol itself. Moving between
|
||||
;; two lane symbols transfers the clip before `span/adopt` claims its new time;
|
||||
;; Shift-drag remains the separate structural `::move-node` operation below.
|
||||
(fn [db [_ [_ from-sid id _] [_ target-sid target-id target-path :as target]
|
||||
frame]]
|
||||
(let [{document :clip st :store} (store/entry (:clip/current db))
|
||||
open (get-in db [:ui :open])
|
||||
target-node (get-in document [:symbols target-sid :nodes target-id])
|
||||
dest-sid (if target-id (node/source target-node) target-sid)
|
||||
dest (clip/symbol document dest-sid)
|
||||
local-frame (if (seq target-path)
|
||||
(:frame (nest/inside document st open target-path frame))
|
||||
(:frame (nest/inside document st open [] frame)))
|
||||
n (get-in document [:symbols from-sid :nodes id])
|
||||
seeded (when (and n dest-sid)
|
||||
(if (= from-sid dest-sid)
|
||||
document
|
||||
(-> document
|
||||
(update-in [:symbols from-sid :nodes] dissoc id)
|
||||
(assoc-in [:symbols dest-sid :nodes id]
|
||||
(dissoc n :parent)))))
|
||||
result (cond
|
||||
(nil? n) {:refused "select a clip to move"}
|
||||
(not (symbol/lane? dest)) {:refused "drop onto an explicit lane"}
|
||||
(and (not= from-sid dest-sid)
|
||||
(contains? (get-in document [:symbols dest-sid :nodes]) id))
|
||||
{:refused "the destination already has a clip with that ID"}
|
||||
(not (integer? local-frame))
|
||||
{:refused "the drop is not on one frame of this lane"}
|
||||
:else (span/adopt seeded dest-sid id local-frame
|
||||
{:extent :grow-symbol
|
||||
:remainder-id (random-uuid)}))]
|
||||
(if-let [why (:refused result)]
|
||||
(refused db why)
|
||||
(-> db
|
||||
(edit/transaction (constantly (:clip result)))
|
||||
(assoc-in [:ui :selection]
|
||||
[:node dest-sid id (conj (vec target-path) id)]))))))
|
||||
|
||||
(rf/reg-event-db
|
||||
::move-node
|
||||
(fn [db [_ from to]]
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
|
||||
(rf/reg-sub ::selection (fn [db _] (get-in db [:ui :selection])))
|
||||
(rf/reg-sub ::target (fn [db _] (get-in db [:ui :target])))
|
||||
(rf/reg-sub ::lane-retry (fn [db _] (get-in db [:ui :lane-retry])))
|
||||
(rf/reg-sub ::retry (fn [db _] (get-in db [:ui :retry])))
|
||||
(rf/reg-sub ::tone (fn [db _] (get-in db [:ui :tone])))
|
||||
(rf/reg-sub ::tool (fn [db _] (get-in db [:ui :tool])))
|
||||
(rf/reg-sub ::auto-key? (fn [db _] (boolean (get-in db [:ui :auto-key?]))))
|
||||
|
|
|
|||
|
|
@ -76,7 +76,8 @@
|
|||
(map (fn [a]
|
||||
(let [m (get nodes a)]
|
||||
{:kind (:kind m)
|
||||
:lane? (node/lane? m)
|
||||
:lane? (and (= :instance (:kind m))
|
||||
(symbol/lane? (clip/symbol clip (node/source m))))
|
||||
:sid in :id a :of (node/source m)
|
||||
:label (crumb-label clip a m)
|
||||
:select [:node in a (conj so-far a)]})))
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
[arthur.domain.paint :as paint]
|
||||
[arthur.domain.params :as params]
|
||||
[arthur.domain.pose :as pose]
|
||||
[arthur.domain.symbol :as symbol]
|
||||
[arthur.domain.trace :as trace]
|
||||
[arthur.events.history :as history]
|
||||
[arthur.events.paint :as paint-events]
|
||||
|
|
@ -281,11 +282,10 @@
|
|||
|
||||
(defn- correction-section [[sid selected-id selected]]
|
||||
(let [clip @(rf/subscribe [::render/clip])
|
||||
parent (get-in clip [:symbols sid :nodes (:parent selected)])
|
||||
targets (cond
|
||||
(node/lane? selected) [selected-id]
|
||||
(node/lane? parent) [selected-id (:id parent)]
|
||||
:else [])]
|
||||
lane-context? (or (symbol/lane? (clip-domain/symbol clip sid))
|
||||
(and (= :instance (:kind selected))
|
||||
(symbol/lane? (clip-domain/symbol clip (node/source selected)))))
|
||||
targets (if lane-context? [selected-id] [])]
|
||||
(when (seq targets)
|
||||
(r/with-let [draft (r/atom (correction-initial clip sid selected-id))]
|
||||
(let [target (:target @draft)
|
||||
|
|
@ -303,7 +303,7 @@
|
|||
(doall (for [[i id] (map-indexed vector targets)]
|
||||
^{:key (str id)}
|
||||
[:option {:value i}
|
||||
(str (if (= id selected-id) "selected · " "lane · ") (brief id))]))]]
|
||||
(str "selected · " (brief id))]))]]
|
||||
[:label.inspector-field "property"
|
||||
[:select {:value (if (= [:xform :rot] (:path @draft)) "rotation" "position")
|
||||
:on-change #(swap! draft assoc :path
|
||||
|
|
|
|||
|
|
@ -171,6 +171,9 @@
|
|||
(into (inside-rows sid child cpath (inc depth) cself cspan))))))
|
||||
(walk [sid path depth ->open]
|
||||
(let [sym (get-in clip [:symbols sid])
|
||||
root-lane? (and (empty? path) (symbol/lane? sym))
|
||||
root-clips (when root-lane? (symbol/children (:nodes sym)))
|
||||
root-ids (into #{} (map :id) root-clips)
|
||||
ordered (->> (:nodes sym)
|
||||
;; Front-most at the top, as a layer list is drawn
|
||||
;; everywhere. `:z` is the lexicographic draw key;
|
||||
|
|
@ -179,8 +182,49 @@
|
|||
reverse
|
||||
;; Sounds are listed below the picture, by
|
||||
;; `sound-rows`, wherever they are.
|
||||
(remove #(= :audio (:kind (val %)))))]
|
||||
(into []
|
||||
(remove #(= :audio (:kind (val %))))
|
||||
;; In an open lane symbol, its clips are blocks
|
||||
;; on the symbol's own row. Span-less decoration
|
||||
;; remains an ordinary row beside it.
|
||||
(remove #(contains? root-ids (key %))))
|
||||
root-row (when root-lane?
|
||||
(let [open? (contains? expanded [::lane])
|
||||
row {:path [::lane]
|
||||
:depth depth
|
||||
:label (or (:name sym) (name sid))
|
||||
:kind :symbol
|
||||
:lane? true
|
||||
:select [:node sid nil []]
|
||||
:expandable? true
|
||||
:expanded? open?
|
||||
:span [0 (:frames sym)]
|
||||
:keys []
|
||||
:cels (mapv (fn [child]
|
||||
{:id (:id child)
|
||||
:label (or (get-in clip [:symbols (node/source child) :name])
|
||||
(some-> (node/source child) name)
|
||||
(node-label (:id child) child))
|
||||
:source (node/source child)
|
||||
:span (mapv ->open (node/placed-span child))
|
||||
:keys (into []
|
||||
(comp (mapcat keyed-frames)
|
||||
(map (comp ->open (local->parent child)))
|
||||
(distinct))
|
||||
(vals (node/channels child)))
|
||||
:select [:node sid (:id child)
|
||||
(conj path (:id child))]})
|
||||
root-clips)}]
|
||||
(cond-> [row]
|
||||
open?
|
||||
(into (if-let [child (first (filter #(under? (conj path (:id %)))
|
||||
root-clips))]
|
||||
(portal sid path (inc depth) ->open child)
|
||||
[{:path [::lane ::portal]
|
||||
:depth (inc depth) :kind :hint
|
||||
:label (if (seq root-clips)
|
||||
"select a clip to inspect"
|
||||
"empty lane")}])))))]
|
||||
(into (vec root-row)
|
||||
(mapcat
|
||||
(fn [[id n]]
|
||||
(let [rpath (conj path id)
|
||||
|
|
@ -197,6 +241,10 @@
|
|||
(map #(local->parent (get-in sym [:nodes %]))
|
||||
(reverse ancestors)))
|
||||
self (comp parent-map (local->parent n))
|
||||
source-sym (when (= :instance (:kind n))
|
||||
(clip/symbol clip (node/source n)))
|
||||
lane? (symbol/lane? source-sym)
|
||||
clips (when lane? (symbol/children (:nodes source-sym)))
|
||||
span (mapv parent-map
|
||||
(or (node/placed-span
|
||||
(cond-> n
|
||||
|
|
@ -208,7 +256,7 @@
|
|||
:label (node-label id n)
|
||||
:kind :node
|
||||
:node-kind (:kind n)
|
||||
:lane? (node/lane? n)
|
||||
:lane? lane?
|
||||
:of (node/source n)
|
||||
:select [:node sid id rpath]
|
||||
:expandable? true
|
||||
|
|
@ -219,16 +267,7 @@
|
|||
(distinct))
|
||||
(vals channels))
|
||||
:dense? (boolean (some :dense (vals channels)))}]
|
||||
;; A CLIP IS NOT A ROW. A lane's symbol clips are
|
||||
;; blocks on the lane's own row, so twelve clips are
|
||||
;; still one row. The clip remains independently
|
||||
;; selectable and addressable; only its presentation
|
||||
;; is shared.
|
||||
(if (node/lane? (get-in sym [:nodes (:parent n)]))
|
||||
[]
|
||||
(let [clips (when (node/lane? n)
|
||||
(symbol/lane-clips (:nodes sym) id))
|
||||
;; A lane of sounds is a lane like any other —
|
||||
(let [;; A lane of sounds is a lane like any other —
|
||||
;; same blocks, same edges, same portal — and
|
||||
;; is listed under the audio heading because
|
||||
;; that is where somebody looks for a sound,
|
||||
|
|
@ -236,7 +275,7 @@
|
|||
sound-lane? (and (seq clips)
|
||||
(every? #(= :audio (:kind %)) clips))
|
||||
row (cond-> row
|
||||
(node/lane? n)
|
||||
lane?
|
||||
(assoc :cels
|
||||
(mapv (fn [child]
|
||||
{:id (:id child)
|
||||
|
|
@ -252,24 +291,26 @@
|
|||
(map (comp self (local->parent child)))
|
||||
(distinct))
|
||||
(vals (node/channels child)))
|
||||
:select [:node sid (:id child) (conj path (:id child))]})
|
||||
:select [:node (node/source n) (:id child)
|
||||
(conj rpath (:id child))]})
|
||||
clips)))]
|
||||
(cond->> (if-not open?
|
||||
[row]
|
||||
(-> [row]
|
||||
(into (channel-rows n rpath (inc depth) self span))
|
||||
;; The one clip an expanded lane opens.
|
||||
(into (when (node/lane? n)
|
||||
(if-let [child (first (filter #(under? (conj path (:id %))) clips))]
|
||||
(portal sid path (inc depth) self child)
|
||||
(into (when lane?
|
||||
(if-let [child (first (filter #(under? (conj rpath (:id %))) clips))]
|
||||
(portal (node/source n) rpath (inc depth) self child)
|
||||
[{:path (conj rpath ::portal)
|
||||
:depth (inc depth)
|
||||
:kind :hint
|
||||
:label (if (seq clips)
|
||||
"select a clip to inspect"
|
||||
"empty lane")}])))
|
||||
(into (inside-rows sid n rpath (inc depth) self span))))
|
||||
sound-lane? (mapv #(assoc % :sound? true)))))))
|
||||
(into (when-not lane?
|
||||
(inside-rows sid n rpath (inc depth) self span)))))
|
||||
sound-lane? (mapv #(assoc % :sound? true))))))
|
||||
ordered))))]
|
||||
(if (get-in clip [:symbols sid])
|
||||
(walk sid [] 0 #(/ % (:rate (clip/grid-time clip sid))))
|
||||
|
|
@ -279,19 +320,14 @@
|
|||
"Audio rows use the same flattened intervals as the mixer, including source
|
||||
in-points, cel speeds, parent timing, and silence beneath visual holds.
|
||||
|
||||
WHAT IS IN A LANE OF THIS SYMBOL IS NOT FLATTENED HERE. A sound in a lane is
|
||||
WHAT IS IN A LANE SYMBOL IS NOT FLATTENED HERE. A sound in a lane is
|
||||
a clip somebody placed and can move, trim and open, and `rows` draws it as
|
||||
one; flattening it as well would show the same sound on two rows, only one of
|
||||
which could be edited. What remains is what this view is for: audio nested
|
||||
inside the symbols this one places, mapped into this ruler."
|
||||
[clip sid expanded]
|
||||
(if-not (get-in clip [:symbols sid]) []
|
||||
(let [nodes (get-in clip [:symbols sid :nodes])
|
||||
in-lane (into #{} (keep (fn [[id n]]
|
||||
(when (and (= :audio (:kind n))
|
||||
(node/lane? (get nodes (:parent n))))
|
||||
id)))
|
||||
nodes)]
|
||||
(let [lane? (symbol/lane? (clip/symbol clip sid))]
|
||||
(vec
|
||||
(mapcat
|
||||
(fn [[path tracks]]
|
||||
|
|
@ -314,8 +350,7 @@
|
|||
(when open? (channel-rows n path 1 identity span)))))
|
||||
(sort-by (comp str key)
|
||||
(group-by :path
|
||||
(remove #(and (= 1 (count (:path %)))
|
||||
(contains? in-lane (first (:path %))))
|
||||
(remove #(and lane? (= 1 (count (:path %))))
|
||||
(nest/audio-tracks clip sid)))))))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
|
@ -451,9 +486,9 @@
|
|||
[:span.spacer]
|
||||
;; After the spacer, both of them: an offer that appears and a reading that
|
||||
;; comes and goes must not shove the fixed controls sideways when they do.
|
||||
(when @(rf/subscribe [::sub/lane-retry])
|
||||
(when @(rf/subscribe [::sub/retry])
|
||||
[:button.retry {:title "the command was refused because the shot is too short"
|
||||
:on-click (act [::ui/lane-retry])} "extend shot and apply"])
|
||||
:on-click (act [::ui/retry])} "extend shot and apply"])
|
||||
;; Measured in the loop, not derived from the clock — the whole question
|
||||
;; while profiling is whether the painting keeps up with the clock, and a
|
||||
;; number computed FROM the clock would answer itself. Shown only while it
|
||||
|
|
|
|||
|
|
@ -4,22 +4,22 @@
|
|||
[arthur.domain.clip :as clip]
|
||||
[arthur.domain.correction :as correction]
|
||||
[arthur.domain.leaf :as leaf]
|
||||
[arthur.domain.lane-test :as fixture]))
|
||||
[arthur.domain.sequence-test :as fixture]))
|
||||
|
||||
(defn- channel [doc id path]
|
||||
(get-in doc [:symbols :main :nodes id :channels path]))
|
||||
|
||||
(deftest authors-the-three-motions-as-ordinary-layer-channels
|
||||
(let [doc (fixture/document)
|
||||
constant (correction/add doc :main :girl [:xform :rot]
|
||||
constant (correction/add doc :main :a [:xform :rot]
|
||||
{:id :flat :support [2 5] :motion :constant :delta 1})
|
||||
ramp (correction/add (:clip constant) :main :girl [:xform :rot]
|
||||
ramp (correction/add (:clip constant) :main :a [:xform :rot]
|
||||
{:id :ramp :support [6 9] :motion :ramp :start 0 :end 2})
|
||||
returned (correction/add (:clip ramp) :main :girl [:xform :rot]
|
||||
returned (correction/add (:clip ramp) :main :a [:xform :rot]
|
||||
{:id :return :support [9 12] :motion :return
|
||||
:start 0 :peak 3 :peak-frame 10})
|
||||
c (channel (:clip returned) :girl [:xform :rot])]
|
||||
(is (= :girl (:selection returned)))
|
||||
c (channel (:clip returned) :a [:xform :rot])]
|
||||
(is (= :a (:selection returned)))
|
||||
(is (= [:flat :ramp :return] (mapv :id (:over c))))
|
||||
(is (= [0 0 1 1 1 0 0 1 2 0 3 0]
|
||||
(mapv #(ch/value-at c % nil) (range 12))))
|
||||
|
|
@ -47,19 +47,19 @@
|
|||
put3 (ch/layer :put3 [0 2] :replace (ch/framed [1 2 3]))
|
||||
add3 (ch/layer :add3 [0 2] :offset (ch/framed [1 1 1]))
|
||||
stacked (assoc base :over [put3 add3])
|
||||
doc (assoc-in doc [:symbols :main :nodes :girl :channels [:xform :pos]] stacked)]
|
||||
(is (:refused (correction/remove-layer doc :main :girl [:xform :pos] :put3))
|
||||
doc (assoc-in doc [:symbols :main :nodes :a :channels [:xform :pos]] stacked)]
|
||||
(is (:refused (correction/remove-layer doc :main :a [:xform :pos] :put3))
|
||||
"removing the replacement would expose a wrong-shaped base")
|
||||
(let [conflicted (assoc-in doc [:symbols :main :nodes :girl :channels [:xform :pos] :over 1 :conflict]
|
||||
(let [conflicted (assoc-in doc [:symbols :main :nodes :a :channels [:xform :pos] :over 1 :conflict]
|
||||
"old topology")
|
||||
retried (correction/retry-layer conflicted :main :girl [:xform :pos] :add3)]
|
||||
retried (correction/retry-layer conflicted :main :a [:xform :pos] :add3)]
|
||||
(is (:clip retried))
|
||||
(is (nil? (get-in (:clip retried)
|
||||
[:symbols :main :nodes :girl :channels [:xform :pos] :over 1 :conflict]))))
|
||||
[:symbols :main :nodes :a :channels [:xform :pos] :over 1 :conflict]))))
|
||||
(let [without-replacement (-> doc
|
||||
(assoc-in [:symbols :main :nodes :girl :channels [:xform :pos] :over]
|
||||
(assoc-in [:symbols :main :nodes :a :channels [:xform :pos] :over]
|
||||
[(assoc add3 :conflict "old topology")]))]
|
||||
(is (:refused (correction/retry-layer without-replacement :main :girl
|
||||
(is (:refused (correction/retry-layer without-replacement :main :a
|
||||
[:xform :pos] :add3))
|
||||
"retry refuses when the current effective base still has the wrong shape"))))
|
||||
|
||||
|
|
@ -74,13 +74,13 @@
|
|||
|
||||
(deftest corrections-round-trip-with-identity-support-and-order
|
||||
(let [doc (fixture/document)
|
||||
one (:clip (correction/add doc :main :girl [:xform :rot]
|
||||
one (:clip (correction/add doc :main :a [:xform :rot]
|
||||
{:id :one :support [0 3] :motion :constant :delta 1}))
|
||||
two (:clip (correction/add one :main :girl [:xform :rot]
|
||||
two (:clip (correction/add one :main :a [:xform :rot]
|
||||
{:id :two :support [3 6] :motion :ramp
|
||||
:start 0 :end 2}))
|
||||
back (leaf/clip "u" (leaf/leaves "u" two))]
|
||||
(is (= two back))
|
||||
(is (= [:one :two]
|
||||
(mapv :id (get-in back [:symbols :main :nodes :girl
|
||||
(mapv :id (get-in back [:symbols :main :nodes :a
|
||||
:channels [:xform :rot] :over]))))))
|
||||
|
|
|
|||
|
|
@ -241,11 +241,9 @@
|
|||
made (clip/new-symbol c :outer id 20 u)]
|
||||
(is (= :symbol-1 id))
|
||||
(is (= :symbol-2 (clip/fresh-id made)) "the next one does not collide")
|
||||
(is (= {:id :symbol-1 :name "symbol-1" :fps 30 :frames 180
|
||||
:nodes {:lane clip/lane-node}}
|
||||
(is (= {:id :symbol-1 :name "symbol-1" :fps 30 :frames 180 :nodes {}}
|
||||
(clip/symbol made :symbol-1))
|
||||
"empty but for the lane every symbol is born with, and as long as the
|
||||
rest of what it was placed in")
|
||||
"empty, ordinary, and as long as the rest of what it was placed in")
|
||||
(is (= {:span [0 180] :time {:mode :map :at 20 :rate 1}}
|
||||
(select-keys (get-in made [:symbols :outer :nodes u]) [:span :time])))
|
||||
(is (= #{:symbol-1} (node/sources (get-in made [:symbols :outer :nodes u])))
|
||||
|
|
|
|||
|
|
@ -1,650 +0,0 @@
|
|||
(ns arthur.domain.lane-test
|
||||
(:require [cljs.test :refer [deftest is testing]]
|
||||
[arthur.domain.bring :as bring]
|
||||
[arthur.domain.channel :as ch]
|
||||
[arthur.domain.clip :as clip]
|
||||
[arthur.domain.history :as history]
|
||||
[arthur.domain.leaf :as leaf]
|
||||
[arthur.domain.nest :as nest]
|
||||
[arthur.domain.node :as node]
|
||||
[arthur.domain.palette :as pal]
|
||||
[arthur.domain.pick :as pick]
|
||||
[arthur.domain.lane :as lane]
|
||||
[arthur.domain.span :as span]
|
||||
[arthur.domain.symbol :as symbol]))
|
||||
|
||||
(defn drawing [id x frames]
|
||||
{:id id :frames frames
|
||||
:nodes {:mark {:id :mark :kind :rect :z "a"
|
||||
:channels {[:geom :size] (ch/framed 4)
|
||||
[:xform :pos] (ch/framed [x 0])}}}})
|
||||
|
||||
(defn cel [id source at duration speed]
|
||||
{:id id :kind :instance :parent :girl :z (name id)
|
||||
:source {:symbol source} :playback {:in 0 :speed speed :end :stop}
|
||||
:time {:at at :rate 1} :span [0 duration]})
|
||||
|
||||
(defn document []
|
||||
(let [a (cel :a :drawing-a 0 4 0)
|
||||
b (assoc-in (cel :b :drawing-b 4 4 0)
|
||||
[:channels [:xform :pos]] (ch/keyed {0 [0 0] 1 [2 0]} :hold))
|
||||
insert (assoc-in (cel :insert :wave 8 4 1) [:playback :in] 3)]
|
||||
{:name "cels" :fps 24 :width 320 :height 200
|
||||
:symbols
|
||||
{:main {:id :main :frames 12
|
||||
:nodes {:girl {:id :girl :kind :group :layout :sequence :z "b"
|
||||
:channels {[:xform :pos] (ch/keyed {0 [0 0] 6 [60 0] 12 [0 0]} :linear)}}
|
||||
:a a :b b :insert insert
|
||||
:plate {:id :plate :kind :rect :z "a"
|
||||
:channels {[:geom :size] (ch/framed 10)
|
||||
[:xform :pos] (ch/keyed {0 [-40 0] 11 [70 0]} :linear)}}}}
|
||||
:drawing-a (drawing :drawing-a 10 1)
|
||||
:drawing-b (drawing :drawing-b 20 1)
|
||||
:wave (assoc-in (drawing :wave 0 10) [:nodes :mark :channels [:xform :pos]]
|
||||
(ch/keyed {0 [0 0] 9 [900 0]} :linear))}}))
|
||||
|
||||
|
||||
(defn sample [doc fs]
|
||||
(let [r (clip/resolver doc :main nil pal/index-of nil)]
|
||||
(into {} (map (fn [f] [f (into {} (map (juxt :node :cx)) (r f))])) fs)))
|
||||
|
||||
(deftest one-lane-mixes-held-drawings-and-playing-content
|
||||
(let [doc (document) at (sample doc (range 12))]
|
||||
(is (empty? (clip/problems doc)))
|
||||
(is (= 40 (get-in at [3 [:a :mark]])))
|
||||
(is (= 60 (get-in at [4 [:b :mark]])))
|
||||
(is (= 72 (get-in at [5 [:b :mark]])))
|
||||
(is (= 340 (get-in at [8 [:insert :mark]])))
|
||||
(is (= 610 (get-in at [11 [:insert :mark]])))
|
||||
(is (= (zipmap (range 12) (range -40 80 10))
|
||||
(into {} (map (fn [[f ops]] [f (js/Math.round (:plate ops))])) at)))
|
||||
(is (nil? (get-in at [4 [:a :mark]])) "half-open cuts have a single owner")))
|
||||
|
||||
(deftest cel-ripple-keeps-lane-keys-and-moves-cel-corrections
|
||||
(let [doc (document)
|
||||
result (lane/extend-hold doc :main :a 2 {:extent :grow-symbol})
|
||||
after (:clip result)
|
||||
nodes (get-in after [:symbols :main :nodes])]
|
||||
(is (= :a (:selection result)))
|
||||
(is (= 14 (get-in after [:symbols :main :frames])))
|
||||
(is (= [0 6] (node/placed-span (:a nodes))))
|
||||
(is (= [6 10] (node/placed-span (:b nodes))))
|
||||
(is (= [10 14] (node/placed-span (:insert nodes))))
|
||||
(doseq [id [:girl :a :b :insert :plate]]
|
||||
(is (= (get-in doc [:symbols :main :nodes id :channels]) (:channels (nodes id))))
|
||||
(is (= (get-in doc [:symbols :main :nodes id :playback]) (:playback (nodes id)))))
|
||||
(let [at (sample after [5 6 7 10])]
|
||||
(is (= 60 (get-in at [5 [:a :mark]])))
|
||||
(is (= 80 (get-in at [6 [:b :mark]])))
|
||||
(is (= 72 (get-in at [7 [:b :mark]])) "B's correction follows B")
|
||||
(is (= 320 (get-in at [10 [:insert :mark]])) "insert starts on source frame 3"))
|
||||
(is (empty? (clip/problems after)))
|
||||
(is (= (assoc-in doc [:symbols :main :frames] 14)
|
||||
(:clip (lane/extend-hold after :main :a -2 {})))
|
||||
"shrinking restores content, except the explicitly grown shot")))
|
||||
|
||||
(deftest overflow-and-invalid-edits-are-atomic
|
||||
(let [doc (document)
|
||||
result (lane/extend-hold doc :main :a 2 {})]
|
||||
(is (:refused result))
|
||||
(is (= 14 (:required-frames result)))
|
||||
(is (not (contains? result :clip)))
|
||||
(doseq [delta [0 -4 0.5 js/NaN]]
|
||||
(is (:refused (lane/extend-hold doc :main :a delta {}))))
|
||||
(is (:refused (lane/extend-hold doc :main :insert 1 {})))
|
||||
(is (:refused (lane/extend-hold doc :main :missing 1 {})))))
|
||||
|
||||
(deftest dragging-a-cel-edge-trims-neighbours-or-ripples-them
|
||||
(let [doc (document)
|
||||
plain (:clip (lane/resize-out doc :main :a 6 {}))
|
||||
across (:clip (lane/resize-out doc :main :a 9 {}))
|
||||
ripple (:clip (lane/resize-out doc :main :a 6 {:ripple? true
|
||||
:extent :grow-symbol}))
|
||||
shrink (:clip (lane/resize-out doc :main :a 2 {:ripple? true}))]
|
||||
(is (= [[0 6] [6 8] [8 12]]
|
||||
(mapv node/placed-span (symbol/lane-clips (get-in plain [:symbols :main :nodes]) :girl)))
|
||||
"a normal grow eats the beginning of the adjacent cel")
|
||||
(is (= [[0 9] [9 12]]
|
||||
(mapv node/placed-span (symbol/lane-clips (get-in across [:symbols :main :nodes]) :girl)))
|
||||
"a long grow removes wholly consumed cels and trims the survivor")
|
||||
(is (= [[0 6] [6 10] [10 14]]
|
||||
(mapv node/placed-span (symbol/lane-clips (get-in ripple [:symbols :main :nodes]) :girl)))
|
||||
"shift-grow moves every later cel")
|
||||
(is (= [[0 2] [2 6] [6 10]]
|
||||
(mapv node/placed-span (symbol/lane-clips (get-in shrink [:symbols :main :nodes]) :girl)))
|
||||
"shift-shrink pulls every later cel left")
|
||||
(is (:refused (lane/resize-out doc :main :a 0 {})))
|
||||
(is (:refused (lane/resize-out doc :main :a 2.5 {})))))
|
||||
|
||||
(deftest the-middle-of-a-cut-rolls-both-edges
|
||||
(let [doc (document)
|
||||
rolled (:clip (lane/roll doc :main :a :b 6))
|
||||
right-only (:clip (lane/resize-in doc :main :b 6))
|
||||
grown-left (:clip (lane/resize-in doc :main :b 2))]
|
||||
(is (= [[0 6] [6 8] [8 12]]
|
||||
(mapv node/placed-span (symbol/lane-clips (get-in rolled [:symbols :main :nodes]) :girl)))
|
||||
"the shared cut moves without moving either clip")
|
||||
(is (= [[0 4] [6 8] [8 12]]
|
||||
(mapv node/placed-span (symbol/lane-clips (get-in right-only [:symbols :main :nodes]) :girl)))
|
||||
"the right side of the junction trims only the right clip")
|
||||
(is (= [[0 2] [2 8] [8 12]]
|
||||
(mapv node/placed-span (symbol/lane-clips (get-in grown-left [:symbols :main :nodes]) :girl)))
|
||||
"growing the right clip left trims the neighbour instead of overlapping")
|
||||
(is (:refused (lane/roll doc :main :a :b 0)))
|
||||
(is (:refused (lane/roll doc :main :a :insert 6)))))
|
||||
|
||||
(deftest a-gap-is-an-uncovered-interval
|
||||
(let [doc (update-in (document) [:symbols :main :nodes] dissoc :b)
|
||||
at (sample doc [3 4 7 8])]
|
||||
(is (= #{:plate} (set (keys (at 4)))))
|
||||
(is (= #{:plate} (set (keys (at 7)))))
|
||||
(is (get-in at [8 [:insert :mark]]))
|
||||
(is (empty? (clip/problems doc)))))
|
||||
|
||||
(deftest validation-rejects-overlap-but-allows-empty-lanes
|
||||
(is (some #(re-find #"overlap" %)
|
||||
(clip/problems (assoc-in (document) [:symbols :main :nodes :b :time :at] 3))))
|
||||
(is (empty? (clip/problems
|
||||
(update-in (document) [:symbols :main :nodes] dissoc :a :b :insert))))
|
||||
(is (seq (clip/problems
|
||||
(assoc-in (document) [:symbols :main :nodes :a :span] [0 ##Inf]))))
|
||||
(is (seq (clip/problems
|
||||
(assoc-in (document) [:symbols :main :nodes :a :playback :speed] -1))))
|
||||
(is (seq (node/problems {:id :old :kind :instance :z "a"
|
||||
:channels {[:source] (ch/framed {:of :wave :in 0})}}))
|
||||
"the obsolete format is rejected"))
|
||||
|
||||
(deftest playback-is-independent-of-property-channel-shape
|
||||
(let [doc (document)
|
||||
n (get-in doc [:symbols :main :nodes :a])
|
||||
keyed (node/toggle-key n [:xform :rot] 0 nil)
|
||||
unkeyed (node/toggle-key keyed [:xform :rot] 0 nil)]
|
||||
(doseq [n [n keyed unkeyed]]
|
||||
(is (= {:symbol :drawing-a :frame 0} (node/placed-frame n 11 1))))
|
||||
(let [n (get-in doc [:symbols :main :nodes :insert])]
|
||||
(is (= {:symbol :wave :frame 5} (node/placed-frame n 2 10)))
|
||||
(is (nil? (node/placed-frame n 7 10)))
|
||||
(is (= 9 (:frame (node/placed-frame (assoc-in n [:playback :end] :hold) 9 10))))
|
||||
(is (= 2 (:frame (node/placed-frame (assoc-in n [:playback :end] :loop) 9 10)))))))
|
||||
|
||||
(deftest navigation-and-hit-testing-use-the-same-source-time
|
||||
(let [doc (document)
|
||||
n (get-in doc [:symbols :main :nodes :insert])]
|
||||
(is (= 5 (:frame (nest/inside doc nil :main [:insert] 10))))
|
||||
(is (= {:at 5 :rate 1} (:time (nest/inside doc nil :main [:insert] 10))))
|
||||
(is (= 0 (:frame (nest/inside doc nil :main [:a] 3))))
|
||||
(is (nil? (:time (nest/inside doc nil :main [:a] 3))))
|
||||
(is (nil? (nest/inside doc nil :main [:a] 4)))
|
||||
(is (= ((pick/bounds-of doc nil :main n) 2)
|
||||
((pick/bounds-of doc nil :main (assoc-in n [:playback :in] 5)) 0)))))
|
||||
|
||||
(deftest seeking-and-source-reuse-do-not-share-cursors
|
||||
(let [doc (assoc-in (document) [:symbols :main :nodes :b :source :symbol] :drawing-a)
|
||||
fs [11 0 5 3 8 4 10 1 6 2 9 7]
|
||||
at (sample doc fs)]
|
||||
(is (= at (sample doc (reverse fs))))
|
||||
(is (= at (sample doc (range 12))))
|
||||
(let [edited (assoc-in doc [:symbols :drawing-a :nodes :mark :channels [:xform :pos]]
|
||||
(ch/framed [99 0]))]
|
||||
(is (= 99 (get-in (sample edited [0 4]) [0 [:a :mark]])))
|
||||
(is (= 139 (get-in (sample edited [0 4]) [4 [:b :mark]]))))))
|
||||
|
||||
(deftest cel-identities-and-playback-round-trip
|
||||
(let [doc (:clip (lane/extend-hold (document) :main :a 2 {:extent :grow-symbol}))
|
||||
leaves (leaf/leaves :project doc)]
|
||||
(is (= doc (leaf/clip :project leaves)))
|
||||
(is (contains? leaves "clip/project/symbol/main/node/a"))
|
||||
(is (not (contains? leaves "clip/project/symbol/main/channel/girl/source")))
|
||||
(let [{copied :clip ids :ids}
|
||||
(bring/symbols (assoc-in (clip/blank) [:symbols :drawing-a] (drawing :drawing-a 99 1))
|
||||
doc [:main] {})]
|
||||
(is (= :drawing-a-2 (:drawing-a ids)))
|
||||
(is (= #{:drawing-a-2 :drawing-b :wave} (clip/places copied (:main ids))))
|
||||
(is (empty? (clip/problems copied))))))
|
||||
|
||||
(deftest one-transaction-undoes-the-ripple-and-shot-extension
|
||||
(let [doc (document)
|
||||
after (:clip (lane/extend-hold doc :main :a 2 {:extent :grow-symbol}))
|
||||
before-leaves (leaf/leaves :p doc)
|
||||
after-leaves (leaf/leaves :p after)
|
||||
h (-> nil history/hold (history/record before-leaves after-leaves 0) history/settle)
|
||||
undo (history/undo h after-leaves)
|
||||
redo (history/redo (:history undo) (:leaves undo))]
|
||||
(is (= 1 (count (:done h))))
|
||||
(is (= before-leaves (:leaves undo)))
|
||||
(is (= after-leaves (:leaves redo)))))
|
||||
|
||||
(deftest create-lane-and-append-drawings
|
||||
(let [doc (:clip (lane/add-lane (clip/blank) :main :girl))
|
||||
a (:clip (lane/append-drawing doc :main :girl :a :drawing-a {}))
|
||||
b (:clip (lane/append-drawing a :main :girl :b :drawing-b {}))]
|
||||
(is (empty? (clip/problems b)))
|
||||
(is (= [1 2] (node/placed-span (get-in b [:symbols :main :nodes :b]))))
|
||||
(is (= 0 (get-in b [:symbols :main :nodes :b :playback :speed])))
|
||||
(is (:refused (lane/append-drawing b :main :girl :a :new {})))))
|
||||
|
||||
(deftest arbitrary-symbols-drop-into-the-same-lane-and-claim-their-time
|
||||
(let [doc (document)
|
||||
dropped (lane/place-symbol doc nil :main :girl :clip :wave 2
|
||||
{:extent :grow-symbol :remainder-id :tail})
|
||||
after (:clip dropped)
|
||||
clips (symbol/lane-clips (get-in after [:symbols :main :nodes]) :girl)]
|
||||
(is (= :clip (:selection dropped)))
|
||||
(is (= [[0 2] [2 12]] (mapv node/placed-span clips))
|
||||
"the natural ten-frame symbol claims [2,12), trimming/removing incumbents")
|
||||
(is (= :wave (node/source (second clips))))
|
||||
(is (= 1 (:speed (node/playback-of (second clips))))
|
||||
"a dropped symbol plays; it is not converted into a drawing hold")
|
||||
(is (empty? (clip/problems after)))))
|
||||
|
||||
(deftest an-existing-symbol-row-can-be-adopted-by-a-lane
|
||||
(let [doc (assoc-in (document) [:symbols :main :nodes :badge]
|
||||
{:id :badge :kind :instance :z "z"
|
||||
:source {:symbol :wave} :span [0 3]
|
||||
:time {:at 1 :rate 1}
|
||||
:playback {:in 2 :speed 1 :end :stop}})
|
||||
result (lane/adopt doc :main :girl :badge 5
|
||||
{:extent :grow-symbol :remainder-id :tail})
|
||||
after (:clip result)
|
||||
n (get-in after [:symbols :main :nodes :badge])]
|
||||
(is (= :girl (:parent n)))
|
||||
(is (= [5 8] (node/placed-span n)))
|
||||
(is (= {:in 2 :speed 1 :end :stop} (:playback n))
|
||||
"adoption changes placement, not source timing")
|
||||
(is (= [[0 4] [4 5] [5 8] [8 12]]
|
||||
(mapv node/placed-span
|
||||
(symbol/lane-clips (get-in after [:symbols :main :nodes]) :girl))))
|
||||
(is (empty? (clip/problems after)))))
|
||||
|
||||
(deftest fractional-placement-rates-convert-the-hold-delta
|
||||
(let [doc (-> (document)
|
||||
(assoc-in [:symbols :main :nodes :a :time :rate] 2)
|
||||
(assoc-in [:symbols :main :nodes :a :span] [0 8]))
|
||||
after (:clip (lane/extend-hold doc :main :a 2 {:extent :grow-symbol}))]
|
||||
(is (= [0 12] (get-in after [:symbols :main :nodes :a :span])))
|
||||
(is (= [6 10] (node/placed-span (get-in after [:symbols :main :nodes :b]))))))
|
||||
|
||||
(deftest bare-shapes-agree-in-reference-and-playback
|
||||
(let [sym (drawing :bare 12 1)]
|
||||
(is (= (symbol/eval-frame sym 0 nil pal/index-of nil)
|
||||
((symbol/resolver sym nil pal/index-of nil) 0))
|
||||
"omitted style colour must not crash a missing cursor")))
|
||||
|
||||
(deftest audio-follows-only-the-playing-cel
|
||||
(let [voice {:id :voice :kind :audio :z "a" :source {:sound "voice"}
|
||||
:span [0 10]
|
||||
:channels {[:audio :gain] (ch/keyed {0 0 5 1} :linear)}}
|
||||
doc (-> (document)
|
||||
(assoc-in [:symbols :wave :nodes :voice] voice)
|
||||
(assoc-in [:symbols :drawing-a :nodes :voice] voice))
|
||||
[track :as tracks] (nest/audio-tracks doc :main)]
|
||||
(is (= 1 (count tracks)) "the frozen drawing contributes no audio")
|
||||
(is (= [8 12] (node/placed-span track)))
|
||||
(is (= [3 7] (:span track)) "the source in-point trims the audio too")
|
||||
(is (= {5 0 10 1} (get-in track [:channels [:audio :gain] :keys])))
|
||||
(let [moved (:clip (lane/extend-hold doc :main :a 2 {:extent :grow-symbol}))
|
||||
[track] (nest/audio-tracks moved :main)]
|
||||
(is (= [10 14] (node/placed-span track)))
|
||||
(is (= [3 7] (:span track))))
|
||||
(let [fast (-> doc
|
||||
(assoc-in [:symbols :main :nodes :girl :time] {:at 2 :rate 2})
|
||||
(assoc-in [:symbols :main :nodes :insert :playback :speed] 2))
|
||||
[track] (nest/audio-tracks fast :main)]
|
||||
(is (= [6 7.75] (node/placed-span track)))
|
||||
(is (= [3 10] (:span track)))
|
||||
(is (= 4 (get-in track [:time :rate]))))))
|
||||
|
||||
(deftest a-looped-insert-schedules-distinct-audio-intervals
|
||||
(let [doc (-> (document)
|
||||
(assoc-in [:symbols :wave :frames] 4)
|
||||
(assoc-in [:symbols :wave :nodes :voice]
|
||||
{:id :voice :kind :audio :z "a" :source {:sound "v"} :span [1 3]})
|
||||
(assoc-in [:symbols :main :nodes :insert :playback]
|
||||
{:in 3 :speed 1 :end :loop}))]
|
||||
(is (= [[10 12]] (mapv node/placed-span (nest/audio-tracks doc :main))))))
|
||||
|
||||
(deftest enclosing-retiming-is-respected-when-extending-the-shot
|
||||
(let [doc (assoc-in (document) [:symbols :main :nodes :girl :time] {:at 8 :rate 2})
|
||||
result (lane/extend-hold doc :main :a 2 {})]
|
||||
(is (= 15 (:required-frames result)))
|
||||
(is (nil? (:clip result)))
|
||||
(is (= 15 (get-in (lane/extend-hold doc :main :a 2 {:extent :grow-symbol})
|
||||
[:clip :symbols :main :frames])))))
|
||||
|
||||
(deftest reuse-shares-content-and-make-unique-decouples-one-cel
|
||||
(let [doc (document)
|
||||
shared (:clip (lane/reuse-drawing doc :main :girl :c :drawing-a
|
||||
{:extent :grow-symbol}))
|
||||
edit (fn [c sym x]
|
||||
(assoc-in c [:symbols sym :nodes :mark :channels [:xform :pos]]
|
||||
(ch/framed [x 0])))]
|
||||
(is (:refused (lane/reuse-drawing doc :main :girl :c :drawing-a {}))
|
||||
"the shot has to be extended on purpose")
|
||||
(is (= :drawing-a (node/source (get-in shared [:symbols :main :nodes :c]))))
|
||||
(is (= [12 13] (node/placed-span (get-in shared [:symbols :main :nodes :c]))))
|
||||
(is (empty? (clip/problems shared)))
|
||||
;; One drawing, two cels: the edit arrives at both.
|
||||
(let [at (sample (edit shared :drawing-a 99) [0 12])]
|
||||
(is (= 99 (get-in at [0 [:a :mark]])))
|
||||
(is (= 99 (get-in at [12 [:c :mark]]))))
|
||||
(let [unique (:clip (lane/make-unique shared :main :c {}))]
|
||||
(is (= :drawing-a-2 (node/source (get-in unique [:symbols :main :nodes :c]))))
|
||||
(is (= (:nodes (get-in shared [:symbols :drawing-a]))
|
||||
(:nodes (get-in unique [:symbols :drawing-a-2])))
|
||||
"a copy of the same drawing, not an empty one")
|
||||
(is (= :drawing-a (node/source (get-in unique [:symbols :main :nodes :a])))
|
||||
"the other cel keeps the original")
|
||||
(let [at (sample (edit unique :drawing-a 99) [0 12])]
|
||||
(is (= 99 (get-in at [0 [:a :mark]])))
|
||||
(is (= 10 (get-in at [12 [:c :mark]])) "the cel made unique is untouched"))
|
||||
(let [at (sample (edit unique :drawing-a-2 99) [0 12])]
|
||||
(is (= 10 (get-in at [0 [:a :mark]])) "and does not reach back"))
|
||||
(is (empty? (clip/problems unique))))
|
||||
;; Nothing else places drawing-b, so there is nothing to decouple from.
|
||||
(is (:refused (lane/make-unique doc :main :b {})))
|
||||
(is (:refused (lane/make-unique doc :main :girl {}))
|
||||
"a lane places nothing itself")))
|
||||
|
||||
(deftest duplicate-copies-the-drawing-and-not-the-cel
|
||||
(let [doc (document)
|
||||
made (:clip (lane/duplicate-drawing doc :main :b :d {:extent :grow-symbol}))
|
||||
n (get-in made [:symbols :main :nodes :d])]
|
||||
(is (= :drawing-b-2 (node/source n)))
|
||||
(is (= (:nodes (get-in doc [:symbols :drawing-b]))
|
||||
(:nodes (get-in made [:symbols :drawing-b-2]))))
|
||||
(is (= [12 13] (node/placed-span n)))
|
||||
(is (= {:in 0 :speed 0 :end :stop} (:playback n)))
|
||||
(is (nil? (:channels n)) "B's own position correction belongs to B's cel")
|
||||
(is (= (get-in doc [:symbols :main :nodes :b])
|
||||
(get-in made [:symbols :main :nodes :b]))
|
||||
"the drawing duplicated is left as it was")
|
||||
(is (empty? (clip/problems made)))))
|
||||
|
||||
(deftest a-shallow-copy-keeps-its-parts-and-a-deep-copy-owns-them
|
||||
;; A drawing assembled from another symbol: copying it shallowly must keep
|
||||
;; using that part, and only an explicit deep copy may promise independence.
|
||||
(let [doc (assoc-in (document) [:symbols :drawing-a :nodes :part]
|
||||
{:id :part :kind :instance :z "b" :span [0 1]
|
||||
:time {:at 0 :rate 1} :source {:symbol :wave}
|
||||
:playback {:in 0 :speed 0 :end :stop}})
|
||||
copy (fn [opts] (:clip (lane/duplicate-drawing
|
||||
doc :main :a :d (merge {:extent :grow-symbol} opts))))
|
||||
shallow (copy {})
|
||||
deep (copy {:deep? true})]
|
||||
(is (= :wave (node/source (get-in shallow [:symbols :drawing-a-2 :nodes :part]))))
|
||||
(is (nil? (get-in shallow [:symbols :wave-2])))
|
||||
(is (= :wave-2 (node/source (get-in deep [:symbols :drawing-a-2 :nodes :part]))))
|
||||
(is (= (:nodes (get-in doc [:symbols :wave])) (:nodes (get-in deep [:symbols :wave-2]))))
|
||||
(is (empty? (clip/problems shallow)))
|
||||
(is (empty? (clip/problems deep)))))
|
||||
|
||||
(deftest reuse-refuses-what-would-not-be-a-document
|
||||
(let [doc (document)]
|
||||
(is (:refused (lane/reuse-drawing doc :main :girl :c :nothing-here {})))
|
||||
(is (:refused (lane/reuse-drawing doc :main :girl :c :main {:extent :grow-symbol}))
|
||||
"a symbol cannot go inside itself")
|
||||
(is (:refused (lane/reuse-drawing doc :main :girl :a :drawing-a {:extent :grow-symbol}))
|
||||
"a cel ID in use is not free")
|
||||
(is (:refused (lane/reuse-drawing doc :main :plate :c :drawing-a {})))
|
||||
(is (:refused (lane/duplicate-drawing doc :main :girl :d {})))))
|
||||
|
||||
(deftest drawing-on-twos-does-not-quantize-the-lane-transform
|
||||
;; Cel length IS the drawing cadence, and it is the only thing on twos
|
||||
;; here: the lane's transform has its own clock and keeps moving every frame.
|
||||
;; Stepping it would be the cel cadence leaking into continuous motion.
|
||||
(let [cel (fn [id source at] (cel id source at 2 0))
|
||||
doc (-> (document)
|
||||
(update-in [:symbols :main :nodes] dissoc :a :b :insert)
|
||||
(update-in [:symbols :main :nodes] merge
|
||||
{:c0 (cel :c0 :drawing-a 0)
|
||||
:c1 (cel :c1 :drawing-b 2)
|
||||
:c2 (cel :c2 :drawing-a 4)}))
|
||||
xs {:c0 10 :c1 20 :c2 10}
|
||||
at (sample doc (range 6))
|
||||
showing (fn [f] (first (dissoc (at f) :plate)))]
|
||||
(is (empty? (clip/problems doc)))
|
||||
(is (= [:c0 :c0 :c1 :c1 :c2 :c2] (mapv #(first (key (showing %))) (range 6)))
|
||||
"the drawing showing changes every second frame")
|
||||
(is (= [0 10 20 30 40 50]
|
||||
(mapv (fn [f] (let [[[id _] cx] (showing f)] (- cx (xs id)))) (range 6)))
|
||||
"and the lane moves on every frame, odd ones included")))
|
||||
|
||||
(defn- drawn
|
||||
"What every frame draws, as sorted values, so a picture can be compared
|
||||
without naming the cels that produced it."
|
||||
[doc fs]
|
||||
(let [at (sample doc fs)]
|
||||
(mapv #(sort (vals (get at %))) fs)))
|
||||
|
||||
(deftest a-drawing-goes-anywhere-in-the-lane-and-ripples-what-follows
|
||||
(let [doc (document)
|
||||
keys-of #(get-in % [:symbols :main :nodes :girl :channels [:xform :pos] :keys])
|
||||
spans #(mapv (fn [id] (node/placed-span (get-in % [:symbols :main :nodes id])))
|
||||
[:a :n :b :insert])
|
||||
r (lane/append-drawing doc :main :girl :n :drawing-n
|
||||
{:at 4 :extent :grow-symbol})]
|
||||
(is (= [[0 4] [4 5] [5 9] [9 13]] (spans (:clip r))))
|
||||
(is (= 13 (get-in r [:clip :symbols :main :frames])))
|
||||
(is (= (keys-of doc) (keys-of (:clip r))) "lane keys stay where they were authored")
|
||||
(is (= :n (:selection r)))
|
||||
(is (= 4 (:frame r)))
|
||||
(is (empty? (clip/problems (:clip r))))
|
||||
;; The same command with no room refuses, and says how much it needs.
|
||||
(is (= 13 (:required-frames (lane/append-drawing doc :main :girl :n :drawing-n {:at 4}))))
|
||||
;; At the very front everything moves.
|
||||
(is (= [[1 5] [0 1] [5 9] [9 13]]
|
||||
(spans (:clip (lane/append-drawing doc :main :girl :n :drawing-n
|
||||
{:at 0 :extent :grow-symbol})))))
|
||||
;; Inside a cel is not a position for another one.
|
||||
(is (re-find #"split it first"
|
||||
(:refused (lane/append-drawing doc :main :girl :n :drawing-n
|
||||
{:at 2 :extent :grow-symbol}))))
|
||||
(is (:refused (lane/append-drawing doc :main :girl :n :drawing-n
|
||||
{:at -1 :extent :grow-symbol})))
|
||||
(is (:refused (lane/append-drawing doc :main :girl :n :drawing-n
|
||||
{:at ##Inf :extent :grow-symbol})))
|
||||
;; Reuse and duplicate take a position too; it is one placement rule.
|
||||
(is (= [4 5] (node/placed-span
|
||||
(get-in (lane/reuse-drawing doc :main :girl :n :drawing-b
|
||||
{:at 4 :extent :grow-symbol})
|
||||
[:clip :symbols :main :nodes :n]))))
|
||||
(is (= [4 5] (node/placed-span
|
||||
(get-in (lane/duplicate-drawing doc :main :b :n
|
||||
{:at 4 :extent :grow-symbol})
|
||||
[:clip :symbols :main :nodes :n]))))))
|
||||
|
||||
(deftest split-then-place-puts-a-drawing-inside-a-hold
|
||||
;; The two commands the doc asks for, composed: neither one guesses.
|
||||
(let [doc (document)
|
||||
cut (:clip (span/split doc :main :a 2 :right))
|
||||
r (lane/append-drawing cut :main :girl :n :drawing-n
|
||||
{:at 2 :extent :grow-symbol})
|
||||
after (:clip r)]
|
||||
(is (= [[0 2] [2 3] [3 5] [5 9] [9 13]]
|
||||
(mapv #(node/placed-span (get-in after [:symbols :main :nodes %]))
|
||||
[:a :n :right :b :insert])))
|
||||
(is (= (get-in doc [:symbols :main :nodes :girl :channels])
|
||||
(get-in after [:symbols :main :nodes :girl :channels]))
|
||||
"the performance is still timed the way it was authored")
|
||||
(is (empty? (clip/problems after)))))
|
||||
|
||||
(deftest a-three-frame-correction-crosses-a-drawing-boundary
|
||||
;; The lane model's worked example. The correction belongs to the GIRL, so it
|
||||
;; applies across whichever drawings are showing under it, and outside its
|
||||
;; three frames the animation evaluates exactly as it did before.
|
||||
(let [doc (document)
|
||||
fs (range 12)
|
||||
before (drawn doc fs)
|
||||
beat (ch/layer :beat [3 6] :offset (ch/framed [30 0]))
|
||||
c (update-in doc [:symbols :main :nodes :girl :channels [:xform :pos] :over]
|
||||
(fnil conj []) beat)
|
||||
after (drawn c fs)
|
||||
outside [0 1 2 6 7 8 9 10 11]]
|
||||
(is (empty? (clip/problems c)))
|
||||
(is (= (mapv before outside) (mapv after outside))
|
||||
"outside the support, frame for frame identical")
|
||||
(let [at (sample c [3 4 5])]
|
||||
;; Frame 3 shows drawing A and frames 4 and 5 show drawing B: one
|
||||
;; correction, reaching across the cut between them.
|
||||
(is (= 70 (get-in at [3 [:a :mark]])))
|
||||
(is (= 90 (get-in at [4 [:b :mark]])))
|
||||
(is (= 102 (get-in at [5 [:b :mark]])) "and B's own correction still applies under it")
|
||||
(is (= [-10 0 10] (mapv (fn [f] (js/Math.round (get-in at [f :plate]))) [3 4 5]))
|
||||
"while the background, which is not in the lane, does not move"))
|
||||
;; One document change: one step, and it persists in the channel's own leaf.
|
||||
(let [b (leaf/leaves :p doc)
|
||||
a (leaf/leaves :p c)
|
||||
h (-> nil history/hold (history/record b a 0) history/settle)]
|
||||
(is (= 1 (count (:done h))))
|
||||
(is (= b (:leaves (history/undo h a))))
|
||||
(is (= c (leaf/clip :p a)) "a correction needs no codec of its own"))))
|
||||
|
||||
(deftest a-correction-on-one-cel-travels-with-it
|
||||
;; The other half of ownership: a layer on a cel is in that
|
||||
;; cel's own frames, so moving the cel moves the correction and
|
||||
;; nothing has to say so.
|
||||
(let [beat (ch/layer :beat [0 2] :offset (ch/framed [7 0]))
|
||||
doc (update-in (document) [:symbols :main :nodes :b :channels [:xform :pos] :over]
|
||||
(fnil conj []) beat)
|
||||
moved (:clip (lane/extend-hold doc :main :a 2 {:extent :grow-symbol}))]
|
||||
;; Stated as the difference from the same document without the correction,
|
||||
;; so the claim is about WHERE the layer applies and not about arithmetic.
|
||||
(let [nudge (fn [with without f]
|
||||
(- (get-in (sample with [f]) [f [:b :mark]])
|
||||
(get-in (sample without [f]) [f [:b :mark]])))]
|
||||
(is (= [7 7 0 0] (mapv #(nudge doc (document) %) [4 5 6 7]))
|
||||
"B's first two frames, which are lane frames 4 and 5")
|
||||
(is (= [7 7 0 0]
|
||||
(mapv #(nudge moved (:clip (lane/extend-hold (document) :main :a 2
|
||||
{:extent :grow-symbol}))
|
||||
%)
|
||||
[6 7 8 9]))
|
||||
"and after A's hold grows, B's first two frames, which are now 6 and 7"))
|
||||
(is (= (get-in doc [:symbols :main :nodes :b :channels])
|
||||
(get-in moved [:symbols :main :nodes :b :channels]))
|
||||
"the layer itself was not touched by the retiming")
|
||||
(is (empty? (clip/problems moved)))))
|
||||
|
||||
(defn- spans [clip ids]
|
||||
(mapv #(node/placed-span (get-in clip [:symbols :main :nodes %])) ids))
|
||||
|
||||
(deftest blanking-leaves-a-gap-and-does-not-close-it
|
||||
(let [doc (document)
|
||||
r (lane/blank doc :main :girl [5 7] {:id :rest})
|
||||
after (:clip r)]
|
||||
;; B spanned the range, so it became two cels with a hole between them.
|
||||
(is (= [[0 4] [4 5] [7 8] [8 12]] (spans after [:a :b :rest :insert])))
|
||||
(is (= :rest (:selection r)))
|
||||
(let [at (sample after [4 5 6 7])]
|
||||
(is (= #{:plate} (set (keys (at 5)))) "nothing is drawn on a blanked frame")
|
||||
(is (= #{:plate} (set (keys (at 6)))))
|
||||
(is (get-in at [4 [:b :mark]]))
|
||||
(is (get-in at [7 [:rest :mark]])))
|
||||
(is (= 12 (get-in after [:symbols :main :frames])))
|
||||
(is (empty? (clip/problems after)))))
|
||||
|
||||
(deftest blanking-a-whole-cel-removes-it-and-keeps-its-drawing
|
||||
(let [doc (document)
|
||||
after (:clip (lane/blank doc :main :girl [4 8] {}))]
|
||||
(is (nil? (get-in after [:symbols :main :nodes :b])))
|
||||
(is (= [[0 4] [8 12]] (spans after [:a :insert])) "and moves nothing")
|
||||
(is (= (get-in doc [:symbols :drawing-b]) (get-in after [:symbols :drawing-b]))
|
||||
"a lane does not own its content")
|
||||
(is (empty? (clip/problems after)))))
|
||||
|
||||
(deftest blanking-a-range-trims-what-it-only-partly-covers
|
||||
(let [doc (document)
|
||||
after (:clip (lane/blank doc :main :girl [3 9] {}))]
|
||||
(is (= [[0 3] [9 12]] (spans after [:a :insert])))
|
||||
(is (nil? (get-in after [:symbols :main :nodes :b])))
|
||||
(is (= (get-in (sample doc [9]) [9 [:insert :mark]])
|
||||
(get-in (sample after [9]) [9 [:insert :mark]]))
|
||||
"the insert kept its own frames, so frame 9 shows what it showed")
|
||||
(is (empty? (clip/problems after)))))
|
||||
|
||||
(deftest overwrite-clears-one-frame-and-does-not-ripple-what-follows
|
||||
(let [r (lane/overwrite-drawing (document) :main :girl :n :drawing-n 5
|
||||
{:extent :keep :remainder-id :right})
|
||||
after (:clip r)
|
||||
nodes (get-in after [:symbols :main :nodes])]
|
||||
(is (= :n (:selection r)))
|
||||
(is (= [[0 4] [4 5] [5 6] [6 8] [8 12]]
|
||||
(mapv #(node/placed-span (get nodes %)) [:a :b :n :right :insert])))
|
||||
(is (= :drawing-b (node/source (:right nodes))))
|
||||
(is (= 12 (get-in after [:symbols :main :frames])))
|
||||
(is (empty? (clip/problems after)))))
|
||||
|
||||
(deftest blank-refuses-what-it-cannot-do-in-one-piece
|
||||
(let [doc (document)]
|
||||
(is (re-find #"free ID" (:refused (lane/blank doc :main :girl [5 7] {})))
|
||||
"splitting a cel needs an ID for the remainder")
|
||||
(is (:refused (lane/blank doc :main :girl [5 7] {:id :a})) "and a free one")
|
||||
(is (:refused (lane/blank doc :main :girl [7 5] {})))
|
||||
(is (:refused (lane/blank doc :main :girl [5 5] {})))
|
||||
(is (:refused (lane/blank doc :main :girl [5 6.5] {})))
|
||||
(is (:refused (lane/blank doc :main :plate [0 2] {})))))
|
||||
|
||||
(deftest the-shot-length-is-authored-and-emptying-a-lane-does-not-shorten-it
|
||||
;; The window and the occupied extent are two facts. A shot with nothing in
|
||||
;; the last half is a shot somebody authored that long, and deleting the last
|
||||
;; drawing must not quietly shorten the film.
|
||||
(let [doc (document)
|
||||
empty-lane (:clip (lane/blank doc :main :girl [0 12] {}))]
|
||||
(is (empty? (symbol/lane-clips (get-in empty-lane [:symbols :main :nodes]) :girl)))
|
||||
(is (= 12 (get-in empty-lane [:symbols :main :frames])))
|
||||
(is (empty? (clip/problems empty-lane)))
|
||||
;; Growing is still the caller's word, and only ever grows.
|
||||
(is (:refused (lane/append-drawing empty-lane :main :girl :n :drawing-n {:at 20})))
|
||||
(is (= 21 (get-in (lane/append-drawing empty-lane :main :girl :n :drawing-n
|
||||
{:at 20 :extent :grow-symbol})
|
||||
[:clip :symbols :main :frames])))
|
||||
(is (= 12 (get-in (:clip (span/trim doc :main :insert :out 9))
|
||||
[:symbols :main :frames]))
|
||||
"and trimming the last cel leaves the window where it was")))
|
||||
|
||||
(deftest a-take-placed-in-a-lane-is-still-heard
|
||||
;; `bring/take` puts a take's sound INSIDE the symbol it makes, so that
|
||||
;; "wherever the symbol is placed it is heard". A lane is one of the places it
|
||||
;; can be placed, and must not be the one place that goes silent.
|
||||
(let [doc (assoc-in (document) [:symbols :take]
|
||||
{:id :take :frames 10 :fps 24
|
||||
:nodes {:pic {:id :pic :kind :instance :z "a"
|
||||
:source {:symbol :wave} :span [0 10]
|
||||
:time {:mode :map :at 0 :rate 1}
|
||||
:playback {:in 0 :speed 1 :end :stop}}
|
||||
:sound {:id :sound :name "sound" :kind :audio
|
||||
:parent nil :z "z-sound"
|
||||
:source {:footage "f1"} :span [0 10]
|
||||
:time {:mode :map :at 0 :rate 1}}}})
|
||||
at-root (clip/place-symbol doc nil :main :take 0 :root nil)
|
||||
in-lane (:clip (lane/place-symbol doc nil :main :girl :drop :take 0
|
||||
{:extent :grow-symbol :remainder-id :tail}))]
|
||||
(is (= 1 (count (nest/audio-tracks at-root :main)))
|
||||
"a take placed at the root is heard")
|
||||
(is (some? in-lane) "the take goes into the lane")
|
||||
(is (= 1 (count (nest/audio-tracks in-lane :main)))
|
||||
"and is still heard from inside a lane")))
|
||||
|
||||
(deftest a-sound-is-a-clip-in-a-lane-like-any-other
|
||||
;; Everything in the timeline is a lane, audio included: a sound claims lane
|
||||
;; time by the same rule, and what a lane will not do is hold both kinds.
|
||||
(let [made (lane/add-lane (document) :main :track)
|
||||
seeded (clip/place-sound (:clip made) :main {:sound "s1"} "voice" 6 1 2 :vo)
|
||||
result (lane/adopt seeded :main :track :vo 2 {:extent :grow-symbol})
|
||||
after (:clip result)
|
||||
n (get-in after [:symbols :main :nodes :vo])]
|
||||
(is (nil? (:refused result)) (str (:refused result)))
|
||||
(is (= :track (:parent n)))
|
||||
(is (= [2 8] (node/placed-span n)))
|
||||
(is (empty? (clip/problems after)))
|
||||
(is (= 1 (count (nest/audio-tracks after :main)))
|
||||
"a sound in a lane is still heard")
|
||||
(is (= [:vo] (mapv :id (symbol/lane-clips (get-in after [:symbols :main :nodes]) :track))))
|
||||
;; The one thing a lane refuses: being half picture and half sound, which
|
||||
;; is the explicit capability rather than a guess per frame.
|
||||
(let [mixed (lane/place-symbol after nil :main :track :also :wave 2
|
||||
{:extent :grow-symbol :remainder-id :rest})]
|
||||
(is (:refused mixed))
|
||||
(is (re-find #"picture or sound" (str (:refused mixed))))
|
||||
(is (= [:vo] (mapv :id (symbol/lane-clips (get-in after [:symbols :main :nodes]) :track)))
|
||||
"and the sound it would have had to delete to make room is still there"))))
|
||||
812
frontend/test/arthur/domain/sequence_test.cljs
Normal file
812
frontend/test/arthur/domain/sequence_test.cljs
Normal file
|
|
@ -0,0 +1,812 @@
|
|||
(ns arthur.domain.sequence-test
|
||||
"The commands that need a SEQUENCE, which is now a symbol drawn as a lane and
|
||||
its own children rather than a group with `:layout :sequence`. Was
|
||||
`lane_test`; the fixtures changed and the assertions did not, except where
|
||||
they are called out below as behaviour that changed on purpose."
|
||||
(:require [cljs.test :refer [deftest is testing]]
|
||||
[arthur.domain.bring :as bring]
|
||||
[arthur.domain.channel :as ch]
|
||||
[arthur.domain.clip :as clip]
|
||||
[arthur.domain.history :as history]
|
||||
[arthur.domain.leaf :as leaf]
|
||||
[arthur.domain.nest :as nest]
|
||||
[arthur.domain.node :as node]
|
||||
[arthur.domain.palette :as pal]
|
||||
[arthur.domain.pick :as pick]
|
||||
[arthur.domain.span :as span]
|
||||
[arthur.domain.symbol :as symbol]))
|
||||
|
||||
(defn drawing [id x frames]
|
||||
{:id id :frames frames
|
||||
:nodes {:mark {:id :mark :kind :rect :z "a"
|
||||
:channels {[:geom :size] (ch/framed 4)
|
||||
[:xform :pos] (ch/framed [x 0])}}}})
|
||||
|
||||
(defn cel
|
||||
"A clip of the sequence. PARENTLESS, because the symbol is the container."
|
||||
[id source at duration speed]
|
||||
{:id id :kind :instance :z (name id)
|
||||
:source {:symbol source} :playback {:in 0 :speed speed :end :stop}
|
||||
:time {:at at :rate 1} :span [0 duration]})
|
||||
|
||||
(defn document
|
||||
"`:main`, drawn as a lane, holding three clips and a background shape.
|
||||
|
||||
`:plate` HAS NO SPAN, so it is on screen for the whole shot and is not in the
|
||||
sequence at all — which is what `symbol/children` skips, and the reason an
|
||||
ordinary shape sitting in a lane symbol is not something an edge edit can trim."
|
||||
[]
|
||||
(let [a (cel :a :drawing-a 0 4 0)
|
||||
b (assoc-in (cel :b :drawing-b 4 4 0)
|
||||
[:channels [:xform :pos]] (ch/keyed {0 [0 0] 1 [2 0]} :hold))
|
||||
insert (assoc-in (cel :insert :wave 8 4 1) [:playback :in] 3)]
|
||||
{:name "cels" :fps 24 :width 320 :height 200
|
||||
:symbols
|
||||
{:main {:id :main :frames 12 :display :lane
|
||||
:nodes {:a a :b b :insert insert
|
||||
:plate {:id :plate :kind :rect :z "a"
|
||||
:channels {[:geom :size] (ch/framed 10)
|
||||
[:xform :pos] (ch/keyed {0 [-40 0] 11 [70 0]} :linear)}}}}
|
||||
:drawing-a (drawing :drawing-a 10 1)
|
||||
:drawing-b (drawing :drawing-b 20 1)
|
||||
:wave (assoc-in (drawing :wave 0 10) [:nodes :mark :channels [:xform :pos]]
|
||||
(ch/keyed {0 [0 0] 9 [900 0]} :linear))}}))
|
||||
|
||||
(defn shot
|
||||
"`doc` with `:main` placed in a containing symbol as the instance `:girl`,
|
||||
carrying the keyed transform the LANE GROUP used to carry, and the plate moved
|
||||
out to be the background that is not in the sequence.
|
||||
|
||||
THE PLACING INSTANCE IS WHERE THE LANE'S TRANSFORM WENT. A lane was a group
|
||||
and could be animated; a symbol cannot, so what moves a whole sequence is now
|
||||
the instance that places it. These are the tests that used to animate `:girl`
|
||||
the group and now animate `:girl` the instance — the same six keys, and the
|
||||
same answer frame for frame."
|
||||
[doc]
|
||||
(-> doc
|
||||
(update-in [:symbols :main :nodes] dissoc :plate)
|
||||
(assoc-in [:symbols :shot]
|
||||
{:id :shot :frames 12 :fps 24
|
||||
:nodes {:girl {:id :girl :kind :instance :z "b" :span [0 12]
|
||||
:time {:mode :map :at 0 :rate 1}
|
||||
:source {:symbol :main}
|
||||
:playback {:in 0 :speed 1 :end :stop}
|
||||
:channels {[:xform :pos]
|
||||
(ch/keyed {0 [0 0] 6 [60 0] 12 [0 0]} :linear)}}
|
||||
:plate (get-in doc [:symbols :main :nodes :plate])}})))
|
||||
|
||||
(defn sample-in [doc sid fs]
|
||||
(let [r (clip/resolver doc sid nil pal/index-of nil)]
|
||||
(into {} (map (fn [f] [f (into {} (map (juxt :node :cx)) (r f))])) fs)))
|
||||
|
||||
(defn sample [doc fs] (sample-in doc :main fs))
|
||||
|
||||
(deftest one-sequence-mixes-held-drawings-and-playing-content
|
||||
(let [doc (document) at (sample doc (range 12))]
|
||||
(is (empty? (clip/problems doc)))
|
||||
(is (= 10 (get-in at [3 [:a :mark]])))
|
||||
(is (= 20 (get-in at [4 [:b :mark]])))
|
||||
(is (= 22 (get-in at [5 [:b :mark]])))
|
||||
(is (= 300 (get-in at [8 [:insert :mark]])))
|
||||
(is (= 600 (get-in at [11 [:insert :mark]])))
|
||||
(is (= (zipmap (range 12) (range -40 80 10))
|
||||
(into {} (map (fn [[f ops]] [f (js/Math.round (:plate ops))])) at)))
|
||||
(is (nil? (get-in at [4 [:a :mark]])) "half-open cuts have a single owner")))
|
||||
|
||||
(deftest the-placing-instance-animates-the-whole-sequence
|
||||
;; What `:girl` the lane group used to do, `:girl` the instance does: one
|
||||
;; transform over whichever drawing is showing under it, every frame.
|
||||
(let [doc (shot (document)) at (sample-in doc :shot (range 12))]
|
||||
(is (empty? (clip/problems doc)))
|
||||
(is (= 40 (get-in at [3 [:girl :a :mark]])))
|
||||
(is (= 60 (get-in at [4 [:girl :b :mark]])))
|
||||
(is (= 72 (get-in at [5 [:girl :b :mark]])))
|
||||
(is (= 340 (get-in at [8 [:girl :insert :mark]])))
|
||||
(is (= 610 (get-in at [11 [:girl :insert :mark]])))
|
||||
(is (= (zipmap (range 12) (range -40 80 10))
|
||||
(into {} (map (fn [[f ops]] [f (js/Math.round (:plate ops))])) at))
|
||||
"and the background, which is not in the sequence, does not move with it")))
|
||||
|
||||
(deftest cel-ripple-keeps-the-containers-keys-and-moves-cel-corrections
|
||||
(let [doc (shot (document))
|
||||
result (span/extend-hold doc :main :a 2 {:extent :grow-symbol})
|
||||
after (:clip result)
|
||||
nodes (get-in after [:symbols :main :nodes])]
|
||||
(is (= :a (:selection result)))
|
||||
(is (= 14 (get-in after [:symbols :main :frames])))
|
||||
(is (= [0 6] (node/placed-span (:a nodes))))
|
||||
(is (= [6 10] (node/placed-span (:b nodes))))
|
||||
(is (= [10 14] (node/placed-span (:insert nodes))))
|
||||
(doseq [id [:a :b :insert]]
|
||||
(is (= (get-in doc [:symbols :main :nodes id :channels]) (:channels (nodes id))))
|
||||
(is (= (get-in doc [:symbols :main :nodes id :playback]) (:playback (nodes id)))))
|
||||
(is (= (get-in doc [:symbols :shot :nodes :girl])
|
||||
(get-in after [:symbols :shot :nodes :girl]))
|
||||
"re-spanning a sequence does not touch what places it")
|
||||
(let [at (sample-in after :shot [5 6 7 10])]
|
||||
(is (= 60 (get-in at [5 [:girl :a :mark]])))
|
||||
(is (= 80 (get-in at [6 [:girl :b :mark]])))
|
||||
(is (= 72 (get-in at [7 [:girl :b :mark]])) "B's correction follows B")
|
||||
(is (= 320 (get-in at [10 [:girl :insert :mark]])) "insert starts on source frame 3"))
|
||||
(is (empty? (clip/problems after)))
|
||||
(is (= (assoc-in doc [:symbols :main :frames] 14)
|
||||
(:clip (span/extend-hold after :main :a -2 {})))
|
||||
"shrinking restores content, except the explicitly grown shot")))
|
||||
|
||||
(deftest overflow-and-invalid-edits-are-atomic
|
||||
(let [doc (document)
|
||||
result (span/extend-hold doc :main :a 2 {})]
|
||||
(is (:refused result))
|
||||
(is (= 14 (:required-frames result)))
|
||||
(is (not (contains? result :clip)))
|
||||
(doseq [delta [0 -4 0.5 js/NaN]]
|
||||
(is (:refused (span/extend-hold doc :main :a delta {}))))
|
||||
(is (:refused (span/extend-hold doc :main :insert 1 {})))
|
||||
(is (:refused (span/extend-hold doc :main :missing 1 {})))
|
||||
(is (:refused (span/extend-hold (update-in doc [:symbols :main] dissoc :display)
|
||||
:main :a 2 {:extent :grow-symbol}))
|
||||
"lengthening a hold is a sequence edit, so it needs a symbol drawn as one")))
|
||||
|
||||
(deftest dragging-a-cel-edge-trims-neighbours-or-ripples-them
|
||||
(let [doc (document)
|
||||
clips #(mapv node/placed-span (symbol/children (get-in % [:symbols :main :nodes])))
|
||||
plain (:clip (span/resize-out doc :main :a 6 {}))
|
||||
across (:clip (span/resize-out doc :main :a 9 {}))
|
||||
ripple (:clip (span/resize-out doc :main :a 6 {:ripple? true
|
||||
:extent :grow-symbol}))
|
||||
shrink (:clip (span/resize-out doc :main :a 2 {:ripple? true}))]
|
||||
(is (= [[0 6] [6 8] [8 12]] (clips plain))
|
||||
"a normal grow eats the beginning of the adjacent cel")
|
||||
(is (= [[0 9] [9 12]] (clips across))
|
||||
"a long grow removes wholly consumed cels and trims the survivor")
|
||||
(is (= [[0 6] [6 10] [10 14]] (clips ripple))
|
||||
"shift-grow moves every later cel")
|
||||
(is (= [[0 2] [2 6] [6 10]] (clips shrink))
|
||||
"shift-shrink pulls every later cel left")
|
||||
(is (:refused (span/resize-out doc :main :a 0 {})))
|
||||
(is (:refused (span/resize-out doc :main :a 2.5 {})))))
|
||||
|
||||
(deftest outside-lane-mode-an-edge-edit-disturbs-nothing
|
||||
;; The claim-time rule FOLLOWS THE MODE. The same document not drawn as a lane
|
||||
;; is a composition, and things in a composition are allowed to be on screen
|
||||
;; together — so growing one clip over another simply does that.
|
||||
(let [doc (update-in (document) [:symbols :main] dissoc :display)
|
||||
after (:clip (span/resize-out doc :main :a 6 {}))
|
||||
nodes (get-in after [:symbols :main :nodes])]
|
||||
(is (= [0 6] (node/placed-span (:a nodes))))
|
||||
(is (= [4 8] (node/placed-span (:b nodes))) "the neighbour is left exactly as it was")
|
||||
(is (= 1 (count (symbol/overlaps (get-in after [:symbols :main])))))
|
||||
(is (empty? (clip/problems after)) "and an overlap is not a reason a document will not load")))
|
||||
|
||||
(deftest the-middle-of-a-cut-rolls-both-edges
|
||||
(let [doc (document)
|
||||
clips #(mapv node/placed-span (symbol/children (get-in % [:symbols :main :nodes])))
|
||||
rolled (:clip (span/roll doc :main :a :b 6))
|
||||
right-only (:clip (span/resize-in doc :main :b 6))
|
||||
grown-left (:clip (span/resize-in doc :main :b 2))]
|
||||
(is (= [[0 6] [6 8] [8 12]] (clips rolled))
|
||||
"the shared cut moves without moving either clip")
|
||||
(is (= [[0 4] [6 8] [8 12]] (clips right-only))
|
||||
"the right side of the junction trims only the right clip")
|
||||
(is (= [[0 2] [2 8] [8 12]] (clips grown-left))
|
||||
"growing the right clip left trims the neighbour instead of overlapping")
|
||||
(is (:refused (span/roll doc :main :a :b 0)))
|
||||
(is (:refused (span/roll doc :main :a :insert 6)))
|
||||
(is (:refused (span/roll (update-in doc [:symbols :main] dissoc :display) :main :a :b 6))
|
||||
"a roll is a sequence edit")))
|
||||
|
||||
(deftest a-gap-is-an-uncovered-interval
|
||||
(let [doc (update-in (document) [:symbols :main :nodes] dissoc :b)
|
||||
at (sample doc [3 4 7 8])]
|
||||
(is (= #{:plate} (set (keys (at 4)))))
|
||||
(is (= #{:plate} (set (keys (at 7)))))
|
||||
(is (get-in at [8 [:insert :mark]]))
|
||||
(is (empty? (clip/problems doc)))))
|
||||
|
||||
(deftest an-overlap-is-a-bug-report-and-not-a-reason-not-to-load
|
||||
;; It used to be `clip/problems`, which means the document will not load. A
|
||||
;; display hint must never be able to do that, so it is its own diagnostic.
|
||||
(let [clashing (assoc-in (document) [:symbols :main :nodes :b :time :at] 3)]
|
||||
(is (= [[:a :b]] (symbol/overlaps (get-in clashing [:symbols :main]))))
|
||||
(is (empty? (clip/problems clashing))
|
||||
"and the document still loads, to be drawn visibly wrong")
|
||||
(is (empty? (symbol/overlaps (get-in (document) [:symbols :main]))))
|
||||
(is (empty? (clip/problems
|
||||
(update-in (document) [:symbols :main :nodes] dissoc :a :b :insert)))
|
||||
"an empty sequence is valid — a shot is authored before it is filled")
|
||||
(is (seq (clip/problems
|
||||
(assoc-in (document) [:symbols :main :nodes :a :span] [0 ##Inf]))))
|
||||
(is (seq (clip/problems
|
||||
(assoc-in (document) [:symbols :main :nodes :a :playback :speed] -1))))
|
||||
(is (seq (clip/problems
|
||||
(assoc-in (document) [:symbols :main :nodes :a :layout] :sequence)))
|
||||
":layout is not a node field any more")
|
||||
(is (seq (node/problems {:id :old :kind :instance :z "a"
|
||||
:channels {[:source] (ch/framed {:of :wave :in 0})}}))
|
||||
"the obsolete format is rejected")))
|
||||
|
||||
(deftest no-command-can-commit-an-overlap
|
||||
;; THE INVARIANT, ASSERTED OVER THE COMMANDS rather than reasoned about: every
|
||||
;; one of them commits through `span/finish`, so sampling them is enough.
|
||||
;; Sampled, in the style of `drawn`, rather than computing expected spans by
|
||||
;; hand — what is being claimed is that no result holds an overlap, whatever
|
||||
;; the numbers are.
|
||||
(let [doc (document)
|
||||
every-command
|
||||
(concat
|
||||
(for [to (range -1 15)] #(span/resize-out % :main :a to {:extent :grow-symbol}))
|
||||
(for [to (range -1 15)] #(span/resize-out % :main :b to {:ripple? true :extent :grow-symbol}))
|
||||
(for [to (range -1 15)] #(span/resize-in % :main :b to))
|
||||
(for [to (range -1 15)] #(span/roll % :main :a :b to))
|
||||
(for [cut (range -1 15)] #(span/split % :main :insert cut :piece))
|
||||
(for [to (range -1 15)] #(span/trim % :main :insert :out to))
|
||||
(for [to (range -1 15)] #(span/move % :main :insert to))
|
||||
(for [d (range -5 6)] #(span/extend-hold % :main :a d {:extent :grow-symbol}))
|
||||
(for [a (range 0 13) b (range 0 13)] #(span/blank % :main [a b] {:id :rest}))
|
||||
(for [at (range -1 15)] #(span/append-drawing % :main :n :drawing-n
|
||||
{:at at :extent :grow-symbol}))
|
||||
(for [at (range -1 15)] #(span/reuse-drawing % :main :n :drawing-b
|
||||
{:at at :extent :grow-symbol}))
|
||||
(for [at (range -1 15)] #(span/duplicate-drawing % :main :b :n
|
||||
{:at at :extent :grow-symbol}))
|
||||
(for [at (range -1 15)] #(span/overwrite-drawing % :main :n :drawing-n at
|
||||
{:extent :grow-symbol
|
||||
:remainder-id :rest}))
|
||||
(for [at (range -1 15)] #(span/place-symbol % nil :main :n :wave at
|
||||
{:extent :grow-symbol
|
||||
:remainder-id :rest}))
|
||||
(for [at (range -1 15)] #(span/adopt % :main :b at {:extent :grow-symbol
|
||||
:remainder-id :rest}))
|
||||
[#(span/make-unique (:clip (span/reuse-drawing % :main :n :drawing-a
|
||||
{:extent :grow-symbol}))
|
||||
:main :n {})])
|
||||
results (keep (fn [command] (:clip (command doc))) every-command)]
|
||||
(is (< 190 (count results)) "the sample is of commands that actually did something")
|
||||
(doseq [after results]
|
||||
(is (empty? (symbol/overlaps (get-in after [:symbols :main])))
|
||||
(str "a command committed an overlap: "
|
||||
(pr-str (mapv (juxt :id node/placed-span)
|
||||
(symbol/children (get-in after [:symbols :main :nodes]))))))
|
||||
(is (empty? (clip/problems after))))))
|
||||
|
||||
(deftest turning-lane-mode-on-is-the-one-thing-that-can-be-refused
|
||||
(let [composed (-> (document)
|
||||
(update-in [:symbols :main] dissoc :display)
|
||||
(assoc-in [:symbols :main :nodes :b :time :at] 3))
|
||||
refused (span/draw-as-lane composed :main true {})
|
||||
trimmed (:clip (span/draw-as-lane composed :main true {:trim? true}))]
|
||||
(is (:refused refused))
|
||||
(is (= 1 (:required-trim refused)))
|
||||
(is (nil? (:clip refused)) "and nothing moved")
|
||||
(is (= :lane (get-in trimmed [:symbols :main :display])))
|
||||
(is (= [[0 3] [3 7] [8 12]]
|
||||
(mapv node/placed-span (symbol/children (get-in trimmed [:symbols :main :nodes]))))
|
||||
"the retry trims later-claims-from-earlier, the rule everything else follows")
|
||||
(is (empty? (symbol/overlaps (get-in trimmed [:symbols :main]))))
|
||||
(is (empty? (clip/problems trimmed)))
|
||||
;; A clip the next one wholly covers has nothing left to be.
|
||||
(let [buried (assoc-in composed [:symbols :main :nodes :b :time :at] 0)
|
||||
after (:clip (span/draw-as-lane buried :main true {:trim? true}))]
|
||||
(is (nil? (get-in after [:symbols :main :nodes :a])))
|
||||
(is (empty? (symbol/overlaps (get-in after [:symbols :main])))))
|
||||
;; Off is always possible, and changes nothing but the hint.
|
||||
(let [off (:clip (span/draw-as-lane (document) :main false {}))]
|
||||
(is (nil? (get-in off [:symbols :main :display])))
|
||||
(is (= (get-in (document) [:symbols :main :nodes])
|
||||
(get-in off [:symbols :main :nodes]))))
|
||||
(is (:refused (span/draw-as-lane (document) :nothing-here true {})))
|
||||
(is (= :lane (get-in (:clip (span/draw-as-lane (document) :main true {}))
|
||||
[:symbols :main :display]))
|
||||
"a sequence that is already one turns on with nothing to trim")))
|
||||
|
||||
(deftest playback-is-independent-of-property-channel-shape
|
||||
(let [doc (document)
|
||||
n (get-in doc [:symbols :main :nodes :a])
|
||||
keyed (node/toggle-key n [:xform :rot] 0 nil)
|
||||
unkeyed (node/toggle-key keyed [:xform :rot] 0 nil)]
|
||||
(doseq [n [n keyed unkeyed]]
|
||||
(is (= {:symbol :drawing-a :frame 0} (node/placed-frame n 11 1))))
|
||||
(let [n (get-in doc [:symbols :main :nodes :insert])]
|
||||
(is (= {:symbol :wave :frame 5} (node/placed-frame n 2 10)))
|
||||
(is (nil? (node/placed-frame n 7 10)))
|
||||
(is (= 9 (:frame (node/placed-frame (assoc-in n [:playback :end] :hold) 9 10))))
|
||||
(is (= 2 (:frame (node/placed-frame (assoc-in n [:playback :end] :loop) 9 10)))))))
|
||||
|
||||
(deftest navigation-and-hit-testing-use-the-same-source-time
|
||||
(let [doc (document)
|
||||
n (get-in doc [:symbols :main :nodes :insert])]
|
||||
(is (= 5 (:frame (nest/inside doc nil :main [:insert] 10))))
|
||||
(is (= {:at 5 :rate 1} (:time (nest/inside doc nil :main [:insert] 10))))
|
||||
(is (= 0 (:frame (nest/inside doc nil :main [:a] 3))))
|
||||
(is (nil? (:time (nest/inside doc nil :main [:a] 3))))
|
||||
(is (nil? (nest/inside doc nil :main [:a] 4)))
|
||||
(is (= ((pick/bounds-of doc nil :main n) 2)
|
||||
((pick/bounds-of doc nil :main (assoc-in n [:playback :in] 5)) 0)))))
|
||||
|
||||
(deftest seeking-and-source-reuse-do-not-share-cursors
|
||||
(let [doc (assoc-in (document) [:symbols :main :nodes :b :source :symbol] :drawing-a)
|
||||
fs [11 0 5 3 8 4 10 1 6 2 9 7]
|
||||
at (sample doc fs)]
|
||||
(is (= at (sample doc (reverse fs))))
|
||||
(is (= at (sample doc (range 12))))
|
||||
(let [edited (assoc-in doc [:symbols :drawing-a :nodes :mark :channels [:xform :pos]]
|
||||
(ch/framed [99 0]))]
|
||||
(is (= 99 (get-in (sample edited [0 4]) [0 [:a :mark]])))
|
||||
(is (= 99 (get-in (sample edited [0 4]) [4 [:b :mark]]))))))
|
||||
|
||||
(deftest cel-identities-and-playback-round-trip
|
||||
(let [doc (:clip (span/extend-hold (document) :main :a 2 {:extent :grow-symbol}))
|
||||
leaves (leaf/leaves :project doc)]
|
||||
(is (= doc (leaf/clip :project leaves)))
|
||||
(is (contains? leaves "clip/project/symbol/main/node/a"))
|
||||
(is (= :lane (get-in (leaf/clip :project leaves) [:symbols :main :display]))
|
||||
"lane mode is saved like any other field of a symbol")
|
||||
(let [{copied :clip ids :ids}
|
||||
(bring/symbols (assoc-in (clip/blank) [:symbols :drawing-a] (drawing :drawing-a 99 1))
|
||||
doc [:main] {})]
|
||||
(is (= :drawing-a-2 (:drawing-a ids)))
|
||||
(is (= #{:drawing-a-2 :drawing-b :wave} (clip/places copied (:main ids))))
|
||||
(is (empty? (clip/problems copied))))))
|
||||
|
||||
(deftest one-transaction-undoes-the-ripple-and-shot-extension
|
||||
(let [doc (document)
|
||||
after (:clip (span/extend-hold doc :main :a 2 {:extent :grow-symbol}))
|
||||
before-leaves (leaf/leaves :p doc)
|
||||
after-leaves (leaf/leaves :p after)
|
||||
h (-> nil history/hold (history/record before-leaves after-leaves 0) history/settle)
|
||||
undo (history/undo h after-leaves)
|
||||
redo (history/redo (:history undo) (:leaves undo))]
|
||||
(is (= 1 (count (:done h))))
|
||||
(is (= before-leaves (:leaves undo)))
|
||||
(is (= after-leaves (:leaves redo)))))
|
||||
|
||||
(deftest a-new-document-is-ordinary-and-a-lane-is-created-explicitly
|
||||
(let [doc (clip/blank)
|
||||
lane (:clip (span/draw-as-lane doc :main true {}))
|
||||
a (:clip (span/append-drawing lane :main :a :drawing-a {}))
|
||||
b (:clip (span/append-drawing a :main :b :drawing-b {}))]
|
||||
(is (= {} (get-in doc [:symbols :main :nodes])))
|
||||
(is (nil? (get-in doc [:symbols :main :display])))
|
||||
(is (= :lane (get-in lane [:symbols :main :display])))
|
||||
(is (empty? (clip/problems b)))
|
||||
(is (= [1 2] (node/placed-span (get-in b [:symbols :main :nodes :b]))))
|
||||
(is (= 0 (get-in b [:symbols :main :nodes :b :playback :speed])))
|
||||
(is (:refused (span/append-drawing b :main :a :new {})))))
|
||||
|
||||
(deftest arbitrary-symbols-drop-into-the-sequence-and-claim-their-time
|
||||
(let [doc (document)
|
||||
dropped (span/place-symbol doc nil :main :clip :wave 2
|
||||
{:extent :grow-symbol :remainder-id :tail})
|
||||
after (:clip dropped)
|
||||
clips (symbol/children (get-in after [:symbols :main :nodes]))]
|
||||
(is (= :clip (:selection dropped)))
|
||||
(is (= [[0 2] [2 12]] (mapv node/placed-span clips))
|
||||
"the natural ten-frame symbol claims [2,12), trimming/removing incumbents")
|
||||
(is (= :wave (node/source (second clips))))
|
||||
(is (= 1 (:speed (node/playback-of (second clips))))
|
||||
"a dropped symbol plays; it is not converted into a drawing hold")
|
||||
(is (empty? (clip/problems after)))
|
||||
;; Outside lane mode the same drop claims nothing.
|
||||
(let [composed (update-in doc [:symbols :main] dissoc :display)
|
||||
after (:clip (span/place-symbol composed nil :main :clip :wave 2
|
||||
{:extent :grow-symbol :remainder-id :tail}))]
|
||||
(is (= [[0 4] [2 12] [4 8] [8 12]]
|
||||
(mapv node/placed-span (symbol/children (get-in after [:symbols :main :nodes]))))
|
||||
"it is simply placed, on screen with what was already there"))))
|
||||
|
||||
(deftest an-existing-clip-can-be-moved-and-claim-where-it-lands
|
||||
(let [doc (assoc-in (document) [:symbols :main :nodes :badge]
|
||||
{:id :badge :kind :instance :z "z"
|
||||
:source {:symbol :wave} :span [0 3]
|
||||
:time {:at 1 :rate 1}
|
||||
:playback {:in 2 :speed 1 :end :stop}})
|
||||
result (span/adopt doc :main :badge 5
|
||||
{:extent :grow-symbol :remainder-id :tail})
|
||||
after (:clip result)
|
||||
n (get-in after [:symbols :main :nodes :badge])]
|
||||
(is (= [5 8] (node/placed-span n)))
|
||||
(is (= {:in 2 :speed 1 :end :stop} (:playback n))
|
||||
"a move changes placement, not source timing")
|
||||
(is (= [[0 4] [4 5] [5 8] [8 12]]
|
||||
(mapv node/placed-span (symbol/children (get-in after [:symbols :main :nodes])))))
|
||||
(is (empty? (clip/problems after)))
|
||||
(is (:refused (span/adopt doc :main :plate 5 {})) "and a shape has no frames to place")))
|
||||
|
||||
(deftest fractional-placement-rates-convert-the-hold-delta
|
||||
(let [doc (-> (document)
|
||||
(assoc-in [:symbols :main :nodes :a :time :rate] 2)
|
||||
(assoc-in [:symbols :main :nodes :a :span] [0 8]))
|
||||
after (:clip (span/extend-hold doc :main :a 2 {:extent :grow-symbol}))]
|
||||
(is (= [0 12] (get-in after [:symbols :main :nodes :a :span])))
|
||||
(is (= [6 10] (node/placed-span (get-in after [:symbols :main :nodes :b]))))))
|
||||
|
||||
(deftest bare-shapes-agree-in-reference-and-playback
|
||||
(let [sym (drawing :bare 12 1)]
|
||||
(is (= (symbol/eval-frame sym 0 nil pal/index-of nil)
|
||||
((symbol/resolver sym nil pal/index-of nil) 0))
|
||||
"omitted style colour must not crash a missing cursor")))
|
||||
|
||||
(deftest audio-follows-only-the-playing-cel
|
||||
(let [voice {:id :voice :kind :audio :z "a" :source {:sound "voice"}
|
||||
:span [0 10]
|
||||
:channels {[:audio :gain] (ch/keyed {0 0 5 1} :linear)}}
|
||||
doc (-> (document)
|
||||
(assoc-in [:symbols :wave :nodes :voice] voice)
|
||||
(assoc-in [:symbols :drawing-a :nodes :voice] voice))
|
||||
[track :as tracks] (nest/audio-tracks doc :main)]
|
||||
(is (= 1 (count tracks)) "the frozen drawing contributes no audio")
|
||||
(is (= [8 12] (node/placed-span track)))
|
||||
(is (= [3 7] (:span track)) "the source in-point trims the audio too")
|
||||
(is (= {5 0 10 1} (get-in track [:channels [:audio :gain] :keys])))
|
||||
(let [moved (:clip (span/extend-hold doc :main :a 2 {:extent :grow-symbol}))
|
||||
[track] (nest/audio-tracks moved :main)]
|
||||
(is (= [10 14] (node/placed-span track)))
|
||||
(is (= [3 7] (:span track))))
|
||||
;; The retime that used to be the lane group's is the placing instance's.
|
||||
(let [fast (-> (shot doc)
|
||||
(assoc-in [:symbols :shot :nodes :girl :time] {:mode :map :at 2 :rate 2})
|
||||
(assoc-in [:symbols :main :nodes :insert :playback :speed] 2))
|
||||
[track] (nest/audio-tracks fast :shot)]
|
||||
(is (= [6 7.75] (node/placed-span track)))
|
||||
(is (= [3 10] (:span track)))
|
||||
(is (= 4 (get-in track [:time :rate]))))))
|
||||
|
||||
(deftest a-looped-insert-schedules-distinct-audio-intervals
|
||||
(let [doc (-> (document)
|
||||
(assoc-in [:symbols :wave :frames] 4)
|
||||
(assoc-in [:symbols :wave :nodes :voice]
|
||||
{:id :voice :kind :audio :z "a" :source {:sound "v"} :span [1 3]})
|
||||
(assoc-in [:symbols :main :nodes :insert :playback]
|
||||
{:in 3 :speed 1 :end :loop}))]
|
||||
(is (= [[10 12]] (mapv node/placed-span (nest/audio-tracks doc :main))))))
|
||||
|
||||
(deftest the-shot-a-sequence-needs-is-in-its-own-frames
|
||||
;; CHANGED ON PURPOSE. It used to be that an enclosing retime changed how many
|
||||
;; frames an edit needed, because the lane was INSIDE the symbol being measured
|
||||
;; and its clock sat between them. The symbol is now the container, so its
|
||||
;; `:frames` is its own authored window and how fast some instance plays it is
|
||||
;; not a fact about it.
|
||||
(let [doc (shot (document))
|
||||
slow (assoc-in doc [:symbols :shot :nodes :girl :time] {:mode :map :at 8 :rate 2})]
|
||||
(is (= 14 (:required-frames (span/extend-hold slow :main :a 2 {}))))
|
||||
(is (= 14 (:required-frames (span/extend-hold doc :main :a 2 {}))))
|
||||
(is (= 14 (get-in (span/extend-hold slow :main :a 2 {:extent :grow-symbol})
|
||||
[:clip :symbols :main :frames])))))
|
||||
|
||||
(deftest reuse-shares-content-and-make-unique-decouples-one-cel
|
||||
(let [doc (document)
|
||||
shared (:clip (span/reuse-drawing doc :main :c :drawing-a {:extent :grow-symbol}))
|
||||
edit (fn [c sym x]
|
||||
(assoc-in c [:symbols sym :nodes :mark :channels [:xform :pos]]
|
||||
(ch/framed [x 0])))]
|
||||
(is (:refused (span/reuse-drawing doc :main :c :drawing-a {}))
|
||||
"the shot has to be extended on purpose")
|
||||
(is (= :drawing-a (node/source (get-in shared [:symbols :main :nodes :c]))))
|
||||
(is (= [12 13] (node/placed-span (get-in shared [:symbols :main :nodes :c]))))
|
||||
(is (empty? (clip/problems shared)))
|
||||
;; One drawing, two cels: the edit arrives at both.
|
||||
(let [at (sample (edit shared :drawing-a 99) [0 12])]
|
||||
(is (= 99 (get-in at [0 [:a :mark]])))
|
||||
(is (= 99 (get-in at [12 [:c :mark]]))))
|
||||
(let [unique (:clip (span/make-unique shared :main :c {}))]
|
||||
(is (= :drawing-a-2 (node/source (get-in unique [:symbols :main :nodes :c]))))
|
||||
(is (= (:nodes (get-in shared [:symbols :drawing-a]))
|
||||
(:nodes (get-in unique [:symbols :drawing-a-2])))
|
||||
"a copy of the same drawing, not an empty one")
|
||||
(is (= :drawing-a (node/source (get-in unique [:symbols :main :nodes :a])))
|
||||
"the other cel keeps the original")
|
||||
(let [at (sample (edit unique :drawing-a 99) [0 12])]
|
||||
(is (= 99 (get-in at [0 [:a :mark]])))
|
||||
(is (= 10 (get-in at [12 [:c :mark]])) "the cel made unique is untouched"))
|
||||
(let [at (sample (edit unique :drawing-a-2 99) [0 12])]
|
||||
(is (= 10 (get-in at [0 [:a :mark]])) "and does not reach back"))
|
||||
(is (empty? (clip/problems unique))))
|
||||
;; Nothing else places drawing-b, so there is nothing to decouple from.
|
||||
(is (:refused (span/make-unique doc :main :b {})))
|
||||
(is (:refused (span/make-unique doc :main :plate {}))
|
||||
"a shape places nothing itself")))
|
||||
|
||||
(deftest duplicate-copies-the-drawing-and-not-the-cel
|
||||
(let [doc (document)
|
||||
made (:clip (span/duplicate-drawing doc :main :b :d {:extent :grow-symbol}))
|
||||
n (get-in made [:symbols :main :nodes :d])]
|
||||
(is (= :drawing-b-2 (node/source n)))
|
||||
(is (= (:nodes (get-in doc [:symbols :drawing-b]))
|
||||
(:nodes (get-in made [:symbols :drawing-b-2]))))
|
||||
(is (= [12 13] (node/placed-span n)))
|
||||
(is (= {:in 0 :speed 0 :end :stop} (:playback n)))
|
||||
(is (nil? (:channels n)) "B's own position correction belongs to B's cel")
|
||||
(is (= (get-in doc [:symbols :main :nodes :b])
|
||||
(get-in made [:symbols :main :nodes :b]))
|
||||
"the drawing duplicated is left as it was")
|
||||
(is (empty? (clip/problems made)))))
|
||||
|
||||
(deftest a-shallow-copy-keeps-its-parts-and-a-deep-copy-owns-them
|
||||
;; A drawing assembled from another symbol: copying it shallowly must keep
|
||||
;; using that part, and only an explicit deep copy may promise independence.
|
||||
(let [doc (assoc-in (document) [:symbols :drawing-a :nodes :part]
|
||||
{:id :part :kind :instance :z "b" :span [0 1]
|
||||
:time {:at 0 :rate 1} :source {:symbol :wave}
|
||||
:playback {:in 0 :speed 0 :end :stop}})
|
||||
copy (fn [opts] (:clip (span/duplicate-drawing
|
||||
doc :main :a :d (merge {:extent :grow-symbol} opts))))
|
||||
shallow (copy {})
|
||||
deep (copy {:deep? true})]
|
||||
(is (= :wave (node/source (get-in shallow [:symbols :drawing-a-2 :nodes :part]))))
|
||||
(is (nil? (get-in shallow [:symbols :wave-2])))
|
||||
(is (= :wave-2 (node/source (get-in deep [:symbols :drawing-a-2 :nodes :part]))))
|
||||
(is (= (:nodes (get-in doc [:symbols :wave])) (:nodes (get-in deep [:symbols :wave-2]))))
|
||||
(is (empty? (clip/problems shallow)))
|
||||
(is (empty? (clip/problems deep)))))
|
||||
|
||||
(deftest reuse-refuses-what-would-not-be-a-document
|
||||
(let [doc (document)]
|
||||
(is (:refused (span/reuse-drawing doc :main :c :nothing-here {})))
|
||||
(is (:refused (span/reuse-drawing doc :main :c :main {:extent :grow-symbol}))
|
||||
"a symbol cannot go inside itself")
|
||||
(is (:refused (span/reuse-drawing doc :main :a :drawing-a {:extent :grow-symbol}))
|
||||
"a cel ID in use is not free")
|
||||
(is (:refused (span/duplicate-drawing doc :main :plate :d {})))))
|
||||
|
||||
(deftest drawing-on-twos-does-not-quantize-the-containers-transform
|
||||
;; Cel length IS the drawing cadence, and it is the only thing on twos here:
|
||||
;; the instance placing the sequence has its own clock and keeps moving every
|
||||
;; frame. Stepping it would be the cel cadence leaking into continuous motion.
|
||||
(let [cel (fn [id source at] (cel id source at 2 0))
|
||||
doc (-> (shot (document))
|
||||
(update-in [:symbols :main :nodes] dissoc :a :b :insert)
|
||||
(update-in [:symbols :main :nodes] merge
|
||||
{:c0 (cel :c0 :drawing-a 0)
|
||||
:c1 (cel :c1 :drawing-b 2)
|
||||
:c2 (cel :c2 :drawing-a 4)}))
|
||||
xs {:c0 10 :c1 20 :c2 10}
|
||||
at (sample-in doc :shot (range 6))
|
||||
showing (fn [f] (first (dissoc (at f) :plate)))]
|
||||
(is (empty? (clip/problems doc)))
|
||||
(is (= [:c0 :c0 :c1 :c1 :c2 :c2] (mapv #(second (key (showing %))) (range 6)))
|
||||
"the drawing showing changes every second frame")
|
||||
(is (= [0 10 20 30 40 50]
|
||||
(mapv (fn [f] (let [[[_ id _] cx] (showing f)] (- cx (xs id)))) (range 6)))
|
||||
"and the sequence moves on every frame, odd ones included")))
|
||||
|
||||
(defn- drawn
|
||||
"What every frame draws, as sorted values, so a picture can be compared
|
||||
without naming the cels that produced it."
|
||||
[doc sid fs]
|
||||
(let [at (sample-in doc sid fs)]
|
||||
(mapv #(sort (vals (get at %))) fs)))
|
||||
|
||||
(deftest a-drawing-goes-anywhere-in-the-sequence-and-ripples-what-follows
|
||||
(let [doc (shot (document))
|
||||
keys-of #(get-in % [:symbols :shot :nodes :girl :channels [:xform :pos] :keys])
|
||||
spans #(mapv (fn [id] (node/placed-span (get-in % [:symbols :main :nodes id])))
|
||||
[:a :n :b :insert])
|
||||
r (span/append-drawing doc :main :n :drawing-n {:at 4 :extent :grow-symbol})]
|
||||
(is (= [[0 4] [4 5] [5 9] [9 13]] (spans (:clip r))))
|
||||
(is (= 13 (get-in r [:clip :symbols :main :frames])))
|
||||
(is (= (keys-of doc) (keys-of (:clip r)))
|
||||
"the container's keys stay where they were authored")
|
||||
(is (= :n (:selection r)))
|
||||
(is (= 4 (:frame r)))
|
||||
(is (empty? (clip/problems (:clip r))))
|
||||
;; The same command with no room refuses, and says how much it needs.
|
||||
(is (= 13 (:required-frames (span/append-drawing doc :main :n :drawing-n {:at 4}))))
|
||||
;; At the very front everything moves.
|
||||
(is (= [[1 5] [0 1] [5 9] [9 13]]
|
||||
(spans (:clip (span/append-drawing doc :main :n :drawing-n
|
||||
{:at 0 :extent :grow-symbol})))))
|
||||
;; Inside a cel is not a position for another one.
|
||||
(is (re-find #"split it first"
|
||||
(:refused (span/append-drawing doc :main :n :drawing-n
|
||||
{:at 2 :extent :grow-symbol}))))
|
||||
(is (:refused (span/append-drawing doc :main :n :drawing-n
|
||||
{:at -1 :extent :grow-symbol})))
|
||||
(is (:refused (span/append-drawing doc :main :n :drawing-n
|
||||
{:at ##Inf :extent :grow-symbol})))
|
||||
;; Reuse and duplicate take a position too; it is one placement rule.
|
||||
(is (= [4 5] (node/placed-span
|
||||
(get-in (span/reuse-drawing doc :main :n :drawing-b
|
||||
{:at 4 :extent :grow-symbol})
|
||||
[:clip :symbols :main :nodes :n]))))
|
||||
(is (= [4 5] (node/placed-span
|
||||
(get-in (span/duplicate-drawing doc :main :b :n
|
||||
{:at 4 :extent :grow-symbol})
|
||||
[:clip :symbols :main :nodes :n]))))))
|
||||
|
||||
(deftest split-then-place-puts-a-drawing-inside-a-hold
|
||||
;; The two commands the doc asks for, composed: neither one guesses.
|
||||
(let [doc (shot (document))
|
||||
cut (:clip (span/split doc :main :a 2 :right))
|
||||
r (span/append-drawing cut :main :n :drawing-n {:at 2 :extent :grow-symbol})
|
||||
after (:clip r)]
|
||||
(is (= [[0 2] [2 3] [3 5] [5 9] [9 13]]
|
||||
(mapv #(node/placed-span (get-in after [:symbols :main :nodes %]))
|
||||
[:a :n :right :b :insert])))
|
||||
(is (= (get-in doc [:symbols :shot :nodes :girl :channels])
|
||||
(get-in after [:symbols :shot :nodes :girl :channels]))
|
||||
"the performance is still timed the way it was authored")
|
||||
(is (empty? (clip/problems after)))))
|
||||
|
||||
(deftest a-three-frame-correction-crosses-a-drawing-boundary
|
||||
;; The lane model's worked example, with the correction on the INSTANCE that
|
||||
;; places the sequence: it applies across whichever drawings are showing under
|
||||
;; it, and outside its three frames the animation evaluates exactly as before.
|
||||
(let [doc (shot (document))
|
||||
fs (range 12)
|
||||
before (drawn doc :shot fs)
|
||||
beat (ch/layer :beat [3 6] :offset (ch/framed [30 0]))
|
||||
c (update-in doc [:symbols :shot :nodes :girl :channels [:xform :pos] :over]
|
||||
(fnil conj []) beat)
|
||||
after (drawn c :shot fs)
|
||||
outside [0 1 2 6 7 8 9 10 11]]
|
||||
(is (empty? (clip/problems c)))
|
||||
(is (= (mapv before outside) (mapv after outside))
|
||||
"outside the support, frame for frame identical")
|
||||
(let [at (sample-in c :shot [3 4 5])]
|
||||
;; Frame 3 shows drawing A and frames 4 and 5 show drawing B: one
|
||||
;; correction, reaching across the cut between them.
|
||||
(is (= 70 (get-in at [3 [:girl :a :mark]])))
|
||||
(is (= 90 (get-in at [4 [:girl :b :mark]])))
|
||||
(is (= 102 (get-in at [5 [:girl :b :mark]]))
|
||||
"and B's own correction still applies under it")
|
||||
(is (= [-10 0 10] (mapv (fn [f] (js/Math.round (get-in at [f :plate]))) [3 4 5]))
|
||||
"while the background, which is not in the sequence, does not move"))
|
||||
;; One document change: one step, and it persists in the channel's own leaf.
|
||||
(let [b (leaf/leaves :p doc)
|
||||
a (leaf/leaves :p c)
|
||||
h (-> nil history/hold (history/record b a 0) history/settle)]
|
||||
(is (= 1 (count (:done h))))
|
||||
(is (= b (:leaves (history/undo h a))))
|
||||
(is (= c (leaf/clip :p a)) "a correction needs no codec of its own"))))
|
||||
|
||||
(deftest a-correction-on-one-cel-travels-with-it
|
||||
;; The other half of ownership: a layer on a cel is in that cel's own frames,
|
||||
;; so moving the cel moves the correction and nothing has to say so.
|
||||
(let [beat (ch/layer :beat [0 2] :offset (ch/framed [7 0]))
|
||||
doc (update-in (document) [:symbols :main :nodes :b :channels [:xform :pos] :over]
|
||||
(fnil conj []) beat)
|
||||
moved (:clip (span/extend-hold doc :main :a 2 {:extent :grow-symbol}))]
|
||||
;; Stated as the difference from the same document without the correction,
|
||||
;; so the claim is about WHERE the layer applies and not about arithmetic.
|
||||
(let [nudge (fn [with without f]
|
||||
(- (get-in (sample with [f]) [f [:b :mark]])
|
||||
(get-in (sample without [f]) [f [:b :mark]])))]
|
||||
(is (= [7 7 0 0] (mapv #(nudge doc (document) %) [4 5 6 7]))
|
||||
"B's first two frames, 4 and 5")
|
||||
(is (= [7 7 0 0]
|
||||
(mapv #(nudge moved (:clip (span/extend-hold (document) :main :a 2
|
||||
{:extent :grow-symbol}))
|
||||
%)
|
||||
[6 7 8 9]))
|
||||
"and after A's hold grows, B's first two frames, which are now 6 and 7"))
|
||||
(is (= (get-in doc [:symbols :main :nodes :b :channels])
|
||||
(get-in moved [:symbols :main :nodes :b :channels]))
|
||||
"the layer itself was not touched by the retiming")
|
||||
(is (empty? (clip/problems moved)))))
|
||||
|
||||
(defn- spans [clip ids]
|
||||
(mapv #(node/placed-span (get-in clip [:symbols :main :nodes %])) ids))
|
||||
|
||||
(deftest blanking-leaves-a-gap-and-does-not-close-it
|
||||
(let [doc (document)
|
||||
r (span/blank doc :main [5 7] {:id :rest})
|
||||
after (:clip r)]
|
||||
;; B spanned the range, so it became two cels with a hole between them.
|
||||
(is (= [[0 4] [4 5] [7 8] [8 12]] (spans after [:a :b :rest :insert])))
|
||||
(is (= :rest (:selection r)))
|
||||
(let [at (sample after [4 5 6 7])]
|
||||
(is (= #{:plate} (set (keys (at 5)))) "nothing is drawn on a blanked frame")
|
||||
(is (= #{:plate} (set (keys (at 6)))))
|
||||
(is (get-in at [4 [:b :mark]]))
|
||||
(is (get-in at [7 [:rest :mark]])))
|
||||
(is (= 12 (get-in after [:symbols :main :frames])))
|
||||
(is (empty? (clip/problems after)))))
|
||||
|
||||
(deftest blanking-a-whole-cel-removes-it-and-keeps-its-drawing
|
||||
(let [doc (document)
|
||||
r (span/blank doc :main [4 8] {})
|
||||
after (:clip r)]
|
||||
(is (nil? (get-in after [:symbols :main :nodes :b])))
|
||||
(is (= [[0 4] [8 12]] (spans after [:a :insert])) "and moves nothing")
|
||||
(is (nil? (:selection r))
|
||||
"and names nothing, because emptying frames selects nothing sensible")
|
||||
(is (= (get-in doc [:symbols :drawing-b]) (get-in after [:symbols :drawing-b]))
|
||||
"a symbol does not own its content")
|
||||
(is (empty? (clip/problems after)))))
|
||||
|
||||
(deftest blanking-a-range-trims-what-it-only-partly-covers
|
||||
(let [doc (document)
|
||||
after (:clip (span/blank doc :main [3 9] {}))]
|
||||
(is (= [[0 3] [9 12]] (spans after [:a :insert])))
|
||||
(is (nil? (get-in after [:symbols :main :nodes :b])))
|
||||
(is (= (get-in (sample doc [9]) [9 [:insert :mark]])
|
||||
(get-in (sample after [9]) [9 [:insert :mark]]))
|
||||
"the insert kept its own frames, so frame 9 shows what it showed")
|
||||
(is (empty? (clip/problems after)))))
|
||||
|
||||
(deftest overwrite-clears-one-frame-and-does-not-ripple-what-follows
|
||||
(let [r (span/overwrite-drawing (document) :main :n :drawing-n 5
|
||||
{:extent :keep :remainder-id :right})
|
||||
after (:clip r)
|
||||
nodes (get-in after [:symbols :main :nodes])]
|
||||
(is (= :n (:selection r)))
|
||||
(is (= [[0 4] [4 5] [5 6] [6 8] [8 12]]
|
||||
(mapv #(node/placed-span (get nodes %)) [:a :b :n :right :insert])))
|
||||
(is (= :drawing-b (node/source (:right nodes))))
|
||||
(is (= 12 (get-in after [:symbols :main :frames])))
|
||||
(is (empty? (clip/problems after)))))
|
||||
|
||||
(deftest blank-refuses-what-it-cannot-do-in-one-piece
|
||||
(let [doc (document)]
|
||||
(is (re-find #"free ID" (:refused (span/blank doc :main [5 7] {})))
|
||||
"splitting a cel needs an ID for the remainder")
|
||||
(is (:refused (span/blank doc :main [5 7] {:id :a})) "and a free one")
|
||||
(is (:refused (span/blank doc :main [7 5] {})))
|
||||
(is (:refused (span/blank doc :main [5 5] {})))
|
||||
(is (:refused (span/blank doc :main [5 6.5] {})))
|
||||
(is (:refused (span/blank (update-in doc [:symbols :main] dissoc :display)
|
||||
:main [0 2] {}))
|
||||
"and a composition has no sequence to leave a hole in")))
|
||||
|
||||
(deftest the-shot-length-is-authored-and-emptying-it-does-not-shorten-it
|
||||
;; The window and the occupied extent are two facts. A shot with nothing in
|
||||
;; the last half is a shot somebody authored that long, and deleting the last
|
||||
;; drawing must not quietly shorten the film.
|
||||
(let [doc (document)
|
||||
emptied (:clip (span/blank doc :main [0 12] {}))]
|
||||
(is (empty? (symbol/children (get-in emptied [:symbols :main :nodes]))))
|
||||
(is (= 12 (get-in emptied [:symbols :main :frames])))
|
||||
(is (empty? (clip/problems emptied)))
|
||||
;; Growing is still the caller's word, and only ever grows.
|
||||
(is (:refused (span/append-drawing emptied :main :n :drawing-n {:at 20})))
|
||||
(is (= 21 (get-in (span/append-drawing emptied :main :n :drawing-n
|
||||
{:at 20 :extent :grow-symbol})
|
||||
[:clip :symbols :main :frames])))
|
||||
(is (= 12 (get-in (:clip (span/trim doc :main :insert :out 9))
|
||||
[:symbols :main :frames]))
|
||||
"and trimming the last cel leaves the window where it was")))
|
||||
|
||||
(deftest a-take-placed-in-a-sequence-is-still-heard
|
||||
;; `bring/take` puts a take's sound INSIDE the symbol it makes, so that
|
||||
;; "wherever the symbol is placed it is heard". A sequence is one of the places
|
||||
;; it can be placed, and must not be the one place that goes silent.
|
||||
(let [doc (assoc-in (document) [:symbols :take]
|
||||
{:id :take :frames 10 :fps 24
|
||||
:nodes {:pic {:id :pic :kind :instance :z "a"
|
||||
:source {:symbol :wave} :span [0 10]
|
||||
:time {:mode :map :at 0 :rate 1}
|
||||
:playback {:in 0 :speed 1 :end :stop}}
|
||||
:sound {:id :sound :name "sound" :kind :audio
|
||||
:parent nil :z "z-sound"
|
||||
:source {:footage "f1"} :span [0 10]
|
||||
:time {:mode :map :at 0 :rate 1}}}})
|
||||
at-root (clip/place-symbol doc nil :main :take 0 :root nil)
|
||||
placed (:clip (span/place-symbol doc nil :main :drop :take 0
|
||||
{:extent :grow-symbol :remainder-id :tail}))]
|
||||
(is (= 1 (count (nest/audio-tracks at-root :main)))
|
||||
"a take placed at the root is heard")
|
||||
(is (some? placed) "the take goes into the sequence")
|
||||
(is (= 1 (count (nest/audio-tracks placed :main)))
|
||||
"and is still heard from inside one")))
|
||||
|
||||
(deftest a-sound-is-a-clip-of-a-sequence-like-any-other
|
||||
;; A sound claims time by the same rule as a picture, and THERE IS NO MIXTURE
|
||||
;; RULE ANY MORE: a symbol whose clips are sounds is an audio lane, and that is
|
||||
;; the whole of it. The refusal that used to say "picture or sound, not both"
|
||||
;; was a property of a lane node, and there is no lane node.
|
||||
(let [seeded (clip/place-sound (document) :main {:sound "s1"} "voice" 6 1 2 :vo)
|
||||
result (span/adopt seeded :main :vo 2 {:extent :grow-symbol})
|
||||
after (:clip result)
|
||||
n (get-in after [:symbols :main :nodes :vo])]
|
||||
(is (nil? (:refused result)) (str (:refused result)))
|
||||
(is (= [2 8] (node/placed-span n)))
|
||||
(is (empty? (clip/problems after)))
|
||||
(is (= 1 (count (nest/audio-tracks after :main)))
|
||||
"a sound in a sequence is still heard")
|
||||
(is (contains? (set (map :id (symbol/children (get-in after [:symbols :main :nodes]))))
|
||||
:vo))
|
||||
;; Picture over sound is picture claiming the frames, like anything else.
|
||||
(let [mixed (:clip (span/place-symbol after nil :main :also :wave 2
|
||||
{:extent :grow-symbol :remainder-id :rest}))]
|
||||
(is (empty? (symbol/overlaps (get-in mixed [:symbols :main]))))
|
||||
(is (empty? (clip/problems mixed))))))
|
||||
|
|
@ -1,53 +1,22 @@
|
|||
(ns arthur.domain.span-test
|
||||
"Split, trim and move, over the two things they have to work on alike: a cel
|
||||
inside a lane, and a symbol placed straight into a shot. The fixture carries
|
||||
both on purpose — the commands were lane-gated for as long as a lane was the
|
||||
only thing anybody had timed, and the point of these tests is that nothing in
|
||||
them reads a lane."
|
||||
"Generic span edits in an explicit lane and an ordinary compositing symbol."
|
||||
(:require [cljs.test :refer [deftest is testing]]
|
||||
[arthur.domain.channel :as ch]
|
||||
[arthur.domain.clip :as clip]
|
||||
[arthur.domain.lane :as lane]
|
||||
[arthur.domain.node :as node]
|
||||
[arthur.domain.palette :as pal]
|
||||
[arthur.domain.sequence-test :as fixture]
|
||||
[arthur.domain.span :as span]))
|
||||
|
||||
(defn- drawing [id x frames]
|
||||
{:id id :frames frames
|
||||
:nodes {:mark {:id :mark :kind :rect :z "a"
|
||||
:channels {[:geom :size] (ch/framed 4)
|
||||
[:xform :pos] (ch/framed [x 0])}}}})
|
||||
(defn document [] (fixture/document))
|
||||
|
||||
(defn- cel [id source at duration speed]
|
||||
{:id id :kind :instance :parent :girl :z (name id)
|
||||
:source {:symbol source} :playback {:in 0 :speed speed :end :stop}
|
||||
:time {:at at :rate 1} :span [0 duration]})
|
||||
|
||||
(defn document
|
||||
"A lane of three cels, and — the part lane_test's fixture has no equivalent of
|
||||
— `:badge`, an instance of an animated symbol placed straight into `:main`
|
||||
with a span of its own and no parent at all. Its frames are the SYMBOL's, so
|
||||
it is the case where the coordinate a command takes is not lane time."
|
||||
[]
|
||||
(let [a (cel :a :drawing-a 0 4 0)
|
||||
b (cel :b :drawing-b 4 4 0)
|
||||
insert (assoc-in (cel :insert :wave 8 4 1) [:playback :in] 3)]
|
||||
{:name "spans" :fps 24 :width 320 :height 200
|
||||
:symbols
|
||||
{:main {:id :main :frames 12
|
||||
:nodes {:girl {:id :girl :kind :group :layout :sequence :z "b"}
|
||||
:a a :b b :insert insert
|
||||
:badge {:id :badge :kind :instance :z "c"
|
||||
(defn- ordinary-document []
|
||||
(-> (document)
|
||||
(update-in [:symbols :main] dissoc :display)
|
||||
(assoc-in [:symbols :main :nodes :badge]
|
||||
{:id :badge :kind :instance :z "z"
|
||||
:source {:symbol :wave}
|
||||
:playback {:in 0 :speed 1 :end :stop}
|
||||
:time {:at 2 :rate 1} :span [0 8]}
|
||||
:plate {:id :plate :kind :rect :z "a"
|
||||
:channels {[:geom :size] (ch/framed 10)
|
||||
[:xform :pos] (ch/keyed {0 [-40 0] 11 [70 0]} :linear)}}}}
|
||||
:drawing-a (drawing :drawing-a 10 1)
|
||||
:drawing-b (drawing :drawing-b 20 1)
|
||||
:wave (assoc-in (drawing :wave 0 10) [:nodes :mark :channels [:xform :pos]]
|
||||
(ch/keyed {0 [0 0] 9 [900 0]} :linear))}}))
|
||||
:time {:at 2 :rate 1} :span [0 8]})))
|
||||
|
||||
(defn- sample [doc fs]
|
||||
(let [r (clip/resolver doc :main nil pal/index-of nil)]
|
||||
|
|
@ -57,172 +26,67 @@
|
|||
(let [at (sample doc fs)]
|
||||
(mapv #(sort (vals (get at %))) fs)))
|
||||
|
||||
(defn- spans [clip ids]
|
||||
(mapv #(node/placed-span (get-in clip [:symbols :main :nodes %])) ids))
|
||||
(deftest fixtures-state-the-mode-explicitly
|
||||
(is (= :lane (get-in (document) [:symbols :main :display])))
|
||||
(is (nil? (get-in (ordinary-document) [:symbols :main :display])))
|
||||
(is (empty? (clip/problems (document))))
|
||||
(is (empty? (clip/problems (ordinary-document)))))
|
||||
|
||||
(deftest the-fixture-places-one-thing-outside-the-lane
|
||||
(let [doc (document)]
|
||||
(is (empty? (clip/problems doc)))
|
||||
(is (nil? (:parent (get-in doc [:symbols :main :nodes :badge])))
|
||||
"so a command acting on it has only the symbol's frames to go by")
|
||||
(is (= [2 10] (node/placed-span (get-in doc [:symbols :main :nodes :badge]))))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; split
|
||||
|
||||
(deftest splitting-changes-nothing-that-is-drawn
|
||||
(let [doc (document)
|
||||
fs (range 12)
|
||||
before (drawn doc fs)]
|
||||
(doseq [[label id cut] [["a held drawing in a lane" :a 2]
|
||||
["a playing insert in a lane" :insert 10]
|
||||
["a placement with no lane at all" :badge 6]]]
|
||||
(deftest splitting-preserves-the-picture-in-both-modes
|
||||
(doseq [[label doc id cut] [["lane clip" (document) :a 2]
|
||||
["ordinary placement" (ordinary-document) :badge 6]]]
|
||||
(testing label
|
||||
(let [r (span/split doc :main id cut :right)
|
||||
(let [before (drawn doc (range 12))
|
||||
r (span/split doc :main id cut :right)
|
||||
after (:clip r)]
|
||||
(is (= :right (:selection r)))
|
||||
(is (= before (drawn after fs)) "the same picture, frame for frame")
|
||||
(is (= (node/placed-span (get-in doc [:symbols :main :nodes id]))
|
||||
[(first (node/placed-span (get-in after [:symbols :main :nodes id])))
|
||||
(second (node/placed-span (get-in after [:symbols :main :nodes :right])))])
|
||||
"the pieces occupy the frames the one node did")
|
||||
(is (= cut (second (node/placed-span (get-in after [:symbols :main :nodes id])))
|
||||
(is (= before (drawn after (range 12))))
|
||||
(is (= cut
|
||||
(second (node/placed-span (get-in after [:symbols :main :nodes id])))
|
||||
(first (node/placed-span (get-in after [:symbols :main :nodes :right])))))
|
||||
(is (= (:time (get-in doc [:symbols :main :nodes id]))
|
||||
(:time (get-in after [:symbols :main :nodes :right])))
|
||||
"one time map, so the right piece's own frames carry on")
|
||||
(is (= (select-keys (get-in doc [:symbols :main :nodes id])
|
||||
[:source :playback :channels :parent :z])
|
||||
(select-keys (get-in after [:symbols :main :nodes :right])
|
||||
[:source :playback :channels :parent :z]))
|
||||
"and it keeps its parent and its depth, so it draws where it drew")
|
||||
(is (= 12 (get-in after [:symbols :main :frames])) "and no shot-length question")
|
||||
(is (empty? (clip/problems after))))))))
|
||||
(is (empty? (clip/problems after)))))))
|
||||
|
||||
(deftest split-refuses-anything-but-one-cut-inside-one-thing
|
||||
(deftest split-refuses-an-edge-a-missing-node-and-a-spanless-node
|
||||
(let [doc (document)]
|
||||
(doseq [cut [0 4 8 12 -1 2.5 ##NaN nil]]
|
||||
(is (:refused (span/split doc :main :b cut :right)) (str "cut at " (pr-str cut))))
|
||||
(is (:refused (span/split doc :main :a 2 :b)) "the new ID has to be free")
|
||||
(doseq [cut [0 4 -1 2.5 ##NaN nil]]
|
||||
(is (:refused (span/split doc :main :a cut :right))))
|
||||
(is (:refused (span/split doc :main :missing 2 :right)))
|
||||
(is (re-find #"group" (:refused (span/split doc :main :girl 2 :right)))
|
||||
"a group is divided by its children, not by its span")
|
||||
(is (re-find #"whole shot" (:refused (span/split doc :main :plate 2 :right)))
|
||||
"and a node with no span has no edges to cut")))
|
||||
(is (re-find #"whole shot" (:refused (span/split doc :main :plate 2 :right))))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; trim
|
||||
|
||||
(deftest trimming-narrows-one-thing-and-moves-nothing-else
|
||||
(let [doc (document)]
|
||||
(doseq [[label id edge to kept] [["a cel in a lane" :b :out 6 [4 6]]
|
||||
["a placement outside one" :badge :out 7 [2 7]]
|
||||
["the front of one outside a lane" :badge :in 5 [5 10]]]]
|
||||
(testing label
|
||||
(let [r (span/trim doc :main id edge to)
|
||||
(deftest trimming-only-narrows-the-selected-placement
|
||||
(doseq [[doc id edge to kept] [[(document) :b :out 6 [4 6]]
|
||||
[(ordinary-document) :badge :in 5 [5 10]]]]
|
||||
(let [before (get-in doc [:symbols :main :nodes id])
|
||||
r (span/trim doc :main id edge to)
|
||||
after (:clip r)]
|
||||
(is (= kept (node/placed-span (get-in after [:symbols :main :nodes id]))))
|
||||
(is (= id (:selection r)))
|
||||
(is (= (select-keys (get-in doc [:symbols :main :nodes id])
|
||||
[:time :playback :channels :source])
|
||||
(select-keys (get-in after [:symbols :main :nodes id])
|
||||
[:time :playback :channels :source]))
|
||||
"only :span changed")
|
||||
(is (= [[0 4] [8 12]] (spans after [:a :insert])) "and no neighbour moved")
|
||||
(is (= 12 (get-in after [:symbols :main :frames])))
|
||||
(is (empty? (clip/problems after))))))))
|
||||
(is (= kept (node/placed-span (get-in after [:symbols :main :nodes id]))))
|
||||
(is (= (dissoc before :span)
|
||||
(dissoc (get-in after [:symbols :main :nodes id]) :span)))
|
||||
(is (empty? (clip/problems after))))))
|
||||
|
||||
(deftest trimming-the-front-does-not-restart-what-is-playing
|
||||
;; The difference between trimming and slipping, asserted on the node that has
|
||||
;; no lane: its own frames are where they were, so the frames that survive
|
||||
;; show exactly what they showed.
|
||||
(let [doc (document)
|
||||
before (sample doc [6 7])
|
||||
after (:clip (span/trim doc :main :badge :in 6))]
|
||||
(is (= [6 10] (node/placed-span (get-in after [:symbols :main :nodes :badge]))))
|
||||
(is (= (:playback (get-in doc [:symbols :main :nodes :badge]))
|
||||
(:playback (get-in after [:symbols :main :nodes :badge]))))
|
||||
(is (= (get-in before [6 [:badge :mark]])
|
||||
(get-in (sample after [6]) [6 [:badge :mark]]))
|
||||
"the same animation on the frames it kept")
|
||||
(is (nil? (get-in (sample after [5]) [5 [:badge :mark]]))
|
||||
"and the frames it gave up show nothing of it")))
|
||||
|
||||
(deftest trim-refuses-to-lengthen-or-to-land-on-an-edge
|
||||
(let [doc (document)]
|
||||
(doseq [[label id edge to] [["at its own start" :b :in 4]
|
||||
["at its own end" :b :out 8]
|
||||
["past its end" :b :out 9]
|
||||
["before its start" :b :in 2]
|
||||
["off a whole frame" :b :out 5.5]
|
||||
["past the end of one outside a lane" :badge :out 11]
|
||||
["before the start of one outside a lane" :badge :in 1]]]
|
||||
(is (:refused (span/trim doc :main id edge to)) label))
|
||||
(is (:refused (span/trim doc :main :b :middle 6)))
|
||||
(is (re-find #"group" (:refused (span/trim doc :main :girl :out 6))))))
|
||||
|
||||
(deftest timeline-edge-resize-allows-an-ordinary-clip-to-grow
|
||||
(let [after (:clip (span/resize-out (document) :main :badge 11))]
|
||||
(deftest resizing-an-ordinary-placement-may-overlap
|
||||
(let [doc (ordinary-document)
|
||||
after (:clip (span/resize-out doc :main :badge 11 {}))]
|
||||
(is (= [2 11] (node/placed-span (get-in after [:symbols :main :nodes :badge]))))
|
||||
(is (:refused (span/resize-out (document) :main :badge 2)))))
|
||||
(is (:refused (span/resize-out doc :main :badge 2 {})))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; move
|
||||
(deftest moving-follows-the-symbol-mode
|
||||
(let [lane (document)
|
||||
ordinary (ordinary-document)]
|
||||
(is (:refused (span/move lane :main :insert 6)) "a lane clip cannot overlap B")
|
||||
(let [after (:clip (span/move ordinary :main :badge 0))]
|
||||
(is (= [0 8] (node/placed-span (get-in after [:symbols :main :nodes :badge])))
|
||||
"an ordinary symbol is free to composite over occupied frames")
|
||||
(is (empty? (clip/problems after))))))
|
||||
|
||||
(deftest moving-keeps-its-length-and-its-source-origin
|
||||
(let [doc (update-in (document) [:symbols :main :nodes] dissoc :b)
|
||||
r (span/move doc :main :insert 4)
|
||||
after (:clip r)]
|
||||
(is (= [[0 4] [4 8]] (spans after [:a :insert])))
|
||||
(is (= :insert (:selection r)))
|
||||
(is (= (:playback (get-in doc [:symbols :main :nodes :insert]))
|
||||
(:playback (get-in after [:symbols :main :nodes :insert]))))
|
||||
;; It began on source frame 3 at lane 8; it begins on source frame 3 at lane 4.
|
||||
(is (= (get-in (sample doc [8]) [8 [:insert :mark]])
|
||||
(get-in (sample after [4]) [4 [:insert :mark]])))
|
||||
(deftest clearing-room-then-moving-is-explicit-composition
|
||||
(let [doc (document)
|
||||
cleared (:clip (span/blank doc :main [4 8] {}))
|
||||
after (:clip (span/move cleared :main :insert 4))]
|
||||
(is (= [4 8] (node/placed-span (get-in after [:symbols :main :nodes :insert]))))
|
||||
(is (empty? (clip/problems after)))))
|
||||
|
||||
(deftest a-move-outside-a-lane-is-free-to-land-on-an-occupied-frame
|
||||
;; The non-overlap rule is the LANE's, and `:badge` is not in one. Things
|
||||
;; placed in a composition are allowed to be on screen together, so there is
|
||||
;; nothing here for a move to refuse.
|
||||
(let [doc (document)
|
||||
r (span/move doc :main :badge 0)
|
||||
after (:clip r)]
|
||||
(is (= [0 8] (node/placed-span (get-in after [:symbols :main :nodes :badge]))))
|
||||
(is (= [[0 4] [4 8] [8 12]] (spans after [:a :b :insert]))
|
||||
"and the lane beside it did not notice")
|
||||
(is (= (get-in (sample doc [2]) [2 [:badge :mark]])
|
||||
(get-in (sample after [0]) [0 [:badge :mark]]))
|
||||
"its source origin came with it")
|
||||
(is (empty? (clip/problems after)))))
|
||||
|
||||
(deftest a-move-onto-an-occupied-frame-of-a-lane-is-refused-rather-than-rippled
|
||||
(let [doc (document)]
|
||||
(is (:refused (span/move doc :main :insert 6)) "it would overlap B")
|
||||
(is (:refused (span/move doc :main :insert 4.5)))
|
||||
(is (re-find #"group" (:refused (span/move doc :main :girl 2))))
|
||||
(is (re-find #"whole shot" (:refused (span/move doc :main :plate 2))))
|
||||
;; Clearing the room first is the composition, and then it goes.
|
||||
(let [cleared (:clip (lane/blank doc :main :girl [4 8] {}))]
|
||||
(is (= [[0 4] [4 8]] (spans (:clip (span/move cleared :main :insert 4))
|
||||
[:a :insert]))))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; the coordinate
|
||||
|
||||
(deftest host-frame-reads-lane-time-for-a-cel-and-symbol-time-for-everything-else
|
||||
(let [doc (document)
|
||||
retimed (assoc-in doc [:symbols :main :nodes :girl :time] {:at 4 :rate 2})]
|
||||
(is (= 6 (span/host-frame doc :main :b 6))
|
||||
"an untimed lane reads the symbol's frames as its own")
|
||||
(is (= 6 (span/host-frame doc :main :badge 6))
|
||||
"and so does a node with no parent, always")
|
||||
(is (= 4 (span/host-frame retimed :main :b 6))
|
||||
"through a lane at :at 4 :rate 2, symbol frame 6 is lane frame 4")
|
||||
(is (= 6 (span/host-frame retimed :main :badge 6))
|
||||
"which is the lane's business and not the badge's")
|
||||
(is (nil? (span/host-frame (assoc-in doc [:symbols :main :nodes :girl :time]
|
||||
{:loop? true})
|
||||
:main :b 6))
|
||||
"and a looping parent has no single answer to give")))
|
||||
(deftest host-frame-of-a-parentless-clip-is-the-symbol-frame
|
||||
(is (= 6 (span/host-frame (document) :main :b 6)))
|
||||
(is (= 6 (span/host-frame (ordinary-document) :main :badge 6))))
|
||||
|
|
|
|||
|
|
@ -1,28 +1,53 @@
|
|||
(ns arthur.events.lane-test
|
||||
(:require [cljs.test :refer [deftest is]]
|
||||
[arthur.domain.lane-test :as fixture]
|
||||
[arthur.domain.clip :as clip]
|
||||
[arthur.domain.correction :as correction]
|
||||
[arthur.domain.lane :as lane]
|
||||
[arthur.events.ui :as ui]
|
||||
[arthur.domain.history :as history]
|
||||
[arthur.domain.leaf :as leaf]
|
||||
[arthur.domain.symbol :as symbol]
|
||||
[arthur.domain.sequence-test :as fixture]
|
||||
[arthur.domain.span :as span]
|
||||
[arthur.events.ui :as ui]
|
||||
[arthur.footage.store :as store]
|
||||
[arthur.ui.timeline :as timeline]))
|
||||
[arthur.ui.timeline :as timeline]
|
||||
[re-frame.core :as rf]
|
||||
[re-frame.db :as rf-db]))
|
||||
|
||||
(deftest one-row-projects-all-cels-and-keeps-selection-addresses
|
||||
(deftest symbol-and-lane-creation-are-distinct-explicit-commands
|
||||
(letfn [(run [event key]
|
||||
(let [doc (clip/blank)
|
||||
id (store/install! {:clip doc :store {}} key)]
|
||||
(reset! rf-db/app-db {:clip/current id :paint/revision 0
|
||||
:ui {:open :main} :playback {:frame 0}})
|
||||
(rf/dispatch-sync event)
|
||||
(let [db @rf-db/app-db
|
||||
saved (:clip (store/entry id))
|
||||
[_ _ instance-id] (get-in db [:ui :selection])
|
||||
sid (get-in saved [:symbols :main :nodes instance-id :source :symbol])]
|
||||
{:db db :symbol (clip/symbol saved sid)})))]
|
||||
(let [{ordinary :symbol} (run [::ui/new-symbol :inside] "explicit-symbol")
|
||||
{lane :symbol lane-db :db} (run [::ui/new-lane] "explicit-lane")]
|
||||
(is (nil? (:display ordinary)) "new symbol means ordinary symbol")
|
||||
(is (= :lane (:display lane)) "only the lane command creates a lane")
|
||||
(is (some? (get-in lane-db [:ui :target]))
|
||||
"the new lane is aimed so drawing and pool drops can go into it"))))
|
||||
|
||||
(deftest an-explicit-lane-is-one-row-of-clips
|
||||
(let [doc (fixture/document)
|
||||
rows (timeline/rows doc :main #{})
|
||||
lane (first (filter :cels rows))]
|
||||
(is (= 2 (count rows)))
|
||||
lane (first (filter :lane? rows))]
|
||||
(is (= 2 (count rows)) "the lane row plus the span-less plate")
|
||||
(is (= [[0 4] [4 8] [8 12]] (mapv :span (:cels lane))))
|
||||
(is (= [[:node :main :a [:a]] [:node :main :b [:b]] [:node :main :insert [:insert]]]
|
||||
(mapv :select (:cels lane))))
|
||||
(is (= [0 6 12] (:keys lane)))
|
||||
(is (= 1 (count (filter :cels (timeline/rows doc :main #{[:girl]})))))))
|
||||
(is (= [[:node :main :a [:a]]
|
||||
[:node :main :b [:b]]
|
||||
[:node :main :insert [:insert]]]
|
||||
(mapv :select (:cels lane))))))
|
||||
|
||||
(deftest a-nested-selection-converts-the-open-playhead-to-its-owning-symbol
|
||||
(deftest an-ordinary-symbol-keeps-a-row-per-node
|
||||
(let [doc (update-in (fixture/document) [:symbols :main] dissoc :display)
|
||||
rows (timeline/rows doc :main #{})]
|
||||
(is (empty? (filter :lane? rows)))
|
||||
(is (= #{[:a] [:b] [:insert] [:plate]} (set (map :path rows))))))
|
||||
|
||||
(deftest a-nested-selection-converts-the-open-playhead-to-its-owner
|
||||
(let [doc (assoc-in (fixture/document) [:symbols :outer]
|
||||
{:id :outer :frames 30
|
||||
:nodes {:take {:id :take :kind :instance :z "a"
|
||||
|
|
@ -34,189 +59,59 @@
|
|||
(is (= 12 (ui/selection-frame doc nil :main
|
||||
[:node :main :a [:a]] 12)))))
|
||||
|
||||
(deftest polygon-landing-follows-the-target-not-the-selection
|
||||
(let [doc (fixture/document)
|
||||
db {:ui {:open :main
|
||||
:selection [:node :main :plate [:plate]]
|
||||
:target {:sid :main :id :insert :path [:insert]}}
|
||||
:playback {:frame 5}}
|
||||
landing (ui/polygon-landing doc {} db)]
|
||||
(is (= doc (:clip landing)))
|
||||
(is (= [:insert] (:path landing))
|
||||
"looking at another shape does not silently move the creation target")
|
||||
(is (false? (:lane? landing)))))
|
||||
(deftest polygon-landing-obeys-the-destination-symbol-mode
|
||||
(let [lane (fixture/document)
|
||||
ordinary (update-in lane [:symbols :main] dissoc :display)
|
||||
db {:ui {:open :main :target {:sid :main :id :plate :path [:plate]}}
|
||||
:playback {:frame 5}}]
|
||||
(is (true? (:lane? (ui/polygon-landing lane {} db))))
|
||||
(is (false? (:lane? (ui/polygon-landing ordinary {} db))))))
|
||||
|
||||
(deftest timeline-polygon-landing-is-decided-by-the-aimed-lane
|
||||
(let [doc (fixture/document)
|
||||
base {:ui {:open :main
|
||||
:selection [:node :main :plate [:plate]]
|
||||
:target {:sid :main :id :girl :path [:girl]}}
|
||||
:playback {:frame 5}}
|
||||
occupied (ui/polygon-landing doc {} base)
|
||||
with-gap (:clip (lane/blank doc :main :girl [5 7] {:id :rest}))
|
||||
gap (ui/polygon-landing with-gap {} (assoc-in base [:playback :frame] 6))]
|
||||
(is (= [:b] (:path occupied))
|
||||
"the cel on screen wins even while an unrelated stage node is selected")
|
||||
(is (= doc (:clip occupied)) "an existing cel needs no document edit")
|
||||
(is (true? (:lane? occupied)))
|
||||
(is (= 1 (count (:path gap))))
|
||||
(is (not (contains? (get-in with-gap [:symbols :main :nodes])
|
||||
(first (:path gap))))
|
||||
"a gap receives a fresh drawing")
|
||||
(is (contains? (get-in (:clip gap) [:symbols :main :nodes])
|
||||
(first (:path gap))))
|
||||
(is (empty? (clip/problems (:clip gap))))))
|
||||
|
||||
(deftest polygon-landing-without-an-aimed-lane-uses-the-ordinary-target
|
||||
(let [doc (fixture/document)
|
||||
db {:ui {:open :main
|
||||
:target {:sid :main :id :plate :path [:plate]}}
|
||||
:playback {:frame 5}}
|
||||
landing (ui/polygon-landing doc {} db)]
|
||||
(is (= doc (:clip landing)))
|
||||
(is (= [] (:path landing)))
|
||||
(is (false? (:lane? landing)))))
|
||||
|
||||
(deftest beginning-a-polygon-materializes-a-missing-timeline-drawing
|
||||
(let [doc (fixture/document)
|
||||
with-gap (:clip (lane/blank doc :main :girl [5 7] {:id :rest}))
|
||||
id (store/install! {:clip with-gap :store {}} "polygon-start-test")
|
||||
db {:clip/current id :paint/revision 0
|
||||
:ui {:open :main
|
||||
:target {:sid :main :id :girl :path [:girl]}}
|
||||
:playback {:frame 6}}
|
||||
after (ui/beginning-polygon db)
|
||||
saved (:clip (store/entry (:clip/current after)))
|
||||
[_ sid cel-id path] (get-in after [:ui :selection])]
|
||||
(is (= :polygon (get-in after [:ui :tool])))
|
||||
(is (= [] (get-in after [:ui :draft])))
|
||||
(is (= :main sid))
|
||||
(is (= [cel-id] path))
|
||||
(is (contains? (get-in saved [:symbols :main :nodes]) cel-id)
|
||||
"the drawing exists before the first draft point is added")
|
||||
(is (= 6 (get-in saved [:symbols :main :nodes cel-id :time :at])))
|
||||
(is (empty? (clip/problems saved)))))
|
||||
|
||||
(deftest beginning-a-polygon-does-not-invent-an-unselected-lane
|
||||
(deftest beginning-a-polygon-does-not-invent-a-lane
|
||||
(let [doc (clip/blank)
|
||||
id (store/install! {:clip doc :store {}} "polygon-lane-start-test")
|
||||
id (store/install! {:clip doc :store {}} "polygon-no-implicit-lane")
|
||||
db {:clip/current id :paint/revision 0
|
||||
:ui {:open :main}
|
||||
:playback {:frame 6}}
|
||||
after (ui/beginning-polygon db)
|
||||
saved (:clip (store/entry (:clip/current after)))
|
||||
lanes (symbol/lanes (get-in saved [:symbols :main :nodes]))]
|
||||
saved (:clip (store/entry (:clip/current after)))]
|
||||
(is (= :polygon (get-in after [:ui :tool])))
|
||||
(is (= [:lane] (mapv :id lanes))
|
||||
"the lane the symbol was born with, and no second one invented here")
|
||||
(is (empty? (symbol/lane-clips (get-in saved [:symbols :main :nodes]) :lane))
|
||||
"and nothing put in it")
|
||||
(is (nil? (get-in after [:ui :target])))
|
||||
(is (nil? (get-in (store/entry (:clip/current after)) [:history :done])))
|
||||
(is (empty? (clip/problems saved)))))
|
||||
(is (= {} (get-in saved [:symbols :main :nodes])))
|
||||
(is (nil? (get-in saved [:symbols :main :display])))
|
||||
(is (nil? (get-in (store/entry (:clip/current after)) [:history :done])))))
|
||||
|
||||
(deftest sequence-commands-use-isolated-history-transactions
|
||||
(let [doc (fixture/document)
|
||||
id (store/install! {:clip doc :store {}} "sequence-test")
|
||||
id (store/install! {:clip doc :store {}} "sequence-command-test")
|
||||
db {:clip/current id :paint/revision 0
|
||||
:ui {:open :main :selection [:node :main :a [:a]]}}
|
||||
refused (ui/apply-lane-command db :main
|
||||
(lane/extend-hold doc :main :a 1 {}) [:retry])]
|
||||
refused (ui/apply-command db :main
|
||||
(span/extend-hold doc :main :a 1 {}) [:retry])]
|
||||
(is (= doc (:clip (store/entry id))))
|
||||
(is (nil? (:history (store/entry id))))
|
||||
(is (= [:retry] (get-in refused [:ui :lane-retry])))
|
||||
(let [r1 (lane/extend-hold doc :main :a 1 {:extent :grow-symbol})
|
||||
db1 (ui/apply-lane-command db :main r1 nil)
|
||||
r2 (lane/extend-hold (:clip r1) :main :a 1 {:extent :grow-symbol})
|
||||
db2 (ui/apply-lane-command db1 :main r2 nil)
|
||||
(is (= [:retry] (get-in refused [:ui :retry])))
|
||||
(let [r1 (span/extend-hold doc :main :a 1 {:extent :grow-symbol})
|
||||
db1 (ui/apply-command db :main r1 nil)
|
||||
r2 (span/extend-hold (:clip r1) :main :a 1 {:extent :grow-symbol})
|
||||
db2 (ui/apply-command db1 :main r2 nil)
|
||||
h (:history (store/entry id))
|
||||
undo (history/undo h (leaf/leaves "u" (:clip r2)))
|
||||
undo2 (history/undo (:history undo) (:leaves undo))]
|
||||
(is (= 2 (count (:done h))) "rapid button presses remain separate commands")
|
||||
(is (= 2 (count (:done h))))
|
||||
(is (= (:clip r1) (leaf/clip "u" (:leaves undo))))
|
||||
(is (= doc (leaf/clip "u" (:leaves undo2))))
|
||||
(is (= [:node :main :a [:a]] (get-in db2 [:ui :selection]))))))
|
||||
|
||||
(deftest a-correction-is-one-step-and-keeps-the-full-selection-address
|
||||
(deftest an-expanded-lane-opens-the-selected-clip
|
||||
(let [doc (fixture/document)
|
||||
id (store/install! {:clip doc :store {}} "correction-event-test")
|
||||
selection [:node :main :a [:outer :a]]
|
||||
db {:clip/current id :paint/revision 0
|
||||
:ui {:open :outer :selection selection}}
|
||||
result (correction/add doc :main :a [:xform :rot]
|
||||
{:id :nudge :support [0 3] :motion :return
|
||||
:start 0 :peak 0.5 :peak-frame 1})
|
||||
after (ui/apply-correction-command db result)
|
||||
entry (store/entry id)]
|
||||
(is (= selection (get-in after [:ui :selection])))
|
||||
(is (= 1 (count (get-in entry [:history :done]))))
|
||||
(is (= 0.5 (get-in (:clip entry)
|
||||
[:symbols :main :nodes :a :channels [:xform :rot]
|
||||
:over 0 :values :keys 1])))
|
||||
(let [refused (ui/apply-correction-command after {:refused "nope"})]
|
||||
(is (= "nope" (get-in refused [:project :status])))
|
||||
(is (= 1 (count (get-in (store/entry id) [:history :done])))))))
|
||||
rows (timeline/rows doc :main #{[:arthur.ui.timeline/lane] [:insert]} [:insert])
|
||||
portal (first (filter :portal? rows))]
|
||||
(is (= [:insert] (:path portal)))
|
||||
(is (= [:node :main :insert [:insert]] (:select portal)))
|
||||
(is (some #{"mark"} (map :label rows)))))
|
||||
|
||||
(deftest an-expanded-lane-opens-the-selected-clip-and-everything-under-it
|
||||
;; The whole document is editable from the root timeline: a lane opens one
|
||||
;; portal — the clip selected in it — and that portal opens the lanes and
|
||||
;; nodes of the symbol it places, mapped into this ruler.
|
||||
(let [doc (fixture/document)
|
||||
open #{[:girl] [:insert]}
|
||||
shut (timeline/rows doc :main open [:insert])
|
||||
of (fn [rows] (mapv (juxt :label :depth) rows))
|
||||
lane-row (first (filter :cels (timeline/rows doc :main open [:insert])))]
|
||||
(is (= 1 (count (filter :portal? shut)))
|
||||
"exactly one clip is opened, not one branch per clip in the lane")
|
||||
(is (= [:insert] (:path (first (filter :portal? shut))))
|
||||
"and it is the selected one")
|
||||
(is (some #{["mark" 2]} (of shut))
|
||||
"the clip's own symbol appears under it, at its depth")
|
||||
(is (= [:node :main :insert [:insert]]
|
||||
(:select (first (filter :portal? shut))))
|
||||
"the portal addresses the same clip its block in the lane does")
|
||||
(is (= (:keys (second (:cels lane-row)))
|
||||
(:keys (first (filter #(= [:b] (:path %)) (timeline/rows doc :main open [:b])))))
|
||||
"a clip's keys are on its block whether or not its portal is open")))
|
||||
|
||||
(deftest a-nested-selection-keeps-the-portal-that-revealed-it-open
|
||||
;; Clicking a shape inside the clip — or the end of its span — is still
|
||||
;; working inside that clip. Matching the selected id alone would close the
|
||||
;; portal the moment anything under it was touched.
|
||||
(let [doc (fixture/document)
|
||||
open #{[:girl] [:insert]}
|
||||
deep (timeline/rows doc :main open [:insert :mark])
|
||||
none (timeline/rows doc :main open [:plate])]
|
||||
(is (= [:insert] (:path (first (filter :portal? deep))))
|
||||
"a selection under the clip keeps that clip's portal")
|
||||
(is (empty? (filter :portal? none)))
|
||||
(is (some #{"select a clip to inspect"} (map :label none))
|
||||
"with nothing selected in it, an open lane says what it is waiting for")))
|
||||
|
||||
(deftest a-held-clip-shows-its-contents-without-inventing-frames-for-them
|
||||
;; `clip/source-time` is nil for a hold, so nested keys have no place on this
|
||||
;; ruler — but the drawing's own nodes must still be reachable from here.
|
||||
(let [doc (fixture/document)
|
||||
rows (timeline/rows doc :main #{[:girl] [:a]} [:a])
|
||||
inside (filter :unmapped? rows)]
|
||||
(is (seq inside) "a held drawing opens")
|
||||
(is (some #{"mark"} (map :label inside)))
|
||||
(is (every? (comp empty? :keys) inside)
|
||||
"no key is placed where the hold cannot say it belongs")
|
||||
(is (= [[0 4]] (distinct (keep :span (filter #(= :node (:kind %)) inside))))
|
||||
"its rows span the hold, which is when it is on screen")))
|
||||
|
||||
(deftest a-lane-of-sounds-is-drawn-as-a-lane-and-not-flattened-twice
|
||||
(let [made (lane/add-lane (fixture/document) :main :track)
|
||||
seeded (clip/place-sound (:clip made) :main {:sound "s1"} "voice" 6 1 2 :vo)
|
||||
doc (:clip (lane/adopt seeded :main :track :vo 2 {:extent :grow-symbol}))
|
||||
picture (remove :sound? (timeline/rows doc :main #{} nil))
|
||||
sound-lanes (filter :sound? (timeline/rows doc :main #{} nil))
|
||||
flattened (timeline/sound-rows doc :main #{})]
|
||||
(is (= 1 (count sound-lanes)) "the sound's lane is one row, like any lane")
|
||||
(is (= [:vo] (mapv :id (:cels (first sound-lanes))))
|
||||
"with the sound on it as a block that can be moved and trimmed")
|
||||
(is (empty? (filter #(= [:track] (:path %)) picture))
|
||||
"and it is not also listed among the picture rows")
|
||||
(is (empty? flattened)
|
||||
"nor flattened into a second, parallel audio row")))
|
||||
(deftest a-lane-of-sounds-is-not-flattened-twice
|
||||
(let [base (:clip (span/draw-as-lane (clip/blank) :main true {}))
|
||||
doc (clip/place-sound base :main {:sound "s1"} "voice" 6 1 2 :vo)
|
||||
lane (first (filter :lane? (timeline/rows doc :main #{} nil)))]
|
||||
(is (= [:vo] (mapv :id (:cels lane))))
|
||||
(is (empty? (timeline/sound-rows doc :main #{})))))
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// Local editor smoke test. Uses the in-memory blank document and disables the
|
||||
// project route, so it never creates an account, project, or server-side write.
|
||||
// Browser smoke test for the explicit-lane workflow. It uses the in-memory
|
||||
// document and disables project routing, so it performs no server-side write.
|
||||
import { spawn } from 'node:child_process';
|
||||
import { mkdtempSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
|
|
@ -7,7 +7,7 @@ import { join } from 'node:path';
|
|||
import assert from 'node:assert/strict';
|
||||
|
||||
const url = process.env.ARTHUR_URL ?? 'http://localhost:8778/';
|
||||
const profile = mkdtempSync(join(tmpdir(), 'arthur-sequence-'));
|
||||
const profile = mkdtempSync(join(tmpdir(), 'arthur-explicit-lane-'));
|
||||
const port = 9335;
|
||||
const chrome = spawn(process.env.CHROME ?? '/usr/bin/chromium', [
|
||||
'--headless=new', '--no-sandbox', '--disable-gpu', '--no-first-run',
|
||||
|
|
@ -16,6 +16,7 @@ const chrome = spawn(process.env.CHROME ?? '/usr/bin/chromium', [
|
|||
], { stdio: 'ignore' });
|
||||
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
|
||||
let ws;
|
||||
|
||||
try {
|
||||
let target;
|
||||
for (let i = 0; i < 100 && !target; i++) {
|
||||
|
|
@ -23,11 +24,12 @@ try {
|
|||
try {
|
||||
target = (await fetch(`http://127.0.0.1:${port}/json/list`).then(r => r.json()))
|
||||
.find(t => t.type === 'page' && t.url.startsWith(url));
|
||||
} catch { /* browser starting */ }
|
||||
} catch { /* Chromium is still starting. */ }
|
||||
}
|
||||
assert(target, 'browser exposes the editor page');
|
||||
ws = new WebSocket(target.webSocketDebuggerUrl);
|
||||
await new Promise((resolve, reject) => { ws.onopen = resolve; ws.onerror = reject; });
|
||||
|
||||
let serial = 0;
|
||||
const pending = new Map();
|
||||
const errors = [];
|
||||
|
|
@ -35,10 +37,10 @@ try {
|
|||
const msg = JSON.parse(data);
|
||||
if (msg.method === 'Runtime.exceptionThrown') errors.push(msg.params.exceptionDetails);
|
||||
if (msg.id && pending.has(msg.id)) {
|
||||
const { resolve, reject } = pending.get(msg.id);
|
||||
const waiting = pending.get(msg.id);
|
||||
pending.delete(msg.id);
|
||||
if (msg.error) reject(new Error(JSON.stringify(msg.error)));
|
||||
else resolve(msg.result);
|
||||
if (msg.error) waiting.reject(new Error(JSON.stringify(msg.error)));
|
||||
else waiting.resolve(msg.result);
|
||||
}
|
||||
};
|
||||
const send = (method, params = {}) => new Promise((resolve, reject) => {
|
||||
|
|
@ -47,13 +49,16 @@ try {
|
|||
ws.send(JSON.stringify({ id, method, params }));
|
||||
});
|
||||
const evaluate = async expression => {
|
||||
const r = await send('Runtime.evaluate', { expression, returnByValue: true, awaitPromise: true });
|
||||
if (r.exceptionDetails) throw new Error(JSON.stringify(r.exceptionDetails));
|
||||
return r.result.value;
|
||||
const result = await send('Runtime.evaluate', {
|
||||
expression, returnByValue: true, awaitPromise: true,
|
||||
});
|
||||
if (result.exceptionDetails) throw new Error(JSON.stringify(result.exceptionDetails));
|
||||
return result.result.value;
|
||||
};
|
||||
|
||||
await send('Runtime.enable');
|
||||
for (let i = 0; i < 100; i++) {
|
||||
if (await evaluate('typeof arthur !== "undefined" && !!arthur.events?.ui && !!document.querySelector("canvas.stage")')) break;
|
||||
if (await evaluate('typeof arthur !== "undefined" && !!document.querySelector("canvas.stage")')) break;
|
||||
await sleep(100);
|
||||
}
|
||||
await evaluate(`(() => {
|
||||
|
|
@ -61,342 +66,145 @@ try {
|
|||
cljs.core.swap_BANG_(re_frame.db.app_db, db => cljs.core.assoc(db, k('route'), k('local-test')));
|
||||
window.laneSnapshot = () => {
|
||||
const db = cljs.core.deref(re_frame.db.app_db);
|
||||
const entry = arthur.footage.store.entry(cljs.core.get(db, k('clip/current')));
|
||||
return cljs.core.clj__GT_js(entry);
|
||||
return cljs.core.clj__GT_js(arthur.footage.store.entry(cljs.core.get(db, k('clip/current'))));
|
||||
};
|
||||
return true;
|
||||
})()`);
|
||||
await sleep(250);
|
||||
// A command is named the same wherever it is drawn, and since the transport
|
||||
// strip was consolidated it is drawn in one of two places: as a button in the
|
||||
// strip, or as a row in one of the strip's menus. So the test asks for it by
|
||||
// name and this finds it — opening each menu in turn to look — rather than the
|
||||
// test knowing which menu anything ended up in. An icon button is matched on
|
||||
// its `aria-label`, which is also what a screen reader is told it is.
|
||||
// Two bars carry commands: the location bar says where an edit lands and holds
|
||||
// what creates things there, the transport strip holds what acts on a cel.
|
||||
const bars = ['.loc', '.pane.time .pane-head', '.section'];
|
||||
const within = (suffix) => bars.map((b) => `${b} ${suffix}`).join(', ');
|
||||
const named = label =>
|
||||
`(b => b.textContent.trim() === ${JSON.stringify(label)}` +
|
||||
` || b.getAttribute('aria-label') === ${JSON.stringify(label)})`;
|
||||
const shut = async () => {
|
||||
await evaluate(`(() => { document.querySelectorAll('.menu-scrim').forEach(s => s.click()); return true })()`);
|
||||
await sleep(120);
|
||||
|
||||
const named = label => `(b => b.getAttribute('aria-label') === ${JSON.stringify(label)}` +
|
||||
` || b.textContent.trim() === ${JSON.stringify(label)})`;
|
||||
const closeMenus = async () => {
|
||||
await evaluate(`(() => { document.querySelectorAll('.menu-scrim').forEach(x => x.click()); return true })()`);
|
||||
await sleep(80);
|
||||
};
|
||||
// Leaves the control on screen and returns what to select it with.
|
||||
const reveal = async label => {
|
||||
await shut();
|
||||
if (await evaluate(`![...document.querySelectorAll('${within('button')}')].find(${named(label)})`)) {
|
||||
const menus = await evaluate(
|
||||
`[...document.querySelectorAll('${within('.menu-wrap > button')}')].map(b => b.textContent.trim())`);
|
||||
let found = false;
|
||||
for (const menu of menus) {
|
||||
await evaluate(`(() => { [...document.querySelectorAll('${within('.menu-wrap > button')}')]
|
||||
.find(b => b.textContent.trim() === ${JSON.stringify(menu)}).click(); return true })()`);
|
||||
await sleep(180);
|
||||
if (await evaluate(`!![...document.querySelectorAll('.menu-item')].find(${named(label)})`)) { found = true; break; }
|
||||
await shut();
|
||||
}
|
||||
assert(found, `a control named: ${label}`);
|
||||
return '.menu-item';
|
||||
}
|
||||
return within('button');
|
||||
};
|
||||
const click = async label => {
|
||||
const where = await reveal(label);
|
||||
const clickNew = async label => {
|
||||
await closeMenus();
|
||||
assert(await evaluate(`(() => {
|
||||
const b = [...document.querySelectorAll('${where}')].find(${named(label)});
|
||||
if (!b || b.disabled) return false;
|
||||
b.click(); return true;
|
||||
})()`), `enabled control: ${label}`);
|
||||
await sleep(180);
|
||||
await shut();
|
||||
const menu = document.querySelector('.loc .menu-wrap > button');
|
||||
if (!menu) return false;
|
||||
menu.click(); return true;
|
||||
})()`), 'new menu exists');
|
||||
await sleep(100);
|
||||
assert(await evaluate(`(() => {
|
||||
const item = [...document.querySelectorAll('.menu-item')].find(${named(label)});
|
||||
if (!item || item.disabled) return false;
|
||||
item.click(); return true;
|
||||
})()`), `enabled creation command: ${label}`);
|
||||
await sleep(220);
|
||||
await closeMenus();
|
||||
};
|
||||
// UUIDs expose a mutable hash cache through clj->js; compare their identity,
|
||||
// not that implementation detail, when asserting exact undo restoration.
|
||||
const shot = async () => JSON.parse(JSON.stringify(await evaluate('laneSnapshot()'),
|
||||
(_key, value) => value?.uuid ?? value));
|
||||
const instances = s => Object.values(s.clip.symbols.main.nodes)
|
||||
.filter(n => n.kind === 'instance')
|
||||
.sort((a, b) => a.time.at - b.time.at);
|
||||
const placed = s => instances(s).map(n => [
|
||||
n.time.at + n.span[0] / (n.time.rate ?? 1),
|
||||
n.time.at + n.span[1] / (n.time.rate ?? 1),
|
||||
]);
|
||||
const key = async (key, extra = {}) => {
|
||||
await send('Input.dispatchKeyEvent', {type: 'keyDown', key, ...extra});
|
||||
await send('Input.dispatchKeyEvent', {type: 'keyUp', key, ...extra});
|
||||
await sleep(180);
|
||||
};
|
||||
const undo = () => key('z', {modifiers: 2});
|
||||
const drag = async (selector, df, {zone = 0.5, shift = false} = {}) => {
|
||||
const points = await evaluate(`(() => {
|
||||
const handle = document.querySelector(${JSON.stringify(selector)});
|
||||
if (!handle) return null;
|
||||
const track = handle.closest('.tl-track');
|
||||
const h = handle.getBoundingClientRect();
|
||||
const t = track.getBoundingClientRect();
|
||||
const frames = Number(document.querySelector('.at-frame').textContent.split('/')[1]);
|
||||
const x = h.left + h.width * ${zone};
|
||||
const y = h.top + h.height / 2;
|
||||
return {x, y, end: x + t.width * ${df} / frames};
|
||||
})()`);
|
||||
assert(points, `drag handle exists: ${selector}`);
|
||||
const modifiers = shift ? 8 : 0;
|
||||
await send('Input.dispatchMouseEvent', {
|
||||
type: 'mousePressed', x: points.x, y: points.y,
|
||||
button: 'left', buttons: 1, clickCount: 1, modifiers,
|
||||
});
|
||||
await sleep(100);
|
||||
await send('Input.dispatchMouseEvent', {
|
||||
type: 'mouseMoved', x: points.end, y: points.y,
|
||||
button: 'left', buttons: 1, modifiers,
|
||||
});
|
||||
await sleep(100);
|
||||
await send('Input.dispatchMouseEvent', {
|
||||
type: 'mouseReleased', x: points.end, y: points.y,
|
||||
button: 'left', buttons: 0, clickCount: 1, modifiers,
|
||||
});
|
||||
await sleep(250);
|
||||
};
|
||||
const tabs = () => evaluate(`(() => {
|
||||
const k = cljs.core.keyword, db = cljs.core.deref(re_frame.db.app_db);
|
||||
return {tabs: cljs.core.clj__GT_js(cljs.core.get_in(db, [k('ui'), k('tabs')])).map(String),
|
||||
open: String(cljs.core.clj__GT_js(cljs.core.get_in(db, [k('ui'), k('open')])))};
|
||||
})()`);
|
||||
// A real two-press double-click, not `.dispatchEvent`: what broke here was
|
||||
// where the browser decides to deliver the click, which a synthetic event
|
||||
// cannot show.
|
||||
const doubleClick = async selector => {
|
||||
const p = await evaluate(`(() => {
|
||||
const el = document.querySelector(${JSON.stringify(selector)});
|
||||
if (!el) return null;
|
||||
const r = el.getBoundingClientRect();
|
||||
return {x: r.left + r.width / 2, y: r.top + r.height / 2};
|
||||
})()`);
|
||||
assert(p, `something to double-click: ${selector}`);
|
||||
for (const clickCount of [1, 2]) {
|
||||
await send('Input.dispatchMouseEvent', {type: 'mousePressed', x: p.x, y: p.y,
|
||||
button: 'left', buttons: 1, clickCount});
|
||||
await send('Input.dispatchMouseEvent', {type: 'mouseReleased', x: p.x, y: p.y,
|
||||
button: 'left', buttons: 0, clickCount});
|
||||
await sleep(60);
|
||||
}
|
||||
await sleep(280);
|
||||
};
|
||||
const dropPoolSymbol = async frame => {
|
||||
const points = await evaluate(`(() => {
|
||||
const source = document.querySelector('.pool-row:not(.main) .pool-item[draggable="true"]');
|
||||
const track = document.querySelector('.tl-track');
|
||||
if (!source || !track) return null;
|
||||
source.scrollIntoView({block: 'center'});
|
||||
const a = source.getBoundingClientRect(), b = track.getBoundingClientRect();
|
||||
const frames = Number(document.querySelector('.at-frame').textContent.split('/')[1]);
|
||||
return {sx: a.left + a.width / 2, sy: a.top + a.height / 2,
|
||||
tx: b.left + b.width * (${frame} + 0.25) / frames,
|
||||
ty: b.top + b.height / 2};
|
||||
})()`);
|
||||
assert(points, 'a library symbol and lane are available to drag');
|
||||
await send('Input.dispatchMouseEvent', {type: 'mouseMoved', x: points.sx, y: points.sy});
|
||||
await send('Input.dispatchMouseEvent', {type: 'mousePressed', x: points.sx, y: points.sy,
|
||||
button: 'left', buttons: 1, clickCount: 1});
|
||||
await send('Input.dispatchMouseEvent', {type: 'mouseMoved', x: points.sx + 12, y: points.sy,
|
||||
button: 'left', buttons: 1});
|
||||
await sleep(120);
|
||||
await send('Input.dispatchMouseEvent', {type: 'mouseMoved', x: points.tx, y: points.ty,
|
||||
button: 'left', buttons: 1});
|
||||
await sleep(120);
|
||||
assert.equal(await evaluate('document.querySelectorAll(".tl-label.ghost").length'), 0,
|
||||
'targeting an existing lane does not preview a temporary new row');
|
||||
const lanePreview = await evaluate(`(() => {
|
||||
const db = cljs.core.deref(re_frame.db.app_db), k = cljs.core.keyword;
|
||||
return {ghosts: document.querySelectorAll('.tl-track .tl-cel.ghost').length,
|
||||
drop: cljs.core.clj__GT_js(cljs.core.get_in(db, [k('ui'), k('drop')]))};
|
||||
})()`);
|
||||
assert.equal(lanePreview.ghosts, 1,
|
||||
`the pool drop preview is drawn inside the targeted lane: ${JSON.stringify(lanePreview)}`);
|
||||
await send('Input.dispatchMouseEvent', {type: 'mouseReleased', x: points.tx, y: points.ty,
|
||||
button: 'left', buttons: 0, clickCount: 1});
|
||||
await sleep(300);
|
||||
};
|
||||
const dragClipBetweenLanes = async frame => {
|
||||
const points = await evaluate(`(() => {
|
||||
const tracks = [...document.querySelectorAll('.tl-track')];
|
||||
const source = tracks[1]?.querySelector('.tl-cel');
|
||||
const target = tracks[0];
|
||||
if (!source || !target) return null;
|
||||
const a = source.getBoundingClientRect(), b = target.getBoundingClientRect();
|
||||
const frames = Number(document.querySelector('.at-frame').textContent.split('/')[1]);
|
||||
return {sx: a.left + a.width / 2, sy: a.top + a.height / 2,
|
||||
tx: b.left + b.width * (${frame} + 0.25) / frames,
|
||||
ty: b.top + b.height / 2};
|
||||
})()`);
|
||||
assert(points, 'two lanes and a source clip are available');
|
||||
await send('Input.dispatchMouseEvent', {type: 'mousePressed', x: points.sx, y: points.sy,
|
||||
button: 'left', buttons: 1, clickCount: 1});
|
||||
await send('Input.dispatchMouseEvent', {type: 'mouseMoved', x: points.tx, y: points.ty,
|
||||
button: 'left', buttons: 1});
|
||||
await sleep(150);
|
||||
assert.equal(await evaluate('document.querySelectorAll(".tl-track")[0].querySelectorAll(".tl-cel.ghost").length'), 1,
|
||||
'cross-lane movement previews in the destination lane');
|
||||
await send('Input.dispatchMouseEvent', {type: 'mouseReleased', x: points.tx, y: points.ty,
|
||||
button: 'left', buttons: 0, clickCount: 1});
|
||||
await sleep(300);
|
||||
};
|
||||
const mainInstances = s => Object.values(s.clip.symbols.main.nodes)
|
||||
.filter(n => n.kind === 'instance');
|
||||
const laneSymbols = s => Object.values(s.clip.symbols).filter(sym => sym.display === 'lane');
|
||||
|
||||
assert.equal(await evaluate('[...document.querySelectorAll(".timing-controls > button")].every(b => b.disabled)'), true,
|
||||
'timing buttons are disabled without a symbol clip');
|
||||
await click('inside');
|
||||
assert.equal((await shot()).clip.symbols.main.display, undefined,
|
||||
'a blank document starts as an ordinary symbol');
|
||||
|
||||
await clickNew('inside');
|
||||
let s = await shot();
|
||||
assert.deepEqual(placed(s), [[0, 1]],
|
||||
'new at the root automatically makes a lane and a one-frame symbol clip');
|
||||
assert.equal(await evaluate('document.querySelectorAll(".tl-label .kind").length'), 1,
|
||||
'new temporal content creates a lane row rather than a row per symbol');
|
||||
assert.equal(await evaluate(`document.querySelectorAll('.cel-sheet, [aria-label="time view"]').length`), 0,
|
||||
'there is one temporal interface');
|
||||
assert.equal(await evaluate('document.querySelectorAll(".timing-controls > button").length'), 3,
|
||||
'timing operations are direct buttons');
|
||||
let placed = mainInstances(s);
|
||||
assert.equal(placed.length, 1, 'new symbol places one instance');
|
||||
assert.equal(s.clip.symbols[placed[0].source.symbol].display, undefined,
|
||||
'new symbol remains ordinary');
|
||||
assert.equal(await evaluate('document.querySelectorAll(".tl-track").length'), 1,
|
||||
'an ordinary symbol is an ordinary timeline row');
|
||||
|
||||
await drag('.tl-cel .tl-edge.out', 3);
|
||||
await clickNew('lane');
|
||||
s = await shot();
|
||||
assert.deepEqual(placed(s), [[0, 4]], 'a right edge directly changes the endpoint');
|
||||
placed = mainInstances(s);
|
||||
assert.equal(placed.length, 2, 'explicit lane places a second symbol');
|
||||
assert.equal(laneSymbols(s).length, 1, 'only the lane command marks a symbol as a lane');
|
||||
assert.equal(await evaluate('[...document.querySelectorAll(".tl-track")].filter(t => t.arthurLane).length'), 1,
|
||||
'the explicit lane is drawn as one linear track');
|
||||
|
||||
for (let i = 0; i < 4; i++) await click('+1');
|
||||
await click('inside');
|
||||
await drag('.tl-cel:nth-of-type(2) .tl-edge.out', 2);
|
||||
for (let i = 0; i < 3; i++) await click('+1');
|
||||
await click('inside');
|
||||
s = await shot();
|
||||
assert.deepEqual(placed(s), [[0, 4], [4, 7], [7, 8]]);
|
||||
|
||||
await drag('.tl-cel:nth-of-type(2) .tl-junction', 1, {zone: 0.5});
|
||||
assert.deepEqual(placed(await shot()), [[0, 5], [5, 7], [7, 8]],
|
||||
'the middle of a junction rolls both edges');
|
||||
await undo();
|
||||
|
||||
await drag('.tl-cel:nth-of-type(2) .tl-junction', 1, {zone: 0.9});
|
||||
assert.deepEqual(placed(await shot()), [[0, 4], [5, 7], [7, 8]],
|
||||
'the right side trims only the right clip');
|
||||
await undo();
|
||||
|
||||
await drag('.tl-cel:nth-of-type(2) .tl-junction', -1, {zone: 0.1});
|
||||
assert.deepEqual(placed(await shot()), [[0, 3], [4, 7], [7, 8]],
|
||||
'the left side trims only the left clip');
|
||||
await undo();
|
||||
|
||||
await drag('.tl-cel:nth-of-type(2) .tl-edge.out', 2, {shift: true});
|
||||
assert.deepEqual(placed(await shot()), [[0, 4], [4, 9], [9, 10]],
|
||||
'Shift-edge ripples every later clip on the lane');
|
||||
await undo();
|
||||
|
||||
await drag('.tl-cel:first-of-type .tl-edge.out', 2);
|
||||
assert.deepEqual(placed(await shot()), [[0, 6], [6, 7], [7, 8]],
|
||||
'ordinary growth trims adjacent spans and never overlaps');
|
||||
|
||||
await dropPoolSymbol(10);
|
||||
s = await shot();
|
||||
assert.deepEqual(placed(s), [[0, 6], [6, 7], [7, 8], [10, 11]],
|
||||
'an arbitrary library symbol drops into an existing lane');
|
||||
assert.equal(instances(s).at(-1).playback.speed, 1,
|
||||
'a dropped symbol plays naturally instead of becoming a held drawing');
|
||||
|
||||
await evaluate(`re_frame.core.dispatch(cljs.core.vector(cljs.core.keyword('arthur.events.ui/new-lane')))`);
|
||||
await sleep(180);
|
||||
s = await shot();
|
||||
const renameControls = await evaluate('document.querySelectorAll(".tl-label .tl-rename").length');
|
||||
assert.equal(renameControls, 2,
|
||||
`both lanes expose rename controls: ${JSON.stringify(s.clip.symbols.main.nodes)}`);
|
||||
await evaluate('document.querySelector(".tl-label .tl-rename").click()');
|
||||
assert.equal(await evaluate('document.querySelectorAll(".tl-rename").length'), 1,
|
||||
'the lane exposes its rename control');
|
||||
await evaluate('document.querySelector(".tl-rename").click()');
|
||||
await sleep(80);
|
||||
assert(await evaluate(`(() => {
|
||||
const input = document.querySelector('.tl-name-input');
|
||||
if (!input) return false;
|
||||
Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set.call(input, 'Foreground');
|
||||
input.dispatchEvent(new InputEvent('input', {bubbles: true, inputType: 'insertText', data: 'Foreground'}));
|
||||
Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')
|
||||
.set.call(input, 'Foreground');
|
||||
input.dispatchEvent(new InputEvent('input', {bubbles: true, inputType: 'insertText'}));
|
||||
input.blur(); return true;
|
||||
})()`), 'lane rename editor opens');
|
||||
await sleep(180);
|
||||
s = await shot();
|
||||
assert(Object.values(s.clip.symbols.main.nodes).some(n => n.layout === 'sequence' && n.name === 'Foreground'),
|
||||
'a lane name is editable and persisted in the document');
|
||||
assert(mainInstances(s).some(n => n.name === 'Foreground'), 'lane name persists');
|
||||
|
||||
await dragClipBetweenLanes(12);
|
||||
s = await shot();
|
||||
const lanes = Object.values(s.clip.symbols.main.nodes).filter(n => n.layout === 'sequence');
|
||||
assert.deepEqual(lanes.map(l => instances(s).filter(n => n.parent === l.id).length).sort(), [1, 3],
|
||||
'a clip body can move from one lane to another');
|
||||
|
||||
// EXPANDING A LANE OPENS THE SELECTED CLIP. Its own keys, and under it the
|
||||
// lanes and nodes of the symbol it places, all on this ruler — which is what
|
||||
// makes the whole document editable from the root timeline.
|
||||
const rowLabels = () => evaluate(
|
||||
`[...document.querySelectorAll('.tl-labels > .tl-label')].map(e => e.textContent.trim())`);
|
||||
const twist = async i => {
|
||||
assert(await evaluate(`(() => {
|
||||
const t = document.querySelectorAll('.tl-labels > .tl-label .tl-twist')[${i}];
|
||||
if (!t || t.disabled) return false;
|
||||
t.click(); return true;
|
||||
})()`), `an expander at row ${i}`);
|
||||
await sleep(220);
|
||||
};
|
||||
await evaluate(`(() => { document.querySelector('.tl-track .tl-cel').click(); return true })()`);
|
||||
await sleep(200);
|
||||
const collapsed = await rowLabels();
|
||||
await twist(0);
|
||||
const opened = await rowLabels();
|
||||
assert(opened.length > collapsed.length, 'the lane opens');
|
||||
assert.equal(opened.filter(l => l.includes('instance')).length, 1,
|
||||
`one clip portal, not one branch per clip: ${JSON.stringify(opened)}`);
|
||||
const portalAt = opened.findIndex(l => l.includes('instance'));
|
||||
await twist(portalAt);
|
||||
const deep = await rowLabels();
|
||||
assert(deep.length > opened.length,
|
||||
`the portal opens the symbol the clip places: ${JSON.stringify(deep)}`);
|
||||
// Selecting something nested must not close the portal that revealed it.
|
||||
await evaluate(`(() => {
|
||||
const k = cljs.core.keyword, db = cljs.core.deref(re_frame.db.app_db);
|
||||
const sel = cljs.core.get_in(db, [k('ui'), k('selection')]);
|
||||
const path = cljs.core.nth(sel, 3);
|
||||
re_frame.core.dispatch(cljs.core.vector(
|
||||
k('arthur.events.ui/select'),
|
||||
cljs.core.vector(k('node'), cljs.core.nth(sel, 1), cljs.core.nth(sel, 2),
|
||||
cljs.core.conj(path, k('made-up-child')))));
|
||||
return true;
|
||||
// Drag a library symbol into the explicit lane. The row itself is the target;
|
||||
// no temporary lane is previewed or created.
|
||||
const drop = await evaluate(`(() => {
|
||||
const source = document.querySelector('.pool-row:not(.main) .pool-item[draggable="true"]');
|
||||
const track = [...document.querySelectorAll('.tl-track')].find(t => t.arthurLane);
|
||||
if (!source || !track) return null;
|
||||
source.scrollIntoView({block: 'center'});
|
||||
const a = source.getBoundingClientRect(), b = track.getBoundingClientRect();
|
||||
return {sx: a.left + a.width / 2, sy: a.top + a.height / 2,
|
||||
tx: b.left + b.width * .085, ty: b.top + b.height / 2};
|
||||
})()`);
|
||||
await sleep(220);
|
||||
assert.equal((await rowLabels()).filter(l => l.includes('instance')).length, 1,
|
||||
'a selection under the clip keeps its portal open');
|
||||
await twist(portalAt);
|
||||
await twist(0);
|
||||
assert(drop, 'a pool symbol and explicit lane are available');
|
||||
await send('Input.dispatchMouseEvent', {type: 'mouseMoved', x: drop.sx, y: drop.sy});
|
||||
await send('Input.dispatchMouseEvent', {type: 'mousePressed', x: drop.sx, y: drop.sy,
|
||||
button: 'left', buttons: 1, clickCount: 1});
|
||||
await send('Input.dispatchMouseEvent', {type: 'mouseMoved', x: drop.sx + 12, y: drop.sy,
|
||||
button: 'left', buttons: 1});
|
||||
await sleep(100);
|
||||
await send('Input.dispatchMouseEvent', {type: 'mouseMoved', x: drop.tx, y: drop.ty,
|
||||
button: 'left', buttons: 1});
|
||||
await sleep(120);
|
||||
assert.equal(await evaluate('document.querySelectorAll(".tl-label.ghost").length'), 0,
|
||||
'pool drop does not preview an invented lane');
|
||||
assert.equal(await evaluate('document.querySelectorAll(".tl-cel.ghost").length'), 1,
|
||||
'pool drop previews inside the existing lane');
|
||||
await send('Input.dispatchMouseEvent', {type: 'mouseReleased', x: drop.tx, y: drop.ty,
|
||||
button: 'left', buttons: 0, clickCount: 1});
|
||||
await sleep(300);
|
||||
s = await shot();
|
||||
assert.equal(Object.keys(laneSymbols(s)[0].nodes).length, 1,
|
||||
`the dropped clip remains in the explicit lane: ${JSON.stringify(s)}`);
|
||||
assert.equal(laneSymbols(s).length, 1, 'the drop creates no extra lane');
|
||||
|
||||
const before = await tabs();
|
||||
const tabChips = () => evaluate('document.querySelectorAll(".tabs .tab").length');
|
||||
const chipsBefore = await tabChips();
|
||||
await doubleClick('.tl-track .tl-cel');
|
||||
const after = await tabs();
|
||||
assert.equal(after.tabs.length, before.tabs.length + 1,
|
||||
`double-clicking a clip opens the symbol it places, as the pool row does: ${JSON.stringify(after)}`);
|
||||
assert(!before.tabs.includes(after.open) && after.tabs.includes(after.open),
|
||||
`the opened symbol is the one in front: ${JSON.stringify(after)}`);
|
||||
assert.equal(await tabChips(), chipsBefore + 1,
|
||||
'the opened symbol is drawn as one more tab');
|
||||
assert.equal(await evaluate('document.querySelectorAll("#app > *").length'), 1,
|
||||
'opening from the timeline leaves the editor standing: a stale node selection ' +
|
||||
'pointing into the symbol just left used to throw and unmount it');
|
||||
// A second explicit lane is a sibling in the open symbol even though the
|
||||
// first remains aimed. Move the clip between their linear tracks.
|
||||
await clickNew('lane');
|
||||
s = await shot();
|
||||
assert.equal(laneSymbols(s).length, 2, 'a second explicit command creates a second lane');
|
||||
const move = await evaluate(`(() => {
|
||||
const tracks = [...document.querySelectorAll('.tl-track')].filter(t => t.arthurLane);
|
||||
const from = tracks.find(t => t.querySelector('.tl-cel'));
|
||||
const to = tracks.find(t => t !== from);
|
||||
const cel = from?.querySelector('.tl-cel');
|
||||
if (!cel || !to) return null;
|
||||
const a = cel.getBoundingClientRect(), b = to.getBoundingClientRect();
|
||||
return {sx: a.left + a.width / 2, sy: a.top + a.height / 2,
|
||||
tx: b.left + b.width * .12, ty: b.top + b.height / 2};
|
||||
})()`);
|
||||
assert(move, 'two explicit lanes and a source clip are available');
|
||||
await send('Input.dispatchMouseEvent', {type: 'mousePressed', x: move.sx, y: move.sy,
|
||||
button: 'left', buttons: 1, clickCount: 1});
|
||||
await send('Input.dispatchMouseEvent', {type: 'mouseMoved', x: move.tx, y: move.ty,
|
||||
button: 'left', buttons: 1});
|
||||
await sleep(150);
|
||||
await send('Input.dispatchMouseEvent', {type: 'mouseReleased', x: move.tx, y: move.ty,
|
||||
button: 'left', buttons: 0, clickCount: 1});
|
||||
await sleep(300);
|
||||
s = await shot();
|
||||
assert.deepEqual(laneSymbols(s).map(x => Object.keys(x.nodes).length).sort(), [0, 1],
|
||||
'a clip body moves from one explicit lane to the other');
|
||||
assert.equal(errors.length, 0, JSON.stringify(errors));
|
||||
console.log('PASS: generic lanes preview, rename, move, place, open, trim, roll, and ripple clips');
|
||||
console.log('PASS: symbols are ordinary; explicit lanes rename, accept drops, and exchange clips');
|
||||
} finally {
|
||||
if (ws?.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({ id: 999999, method: 'Browser.close' }));
|
||||
await sleep(350);
|
||||
}
|
||||
ws?.close();
|
||||
chrome.kill();
|
||||
await new Promise(resolve => { if (chrome.exitCode !== null || chrome.signalCode !== null) resolve(); else chrome.once('exit', resolve); });
|
||||
if (ws?.readyState === WebSocket.OPEN) ws.close();
|
||||
chrome.kill('SIGTERM');
|
||||
await new Promise(resolve => chrome.once('exit', resolve));
|
||||
try {
|
||||
rmSync(profile, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
||||
} catch (error) {
|
||||
console.warn(`Temporary browser profile retained at ${profile}: ${error.code}`);
|
||||
if (error.code !== 'ENOTEMPTY') throw error;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue