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
|
|
@ -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