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>
This commit is contained in:
Your Name 2026-10-01 15:13:07 -04:00
parent 26ada03591
commit 5ebe776ce4
12 changed files with 590 additions and 174 deletions

View file

@ -1,12 +1,14 @@
# Lane and cel handoff
# Lane and symbol-clip handoff
Status (2026-10-01): the timeline is the one timing interface. Cels are ordinary
nodes with their own playback clock and appear as blocks on one row per drawing
lane. Blocks move by mouse; edge drags trim without overlap; Shift-right-edge
drags ripple every later cel; 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. Rotation and position corrections survive
regeneration and expose conflicts for removal or retry.
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
@ -39,9 +41,10 @@ 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 |
| 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` |
| drawing | the content a cel names — an ordinary symbol |
| 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
@ -82,19 +85,29 @@ decision, not a cleanup.
- **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
- **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 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.
## Current timeline interaction
- Creating a symbol inside an aimed drawing lane creates a one-frame cel at the
playhead. Drawing a polygon uses the existing cel there or creates the same
one-frame cel when the frame is empty.
- Dragging a cel body moves it. A linked audio node follows a picture move;
- 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;
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
every later cel by the same delta.
every later clip by the same delta.
- 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
@ -156,9 +169,10 @@ The implemented correction slice and its remaining UI limits are recorded in
## 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.
- **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 —
@ -172,9 +186,9 @@ The implemented correction slice and its remaining UI limits are recorded in
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.
- **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
@ -186,14 +200,14 @@ The implemented correction slice and its remaining UI limits are recorded in
From `frontend/`:
npx shadow-cljs compile test && node out/node-tests.js # 437 tests, 5,804 assertions
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:
node --experimental-websocket test/browser/lane.mjs # the lane/cel flow
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

View file

@ -1,6 +1,6 @@
# The Lane Model
Revised 2026-10-01. Cel ownership, source playback, one-row drawing lanes,
Revised 2026-10-01. Clip ownership, source playback, one-row generic lanes,
direct clip movement and edge editing, correction evaluation, and correction
authoring for rotation and position are implemented. The former cel-sheet
projection was removed: the timeline is the single timing interface. Sections
@ -147,8 +147,9 @@ cel transform and then the content's own transform.
The interval in lane time is derived through `:time`; do not also store parent
start/end values. Sequence children require finite intervals and positive
placement rates. Ordering and overlap checks use the mapped intervals, not `:z`.
The sequence group may contain visual cels or audio cels; its
capability must reject an incompatible mixture rather than infer it per frame.
The current sequence group contains visual symbol clips. Audio remains an
independent root node (and can be linked to picture); if audio lanes are added,
their capability must be explicit rather than inferred per frame.
A source reference is fixed within a cel. The lane changes content when
another cel becomes active. This is a deliberate revision of the original