arthur/docs/lane-handoff.md

220 lines
13 KiB
Markdown
Raw Normal View History

A lane is a generic row of symbol clips A lane was a drawing lane: the only thing that could go in one was a one-frame held cel, and every other symbol instance stayed a permanent root row of its own. Those are not two kinds of timing, they are one kind with two creation policies. `lane/place-symbol` drops any library symbol in as a clip that plays naturally at speed one, `lane/adopt` moves an instance that is already in the document into a lane keeping its source, span, playback and corrections, and `append-drawing`/`overwrite-drawing` keep being the policy that makes a new empty symbol a one-frame hold. The child shape they produce is the same. Both new commands claim their interval through `blank` before they write, so the partition rule is unchanged and unduplicated: placing into occupied lane time trims, removes or splits the incumbents, and a lane still never stores an overlap. Real compositing overlap is another lane, where the order is explicit. Creating a symbol with nothing aimed now makes a lane and a clip in it instead of a loose root instance, and a pool drop prefers an explicitly targeted lane, then the selected one, and makes a lane only when there is neither. That is what stops the row-per-symbol growth coming back in through the drop path, and it is why `add-lane` now takes a z in front of the existing root nodes and calls what it makes a "lane" rather than "drawings". The timeline learned the two gestures that a generic lane needs. A clip body dragged over another lane's track previews there as a dashed block and lands through `::adopt-in-lane`; the track is found with `elementsFromPoint` and its selection read back off the element, because a pointer capture does not retarget. A pool drop over an existing lane previews as a dashed clip inside that lane instead of a temporary new row that appears and then vanishes -- which also needed the drag-leave check to be geometric, since inserting the preview changes the element under the pointer and Chromium then reports a leave with no related target. Lanes are renameable from their label, by double-click, F2, or the pencil, through `::rename-node`. `symbol/lane-cels` is `symbol/lane-clips`, and the vocabulary table in the handoff now separates the two words it had merged: a clip is an instance in a lane, and a cel is specifically the one-frame held source that drawing creation makes. Keeping `cel` for the policy is what lets the lane stop being about drawings at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-01 15:13:07 -04:00
# Lane and symbol-clip handoff
Status (2026-10-01): the timeline is the one timing interface. A lane is a
generic non-overlapping row of symbol clips; it is not a special drawing type.
Dropping a library symbol makes a naturally playing clip, while creating a new
empty symbol makes a one-frame held clip at the playhead. With no destination
lane, either operation creates one. Existing legacy root symbol rows can be
dragged into a lane. Blocks move by mouse; edge drags claim time by trimming
neighbors; Shift-edge drags ripple every later clip; and the center of a shared
cut composes the two edge edits into a rolling edit. Linked audio follows picture
moves while its edges remain independently trimmable.
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 |
A lane is a generic row of symbol clips A lane was a drawing lane: the only thing that could go in one was a one-frame held cel, and every other symbol instance stayed a permanent root row of its own. Those are not two kinds of timing, they are one kind with two creation policies. `lane/place-symbol` drops any library symbol in as a clip that plays naturally at speed one, `lane/adopt` moves an instance that is already in the document into a lane keeping its source, span, playback and corrections, and `append-drawing`/`overwrite-drawing` keep being the policy that makes a new empty symbol a one-frame hold. The child shape they produce is the same. Both new commands claim their interval through `blank` before they write, so the partition rule is unchanged and unduplicated: placing into occupied lane time trims, removes or splits the incumbents, and a lane still never stores an overlap. Real compositing overlap is another lane, where the order is explicit. Creating a symbol with nothing aimed now makes a lane and a clip in it instead of a loose root instance, and a pool drop prefers an explicitly targeted lane, then the selected one, and makes a lane only when there is neither. That is what stops the row-per-symbol growth coming back in through the drop path, and it is why `add-lane` now takes a z in front of the existing root nodes and calls what it makes a "lane" rather than "drawings". The timeline learned the two gestures that a generic lane needs. A clip body dragged over another lane's track previews there as a dashed block and lands through `::adopt-in-lane`; the track is found with `elementsFromPoint` and its selection read back off the element, because a pointer capture does not retarget. A pool drop over an existing lane previews as a dashed clip inside that lane instead of a temporary new row that appears and then vanishes -- which also needed the drag-leave check to be geometric, since inserting the preview changes the element under the pointer and Chromium then reports a leave with no related target. Lanes are renameable from their label, by double-click, F2, or the pencil, through `::rename-node`. `symbol/lane-cels` is `symbol/lane-clips`, and the vocabulary table in the handoff now separates the two words it had merged: a clip is an instance in a lane, and a cel is specifically the one-frame held source that drawing creation makes. Keeping `cel` for the policy is what lets the lane stop being about drawings at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-01 15:13:07 -04:00
| clip | an instance in a lane. It can hold one source frame or play a symbol naturally |
| cel | specifically a one-frame source held over a clip's duration; the empty-symbol/drawing creation policy |
| lane | a group with `:layout :sequence` |
A lane is a generic row of symbol clips A lane was a drawing lane: the only thing that could go in one was a one-frame held cel, and every other symbol instance stayed a permanent root row of its own. Those are not two kinds of timing, they are one kind with two creation policies. `lane/place-symbol` drops any library symbol in as a clip that plays naturally at speed one, `lane/adopt` moves an instance that is already in the document into a lane keeping its source, span, playback and corrections, and `append-drawing`/`overwrite-drawing` keep being the policy that makes a new empty symbol a one-frame hold. The child shape they produce is the same. Both new commands claim their interval through `blank` before they write, so the partition rule is unchanged and unduplicated: placing into occupied lane time trims, removes or splits the incumbents, and a lane still never stores an overlap. Real compositing overlap is another lane, where the order is explicit. Creating a symbol with nothing aimed now makes a lane and a clip in it instead of a loose root instance, and a pool drop prefers an explicitly targeted lane, then the selected one, and makes a lane only when there is neither. That is what stops the row-per-symbol growth coming back in through the drop path, and it is why `add-lane` now takes a z in front of the existing root nodes and calls what it makes a "lane" rather than "drawings". The timeline learned the two gestures that a generic lane needs. A clip body dragged over another lane's track previews there as a dashed block and lands through `::adopt-in-lane`; the track is found with `elementsFromPoint` and its selection read back off the element, because a pointer capture does not retarget. A pool drop over an existing lane previews as a dashed clip inside that lane instead of a temporary new row that appears and then vanishes -- which also needed the drag-leave check to be geometric, since inserting the preview changes the element under the pointer and Chromium then reports a leave with no related target. Lanes are renameable from their label, by double-click, F2, or the pencil, through `::rename-node`. `symbol/lane-cels` is `symbol/lane-clips`, and the vocabulary table in the handoff now separates the two words it had merged: a clip is an instance in a lane, and a cel is specifically the one-frame held source that drawing creation makes. Keeping `cel` for the policy is what lets the lane stop being about drawings at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-01 15:13:07 -04:00
| drawing | content authored into a symbol; not a different timeline node type |
| 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.
## 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 lane is a generic row of symbol clips A lane was a drawing lane: the only thing that could go in one was a one-frame held cel, and every other symbol instance stayed a permanent root row of its own. Those are not two kinds of timing, they are one kind with two creation policies. `lane/place-symbol` drops any library symbol in as a clip that plays naturally at speed one, `lane/adopt` moves an instance that is already in the document into a lane keeping its source, span, playback and corrections, and `append-drawing`/`overwrite-drawing` keep being the policy that makes a new empty symbol a one-frame hold. The child shape they produce is the same. Both new commands claim their interval through `blank` before they write, so the partition rule is unchanged and unduplicated: placing into occupied lane time trims, removes or splits the incumbents, and a lane still never stores an overlap. Real compositing overlap is another lane, where the order is explicit. Creating a symbol with nothing aimed now makes a lane and a clip in it instead of a loose root instance, and a pool drop prefers an explicitly targeted lane, then the selected one, and makes a lane only when there is neither. That is what stops the row-per-symbol growth coming back in through the drop path, and it is why `add-lane` now takes a z in front of the existing root nodes and calls what it makes a "lane" rather than "drawings". The timeline learned the two gestures that a generic lane needs. A clip body dragged over another lane's track previews there as a dashed block and lands through `::adopt-in-lane`; the track is found with `elementsFromPoint` and its selection read back off the element, because a pointer capture does not retarget. A pool drop over an existing lane previews as a dashed clip inside that lane instead of a temporary new row that appears and then vanishes -- which also needed the drag-leave check to be geometric, since inserting the preview changes the element under the pointer and Chromium then reports a leave with no related target. Lanes are renameable from their label, by double-click, F2, or the pencil, through `::rename-node`. `symbol/lane-cels` is `symbol/lane-clips`, and the vocabulary table in the handoff now separates the two words it had merged: a clip is an instance in a lane, and a cel is specifically the one-frame held source that drawing creation makes. Keeping `cel` for the policy is what lets the lane stop being about drawings at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-01 15:13:07 -04:00
- **A clip is not a row.** Rows, expansion and selection are editor state. The
document has never known about rows and must not learn.
A lane is a generic row of symbol clips A lane was a drawing lane: the only thing that could go in one was a one-frame held cel, and every other symbol instance stayed a permanent root row of its own. Those are not two kinds of timing, they are one kind with two creation policies. `lane/place-symbol` drops any library symbol in as a clip that plays naturally at speed one, `lane/adopt` moves an instance that is already in the document into a lane keeping its source, span, playback and corrections, and `append-drawing`/`overwrite-drawing` keep being the policy that makes a new empty symbol a one-frame hold. The child shape they produce is the same. Both new commands claim their interval through `blank` before they write, so the partition rule is unchanged and unduplicated: placing into occupied lane time trims, removes or splits the incumbents, and a lane still never stores an overlap. Real compositing overlap is another lane, where the order is explicit. Creating a symbol with nothing aimed now makes a lane and a clip in it instead of a loose root instance, and a pool drop prefers an explicitly targeted lane, then the selected one, and makes a lane only when there is neither. That is what stops the row-per-symbol growth coming back in through the drop path, and it is why `add-lane` now takes a z in front of the existing root nodes and calls what it makes a "lane" rather than "drawings". The timeline learned the two gestures that a generic lane needs. A clip body dragged over another lane's track previews there as a dashed block and lands through `::adopt-in-lane`; the track is found with `elementsFromPoint` and its selection read back off the element, because a pointer capture does not retarget. A pool drop over an existing lane previews as a dashed clip inside that lane instead of a temporary new row that appears and then vanishes -- which also needed the drag-leave check to be geometric, since inserting the preview changes the element under the pointer and Chromium then reports a leave with no related target. Lanes are renameable from their label, by double-click, F2, or the pencil, through `::rename-node`. `symbol/lane-cels` is `symbol/lane-clips`, and the vocabulary table in the handoff now separates the two words it had merged: a clip is an instance in a lane, and a cel is specifically the one-frame held source that drawing creation makes. Keeping `cel` for the policy is what lets the lane stop being about drawings at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-01 15:13:07 -04:00
- **A lane is generic.** Drawing creation, library placement, and adopting an
existing root instance all produce the same child instance shape. The only
difference is playback policy: a new empty drawing holds source frame zero;
a dropped library symbol plays at speed one.
- **Placement claims time.** Lanes never store overlaps. A new or extended clip
trims, removes, or splits whatever previously owned the claimed interval.
Real compositing overlap uses another lane, where ordering remains explicit.
2026-10-01 14:37:59 -04:00
## Current timeline interaction
A lane is a generic row of symbol clips A lane was a drawing lane: the only thing that could go in one was a one-frame held cel, and every other symbol instance stayed a permanent root row of its own. Those are not two kinds of timing, they are one kind with two creation policies. `lane/place-symbol` drops any library symbol in as a clip that plays naturally at speed one, `lane/adopt` moves an instance that is already in the document into a lane keeping its source, span, playback and corrections, and `append-drawing`/`overwrite-drawing` keep being the policy that makes a new empty symbol a one-frame hold. The child shape they produce is the same. Both new commands claim their interval through `blank` before they write, so the partition rule is unchanged and unduplicated: placing into occupied lane time trims, removes or splits the incumbents, and a lane still never stores an overlap. Real compositing overlap is another lane, where the order is explicit. Creating a symbol with nothing aimed now makes a lane and a clip in it instead of a loose root instance, and a pool drop prefers an explicitly targeted lane, then the selected one, and makes a lane only when there is neither. That is what stops the row-per-symbol growth coming back in through the drop path, and it is why `add-lane` now takes a z in front of the existing root nodes and calls what it makes a "lane" rather than "drawings". The timeline learned the two gestures that a generic lane needs. A clip body dragged over another lane's track previews there as a dashed block and lands through `::adopt-in-lane`; the track is found with `elementsFromPoint` and its selection read back off the element, because a pointer capture does not retarget. A pool drop over an existing lane previews as a dashed clip inside that lane instead of a temporary new row that appears and then vanishes -- which also needed the drag-leave check to be geometric, since inserting the preview changes the element under the pointer and Chromium then reports a leave with no related target. Lanes are renameable from their label, by double-click, F2, or the pencil, through `::rename-node`. `symbol/lane-cels` is `symbol/lane-clips`, and the vocabulary table in the handoff now separates the two words it had merged: a clip is an instance in a lane, and a cel is specifically the one-frame held source that drawing creation makes. Keeping `cel` for the policy is what lets the lane stop being about drawings at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-01 15:13:07 -04:00
- Creating a symbol inside an aimed lane creates a one-frame held clip at the
playhead. With no aimed lane it first creates a lane. Drawing a polygon uses
the existing clip there or creates the same one-frame clip when the frame is
empty.
- Dropping any library symbol into a lane creates a natural-duration playing
clip. Dropping it on unclaimed timeline or stage space first creates a lane.
- Dragging a clip body moves it. A linked audio node follows a picture move;
2026-10-01 14:37:59 -04:00
moving or trimming the audio itself remains independent.
- Dragging a right edge changes its endpoint. Growth consumes adjacent spans
instead of overlapping them. Shift-drag inserts or removes lane time by moving
A lane is a generic row of symbol clips A lane was a drawing lane: the only thing that could go in one was a one-frame held cel, and every other symbol instance stayed a permanent root row of its own. Those are not two kinds of timing, they are one kind with two creation policies. `lane/place-symbol` drops any library symbol in as a clip that plays naturally at speed one, `lane/adopt` moves an instance that is already in the document into a lane keeping its source, span, playback and corrections, and `append-drawing`/`overwrite-drawing` keep being the policy that makes a new empty symbol a one-frame hold. The child shape they produce is the same. Both new commands claim their interval through `blank` before they write, so the partition rule is unchanged and unduplicated: placing into occupied lane time trims, removes or splits the incumbents, and a lane still never stores an overlap. Real compositing overlap is another lane, where the order is explicit. Creating a symbol with nothing aimed now makes a lane and a clip in it instead of a loose root instance, and a pool drop prefers an explicitly targeted lane, then the selected one, and makes a lane only when there is neither. That is what stops the row-per-symbol growth coming back in through the drop path, and it is why `add-lane` now takes a z in front of the existing root nodes and calls what it makes a "lane" rather than "drawings". The timeline learned the two gestures that a generic lane needs. A clip body dragged over another lane's track previews there as a dashed block and lands through `::adopt-in-lane`; the track is found with `elementsFromPoint` and its selection read back off the element, because a pointer capture does not retarget. A pool drop over an existing lane previews as a dashed clip inside that lane instead of a temporary new row that appears and then vanishes -- which also needed the drag-leave check to be geometric, since inserting the preview changes the element under the pointer and Chromium then reports a leave with no related target. Lanes are renameable from their label, by double-click, F2, or the pencil, through `::rename-node`. `symbol/lane-cels` is `symbol/lane-clips`, and the vocabulary table in the handoff now separates the two words it had merged: a clip is an instance in a lane, and a cel is specifically the one-frame held source that drawing creation makes. Keeping `cel` for the policy is what lets the lane stop being about drawings at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-01 15:13:07 -04:00
every later clip by the same delta.
2026-10-01 14:37:59 -04:00
- At a shared boundary, the left and right hit zones trim one side. The center
is a rolling edit: right-edge resize followed by left-edge resize at one frame.
- Split, trim-in, and trim-out are direct buttons and are disabled without an
editable selected span. Movement is a mouse gesture, not a toolbar command.
2026-10-01 14:37:59 -04:00
## Next steps, in order
2026-10-01 01:21:55 -04:00
The implemented correction slice and its remaining UI limits are recorded in
[Correction authoring](correction-authoring-plan.md).
1. **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.
2. **Deleting reused content.** Reference discovery exists (`node/sources`,
`clip/places`, `clip/contains-symbol?`); the policy does not.
3. **Displayed-range correction gestures.** The first correction panel asks for
explicit owner frames. Dragging a range in a retimed/nested view still needs
a proved mapping; do not make it snap through floors or loops.
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.
- **Correction commands live in `domain/correction.cljs`.** IDs come from the
event caller; the pure command materializes default transform channels,
appends one layer, and validates the complete document. The inspector authors
rotation and position offsets in explicit owner frames. One Apply is one undo
step. `channel/reconcile` is the shared ordered-stack compatibility rule used
by validation, conflict reporting, and regeneration.
- **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
A lane is a generic row of symbol clips A lane was a drawing lane: the only thing that could go in one was a one-frame held cel, and every other symbol instance stayed a permanent root row of its own. Those are not two kinds of timing, they are one kind with two creation policies. `lane/place-symbol` drops any library symbol in as a clip that plays naturally at speed one, `lane/adopt` moves an instance that is already in the document into a lane keeping its source, span, playback and corrections, and `append-drawing`/`overwrite-drawing` keep being the policy that makes a new empty symbol a one-frame hold. The child shape they produce is the same. Both new commands claim their interval through `blank` before they write, so the partition rule is unchanged and unduplicated: placing into occupied lane time trims, removes or splits the incumbents, and a lane still never stores an overlap. Real compositing overlap is another lane, where the order is explicit. Creating a symbol with nothing aimed now makes a lane and a clip in it instead of a loose root instance, and a pool drop prefers an explicitly targeted lane, then the selected one, and makes a lane only when there is neither. That is what stops the row-per-symbol growth coming back in through the drop path, and it is why `add-lane` now takes a z in front of the existing root nodes and calls what it makes a "lane" rather than "drawings". The timeline learned the two gestures that a generic lane needs. A clip body dragged over another lane's track previews there as a dashed block and lands through `::adopt-in-lane`; the track is found with `elementsFromPoint` and its selection read back off the element, because a pointer capture does not retarget. A pool drop over an existing lane previews as a dashed clip inside that lane instead of a temporary new row that appears and then vanishes -- which also needed the drag-leave check to be geometric, since inserting the preview changes the element under the pointer and Chromium then reports a leave with no related target. Lanes are renameable from their label, by double-click, F2, or the pencil, through `::rename-node`. `symbol/lane-cels` is `symbol/lane-clips`, and the vocabulary table in the handoff now separates the two words it had merged: a clip is an instance in a lane, and a cel is specifically the one-frame held source that drawing creation makes. Keeping `cel` for the policy is what lets the lane stop being about drawings at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-01 15:13:07 -04:00
- **Audio remains outside visual lanes.** `symbol/lane-problems` deliberately
requires symbol instances. Audio is still an independent root node that can
link to picture; making audio itself lane-based would need an explicit lane
capability rather than a mixed child rule.
- **`: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.
A lane is a generic row of symbol clips A lane was a drawing lane: the only thing that could go in one was a one-frame held cel, and every other symbol instance stayed a permanent root row of its own. Those are not two kinds of timing, they are one kind with two creation policies. `lane/place-symbol` drops any library symbol in as a clip that plays naturally at speed one, `lane/adopt` moves an instance that is already in the document into a lane keeping its source, span, playback and corrections, and `append-drawing`/`overwrite-drawing` keep being the policy that makes a new empty symbol a one-frame hold. The child shape they produce is the same. Both new commands claim their interval through `blank` before they write, so the partition rule is unchanged and unduplicated: placing into occupied lane time trims, removes or splits the incumbents, and a lane still never stores an overlap. Real compositing overlap is another lane, where the order is explicit. Creating a symbol with nothing aimed now makes a lane and a clip in it instead of a loose root instance, and a pool drop prefers an explicitly targeted lane, then the selected one, and makes a lane only when there is neither. That is what stops the row-per-symbol growth coming back in through the drop path, and it is why `add-lane` now takes a z in front of the existing root nodes and calls what it makes a "lane" rather than "drawings". The timeline learned the two gestures that a generic lane needs. A clip body dragged over another lane's track previews there as a dashed block and lands through `::adopt-in-lane`; the track is found with `elementsFromPoint` and its selection read back off the element, because a pointer capture does not retarget. A pool drop over an existing lane previews as a dashed clip inside that lane instead of a temporary new row that appears and then vanishes -- which also needed the drag-leave check to be geometric, since inserting the preview changes the element under the pointer and Chromium then reports a leave with no related target. Lanes are renameable from their label, by double-click, F2, or the pencil, through `::rename-node`. `symbol/lane-cels` is `symbol/lane-clips`, and the vocabulary table in the handoff now separates the two words it had merged: a clip is an instance in a lane, and a cel is specifically the one-frame held source that drawing creation makes. Keeping `cel` for the policy is what lets the lane stop being about drawings at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-01 15:13:07 -04:00
- **Slip and retime are still absent.** The timeline action strip now applies
split/trim uniformly to a selected root or lane clip, but source-time slip and
retime still need their own proved semantics before they become controls.
- **`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/`:
A lane is a generic row of symbol clips A lane was a drawing lane: the only thing that could go in one was a one-frame held cel, and every other symbol instance stayed a permanent root row of its own. Those are not two kinds of timing, they are one kind with two creation policies. `lane/place-symbol` drops any library symbol in as a clip that plays naturally at speed one, `lane/adopt` moves an instance that is already in the document into a lane keeping its source, span, playback and corrections, and `append-drawing`/`overwrite-drawing` keep being the policy that makes a new empty symbol a one-frame hold. The child shape they produce is the same. Both new commands claim their interval through `blank` before they write, so the partition rule is unchanged and unduplicated: placing into occupied lane time trims, removes or splits the incumbents, and a lane still never stores an overlap. Real compositing overlap is another lane, where the order is explicit. Creating a symbol with nothing aimed now makes a lane and a clip in it instead of a loose root instance, and a pool drop prefers an explicitly targeted lane, then the selected one, and makes a lane only when there is neither. That is what stops the row-per-symbol growth coming back in through the drop path, and it is why `add-lane` now takes a z in front of the existing root nodes and calls what it makes a "lane" rather than "drawings". The timeline learned the two gestures that a generic lane needs. A clip body dragged over another lane's track previews there as a dashed block and lands through `::adopt-in-lane`; the track is found with `elementsFromPoint` and its selection read back off the element, because a pointer capture does not retarget. A pool drop over an existing lane previews as a dashed clip inside that lane instead of a temporary new row that appears and then vanishes -- which also needed the drag-leave check to be geometric, since inserting the preview changes the element under the pointer and Chromium then reports a leave with no related target. Lanes are renameable from their label, by double-click, F2, or the pencil, through `::rename-node`. `symbol/lane-cels` is `symbol/lane-clips`, and the vocabulary table in the handoff now separates the two words it had merged: a clip is an instance in a lane, and a cel is specifically the one-frame held source that drawing creation makes. Keeping `cel` for the policy is what lets the lane stop being about drawings at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-01 15:13:07 -04:00
npx shadow-cljs compile test && node out/node-tests.js # 469 tests, 9,592 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:
A lane is a generic row of symbol clips A lane was a drawing lane: the only thing that could go in one was a one-frame held cel, and every other symbol instance stayed a permanent root row of its own. Those are not two kinds of timing, they are one kind with two creation policies. `lane/place-symbol` drops any library symbol in as a clip that plays naturally at speed one, `lane/adopt` moves an instance that is already in the document into a lane keeping its source, span, playback and corrections, and `append-drawing`/`overwrite-drawing` keep being the policy that makes a new empty symbol a one-frame hold. The child shape they produce is the same. Both new commands claim their interval through `blank` before they write, so the partition rule is unchanged and unduplicated: placing into occupied lane time trims, removes or splits the incumbents, and a lane still never stores an overlap. Real compositing overlap is another lane, where the order is explicit. Creating a symbol with nothing aimed now makes a lane and a clip in it instead of a loose root instance, and a pool drop prefers an explicitly targeted lane, then the selected one, and makes a lane only when there is neither. That is what stops the row-per-symbol growth coming back in through the drop path, and it is why `add-lane` now takes a z in front of the existing root nodes and calls what it makes a "lane" rather than "drawings". The timeline learned the two gestures that a generic lane needs. A clip body dragged over another lane's track previews there as a dashed block and lands through `::adopt-in-lane`; the track is found with `elementsFromPoint` and its selection read back off the element, because a pointer capture does not retarget. A pool drop over an existing lane previews as a dashed clip inside that lane instead of a temporary new row that appears and then vanishes -- which also needed the drag-leave check to be geometric, since inserting the preview changes the element under the pointer and Chromium then reports a leave with no related target. Lanes are renameable from their label, by double-click, F2, or the pencil, through `::rename-node`. `symbol/lane-cels` is `symbol/lane-clips`, and the vocabulary table in the handoff now separates the two words it had merged: a clip is an instance in a lane, and a cel is specifically the one-frame held source that drawing creation makes. Keeping `cel` for the policy is what lets the lane stop being about drawings at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-01 15:13:07 -04:00
node --experimental-websocket test/browser/lane.mjs # generic symbol-lane 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.