arthur/docs/lane-is-a-view-plan.md
Your Name 10b96761fa Plan: a lane is a view, not a thing in the document
The lane model put a second container in the node map — a group with
`:layout :sequence`, with its own membership, its own validation and its own
fourteen commands — and every part of the editor then had to ask which kind of
container it was looking at. The decision recorded here is to delete all of
it: a lane becomes a way of DRAWING a symbol whose children are sequential and
non-overlapping, the display goes back to a row per symbol, and the word
survives only in the timeline and in the drag handling that re-spans a
symbol's children while it is drawn that way.

The commands are not the part being thrown away. `extend-hold`, `resize-out`,
`roll`, `blank` and the rest are what endpoint dragging IS, and their
arithmetic is right; what changes is their subject, from "the children of lane
L in symbol S" to "the children of symbol S". They belong in `span.cljs`,
which already owns re-spanning and `finish`.

The plan takes a position on the one question that decides whether this is a
simplification or a circle: lane mode is a saved hint on the symbol rather
than unsaved view state, because the drag rules follow the mode, and a toggle
the document does not record would make one gesture do two different things
to it. Nothing outside the timeline may read the hint.

It also lists what must not be lost on the way, all of which broke at least
once today: a held clip's contents reachable with no keys and no draggable
edges, double-click to open surviving the selection it leaves behind,
selection waiting for pointer-up, no drop silently deleting what it lands on,
and a sound drawn once.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-01 16:16:30 -04:00

7.1 KiB

A lane is a view

Plan, 2026-10-01, written at 2dc5735. It undoes the lane model as a thing in the document and keeps what it was for.

The decision

A lane is a view over a symbol with sequential, non-overlapping children.

Nothing in the document is a lane. There is no lane type, no lane group, no :layout :sequence, no lane commands and no lane validation. The word survives in exactly two places: the UI, where a symbol can be DRAWN as a lane, and the drag handling that re-spans a symbol's children while it is being drawn that way.

The display model goes back to a row per symbol. A symbol in lane mode draws its children as blocks on its own single row; expanded, they are rows like anything else. Everything else is an ordinary row that expands into what it places.

What a lane was, and what each part becomes

was becomes
a group node with :layout :sequence nothing — the symbol is the container
node/lane? a view question: is this symbol drawn in lane mode
symbol/lane-clips nodes lane-id the children of a symbol, sorted by node/placed-span
symbol/lane-problems gone. Nothing enforces non-overlap; the drag that claims time produces it
lane/lane-frame clip/source-time — one clock instead of two
domain/lane.cljs re-based onto domain/span.cljs: re-spanning a symbol's children
clip/lane-node, the born-with lane gone; a symbol is born empty again
::ui/new-lane, ::ui/adopt-in-lane, lane renaming gone, gone, and ordinary node renaming

lane.cljs's fourteen commands are not deleted — they are what "endpoint drag overlap handling" means, and they already do the right arithmetic. What changes is their subject: every one of them currently takes a host symbol AND a lane id and asks lane-clips nodes lane-id; each takes a symbol and asks for its children. extend-hold, resize-out, resize-in, roll, blank, place-symbol, adopt, append-drawing, reuse-drawing, duplicate-drawing, overwrite-drawing, make-unique. span/finish is already the one commit path and stays exactly as it is.

Put them in span.cljs, which already owns "one write to one node's span" and finish. The sequence operations are the same subject — re-spanning children — and keeping them apart was a consequence of lanes existing.

Where lane mode lives

A symbol is drawn as a lane because somebody said so, not because of what its children happen to look like at this moment. Deriving it from "the children do not currently overlap" means a symbol stops being a lane the moment a drag makes two children overlap, and the rules that were maintaining non-overlap switch off exactly when they are needed.

Two options:

  1. Editor state, [:ui :lane-mode #{sid}]. Purest reading of "a lane is a view". But the drag rules follow the mode, so an unsaved, per-person toggle would decide whether dropping a symbol trims its neighbour or composites over it — the same gesture doing two different things to the document depending on something the document does not record.
  2. A display hint on the symbol, e.g. :display :lane, saved like any other field (clip-keys, leaf/leaves, leaf/clip in the same commit). Still not a type: nothing in evaluation reads it, symbol/problems does not check it, and a symbol with it set behaves identically on the stage.

Recommended: 2. It is one field, it keeps editing rules reproducible between people, and it does not make the symbol a different kind of thing. The thing to hold the line on is that NOTHING outside the timeline and its drag handling is allowed to read it.

Order of work

Each step compiles, passes npm test, and leaves the editor usable.

  1. Row per symbol. In ui/timeline.cljs, delete portal, the ::portal hint row, the under? lineage predicate and the chosen argument; emit a row for a clip whose parent is a lane instead of skipping it. Keep the :cels blocks for the collapsed row, and keep inside-rows — including its :unmapped? branch, which is what makes a held drawing's contents reachable at all.
  2. Lane mode as a hint. Add the field, draw a symbol's children as blocks when it is set and as rows when it is not, and move the lane-row drag handling onto it. Now both display paths exist and nothing in the domain has changed yet.
  3. Re-base the commands. Move lane.cljs into span.cljs, replacing (lane-clips nodes lane-id) with the symbol's children and dropping the lane-id argument. The tests in frontend/test/arthur/domain/lane_test.cljs are the proof of this step: they should need their fixtures changed and their assertions kept, and any assertion that has to change is a behaviour change worth noticing.
  4. Delete the rest. node/lane?, symbol/lane-clips, symbol/lane-problems, clip/lane-node, ::ui/new-lane, ::ui/adopt-in-lane, the lane branch of ::ui/new-symbol, lane renaming, aimed-lane, and the :lane?/sound-lane? row flags. Rename what is left so the word does not appear outside the timeline.
  5. Audio falls out. An audio node is already a parent-less child of a symbol, which is exactly the new shape — so the audio-in-lane rules added in 2dc5735 (holds-other?, the mixed-lane refusal, the in-lane filter in sound-rows) delete rather than migrate. A symbol drawn as a lane whose children are sounds is an audio lane, and that is the whole of it.
  6. Shift-to-reparent stays as it is: nest/move-node and nest/move-refusal never knew about lanes.

What must not be lost

All of this was broken at some point today and is now proved; each has a test to keep.

  • A held clip's contents are reachable from the root timeline, with no keys and no draggable edges — source-time is nil for a hold, and the walk used to stop there.
  • Double-clicking a clip opens its symbol as a tab, and the editor survives it: symbol/lineage must not report a cycle for an id the symbol does not hold, and opening a symbol must drop a selection pointing into the one being left.
  • Selection waits for pointer-up, so a press does not re-draw the timeline out from under the gesture it is starting.
  • A drop never silently deletes what it lands on.
  • A sound is drawn once, not twice.

Open questions

  1. What happens to a symbol in lane mode whose children overlap anyway — through a reparent, a paste, or a document written before the mode existed? Drawing it as rows is honest and loses the mode silently; drawing overlapping blocks is a lie. Suggest: draw the blocks, and report it the way clip/conflicts reports a correction nobody has resolved — a decision waiting for a person, not a problem.
  2. Is lane mode a property of the symbol or of the instance placing it? A symbol placed twice would be drawn the same way in both places under the first reading. That is probably right, and worth saying out loud.
  3. Does a lane row's edge drag trim the placing instance's span, or ripple the children? Same handle, two commands; the row is now an instance, so it has a span of its own for the first time.