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
|
|
|
# A lane is a view
|
|
|
|
|
|
|
|
|
|
Plan, 2026-10-01, written at `2dc5735`. It undoes the lane model as a thing in
|
Non-overlap is an invariant, not a report
The plan asked what to draw when a symbol in lane mode holds overlapping
children, and offered to report it as a decision waiting for a person. Wrong
question: they never overlap. Placement claims time — anything placed, moved
or grown over occupied time trims, removes or splits what it lands on — so the
operation that could have made an overlap did not, and `span/finish`, which is
already the single commit path and already refuses rather than half-applying,
is where that is enforced. An overlap is then a bug in a command and not a
state to design around.
The check stays, named `symbol/overlaps` and used three ways: the commit path
refuses one, a property test asserts no command can produce one, and a
document that somehow holds one still LOADS and is drawn visibly wrong with
the status saying so. Not `problems`, which stops a document loading, and not
`conflicts`, which means somebody has a decision to make — a display hint must
never be able to keep a document from opening.
The one place a person can ask for the impossible is toggling lane mode on
over children that already overlap. That refuses and offers to trim them into
a sequence, through the `:required-frames` retry the model already uses.
Also written down, because it is the pair the modifier exists to separate:
a plain body drag is temporal and replaces, trimming extents as needed; shift
is structural and goes through `nest/move-node` into the symbol under the
pointer, which has to keep working for symbols held in a lane.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-01 16:22:33 -04:00
|
|
|
the document and keeps what it was for. Build on what is there and tear out
|
|
|
|
|
half of it.
|
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
|
|
|
|
|
|
|
|
## 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` |
|
Non-overlap is an invariant, not a report
The plan asked what to draw when a symbol in lane mode holds overlapping
children, and offered to report it as a decision waiting for a person. Wrong
question: they never overlap. Placement claims time — anything placed, moved
or grown over occupied time trims, removes or splits what it lands on — so the
operation that could have made an overlap did not, and `span/finish`, which is
already the single commit path and already refuses rather than half-applying,
is where that is enforced. An overlap is then a bug in a command and not a
state to design around.
The check stays, named `symbol/overlaps` and used three ways: the commit path
refuses one, a property test asserts no command can produce one, and a
document that somehow holds one still LOADS and is drawn visibly wrong with
the status saying so. Not `problems`, which stops a document loading, and not
`conflicts`, which means somebody has a decision to make — a display hint must
never be able to keep a document from opening.
The one place a person can ask for the impossible is toggling lane mode on
over children that already overlap. That refuses and offers to trim them into
a sequence, through the `:required-frames` retry the model already uses.
Also written down, because it is the pair the modifier exists to separate:
a plain body drag is temporal and replaces, trimming extents as needed; shift
is structural and goes through `nest/move-node` into the symbol under the
pointer, which has to keep working for symbols held in a lane.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-01 16:22:33 -04:00
|
|
|
| `symbol/lane-problems` | `symbol/overlaps`, a diagnostic the write path calls |
|
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
|
|
|
| `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.
|
|
|
|
|
|
Non-overlap is an invariant, not a report
The plan asked what to draw when a symbol in lane mode holds overlapping
children, and offered to report it as a decision waiting for a person. Wrong
question: they never overlap. Placement claims time — anything placed, moved
or grown over occupied time trims, removes or splits what it lands on — so the
operation that could have made an overlap did not, and `span/finish`, which is
already the single commit path and already refuses rather than half-applying,
is where that is enforced. An overlap is then a bug in a command and not a
state to design around.
The check stays, named `symbol/overlaps` and used three ways: the commit path
refuses one, a property test asserts no command can produce one, and a
document that somehow holds one still LOADS and is drawn visibly wrong with
the status saying so. Not `problems`, which stops a document loading, and not
`conflicts`, which means somebody has a decision to make — a display hint must
never be able to keep a document from opening.
The one place a person can ask for the impossible is toggling lane mode on
over children that already overlap. That refuses and offers to trim them into
a sequence, through the `:required-frames` retry the model already uses.
Also written down, because it is the pair the modifier exists to separate:
a plain body drag is temporal and replaces, trimming extents as needed; shift
is structural and goes through `nest/move-node` into the symbol under the
pointer, which has to keep working for symbols held in a lane.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-01 16:22:33 -04:00
|
|
|
## Children in lane mode never overlap
|
|
|
|
|
|
|
|
|
|
This is an invariant, not a condition to check for and report. Placement
|
|
|
|
|
claims time: anything placed, moved or grown over occupied time TRIMS the
|
|
|
|
|
extents it lands on — trimming the incumbent, removing one wholly covered, or
|
|
|
|
|
splitting one it lands inside — so the result has no overlap because the
|
|
|
|
|
operation that could have made one did not. That is `blank` followed by a
|
|
|
|
|
non-rippling placement, which is what `overwrite-drawing` already composes.
|
|
|
|
|
|
|
|
|
|
Enforced at the boundary, which already exists: `span/finish` is the single
|
|
|
|
|
commit path for every one of these commands, it validates before it returns,
|
|
|
|
|
and it refuses rather than half-applying. So `finish` gains the overlap check
|
|
|
|
|
for a symbol in lane mode, and no command can commit one. An overlap that
|
|
|
|
|
appears anyway is a bug in a command, not a state to design around.
|
|
|
|
|
|
|
|
|
|
Keep the check as a named diagnostic — `symbol/overlaps`, taking a symbol and
|
|
|
|
|
returning the pairs — used three ways:
|
|
|
|
|
|
|
|
|
|
1. `span/finish` refuses when it would commit one.
|
|
|
|
|
2. The test suite asserts no command can produce one: a property over the
|
|
|
|
|
commands in the style of `drawn` in `lane_test`, which samples rather than
|
|
|
|
|
computing expected numbers by hand.
|
|
|
|
|
3. A document that somehow arrives holding one still LOADS — a display hint
|
|
|
|
|
must never be able to stop a document loading — and the timeline draws it
|
|
|
|
|
visibly wrong with the status line saying so. Not `clip/problems`, which
|
|
|
|
|
means the document will not load, and not `clip/conflicts`, which means a
|
|
|
|
|
person has a decision to make. This is neither: it is a bug report.
|
|
|
|
|
|
|
|
|
|
Toggling lane mode ON for a symbol whose children already overlap is the one
|
|
|
|
|
place a person can ask for the impossible. Refuse it and say why, with the
|
|
|
|
|
`:required-frames` retry pattern offering to trim them into a sequence — the
|
|
|
|
|
domain reports what it would need, the UI offers one button.
|
|
|
|
|
|
|
|
|
|
Outside lane mode nothing is enforced, because overlapping children are what
|
|
|
|
|
compositing IS. An endpoint drag there is an ordinary span edit that may
|
|
|
|
|
overlap; the claim-time rule follows the mode.
|
|
|
|
|
|
|
|
|
|
## The two drag intentions
|
|
|
|
|
|
|
|
|
|
Unchanged from `docs/lane-nesting-notes.md`, and both kept:
|
|
|
|
|
|
|
|
|
|
- **Plain drag** of a clip body is temporal: it moves in time, within its
|
|
|
|
|
symbol or into another symbol drawn as a lane, and it REPLACES — trimming,
|
|
|
|
|
removing and splitting extents as needed so nothing overlaps.
|
|
|
|
|
- **Shift-drag** is structural: the dragged node goes INSIDE the symbol the
|
|
|
|
|
clip under the pointer places, through `nest/move-node`, which preserves the
|
|
|
|
|
world transform and the root timing. This must keep working for symbols
|
|
|
|
|
contained in a lane, which is the case it exists for.
|
|
|
|
|
|
|
|
|
|
Overlap cannot distinguish them — dropping on occupied time already means
|
|
|
|
|
claiming it — so the modifier says which, and the label by the pointer says it
|
|
|
|
|
back. `nest/move-refusal` already answers before the drop.
|
|
|
|
|
|
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
|
|
|
## 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
|
Non-overlap is an invariant, not a report
The plan asked what to draw when a symbol in lane mode holds overlapping
children, and offered to report it as a decision waiting for a person. Wrong
question: they never overlap. Placement claims time — anything placed, moved
or grown over occupied time trims, removes or splits what it lands on — so the
operation that could have made an overlap did not, and `span/finish`, which is
already the single commit path and already refuses rather than half-applying,
is where that is enforced. An overlap is then a bug in a command and not a
state to design around.
The check stays, named `symbol/overlaps` and used three ways: the commit path
refuses one, a property test asserts no command can produce one, and a
document that somehow holds one still LOADS and is drawn visibly wrong with
the status saying so. Not `problems`, which stops a document loading, and not
`conflicts`, which means somebody has a decision to make — a display hint must
never be able to keep a document from opening.
The one place a person can ask for the impossible is toggling lane mode on
over children that already overlap. That refuses and offers to trim them into
a sequence, through the `:required-frames` retry the model already uses.
Also written down, because it is the pair the modifier exists to separate:
a plain body drag is temporal and replaces, trimming extents as needed; shift
is structural and goes through `nest/move-node` into the symbol under the
pointer, which has to keep working for symbols held in a lane.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-01 16:22:33 -04:00
|
|
|
not currently overlap" means a symbol stops being a lane the moment anything
|
|
|
|
|
overlaps, and the rules that maintain non-overlap switch off exactly when they
|
|
|
|
|
are needed.
|
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
|
|
|
|
|
|
|
|
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
|
Non-overlap is an invariant, not a report
The plan asked what to draw when a symbol in lane mode holds overlapping
children, and offered to report it as a decision waiting for a person. Wrong
question: they never overlap. Placement claims time — anything placed, moved
or grown over occupied time trims, removes or splits what it lands on — so the
operation that could have made an overlap did not, and `span/finish`, which is
already the single commit path and already refuses rather than half-applying,
is where that is enforced. An overlap is then a bug in a command and not a
state to design around.
The check stays, named `symbol/overlaps` and used three ways: the commit path
refuses one, a property test asserts no command can produce one, and a
document that somehow holds one still LOADS and is drawn visibly wrong with
the status saying so. Not `problems`, which stops a document loading, and not
`conflicts`, which means somebody has a decision to make — a display hint must
never be able to keep a document from opening.
The one place a person can ask for the impossible is toggling lane mode on
over children that already overlap. That refuses and offers to trim them into
a sequence, through the `:required-frames` retry the model already uses.
Also written down, because it is the pair the modifier exists to separate:
a plain body drag is temporal and replaces, trimming extents as needed; shift
is structural and goes through `nest/move-node` into the symbol under the
pointer, which has to keep working for symbols held in a lane.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-01 16:22:33 -04:00
|
|
|
to hold the line on is that nothing outside the timeline, and the commit
|
|
|
|
|
path's overlap check, is allowed to read it.
|
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
|
|
|
|
2026-10-01 16:32:02 -04:00
|
|
|
## Two things are called loop
|
|
|
|
|
|
|
|
|
|
Before any of this, name them apart, in the way the vocabulary table in
|
|
|
|
|
`lane-handoff.md` names a cel apart from an exposure.
|
|
|
|
|
|
|
|
|
|
- **Loop playback** is the transport repeating the open symbol while it plays.
|
|
|
|
|
It is `[:playback :loop?]` in app-db, the ⟳ button in the strip, and it is
|
|
|
|
|
EDITOR STATE. The document does not know about it.
|
|
|
|
|
- **A looping instance** is a node repeating the symbol it places: a four-frame
|
|
|
|
|
tire turning for the hundred and twenty frames the instance is on screen.
|
|
|
|
|
It is `:playback {:end :loop}` on the node, and it is in the DOCUMENT.
|
|
|
|
|
|
|
|
|
|
The car tire is the second one, and here is its actual status: it already
|
|
|
|
|
works in the evaluator and cannot be asked for. `node/placed-frame` does the
|
|
|
|
|
modulo, `node/problems` already admits `:end` of `:stop`, `:hold` or `:loop`,
|
|
|
|
|
`nest/audio-tracks` already expands a loop into its periods — and nothing in
|
|
|
|
|
the UI sets it. It is implemented and unreachable.
|
|
|
|
|
|
|
|
|
|
So three things are missing, and they are the work:
|
|
|
|
|
|
|
|
|
|
1. **A control.** Where an instance's playback is edited: `:in`, `:speed`, and
|
|
|
|
|
what happens at the end. One place, three fields, rather than a loop
|
|
|
|
|
checkbox somewhere else.
|
|
|
|
|
2. **Drawing the repeats.** `ui/timeline`'s docstring already admits that only
|
|
|
|
|
the first pass of a looping instance is drawn, so a tire turning thirty
|
|
|
|
|
times shows one turn's keys and then nothing. A looping block should show
|
|
|
|
|
its passes — at minimum the period boundaries, so the row says how many
|
|
|
|
|
times round it goes.
|
|
|
|
|
3. **The audio period guard** below, which a loop needs whether or not holds
|
|
|
|
|
become loops.
|
|
|
|
|
|
A held cel is a one-frame symbol that loops
Holding a drawing and looping a one-frame symbol are the same picture, and the
second is a case the model already carries, so the first is a special case
kept for nothing. Speed 0 goes; a cel becomes a one-frame symbol with
`:end :loop` and a span. Looping and span are already properties of the
instance and `:frames` already belongs to the symbol, so nothing moves — a
mode is deleted. Two spellings of looping collapse to one, `:time :loop?`
giving way to `:playback :end :loop`, and `extend-hold` collapses into
`resize-out`, since it exists only to refuse anything that is not frozen
before editing a span.
What it buys is one rule where there were three refusals. `nest/inside` has no
invertible clock for a hold, for `:end :hold`, or for a loop, so
shift-to-reparent refuses all three — which is most of what anybody would drag
onto. Resolving the move with the destination's map at the CURRENT frame
covers every one: a loop is affine within the period the frame falls in, a
one-frame loop is that rule with a period of one — which lands exactly where
the nesting notes argued it should from first principles — and `:end :hold` is
affine in the played part and frozen in the tail.
The trap is written down beside it, because it would be found the hard way:
`audio-tracks` expands a loop into one walk per period, and is saved today
only by the `(pos? speed)` guard that a held cel fails. Make every drawing a
loop and a drawing held for 120 frames becomes 120 walks emitting any nested
sound 120 times. An audibility precheck has to land in the same change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-01 16:31:06 -04:00
|
|
|
## Tear out the held cel
|
|
|
|
|
|
|
|
|
|
A held cel is a 1-frame symbol shown for many frames. A 1-frame symbol with
|
|
|
|
|
`:end :loop`, lengthened, is the same picture by a different route — and the
|
|
|
|
|
second route is a case the model already has, so keeping the first one is
|
|
|
|
|
keeping a special case for free.
|
|
|
|
|
|
|
|
|
|
**What is already true**, so that nothing has to move: `:span` is on the node,
|
|
|
|
|
in its own frames; `:time` (`:at`, `:rate`) is on the node; looping is on the
|
2026-10-01 16:32:02 -04:00
|
|
|
node, as `:playback :end`. The SYMBOL owns only `:frames`, the authored
|
|
|
|
|
window. So looping and span are instance properties already, and this change
|
|
|
|
|
is about deleting a mode, not relocating a field.
|
|
|
|
|
|
|
|
|
|
It does mean the two changes are joined at one point: making every drawing a
|
|
|
|
|
looping instance is not safe until a looping instance can be seen and edited,
|
|
|
|
|
or every drawing in the document acquires a property with no control on it.
|
A held cel is a one-frame symbol that loops
Holding a drawing and looping a one-frame symbol are the same picture, and the
second is a case the model already carries, so the first is a special case
kept for nothing. Speed 0 goes; a cel becomes a one-frame symbol with
`:end :loop` and a span. Looping and span are already properties of the
instance and `:frames` already belongs to the symbol, so nothing moves — a
mode is deleted. Two spellings of looping collapse to one, `:time :loop?`
giving way to `:playback :end :loop`, and `extend-hold` collapses into
`resize-out`, since it exists only to refuse anything that is not frozen
before editing a span.
What it buys is one rule where there were three refusals. `nest/inside` has no
invertible clock for a hold, for `:end :hold`, or for a loop, so
shift-to-reparent refuses all three — which is most of what anybody would drag
onto. Resolving the move with the destination's map at the CURRENT frame
covers every one: a loop is affine within the period the frame falls in, a
one-frame loop is that rule with a period of one — which lands exactly where
the nesting notes argued it should from first principles — and `:end :hold` is
affine in the played part and frozen in the tail.
The trap is written down beside it, because it would be found the hard way:
`audio-tracks` expands a loop into one walk per period, and is saved today
only by the `(pos? speed)` guard that a held cel fails. Make every drawing a
loop and a drawing held for 120 frames becomes 120 walks emitting any nested
sound 120 times. An audibility precheck has to land in the same change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-01 16:31:06 -04:00
|
|
|
|
|
|
|
|
**What has to be decided and collapsed:**
|
|
|
|
|
|
|
|
|
|
- There are two spellings of looping — `:time :loop?` and `:playback :end
|
|
|
|
|
:loop` — and both are read, in `node/placed-frame` and in
|
|
|
|
|
`nest/audio-tracks`. Keep one. `:playback {:in :speed :end}` already says
|
|
|
|
|
what happens at the ends, so `:end :loop` is the one to keep and
|
|
|
|
|
`:time :loop?` is the one to delete.
|
|
|
|
|
- `:playback :speed 0` stops being produced. Make it illegal in
|
|
|
|
|
`node/problems` rather than legal-but-unused, so a frozen clock has exactly
|
|
|
|
|
one spelling: a 1-frame loop.
|
|
|
|
|
- `lane/extend-hold` exists only because holds were special — it refuses
|
|
|
|
|
anything whose speed is not 0 and then edits a span. Once a hold is a loop,
|
|
|
|
|
lengthening one IS `resize-out`, and the command collapses into it.
|
|
|
|
|
- `cel` survives as the word for the creation policy — a new empty symbol is
|
|
|
|
|
one frame — but stops naming a playback mode.
|
|
|
|
|
|
|
|
|
|
**What it buys, and this is the point:** one rule for nesting, which settles
|
|
|
|
|
the refusal that blocks shift-to-reparent today. `nest/inside` currently has
|
|
|
|
|
no `:time` for a hold, for `:end :hold`, or for a loop, and so refuses all
|
|
|
|
|
three. The general rule that covers all of them: **resolve the move with the
|
|
|
|
|
destination's map at the CURRENT frame — the affine piece the current frame
|
|
|
|
|
falls in.**
|
|
|
|
|
|
|
|
|
|
- A loop of length L is affine within the period the current frame is in.
|
|
|
|
|
Timing is preserved inside that period and repeats after it, which is what
|
|
|
|
|
looping means.
|
|
|
|
|
- A 1-frame loop — the ex-hold — has a period of length 1, so the map within
|
|
|
|
|
it is trivially invertible and lands the moved node on frame 0, aligned to
|
|
|
|
|
the current frame. That is exactly the answer `docs/lane-nesting-notes.md`
|
|
|
|
|
argues for from first principles, arrived at here as an instance of the
|
|
|
|
|
general rule instead of a special case.
|
|
|
|
|
- `:end :hold` is affine in the played part and frozen in the tail, which the
|
|
|
|
|
same sentence covers.
|
|
|
|
|
|
|
|
|
|
**The trap, which must be handled in the same change.** `nest/audio-tracks`
|
|
|
|
|
expands a loop into one walk PER PERIOD:
|
|
|
|
|
|
|
|
|
|
periods (range (floor (/ (to-local source lo) length))
|
|
|
|
|
(ceil (/ (to-local source hi) length)))
|
|
|
|
|
|
|
|
|
|
Today a held cel is skipped entirely — `(pos? speed)` is the guard, and the
|
|
|
|
|
comment says a visual freeze does not emit a sustained audio sample. Turn
|
|
|
|
|
every drawing into a 1-frame loop and that guard stops firing: a drawing held
|
|
|
|
|
for 120 frames becomes 120 recursive walks, and any sound inside it is emitted
|
|
|
|
|
120 times. That is both a wrong mix and a performance cliff on the most common
|
|
|
|
|
node in the document. Required with this change: a cheap `symbol/audible?`
|
|
|
|
|
precheck so a source with no audio anywhere inside it is never period-expanded,
|
|
|
|
|
and a cap or a different formulation for the ones that are.
|
|
|
|
|
|
|
|
|
|
Also worth knowing before the change: `ui/timeline`'s own docstring already
|
|
|
|
|
says a looping instance draws only its first pass. With every drawing a loop,
|
|
|
|
|
that sentence now describes every drawing — harmless, since one pass of a
|
|
|
|
|
1-frame symbol is the whole of it, but the docstring should stop sounding like
|
|
|
|
|
a limitation.
|
|
|
|
|
|
|
|
|
|
**Dropping into a 1-frame symbol.** The window is authored and crops what it
|
|
|
|
|
holds, so a 10-frame symbol dropped into a 1-frame drawing shows its frame 0
|
|
|
|
|
and nothing else. That is consistent — `:frames` is the shot length and
|
|
|
|
|
`:extent :grow-symbol` is the opt-in — but it is probably not what somebody
|
|
|
|
|
dragging means. Offer the growth through the `:required-frames` retry the
|
|
|
|
|
model already uses: the command reports what it would need, the UI offers one
|
|
|
|
|
button.
|
|
|
|
|
|
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
|
|
|
## 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.
|
Non-overlap is an invariant, not a report
The plan asked what to draw when a symbol in lane mode holds overlapping
children, and offered to report it as a decision waiting for a person. Wrong
question: they never overlap. Placement claims time — anything placed, moved
or grown over occupied time trims, removes or splits what it lands on — so the
operation that could have made an overlap did not, and `span/finish`, which is
already the single commit path and already refuses rather than half-applying,
is where that is enforced. An overlap is then a bug in a command and not a
state to design around.
The check stays, named `symbol/overlaps` and used three ways: the commit path
refuses one, a property test asserts no command can produce one, and a
document that somehow holds one still LOADS and is drawn visibly wrong with
the status saying so. Not `problems`, which stops a document loading, and not
`conflicts`, which means somebody has a decision to make — a display hint must
never be able to keep a document from opening.
The one place a person can ask for the impossible is toggling lane mode on
over children that already overlap. That refuses and offers to trim them into
a sequence, through the `:required-frames` retry the model already uses.
Also written down, because it is the pair the modifier exists to separate:
a plain body drag is temporal and replaces, trimming extents as needed; shift
is structural and goes through `nest/move-node` into the symbol under the
pointer, which has to keep working for symbols held in a lane.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-01 16:22:33 -04:00
|
|
|
2. **Lane mode as a hint.** Add the field and the toggle, 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. Both display paths now exist and nothing in
|
|
|
|
|
the domain has changed.
|
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
|
|
|
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
|
Non-overlap is an invariant, not a report
The plan asked what to draw when a symbol in lane mode holds overlapping
children, and offered to report it as a decision waiting for a person. Wrong
question: they never overlap. Placement claims time — anything placed, moved
or grown over occupied time trims, removes or splits what it lands on — so the
operation that could have made an overlap did not, and `span/finish`, which is
already the single commit path and already refuses rather than half-applying,
is where that is enforced. An overlap is then a bug in a command and not a
state to design around.
The check stays, named `symbol/overlaps` and used three ways: the commit path
refuses one, a property test asserts no command can produce one, and a
document that somehow holds one still LOADS and is drawn visibly wrong with
the status saying so. Not `problems`, which stops a document loading, and not
`conflicts`, which means somebody has a decision to make — a display hint must
never be able to keep a document from opening.
The one place a person can ask for the impossible is toggling lane mode on
over children that already overlap. That refuses and offers to trim them into
a sequence, through the `:required-frames` retry the model already uses.
Also written down, because it is the pair the modifier exists to separate:
a plain body drag is temporal and replaces, trimming extents as needed; shift
is structural and goes through `nest/move-node` into the symbol under the
pointer, which has to keep working for symbols held in a lane.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-01 16:22:33 -04:00
|
|
|
`lane-id` argument. `frontend/test/arthur/domain/lane_test.cljs` is the
|
|
|
|
|
proof: its fixtures should change and its assertions should not, and any
|
|
|
|
|
assertion that has to change is a behaviour change worth noticing.
|
|
|
|
|
4. **Move the invariant.** `symbol/lane-problems` becomes `symbol/overlaps`,
|
|
|
|
|
called by `span/finish` for a symbol in lane mode, plus the property test
|
|
|
|
|
that no command can produce an overlap.
|
|
|
|
|
5. **Delete the rest.** `node/lane?`, `symbol/lane-clips`, `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.
|
|
|
|
|
6. **Audio falls out.** An audio node is already a parent-less child of a
|
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
|
|
|
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.
|
Non-overlap is an invariant, not a report
The plan asked what to draw when a symbol in lane mode holds overlapping
children, and offered to report it as a decision waiting for a person. Wrong
question: they never overlap. Placement claims time — anything placed, moved
or grown over occupied time trims, removes or splits what it lands on — so the
operation that could have made an overlap did not, and `span/finish`, which is
already the single commit path and already refuses rather than half-applying,
is where that is enforced. An overlap is then a bug in a command and not a
state to design around.
The check stays, named `symbol/overlaps` and used three ways: the commit path
refuses one, a property test asserts no command can produce one, and a
document that somehow holds one still LOADS and is drawn visibly wrong with
the status saying so. Not `problems`, which stops a document loading, and not
`conflicts`, which means somebody has a decision to make — a display hint must
never be able to keep a document from opening.
The one place a person can ask for the impossible is toggling lane mode on
over children that already overlap. That refuses and offers to trim them into
a sequence, through the `:required-frames` retry the model already uses.
Also written down, because it is the pair the modifier exists to separate:
a plain body drag is temporal and replaces, trimming extents as needed; shift
is structural and goes through `nest/move-node` into the symbol under the
pointer, which has to keep working for symbols held in a lane.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-01 16:22:33 -04:00
|
|
|
7. **Shift-to-reparent stays** as it is: `nest/move-node` and
|
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
|
|
|
`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
|
|
|
|
|
|
Non-overlap is an invariant, not a report
The plan asked what to draw when a symbol in lane mode holds overlapping
children, and offered to report it as a decision waiting for a person. Wrong
question: they never overlap. Placement claims time — anything placed, moved
or grown over occupied time trims, removes or splits what it lands on — so the
operation that could have made an overlap did not, and `span/finish`, which is
already the single commit path and already refuses rather than half-applying,
is where that is enforced. An overlap is then a bug in a command and not a
state to design around.
The check stays, named `symbol/overlaps` and used three ways: the commit path
refuses one, a property test asserts no command can produce one, and a
document that somehow holds one still LOADS and is drawn visibly wrong with
the status saying so. Not `problems`, which stops a document loading, and not
`conflicts`, which means somebody has a decision to make — a display hint must
never be able to keep a document from opening.
The one place a person can ask for the impossible is toggling lane mode on
over children that already overlap. That refuses and offers to trim them into
a sequence, through the `:required-frames` retry the model already uses.
Also written down, because it is the pair the modifier exists to separate:
a plain body drag is temporal and replaces, trimming extents as needed; shift
is structural and goes through `nest/move-node` into the symbol under the
pointer, which has to keep working for symbols held in a lane.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-01 16:22:33 -04:00
|
|
|
1. **Is lane mode a property of the symbol or of the instance placing it?** A
|
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
|
|
|
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.
|
Non-overlap is an invariant, not a report
The plan asked what to draw when a symbol in lane mode holds overlapping
children, and offered to report it as a decision waiting for a person. Wrong
question: they never overlap. Placement claims time — anything placed, moved
or grown over occupied time trims, removes or splits what it lands on — so the
operation that could have made an overlap did not, and `span/finish`, which is
already the single commit path and already refuses rather than half-applying,
is where that is enforced. An overlap is then a bug in a command and not a
state to design around.
The check stays, named `symbol/overlaps` and used three ways: the commit path
refuses one, a property test asserts no command can produce one, and a
document that somehow holds one still LOADS and is drawn visibly wrong with
the status saying so. Not `problems`, which stops a document loading, and not
`conflicts`, which means somebody has a decision to make — a display hint must
never be able to keep a document from opening.
The one place a person can ask for the impossible is toggling lane mode on
over children that already overlap. That refuses and offers to trim them into
a sequence, through the `:required-frames` retry the model already uses.
Also written down, because it is the pair the modifier exists to separate:
a plain body drag is temporal and replaces, trimming extents as needed; shift
is structural and goes through `nest/move-node` into the symbol under the
pointer, which has to keep working for symbols held in a lane.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-01 16:22:33 -04:00
|
|
|
2. **Does a lane row's edge drag trim the placing instance's span, or ripple
|
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
|
|
|
the children?** Same handle, two commands; the row is now an instance, so
|
|
|
|
|
it has a span of its own for the first time.
|
A held cel is a one-frame symbol that loops
Holding a drawing and looping a one-frame symbol are the same picture, and the
second is a case the model already carries, so the first is a special case
kept for nothing. Speed 0 goes; a cel becomes a one-frame symbol with
`:end :loop` and a span. Looping and span are already properties of the
instance and `:frames` already belongs to the symbol, so nothing moves — a
mode is deleted. Two spellings of looping collapse to one, `:time :loop?`
giving way to `:playback :end :loop`, and `extend-hold` collapses into
`resize-out`, since it exists only to refuse anything that is not frozen
before editing a span.
What it buys is one rule where there were three refusals. `nest/inside` has no
invertible clock for a hold, for `:end :hold`, or for a loop, so
shift-to-reparent refuses all three — which is most of what anybody would drag
onto. Resolving the move with the destination's map at the CURRENT frame
covers every one: a loop is affine within the period the frame falls in, a
one-frame loop is that rule with a period of one — which lands exactly where
the nesting notes argued it should from first principles — and `:end :hold` is
affine in the played part and frozen in the tail.
The trap is written down beside it, because it would be found the hard way:
`audio-tracks` expands a loop into one walk per period, and is saved today
only by the `(pos? speed)` guard that a held cel fails. Make every drawing a
loop and a drawing held for 120 frames becomes 120 walks emitting any nested
sound 120 times. An audibility precheck has to land in the same change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-01 16:31:06 -04:00
|
|
|
3. **Does tearing out the held cel come before or after the lane work?** It
|
|
|
|
|
is independent of it — `nest` never knew about lanes — and it is what makes
|
|
|
|
|
shift-to-reparent work on the thing people would actually drag onto. Doing
|
|
|
|
|
it first means the lane work lands on a model with one playback mode fewer;
|
|
|
|
|
doing it after means two changes to `lane_test`'s fixtures instead of one.
|