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>
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:
- 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. - A display hint on the symbol, e.g.
:display :lane, saved like any other field (clip-keys,leaf/leaves,leaf/clipin the same commit). Still not a type: nothing in evaluation reads it,symbol/problemsdoes 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.
- Row per symbol. In
ui/timeline.cljs, deleteportal, the::portalhint row, theunder?lineage predicate and thechosenargument; emit a row for a clip whose parent is a lane instead of skipping it. Keep the:celsblocks for the collapsed row, and keepinside-rows— including its:unmapped?branch, which is what makes a held drawing's contents reachable at all. - 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.
- Re-base the commands. Move
lane.cljsintospan.cljs, replacing(lane-clips nodes lane-id)with the symbol's children and dropping thelane-idargument. The tests infrontend/test/arthur/domain/lane_test.cljsare 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. - 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. - 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, thein-lanefilter insound-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. - Shift-to-reparent stays as it is:
nest/move-nodeandnest/move-refusalnever 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-timeis 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/lineagemust 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
- 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/conflictsreports a correction nobody has resolved — a decision waiting for a person, not aproblem. - 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.
- 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.