The cel sheet is a second projection of the same lane rows: frames run down, lanes run across, and each occupied cell carries the timeline cel's exact selection address. The shared action strip proves the point in the browser test by selecting a cell and issuing the existing hold command. Before exposing that second entrance, fix the boundary mistakes it revealed. Nested commands now convert the open playhead through their enclosing instance path. Overwrite composes blanking with non-rippling placement as one transaction. Picture-rate and pose sampling select only the generated base frame while hand corrections retain the node's authored frame. Stack validation follows covering replacement layers so a document accepted by the validator cannot throw solely because a later offset sees a different shape. 429 tests, 5,767 assertions; both browser flows; 56 Django tests; optimized frontend build.
184 lines
10 KiB
Markdown
184 lines
10 KiB
Markdown
# Lane and cel handoff
|
|
|
|
Status (2026-09-30): the lane model is implemented through its commands and its
|
|
first two views. Cels are ordinary nodes with their own playback clock, the
|
|
timeline draws them as one row, the cel sheet draws frames down and lanes across,
|
|
and both views issue the same commands. Correction layers evaluate and survive
|
|
regeneration. What is missing is the commands that make a correction.
|
|
|
|
The commits beginning at `3d3c1bb` are the argument for the model and are worth
|
|
reading before touching what they 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 non-rippling placement.**
|
|
`lane/overwrite-drawing` composes those pieces as one transaction. Insertion
|
|
retains its ripple rule; overwrite does not move any surviving cel.
|
|
- **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 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.
|
|
2. **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.
|
|
3. **Deleting reused content.** Reference discovery exists (`node/sources`,
|
|
`clip/places`, `clip/contains-symbol?`); the policy does not.
|
|
4. **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 rule for whether one offset fits a base,
|
|
used by `conflicts` and regeneration. Validation additionally follows prior
|
|
replacement layers, so it cannot approve a stack that throws when read.
|
|
- **Generated sampling applies to the base, not the hand correction.** Picture
|
|
rate and pose selection may choose an earlier generated frame; correction
|
|
support and values still read the node's current authored frame.
|
|
- **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 # 429 tests, 5,767 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.
|