A position is an argument, not another command
Everything could only be added to the end, because `append` computed its own position — the max end of the lane — and so had no opinion to state. Insert is not a new command; it is the argument that function was missing. `:at` takes a lane frame or `:end`, `:end` is the position where nothing has to move, and appending stops being a separate operation from inserting. New, reused and duplicated drawings all take it, because there was only ever one placement rule. Placing ripples: occurrences at or after the position move later by the new exposure's duration, and `:keep` against `:grow-symbol` still decides what happens at the shot's end. OVERWRITE is deliberately not a policy argument yet. Taking frames away from the occurrence already there is TRIMMING, and an argument whose second value is unimplemented is worse than an argument that is not there. A position strictly inside an existing exposure refuses and names `split`, rather than splitting on the quiet: one command performing two is how a command stops being predictable. Then split, which turned out to cost almost nothing, and that is the interesting part. The two pieces keep ONE `:time` and differ only in `:span`. The right piece's own frames therefore carry on exactly where the left's stopped, so its source clock, its keys and its corrections go on meaning what they meant: a held drawing holds the same frame either side of the cut, and a playing insert plays through it without a seam. There is no arithmetic on in-points to get wrong, and no shot-length question, since the pieces occupy the frames the one exposure occupied. The test samples every frame before and after and asserts the picture is identical — for a hold, for an exposure with a correction of its own, and for a playing insert. That is not a clever split. It is `:span` being in the node's OWN coordinates, which was decided long before there were lanes, paying for something it was not designed for. The same property is why extending a hold leaves lane keys alone. Both new commands act at the playhead, which needed `lane-frame` — the symbol's frame as a frame of the lane's own time, nil through a stepped or looping lane where one is not the other. Nil refuses; it does not snap to a nearby frame. Two smaller things found while doing it. `placeable` promised "a whole lane frame" in its refusal and then accepted 2.5, so both it and `split` now require an integer, as `extend-hold` already did for its delta. And `lane-end` is private: `:end` is the only way to ask for it. 401 tests, 5,612 assertions. The browser flow now splits an exposure at the playhead and puts a drawing in the gap, and checks that six exposures are still one row. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
9446829774
commit
26517af2fd
6 changed files with 296 additions and 42 deletions
|
|
@ -1,9 +1,9 @@
|
|||
# The Lane Model
|
||||
|
||||
Revised 2026-09-30. Target design. Occurrence ownership, source playback, the
|
||||
content and exposure commands and a one-row cel strip are implemented;
|
||||
correction layers, the range and retiming commands, and the remaining views are
|
||||
not. See the status note under
|
||||
content and exposure commands, placement anywhere in a lane, and a one-row cel
|
||||
strip are implemented; correction layers, overwrite and the retiming commands,
|
||||
and the remaining views are not. See the status note under
|
||||
[Proof obligations](#proof-obligations-and-implementation-order).
|
||||
|
||||
This revises the Claude artifact [The Lane Model](https://claude.ai/code/artifact/cd42981d-ed08-493f-94df-b7dd6657f0e6).
|
||||
|
|
@ -442,23 +442,43 @@ The source-channel prototype has been removed: an occurrence names one symbol
|
|||
and carries its own playback clock, and `node/problems` rejects the old
|
||||
`[:source]` channel. What a lane IS lives in `arthur.domain.symbol` beside the
|
||||
other rules about a node map; `arthur.domain.sequence` holds the commands over
|
||||
one — add lane, append drawing, reuse drawing, duplicate drawing, make unique,
|
||||
and extend hold with explicit ripple and shot-length policy. Each is one history
|
||||
step, and each refuses rather than half-applying. The timeline draws a lane's
|
||||
occurrences as cel blocks on the lane's own row, and offers Make unique only
|
||||
where the selected exposure actually shares its drawing.
|
||||
one — add lane, place a drawing (new, reused or duplicated), make unique, split,
|
||||
and extend hold. Each is one history step, and each refuses rather than
|
||||
half-applying. The timeline draws a lane's occurrences as cel blocks on the
|
||||
lane's own row, and offers Make unique only where the selected exposure actually
|
||||
shares its drawing.
|
||||
|
||||
There is ONE placement function and a position argument, so appending is not a
|
||||
different operation from inserting: `:end` is a position like any other, the one
|
||||
where nothing has to move. Placing ripples — occurrences at or after the
|
||||
position move later by the new exposure's duration — and `:keep` versus
|
||||
`:grow-symbol` still decides what happens at the shot's end. OVERWRITE is not a
|
||||
policy argument yet, deliberately: taking frames away from the occurrence
|
||||
already there is trimming, and until `trim` exists, placement that would need it
|
||||
refuses instead of approximating it. A position inside an existing exposure
|
||||
refuses too, and names `split` — one command does not quietly perform two.
|
||||
|
||||
Splitting turned out to cost almost nothing, which is evidence for the
|
||||
representation rather than for the command. The two pieces keep ONE `:time` and
|
||||
differ only in `:span`, so the right piece's own frames carry on where the
|
||||
left's stopped and its source clock, keys and corrections go on meaning what
|
||||
they meant — a held drawing holds the same frame either side, a playing insert
|
||||
plays through the cut without a seam, and the test for it samples every frame
|
||||
before and after and asserts the picture is identical. That falls out of `:span`
|
||||
being in the node's own coordinates; it is not something split arranges.
|
||||
|
||||
Content copies are shallow by default and keep their references to other
|
||||
symbols; `:deep? true` is the explicit copy that shares nothing, so the promise
|
||||
of independence is only made where it is kept.
|
||||
|
||||
Correction layers, the range and retiming commands (blank, split, trim, move,
|
||||
slip source, retime) and the exposure-sheet view are not implemented; a refusal
|
||||
is the current behavior where the model demands an explicit choice nobody has
|
||||
made yet. The suite stands at 397 tests and 5,561 assertions, with
|
||||
Correction layers, overwrite, and the range and retiming commands (blank, trim,
|
||||
move, slip source, retime) and the exposure-sheet view are not implemented; a
|
||||
refusal is the current behavior where the model demands an explicit choice
|
||||
nobody has made yet. The suite stands at 401 tests and 5,612 assertions, with
|
||||
`frontend/test/browser/sequence.mjs` driving the editor through create, hold,
|
||||
overflow, undo, reuse, make unique and duplicate. Rewrite tests that encode
|
||||
superseded behavior rather than preserving behavior to keep them green.
|
||||
overflow, undo, reuse, make unique, duplicate, split and insert. Rewrite tests
|
||||
that encode superseded behavior rather than preserving behavior to keep them
|
||||
green.
|
||||
|
||||
Build small adversarial documents and test their domain operations before
|
||||
expanding the interface:
|
||||
|
|
|
|||
|
|
@ -88,6 +88,41 @@
|
|||
nodes later)]
|
||||
(finish clip sid nodes id extent)))))
|
||||
|
||||
(defn split
|
||||
"Cut occurrence `id` in two at lane frame `cut`. The left piece keeps its
|
||||
identity; the right gets `new-id`.
|
||||
|
||||
NOTHING BUT `:span` DIFFERS between the two pieces. They keep one `:time`, so
|
||||
the right piece's own frames carry on exactly where the left's stopped, and its
|
||||
source clock, its keys and its corrections therefore go on meaning what they
|
||||
meant before the cut — preserved by construction rather than by arithmetic on
|
||||
in-points that could be wrong. A held drawing holds the same frame on both
|
||||
sides; a playing insert plays on through the cut without a seam. That is what
|
||||
`:span` being in the node's OWN coordinates buys, and it is why splitting
|
||||
needs no shot-length policy: the pieces occupy the frames the one exposure
|
||||
occupied.
|
||||
|
||||
The right piece is the selection, because it is the piece that was made."
|
||||
[clip sid id cut new-id]
|
||||
(let [nodes (get-in clip [:symbols sid :nodes])
|
||||
n (get nodes id)
|
||||
lane (get nodes (:parent n))
|
||||
{:keys [at rate]} (node/time-of n)
|
||||
[lo hi] (or (node/placed-span n) [nil nil])]
|
||||
(cond
|
||||
(not (node/sequence? lane)) {:refused "select an occurrence in a sequence lane"}
|
||||
(not (integer? cut)) {:refused "a cut is a whole lane frame"}
|
||||
(contains? nodes new-id) {:refused "the new occurrence ID is already used"}
|
||||
(not (and lo (< lo cut hi)))
|
||||
{:refused (str "frame " cut " is not inside this exposure")}
|
||||
:else
|
||||
(let [local (* rate (- cut at))
|
||||
nodes (-> nodes
|
||||
(assoc-in [id :span 1] local)
|
||||
(assoc new-id (assoc n :id new-id :z (str "a-" new-id)
|
||||
:span [local (second (:span n))])))]
|
||||
(finish clip sid nodes new-id :keep)))))
|
||||
|
||||
(defn add-lane [clip sid id]
|
||||
(if (or (nil? (clip/symbol clip sid)) (get-in clip [:symbols sid :nodes id]))
|
||||
{:refused "the symbol is missing or the lane ID is already used"}
|
||||
|
|
@ -111,27 +146,65 @@
|
|||
:span [0 1] :time {:at at :rate 1}
|
||||
:source {:symbol drawing-id} :playback {:in 0 :speed 0 :end :stop}})
|
||||
|
||||
(defn- append
|
||||
"Put a held occurrence of `drawing-id` after everything already in `lane-id`.
|
||||
`:frame` in the result is where it lands, for a caller that wants to look at
|
||||
what it just made."
|
||||
[clip sid lane-id id drawing-id extent]
|
||||
(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]} (lane-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/sequence-members nodes lane-id))))
|
||||
|
||||
(defn- place
|
||||
"Put a held occurrence 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 occurrence 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]
|
||||
(let [nodes (get-in clip [:symbols sid :nodes])
|
||||
at (apply max 0 (map #(second (node/placed-span %))
|
||||
(symbol/sequence-members nodes lane-id)))
|
||||
result (finish clip sid (assoc nodes id (held id lane-id drawing-id at)) id extent)
|
||||
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 (filter #(>= (first (node/placed-span %)) lo)
|
||||
(symbol/sequence-members 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 (finish clip sid nodes id extent)
|
||||
m (lane-map nodes lane-id)]
|
||||
(cond-> result
|
||||
(:clip result) (assoc :frame (+ (:at m) (/ at (:rate m)))))))
|
||||
|
||||
(defn- appendable
|
||||
"Why a held occurrence cannot go into `lane-id`, or nil."
|
||||
[clip sid lane-id id]
|
||||
(defn- placeable
|
||||
"Why a held occurrence 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)]
|
||||
lane (get nodes lane-id)
|
||||
;; INSIDE an exposure is not a position for another one. Splitting that
|
||||
;; exposure 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/sequence-members nodes lane-id)))]
|
||||
(cond
|
||||
(not (node/sequence? lane)) "select a sequence lane"
|
||||
(contains? nodes id) "the new occurrence 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 an exposure; split it first")
|
||||
(nil? (lane-map nodes lane-id)) "drawing creation through a stepped or looping lane is not supported"
|
||||
:else (first (symbol/sequence-problems nodes)))))
|
||||
|
||||
|
|
@ -142,13 +215,13 @@
|
|||
Fresh content, not a blank range: a lane with no occurrence 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 [extent] :or {extent :keep}}]
|
||||
(if-let [why (or (appendable clip sid lane-id id)
|
||||
[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}
|
||||
(append (assoc-in clip [:symbols drawing-id]
|
||||
(place (assoc-in clip [:symbols drawing-id]
|
||||
{:id drawing-id :name (name drawing-id) :frames 1 :nodes {}})
|
||||
sid lane-id id drawing-id extent)))
|
||||
sid lane-id id drawing-id at extent)))
|
||||
|
||||
(defn reuse-drawing
|
||||
"Append a held occurrence of content the document ALREADY has, so the same
|
||||
|
|
@ -157,15 +230,15 @@
|
|||
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 [extent] :or {extent :keep}}]
|
||||
(if-let [why (or (appendable clip sid lane-id id)
|
||||
[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}
|
||||
(append clip sid lane-id id drawing-id extent)))
|
||||
(place clip sid lane-id id drawing-id at extent)))
|
||||
|
||||
(defn- copied
|
||||
"A copy of symbol `from`, as `{:clip :id}`.
|
||||
|
|
@ -192,15 +265,15 @@
|
|||
rather than a copy of `id`'s own transform or corrections: those belong to
|
||||
that exposure, and carrying them over would make duplicating a drawing quietly
|
||||
duplicate the treatment of one use of it."
|
||||
[clip sid id new-id {:keys [extent deep?] :or {extent :keep}}]
|
||||
[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 an occurrence to duplicate")
|
||||
(when-not (clip/symbol clip from) "the drawing it places is missing")
|
||||
(appendable clip sid (:parent n) new-id))]
|
||||
(placeable clip sid (:parent n) new-id at))]
|
||||
{:refused why}
|
||||
(let [{c :clip copy :id} (copied clip from deep?)]
|
||||
(append c sid (:parent n) new-id copy extent)))))
|
||||
(place c sid (:parent n) new-id copy at extent)))))
|
||||
|
||||
(defn make-unique
|
||||
"Point occurrence `id` at a private copy of its content, leaving every other
|
||||
|
|
|
|||
|
|
@ -61,9 +61,10 @@
|
|||
(defn- committed
|
||||
"One appending command, as effects: commit it, and look at what it made.
|
||||
|
||||
Seeking is the whole reason these are `-fx` events. A new exposure lands after
|
||||
everything already in the lane, which is usually off the playhead, and a
|
||||
drawing you cannot see is not one you can draw in."
|
||||
Seeking is the whole reason these are `-fx` events. An appended exposure lands
|
||||
past the end of the lane, off the playhead, and a drawing you cannot see is not
|
||||
one you can draw in. An inserted one is already under the playhead and the
|
||||
seek is a no-op, which is the same rule and not a second one."
|
||||
[db sid result retry]
|
||||
(let [{clip :clip st :store} (store/entry (:clip/current db))
|
||||
path (nth (get-in db [:ui :selection]) 3 nil)
|
||||
|
|
@ -110,6 +111,35 @@
|
|||
{: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.
|
||||
(fn [{:keys [db]} [_ extent]]
|
||||
(let [clip (:clip (store/entry (:clip/current db)))
|
||||
[_ sid id] (get-in db [:ui :selection])
|
||||
lane (selected-lane clip sid id)
|
||||
at (sequence/lane-frame clip sid lane (get-in db [:playback :frame]))
|
||||
result (if at
|
||||
(sequence/append-drawing clip sid lane (random-uuid) (clip/fresh-id clip)
|
||||
{:at at :extent (or extent :keep)})
|
||||
{:refused "this lane's frames are not the open symbol's"})]
|
||||
(committed db sid result [::insert-drawing :grow-symbol]))))
|
||||
|
||||
(rf/reg-event-db
|
||||
::split-exposure
|
||||
(fn [db _]
|
||||
(let [clip (:clip (store/entry (:clip/current db)))
|
||||
[_ sid id] (get-in db [:ui :selection])
|
||||
cut (sequence/lane-frame clip sid (:parent (get-in clip [:symbols sid :nodes id]))
|
||||
(get-in db [:playback :frame]))]
|
||||
(apply-sequence-command
|
||||
db sid (if cut
|
||||
(sequence/split clip sid id cut (random-uuid))
|
||||
{:refused "this lane's frames are not the open symbol's"})
|
||||
nil))))
|
||||
|
||||
(rf/reg-event-db
|
||||
::make-unique
|
||||
(fn [db [_ deep?]]
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@
|
|||
(:require [clojure.string :as str]
|
||||
[arthur.domain.node :as node]
|
||||
[arthur.domain.nest :as nest]
|
||||
[arthur.domain.sequence :as sequence]
|
||||
[arthur.domain.symbol :as symbol]
|
||||
[arthur.domain.trace :as trace]
|
||||
[arthur.events.playback :as pb]
|
||||
|
|
@ -236,7 +237,13 @@
|
|||
shared? (and cel? (< 1 (count (for [[_ sym] (:symbols clip)
|
||||
[_ other] (:nodes sym)
|
||||
:when (= (node/source n) (node/source other))]
|
||||
other))))]
|
||||
other))))
|
||||
;; Both act at the playhead, so both are offered only where the playhead
|
||||
;; is somewhere they mean something.
|
||||
at (when lane? (sequence/lane-frame clip sid (:id lane) frame))
|
||||
insertable? (and lane? (integer? at))
|
||||
splittable? (and cel? (integer? at)
|
||||
(let [[lo hi] (node/placed-span n)] (< lo at hi)))]
|
||||
[:div.pane-head
|
||||
[:button {:on-click #(rf/dispatch [::pb/toggle])} (if playing? "pause" "play")]
|
||||
[:button {:on-click #(rf/dispatch [::pb/seek 0])} "|<"]
|
||||
|
|
@ -262,6 +269,12 @@
|
|||
[:button {:disabled (not lane?)
|
||||
:title "append a new independent drawing to the selected lane"
|
||||
:on-click #(rf/dispatch [::ui/append-drawing])} "new drawing"]
|
||||
[:button {:disabled (not insertable?)
|
||||
:title "a new drawing at the playhead; later drawings ripple later"
|
||||
:on-click #(rf/dispatch [::ui/insert-drawing])} "insert"]
|
||||
[:button {:disabled (not splittable?)
|
||||
:title "cut this exposure in two at the playhead; the picture does not change"
|
||||
:on-click #(rf/dispatch [::ui/split-exposure])} "split"]
|
||||
[:button {:disabled (not cel?)
|
||||
:title "expose this same drawing again — one drawing, two exposures"
|
||||
:on-click #(rf/dispatch [::ui/reuse-drawing])} "reuse"]
|
||||
|
|
|
|||
|
|
@ -334,3 +334,96 @@
|
|||
(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 occurrences 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 (sequence/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 (sequence/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 (sequence/append-drawing doc :main :girl :n :drawing-n
|
||||
{:at 0 :extent :grow-symbol})))))
|
||||
;; Inside an exposure is not a position for another one.
|
||||
(is (re-find #"split it first"
|
||||
(:refused (sequence/append-drawing doc :main :girl :n :drawing-n
|
||||
{:at 2 :extent :grow-symbol}))))
|
||||
(is (:refused (sequence/append-drawing doc :main :girl :n :drawing-n
|
||||
{:at -1 :extent :grow-symbol})))
|
||||
(is (:refused (sequence/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 (sequence/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 (sequence/duplicate-drawing doc :main :b :n
|
||||
{:at 4 :extent :grow-symbol})
|
||||
[:clip :symbols :main :nodes :n]))))))
|
||||
|
||||
(deftest splitting-an-exposure-changes-nothing-that-is-drawn
|
||||
(let [doc (document)
|
||||
fs (range 12)
|
||||
before (drawn doc fs)]
|
||||
(doseq [[label id cut] [["a held drawing" :a 2]
|
||||
["an exposure with a correction of its own" :b 6]
|
||||
["a playing insert" :insert 10]]]
|
||||
(testing label
|
||||
(let [r (sequence/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 exposure did")
|
||||
(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])
|
||||
(select-keys (get-in after [:symbols :main :nodes :right]) [:source :playback :channels])))
|
||||
(is (= 12 (get-in after [:symbols :main :frames])) "and no shot-length question")
|
||||
(is (empty? (clip/problems after))))))))
|
||||
|
||||
(deftest split-refuses-anything-that-is-not-one-cut-inside-one-exposure
|
||||
(let [doc (document)]
|
||||
(doseq [cut [0 4 8 12 -1 2.5 ##NaN nil]]
|
||||
(is (:refused (sequence/split doc :main :b cut :right)) (str "cut at " (pr-str cut))))
|
||||
(is (:refused (sequence/split doc :main :girl 2 :right)) "a lane is not an exposure")
|
||||
(is (:refused (sequence/split doc :main :plate 2 :right)) "nor is a shape outside one")
|
||||
(is (:refused (sequence/split doc :main :a 2 :b)) "the new ID has to be free")))
|
||||
|
||||
(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 (sequence/split doc :main :a 2 :right))
|
||||
r (sequence/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)))))
|
||||
|
|
|
|||
|
|
@ -149,8 +149,33 @@ try {
|
|||
assert.equal(new Set(cels.map(n => n.source.symbol)).size, 4,
|
||||
'four exposures of four drawings: nothing is shared once every copy is made');
|
||||
assert.equal(s.history.done.length, before.history.done.length + 3, 'three more commands, three more steps');
|
||||
|
||||
// A drawing into the middle of a hold: split, then insert. Both act at the
|
||||
// playhead, and neither guesses what the other one is for.
|
||||
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)])
|
||||
.sort((a, b) => a[0] - b[0]);
|
||||
assert.deepEqual(placed(s), [[0, 4], [4, 5], [5, 6], [6, 7]]);
|
||||
await evaluate(`document.querySelector('.tl-cel').click()`);
|
||||
await sleep(200);
|
||||
assert.equal(await enabled('split'), false, 'the start of an exposure is not inside it');
|
||||
await click('+1');
|
||||
await click('+1');
|
||||
assert.equal(await enabled('split'), true);
|
||||
await click('split');
|
||||
s = await shot();
|
||||
assert.deepEqual(placed(s), [[0, 2], [2, 4], [4, 5], [5, 6], [6, 7]],
|
||||
'one exposure became two, over the frames it had');
|
||||
await click('insert');
|
||||
s = await shot();
|
||||
assert.deepEqual(placed(s), [[0, 2], [2, 3], [3, 5], [5, 6], [6, 7], [7, 8]],
|
||||
'the new drawing took frame 2 and everything from there rippled later');
|
||||
assert.equal(await evaluate('document.querySelectorAll(".tl-cel").length'), 6);
|
||||
assert.equal(await evaluate('document.querySelectorAll(".tl-label:not(.tl-corner)").length'), 1,
|
||||
'six exposures, still one row');
|
||||
assert.equal(s.history.done.length, before.history.done.length + 5);
|
||||
assert.equal(errors.length, 0, JSON.stringify(errors));
|
||||
console.log('PASS: create lane/drawings, one-row cels, hold ripple, seek, explicit overflow, atomic undo, reuse/make unique/duplicate; no server writes');
|
||||
console.log('PASS: create lane/drawings, one-row cels, hold ripple, seek, explicit overflow, atomic undo, reuse/make unique/duplicate, split and insert; no server writes');
|
||||
} finally {
|
||||
if (ws?.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({ id: 999999, method: 'Browser.close' }));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue