`docs/lane-handoff.md`, after `timing-handoff.md`'s shape, because the next thread starts cold and the expensive part of that is not the code — it is the decisions that were argued out and would otherwise be argued again. So the section that matters most is the one listing what NOT to re-litigate: the shot length is authored, placing ripples and overwrite is blank-then-place, a position inside a cel refuses and names split, a correction has no time space of its own, a layer's values are a channel, a conflict is not a problem, and a command refuses rather than guesses. Each of those is a paragraph here and a commit message in full. Then the vocabulary, since it was settled one commit ago and the old words are still in this repository's history: instance, cel, lane, drawing, and placement for where a node sits only. With the warning that `exposure` still means the `:expose` grid and always did. Then the mechanisms that keep paying out — `:span` in the node's own frames above all, which is why split, trim and blank cost almost nothing — the known gaps, and how to run the suites, including that a release build clobbers the dev bundle the browser tests need. Recommended next piece is the cel sheet: it needs no new model, and it is the first real evidence the document is not shaped by the timeline that grew up with it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
190 lines
11 KiB
Markdown
190 lines
11 KiB
Markdown
# Lane and cel handoff
|
|
|
|
Status (2026-09-30): the lane model is implemented through its commands. Cels are
|
|
ordinary nodes with their own playback clock, a lane draws them as one row, the
|
|
cel commands all exist, and correction layers evaluate and survive regeneration.
|
|
What is missing is a second view and the commands that make a correction.
|
|
|
|
Seven commits on branch `lanes`, off `624242b`. `master` is untouched and can be
|
|
fast-forwarded. Each commit message is the argument for its change and is worth
|
|
reading before touching what it did — they are the design record, more than this
|
|
file is.
|
|
|
|
3d3c1bb An occurrence is a node, with a clock of its own
|
|
9446829 Reuse, duplicate and make unique: deciding what is shared
|
|
26517af A position is an argument, not another command
|
|
94c0a21 A correction is a layer, and a layer's values are a channel
|
|
72b57e3 Regenerate the base, keep the hand work, and say when you cannot
|
|
76106d3 The shot is as long as somebody said it was
|
|
598c186 One word for one thing: it is a cel
|
|
|
|
## Read first, in this order
|
|
|
|
1. [The Lane Model](lane-model.md) — the design, and the status note under
|
|
*Proof obligations* says what is built. It supersedes `animation-model.md`,
|
|
`timing-model.md` and `architecture.md` wherever they overlap.
|
|
2. `frontend/src/arthur/domain/lane.cljs` — every command, and the reasoning in
|
|
its docstrings.
|
|
3. `frontend/test/arthur/domain/lane_test.cljs` — what the model is asserted to
|
|
do. It is the fastest way to see the shapes.
|
|
4. `frontend/src/arthur/domain/channel.cljs`, the correction-layer section.
|
|
|
|
## Vocabulary — one word for one thing
|
|
|
|
Renamed in `598c186`, after four words had accumulated for one object. Use these
|
|
and do not reintroduce the others.
|
|
|
|
| word | means |
|
|
| --- | --- |
|
|
| instance | the `:kind`. The general thing, anywhere in a document |
|
|
| cel | an instance in a lane. One drawing, held for some duration |
|
|
| lane | a group with `:layout :sequence` |
|
|
| drawing | the content a cel names — an ordinary symbol |
|
|
| placement | ONLY where a node sits: `nest/placement`, and the transform that puts a face on the stage. Never the node itself |
|
|
|
|
`occurrence` and `exposure` are not words for a cel. **`exposure` means something
|
|
else and still does**: `:time :expose` is how many frames each step of a subtree
|
|
lasts, which is what shooting on twos is — `node/expose`, `clock/exposed-frame`,
|
|
`subs/render ::exposure`. Keeping these apart is why the block is called a cel.
|
|
|
|
`:layout :sequence` stays as the field, and is the one place two words are kept
|
|
on purpose: the layout names the RULE — children follow one another and may not
|
|
overlap — and a group carrying it is called a lane. `node/lane?` is where they
|
|
meet.
|
|
|
|
The second view is the CEL SHEET, not the exposure sheet.
|
|
|
|
## Decisions already made — do not re-litigate
|
|
|
|
These were each argued out and are load-bearing. Changing one is a design
|
|
decision, not a cleanup.
|
|
|
|
- **The shot length is authored.** `:frames` is the symbol's window; the
|
|
occupied extent of its lanes is a different fact derived from the cels. A
|
|
command grows the window only when the caller passes `:extent :grow-symbol`,
|
|
and never shrinks it. Blanking the end of a shot leaves empty frames at the
|
|
end, because deriving the window from the extent would make deleting the last
|
|
drawing silently shorten the film. `lane/finish`.
|
|
- **Placing ripples; overwrite is `blank` then `place`.** Overwrite is not a
|
|
policy argument and should not become one until there is a reason `blank`
|
|
cannot serve. An argument whose second value is unimplemented is worse than
|
|
no argument.
|
|
- **A position inside a cel refuses and names `split`.** One command must not
|
|
quietly perform two. The UI offers the retry.
|
|
- **A correction has no time space of its own.** Its `:support` and its values'
|
|
keys are in the frames the base channel's keys are in — the node's. A
|
|
correction on a lane is in lane frames and reaches across the drawings under
|
|
it; one on a cel travels with that cel. Ownership already answered it.
|
|
- **A layer's values are a channel.** Constant, ramp and return motion are one
|
|
mechanism. Do not add a second way to say what a value is over time.
|
|
- **A conflict is not a `problem`.** A document whose topology outgrew a
|
|
correction loads, evaluates and saves; `clip/conflicts` lists the decisions
|
|
waiting for a person. `problems` means the document will not load.
|
|
- **Refuse rather than guess.** Every command returns `{:clip :selection}` or
|
|
`{:refused why}`, never a half-applied edit. Where the model needs a choice
|
|
nobody has made, refusing and saying why is the behaviour, not a placeholder.
|
|
- **A cel is not a row.** Rows, expansion and selection are editor state. The
|
|
document has never known about rows and must not learn.
|
|
|
|
## Next steps, in order
|
|
|
|
1. **The cel sheet.** The doc's own test that the document is separate from its
|
|
presentation: frames down, a column per lane, one cell per frame naming the
|
|
drawing. It needs NO new model — `symbol/lane-cels` and the existing commands
|
|
are the whole API. The obligation is that the same command issued from the
|
|
sheet and from the timeline produces identical document changes and the same
|
|
selection. This is the recommended next piece: it is self-contained, it is
|
|
specified, and it is the first real evidence the model is not shaped by the
|
|
timeline that grew up with it.
|
|
2. **The commands that make a correction** — Constant adjustment, Ramp, Return
|
|
motion over a selected range, per `lane-model.md`. The evaluator is done and
|
|
has no opinion about how a range or a motion shape is chosen, which is now a
|
|
view question. A panel also needs to offer `clip/conflicts` for resolution.
|
|
Note the one open question: a correction needs a stable `:id` from somewhere,
|
|
and cel ids come from the caller because this namespace is pure.
|
|
3. **Slip source and retime.** Both have real design questions open and the doc
|
|
says to refuse rather than approximate: retime needs a defined warp and
|
|
interpolation behaviour, and is not moving keys whose numbers happen to fall
|
|
inside a selection.
|
|
4. **Deleting reused content.** Reference discovery exists (`node/sources`,
|
|
`clip/places`, `clip/contains-symbol?`); the policy does not.
|
|
5. **Collaboration.** `lane-model.md` is explicit that one leaf per channel does
|
|
NOT solve two people editing different keys of the same channel. No conflict
|
|
policy exists for that.
|
|
|
|
## Mechanisms to reuse — these keep paying out
|
|
|
|
- **`:span` is in the node's OWN frames** and `:time` says where they land in
|
|
the lane. Moving an edge of a cel is therefore one write to `:span`, with
|
|
`:time` and `:playback` untouched. This is why split costs nothing, why the
|
|
two halves of a split go on meaning what the one cel meant, why trimming the
|
|
front of a playing insert starts it later into its animation instead of
|
|
restarting it, and why extending a hold leaves lane keys alone. `lane/local`
|
|
and `lane/edged` are the whole geometry; trim, split and blank are all it.
|
|
- **`lane/finish`** is the one commit path: it validates, applies the shot-length
|
|
policy, and returns the refusal. New commands go through it.
|
|
- **`:required-frames` plus the retry event** is the pattern for "this needs a
|
|
decision you have not made": the domain reports what it would need, the UI
|
|
offers one button. `events/ui/lane-retry`.
|
|
- **`lane/lane-frame`** converts a symbol frame to a lane frame, or returns nil
|
|
through a stepped or looping lane where there is no single answer. Nil refuses;
|
|
it never snaps.
|
|
- **`channel/conflict-with`** is the single rule for "can this layer apply to
|
|
this base", used by the validator, by `conflicts`, and by the regeneration
|
|
that marks them.
|
|
- **Two test patterns worth copying.** `the-cursor-agrees-with-the-specification-in-any-frame-order`
|
|
holds the optimized cursor to `value-at` in forward, backward and random order
|
|
— add a case to it for any new channel shape. And `drawn` in `lane_test`
|
|
samples every frame before and after an edit, which is how split and trim are
|
|
proved to change nothing: state a claim as "the same picture" rather than as
|
|
numbers computed by hand.
|
|
|
|
## Known gaps and traps
|
|
|
|
- **Audio lanes do not work.** `symbol/lane-problems` requires `:instance`
|
|
children, so an audio node in a lane is rejected outright. `lane-model.md`
|
|
says a lane may hold visual OR audio cels and should reject only a mixture.
|
|
- **`:z` is required on cels and means nothing there.** A lane never has two
|
|
cels on one frame, so draw order between them cannot matter. `node/problems`
|
|
requires `:z` on every node uniformly, which is its own kind of simplicity —
|
|
but the field is noise on a cel.
|
|
- **`channel/offset-onto` throws** on a shape mismatch that no regeneration has
|
|
recorded as a conflict. That is deliberate — a correction that silently does
|
|
not take is the failure the design exists to prevent, and `channel/problems`
|
|
catches the authored case — but it is a throw in the read path, so any new
|
|
producer of layers must not create a mismatched one.
|
|
- **`docs/timing-handoff.md` is a separate, unreconciled thread.** Performance-
|
|
pose selection and plate drawings/tracing, instance-specific picture-rate
|
|
requests, `pose/put-cut` addressing only `:main`. It predates the lane model
|
|
and nobody has squared the two.
|
|
- **The button row in the timeline pane is a test harness, not a design.** It is
|
|
how the commands were made reachable and provable. `lane-model.md` describes
|
|
the real cel action strip, the breadcrumb and the location bar; none exist.
|
|
- **`shadow-cljs release app` clobbers the dev bundle.** Both builds write
|
|
`../static/arthur/js`, which Django serves, and the optimized build does not
|
|
export the `arthur` global — so after a release the browser tests fail with
|
|
`ReferenceError: arthur is not defined`. Run `npx shadow-cljs compile app` to
|
|
restore it. A running `watch app` does not notice; it rebuilds on the next
|
|
source change.
|
|
|
|
## Running it
|
|
|
|
From `frontend/`:
|
|
|
|
npx shadow-cljs compile test && node out/node-tests.js # 424 tests, 5,749 assertions
|
|
npx shadow-cljs compile app # the bundle Django serves
|
|
npx shadow-cljs release app # then `compile app` again — see above
|
|
|
|
The browser tests need the Django dev server up (`mise exec -- python manage.py
|
|
runserver 8778` from the repo root) and a compiled dev bundle:
|
|
|
|
node --experimental-websocket test/browser/lane.mjs # the lane/cel flow
|
|
CHROME=/usr/bin/chromium node --experimental-websocket test/browser/take.mjs
|
|
|
|
`take.mjs` defaults to a macOS Chrome path, hence `CHROME=`. It writes a real
|
|
project to the local server by design; `lane.mjs` never writes to the server.
|
|
|
|
From the repo root: `mise exec -- python manage.py test clips` — 56 tests.
|
|
|
|
Documents are schema 3. A version 2 document is not read and nothing converts
|
|
one; there is no backward compatibility to preserve anywhere in this work.
|