From 10b96761fac881e7f551bb0165cd9f086bbd9558 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 1 Oct 2026 16:16:30 -0400 Subject: [PATCH] Plan: a lane is a view, not a thing in the document MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- docs/lane-is-a-view-plan.md | 135 ++++++++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 docs/lane-is-a-view-plan.md diff --git a/docs/lane-is-a-view-plan.md b/docs/lane-is-a-view-plan.md new file mode 100644 index 0000000..5f2f899 --- /dev/null +++ b/docs/lane-is-a-view-plan.md @@ -0,0 +1,135 @@ +# 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.