arthur/docs/lane-model.md

581 lines
33 KiB
Markdown
Raw Normal View History

An occurrence is a node, with a clock of its own A lane's drawings were going to be one instance whose source was a KEYED channel: frame 0 says `:drawing-a`, frame 4 says `:drawing-b`, and the cels of a row are that channel's keys. Two things followed from it, and both were wrong. The first is that playback meant whichever shape the channel happened to have. A framed source played its symbol; a keyed source froze the selected frame. So `node/placed-at` read animation out of storage, and adding an ordinary key to a still turned it into an animation — the last-key bug, which was not a bug in the code so much as the rule working as written. But WHICH drawing is used and HOW time runs inside it are independent questions, and all four combinations are ordinary: hold one drawing, play one animation, cut between held drawings, cut between playing ones. So an occurrence names one symbol in `:source {:symbol ...}` and says how its source time advances in `:playback {:in :speed :end}` — `source = in + speed * f`, a hold being speed 0, with `:stop`, `:hold` or `:loop` at the end named rather than guessed. `node/placed-frame` samples it forwards, which works for holds too, and `node/source-time` is the separate, invertible edit map, nil where inversion is meaningless. The two were one function before, and a hold had to lie about one of them. The second is that a keyed source only looked necessary because an occurrence was assumed to need a ROW. It does not. A lane is a group with `:layout :sequence`, its occurrences are ordinary instances in the same flat node map, and `timeline/rows` draws them as cel blocks on the lane's own row: twelve exposures, one row, each cel still separately selectable and addressable. The vertical growth that justified the keyed source is a presentation question, and it is answered in the view. `arthur.domain.sequence` holds the first commands over that shape — add lane, append drawing, extend hold — each one history step, each refusing rather than half-applying. Extending a hold leaves the lane's keys at their authored times, because you are adjusting drawings underneath timed motion; a correction owned by an occurrence travels with it. Ownership does that work, so no key needs a flag saying what it follows. Ripple past the symbol's end is refused with the frame count it would need, and `:extent :grow-symbol` is the caller saying yes. `clip/blank` no longer carries `:subjects {} :features {} :groups {}`. Empty maps write no leaf, so a blank document could not survive its own round trip — `leaf/leaves` promises exactness and was the only honest side of that. Documents are schema 3. A version 2 document is not read; nothing here converts one. `docs/lane-model.md` is the design, and says which of its parts are built. 392 tests, 5,525 assertions, and `test/browser/sequence.mjs` drives the editor through create, hold, explicit overflow and undo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:20:23 -04:00
# The Lane Model
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
Revised 2026-09-30. Target design. Cel ownership, source playback, the
content and cel commands, placement anywhere in a lane, a one-row cel strip
A correction is a layer, and a layer's values are a channel `:over` was specified in animation-model.md, refused in two places, and produced by nothing: `check-unimplemented!` threw on read and `channel/problems` reported it. It reads now. This is the part of the model the rotoscoping half depends on — generate motion, correct it by hand, turn the knob, keep the correction — and it was the last thing in the design that had never been tried. The shape that made it small: A LAYER'S VALUES ARE A CHANNEL. {:id :nudge :support [88 98] :op :offset :values {:animated? true :interp :linear :keys {88 [2 0], 96 [0 0]}}} So the three commands the lane model asks for over a selected range — a constant adjustment, a ramp, a return motion — are one mechanism and not three: framed values say the same thing on every frame they cover, keyed values move, and neither needs a new way to say what a value is over time. A layer reads through `value-at` and `cursor` like any channel, which is also what stopped blending from becoming two implementations: `over-at` is shared, and the specification and the playback path differ only in how they READ a layer — recursively through `value-at`, or through a reading head of its own. One level deep; a layer's values may not carry layers, which the stack already orders. That was the risk worth spiking for. A cursor that drifts produces the wrong pose rather than an error, and a stack means several reading heads per channel where there was one. The agreement test that holds the cursor to the specification in forward, backward and random frame order now covers stacked channels too — including a layer whose head is asked for nothing across the long stretches outside its support and then asked again, which is where drift would hide. `:support` is half-open and explicit. Outside it the base evaluates exactly as it did before, which is the whole difference between a bounded correction and inserting boundary keys: the latter alters the neighbouring segments, and the lane model says so. A LAYER HAS NO TIME SPACE OF ITS OWN, and this is the design question the doc left open. Its support and its values' keys are in the frames the base channel's keys are in — the node's. A correction on a lane is therefore in lane frames and reaches across the drawings exposed beneath it; one on a single occurrence is in that occurrence's frames and travels with it when the exposure moves. Ownership had already answered it, so there is no field to disagree with, and both halves are under test at lane level. Two things cost nothing, which is worth recording. A channel is ONE LEAF, so a correction persists inside it with no codec change at all. And `node/problems` already reports every channel's problems, so a malformed layer surfaces at the document level and in the sequence commands' post-check without plumbing. What is still missing is a command that MAKES one, and with it the question of how a view offers a constant, a ramp and a return over a selected range. The evaluator no longer has an opinion about that, which was the point. `offset` adds component-wise and never writes into a dense value, which is a view onto the block itself; a shape mismatch throws rather than being dropped, since a correction that silently does not take is the failure this design exists to prevent. `replace` can supply a value over an absent base and `offset` cannot, as animation-model.md required. 408 tests, 5,655 assertions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 16:07:38 -04:00
and the correction-layer evaluator are implemented; the commands that produce a
correction, overwrite, the retiming commands and the remaining views are not.
See the status note under
Reuse, duplicate and make unique: deciding what is shared The model's whole claim is that content and its occurrences are different things, and until now nothing in the editor could tell them apart: you could make a drawing and time it, but not expose one drawing twice, and so never find out whether an edit arrives in two places. That is the first proof obligation in the lane model and it was the one the commands could not reach. Three commands, and the distinctions between them are the point: reuse another occurrence of the same drawing. A decision to share, made on purpose, because sharing discovered later — when an edit turns up somewhere you did not expect — is the bad version. duplicate a copy of the drawing, appended, for when what is on screen is the starting point for the next one. make unique this occurrence gets a private copy; the others keep sharing. The undo of reuse, and refused when nothing else uses the drawing: a copy nobody asked for is a second identical symbol in the library for no reason a person could see. Duplicate copies the CONTENT and not the exposure. Its new occurrence is a plain one-frame hold, not a copy of the source occurrence's transform or corrections, because those belong to that use of the drawing — carrying them over would make duplicating a drawing quietly duplicate the treatment of one exposure of it. A copy is SHALLOW by default and keeps its references to other symbols, so a head built out of reusable eyes still uses those eyes. `:deep? true` copies everything it places with new ids throughout. The lane model asks for both and says why: never promise decoupling while leaving the edited object shared, and only the deep copy can keep that promise. `bring/symbols` already did the reachability walk and the id remapping, so the deep copy is that function pointed at its own clip. `node/sources` was still being read as a SET at five call sites, each with a comment about a lane that cuts between several drawings — the keyed source that no longer exists. An occurrence names one symbol, so they now ask `node/source`, and `placed-frame` answers with `:symbol` rather than `:of`, which was the last echo of the retired field name. To let the commands use `clip/free-id` and the copy machinery, the lane's own validation moved from `domain/sequence` to `domain/symbol`, which is where it belonged anyway: a sequence is the one composition rule a node map carries, and it now sits beside the parent and stencil checks rather than in the namespace that happens to build lanes. That also breaks the cycle — sequence can require clip and bring, and nothing below it requires sequence. Preconditions still check only the LANE's shape: refusing an exposure edit over an unrelated defect elsewhere in the symbol would be this command answering for a part of the document it never touches. The cel strip gains reuse, duplicate and make unique, the last shown only where the selected exposure actually shares its drawing. Drawing on twos is also now under test: exposure length is the cadence, the lane's transform has its own clock, and it still moves on every frame — stepping it would be the cel cadence leaking into continuous motion. 397 tests, 5,561 assertions. `test/browser/sequence.mjs` drives the three new commands through the real editor and checks that three exposures are still one row. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:30:59 -04:00
[Proof obligations](#proof-obligations-and-implementation-order).
An occurrence is a node, with a clock of its own A lane's drawings were going to be one instance whose source was a KEYED channel: frame 0 says `:drawing-a`, frame 4 says `:drawing-b`, and the cels of a row are that channel's keys. Two things followed from it, and both were wrong. The first is that playback meant whichever shape the channel happened to have. A framed source played its symbol; a keyed source froze the selected frame. So `node/placed-at` read animation out of storage, and adding an ordinary key to a still turned it into an animation — the last-key bug, which was not a bug in the code so much as the rule working as written. But WHICH drawing is used and HOW time runs inside it are independent questions, and all four combinations are ordinary: hold one drawing, play one animation, cut between held drawings, cut between playing ones. So an occurrence names one symbol in `:source {:symbol ...}` and says how its source time advances in `:playback {:in :speed :end}` — `source = in + speed * f`, a hold being speed 0, with `:stop`, `:hold` or `:loop` at the end named rather than guessed. `node/placed-frame` samples it forwards, which works for holds too, and `node/source-time` is the separate, invertible edit map, nil where inversion is meaningless. The two were one function before, and a hold had to lie about one of them. The second is that a keyed source only looked necessary because an occurrence was assumed to need a ROW. It does not. A lane is a group with `:layout :sequence`, its occurrences are ordinary instances in the same flat node map, and `timeline/rows` draws them as cel blocks on the lane's own row: twelve exposures, one row, each cel still separately selectable and addressable. The vertical growth that justified the keyed source is a presentation question, and it is answered in the view. `arthur.domain.sequence` holds the first commands over that shape — add lane, append drawing, extend hold — each one history step, each refusing rather than half-applying. Extending a hold leaves the lane's keys at their authored times, because you are adjusting drawings underneath timed motion; a correction owned by an occurrence travels with it. Ownership does that work, so no key needs a flag saying what it follows. Ripple past the symbol's end is refused with the frame count it would need, and `:extent :grow-symbol` is the caller saying yes. `clip/blank` no longer carries `:subjects {} :features {} :groups {}`. Empty maps write no leaf, so a blank document could not survive its own round trip — `leaf/leaves` promises exactness and was the only honest side of that. Documents are schema 3. A version 2 document is not read; nothing here converts one. `docs/lane-model.md` is the design, and says which of its parts are built. 392 tests, 5,525 assertions, and `test/browser/sequence.mjs` drives the editor through create, hold, explicit overflow and undo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:20:23 -04:00
[Lane and cel handoff](lane-handoff.md) records what is built, the decisions
that are settled, and what to do next.
An occurrence is a node, with a clock of its own A lane's drawings were going to be one instance whose source was a KEYED channel: frame 0 says `:drawing-a`, frame 4 says `:drawing-b`, and the cels of a row are that channel's keys. Two things followed from it, and both were wrong. The first is that playback meant whichever shape the channel happened to have. A framed source played its symbol; a keyed source froze the selected frame. So `node/placed-at` read animation out of storage, and adding an ordinary key to a still turned it into an animation — the last-key bug, which was not a bug in the code so much as the rule working as written. But WHICH drawing is used and HOW time runs inside it are independent questions, and all four combinations are ordinary: hold one drawing, play one animation, cut between held drawings, cut between playing ones. So an occurrence names one symbol in `:source {:symbol ...}` and says how its source time advances in `:playback {:in :speed :end}` — `source = in + speed * f`, a hold being speed 0, with `:stop`, `:hold` or `:loop` at the end named rather than guessed. `node/placed-frame` samples it forwards, which works for holds too, and `node/source-time` is the separate, invertible edit map, nil where inversion is meaningless. The two were one function before, and a hold had to lie about one of them. The second is that a keyed source only looked necessary because an occurrence was assumed to need a ROW. It does not. A lane is a group with `:layout :sequence`, its occurrences are ordinary instances in the same flat node map, and `timeline/rows` draws them as cel blocks on the lane's own row: twelve exposures, one row, each cel still separately selectable and addressable. The vertical growth that justified the keyed source is a presentation question, and it is answered in the view. `arthur.domain.sequence` holds the first commands over that shape — add lane, append drawing, extend hold — each one history step, each refusing rather than half-applying. Extending a hold leaves the lane's keys at their authored times, because you are adjusting drawings underneath timed motion; a correction owned by an occurrence travels with it. Ownership does that work, so no key needs a flag saying what it follows. Ripple past the symbol's end is refused with the frame count it would need, and `:extent :grow-symbol` is the caller saying yes. `clip/blank` no longer carries `:subjects {} :features {} :groups {}`. Empty maps write no leaf, so a blank document could not survive its own round trip — `leaf/leaves` promises exactness and was the only honest side of that. Documents are schema 3. A version 2 document is not read; nothing here converts one. `docs/lane-model.md` is the design, and says which of its parts are built. 392 tests, 5,525 assertions, and `test/browser/sequence.mjs` drives the editor through create, hold, explicit overflow and undo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:20:23 -04:00
This revises the Claude artifact [The Lane Model](https://claude.ai/code/artifact/cd42981d-ed08-493f-94df-b7dd6657f0e6).
Its prose and diagram source were recovered from session
`1c603f71-84eb-498e-aeaf-4c0346f1f513`; the live artifact was not accessible for
reading or editing here. This repository document is the revised design. The
original artifact has not been updated, and edits made there outside the recorded
session may not be represented here.
For the subjects covered here, this document supersedes the original artifact
and conflicting proposals in `animation-model.md`, `timing-model.md`, and
`architecture.md`. Those documents retain useful detail about the existing system.
## Goal and compatibility policy
Arthur is one animation document with several ways to see and edit it: drawing
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
on the stage, arranging clips, timing cels, editing curves, and generating
An occurrence is a node, with a clock of its own A lane's drawings were going to be one instance whose source was a KEYED channel: frame 0 says `:drawing-a`, frame 4 says `:drawing-b`, and the cels of a row are that channel's keys. Two things followed from it, and both were wrong. The first is that playback meant whichever shape the channel happened to have. A framed source played its symbol; a keyed source froze the selected frame. So `node/placed-at` read animation out of storage, and adding an ordinary key to a still turned it into an animation — the last-key bug, which was not a bug in the code so much as the rule working as written. But WHICH drawing is used and HOW time runs inside it are independent questions, and all four combinations are ordinary: hold one drawing, play one animation, cut between held drawings, cut between playing ones. So an occurrence names one symbol in `:source {:symbol ...}` and says how its source time advances in `:playback {:in :speed :end}` — `source = in + speed * f`, a hold being speed 0, with `:stop`, `:hold` or `:loop` at the end named rather than guessed. `node/placed-frame` samples it forwards, which works for holds too, and `node/source-time` is the separate, invertible edit map, nil where inversion is meaningless. The two were one function before, and a hold had to lie about one of them. The second is that a keyed source only looked necessary because an occurrence was assumed to need a ROW. It does not. A lane is a group with `:layout :sequence`, its occurrences are ordinary instances in the same flat node map, and `timeline/rows` draws them as cel blocks on the lane's own row: twelve exposures, one row, each cel still separately selectable and addressable. The vertical growth that justified the keyed source is a presentation question, and it is answered in the view. `arthur.domain.sequence` holds the first commands over that shape — add lane, append drawing, extend hold — each one history step, each refusing rather than half-applying. Extending a hold leaves the lane's keys at their authored times, because you are adjusting drawings underneath timed motion; a correction owned by an occurrence travels with it. Ownership does that work, so no key needs a flag saying what it follows. Ripple past the symbol's end is refused with the frame count it would need, and `:extent :grow-symbol` is the caller saying yes. `clip/blank` no longer carries `:subjects {} :features {} :groups {}`. Empty maps write no leaf, so a blank document could not survive its own round trip — `leaf/leaves` promises exactness and was the only honest side of that. Documents are schema 3. A version 2 document is not read; nothing here converts one. `docs/lane-model.md` is the design, and says which of its parts are built. 392 tests, 5,525 assertions, and `test/browser/sequence.mjs` drives the editor through create, hold, explicit overflow and undo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:20:23 -04:00
motion from footage. Each view exposes relevant facts and invokes shared editing
operations. Switching views must preserve the meaning of the work.
The user explicitly requires no backward compatibility. Replace obsolete shapes,
APIs, and tests when a better model requires it. Do not retain compatibility
branches, adapters, or migrations solely to preserve the current document format.
A format marker can reject unsupported files clearly; it does not promise to
convert them. This policy does not authorize deleting existing user assets.
Simplicity means predictable composition, clear ownership, and few independent
rules. Minimizing field count is secondary to representing independent choices.
## What stays
- A symbol is the one container for authored scene nodes. A drawing can be a
one-frame symbol; an animation uses the same container over more frames.
- Nodes have stable identities and flat parent references. Shared content is
referenced rather than copied implicitly.
- Animatable properties are addressed by channel paths. Generated and authored
values participate in the same evaluation machinery.
- Authored data, generated blocks, and source media remain separate. Documents
reference immutable blocks; caches and resolver indexes remain derived.
- A pure reference evaluator specifies the result. Playback, seeking, preview,
export, and optimized cursors must agree with it.
- Existence, visibility, and missing measured data remain distinct facts.
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
## Content, cels, lanes, and rows
An occurrence is a node, with a clock of its own A lane's drawings were going to be one instance whose source was a KEYED channel: frame 0 says `:drawing-a`, frame 4 says `:drawing-b`, and the cels of a row are that channel's keys. Two things followed from it, and both were wrong. The first is that playback meant whichever shape the channel happened to have. A framed source played its symbol; a keyed source froze the selected frame. So `node/placed-at` read animation out of storage, and adding an ordinary key to a still turned it into an animation — the last-key bug, which was not a bug in the code so much as the rule working as written. But WHICH drawing is used and HOW time runs inside it are independent questions, and all four combinations are ordinary: hold one drawing, play one animation, cut between held drawings, cut between playing ones. So an occurrence names one symbol in `:source {:symbol ...}` and says how its source time advances in `:playback {:in :speed :end}` — `source = in + speed * f`, a hold being speed 0, with `:stop`, `:hold` or `:loop` at the end named rather than guessed. `node/placed-frame` samples it forwards, which works for holds too, and `node/source-time` is the separate, invertible edit map, nil where inversion is meaningless. The two were one function before, and a hold had to lie about one of them. The second is that a keyed source only looked necessary because an occurrence was assumed to need a ROW. It does not. A lane is a group with `:layout :sequence`, its occurrences are ordinary instances in the same flat node map, and `timeline/rows` draws them as cel blocks on the lane's own row: twelve exposures, one row, each cel still separately selectable and addressable. The vertical growth that justified the keyed source is a presentation question, and it is answered in the view. `arthur.domain.sequence` holds the first commands over that shape — add lane, append drawing, extend hold — each one history step, each refusing rather than half-applying. Extending a hold leaves the lane's keys at their authored times, because you are adjusting drawings underneath timed motion; a correction owned by an occurrence travels with it. Ownership does that work, so no key needs a flag saying what it follows. Ripple past the symbol's end is refused with the frame count it would need, and `:extent :grow-symbol` is the caller saying yes. `clip/blank` no longer carries `:subjects {} :features {} :groups {}`. Empty maps write no leaf, so a blank document could not survive its own round trip — `leaf/leaves` promises exactness and was the only honest side of that. Documents are schema 3. A version 2 document is not read; nothing here converts one. `docs/lane-model.md` is the design, and says which of its parts are built. 392 tests, 5,525 assertions, and `test/browser/sequence.mjs` drives the editor through create, hold, explicit overflow and undo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:20:23 -04:00
These have different identities and responsibilities:
| Concept | Owns | Example |
| --- | --- | --- |
| Content | Reusable nodes and their animation | Drawing `a2`, an animated head, or a sound asset |
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
| Cel | One use of content, its interval, source playback, and local treatment | `a2` exposed on frames 12–16 |
| Lane | A sequence of cels and shared properties | The girl's drawings and the girl's overall transform |
| View row or column | Presentation and editor state | Timeline row, cel-sheet column, or property curve |
An occurrence is a node, with a clock of its own A lane's drawings were going to be one instance whose source was a KEYED channel: frame 0 says `:drawing-a`, frame 4 says `:drawing-b`, and the cels of a row are that channel's keys. Two things followed from it, and both were wrong. The first is that playback meant whichever shape the channel happened to have. A framed source played its symbol; a keyed source froze the selected frame. So `node/placed-at` read animation out of storage, and adding an ordinary key to a still turned it into an animation — the last-key bug, which was not a bug in the code so much as the rule working as written. But WHICH drawing is used and HOW time runs inside it are independent questions, and all four combinations are ordinary: hold one drawing, play one animation, cut between held drawings, cut between playing ones. So an occurrence names one symbol in `:source {:symbol ...}` and says how its source time advances in `:playback {:in :speed :end}` — `source = in + speed * f`, a hold being speed 0, with `:stop`, `:hold` or `:loop` at the end named rather than guessed. `node/placed-frame` samples it forwards, which works for holds too, and `node/source-time` is the separate, invertible edit map, nil where inversion is meaningless. The two were one function before, and a hold had to lie about one of them. The second is that a keyed source only looked necessary because an occurrence was assumed to need a ROW. It does not. A lane is a group with `:layout :sequence`, its occurrences are ordinary instances in the same flat node map, and `timeline/rows` draws them as cel blocks on the lane's own row: twelve exposures, one row, each cel still separately selectable and addressable. The vertical growth that justified the keyed source is a presentation question, and it is answered in the view. `arthur.domain.sequence` holds the first commands over that shape — add lane, append drawing, extend hold — each one history step, each refusing rather than half-applying. Extending a hold leaves the lane's keys at their authored times, because you are adjusting drawings underneath timed motion; a correction owned by an occurrence travels with it. Ownership does that work, so no key needs a flag saying what it follows. Ripple past the symbol's end is refused with the frame count it would need, and `:extent :grow-symbol` is the caller saying yes. `clip/blank` no longer carries `:subjects {} :features {} :groups {}`. Empty maps write no leaf, so a blank document could not survive its own round trip — `leaf/leaves` promises exactness and was the only honest side of that. Documents are schema 3. A version 2 document is not read; nothing here converts one. `docs/lane-model.md` is the design, and says which of its parts are built. 392 tests, 5,525 assertions, and `test/browser/sequence.mjs` drives the editor through create, hold, explicit overflow and undo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:20:23 -04:00
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
Use the existing instance/node identity mechanism for cels. A cel
An occurrence is a node, with a clock of its own A lane's drawings were going to be one instance whose source was a KEYED channel: frame 0 says `:drawing-a`, frame 4 says `:drawing-b`, and the cels of a row are that channel's keys. Two things followed from it, and both were wrong. The first is that playback meant whichever shape the channel happened to have. A framed source played its symbol; a keyed source froze the selected frame. So `node/placed-at` read animation out of storage, and adding an ordinary key to a still turned it into an animation — the last-key bug, which was not a bug in the code so much as the rule working as written. But WHICH drawing is used and HOW time runs inside it are independent questions, and all four combinations are ordinary: hold one drawing, play one animation, cut between held drawings, cut between playing ones. So an occurrence names one symbol in `:source {:symbol ...}` and says how its source time advances in `:playback {:in :speed :end}` — `source = in + speed * f`, a hold being speed 0, with `:stop`, `:hold` or `:loop` at the end named rather than guessed. `node/placed-frame` samples it forwards, which works for holds too, and `node/source-time` is the separate, invertible edit map, nil where inversion is meaningless. The two were one function before, and a hold had to lie about one of them. The second is that a keyed source only looked necessary because an occurrence was assumed to need a ROW. It does not. A lane is a group with `:layout :sequence`, its occurrences are ordinary instances in the same flat node map, and `timeline/rows` draws them as cel blocks on the lane's own row: twelve exposures, one row, each cel still separately selectable and addressable. The vertical growth that justified the keyed source is a presentation question, and it is answered in the view. `arthur.domain.sequence` holds the first commands over that shape — add lane, append drawing, extend hold — each one history step, each refusing rather than half-applying. Extending a hold leaves the lane's keys at their authored times, because you are adjusting drawings underneath timed motion; a correction owned by an occurrence travels with it. Ownership does that work, so no key needs a flag saying what it follows. Ripple past the symbol's end is refused with the frame count it would need, and `:extent :grow-symbol` is the caller saying yes. `clip/blank` no longer carries `:subjects {} :features {} :groups {}`. Empty maps write no leaf, so a blank document could not survive its own round trip — `leaf/leaves` promises exactness and was the only honest side of that. Documents are schema 3. A version 2 document is not read; nothing here converts one. `docs/lane-model.md` is the design, and says which of its parts are built. 392 tests, 5,525 assertions, and `test/browser/sequence.mjs` drives the editor through create, hold, explicit overflow and undo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:20:23 -04:00
should not acquire a second identity system just because it is shown as a cel.
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
Lanes group cels; they do not introduce another node-holding content type.
An occurrence is a node, with a clock of its own A lane's drawings were going to be one instance whose source was a KEYED channel: frame 0 says `:drawing-a`, frame 4 says `:drawing-b`, and the cels of a row are that channel's keys. Two things followed from it, and both were wrong. The first is that playback meant whichever shape the channel happened to have. A framed source played its symbol; a keyed source froze the selected frame. So `node/placed-at` read animation out of storage, and adding an ordinary key to a still turned it into an animation — the last-key bug, which was not a bug in the code so much as the rule working as written. But WHICH drawing is used and HOW time runs inside it are independent questions, and all four combinations are ordinary: hold one drawing, play one animation, cut between held drawings, cut between playing ones. So an occurrence names one symbol in `:source {:symbol ...}` and says how its source time advances in `:playback {:in :speed :end}` — `source = in + speed * f`, a hold being speed 0, with `:stop`, `:hold` or `:loop` at the end named rather than guessed. `node/placed-frame` samples it forwards, which works for holds too, and `node/source-time` is the separate, invertible edit map, nil where inversion is meaningless. The two were one function before, and a hold had to lie about one of them. The second is that a keyed source only looked necessary because an occurrence was assumed to need a ROW. It does not. A lane is a group with `:layout :sequence`, its occurrences are ordinary instances in the same flat node map, and `timeline/rows` draws them as cel blocks on the lane's own row: twelve exposures, one row, each cel still separately selectable and addressable. The vertical growth that justified the keyed source is a presentation question, and it is answered in the view. `arthur.domain.sequence` holds the first commands over that shape — add lane, append drawing, extend hold — each one history step, each refusing rather than half-applying. Extending a hold leaves the lane's keys at their authored times, because you are adjusting drawings underneath timed motion; a correction owned by an occurrence travels with it. Ownership does that work, so no key needs a flag saying what it follows. Ripple past the symbol's end is refused with the frame count it would need, and `:extent :grow-symbol` is the caller saying yes. `clip/blank` no longer carries `:subjects {} :features {} :groups {}`. Empty maps write no leaf, so a blank document could not survive its own round trip — `leaf/leaves` promises exactness and was the only honest side of that. Documents are schema 3. A version 2 document is not read; nothing here converts one. `docs/lane-model.md` is the design, and says which of its parts are built. 392 tests, 5,525 assertions, and `test/browser/sequence.mjs` drives the editor through create, hold, explicit overflow and undo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:20:23 -04:00
The concrete candidate below uses existing group and instance nodes; its ownership
boundaries are part of the design. It is now the implemented shape, and the field
spellings below are the ones the runtime reads.
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
Each cel has a stable ID. Moving it, changing its hold, swapping its source,
or trimming it preserves that ID. Repeating it creates a new cel that may
An occurrence is a node, with a clock of its own A lane's drawings were going to be one instance whose source was a KEYED channel: frame 0 says `:drawing-a`, frame 4 says `:drawing-b`, and the cels of a row are that channel's keys. Two things followed from it, and both were wrong. The first is that playback meant whichever shape the channel happened to have. A framed source played its symbol; a keyed source froze the selected frame. So `node/placed-at` read animation out of storage, and adding an ordinary key to a still turned it into an animation — the last-key bug, which was not a bug in the code so much as the rule working as written. But WHICH drawing is used and HOW time runs inside it are independent questions, and all four combinations are ordinary: hold one drawing, play one animation, cut between held drawings, cut between playing ones. So an occurrence names one symbol in `:source {:symbol ...}` and says how its source time advances in `:playback {:in :speed :end}` — `source = in + speed * f`, a hold being speed 0, with `:stop`, `:hold` or `:loop` at the end named rather than guessed. `node/placed-frame` samples it forwards, which works for holds too, and `node/source-time` is the separate, invertible edit map, nil where inversion is meaningless. The two were one function before, and a hold had to lie about one of them. The second is that a keyed source only looked necessary because an occurrence was assumed to need a ROW. It does not. A lane is a group with `:layout :sequence`, its occurrences are ordinary instances in the same flat node map, and `timeline/rows` draws them as cel blocks on the lane's own row: twelve exposures, one row, each cel still separately selectable and addressable. The vertical growth that justified the keyed source is a presentation question, and it is answered in the view. `arthur.domain.sequence` holds the first commands over that shape — add lane, append drawing, extend hold — each one history step, each refusing rather than half-applying. Extending a hold leaves the lane's keys at their authored times, because you are adjusting drawings underneath timed motion; a correction owned by an occurrence travels with it. Ownership does that work, so no key needs a flag saying what it follows. Ripple past the symbol's end is refused with the frame count it would need, and `:extent :grow-symbol` is the caller saying yes. `clip/blank` no longer carries `:subjects {} :features {} :groups {}`. Empty maps write no leaf, so a blank document could not survive its own round trip — `leaf/leaves` promises exactness and was the only honest side of that. Documents are schema 3. A version 2 document is not read; nothing here converts one. `docs/lane-model.md` is the design, and says which of its parts are built. 392 tests, 5,525 assertions, and `test/browser/sequence.mjs` drives the editor through create, hold, explicit overflow and undo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:20:23 -04:00
reference the same content. A split retains the original ID on the left and gives
the right piece a new ID; commands return the resulting selection explicitly.
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
Cels are the canonical authored arrangement. A source-at-time channel or
An occurrence is a node, with a clock of its own A lane's drawings were going to be one instance whose source was a KEYED channel: frame 0 says `:drawing-a`, frame 4 says `:drawing-b`, and the cels of a row are that channel's keys. Two things followed from it, and both were wrong. The first is that playback meant whichever shape the channel happened to have. A framed source played its symbol; a keyed source froze the selected frame. So `node/placed-at` read animation out of storage, and adding an ordinary key to a still turned it into an animation — the last-key bug, which was not a bug in the code so much as the rule working as written. But WHICH drawing is used and HOW time runs inside it are independent questions, and all four combinations are ordinary: hold one drawing, play one animation, cut between held drawings, cut between playing ones. So an occurrence names one symbol in `:source {:symbol ...}` and says how its source time advances in `:playback {:in :speed :end}` — `source = in + speed * f`, a hold being speed 0, with `:stop`, `:hold` or `:loop` at the end named rather than guessed. `node/placed-frame` samples it forwards, which works for holds too, and `node/source-time` is the separate, invertible edit map, nil where inversion is meaningless. The two were one function before, and a hold had to lie about one of them. The second is that a keyed source only looked necessary because an occurrence was assumed to need a ROW. It does not. A lane is a group with `:layout :sequence`, its occurrences are ordinary instances in the same flat node map, and `timeline/rows` draws them as cel blocks on the lane's own row: twelve exposures, one row, each cel still separately selectable and addressable. The vertical growth that justified the keyed source is a presentation question, and it is answered in the view. `arthur.domain.sequence` holds the first commands over that shape — add lane, append drawing, extend hold — each one history step, each refusing rather than half-applying. Extending a hold leaves the lane's keys at their authored times, because you are adjusting drawings underneath timed motion; a correction owned by an occurrence travels with it. Ownership does that work, so no key needs a flag saying what it follows. Ripple past the symbol's end is refused with the frame count it would need, and `:extent :grow-symbol` is the caller saying yes. `clip/blank` no longer carries `:subjects {} :features {} :groups {}`. Empty maps write no leaf, so a blank document could not survive its own round trip — `leaf/leaves` promises exactness and was the only honest side of that. Documents are schema 3. A version 2 document is not read; nothing here converts one. `docs/lane-model.md` is the design, and says which of its parts are built. 392 tests, 5,525 assertions, and `test/browser/sequence.mjs` drives the editor through create, hold, explicit overflow and undo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:20:23 -04:00
interval index may be compiled from them for evaluation, but is not a second
editable copy of the schedule. This replaces the earlier proposal that every cel
must be represented solely as a source key. Ordinary property animation still
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
uses lightweight keys; it does not need cel objects.
An occurrence is a node, with a clock of its own A lane's drawings were going to be one instance whose source was a KEYED channel: frame 0 says `:drawing-a`, frame 4 says `:drawing-b`, and the cels of a row are that channel's keys. Two things followed from it, and both were wrong. The first is that playback meant whichever shape the channel happened to have. A framed source played its symbol; a keyed source froze the selected frame. So `node/placed-at` read animation out of storage, and adding an ordinary key to a still turned it into an animation — the last-key bug, which was not a bug in the code so much as the rule working as written. But WHICH drawing is used and HOW time runs inside it are independent questions, and all four combinations are ordinary: hold one drawing, play one animation, cut between held drawings, cut between playing ones. So an occurrence names one symbol in `:source {:symbol ...}` and says how its source time advances in `:playback {:in :speed :end}` — `source = in + speed * f`, a hold being speed 0, with `:stop`, `:hold` or `:loop` at the end named rather than guessed. `node/placed-frame` samples it forwards, which works for holds too, and `node/source-time` is the separate, invertible edit map, nil where inversion is meaningless. The two were one function before, and a hold had to lie about one of them. The second is that a keyed source only looked necessary because an occurrence was assumed to need a ROW. It does not. A lane is a group with `:layout :sequence`, its occurrences are ordinary instances in the same flat node map, and `timeline/rows` draws them as cel blocks on the lane's own row: twelve exposures, one row, each cel still separately selectable and addressable. The vertical growth that justified the keyed source is a presentation question, and it is answered in the view. `arthur.domain.sequence` holds the first commands over that shape — add lane, append drawing, extend hold — each one history step, each refusing rather than half-applying. Extending a hold leaves the lane's keys at their authored times, because you are adjusting drawings underneath timed motion; a correction owned by an occurrence travels with it. Ownership does that work, so no key needs a flag saying what it follows. Ripple past the symbol's end is refused with the frame count it would need, and `:extent :grow-symbol` is the caller saying yes. `clip/blank` no longer carries `:subjects {} :features {} :groups {}`. Empty maps write no leaf, so a blank document could not survive its own round trip — `leaf/leaves` promises exactness and was the only honest side of that. Documents are schema 3. A version 2 document is not read; nothing here converts one. `docs/lane-model.md` is the design, and says which of its parts are built. 392 tests, 5,525 assertions, and `test/browser/sequence.mjs` drives the editor through create, hold, explicit overflow and undo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:20:23 -04:00
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
A lane has non-overlapping half-open cel intervals `[start,end)`
An occurrence is a node, with a clock of its own A lane's drawings were going to be one instance whose source was a KEYED channel: frame 0 says `:drawing-a`, frame 4 says `:drawing-b`, and the cels of a row are that channel's keys. Two things followed from it, and both were wrong. The first is that playback meant whichever shape the channel happened to have. A framed source played its symbol; a keyed source froze the selected frame. So `node/placed-at` read animation out of storage, and adding an ordinary key to a still turned it into an animation — the last-key bug, which was not a bug in the code so much as the rule working as written. But WHICH drawing is used and HOW time runs inside it are independent questions, and all four combinations are ordinary: hold one drawing, play one animation, cut between held drawings, cut between playing ones. So an occurrence names one symbol in `:source {:symbol ...}` and says how its source time advances in `:playback {:in :speed :end}` — `source = in + speed * f`, a hold being speed 0, with `:stop`, `:hold` or `:loop` at the end named rather than guessed. `node/placed-frame` samples it forwards, which works for holds too, and `node/source-time` is the separate, invertible edit map, nil where inversion is meaningless. The two were one function before, and a hold had to lie about one of them. The second is that a keyed source only looked necessary because an occurrence was assumed to need a ROW. It does not. A lane is a group with `:layout :sequence`, its occurrences are ordinary instances in the same flat node map, and `timeline/rows` draws them as cel blocks on the lane's own row: twelve exposures, one row, each cel still separately selectable and addressable. The vertical growth that justified the keyed source is a presentation question, and it is answered in the view. `arthur.domain.sequence` holds the first commands over that shape — add lane, append drawing, extend hold — each one history step, each refusing rather than half-applying. Extending a hold leaves the lane's keys at their authored times, because you are adjusting drawings underneath timed motion; a correction owned by an occurrence travels with it. Ownership does that work, so no key needs a flag saying what it follows. Ripple past the symbol's end is refused with the frame count it would need, and `:extent :grow-symbol` is the caller saying yes. `clip/blank` no longer carries `:subjects {} :features {} :groups {}`. Empty maps write no leaf, so a blank document could not survive its own round trip — `leaf/leaves` promises exactness and was the only honest side of that. Documents are schema 3. A version 2 document is not read; nothing here converts one. `docs/lane-model.md` is the design, and says which of its parts are built. 392 tests, 5,525 assertions, and `test/browser/sequence.mjs` drives the editor through create, hold, explicit overflow and undo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:20:23 -04:00
in its own time space. Uncovered intervals are gaps. Empty lanes are valid.
Compositing and simultaneous sounds are represented by multiple lanes or ordinary
scene composition; an accidental overlap never silently selects a winner.
Transitions, if added, need explicit overlap and mixing semantics.
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
Properties can belong to content, one cel, or the lane. For example:
An occurrence is a node, with a clock of its own A lane's drawings were going to be one instance whose source was a KEYED channel: frame 0 says `:drawing-a`, frame 4 says `:drawing-b`, and the cels of a row are that channel's keys. Two things followed from it, and both were wrong. The first is that playback meant whichever shape the channel happened to have. A framed source played its symbol; a keyed source froze the selected frame. So `node/placed-at` read animation out of storage, and adding an ordinary key to a still turned it into an animation — the last-key bug, which was not a bug in the code so much as the rule working as written. But WHICH drawing is used and HOW time runs inside it are independent questions, and all four combinations are ordinary: hold one drawing, play one animation, cut between held drawings, cut between playing ones. So an occurrence names one symbol in `:source {:symbol ...}` and says how its source time advances in `:playback {:in :speed :end}` — `source = in + speed * f`, a hold being speed 0, with `:stop`, `:hold` or `:loop` at the end named rather than guessed. `node/placed-frame` samples it forwards, which works for holds too, and `node/source-time` is the separate, invertible edit map, nil where inversion is meaningless. The two were one function before, and a hold had to lie about one of them. The second is that a keyed source only looked necessary because an occurrence was assumed to need a ROW. It does not. A lane is a group with `:layout :sequence`, its occurrences are ordinary instances in the same flat node map, and `timeline/rows` draws them as cel blocks on the lane's own row: twelve exposures, one row, each cel still separately selectable and addressable. The vertical growth that justified the keyed source is a presentation question, and it is answered in the view. `arthur.domain.sequence` holds the first commands over that shape — add lane, append drawing, extend hold — each one history step, each refusing rather than half-applying. Extending a hold leaves the lane's keys at their authored times, because you are adjusting drawings underneath timed motion; a correction owned by an occurrence travels with it. Ownership does that work, so no key needs a flag saying what it follows. Ripple past the symbol's end is refused with the frame count it would need, and `:extent :grow-symbol` is the caller saying yes. `clip/blank` no longer carries `:subjects {} :features {} :groups {}`. Empty maps write no leaf, so a blank document could not survive its own round trip — `leaf/leaves` promises exactness and was the only honest side of that. Documents are schema 3. A version 2 document is not read; nothing here converts one. `docs/lane-model.md` is the design, and says which of its parts are built. 392 tests, 5,525 assertions, and `test/browser/sequence.mjs` drives the editor through create, hold, explicit overflow and undo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:20:23 -04:00
- Rotate the reusable drawing: all its uses change.
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
- Rotate one cel: only that cel changes.
An occurrence is a node, with a clock of its own A lane's drawings were going to be one instance whose source was a KEYED channel: frame 0 says `:drawing-a`, frame 4 says `:drawing-b`, and the cels of a row are that channel's keys. Two things followed from it, and both were wrong. The first is that playback meant whichever shape the channel happened to have. A framed source played its symbol; a keyed source froze the selected frame. So `node/placed-at` read animation out of storage, and adding an ordinary key to a still turned it into an animation — the last-key bug, which was not a bug in the code so much as the rule working as written. But WHICH drawing is used and HOW time runs inside it are independent questions, and all four combinations are ordinary: hold one drawing, play one animation, cut between held drawings, cut between playing ones. So an occurrence names one symbol in `:source {:symbol ...}` and says how its source time advances in `:playback {:in :speed :end}` — `source = in + speed * f`, a hold being speed 0, with `:stop`, `:hold` or `:loop` at the end named rather than guessed. `node/placed-frame` samples it forwards, which works for holds too, and `node/source-time` is the separate, invertible edit map, nil where inversion is meaningless. The two were one function before, and a hold had to lie about one of them. The second is that a keyed source only looked necessary because an occurrence was assumed to need a ROW. It does not. A lane is a group with `:layout :sequence`, its occurrences are ordinary instances in the same flat node map, and `timeline/rows` draws them as cel blocks on the lane's own row: twelve exposures, one row, each cel still separately selectable and addressable. The vertical growth that justified the keyed source is a presentation question, and it is answered in the view. `arthur.domain.sequence` holds the first commands over that shape — add lane, append drawing, extend hold — each one history step, each refusing rather than half-applying. Extending a hold leaves the lane's keys at their authored times, because you are adjusting drawings underneath timed motion; a correction owned by an occurrence travels with it. Ownership does that work, so no key needs a flag saying what it follows. Ripple past the symbol's end is refused with the frame count it would need, and `:extent :grow-symbol` is the caller saying yes. `clip/blank` no longer carries `:subjects {} :features {} :groups {}`. Empty maps write no leaf, so a blank document could not survive its own round trip — `leaf/leaves` promises exactness and was the only honest side of that. Documents are schema 3. A version 2 document is not read; nothing here converts one. `docs/lane-model.md` is the design, and says which of its parts are built. 392 tests, 5,525 assertions, and `test/browser/sequence.mjs` drives the editor through create, hold, explicit overflow and undo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:20:23 -04:00
- Animate the lane's rotation: whichever drawing is showing follows it.
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
A cel can have its own transform, gain, corrections, and source timing
An occurrence is a node, with a clock of its own A lane's drawings were going to be one instance whose source was a KEYED channel: frame 0 says `:drawing-a`, frame 4 says `:drawing-b`, and the cels of a row are that channel's keys. Two things followed from it, and both were wrong. The first is that playback meant whichever shape the channel happened to have. A framed source played its symbol; a keyed source froze the selected frame. So `node/placed-at` read animation out of storage, and adding an ordinary key to a still turned it into an animation — the last-key bug, which was not a bug in the code so much as the rule working as written. But WHICH drawing is used and HOW time runs inside it are independent questions, and all four combinations are ordinary: hold one drawing, play one animation, cut between held drawings, cut between playing ones. So an occurrence names one symbol in `:source {:symbol ...}` and says how its source time advances in `:playback {:in :speed :end}` — `source = in + speed * f`, a hold being speed 0, with `:stop`, `:hold` or `:loop` at the end named rather than guessed. `node/placed-frame` samples it forwards, which works for holds too, and `node/source-time` is the separate, invertible edit map, nil where inversion is meaningless. The two were one function before, and a hold had to lie about one of them. The second is that a keyed source only looked necessary because an occurrence was assumed to need a ROW. It does not. A lane is a group with `:layout :sequence`, its occurrences are ordinary instances in the same flat node map, and `timeline/rows` draws them as cel blocks on the lane's own row: twelve exposures, one row, each cel still separately selectable and addressable. The vertical growth that justified the keyed source is a presentation question, and it is answered in the view. `arthur.domain.sequence` holds the first commands over that shape — add lane, append drawing, extend hold — each one history step, each refusing rather than half-applying. Extending a hold leaves the lane's keys at their authored times, because you are adjusting drawings underneath timed motion; a correction owned by an occurrence travels with it. Ownership does that work, so no key needs a flag saying what it follows. Ripple past the symbol's end is refused with the frame count it would need, and `:extent :grow-symbol` is the caller saying yes. `clip/blank` no longer carries `:subjects {} :features {} :groups {}`. Empty maps write no leaf, so a blank document could not survive its own round trip — `leaf/leaves` promises exactness and was the only honest side of that. Documents are schema 3. A version 2 document is not read; nothing here converts one. `docs/lane-model.md` is the design, and says which of its parts are built. 392 tests, 5,525 assertions, and `test/browser/sequence.mjs` drives the editor through create, hold, explicit overflow and undo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:20:23 -04:00
while remaining a block in the same timeline row. Independent treatment never
requires a new row or an otherwise unnecessary wrapper symbol.
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
### Concrete candidate: a lane and ordinary instances
An occurrence is a node, with a clock of its own A lane's drawings were going to be one instance whose source was a KEYED channel: frame 0 says `:drawing-a`, frame 4 says `:drawing-b`, and the cels of a row are that channel's keys. Two things followed from it, and both were wrong. The first is that playback meant whichever shape the channel happened to have. A framed source played its symbol; a keyed source froze the selected frame. So `node/placed-at` read animation out of storage, and adding an ordinary key to a still turned it into an animation — the last-key bug, which was not a bug in the code so much as the rule working as written. But WHICH drawing is used and HOW time runs inside it are independent questions, and all four combinations are ordinary: hold one drawing, play one animation, cut between held drawings, cut between playing ones. So an occurrence names one symbol in `:source {:symbol ...}` and says how its source time advances in `:playback {:in :speed :end}` — `source = in + speed * f`, a hold being speed 0, with `:stop`, `:hold` or `:loop` at the end named rather than guessed. `node/placed-frame` samples it forwards, which works for holds too, and `node/source-time` is the separate, invertible edit map, nil where inversion is meaningless. The two were one function before, and a hold had to lie about one of them. The second is that a keyed source only looked necessary because an occurrence was assumed to need a ROW. It does not. A lane is a group with `:layout :sequence`, its occurrences are ordinary instances in the same flat node map, and `timeline/rows` draws them as cel blocks on the lane's own row: twelve exposures, one row, each cel still separately selectable and addressable. The vertical growth that justified the keyed source is a presentation question, and it is answered in the view. `arthur.domain.sequence` holds the first commands over that shape — add lane, append drawing, extend hold — each one history step, each refusing rather than half-applying. Extending a hold leaves the lane's keys at their authored times, because you are adjusting drawings underneath timed motion; a correction owned by an occurrence travels with it. Ownership does that work, so no key needs a flag saying what it follows. Ripple past the symbol's end is refused with the frame count it would need, and `:extent :grow-symbol` is the caller saying yes. `clip/blank` no longer carries `:subjects {} :features {} :groups {}`. Empty maps write no leaf, so a blank document could not survive its own round trip — `leaf/leaves` promises exactness and was the only honest side of that. Documents are schema 3. A version 2 document is not read; nothing here converts one. `docs/lane-model.md` is the design, and says which of its parts are built. 392 tests, 5,525 assertions, and `test/browser/sequence.mjs` drives the editor through create, hold, explicit overflow and undo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:20:23 -04:00
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
A lane is a group node with `:layout :sequence`. Its cels are ordinary
An occurrence is a node, with a clock of its own A lane's drawings were going to be one instance whose source was a KEYED channel: frame 0 says `:drawing-a`, frame 4 says `:drawing-b`, and the cels of a row are that channel's keys. Two things followed from it, and both were wrong. The first is that playback meant whichever shape the channel happened to have. A framed source played its symbol; a keyed source froze the selected frame. So `node/placed-at` read animation out of storage, and adding an ordinary key to a still turned it into an animation — the last-key bug, which was not a bug in the code so much as the rule working as written. But WHICH drawing is used and HOW time runs inside it are independent questions, and all four combinations are ordinary: hold one drawing, play one animation, cut between held drawings, cut between playing ones. So an occurrence names one symbol in `:source {:symbol ...}` and says how its source time advances in `:playback {:in :speed :end}` — `source = in + speed * f`, a hold being speed 0, with `:stop`, `:hold` or `:loop` at the end named rather than guessed. `node/placed-frame` samples it forwards, which works for holds too, and `node/source-time` is the separate, invertible edit map, nil where inversion is meaningless. The two were one function before, and a hold had to lie about one of them. The second is that a keyed source only looked necessary because an occurrence was assumed to need a ROW. It does not. A lane is a group with `:layout :sequence`, its occurrences are ordinary instances in the same flat node map, and `timeline/rows` draws them as cel blocks on the lane's own row: twelve exposures, one row, each cel still separately selectable and addressable. The vertical growth that justified the keyed source is a presentation question, and it is answered in the view. `arthur.domain.sequence` holds the first commands over that shape — add lane, append drawing, extend hold — each one history step, each refusing rather than half-applying. Extending a hold leaves the lane's keys at their authored times, because you are adjusting drawings underneath timed motion; a correction owned by an occurrence travels with it. Ownership does that work, so no key needs a flag saying what it follows. Ripple past the symbol's end is refused with the frame count it would need, and `:extent :grow-symbol` is the caller saying yes. `clip/blank` no longer carries `:subjects {} :features {} :groups {}`. Empty maps write no leaf, so a blank document could not survive its own round trip — `leaf/leaves` promises exactness and was the only honest side of that. Documents are schema 3. A version 2 document is not read; nothing here converts one. `docs/lane-model.md` is the design, and says which of its parts are built. 392 tests, 5,525 assertions, and `test/browser/sequence.mjs` drives the editor through create, hold, explicit overflow and undo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:20:23 -04:00
instance nodes whose `:parent` points to the group. All remain in their symbol's
flat node map. The sequence constraint is document semantics; which rows the UI
expands remains editor state. Ordinary groups retain unconstrained composition.
This example is a document the runtime accepts, built and evaluated by
`frontend/test/arthur/domain/lane_test.cljs`. Times here are zero-based. Channels
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
use the existing representation; cel source references and playback have
An occurrence is a node, with a clock of its own A lane's drawings were going to be one instance whose source was a KEYED channel: frame 0 says `:drawing-a`, frame 4 says `:drawing-b`, and the cels of a row are that channel's keys. Two things followed from it, and both were wrong. The first is that playback meant whichever shape the channel happened to have. A framed source played its symbol; a keyed source froze the selected frame. So `node/placed-at` read animation out of storage, and adding an ordinary key to a still turned it into an animation — the last-key bug, which was not a bug in the code so much as the rule working as written. But WHICH drawing is used and HOW time runs inside it are independent questions, and all four combinations are ordinary: hold one drawing, play one animation, cut between held drawings, cut between playing ones. So an occurrence names one symbol in `:source {:symbol ...}` and says how its source time advances in `:playback {:in :speed :end}` — `source = in + speed * f`, a hold being speed 0, with `:stop`, `:hold` or `:loop` at the end named rather than guessed. `node/placed-frame` samples it forwards, which works for holds too, and `node/source-time` is the separate, invertible edit map, nil where inversion is meaningless. The two were one function before, and a hold had to lie about one of them. The second is that a keyed source only looked necessary because an occurrence was assumed to need a ROW. It does not. A lane is a group with `:layout :sequence`, its occurrences are ordinary instances in the same flat node map, and `timeline/rows` draws them as cel blocks on the lane's own row: twelve exposures, one row, each cel still separately selectable and addressable. The vertical growth that justified the keyed source is a presentation question, and it is answered in the view. `arthur.domain.sequence` holds the first commands over that shape — add lane, append drawing, extend hold — each one history step, each refusing rather than half-applying. Extending a hold leaves the lane's keys at their authored times, because you are adjusting drawings underneath timed motion; a correction owned by an occurrence travels with it. Ownership does that work, so no key needs a flag saying what it follows. Ripple past the symbol's end is refused with the frame count it would need, and `:extent :grow-symbol` is the caller saying yes. `clip/blank` no longer carries `:subjects {} :features {} :groups {}`. Empty maps write no leaf, so a blank document could not survive its own round trip — `leaf/leaves` promises exactness and was the only honest side of that. Documents are schema 3. A version 2 document is not read; nothing here converts one. `docs/lane-model.md` is the design, and says which of its parts are built. 392 tests, 5,525 assertions, and `test/browser/sequence.mjs` drives the editor through create, hold, explicit overflow and undo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:20:23 -04:00
replaced the `[:source]` channel, which no longer exists.
```clojure
;; Within :main's :nodes; referenced drawings/animations live in :symbols.
{:girl
{:id :girl :kind :group :layout :sequence :z "b"
:channels {[:xform :rot]
{:animated? true :interp :linear :keys {0 0, 6 30, 12 0}}}}
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
:cel-a
{:id :cel-a :kind :instance :parent :girl :z "a"
An occurrence is a node, with a clock of its own A lane's drawings were going to be one instance whose source was a KEYED channel: frame 0 says `:drawing-a`, frame 4 says `:drawing-b`, and the cels of a row are that channel's keys. Two things followed from it, and both were wrong. The first is that playback meant whichever shape the channel happened to have. A framed source played its symbol; a keyed source froze the selected frame. So `node/placed-at` read animation out of storage, and adding an ordinary key to a still turned it into an animation — the last-key bug, which was not a bug in the code so much as the rule working as written. But WHICH drawing is used and HOW time runs inside it are independent questions, and all four combinations are ordinary: hold one drawing, play one animation, cut between held drawings, cut between playing ones. So an occurrence names one symbol in `:source {:symbol ...}` and says how its source time advances in `:playback {:in :speed :end}` — `source = in + speed * f`, a hold being speed 0, with `:stop`, `:hold` or `:loop` at the end named rather than guessed. `node/placed-frame` samples it forwards, which works for holds too, and `node/source-time` is the separate, invertible edit map, nil where inversion is meaningless. The two were one function before, and a hold had to lie about one of them. The second is that a keyed source only looked necessary because an occurrence was assumed to need a ROW. It does not. A lane is a group with `:layout :sequence`, its occurrences are ordinary instances in the same flat node map, and `timeline/rows` draws them as cel blocks on the lane's own row: twelve exposures, one row, each cel still separately selectable and addressable. The vertical growth that justified the keyed source is a presentation question, and it is answered in the view. `arthur.domain.sequence` holds the first commands over that shape — add lane, append drawing, extend hold — each one history step, each refusing rather than half-applying. Extending a hold leaves the lane's keys at their authored times, because you are adjusting drawings underneath timed motion; a correction owned by an occurrence travels with it. Ownership does that work, so no key needs a flag saying what it follows. Ripple past the symbol's end is refused with the frame count it would need, and `:extent :grow-symbol` is the caller saying yes. `clip/blank` no longer carries `:subjects {} :features {} :groups {}`. Empty maps write no leaf, so a blank document could not survive its own round trip — `leaf/leaves` promises exactness and was the only honest side of that. Documents are schema 3. A version 2 document is not read; nothing here converts one. `docs/lane-model.md` is the design, and says which of its parts are built. 392 tests, 5,525 assertions, and `test/browser/sequence.mjs` drives the editor through create, hold, explicit overflow and undo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:20:23 -04:00
:time {:at 0 :rate 1} :span [0 4]
:source {:symbol :drawing-a}
:playback {:in 0 :speed 0 :end :stop}}
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
:cel-b
{:id :cel-b :kind :instance :parent :girl :z "b"
An occurrence is a node, with a clock of its own A lane's drawings were going to be one instance whose source was a KEYED channel: frame 0 says `:drawing-a`, frame 4 says `:drawing-b`, and the cels of a row are that channel's keys. Two things followed from it, and both were wrong. The first is that playback meant whichever shape the channel happened to have. A framed source played its symbol; a keyed source froze the selected frame. So `node/placed-at` read animation out of storage, and adding an ordinary key to a still turned it into an animation — the last-key bug, which was not a bug in the code so much as the rule working as written. But WHICH drawing is used and HOW time runs inside it are independent questions, and all four combinations are ordinary: hold one drawing, play one animation, cut between held drawings, cut between playing ones. So an occurrence names one symbol in `:source {:symbol ...}` and says how its source time advances in `:playback {:in :speed :end}` — `source = in + speed * f`, a hold being speed 0, with `:stop`, `:hold` or `:loop` at the end named rather than guessed. `node/placed-frame` samples it forwards, which works for holds too, and `node/source-time` is the separate, invertible edit map, nil where inversion is meaningless. The two were one function before, and a hold had to lie about one of them. The second is that a keyed source only looked necessary because an occurrence was assumed to need a ROW. It does not. A lane is a group with `:layout :sequence`, its occurrences are ordinary instances in the same flat node map, and `timeline/rows` draws them as cel blocks on the lane's own row: twelve exposures, one row, each cel still separately selectable and addressable. The vertical growth that justified the keyed source is a presentation question, and it is answered in the view. `arthur.domain.sequence` holds the first commands over that shape — add lane, append drawing, extend hold — each one history step, each refusing rather than half-applying. Extending a hold leaves the lane's keys at their authored times, because you are adjusting drawings underneath timed motion; a correction owned by an occurrence travels with it. Ownership does that work, so no key needs a flag saying what it follows. Ripple past the symbol's end is refused with the frame count it would need, and `:extent :grow-symbol` is the caller saying yes. `clip/blank` no longer carries `:subjects {} :features {} :groups {}`. Empty maps write no leaf, so a blank document could not survive its own round trip — `leaf/leaves` promises exactness and was the only honest side of that. Documents are schema 3. A version 2 document is not read; nothing here converts one. `docs/lane-model.md` is the design, and says which of its parts are built. 392 tests, 5,525 assertions, and `test/browser/sequence.mjs` drives the editor through create, hold, explicit overflow and undo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:20:23 -04:00
:time {:at 4 :rate 1} :span [0 4]
:source {:symbol :drawing-b}
:playback {:in 0 :speed 0 :end :stop}
:channels {[:xform :pos]
{:animated? true :interp :hold :keys {0 [0 0], 1 [2 0]}}}}
:animated-insert
{:id :animated-insert :kind :instance :parent :girl :z "c"
:time {:at 8 :rate 1} :span [0 4]
:source {:symbol :wave}
:playback {:in 3 :speed 1 :end :stop}}}
```
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
The lane's rotation reads lane time. Each cel's channels read cel
An occurrence is a node, with a clock of its own A lane's drawings were going to be one instance whose source was a KEYED channel: frame 0 says `:drawing-a`, frame 4 says `:drawing-b`, and the cels of a row are that channel's keys. Two things followed from it, and both were wrong. The first is that playback meant whichever shape the channel happened to have. A framed source played its symbol; a keyed source froze the selected frame. So `node/placed-at` read animation out of storage, and adding an ordinary key to a still turned it into an animation — the last-key bug, which was not a bug in the code so much as the rule working as written. But WHICH drawing is used and HOW time runs inside it are independent questions, and all four combinations are ordinary: hold one drawing, play one animation, cut between held drawings, cut between playing ones. So an occurrence names one symbol in `:source {:symbol ...}` and says how its source time advances in `:playback {:in :speed :end}` — `source = in + speed * f`, a hold being speed 0, with `:stop`, `:hold` or `:loop` at the end named rather than guessed. `node/placed-frame` samples it forwards, which works for holds too, and `node/source-time` is the separate, invertible edit map, nil where inversion is meaningless. The two were one function before, and a hold had to lie about one of them. The second is that a keyed source only looked necessary because an occurrence was assumed to need a ROW. It does not. A lane is a group with `:layout :sequence`, its occurrences are ordinary instances in the same flat node map, and `timeline/rows` draws them as cel blocks on the lane's own row: twelve exposures, one row, each cel still separately selectable and addressable. The vertical growth that justified the keyed source is a presentation question, and it is answered in the view. `arthur.domain.sequence` holds the first commands over that shape — add lane, append drawing, extend hold — each one history step, each refusing rather than half-applying. Extending a hold leaves the lane's keys at their authored times, because you are adjusting drawings underneath timed motion; a correction owned by an occurrence travels with it. Ownership does that work, so no key needs a flag saying what it follows. Ripple past the symbol's end is refused with the frame count it would need, and `:extent :grow-symbol` is the caller saying yes. `clip/blank` no longer carries `:subjects {} :features {} :groups {}`. Empty maps write no leaf, so a blank document could not survive its own round trip — `leaf/leaves` promises exactness and was the only honest side of that. Documents are schema 3. A version 2 document is not read; nothing here converts one. `docs/lane-model.md` is the design, and says which of its parts are built. 392 tests, 5,525 assertions, and `test/browser/sequence.mjs` drives the editor through create, hold, explicit overflow and undo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:20:23 -04:00
time. Its content reads source time. The stills sample frame 0, while the insert
samples source frames 3, 4, 5, and 6. The group transform composes with the
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
cel transform and then the content's own transform.
An occurrence is a node, with a clock of its own A lane's drawings were going to be one instance whose source was a KEYED channel: frame 0 says `:drawing-a`, frame 4 says `:drawing-b`, and the cels of a row are that channel's keys. Two things followed from it, and both were wrong. The first is that playback meant whichever shape the channel happened to have. A framed source played its symbol; a keyed source froze the selected frame. So `node/placed-at` read animation out of storage, and adding an ordinary key to a still turned it into an animation — the last-key bug, which was not a bug in the code so much as the rule working as written. But WHICH drawing is used and HOW time runs inside it are independent questions, and all four combinations are ordinary: hold one drawing, play one animation, cut between held drawings, cut between playing ones. So an occurrence names one symbol in `:source {:symbol ...}` and says how its source time advances in `:playback {:in :speed :end}` — `source = in + speed * f`, a hold being speed 0, with `:stop`, `:hold` or `:loop` at the end named rather than guessed. `node/placed-frame` samples it forwards, which works for holds too, and `node/source-time` is the separate, invertible edit map, nil where inversion is meaningless. The two were one function before, and a hold had to lie about one of them. The second is that a keyed source only looked necessary because an occurrence was assumed to need a ROW. It does not. A lane is a group with `:layout :sequence`, its occurrences are ordinary instances in the same flat node map, and `timeline/rows` draws them as cel blocks on the lane's own row: twelve exposures, one row, each cel still separately selectable and addressable. The vertical growth that justified the keyed source is a presentation question, and it is answered in the view. `arthur.domain.sequence` holds the first commands over that shape — add lane, append drawing, extend hold — each one history step, each refusing rather than half-applying. Extending a hold leaves the lane's keys at their authored times, because you are adjusting drawings underneath timed motion; a correction owned by an occurrence travels with it. Ownership does that work, so no key needs a flag saying what it follows. Ripple past the symbol's end is refused with the frame count it would need, and `:extent :grow-symbol` is the caller saying yes. `clip/blank` no longer carries `:subjects {} :features {} :groups {}`. Empty maps write no leaf, so a blank document could not survive its own round trip — `leaf/leaves` promises exactness and was the only honest side of that. Documents are schema 3. A version 2 document is not read; nothing here converts one. `docs/lane-model.md` is the design, and says which of its parts are built. 392 tests, 5,525 assertions, and `test/browser/sequence.mjs` drives the editor through create, hold, explicit overflow and undo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:20:23 -04:00
`:span` remains in the node's own coordinates, consistent with ordinary nodes.
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`.
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
The sequence group may contain visual cels or audio cels; its
An occurrence is a node, with a clock of its own A lane's drawings were going to be one instance whose source was a KEYED channel: frame 0 says `:drawing-a`, frame 4 says `:drawing-b`, and the cels of a row are that channel's keys. Two things followed from it, and both were wrong. The first is that playback meant whichever shape the channel happened to have. A framed source played its symbol; a keyed source froze the selected frame. So `node/placed-at` read animation out of storage, and adding an ordinary key to a still turned it into an animation — the last-key bug, which was not a bug in the code so much as the rule working as written. But WHICH drawing is used and HOW time runs inside it are independent questions, and all four combinations are ordinary: hold one drawing, play one animation, cut between held drawings, cut between playing ones. So an occurrence names one symbol in `:source {:symbol ...}` and says how its source time advances in `:playback {:in :speed :end}` — `source = in + speed * f`, a hold being speed 0, with `:stop`, `:hold` or `:loop` at the end named rather than guessed. `node/placed-frame` samples it forwards, which works for holds too, and `node/source-time` is the separate, invertible edit map, nil where inversion is meaningless. The two were one function before, and a hold had to lie about one of them. The second is that a keyed source only looked necessary because an occurrence was assumed to need a ROW. It does not. A lane is a group with `:layout :sequence`, its occurrences are ordinary instances in the same flat node map, and `timeline/rows` draws them as cel blocks on the lane's own row: twelve exposures, one row, each cel still separately selectable and addressable. The vertical growth that justified the keyed source is a presentation question, and it is answered in the view. `arthur.domain.sequence` holds the first commands over that shape — add lane, append drawing, extend hold — each one history step, each refusing rather than half-applying. Extending a hold leaves the lane's keys at their authored times, because you are adjusting drawings underneath timed motion; a correction owned by an occurrence travels with it. Ownership does that work, so no key needs a flag saying what it follows. Ripple past the symbol's end is refused with the frame count it would need, and `:extent :grow-symbol` is the caller saying yes. `clip/blank` no longer carries `:subjects {} :features {} :groups {}`. Empty maps write no leaf, so a blank document could not survive its own round trip — `leaf/leaves` promises exactness and was the only honest side of that. Documents are schema 3. A version 2 document is not read; nothing here converts one. `docs/lane-model.md` is the design, and says which of its parts are built. 392 tests, 5,525 assertions, and `test/browser/sequence.mjs` drives the editor through create, hold, explicit overflow and undo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:20:23 -04:00
capability must reject an incompatible mixture rather than infer it per frame.
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
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
An occurrence is a node, with a clock of its own A lane's drawings were going to be one instance whose source was a KEYED channel: frame 0 says `:drawing-a`, frame 4 says `:drawing-b`, and the cels of a row are that channel's keys. Two things followed from it, and both were wrong. The first is that playback meant whichever shape the channel happened to have. A framed source played its symbol; a keyed source froze the selected frame. So `node/placed-at` read animation out of storage, and adding an ordinary key to a still turned it into an animation — the last-key bug, which was not a bug in the code so much as the rule working as written. But WHICH drawing is used and HOW time runs inside it are independent questions, and all four combinations are ordinary: hold one drawing, play one animation, cut between held drawings, cut between playing ones. So an occurrence names one symbol in `:source {:symbol ...}` and says how its source time advances in `:playback {:in :speed :end}` — `source = in + speed * f`, a hold being speed 0, with `:stop`, `:hold` or `:loop` at the end named rather than guessed. `node/placed-frame` samples it forwards, which works for holds too, and `node/source-time` is the separate, invertible edit map, nil where inversion is meaningless. The two were one function before, and a hold had to lie about one of them. The second is that a keyed source only looked necessary because an occurrence was assumed to need a ROW. It does not. A lane is a group with `:layout :sequence`, its occurrences are ordinary instances in the same flat node map, and `timeline/rows` draws them as cel blocks on the lane's own row: twelve exposures, one row, each cel still separately selectable and addressable. The vertical growth that justified the keyed source is a presentation question, and it is answered in the view. `arthur.domain.sequence` holds the first commands over that shape — add lane, append drawing, extend hold — each one history step, each refusing rather than half-applying. Extending a hold leaves the lane's keys at their authored times, because you are adjusting drawings underneath timed motion; a correction owned by an occurrence travels with it. Ownership does that work, so no key needs a flag saying what it follows. Ripple past the symbol's end is refused with the frame count it would need, and `:extent :grow-symbol` is the caller saying yes. `clip/blank` no longer carries `:subjects {} :features {} :groups {}`. Empty maps write no leaf, so a blank document could not survive its own round trip — `leaf/leaves` promises exactness and was the only honest side of that. Documents are schema 3. A version 2 document is not read; nothing here converts one. `docs/lane-model.md` is the design, and says which of its parts are built. 392 tests, 5,525 assertions, and `test/browser/sequence.mjs` drives the editor through create, hold, explicit overflow and undo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:20:23 -04:00
diagnosis that making `:of` a channel was necessary to avoid vertical growth:
multiple instances can occupy one row when the view presents their containing
sequence. A lane-level source schedule is therefore derived, not authored twice.
## Source selection and source playback are independent
The current implementation makes framed sources play and keyed sources hold.
Retire that rule. Channel storage shape must not determine playback behavior.
Adding or removing a key must not turn a still into an animation or vice versa.
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
A cel names content and describes how its source time is sampled. In the
basic case, after mapping lane time into cel time:
An occurrence is a node, with a clock of its own A lane's drawings were going to be one instance whose source was a KEYED channel: frame 0 says `:drawing-a`, frame 4 says `:drawing-b`, and the cels of a row are that channel's keys. Two things followed from it, and both were wrong. The first is that playback meant whichever shape the channel happened to have. A framed source played its symbol; a keyed source froze the selected frame. So `node/placed-at` read animation out of storage, and adding an ordinary key to a still turned it into an animation — the last-key bug, which was not a bug in the code so much as the rule working as written. But WHICH drawing is used and HOW time runs inside it are independent questions, and all four combinations are ordinary: hold one drawing, play one animation, cut between held drawings, cut between playing ones. So an occurrence names one symbol in `:source {:symbol ...}` and says how its source time advances in `:playback {:in :speed :end}` — `source = in + speed * f`, a hold being speed 0, with `:stop`, `:hold` or `:loop` at the end named rather than guessed. `node/placed-frame` samples it forwards, which works for holds too, and `node/source-time` is the separate, invertible edit map, nil where inversion is meaningless. The two were one function before, and a hold had to lie about one of them. The second is that a keyed source only looked necessary because an occurrence was assumed to need a ROW. It does not. A lane is a group with `:layout :sequence`, its occurrences are ordinary instances in the same flat node map, and `timeline/rows` draws them as cel blocks on the lane's own row: twelve exposures, one row, each cel still separately selectable and addressable. The vertical growth that justified the keyed source is a presentation question, and it is answered in the view. `arthur.domain.sequence` holds the first commands over that shape — add lane, append drawing, extend hold — each one history step, each refusing rather than half-applying. Extending a hold leaves the lane's keys at their authored times, because you are adjusting drawings underneath timed motion; a correction owned by an occurrence travels with it. Ownership does that work, so no key needs a flag saying what it follows. Ripple past the symbol's end is refused with the frame count it would need, and `:extent :grow-symbol` is the caller saying yes. `clip/blank` no longer carries `:subjects {} :features {} :groups {}`. Empty maps write no leaf, so a blank document could not survive its own round trip — `leaf/leaves` promises exactness and was the only honest side of that. Documents are schema 3. A version 2 document is not read; nothing here converts one. `docs/lane-model.md` is the design, and says which of its parts are built. 392 tests, 5,525 assertions, and `test/browser/sequence.mjs` drives the editor through create, hold, explicit overflow and undo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:20:23 -04:00
```text
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
source_time = in_point + speed × cel_time
An occurrence is a node, with a clock of its own A lane's drawings were going to be one instance whose source was a KEYED channel: frame 0 says `:drawing-a`, frame 4 says `:drawing-b`, and the cels of a row are that channel's keys. Two things followed from it, and both were wrong. The first is that playback meant whichever shape the channel happened to have. A framed source played its symbol; a keyed source froze the selected frame. So `node/placed-at` read animation out of storage, and adding an ordinary key to a still turned it into an animation — the last-key bug, which was not a bug in the code so much as the rule working as written. But WHICH drawing is used and HOW time runs inside it are independent questions, and all four combinations are ordinary: hold one drawing, play one animation, cut between held drawings, cut between playing ones. So an occurrence names one symbol in `:source {:symbol ...}` and says how its source time advances in `:playback {:in :speed :end}` — `source = in + speed * f`, a hold being speed 0, with `:stop`, `:hold` or `:loop` at the end named rather than guessed. `node/placed-frame` samples it forwards, which works for holds too, and `node/source-time` is the separate, invertible edit map, nil where inversion is meaningless. The two were one function before, and a hold had to lie about one of them. The second is that a keyed source only looked necessary because an occurrence was assumed to need a ROW. It does not. A lane is a group with `:layout :sequence`, its occurrences are ordinary instances in the same flat node map, and `timeline/rows` draws them as cel blocks on the lane's own row: twelve exposures, one row, each cel still separately selectable and addressable. The vertical growth that justified the keyed source is a presentation question, and it is answered in the view. `arthur.domain.sequence` holds the first commands over that shape — add lane, append drawing, extend hold — each one history step, each refusing rather than half-applying. Extending a hold leaves the lane's keys at their authored times, because you are adjusting drawings underneath timed motion; a correction owned by an occurrence travels with it. Ownership does that work, so no key needs a flag saying what it follows. Ripple past the symbol's end is refused with the frame count it would need, and `:extent :grow-symbol` is the caller saying yes. `clip/blank` no longer carries `:subjects {} :features {} :groups {}`. Empty maps write no leaf, so a blank document could not survive its own round trip — `leaf/leaves` promises exactness and was the only honest side of that. Documents are schema 3. A version 2 document is not read; nothing here converts one. `docs/lane-model.md` is the design, and says which of its parts are built. 392 tests, 5,525 assertions, and `test/browser/sequence.mjs` drives the editor through create, hold, explicit overflow and undo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:20:23 -04:00
```
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
A newly created cel starts at local time zero. Moving it preserves this
An occurrence is a node, with a clock of its own A lane's drawings were going to be one instance whose source was a KEYED channel: frame 0 says `:drawing-a`, frame 4 says `:drawing-b`, and the cels of a row are that channel's keys. Two things followed from it, and both were wrong. The first is that playback meant whichever shape the channel happened to have. A framed source played its symbol; a keyed source froze the selected frame. So `node/placed-at` read animation out of storage, and adding an ordinary key to a still turned it into an animation — the last-key bug, which was not a bug in the code so much as the rule working as written. But WHICH drawing is used and HOW time runs inside it are independent questions, and all four combinations are ordinary: hold one drawing, play one animation, cut between held drawings, cut between playing ones. So an occurrence names one symbol in `:source {:symbol ...}` and says how its source time advances in `:playback {:in :speed :end}` — `source = in + speed * f`, a hold being speed 0, with `:stop`, `:hold` or `:loop` at the end named rather than guessed. `node/placed-frame` samples it forwards, which works for holds too, and `node/source-time` is the separate, invertible edit map, nil where inversion is meaningless. The two were one function before, and a hold had to lie about one of them. The second is that a keyed source only looked necessary because an occurrence was assumed to need a ROW. It does not. A lane is a group with `:layout :sequence`, its occurrences are ordinary instances in the same flat node map, and `timeline/rows` draws them as cel blocks on the lane's own row: twelve exposures, one row, each cel still separately selectable and addressable. The vertical growth that justified the keyed source is a presentation question, and it is answered in the view. `arthur.domain.sequence` holds the first commands over that shape — add lane, append drawing, extend hold — each one history step, each refusing rather than half-applying. Extending a hold leaves the lane's keys at their authored times, because you are adjusting drawings underneath timed motion; a correction owned by an occurrence travels with it. Ownership does that work, so no key needs a flag saying what it follows. Ripple past the symbol's end is refused with the frame count it would need, and `:extent :grow-symbol` is the caller saying yes. `clip/blank` no longer carries `:subjects {} :features {} :groups {}`. Empty maps write no leaf, so a blank document could not survive its own round trip — `leaf/leaves` promises exactness and was the only honest side of that. Documents are schema 3. A version 2 document is not read; nothing here converts one. `docs/lane-model.md` is the design, and says which of its parts are built. 392 tests, 5,525 assertions, and `test/browser/sequence.mjs` drives the editor through create, hold, explicit overflow and undo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:20:23 -04:00
origin relative to its content. Trimming can narrow its local support without
resetting that origin; split pieces likewise preserve the source and property
values at the cut. Trimming, slipping, and retiming are distinct operations with
explicitly different effects on the interval and the source map.
| Intent | Source playback |
| --- | --- |
| Hold a drawing | Constant source frame, equivalently speed 0 |
| Play an animated symbol | Advancing source time, normally speed 1 |
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
| Cut between animations | Several cels, each with its own in-point and speed |
An occurrence is a node, with a clock of its own A lane's drawings were going to be one instance whose source was a KEYED channel: frame 0 says `:drawing-a`, frame 4 says `:drawing-b`, and the cels of a row are that channel's keys. Two things followed from it, and both were wrong. The first is that playback meant whichever shape the channel happened to have. A framed source played its symbol; a keyed source froze the selected frame. So `node/placed-at` read animation out of storage, and adding an ordinary key to a still turned it into an animation — the last-key bug, which was not a bug in the code so much as the rule working as written. But WHICH drawing is used and HOW time runs inside it are independent questions, and all four combinations are ordinary: hold one drawing, play one animation, cut between held drawings, cut between playing ones. So an occurrence names one symbol in `:source {:symbol ...}` and says how its source time advances in `:playback {:in :speed :end}` — `source = in + speed * f`, a hold being speed 0, with `:stop`, `:hold` or `:loop` at the end named rather than guessed. `node/placed-frame` samples it forwards, which works for holds too, and `node/source-time` is the separate, invertible edit map, nil where inversion is meaningless. The two were one function before, and a hold had to lie about one of them. The second is that a keyed source only looked necessary because an occurrence was assumed to need a ROW. It does not. A lane is a group with `:layout :sequence`, its occurrences are ordinary instances in the same flat node map, and `timeline/rows` draws them as cel blocks on the lane's own row: twelve exposures, one row, each cel still separately selectable and addressable. The vertical growth that justified the keyed source is a presentation question, and it is answered in the view. `arthur.domain.sequence` holds the first commands over that shape — add lane, append drawing, extend hold — each one history step, each refusing rather than half-applying. Extending a hold leaves the lane's keys at their authored times, because you are adjusting drawings underneath timed motion; a correction owned by an occurrence travels with it. Ownership does that work, so no key needs a flag saying what it follows. Ripple past the symbol's end is refused with the frame count it would need, and `:extent :grow-symbol` is the caller saying yes. `clip/blank` no longer carries `:subjects {} :features {} :groups {}`. Empty maps write no leaf, so a blank document could not survive its own round trip — `leaf/leaves` promises exactness and was the only honest side of that. Documents are schema 3. A version 2 document is not read; nothing here converts one. `docs/lane-model.md` is the design, and says which of its parts are built. 392 tests, 5,525 assertions, and `test/browser/sequence.mjs` drives the editor through create, hold, explicit overflow and undo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:20:23 -04:00
| Mix stills and animation in a lane | Constant and advancing maps in the same sequence |
The source reference itself is discrete and never numerically interpolated.
Interpolation belongs to properties that support it; a property registry should
declare value types, defaults, and permitted interpolation and correction modes.
Generic key toggles must consult those capabilities rather than assume every
non-boolean value can be tweened.
Define source bounds and end behavior explicitly: stop contributing outside the
source, hold an endpoint, or loop an explicit range. A still uses a valid constant
frame. A loop uses a nonempty half-open range and a defined modulo rule. Playback
never guesses these policies from whether a channel happens to have keys.
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
Audio shares cel arrangement, trimming, gain ownership, and clock mapping.
An occurrence is a node, with a clock of its own A lane's drawings were going to be one instance whose source was a KEYED channel: frame 0 says `:drawing-a`, frame 4 says `:drawing-b`, and the cels of a row are that channel's keys. Two things followed from it, and both were wrong. The first is that playback meant whichever shape the channel happened to have. A framed source played its symbol; a keyed source froze the selected frame. So `node/placed-at` read animation out of storage, and adding an ordinary key to a still turned it into an animation — the last-key bug, which was not a bug in the code so much as the rule working as written. But WHICH drawing is used and HOW time runs inside it are independent questions, and all four combinations are ordinary: hold one drawing, play one animation, cut between held drawings, cut between playing ones. So an occurrence names one symbol in `:source {:symbol ...}` and says how its source time advances in `:playback {:in :speed :end}` — `source = in + speed * f`, a hold being speed 0, with `:stop`, `:hold` or `:loop` at the end named rather than guessed. `node/placed-frame` samples it forwards, which works for holds too, and `node/source-time` is the separate, invertible edit map, nil where inversion is meaningless. The two were one function before, and a hold had to lie about one of them. The second is that a keyed source only looked necessary because an occurrence was assumed to need a ROW. It does not. A lane is a group with `:layout :sequence`, its occurrences are ordinary instances in the same flat node map, and `timeline/rows` draws them as cel blocks on the lane's own row: twelve exposures, one row, each cel still separately selectable and addressable. The vertical growth that justified the keyed source is a presentation question, and it is answered in the view. `arthur.domain.sequence` holds the first commands over that shape — add lane, append drawing, extend hold — each one history step, each refusing rather than half-applying. Extending a hold leaves the lane's keys at their authored times, because you are adjusting drawings underneath timed motion; a correction owned by an occurrence travels with it. Ownership does that work, so no key needs a flag saying what it follows. Ripple past the symbol's end is refused with the frame count it would need, and `:extent :grow-symbol` is the caller saying yes. `clip/blank` no longer carries `:subjects {} :features {} :groups {}`. Empty maps write no leaf, so a blank document could not survive its own round trip — `leaf/leaves` promises exactness and was the only honest side of that. Documents are schema 3. A version 2 document is not read; nothing here converts one. `docs/lane-model.md` is the design, and says which of its parts are built. 392 tests, 5,525 assertions, and `test/browser/sequence.mjs` drives the editor through create, hold, explicit overflow and undo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:20:23 -04:00
It does not inherit visual frame-hold semantics: holding one audio sample is not
an audio freeze effect. Validate supported playback policies by media capability.
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
Actual audio scheduling must follow active cels, including gaps and cuts,
An occurrence is a node, with a clock of its own A lane's drawings were going to be one instance whose source was a KEYED channel: frame 0 says `:drawing-a`, frame 4 says `:drawing-b`, and the cels of a row are that channel's keys. Two things followed from it, and both were wrong. The first is that playback meant whichever shape the channel happened to have. A framed source played its symbol; a keyed source froze the selected frame. So `node/placed-at` read animation out of storage, and adding an ordinary key to a still turned it into an animation — the last-key bug, which was not a bug in the code so much as the rule working as written. But WHICH drawing is used and HOW time runs inside it are independent questions, and all four combinations are ordinary: hold one drawing, play one animation, cut between held drawings, cut between playing ones. So an occurrence names one symbol in `:source {:symbol ...}` and says how its source time advances in `:playback {:in :speed :end}` — `source = in + speed * f`, a hold being speed 0, with `:stop`, `:hold` or `:loop` at the end named rather than guessed. `node/placed-frame` samples it forwards, which works for holds too, and `node/source-time` is the separate, invertible edit map, nil where inversion is meaningless. The two were one function before, and a hold had to lie about one of them. The second is that a keyed source only looked necessary because an occurrence was assumed to need a ROW. It does not. A lane is a group with `:layout :sequence`, its occurrences are ordinary instances in the same flat node map, and `timeline/rows` draws them as cel blocks on the lane's own row: twelve exposures, one row, each cel still separately selectable and addressable. The vertical growth that justified the keyed source is a presentation question, and it is answered in the view. `arthur.domain.sequence` holds the first commands over that shape — add lane, append drawing, extend hold — each one history step, each refusing rather than half-applying. Extending a hold leaves the lane's keys at their authored times, because you are adjusting drawings underneath timed motion; a correction owned by an occurrence travels with it. Ownership does that work, so no key needs a flag saying what it follows. Ripple past the symbol's end is refused with the frame count it would need, and `:extent :grow-symbol` is the caller saying yes. `clip/blank` no longer carries `:subjects {} :features {} :groups {}`. Empty maps write no leaf, so a blank document could not survive its own round trip — `leaf/leaves` promises exactness and was the only honest side of that. Documents are schema 3. A version 2 document is not read; nothing here converts one. `docs/lane-model.md` is the design, and says which of its parts are built. 392 tests, 5,525 assertions, and `test/browser/sequence.mjs` drives the editor through create, hold, explicit overflow and undo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:20:23 -04:00
rather than playing every sound reachable through a structural reference.
## Time spaces and sampling
Name the relevant space whenever an API accepts a time or range: project,
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
symbol/lane, cel, or source. Store authored frame coordinates exactly;
An occurrence is a node, with a clock of its own A lane's drawings were going to be one instance whose source was a KEYED channel: frame 0 says `:drawing-a`, frame 4 says `:drawing-b`, and the cels of a row are that channel's keys. Two things followed from it, and both were wrong. The first is that playback meant whichever shape the channel happened to have. A framed source played its symbol; a keyed source froze the selected frame. So `node/placed-at` read animation out of storage, and adding an ordinary key to a still turned it into an animation — the last-key bug, which was not a bug in the code so much as the rule working as written. But WHICH drawing is used and HOW time runs inside it are independent questions, and all four combinations are ordinary: hold one drawing, play one animation, cut between held drawings, cut between playing ones. So an occurrence names one symbol in `:source {:symbol ...}` and says how its source time advances in `:playback {:in :speed :end}` — `source = in + speed * f`, a hold being speed 0, with `:stop`, `:hold` or `:loop` at the end named rather than guessed. `node/placed-frame` samples it forwards, which works for holds too, and `node/source-time` is the separate, invertible edit map, nil where inversion is meaningless. The two were one function before, and a hold had to lie about one of them. The second is that a keyed source only looked necessary because an occurrence was assumed to need a ROW. It does not. A lane is a group with `:layout :sequence`, its occurrences are ordinary instances in the same flat node map, and `timeline/rows` draws them as cel blocks on the lane's own row: twelve exposures, one row, each cel still separately selectable and addressable. The vertical growth that justified the keyed source is a presentation question, and it is answered in the view. `arthur.domain.sequence` holds the first commands over that shape — add lane, append drawing, extend hold — each one history step, each refusing rather than half-applying. Extending a hold leaves the lane's keys at their authored times, because you are adjusting drawings underneath timed motion; a correction owned by an occurrence travels with it. Ownership does that work, so no key needs a flag saying what it follows. Ripple past the symbol's end is refused with the frame count it would need, and `:extent :grow-symbol` is the caller saying yes. `clip/blank` no longer carries `:subjects {} :features {} :groups {}`. Empty maps write no leaf, so a blank document could not survive its own round trip — `leaf/leaves` promises exactness and was the only honest side of that. Documents are schema 3. A version 2 document is not read; nothing here converts one. `docs/lane-model.md` is the design, and says which of its parts are built. 392 tests, 5,525 assertions, and `test/browser/sequence.mjs` drives the editor through create, hold, explicit overflow and undo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:20:23 -04:00
avoid cumulative rounding when moving through nested mappings. Quantize at a
declared sampling boundary, not at every traversal step. Audio also needs its
continuous clock/sample space rather than visual frame quantization.
A hold is an evaluable time map with no unique inverse. A loop can map many
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
displayed cels to one source time. APIs must distinguish forward sampling
from inverse editing, and expose enough context to resolve a cel or
An occurrence is a node, with a clock of its own A lane's drawings were going to be one instance whose source was a KEYED channel: frame 0 says `:drawing-a`, frame 4 says `:drawing-b`, and the cels of a row are that channel's keys. Two things followed from it, and both were wrong. The first is that playback meant whichever shape the channel happened to have. A framed source played its symbol; a keyed source froze the selected frame. So `node/placed-at` read animation out of storage, and adding an ordinary key to a still turned it into an animation — the last-key bug, which was not a bug in the code so much as the rule working as written. But WHICH drawing is used and HOW time runs inside it are independent questions, and all four combinations are ordinary: hold one drawing, play one animation, cut between held drawings, cut between playing ones. So an occurrence names one symbol in `:source {:symbol ...}` and says how its source time advances in `:playback {:in :speed :end}` — `source = in + speed * f`, a hold being speed 0, with `:stop`, `:hold` or `:loop` at the end named rather than guessed. `node/placed-frame` samples it forwards, which works for holds too, and `node/source-time` is the separate, invertible edit map, nil where inversion is meaningless. The two were one function before, and a hold had to lie about one of them. The second is that a keyed source only looked necessary because an occurrence was assumed to need a ROW. It does not. A lane is a group with `:layout :sequence`, its occurrences are ordinary instances in the same flat node map, and `timeline/rows` draws them as cel blocks on the lane's own row: twelve exposures, one row, each cel still separately selectable and addressable. The vertical growth that justified the keyed source is a presentation question, and it is answered in the view. `arthur.domain.sequence` holds the first commands over that shape — add lane, append drawing, extend hold — each one history step, each refusing rather than half-applying. Extending a hold leaves the lane's keys at their authored times, because you are adjusting drawings underneath timed motion; a correction owned by an occurrence travels with it. Ownership does that work, so no key needs a flag saying what it follows. Ripple past the symbol's end is refused with the frame count it would need, and `:extent :grow-symbol` is the caller saying yes. `clip/blank` no longer carries `:subjects {} :features {} :groups {}`. Empty maps write no leaf, so a blank document could not survive its own round trip — `leaf/leaves` promises exactness and was the only honest side of that. Documents are schema 3. A version 2 document is not read; nothing here converts one. `docs/lane-model.md` is the design, and says which of its parts are built. 392 tests, 5,525 assertions, and `test/browser/sequence.mjs` drives the editor through create, hold, explicit overflow and undo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:20:23 -04:00
explicitly refuse an ambiguous operation. Do not report a missing time map merely
because inversion is unavailable.
Separate invertible placement timing from source sampling. A zero source speed
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
can mean hold without making the cel's own edit clock non-invertible.
An occurrence is a node, with a clock of its own A lane's drawings were going to be one instance whose source was a KEYED channel: frame 0 says `:drawing-a`, frame 4 says `:drawing-b`, and the cels of a row are that channel's keys. Two things followed from it, and both were wrong. The first is that playback meant whichever shape the channel happened to have. A framed source played its symbol; a keyed source froze the selected frame. So `node/placed-at` read animation out of storage, and adding an ordinary key to a still turned it into an animation — the last-key bug, which was not a bug in the code so much as the rule working as written. But WHICH drawing is used and HOW time runs inside it are independent questions, and all four combinations are ordinary: hold one drawing, play one animation, cut between held drawings, cut between playing ones. So an occurrence names one symbol in `:source {:symbol ...}` and says how its source time advances in `:playback {:in :speed :end}` — `source = in + speed * f`, a hold being speed 0, with `:stop`, `:hold` or `:loop` at the end named rather than guessed. `node/placed-frame` samples it forwards, which works for holds too, and `node/source-time` is the separate, invertible edit map, nil where inversion is meaningless. The two were one function before, and a hold had to lie about one of them. The second is that a keyed source only looked necessary because an occurrence was assumed to need a ROW. It does not. A lane is a group with `:layout :sequence`, its occurrences are ordinary instances in the same flat node map, and `timeline/rows` draws them as cel blocks on the lane's own row: twelve exposures, one row, each cel still separately selectable and addressable. The vertical growth that justified the keyed source is a presentation question, and it is answered in the view. `arthur.domain.sequence` holds the first commands over that shape — add lane, append drawing, extend hold — each one history step, each refusing rather than half-applying. Extending a hold leaves the lane's keys at their authored times, because you are adjusting drawings underneath timed motion; a correction owned by an occurrence travels with it. Ownership does that work, so no key needs a flag saying what it follows. Ripple past the symbol's end is refused with the frame count it would need, and `:extent :grow-symbol` is the caller saying yes. `clip/blank` no longer carries `:subjects {} :features {} :groups {}`. Empty maps write no leaf, so a blank document could not survive its own round trip — `leaf/leaves` promises exactness and was the only honest side of that. Documents are schema 3. A version 2 document is not read; nothing here converts one. `docs/lane-model.md` is the design, and says which of its parts are built. 392 tests, 5,525 assertions, and `test/browser/sequence.mjs` drives the editor through create, hold, explicit overflow and undo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:20:23 -04:00
Reparenting through changing transforms or non-invertible timing must either
preserve the full result by an explicit bake or return a reason it cannot; a
matrix captured at one frame does not prove preservation across the animation.
The source's frame step, generated-pose sampling, and the lane's transform clock
are independent scopes. Drawing on twos must not accidentally step a smooth lane
transform. An explicit whole-subtree stepping operation can exist separately.
Share the quantization primitive where possible, but retain its units, phase,
rounding policy, and order relative to retiming and lead. The original suggestion
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
that cel and picture-rate sampling are simply one floor is insufficient:
An occurrence is a node, with a clock of its own A lane's drawings were going to be one instance whose source was a KEYED channel: frame 0 says `:drawing-a`, frame 4 says `:drawing-b`, and the cels of a row are that channel's keys. Two things followed from it, and both were wrong. The first is that playback meant whichever shape the channel happened to have. A framed source played its symbol; a keyed source froze the selected frame. So `node/placed-at` read animation out of storage, and adding an ordinary key to a still turned it into an animation — the last-key bug, which was not a bug in the code so much as the rule working as written. But WHICH drawing is used and HOW time runs inside it are independent questions, and all four combinations are ordinary: hold one drawing, play one animation, cut between held drawings, cut between playing ones. So an occurrence names one symbol in `:source {:symbol ...}` and says how its source time advances in `:playback {:in :speed :end}` — `source = in + speed * f`, a hold being speed 0, with `:stop`, `:hold` or `:loop` at the end named rather than guessed. `node/placed-frame` samples it forwards, which works for holds too, and `node/source-time` is the separate, invertible edit map, nil where inversion is meaningless. The two were one function before, and a hold had to lie about one of them. The second is that a keyed source only looked necessary because an occurrence was assumed to need a ROW. It does not. A lane is a group with `:layout :sequence`, its occurrences are ordinary instances in the same flat node map, and `timeline/rows` draws them as cel blocks on the lane's own row: twelve exposures, one row, each cel still separately selectable and addressable. The vertical growth that justified the keyed source is a presentation question, and it is answered in the view. `arthur.domain.sequence` holds the first commands over that shape — add lane, append drawing, extend hold — each one history step, each refusing rather than half-applying. Extending a hold leaves the lane's keys at their authored times, because you are adjusting drawings underneath timed motion; a correction owned by an occurrence travels with it. Ownership does that work, so no key needs a flag saying what it follows. Ripple past the symbol's end is refused with the frame count it would need, and `:extent :grow-symbol` is the caller saying yes. `clip/blank` no longer carries `:subjects {} :features {} :groups {}`. Empty maps write no leaf, so a blank document could not survive its own round trip — `leaf/leaves` promises exactness and was the only honest side of that. Documents are schema 3. A version 2 document is not read; nothing here converts one. `docs/lane-model.md` is the design, and says which of its parts are built. 392 tests, 5,525 assertions, and `test/browser/sequence.mjs` drives the editor through create, hold, explicit overflow and undo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:20:23 -04:00
noninteger grids and source-frame quantization require specified behavior.
Identity timing can be implicit; remove `:time :mode` if it only duplicates that.
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
A cel interval is authored. Lane content extent is derived from its
cels, including the explicit end of the last one. A separately authored
An occurrence is a node, with a clock of its own A lane's drawings were going to be one instance whose source was a KEYED channel: frame 0 says `:drawing-a`, frame 4 says `:drawing-b`, and the cels of a row are that channel's keys. Two things followed from it, and both were wrong. The first is that playback meant whichever shape the channel happened to have. A framed source played its symbol; a keyed source froze the selected frame. So `node/placed-at` read animation out of storage, and adding an ordinary key to a still turned it into an animation — the last-key bug, which was not a bug in the code so much as the rule working as written. But WHICH drawing is used and HOW time runs inside it are independent questions, and all four combinations are ordinary: hold one drawing, play one animation, cut between held drawings, cut between playing ones. So an occurrence names one symbol in `:source {:symbol ...}` and says how its source time advances in `:playback {:in :speed :end}` — `source = in + speed * f`, a hold being speed 0, with `:stop`, `:hold` or `:loop` at the end named rather than guessed. `node/placed-frame` samples it forwards, which works for holds too, and `node/source-time` is the separate, invertible edit map, nil where inversion is meaningless. The two were one function before, and a hold had to lie about one of them. The second is that a keyed source only looked necessary because an occurrence was assumed to need a ROW. It does not. A lane is a group with `:layout :sequence`, its occurrences are ordinary instances in the same flat node map, and `timeline/rows` draws them as cel blocks on the lane's own row: twelve exposures, one row, each cel still separately selectable and addressable. The vertical growth that justified the keyed source is a presentation question, and it is answered in the view. `arthur.domain.sequence` holds the first commands over that shape — add lane, append drawing, extend hold — each one history step, each refusing rather than half-applying. Extending a hold leaves the lane's keys at their authored times, because you are adjusting drawings underneath timed motion; a correction owned by an occurrence travels with it. Ownership does that work, so no key needs a flag saying what it follows. Ripple past the symbol's end is refused with the frame count it would need, and `:extent :grow-symbol` is the caller saying yes. `clip/blank` no longer carries `:subjects {} :features {} :groups {}`. Empty maps write no leaf, so a blank document could not survive its own round trip — `leaf/leaves` promises exactness and was the only honest side of that. Documents are schema 3. A version 2 document is not read; nothing here converts one. `docs/lane-model.md` is the design, and says which of its parts are built. 392 tests, 5,525 assertions, and `test/browser/sequence.mjs` drives the editor through create, hold, explicit overflow and undo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:20:23 -04:00
container trim/window is legitimate when it intentionally gates children. Do not
conflate that window with occupied extent or infer a final hold from the next key
when no next key exists. A range of frame numbers alone cannot encode visibility
or a missing measurement.
## Shared editing operations
Every view issues the same domain commands. A command accepts an explicit target
and edit policy, computes a valid change, and returns the change, resulting
selection, and any refusal reason. A button and a drag must not implement two
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
versions of cel extension.
An occurrence is a node, with a clock of its own A lane's drawings were going to be one instance whose source was a KEYED channel: frame 0 says `:drawing-a`, frame 4 says `:drawing-b`, and the cels of a row are that channel's keys. Two things followed from it, and both were wrong. The first is that playback meant whichever shape the channel happened to have. A framed source played its symbol; a keyed source froze the selected frame. So `node/placed-at` read animation out of storage, and adding an ordinary key to a still turned it into an animation — the last-key bug, which was not a bug in the code so much as the rule working as written. But WHICH drawing is used and HOW time runs inside it are independent questions, and all four combinations are ordinary: hold one drawing, play one animation, cut between held drawings, cut between playing ones. So an occurrence names one symbol in `:source {:symbol ...}` and says how its source time advances in `:playback {:in :speed :end}` — `source = in + speed * f`, a hold being speed 0, with `:stop`, `:hold` or `:loop` at the end named rather than guessed. `node/placed-frame` samples it forwards, which works for holds too, and `node/source-time` is the separate, invertible edit map, nil where inversion is meaningless. The two were one function before, and a hold had to lie about one of them. The second is that a keyed source only looked necessary because an occurrence was assumed to need a ROW. It does not. A lane is a group with `:layout :sequence`, its occurrences are ordinary instances in the same flat node map, and `timeline/rows` draws them as cel blocks on the lane's own row: twelve exposures, one row, each cel still separately selectable and addressable. The vertical growth that justified the keyed source is a presentation question, and it is answered in the view. `arthur.domain.sequence` holds the first commands over that shape — add lane, append drawing, extend hold — each one history step, each refusing rather than half-applying. Extending a hold leaves the lane's keys at their authored times, because you are adjusting drawings underneath timed motion; a correction owned by an occurrence travels with it. Ownership does that work, so no key needs a flag saying what it follows. Ripple past the symbol's end is refused with the frame count it would need, and `:extent :grow-symbol` is the caller saying yes. `clip/blank` no longer carries `:subjects {} :features {} :groups {}`. Empty maps write no leaf, so a blank document could not survive its own round trip — `leaf/leaves` promises exactness and was the only honest side of that. Documents are schema 3. A version 2 document is not read; nothing here converts one. `docs/lane-model.md` is the design, and says which of its parts are built. 392 tests, 5,525 assertions, and `test/browser/sequence.mjs` drives the editor through create, hold, explicit overflow and undo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:20:23 -04:00
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
An edit target identifies the symbol, cel path, selected entities or
An occurrence is a node, with a clock of its own A lane's drawings were going to be one instance whose source was a KEYED channel: frame 0 says `:drawing-a`, frame 4 says `:drawing-b`, and the cels of a row are that channel's keys. Two things followed from it, and both were wrong. The first is that playback meant whichever shape the channel happened to have. A framed source played its symbol; a keyed source froze the selected frame. So `node/placed-at` read animation out of storage, and adding an ordinary key to a still turned it into an animation — the last-key bug, which was not a bug in the code so much as the rule working as written. But WHICH drawing is used and HOW time runs inside it are independent questions, and all four combinations are ordinary: hold one drawing, play one animation, cut between held drawings, cut between playing ones. So an occurrence names one symbol in `:source {:symbol ...}` and says how its source time advances in `:playback {:in :speed :end}` — `source = in + speed * f`, a hold being speed 0, with `:stop`, `:hold` or `:loop` at the end named rather than guessed. `node/placed-frame` samples it forwards, which works for holds too, and `node/source-time` is the separate, invertible edit map, nil where inversion is meaningless. The two were one function before, and a hold had to lie about one of them. The second is that a keyed source only looked necessary because an occurrence was assumed to need a ROW. It does not. A lane is a group with `:layout :sequence`, its occurrences are ordinary instances in the same flat node map, and `timeline/rows` draws them as cel blocks on the lane's own row: twelve exposures, one row, each cel still separately selectable and addressable. The vertical growth that justified the keyed source is a presentation question, and it is answered in the view. `arthur.domain.sequence` holds the first commands over that shape — add lane, append drawing, extend hold — each one history step, each refusing rather than half-applying. Extending a hold leaves the lane's keys at their authored times, because you are adjusting drawings underneath timed motion; a correction owned by an occurrence travels with it. Ownership does that work, so no key needs a flag saying what it follows. Ripple past the symbol's end is refused with the frame count it would need, and `:extent :grow-symbol` is the caller saying yes. `clip/blank` no longer carries `:subjects {} :features {} :groups {}`. Empty maps write no leaf, so a blank document could not survive its own round trip — `leaf/leaves` promises exactness and was the only honest side of that. Documents are schema 3. A version 2 document is not read; nothing here converts one. `docs/lane-model.md` is the design, and says which of its parts are built. 392 tests, 5,525 assertions, and `test/browser/sequence.mjs` drives the editor through create, hold, explicit overflow and undo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:20:23 -04:00
properties, and the time range with its space. Navigation also distinguishes
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
editing shared content directly from editing it through a particular cel.
An occurrence is a node, with a clock of its own A lane's drawings were going to be one instance whose source was a KEYED channel: frame 0 says `:drawing-a`, frame 4 says `:drawing-b`, and the cels of a row are that channel's keys. Two things followed from it, and both were wrong. The first is that playback meant whichever shape the channel happened to have. A framed source played its symbol; a keyed source froze the selected frame. So `node/placed-at` read animation out of storage, and adding an ordinary key to a still turned it into an animation — the last-key bug, which was not a bug in the code so much as the rule working as written. But WHICH drawing is used and HOW time runs inside it are independent questions, and all four combinations are ordinary: hold one drawing, play one animation, cut between held drawings, cut between playing ones. So an occurrence names one symbol in `:source {:symbol ...}` and says how its source time advances in `:playback {:in :speed :end}` — `source = in + speed * f`, a hold being speed 0, with `:stop`, `:hold` or `:loop` at the end named rather than guessed. `node/placed-frame` samples it forwards, which works for holds too, and `node/source-time` is the separate, invertible edit map, nil where inversion is meaningless. The two were one function before, and a hold had to lie about one of them. The second is that a keyed source only looked necessary because an occurrence was assumed to need a ROW. It does not. A lane is a group with `:layout :sequence`, its occurrences are ordinary instances in the same flat node map, and `timeline/rows` draws them as cel blocks on the lane's own row: twelve exposures, one row, each cel still separately selectable and addressable. The vertical growth that justified the keyed source is a presentation question, and it is answered in the view. `arthur.domain.sequence` holds the first commands over that shape — add lane, append drawing, extend hold — each one history step, each refusing rather than half-applying. Extending a hold leaves the lane's keys at their authored times, because you are adjusting drawings underneath timed motion; a correction owned by an occurrence travels with it. Ownership does that work, so no key needs a flag saying what it follows. Ripple past the symbol's end is refused with the frame count it would need, and `:extent :grow-symbol` is the caller saying yes. `clip/blank` no longer carries `:subjects {} :features {} :groups {}`. Empty maps write no leaf, so a blank document could not survive its own round trip — `leaf/leaves` promises exactness and was the only honest side of that. Documents are schema 3. A version 2 document is not read; nothing here converts one. `docs/lane-model.md` is the design, and says which of its parts are built. 392 tests, 5,525 assertions, and `test/browser/sequence.mjs` drives the editor through create, hold, explicit overflow and undo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:20:23 -04:00
Crossing a source cut must not silently redirect an active drawing edit to a
different symbol: retain the explicit content target until navigation changes it.
Core commands include new drawing, reuse drawing, duplicate drawing, make unique,
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
blank range, split, trim, move, extend cel, slip source, retime, and apply a
An occurrence is a node, with a clock of its own A lane's drawings were going to be one instance whose source was a KEYED channel: frame 0 says `:drawing-a`, frame 4 says `:drawing-b`, and the cels of a row are that channel's keys. Two things followed from it, and both were wrong. The first is that playback meant whichever shape the channel happened to have. A framed source played its symbol; a keyed source froze the selected frame. So `node/placed-at` read animation out of storage, and adding an ordinary key to a still turned it into an animation — the last-key bug, which was not a bug in the code so much as the rule working as written. But WHICH drawing is used and HOW time runs inside it are independent questions, and all four combinations are ordinary: hold one drawing, play one animation, cut between held drawings, cut between playing ones. So an occurrence names one symbol in `:source {:symbol ...}` and says how its source time advances in `:playback {:in :speed :end}` — `source = in + speed * f`, a hold being speed 0, with `:stop`, `:hold` or `:loop` at the end named rather than guessed. `node/placed-frame` samples it forwards, which works for holds too, and `node/source-time` is the separate, invertible edit map, nil where inversion is meaningless. The two were one function before, and a hold had to lie about one of them. The second is that a keyed source only looked necessary because an occurrence was assumed to need a ROW. It does not. A lane is a group with `:layout :sequence`, its occurrences are ordinary instances in the same flat node map, and `timeline/rows` draws them as cel blocks on the lane's own row: twelve exposures, one row, each cel still separately selectable and addressable. The vertical growth that justified the keyed source is a presentation question, and it is answered in the view. `arthur.domain.sequence` holds the first commands over that shape — add lane, append drawing, extend hold — each one history step, each refusing rather than half-applying. Extending a hold leaves the lane's keys at their authored times, because you are adjusting drawings underneath timed motion; a correction owned by an occurrence travels with it. Ownership does that work, so no key needs a flag saying what it follows. Ripple past the symbol's end is refused with the frame count it would need, and `:extent :grow-symbol` is the caller saying yes. `clip/blank` no longer carries `:subjects {} :features {} :groups {}`. Empty maps write no leaf, so a blank document could not survive its own round trip — `leaf/leaves` promises exactness and was the only honest side of that. Documents are schema 3. A version 2 document is not read; nothing here converts one. `docs/lane-model.md` is the design, and says which of its parts are built. 392 tests, 5,525 assertions, and `test/browser/sequence.mjs` drives the editor through create, hold, explicit overflow and undo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:20:23 -04:00
bounded property edit. Ripple/overwrite policy and the set of affected lanes are
explicit command arguments. Preview consequences before committing a gesture.
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
New drawing creates fresh empty content and a cel. Blank range removes
An occurrence is a node, with a clock of its own A lane's drawings were going to be one instance whose source was a KEYED channel: frame 0 says `:drawing-a`, frame 4 says `:drawing-b`, and the cels of a row are that channel's keys. Two things followed from it, and both were wrong. The first is that playback meant whichever shape the channel happened to have. A framed source played its symbol; a keyed source froze the selected frame. So `node/placed-at` read animation out of storage, and adding an ordinary key to a still turned it into an animation — the last-key bug, which was not a bug in the code so much as the rule working as written. But WHICH drawing is used and HOW time runs inside it are independent questions, and all four combinations are ordinary: hold one drawing, play one animation, cut between held drawings, cut between playing ones. So an occurrence names one symbol in `:source {:symbol ...}` and says how its source time advances in `:playback {:in :speed :end}` — `source = in + speed * f`, a hold being speed 0, with `:stop`, `:hold` or `:loop` at the end named rather than guessed. `node/placed-frame` samples it forwards, which works for holds too, and `node/source-time` is the separate, invertible edit map, nil where inversion is meaningless. The two were one function before, and a hold had to lie about one of them. The second is that a keyed source only looked necessary because an occurrence was assumed to need a ROW. It does not. A lane is a group with `:layout :sequence`, its occurrences are ordinary instances in the same flat node map, and `timeline/rows` draws them as cel blocks on the lane's own row: twelve exposures, one row, each cel still separately selectable and addressable. The vertical growth that justified the keyed source is a presentation question, and it is answered in the view. `arthur.domain.sequence` holds the first commands over that shape — add lane, append drawing, extend hold — each one history step, each refusing rather than half-applying. Extending a hold leaves the lane's keys at their authored times, because you are adjusting drawings underneath timed motion; a correction owned by an occurrence travels with it. Ownership does that work, so no key needs a flag saying what it follows. Ripple past the symbol's end is refused with the frame count it would need, and `:extent :grow-symbol` is the caller saying yes. `clip/blank` no longer carries `:subjects {} :features {} :groups {}`. Empty maps write no leaf, so a blank document could not survive its own round trip — `leaf/leaves` promises exactness and was the only honest side of that. Documents are schema 3. A version 2 document is not read; nothing here converts one. `docs/lane-model.md` is the design, and says which of its parts are built. 392 tests, 5,525 assertions, and `test/browser/sequence.mjs` drives the editor through create, hold, explicit overflow and undo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:20:23 -04:00
content coverage without inventing a hidden drawing. These are different actions.
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
Reuse creates another cel pointing at existing content. Duplicate creates
a new content identity. Make unique rebinds the selected cel only.
An occurrence is a node, with a clock of its own A lane's drawings were going to be one instance whose source was a KEYED channel: frame 0 says `:drawing-a`, frame 4 says `:drawing-b`, and the cels of a row are that channel's keys. Two things followed from it, and both were wrong. The first is that playback meant whichever shape the channel happened to have. A framed source played its symbol; a keyed source froze the selected frame. So `node/placed-at` read animation out of storage, and adding an ordinary key to a still turned it into an animation — the last-key bug, which was not a bug in the code so much as the rule working as written. But WHICH drawing is used and HOW time runs inside it are independent questions, and all four combinations are ordinary: hold one drawing, play one animation, cut between held drawings, cut between playing ones. So an occurrence names one symbol in `:source {:symbol ...}` and says how its source time advances in `:playback {:in :speed :end}` — `source = in + speed * f`, a hold being speed 0, with `:stop`, `:hold` or `:loop` at the end named rather than guessed. `node/placed-frame` samples it forwards, which works for holds too, and `node/source-time` is the separate, invertible edit map, nil where inversion is meaningless. The two were one function before, and a hold had to lie about one of them. The second is that a keyed source only looked necessary because an occurrence was assumed to need a ROW. It does not. A lane is a group with `:layout :sequence`, its occurrences are ordinary instances in the same flat node map, and `timeline/rows` draws them as cel blocks on the lane's own row: twelve exposures, one row, each cel still separately selectable and addressable. The vertical growth that justified the keyed source is a presentation question, and it is answered in the view. `arthur.domain.sequence` holds the first commands over that shape — add lane, append drawing, extend hold — each one history step, each refusing rather than half-applying. Extending a hold leaves the lane's keys at their authored times, because you are adjusting drawings underneath timed motion; a correction owned by an occurrence travels with it. Ownership does that work, so no key needs a flag saying what it follows. Ripple past the symbol's end is refused with the frame count it would need, and `:extent :grow-symbol` is the caller saying yes. `clip/blank` no longer carries `:subjects {} :features {} :groups {}`. Empty maps write no leaf, so a blank document could not survive its own round trip — `leaf/leaves` promises exactness and was the only honest side of that. Documents are schema 3. A version 2 document is not read; nothing here converts one. `docs/lane-model.md` is the design, and says which of its parts are built. 392 tests, 5,525 assertions, and `test/browser/sequence.mjs` drives the editor through create, hold, explicit overflow and undo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:20:23 -04:00
Copy semantics must specify nested sharing. A normal content copy duplicates its
owned nodes and channels while preserving references to other reusable symbols.
For a fully independent drawing assembled from nested symbols, provide an
explicit deep-copy operation with ID remapping. Never promise decoupling while
leaving the relevant edited object shared. Immutable media blocks may remain shared.
Commands are atomic undo transactions, even when they touch several leaves.
Pointer movement and keyboard invocation use explicit begin/preview/commit or
cancel boundaries; a timing heuristic alone must not decide user intent.
Collaboration applies a transaction consistently, validates affected references,
and detects conflicts at the owned data being changed. One leaf per channel does
not solve simultaneous edits to different keys of that same channel; define a
conflict policy rather than claiming that granularity solves all collaboration.
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
### Default timing behavior: cel edits preserve lane keys
An occurrence is a node, with a clock of its own A lane's drawings were going to be one instance whose source was a KEYED channel: frame 0 says `:drawing-a`, frame 4 says `:drawing-b`, and the cels of a row are that channel's keys. Two things followed from it, and both were wrong. The first is that playback meant whichever shape the channel happened to have. A framed source played its symbol; a keyed source froze the selected frame. So `node/placed-at` read animation out of storage, and adding an ordinary key to a still turned it into an animation — the last-key bug, which was not a bug in the code so much as the rule working as written. But WHICH drawing is used and HOW time runs inside it are independent questions, and all four combinations are ordinary: hold one drawing, play one animation, cut between held drawings, cut between playing ones. So an occurrence names one symbol in `:source {:symbol ...}` and says how its source time advances in `:playback {:in :speed :end}` — `source = in + speed * f`, a hold being speed 0, with `:stop`, `:hold` or `:loop` at the end named rather than guessed. `node/placed-frame` samples it forwards, which works for holds too, and `node/source-time` is the separate, invertible edit map, nil where inversion is meaningless. The two were one function before, and a hold had to lie about one of them. The second is that a keyed source only looked necessary because an occurrence was assumed to need a ROW. It does not. A lane is a group with `:layout :sequence`, its occurrences are ordinary instances in the same flat node map, and `timeline/rows` draws them as cel blocks on the lane's own row: twelve exposures, one row, each cel still separately selectable and addressable. The vertical growth that justified the keyed source is a presentation question, and it is answered in the view. `arthur.domain.sequence` holds the first commands over that shape — add lane, append drawing, extend hold — each one history step, each refusing rather than half-applying. Extending a hold leaves the lane's keys at their authored times, because you are adjusting drawings underneath timed motion; a correction owned by an occurrence travels with it. Ownership does that work, so no key needs a flag saying what it follows. Ripple past the symbol's end is refused with the frame count it would need, and `:extent :grow-symbol` is the caller saying yes. `clip/blank` no longer carries `:subjects {} :features {} :groups {}`. Empty maps write no leaf, so a blank document could not survive its own round trip — `leaf/leaves` promises exactness and was the only honest side of that. Documents are schema 3. A version 2 document is not read; nothing here converts one. `docs/lane-model.md` is the design, and says which of its parts are built. 392 tests, 5,525 assertions, and `test/browser/sequence.mjs` drives the editor through create, hold, explicit overflow and undo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:20:23 -04:00
Working default from the follow-up discussion: extending a drawing's hold changes
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
cel timing, leaving lane animation at its authored times. The user raised
An occurrence is a node, with a clock of its own A lane's drawings were going to be one instance whose source was a KEYED channel: frame 0 says `:drawing-a`, frame 4 says `:drawing-b`, and the cels of a row are that channel's keys. Two things followed from it, and both were wrong. The first is that playback meant whichever shape the channel happened to have. A framed source played its symbol; a keyed source froze the selected frame. So `node/placed-at` read animation out of storage, and adding an ordinary key to a still turned it into an animation — the last-key bug, which was not a bug in the code so much as the rule working as written. But WHICH drawing is used and HOW time runs inside it are independent questions, and all four combinations are ordinary: hold one drawing, play one animation, cut between held drawings, cut between playing ones. So an occurrence names one symbol in `:source {:symbol ...}` and says how its source time advances in `:playback {:in :speed :end}` — `source = in + speed * f`, a hold being speed 0, with `:stop`, `:hold` or `:loop` at the end named rather than guessed. `node/placed-frame` samples it forwards, which works for holds too, and `node/source-time` is the separate, invertible edit map, nil where inversion is meaningless. The two were one function before, and a hold had to lie about one of them. The second is that a keyed source only looked necessary because an occurrence was assumed to need a ROW. It does not. A lane is a group with `:layout :sequence`, its occurrences are ordinary instances in the same flat node map, and `timeline/rows` draws them as cel blocks on the lane's own row: twelve exposures, one row, each cel still separately selectable and addressable. The vertical growth that justified the keyed source is a presentation question, and it is answered in the view. `arthur.domain.sequence` holds the first commands over that shape — add lane, append drawing, extend hold — each one history step, each refusing rather than half-applying. Extending a hold leaves the lane's keys at their authored times, because you are adjusting drawings underneath timed motion; a correction owned by an occurrence travels with it. Ownership does that work, so no key needs a flag saying what it follows. Ripple past the symbol's end is refused with the frame count it would need, and `:extent :grow-symbol` is the caller saying yes. `clip/blank` no longer carries `:subjects {} :features {} :groups {}`. Empty maps write no leaf, so a blank document could not survive its own round trip — `leaf/leaves` promises exactness and was the only honest side of that. Documents are schema 3. A version 2 document is not read; nothing here converts one. `docs/lane-model.md` is the design, and says which of its parts are built. 392 tests, 5,525 assertions, and `test/browser/sequence.mjs` drives the editor through create, hold, explicit overflow and undo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:20:23 -04:00
keeping keyframes in place as a possibility; this is the proposed predictable
default, not a claim that they selected every timing policy below.
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
Ownership supplies the remaining rule: properties attached to a cel
An occurrence is a node, with a clock of its own A lane's drawings were going to be one instance whose source was a KEYED channel: frame 0 says `:drawing-a`, frame 4 says `:drawing-b`, and the cels of a row are that channel's keys. Two things followed from it, and both were wrong. The first is that playback meant whichever shape the channel happened to have. A framed source played its symbol; a keyed source froze the selected frame. So `node/placed-at` read animation out of storage, and adding an ordinary key to a still turned it into an animation — the last-key bug, which was not a bug in the code so much as the rule working as written. But WHICH drawing is used and HOW time runs inside it are independent questions, and all four combinations are ordinary: hold one drawing, play one animation, cut between held drawings, cut between playing ones. So an occurrence names one symbol in `:source {:symbol ...}` and says how its source time advances in `:playback {:in :speed :end}` — `source = in + speed * f`, a hold being speed 0, with `:stop`, `:hold` or `:loop` at the end named rather than guessed. `node/placed-frame` samples it forwards, which works for holds too, and `node/source-time` is the separate, invertible edit map, nil where inversion is meaningless. The two were one function before, and a hold had to lie about one of them. The second is that a keyed source only looked necessary because an occurrence was assumed to need a ROW. It does not. A lane is a group with `:layout :sequence`, its occurrences are ordinary instances in the same flat node map, and `timeline/rows` draws them as cel blocks on the lane's own row: twelve exposures, one row, each cel still separately selectable and addressable. The vertical growth that justified the keyed source is a presentation question, and it is answered in the view. `arthur.domain.sequence` holds the first commands over that shape — add lane, append drawing, extend hold — each one history step, each refusing rather than half-applying. Extending a hold leaves the lane's keys at their authored times, because you are adjusting drawings underneath timed motion; a correction owned by an occurrence travels with it. Ownership does that work, so no key needs a flag saying what it follows. Ripple past the symbol's end is refused with the frame count it would need, and `:extent :grow-symbol` is the caller saying yes. `clip/blank` no longer carries `:subjects {} :features {} :groups {}`. Empty maps write no leaf, so a blank document could not survive its own round trip — `leaf/leaves` promises exactness and was the only honest side of that. Documents are schema 3. A version 2 document is not read; nothing here converts one. `docs/lane-model.md` is the design, and says which of its parts are built. 392 tests, 5,525 assertions, and `test/browser/sequence.mjs` drives the editor through create, hold, explicit overflow and undo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:20:23 -04:00
travel with it. Extending its end does not stretch those properties; moving it
changes where their existing local times land. No per-key attachment flag is
needed to recover ownership that the document already expresses.
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
For the concrete example, extend `:cel-a` by two lane frames with ripple:
An occurrence is a node, with a clock of its own A lane's drawings were going to be one instance whose source was a KEYED channel: frame 0 says `:drawing-a`, frame 4 says `:drawing-b`, and the cels of a row are that channel's keys. Two things followed from it, and both were wrong. The first is that playback meant whichever shape the channel happened to have. A framed source played its symbol; a keyed source froze the selected frame. So `node/placed-at` read animation out of storage, and adding an ordinary key to a still turned it into an animation — the last-key bug, which was not a bug in the code so much as the rule working as written. But WHICH drawing is used and HOW time runs inside it are independent questions, and all four combinations are ordinary: hold one drawing, play one animation, cut between held drawings, cut between playing ones. So an occurrence names one symbol in `:source {:symbol ...}` and says how its source time advances in `:playback {:in :speed :end}` — `source = in + speed * f`, a hold being speed 0, with `:stop`, `:hold` or `:loop` at the end named rather than guessed. `node/placed-frame` samples it forwards, which works for holds too, and `node/source-time` is the separate, invertible edit map, nil where inversion is meaningless. The two were one function before, and a hold had to lie about one of them. The second is that a keyed source only looked necessary because an occurrence was assumed to need a ROW. It does not. A lane is a group with `:layout :sequence`, its occurrences are ordinary instances in the same flat node map, and `timeline/rows` draws them as cel blocks on the lane's own row: twelve exposures, one row, each cel still separately selectable and addressable. The vertical growth that justified the keyed source is a presentation question, and it is answered in the view. `arthur.domain.sequence` holds the first commands over that shape — add lane, append drawing, extend hold — each one history step, each refusing rather than half-applying. Extending a hold leaves the lane's keys at their authored times, because you are adjusting drawings underneath timed motion; a correction owned by an occurrence travels with it. Ownership does that work, so no key needs a flag saying what it follows. Ripple past the symbol's end is refused with the frame count it would need, and `:extent :grow-symbol` is the caller saying yes. `clip/blank` no longer carries `:subjects {} :features {} :groups {}`. Empty maps write no leaf, so a blank document could not survive its own round trip — `leaf/leaves` promises exactness and was the only honest side of that. Documents are schema 3. A version 2 document is not read; nothing here converts one. `docs/lane-model.md` is the design, and says which of its parts are built. 392 tests, 5,525 assertions, and `test/browser/sequence.mjs` drives the editor through create, hold, explicit overflow and undo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:20:23 -04:00
| Fact | Before | After |
| --- | --- | --- |
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
| Cel A's lane interval | `[0,4)` | `[0,6)` |
| Cel B's lane interval | `[4,8)` | `[6,10)` |
An occurrence is a node, with a clock of its own A lane's drawings were going to be one instance whose source was a KEYED channel: frame 0 says `:drawing-a`, frame 4 says `:drawing-b`, and the cels of a row are that channel's keys. Two things followed from it, and both were wrong. The first is that playback meant whichever shape the channel happened to have. A framed source played its symbol; a keyed source froze the selected frame. So `node/placed-at` read animation out of storage, and adding an ordinary key to a still turned it into an animation — the last-key bug, which was not a bug in the code so much as the rule working as written. But WHICH drawing is used and HOW time runs inside it are independent questions, and all four combinations are ordinary: hold one drawing, play one animation, cut between held drawings, cut between playing ones. So an occurrence names one symbol in `:source {:symbol ...}` and says how its source time advances in `:playback {:in :speed :end}` — `source = in + speed * f`, a hold being speed 0, with `:stop`, `:hold` or `:loop` at the end named rather than guessed. `node/placed-frame` samples it forwards, which works for holds too, and `node/source-time` is the separate, invertible edit map, nil where inversion is meaningless. The two were one function before, and a hold had to lie about one of them. The second is that a keyed source only looked necessary because an occurrence was assumed to need a ROW. It does not. A lane is a group with `:layout :sequence`, its occurrences are ordinary instances in the same flat node map, and `timeline/rows` draws them as cel blocks on the lane's own row: twelve exposures, one row, each cel still separately selectable and addressable. The vertical growth that justified the keyed source is a presentation question, and it is answered in the view. `arthur.domain.sequence` holds the first commands over that shape — add lane, append drawing, extend hold — each one history step, each refusing rather than half-applying. Extending a hold leaves the lane's keys at their authored times, because you are adjusting drawings underneath timed motion; a correction owned by an occurrence travels with it. Ownership does that work, so no key needs a flag saying what it follows. Ripple past the symbol's end is refused with the frame count it would need, and `:extent :grow-symbol` is the caller saying yes. `clip/blank` no longer carries `:subjects {} :features {} :groups {}`. Empty maps write no leaf, so a blank document could not survive its own round trip — `leaf/leaves` promises exactness and was the only honest side of that. Documents are schema 3. A version 2 document is not read; nothing here converts one. `docs/lane-model.md` is the design, and says which of its parts are built. 392 tests, 5,525 assertions, and `test/browser/sequence.mjs` drives the editor through create, hold, explicit overflow and undo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:20:23 -04:00
| Animated insert's lane interval | `[8,12)` | `[10,14)` |
| Girl's rotation peak | Lane frame 6 | Lane frame 6 |
| B's position change | B frame 1, lane frame 5 | B frame 1, lane frame 7 |
| Insert's first source frame | Source frame 3 | Source frame 3 |
The rotation peak now coincides with a different point in the drawing sequence.
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
That is the intended consequence of changing cels underneath timed motion.
The position correction stays attached to drawing B's cel. Neither the
An occurrence is a node, with a clock of its own A lane's drawings were going to be one instance whose source was a KEYED channel: frame 0 says `:drawing-a`, frame 4 says `:drawing-b`, and the cels of a row are that channel's keys. Two things followed from it, and both were wrong. The first is that playback meant whichever shape the channel happened to have. A framed source played its symbol; a keyed source froze the selected frame. So `node/placed-at` read animation out of storage, and adding an ordinary key to a still turned it into an animation — the last-key bug, which was not a bug in the code so much as the rule working as written. But WHICH drawing is used and HOW time runs inside it are independent questions, and all four combinations are ordinary: hold one drawing, play one animation, cut between held drawings, cut between playing ones. So an occurrence names one symbol in `:source {:symbol ...}` and says how its source time advances in `:playback {:in :speed :end}` — `source = in + speed * f`, a hold being speed 0, with `:stop`, `:hold` or `:loop` at the end named rather than guessed. `node/placed-frame` samples it forwards, which works for holds too, and `node/source-time` is the separate, invertible edit map, nil where inversion is meaningless. The two were one function before, and a hold had to lie about one of them. The second is that a keyed source only looked necessary because an occurrence was assumed to need a ROW. It does not. A lane is a group with `:layout :sequence`, its occurrences are ordinary instances in the same flat node map, and `timeline/rows` draws them as cel blocks on the lane's own row: twelve exposures, one row, each cel still separately selectable and addressable. The vertical growth that justified the keyed source is a presentation question, and it is answered in the view. `arthur.domain.sequence` holds the first commands over that shape — add lane, append drawing, extend hold — each one history step, each refusing rather than half-applying. Extending a hold leaves the lane's keys at their authored times, because you are adjusting drawings underneath timed motion; a correction owned by an occurrence travels with it. Ownership does that work, so no key needs a flag saying what it follows. Ripple past the symbol's end is refused with the frame count it would need, and `:extent :grow-symbol` is the caller saying yes. `clip/blank` no longer carries `:subjects {} :features {} :groups {}`. Empty maps write no leaf, so a blank document could not survive its own round trip — `leaf/leaves` promises exactness and was the only honest side of that. Documents are schema 3. A version 2 document is not read; nothing here converts one. `docs/lane-model.md` is the design, and says which of its parts are built. 392 tests, 5,525 assertions, and `test/browser/sequence.mjs` drives the editor through create, hold, explicit overflow and undo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:20:23 -04:00
background's keys nor audio on another lane moves.
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
The command contract for this edit names the symbol and cel, a delta in
lane frames, `:ripple` behavior, and an explicit scope of cel timing. It
An occurrence is a node, with a clock of its own A lane's drawings were going to be one instance whose source was a KEYED channel: frame 0 says `:drawing-a`, frame 4 says `:drawing-b`, and the cels of a row are that channel's keys. Two things followed from it, and both were wrong. The first is that playback meant whichever shape the channel happened to have. A framed source played its symbol; a keyed source froze the selected frame. So `node/placed-at` read animation out of storage, and adding an ordinary key to a still turned it into an animation — the last-key bug, which was not a bug in the code so much as the rule working as written. But WHICH drawing is used and HOW time runs inside it are independent questions, and all four combinations are ordinary: hold one drawing, play one animation, cut between held drawings, cut between playing ones. So an occurrence names one symbol in `:source {:symbol ...}` and says how its source time advances in `:playback {:in :speed :end}` — `source = in + speed * f`, a hold being speed 0, with `:stop`, `:hold` or `:loop` at the end named rather than guessed. `node/placed-frame` samples it forwards, which works for holds too, and `node/source-time` is the separate, invertible edit map, nil where inversion is meaningless. The two were one function before, and a hold had to lie about one of them. The second is that a keyed source only looked necessary because an occurrence was assumed to need a ROW. It does not. A lane is a group with `:layout :sequence`, its occurrences are ordinary instances in the same flat node map, and `timeline/rows` draws them as cel blocks on the lane's own row: twelve exposures, one row, each cel still separately selectable and addressable. The vertical growth that justified the keyed source is a presentation question, and it is answered in the view. `arthur.domain.sequence` holds the first commands over that shape — add lane, append drawing, extend hold — each one history step, each refusing rather than half-applying. Extending a hold leaves the lane's keys at their authored times, because you are adjusting drawings underneath timed motion; a correction owned by an occurrence travels with it. Ownership does that work, so no key needs a flag saying what it follows. Ripple past the symbol's end is refused with the frame count it would need, and `:extent :grow-symbol` is the caller saying yes. `clip/blank` no longer carries `:subjects {} :features {} :groups {}`. Empty maps write no leaf, so a blank document could not survive its own round trip — `leaf/leaves` promises exactness and was the only honest side of that. Documents are schema 3. A version 2 document is not read; nothing here converts one. `docs/lane-model.md` is the design, and says which of its parts are built. 392 tests, 5,525 assertions, and `test/browser/sequence.mjs` drives the editor through create, hold, explicit overflow and undo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:20:23 -04:00
extends A's local support by the delta converted through A's placement rate,
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
and shifts subsequent cel placements by that delta in lane time. It does
An occurrence is a node, with a clock of its own A lane's drawings were going to be one instance whose source was a KEYED channel: frame 0 says `:drawing-a`, frame 4 says `:drawing-b`, and the cels of a row are that channel's keys. Two things followed from it, and both were wrong. The first is that playback meant whichever shape the channel happened to have. A framed source played its symbol; a keyed source froze the selected frame. So `node/placed-at` read animation out of storage, and adding an ordinary key to a still turned it into an animation — the last-key bug, which was not a bug in the code so much as the rule working as written. But WHICH drawing is used and HOW time runs inside it are independent questions, and all four combinations are ordinary: hold one drawing, play one animation, cut between held drawings, cut between playing ones. So an occurrence names one symbol in `:source {:symbol ...}` and says how its source time advances in `:playback {:in :speed :end}` — `source = in + speed * f`, a hold being speed 0, with `:stop`, `:hold` or `:loop` at the end named rather than guessed. `node/placed-frame` samples it forwards, which works for holds too, and `node/source-time` is the separate, invertible edit map, nil where inversion is meaningless. The two were one function before, and a hold had to lie about one of them. The second is that a keyed source only looked necessary because an occurrence was assumed to need a ROW. It does not. A lane is a group with `:layout :sequence`, its occurrences are ordinary instances in the same flat node map, and `timeline/rows` draws them as cel blocks on the lane's own row: twelve exposures, one row, each cel still separately selectable and addressable. The vertical growth that justified the keyed source is a presentation question, and it is answered in the view. `arthur.domain.sequence` holds the first commands over that shape — add lane, append drawing, extend hold — each one history step, each refusing rather than half-applying. Extending a hold leaves the lane's keys at their authored times, because you are adjusting drawings underneath timed motion; a correction owned by an occurrence travels with it. Ownership does that work, so no key needs a flag saying what it follows. Ripple past the symbol's end is refused with the frame count it would need, and `:extent :grow-symbol` is the caller saying yes. `clip/blank` no longer carries `:subjects {} :features {} :groups {}`. Empty maps write no leaf, so a blank document could not survive its own round trip — `leaf/leaves` promises exactness and was the only honest side of that. Documents are schema 3. A version 2 document is not read; nothing here converts one. `docs/lane-model.md` is the design, and says which of its parts are built. 392 tests, 5,525 assertions, and `test/browser/sequence.mjs` drives the editor through create, hold, explicit overflow and undo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:20:23 -04:00
not modify any channel's key map, source in-point, or playback speed. Reject a
nonpositive resulting duration. Validate and commit the entire change together.
The symbol's authored end is another explicit boundary: preview an overflow and
offer to extend the symbol or cancel. A command can request that extension as
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
part of its transaction; it must not silently truncate later cels or grow
An occurrence is a node, with a clock of its own A lane's drawings were going to be one instance whose source was a KEYED channel: frame 0 says `:drawing-a`, frame 4 says `:drawing-b`, and the cels of a row are that channel's keys. Two things followed from it, and both were wrong. The first is that playback meant whichever shape the channel happened to have. A framed source played its symbol; a keyed source froze the selected frame. So `node/placed-at` read animation out of storage, and adding an ordinary key to a still turned it into an animation — the last-key bug, which was not a bug in the code so much as the rule working as written. But WHICH drawing is used and HOW time runs inside it are independent questions, and all four combinations are ordinary: hold one drawing, play one animation, cut between held drawings, cut between playing ones. So an occurrence names one symbol in `:source {:symbol ...}` and says how its source time advances in `:playback {:in :speed :end}` — `source = in + speed * f`, a hold being speed 0, with `:stop`, `:hold` or `:loop` at the end named rather than guessed. `node/placed-frame` samples it forwards, which works for holds too, and `node/source-time` is the separate, invertible edit map, nil where inversion is meaningless. The two were one function before, and a hold had to lie about one of them. The second is that a keyed source only looked necessary because an occurrence was assumed to need a ROW. It does not. A lane is a group with `:layout :sequence`, its occurrences are ordinary instances in the same flat node map, and `timeline/rows` draws them as cel blocks on the lane's own row: twelve exposures, one row, each cel still separately selectable and addressable. The vertical growth that justified the keyed source is a presentation question, and it is answered in the view. `arthur.domain.sequence` holds the first commands over that shape — add lane, append drawing, extend hold — each one history step, each refusing rather than half-applying. Extending a hold leaves the lane's keys at their authored times, because you are adjusting drawings underneath timed motion; a correction owned by an occurrence travels with it. Ownership does that work, so no key needs a flag saying what it follows. Ripple past the symbol's end is refused with the frame count it would need, and `:extent :grow-symbol` is the caller saying yes. `clip/blank` no longer carries `:subjects {} :features {} :groups {}`. Empty maps write no leaf, so a blank document could not survive its own round trip — `leaf/leaves` promises exactness and was the only honest side of that. Documents are schema 3. A version 2 document is not read; nothing here converts one. `docs/lane-model.md` is the design, and says which of its parts are built. 392 tests, 5,525 assertions, and `test/browser/sequence.mjs` drives the editor through create, hold, explicit overflow and undo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:20:23 -04:00
other uses of a shared symbol. In the example, a 12-frame symbol needs an explicit
extension to 14 frames or the edit must be refused without partial changes.
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
Retime performance is a separate operation over explicitly selected cels
An occurrence is a node, with a clock of its own A lane's drawings were going to be one instance whose source was a KEYED channel: frame 0 says `:drawing-a`, frame 4 says `:drawing-b`, and the cels of a row are that channel's keys. Two things followed from it, and both were wrong. The first is that playback meant whichever shape the channel happened to have. A framed source played its symbol; a keyed source froze the selected frame. So `node/placed-at` read animation out of storage, and adding an ordinary key to a still turned it into an animation — the last-key bug, which was not a bug in the code so much as the rule working as written. But WHICH drawing is used and HOW time runs inside it are independent questions, and all four combinations are ordinary: hold one drawing, play one animation, cut between held drawings, cut between playing ones. So an occurrence names one symbol in `:source {:symbol ...}` and says how its source time advances in `:playback {:in :speed :end}` — `source = in + speed * f`, a hold being speed 0, with `:stop`, `:hold` or `:loop` at the end named rather than guessed. `node/placed-frame` samples it forwards, which works for holds too, and `node/source-time` is the separate, invertible edit map, nil where inversion is meaningless. The two were one function before, and a hold had to lie about one of them. The second is that a keyed source only looked necessary because an occurrence was assumed to need a ROW. It does not. A lane is a group with `:layout :sequence`, its occurrences are ordinary instances in the same flat node map, and `timeline/rows` draws them as cel blocks on the lane's own row: twelve exposures, one row, each cel still separately selectable and addressable. The vertical growth that justified the keyed source is a presentation question, and it is answered in the view. `arthur.domain.sequence` holds the first commands over that shape — add lane, append drawing, extend hold — each one history step, each refusing rather than half-applying. Extending a hold leaves the lane's keys at their authored times, because you are adjusting drawings underneath timed motion; a correction owned by an occurrence travels with it. Ownership does that work, so no key needs a flag saying what it follows. Ripple past the symbol's end is refused with the frame count it would need, and `:extent :grow-symbol` is the caller saying yes. `clip/blank` no longer carries `:subjects {} :features {} :groups {}`. Empty maps write no leaf, so a blank document could not survive its own round trip — `leaf/leaves` promises exactness and was the only honest side of that. Documents are schema 3. A version 2 document is not read; nothing here converts one. `docs/lane-model.md` is the design, and says which of its parts are built. 392 tests, 5,525 assertions, and `test/browser/sequence.mjs` drives the editor through create, hold, explicit overflow and undo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:20:23 -04:00
and channels. It applies the same time transformation to their relevant clocks,
keys, and correction supports. Stretching an interval requires a defined warp
and interpolation behavior; it is not merely moving keys whose frame numbers
happen to lie inside the selection. Until supported, refuse this operation
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
rather than approximating it with a cel ripple.
An occurrence is a node, with a clock of its own A lane's drawings were going to be one instance whose source was a KEYED channel: frame 0 says `:drawing-a`, frame 4 says `:drawing-b`, and the cels of a row are that channel's keys. Two things followed from it, and both were wrong. The first is that playback meant whichever shape the channel happened to have. A framed source played its symbol; a keyed source froze the selected frame. So `node/placed-at` read animation out of storage, and adding an ordinary key to a still turned it into an animation — the last-key bug, which was not a bug in the code so much as the rule working as written. But WHICH drawing is used and HOW time runs inside it are independent questions, and all four combinations are ordinary: hold one drawing, play one animation, cut between held drawings, cut between playing ones. So an occurrence names one symbol in `:source {:symbol ...}` and says how its source time advances in `:playback {:in :speed :end}` — `source = in + speed * f`, a hold being speed 0, with `:stop`, `:hold` or `:loop` at the end named rather than guessed. `node/placed-frame` samples it forwards, which works for holds too, and `node/source-time` is the separate, invertible edit map, nil where inversion is meaningless. The two were one function before, and a hold had to lie about one of them. The second is that a keyed source only looked necessary because an occurrence was assumed to need a ROW. It does not. A lane is a group with `:layout :sequence`, its occurrences are ordinary instances in the same flat node map, and `timeline/rows` draws them as cel blocks on the lane's own row: twelve exposures, one row, each cel still separately selectable and addressable. The vertical growth that justified the keyed source is a presentation question, and it is answered in the view. `arthur.domain.sequence` holds the first commands over that shape — add lane, append drawing, extend hold — each one history step, each refusing rather than half-applying. Extending a hold leaves the lane's keys at their authored times, because you are adjusting drawings underneath timed motion; a correction owned by an occurrence travels with it. Ownership does that work, so no key needs a flag saying what it follows. Ripple past the symbol's end is refused with the frame count it would need, and `:extent :grow-symbol` is the caller saying yes. `clip/blank` no longer carries `:subjects {} :features {} :groups {}`. Empty maps write no leaf, so a blank document could not survive its own round trip — `leaf/leaves` promises exactness and was the only honest side of that. Documents are schema 3. A version 2 document is not read; nothing here converts one. `docs/lane-model.md` is the design, and says which of its parts are built. 392 tests, 5,525 assertions, and `test/browser/sequence.mjs` drives the editor through create, hold, explicit overflow and undo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:20:23 -04:00
The initial UI should default stage transforms to the lane when drawing in a cel
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
workflow, so movement usually remains independent of cel timing. The
inspector names the target: lane motion, this cel, or shared drawing.
An occurrence is a node, with a clock of its own A lane's drawings were going to be one instance whose source was a KEYED channel: frame 0 says `:drawing-a`, frame 4 says `:drawing-b`, and the cels of a row are that channel's keys. Two things followed from it, and both were wrong. The first is that playback meant whichever shape the channel happened to have. A framed source played its symbol; a keyed source froze the selected frame. So `node/placed-at` read animation out of storage, and adding an ordinary key to a still turned it into an animation — the last-key bug, which was not a bug in the code so much as the rule working as written. But WHICH drawing is used and HOW time runs inside it are independent questions, and all four combinations are ordinary: hold one drawing, play one animation, cut between held drawings, cut between playing ones. So an occurrence names one symbol in `:source {:symbol ...}` and says how its source time advances in `:playback {:in :speed :end}` — `source = in + speed * f`, a hold being speed 0, with `:stop`, `:hold` or `:loop` at the end named rather than guessed. `node/placed-frame` samples it forwards, which works for holds too, and `node/source-time` is the separate, invertible edit map, nil where inversion is meaningless. The two were one function before, and a hold had to lie about one of them. The second is that a keyed source only looked necessary because an occurrence was assumed to need a ROW. It does not. A lane is a group with `:layout :sequence`, its occurrences are ordinary instances in the same flat node map, and `timeline/rows` draws them as cel blocks on the lane's own row: twelve exposures, one row, each cel still separately selectable and addressable. The vertical growth that justified the keyed source is a presentation question, and it is answered in the view. `arthur.domain.sequence` holds the first commands over that shape — add lane, append drawing, extend hold — each one history step, each refusing rather than half-applying. Extending a hold leaves the lane's keys at their authored times, because you are adjusting drawings underneath timed motion; a correction owned by an occurrence travels with it. Ownership does that work, so no key needs a flag saying what it follows. Ripple past the symbol's end is refused with the frame count it would need, and `:extent :grow-symbol` is the caller saying yes. `clip/blank` no longer carries `:subjects {} :features {} :groups {}`. Empty maps write no leaf, so a blank document could not survive its own round trip — `leaf/leaves` promises exactness and was the only honest side of that. Documents are schema 3. A version 2 document is not read; nothing here converts one. `docs/lane-model.md` is the design, and says which of its parts are built. 392 tests, 5,525 assertions, and `test/browser/sequence.mjs` drives the editor through create, hold, explicit overflow and undo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:20:23 -04:00
Changing that scope is explicit. It changes what the edit means, not just which
panel happens to be open.
## Three-frame rotation and correction layers
A range says where an edit applies; it does not specify the motion. Offer distinct
commands for a constant adjustment, a ramp, and a return-to-start motion. For UI
frames 10–12, the internal range contains exactly three frame samples after
conversion from the displayed numbering convention.
- Constant adjustment: the same offset throughout those three samples.
- Ramp: interpolate from the specified start value to the target over the range.
- Return motion: interpolate from the starting value to a peak and back.
For a return motion sampled on three frames, the values can be `0, angle, 0`.
Outside the selected range, the underlying animation must evaluate exactly as it
did before. A range-scoped correction layer expresses this directly; blindly
inserting boundary keys can alter neighboring segments or destroy existing motion.
Implement corrections as an ordered stack over the base channel. Each correction
has stable identity, explicit support interval, blend operation, and values in a
named time space. Outside its support it is inactive. `replace` can supply a value
over an absent base; `offset` cannot offset a nonexistent value. Blend capability
depends on property type, and geometry corrections require compatible topology.
Regeneration replaces the generated base and preserves corrections. If changed
topology or removed targets make a correction incompatible, report a resolvable
conflict instead of silently dropping or misapplying it. Provenance explains
where the base came from; explicit sampling policy determines its playback.
This is core to the workflow: generate motion, correct it by hand, adjust the
generator, and keep the corrections. It should be proven before adding many views.
## Other unifications worth keeping
Pose choices, tracing-frame choices, and ordinary held values should share the
channel evaluator and cursor infrastructure. Preserve their different ownership,
fallback behavior, and sampling scope. A pose choice must address the relevant
content/feature explicitly; switching to another symbol must not accidentally
reuse a track just because both symbols contain a node with the same local name.
Keep the two animation idioms distinct: keyed geometry modifies one mark over
time; drawing substitution selects content that may have different structure.
Linear geometry interpolation requires compatible vertex correspondence, not
merely two drawings that happen to look related.
Derived library grouping may collect drawings used by a single lane. This is a
convenience, not ownership or deletion authority. Reference discovery for cycle
validation, copying, and deletion examines all structural references, including
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
currently inactive cels. Authored folders, favorites, and labels remain
An occurrence is a node, with a clock of its own A lane's drawings were going to be one instance whose source was a KEYED channel: frame 0 says `:drawing-a`, frame 4 says `:drawing-b`, and the cels of a row are that channel's keys. Two things followed from it, and both were wrong. The first is that playback meant whichever shape the channel happened to have. A framed source played its symbol; a keyed source froze the selected frame. So `node/placed-at` read animation out of storage, and adding an ordinary key to a still turned it into an animation — the last-key bug, which was not a bug in the code so much as the rule working as written. But WHICH drawing is used and HOW time runs inside it are independent questions, and all four combinations are ordinary: hold one drawing, play one animation, cut between held drawings, cut between playing ones. So an occurrence names one symbol in `:source {:symbol ...}` and says how its source time advances in `:playback {:in :speed :end}` — `source = in + speed * f`, a hold being speed 0, with `:stop`, `:hold` or `:loop` at the end named rather than guessed. `node/placed-frame` samples it forwards, which works for holds too, and `node/source-time` is the separate, invertible edit map, nil where inversion is meaningless. The two were one function before, and a hold had to lie about one of them. The second is that a keyed source only looked necessary because an occurrence was assumed to need a ROW. It does not. A lane is a group with `:layout :sequence`, its occurrences are ordinary instances in the same flat node map, and `timeline/rows` draws them as cel blocks on the lane's own row: twelve exposures, one row, each cel still separately selectable and addressable. The vertical growth that justified the keyed source is a presentation question, and it is answered in the view. `arthur.domain.sequence` holds the first commands over that shape — add lane, append drawing, extend hold — each one history step, each refusing rather than half-applying. Extending a hold leaves the lane's keys at their authored times, because you are adjusting drawings underneath timed motion; a correction owned by an occurrence travels with it. Ownership does that work, so no key needs a flag saying what it follows. Ripple past the symbol's end is refused with the frame count it would need, and `:extent :grow-symbol` is the caller saying yes. `clip/blank` no longer carries `:subjects {} :features {} :groups {}`. Empty maps write no leaf, so a blank document could not survive its own round trip — `leaf/leaves` promises exactness and was the only honest side of that. Documents are schema 3. A version 2 document is not read; nothing here converts one. `docs/lane-model.md` is the design, and says which of its parts are built. 392 tests, 5,525 assertions, and `test/browser/sequence.mjs` drives the editor through create, hold, explicit overflow and undo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:20:23 -04:00
legitimate user data even when the UI could have suggested defaults.
## UX: location, selection, and controls
The breadcrumb sits above the timeline and states the editing location, shared
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
content identity, and cel context when applicable. Show local time and
An occurrence is a node, with a clock of its own A lane's drawings were going to be one instance whose source was a KEYED channel: frame 0 says `:drawing-a`, frame 4 says `:drawing-b`, and the cels of a row are that channel's keys. Two things followed from it, and both were wrong. The first is that playback meant whichever shape the channel happened to have. A framed source played its symbol; a keyed source froze the selected frame. So `node/placed-at` read animation out of storage, and adding an ordinary key to a still turned it into an animation — the last-key bug, which was not a bug in the code so much as the rule working as written. But WHICH drawing is used and HOW time runs inside it are independent questions, and all four combinations are ordinary: hold one drawing, play one animation, cut between held drawings, cut between playing ones. So an occurrence names one symbol in `:source {:symbol ...}` and says how its source time advances in `:playback {:in :speed :end}` — `source = in + speed * f`, a hold being speed 0, with `:stop`, `:hold` or `:loop` at the end named rather than guessed. `node/placed-frame` samples it forwards, which works for holds too, and `node/source-time` is the separate, invertible edit map, nil where inversion is meaningless. The two were one function before, and a hold had to lie about one of them. The second is that a keyed source only looked necessary because an occurrence was assumed to need a ROW. It does not. A lane is a group with `:layout :sequence`, its occurrences are ordinary instances in the same flat node map, and `timeline/rows` draws them as cel blocks on the lane's own row: twelve exposures, one row, each cel still separately selectable and addressable. The vertical growth that justified the keyed source is a presentation question, and it is answered in the view. `arthur.domain.sequence` holds the first commands over that shape — add lane, append drawing, extend hold — each one history step, each refusing rather than half-applying. Extending a hold leaves the lane's keys at their authored times, because you are adjusting drawings underneath timed motion; a correction owned by an occurrence travels with it. Ownership does that work, so no key needs a flag saying what it follows. Ripple past the symbol's end is refused with the frame count it would need, and `:extent :grow-symbol` is the caller saying yes. `clip/blank` no longer carries `:subjects {} :features {} :groups {}`. Empty maps write no leaf, so a blank document could not survive its own round trip — `leaf/leaves` promises exactness and was the only honest side of that. Documents are schema 3. A version 2 document is not read; nothing here converts one. `docs/lane-model.md` is the design, and says which of its parts are built. 392 tests, 5,525 assertions, and `test/browser/sequence.mjs` drives the editor through create, hold, explicit overflow and undo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:20:23 -04:00
its project context where a useful mapping exists. Holds and loops need an honest
description instead of a fictitious unique global frame.
Creation controls next to the breadcrumb act in that explicit location. Selection
does not secretly change where a new symbol goes. A shared drawing indicates its
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
reuse and offers Make this cel unique. Names help identify content;
An occurrence is a node, with a clock of its own A lane's drawings were going to be one instance whose source was a KEYED channel: frame 0 says `:drawing-a`, frame 4 says `:drawing-b`, and the cels of a row are that channel's keys. Two things followed from it, and both were wrong. The first is that playback meant whichever shape the channel happened to have. A framed source played its symbol; a keyed source froze the selected frame. So `node/placed-at` read animation out of storage, and adding an ordinary key to a still turned it into an animation — the last-key bug, which was not a bug in the code so much as the rule working as written. But WHICH drawing is used and HOW time runs inside it are independent questions, and all four combinations are ordinary: hold one drawing, play one animation, cut between held drawings, cut between playing ones. So an occurrence names one symbol in `:source {:symbol ...}` and says how its source time advances in `:playback {:in :speed :end}` — `source = in + speed * f`, a hold being speed 0, with `:stop`, `:hold` or `:loop` at the end named rather than guessed. `node/placed-frame` samples it forwards, which works for holds too, and `node/source-time` is the separate, invertible edit map, nil where inversion is meaningless. The two were one function before, and a hold had to lie about one of them. The second is that a keyed source only looked necessary because an occurrence was assumed to need a ROW. It does not. A lane is a group with `:layout :sequence`, its occurrences are ordinary instances in the same flat node map, and `timeline/rows` draws them as cel blocks on the lane's own row: twelve exposures, one row, each cel still separately selectable and addressable. The vertical growth that justified the keyed source is a presentation question, and it is answered in the view. `arthur.domain.sequence` holds the first commands over that shape — add lane, append drawing, extend hold — each one history step, each refusing rather than half-applying. Extending a hold leaves the lane's keys at their authored times, because you are adjusting drawings underneath timed motion; a correction owned by an occurrence travels with it. Ownership does that work, so no key needs a flag saying what it follows. Ripple past the symbol's end is refused with the frame count it would need, and `:extent :grow-symbol` is the caller saying yes. `clip/blank` no longer carries `:subjects {} :features {} :groups {}`. Empty maps write no leaf, so a blank document could not survive its own round trip — `leaf/leaves` promises exactness and was the only honest side of that. Documents are schema 3. A version 2 document is not read; nothing here converts one. `docs/lane-model.md` is the design, and says which of its parts are built. 392 tests, 5,525 assertions, and `test/browser/sequence.mjs` drives the editor through create, hold, explicit overflow and undo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:20:23 -04:00
linked-use indicators must rely on IDs, because different drawings can share names.
| Surface | Primary scope and controls |
| --- | --- |
| Topbar | Project name, save/open/export, project rate and stage size |
| Location bar | Breadcrumb, add lane/content, shared-content context |
| Cel action strip | New drawing, duplicate drawing, hold longer/shorter, blank range |
| Lane header | Lane selection, lock, mute/solo where applicable, onion settings, expansion |
| Stage tools | Drawing and transform modes, active target and scope |
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
| Inspector | Selected content/cel/lane properties and valid key controls |
An occurrence is a node, with a clock of its own A lane's drawings were going to be one instance whose source was a KEYED channel: frame 0 says `:drawing-a`, frame 4 says `:drawing-b`, and the cels of a row are that channel's keys. Two things followed from it, and both were wrong. The first is that playback meant whichever shape the channel happened to have. A framed source played its symbol; a keyed source froze the selected frame. So `node/placed-at` read animation out of storage, and adding an ordinary key to a still turned it into an animation — the last-key bug, which was not a bug in the code so much as the rule working as written. But WHICH drawing is used and HOW time runs inside it are independent questions, and all four combinations are ordinary: hold one drawing, play one animation, cut between held drawings, cut between playing ones. So an occurrence names one symbol in `:source {:symbol ...}` and says how its source time advances in `:playback {:in :speed :end}` — `source = in + speed * f`, a hold being speed 0, with `:stop`, `:hold` or `:loop` at the end named rather than guessed. `node/placed-frame` samples it forwards, which works for holds too, and `node/source-time` is the separate, invertible edit map, nil where inversion is meaningless. The two were one function before, and a hold had to lie about one of them. The second is that a keyed source only looked necessary because an occurrence was assumed to need a ROW. It does not. A lane is a group with `:layout :sequence`, its occurrences are ordinary instances in the same flat node map, and `timeline/rows` draws them as cel blocks on the lane's own row: twelve exposures, one row, each cel still separately selectable and addressable. The vertical growth that justified the keyed source is a presentation question, and it is answered in the view. `arthur.domain.sequence` holds the first commands over that shape — add lane, append drawing, extend hold — each one history step, each refusing rather than half-applying. Extending a hold leaves the lane's keys at their authored times, because you are adjusting drawings underneath timed motion; a correction owned by an occurrence travels with it. Ownership does that work, so no key needs a flag saying what it follows. Ripple past the symbol's end is refused with the frame count it would need, and `:extent :grow-symbol` is the caller saying yes. `clip/blank` no longer carries `:subjects {} :features {} :groups {}`. Empty maps write no leaf, so a blank document could not survive its own round trip — `leaf/leaves` promises exactness and was the only honest side of that. Documents are schema 3. A version 2 document is not read; nothing here converts one. `docs/lane-model.md` is the design, and says which of its parts are built. 392 tests, 5,525 assertions, and `test/browser/sequence.mjs` drives the editor through create, hold, explicit overflow and undo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:20:23 -04:00
Cel actions have visible contextual buttons, shortcuts, a context menu, and
command-palette entries. These are different entrances to the same commands.
Shortcut names from the original sketch (`N`, `D`, `H`, `B`, `K`) are provisional;
their meanings must match the visible labels and avoid tool conflicts.
The inspector normally edits values and the timeline normally edits timing, but
this is an organizational default. Numeric duration and in-point controls are
useful inspector edits to the same domain facts. Do not ban a convenient control
just to preserve a visual division.
Default nesting navigation enters content; expanding a lane reveals properties.
Other views may show hierarchies differently without changing the document.
Tabs can pin explicit locations. Zoom, expansion, onion preferences, and current
selection are editor state rather than animation content. Persistent workspace
preferences can be saved separately.
## A session, revised
1. In `main`, create a girl lane and a new drawing. Draw; use New drawing (`N`)
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
to create the next one with the previous cel ghosted behind it.
An occurrence is a node, with a clock of its own A lane's drawings were going to be one instance whose source was a KEYED channel: frame 0 says `:drawing-a`, frame 4 says `:drawing-b`, and the cels of a row are that channel's keys. Two things followed from it, and both were wrong. The first is that playback meant whichever shape the channel happened to have. A framed source played its symbol; a keyed source froze the selected frame. So `node/placed-at` read animation out of storage, and adding an ordinary key to a still turned it into an animation — the last-key bug, which was not a bug in the code so much as the rule working as written. But WHICH drawing is used and HOW time runs inside it are independent questions, and all four combinations are ordinary: hold one drawing, play one animation, cut between held drawings, cut between playing ones. So an occurrence names one symbol in `:source {:symbol ...}` and says how its source time advances in `:playback {:in :speed :end}` — `source = in + speed * f`, a hold being speed 0, with `:stop`, `:hold` or `:loop` at the end named rather than guessed. `node/placed-frame` samples it forwards, which works for holds too, and `node/source-time` is the separate, invertible edit map, nil where inversion is meaningless. The two were one function before, and a hold had to lie about one of them. The second is that a keyed source only looked necessary because an occurrence was assumed to need a ROW. It does not. A lane is a group with `:layout :sequence`, its occurrences are ordinary instances in the same flat node map, and `timeline/rows` draws them as cel blocks on the lane's own row: twelve exposures, one row, each cel still separately selectable and addressable. The vertical growth that justified the keyed source is a presentation question, and it is answered in the view. `arthur.domain.sequence` holds the first commands over that shape — add lane, append drawing, extend hold — each one history step, each refusing rather than half-applying. Extending a hold leaves the lane's keys at their authored times, because you are adjusting drawings underneath timed motion; a correction owned by an occurrence travels with it. Ownership does that work, so no key needs a flag saying what it follows. Ripple past the symbol's end is refused with the frame count it would need, and `:extent :grow-symbol` is the caller saying yes. `clip/blank` no longer carries `:subjects {} :features {} :groups {}`. Empty maps write no leaf, so a blank document could not survive its own round trip — `leaf/leaves` promises exactness and was the only honest side of that. Documents are schema 3. A version 2 document is not read; nothing here converts one. `docs/lane-model.md` is the design, and says which of its parts are built. 392 tests, 5,525 assertions, and `test/browser/sequence.mjs` drives the editor through create, hold, explicit overflow and undo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:20:23 -04:00
2. Use Duplicate drawing (`D`) when the current shapes are the starting point.
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
Use Reuse drawing for a deliberately linked cel. The UI shows the
An occurrence is a node, with a clock of its own A lane's drawings were going to be one instance whose source was a KEYED channel: frame 0 says `:drawing-a`, frame 4 says `:drawing-b`, and the cels of a row are that channel's keys. Two things followed from it, and both were wrong. The first is that playback meant whichever shape the channel happened to have. A framed source played its symbol; a keyed source froze the selected frame. So `node/placed-at` read animation out of storage, and adding an ordinary key to a still turned it into an animation — the last-key bug, which was not a bug in the code so much as the rule working as written. But WHICH drawing is used and HOW time runs inside it are independent questions, and all four combinations are ordinary: hold one drawing, play one animation, cut between held drawings, cut between playing ones. So an occurrence names one symbol in `:source {:symbol ...}` and says how its source time advances in `:playback {:in :speed :end}` — `source = in + speed * f`, a hold being speed 0, with `:stop`, `:hold` or `:loop` at the end named rather than guessed. `node/placed-frame` samples it forwards, which works for holds too, and `node/source-time` is the separate, invertible edit map, nil where inversion is meaningless. The two were one function before, and a hold had to lie about one of them. The second is that a keyed source only looked necessary because an occurrence was assumed to need a ROW. It does not. A lane is a group with `:layout :sequence`, its occurrences are ordinary instances in the same flat node map, and `timeline/rows` draws them as cel blocks on the lane's own row: twelve exposures, one row, each cel still separately selectable and addressable. The vertical growth that justified the keyed source is a presentation question, and it is answered in the view. `arthur.domain.sequence` holds the first commands over that shape — add lane, append drawing, extend hold — each one history step, each refusing rather than half-applying. Extending a hold leaves the lane's keys at their authored times, because you are adjusting drawings underneath timed motion; a correction owned by an occurrence travels with it. Ownership does that work, so no key needs a flag saying what it follows. Ripple past the symbol's end is refused with the frame count it would need, and `:extent :grow-symbol` is the caller saying yes. `clip/blank` no longer carries `:subjects {} :features {} :groups {}`. Empty maps write no leaf, so a blank document could not survive its own round trip — `leaf/leaves` promises exactness and was the only honest side of that. Documents are schema 3. A version 2 document is not read; nothing here converts one. `docs/lane-model.md` is the design, and says which of its parts are built. 392 tests, 5,525 assertions, and `test/browser/sequence.mjs` drives the editor through create, hold, explicit overflow and undo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:20:23 -04:00
difference before an edit can change other uses.
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
3. Time the performance. Hold longer (`H`) extends the selected cel and
ripples later cels in the explicitly targeted lane. A trim gesture
An occurrence is a node, with a clock of its own A lane's drawings were going to be one instance whose source was a KEYED channel: frame 0 says `:drawing-a`, frame 4 says `:drawing-b`, and the cels of a row are that channel's keys. Two things followed from it, and both were wrong. The first is that playback meant whichever shape the channel happened to have. A framed source played its symbol; a keyed source froze the selected frame. So `node/placed-at` read animation out of storage, and adding an ordinary key to a still turned it into an animation — the last-key bug, which was not a bug in the code so much as the rule working as written. But WHICH drawing is used and HOW time runs inside it are independent questions, and all four combinations are ordinary: hold one drawing, play one animation, cut between held drawings, cut between playing ones. So an occurrence names one symbol in `:source {:symbol ...}` and says how its source time advances in `:playback {:in :speed :end}` — `source = in + speed * f`, a hold being speed 0, with `:stop`, `:hold` or `:loop` at the end named rather than guessed. `node/placed-frame` samples it forwards, which works for holds too, and `node/source-time` is the separate, invertible edit map, nil where inversion is meaningless. The two were one function before, and a hold had to lie about one of them. The second is that a keyed source only looked necessary because an occurrence was assumed to need a ROW. It does not. A lane is a group with `:layout :sequence`, its occurrences are ordinary instances in the same flat node map, and `timeline/rows` draws them as cel blocks on the lane's own row: twelve exposures, one row, each cel still separately selectable and addressable. The vertical growth that justified the keyed source is a presentation question, and it is answered in the view. `arthur.domain.sequence` holds the first commands over that shape — add lane, append drawing, extend hold — each one history step, each refusing rather than half-applying. Extending a hold leaves the lane's keys at their authored times, because you are adjusting drawings underneath timed motion; a correction owned by an occurrence travels with it. Ownership does that work, so no key needs a flag saying what it follows. Ripple past the symbol's end is refused with the frame count it would need, and `:extent :grow-symbol` is the caller saying yes. `clip/blank` no longer carries `:subjects {} :features {} :groups {}`. Empty maps write no leaf, so a blank document could not survive its own round trip — `leaf/leaves` promises exactness and was the only honest side of that. Documents are schema 3. A version 2 document is not read; nothing here converts one. `docs/lane-model.md` is the design, and says which of its parts are built. 392 tests, 5,525 assertions, and `test/browser/sequence.mjs` drives the editor through create, hold, explicit overflow and undo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:20:23 -04:00
can use overwrite instead. The preview shows which boundaries will move.
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
4. Choose a two-frame default cel for newly created drawings, or run a
separate Retime cels command on a selected range. This does not quantize
lane transforms or silently retime already authored cels.
An occurrence is a node, with a clock of its own A lane's drawings were going to be one instance whose source was a KEYED channel: frame 0 says `:drawing-a`, frame 4 says `:drawing-b`, and the cels of a row are that channel's keys. Two things followed from it, and both were wrong. The first is that playback meant whichever shape the channel happened to have. A framed source played its symbol; a keyed source froze the selected frame. So `node/placed-at` read animation out of storage, and adding an ordinary key to a still turned it into an animation — the last-key bug, which was not a bug in the code so much as the rule working as written. But WHICH drawing is used and HOW time runs inside it are independent questions, and all four combinations are ordinary: hold one drawing, play one animation, cut between held drawings, cut between playing ones. So an occurrence names one symbol in `:source {:symbol ...}` and says how its source time advances in `:playback {:in :speed :end}` — `source = in + speed * f`, a hold being speed 0, with `:stop`, `:hold` or `:loop` at the end named rather than guessed. `node/placed-frame` samples it forwards, which works for holds too, and `node/source-time` is the separate, invertible edit map, nil where inversion is meaningless. The two were one function before, and a hold had to lie about one of them. The second is that a keyed source only looked necessary because an occurrence was assumed to need a ROW. It does not. A lane is a group with `:layout :sequence`, its occurrences are ordinary instances in the same flat node map, and `timeline/rows` draws them as cel blocks on the lane's own row: twelve exposures, one row, each cel still separately selectable and addressable. The vertical growth that justified the keyed source is a presentation question, and it is answered in the view. `arthur.domain.sequence` holds the first commands over that shape — add lane, append drawing, extend hold — each one history step, each refusing rather than half-applying. Extending a hold leaves the lane's keys at their authored times, because you are adjusting drawings underneath timed motion; a correction owned by an occurrence travels with it. Ownership does that work, so no key needs a flag saying what it follows. Ripple past the symbol's end is refused with the frame count it would need, and `:extent :grow-symbol` is the caller saying yes. `clip/blank` no longer carries `:subjects {} :features {} :groups {}`. Empty maps write no leaf, so a blank document could not survive its own round trip — `leaf/leaves` promises exactness and was the only honest side of that. Documents are schema 3. A version 2 document is not read; nothing here converts one. `docs/lane-model.md` is the design, and says which of its parts are built. 392 tests, 5,525 assertions, and `test/browser/sequence.mjs` drives the editor through create, hold, explicit overflow and undo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:20:23 -04:00
5. Place the background in a lane below. Its source holds one frame throughout
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
its cel. Key the lane's X position at the beginning and end and choose
An occurrence is a node, with a clock of its own A lane's drawings were going to be one instance whose source was a KEYED channel: frame 0 says `:drawing-a`, frame 4 says `:drawing-b`, and the cels of a row are that channel's keys. Two things followed from it, and both were wrong. The first is that playback meant whichever shape the channel happened to have. A framed source played its symbol; a keyed source froze the selected frame. So `node/placed-at` read animation out of storage, and adding an ordinary key to a still turned it into an animation — the last-key bug, which was not a bug in the code so much as the rule working as written. But WHICH drawing is used and HOW time runs inside it are independent questions, and all four combinations are ordinary: hold one drawing, play one animation, cut between held drawings, cut between playing ones. So an occurrence names one symbol in `:source {:symbol ...}` and says how its source time advances in `:playback {:in :speed :end}` — `source = in + speed * f`, a hold being speed 0, with `:stop`, `:hold` or `:loop` at the end named rather than guessed. `node/placed-frame` samples it forwards, which works for holds too, and `node/source-time` is the separate, invertible edit map, nil where inversion is meaningless. The two were one function before, and a hold had to lie about one of them. The second is that a keyed source only looked necessary because an occurrence was assumed to need a ROW. It does not. A lane is a group with `:layout :sequence`, its occurrences are ordinary instances in the same flat node map, and `timeline/rows` draws them as cel blocks on the lane's own row: twelve exposures, one row, each cel still separately selectable and addressable. The vertical growth that justified the keyed source is a presentation question, and it is answered in the view. `arthur.domain.sequence` holds the first commands over that shape — add lane, append drawing, extend hold — each one history step, each refusing rather than half-applying. Extending a hold leaves the lane's keys at their authored times, because you are adjusting drawings underneath timed motion; a correction owned by an occurrence travels with it. Ownership does that work, so no key needs a flag saying what it follows. Ripple past the symbol's end is refused with the frame count it would need, and `:extent :grow-symbol` is the caller saying yes. `clip/blank` no longer carries `:subjects {} :features {} :groups {}`. Empty maps write no leaf, so a blank document could not survive its own round trip — `leaf/leaves` promises exactness and was the only honest side of that. Documents are schema 3. A version 2 document is not read; nothing here converts one. `docs/lane-model.md` is the design, and says which of its parts are built. 392 tests, 5,525 assertions, and `test/browser/sequence.mjs` drives the editor through create, hold, explicit overflow and undo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:20:23 -04:00
linear interpolation. The background slides while the girl's drawings cut.
6. Select three frames on the girl's lane, choose Return motion, and rotate to
the desired peak. A bounded rotation correction affects the girl across any
drawing boundaries in that range. Existing motion survives outside it.
7. Insert a playing animated symbol among the girl's held drawings. Set that
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
cel's source playback to advance. No lane conversion is required.
An occurrence is a node, with a clock of its own A lane's drawings were going to be one instance whose source was a KEYED channel: frame 0 says `:drawing-a`, frame 4 says `:drawing-b`, and the cels of a row are that channel's keys. Two things followed from it, and both were wrong. The first is that playback meant whichever shape the channel happened to have. A framed source played its symbol; a keyed source froze the selected frame. So `node/placed-at` read animation out of storage, and adding an ordinary key to a still turned it into an animation — the last-key bug, which was not a bug in the code so much as the rule working as written. But WHICH drawing is used and HOW time runs inside it are independent questions, and all four combinations are ordinary: hold one drawing, play one animation, cut between held drawings, cut between playing ones. So an occurrence names one symbol in `:source {:symbol ...}` and says how its source time advances in `:playback {:in :speed :end}` — `source = in + speed * f`, a hold being speed 0, with `:stop`, `:hold` or `:loop` at the end named rather than guessed. `node/placed-frame` samples it forwards, which works for holds too, and `node/source-time` is the separate, invertible edit map, nil where inversion is meaningless. The two were one function before, and a hold had to lie about one of them. The second is that a keyed source only looked necessary because an occurrence was assumed to need a ROW. It does not. A lane is a group with `:layout :sequence`, its occurrences are ordinary instances in the same flat node map, and `timeline/rows` draws them as cel blocks on the lane's own row: twelve exposures, one row, each cel still separately selectable and addressable. The vertical growth that justified the keyed source is a presentation question, and it is answered in the view. `arthur.domain.sequence` holds the first commands over that shape — add lane, append drawing, extend hold — each one history step, each refusing rather than half-applying. Extending a hold leaves the lane's keys at their authored times, because you are adjusting drawings underneath timed motion; a correction owned by an occurrence travels with it. Ownership does that work, so no key needs a flag saying what it follows. Ripple past the symbol's end is refused with the frame count it would need, and `:extent :grow-symbol` is the caller saying yes. `clip/blank` no longer carries `:subjects {} :features {} :groups {}`. Empty maps write no leaf, so a blank document could not survive its own round trip — `leaf/leaves` promises exactness and was the only honest side of that. Documents are schema 3. A version 2 document is not read; nothing here converts one. `docs/lane-model.md` is the design, and says which of its parts are built. 392 tests, 5,525 assertions, and `test/browser/sequence.mjs` drives the editor through create, hold, explicit overflow and undo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:20:23 -04:00
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
The timeline shows named cel blocks with property marks and optional curve
subrows. The cel sheet shows the same cels by frame and lane. The
An occurrence is a node, with a clock of its own A lane's drawings were going to be one instance whose source was a KEYED channel: frame 0 says `:drawing-a`, frame 4 says `:drawing-b`, and the cels of a row are that channel's keys. Two things followed from it, and both were wrong. The first is that playback meant whichever shape the channel happened to have. A framed source played its symbol; a keyed source froze the selected frame. So `node/placed-at` read animation out of storage, and adding an ordinary key to a still turned it into an animation — the last-key bug, which was not a bug in the code so much as the rule working as written. But WHICH drawing is used and HOW time runs inside it are independent questions, and all four combinations are ordinary: hold one drawing, play one animation, cut between held drawings, cut between playing ones. So an occurrence names one symbol in `:source {:symbol ...}` and says how its source time advances in `:playback {:in :speed :end}` — `source = in + speed * f`, a hold being speed 0, with `:stop`, `:hold` or `:loop` at the end named rather than guessed. `node/placed-frame` samples it forwards, which works for holds too, and `node/source-time` is the separate, invertible edit map, nil where inversion is meaningless. The two were one function before, and a hold had to lie about one of them. The second is that a keyed source only looked necessary because an occurrence was assumed to need a ROW. It does not. A lane is a group with `:layout :sequence`, its occurrences are ordinary instances in the same flat node map, and `timeline/rows` draws them as cel blocks on the lane's own row: twelve exposures, one row, each cel still separately selectable and addressable. The vertical growth that justified the keyed source is a presentation question, and it is answered in the view. `arthur.domain.sequence` holds the first commands over that shape — add lane, append drawing, extend hold — each one history step, each refusing rather than half-applying. Extending a hold leaves the lane's keys at their authored times, because you are adjusting drawings underneath timed motion; a correction owned by an occurrence travels with it. Ownership does that work, so no key needs a flag saying what it follows. Ripple past the symbol's end is refused with the frame count it would need, and `:extent :grow-symbol` is the caller saying yes. `clip/blank` no longer carries `:subjects {} :features {} :groups {}`. Empty maps write no leaf, so a blank document could not survive its own round trip — `leaf/leaves` promises exactness and was the only honest side of that. Documents are schema 3. A version 2 document is not read; nothing here converts one. `docs/lane-model.md` is the design, and says which of its parts are built. 392 tests, 5,525 assertions, and `test/browser/sequence.mjs` drives the editor through create, hold, explicit overflow and undo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:20:23 -04:00
graph editor edits the same properties; the stage resolves the same document.
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
Onion skin is configurable and counts neighboring cel events, skipping gaps
An occurrence is a node, with a clock of its own A lane's drawings were going to be one instance whose source was a KEYED channel: frame 0 says `:drawing-a`, frame 4 says `:drawing-b`, and the cels of a row are that channel's keys. Two things followed from it, and both were wrong. The first is that playback meant whichever shape the channel happened to have. A framed source played its symbol; a keyed source froze the selected frame. So `node/placed-at` read animation out of storage, and adding an ordinary key to a still turned it into an animation — the last-key bug, which was not a bug in the code so much as the rule working as written. But WHICH drawing is used and HOW time runs inside it are independent questions, and all four combinations are ordinary: hold one drawing, play one animation, cut between held drawings, cut between playing ones. So an occurrence names one symbol in `:source {:symbol ...}` and says how its source time advances in `:playback {:in :speed :end}` — `source = in + speed * f`, a hold being speed 0, with `:stop`, `:hold` or `:loop` at the end named rather than guessed. `node/placed-frame` samples it forwards, which works for holds too, and `node/source-time` is the separate, invertible edit map, nil where inversion is meaningless. The two were one function before, and a hold had to lie about one of them. The second is that a keyed source only looked necessary because an occurrence was assumed to need a ROW. It does not. A lane is a group with `:layout :sequence`, its occurrences are ordinary instances in the same flat node map, and `timeline/rows` draws them as cel blocks on the lane's own row: twelve exposures, one row, each cel still separately selectable and addressable. The vertical growth that justified the keyed source is a presentation question, and it is answered in the view. `arthur.domain.sequence` holds the first commands over that shape — add lane, append drawing, extend hold — each one history step, each refusing rather than half-applying. Extending a hold leaves the lane's keys at their authored times, because you are adjusting drawings underneath timed motion; a correction owned by an occurrence travels with it. Ownership does that work, so no key needs a flag saying what it follows. Ripple past the symbol's end is refused with the frame count it would need, and `:extent :grow-symbol` is the caller saying yes. `clip/blank` no longer carries `:subjects {} :features {} :groups {}`. Empty maps write no leaf, so a blank document could not survive its own round trip — `leaf/leaves` promises exactness and was the only honest side of that. Documents are schema 3. A version 2 document is not read; nothing here converts one. `docs/lane-model.md` is the design, and says which of its parts are built. 392 tests, 5,525 assertions, and `test/browser/sequence.mjs` drives the editor through create, hold, explicit overflow and undo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:20:23 -04:00
by default; a long hold does not consume the budget. Repeated uses of the same
drawing remain distinct events. Deduplicating identical ghosts is a display option.
## Proof obligations and implementation order
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
The source-channel prototype has been removed: a cel names one symbol
An occurrence is a node, with a clock of its own A lane's drawings were going to be one instance whose source was a KEYED channel: frame 0 says `:drawing-a`, frame 4 says `:drawing-b`, and the cels of a row are that channel's keys. Two things followed from it, and both were wrong. The first is that playback meant whichever shape the channel happened to have. A framed source played its symbol; a keyed source froze the selected frame. So `node/placed-at` read animation out of storage, and adding an ordinary key to a still turned it into an animation — the last-key bug, which was not a bug in the code so much as the rule working as written. But WHICH drawing is used and HOW time runs inside it are independent questions, and all four combinations are ordinary: hold one drawing, play one animation, cut between held drawings, cut between playing ones. So an occurrence names one symbol in `:source {:symbol ...}` and says how its source time advances in `:playback {:in :speed :end}` — `source = in + speed * f`, a hold being speed 0, with `:stop`, `:hold` or `:loop` at the end named rather than guessed. `node/placed-frame` samples it forwards, which works for holds too, and `node/source-time` is the separate, invertible edit map, nil where inversion is meaningless. The two were one function before, and a hold had to lie about one of them. The second is that a keyed source only looked necessary because an occurrence was assumed to need a ROW. It does not. A lane is a group with `:layout :sequence`, its occurrences are ordinary instances in the same flat node map, and `timeline/rows` draws them as cel blocks on the lane's own row: twelve exposures, one row, each cel still separately selectable and addressable. The vertical growth that justified the keyed source is a presentation question, and it is answered in the view. `arthur.domain.sequence` holds the first commands over that shape — add lane, append drawing, extend hold — each one history step, each refusing rather than half-applying. Extending a hold leaves the lane's keys at their authored times, because you are adjusting drawings underneath timed motion; a correction owned by an occurrence travels with it. Ownership does that work, so no key needs a flag saying what it follows. Ripple past the symbol's end is refused with the frame count it would need, and `:extent :grow-symbol` is the caller saying yes. `clip/blank` no longer carries `:subjects {} :features {} :groups {}`. Empty maps write no leaf, so a blank document could not survive its own round trip — `leaf/leaves` promises exactness and was the only honest side of that. Documents are schema 3. A version 2 document is not read; nothing here converts one. `docs/lane-model.md` is the design, and says which of its parts are built. 392 tests, 5,525 assertions, and `test/browser/sequence.mjs` drives the editor through create, hold, explicit overflow and undo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:20:23 -04:00
and carries its own playback clock, and `node/problems` rejects the old
Reuse, duplicate and make unique: deciding what is shared The model's whole claim is that content and its occurrences are different things, and until now nothing in the editor could tell them apart: you could make a drawing and time it, but not expose one drawing twice, and so never find out whether an edit arrives in two places. That is the first proof obligation in the lane model and it was the one the commands could not reach. Three commands, and the distinctions between them are the point: reuse another occurrence of the same drawing. A decision to share, made on purpose, because sharing discovered later — when an edit turns up somewhere you did not expect — is the bad version. duplicate a copy of the drawing, appended, for when what is on screen is the starting point for the next one. make unique this occurrence gets a private copy; the others keep sharing. The undo of reuse, and refused when nothing else uses the drawing: a copy nobody asked for is a second identical symbol in the library for no reason a person could see. Duplicate copies the CONTENT and not the exposure. Its new occurrence is a plain one-frame hold, not a copy of the source occurrence's transform or corrections, because those belong to that use of the drawing — carrying them over would make duplicating a drawing quietly duplicate the treatment of one exposure of it. A copy is SHALLOW by default and keeps its references to other symbols, so a head built out of reusable eyes still uses those eyes. `:deep? true` copies everything it places with new ids throughout. The lane model asks for both and says why: never promise decoupling while leaving the edited object shared, and only the deep copy can keep that promise. `bring/symbols` already did the reachability walk and the id remapping, so the deep copy is that function pointed at its own clip. `node/sources` was still being read as a SET at five call sites, each with a comment about a lane that cuts between several drawings — the keyed source that no longer exists. An occurrence names one symbol, so they now ask `node/source`, and `placed-frame` answers with `:symbol` rather than `:of`, which was the last echo of the retired field name. To let the commands use `clip/free-id` and the copy machinery, the lane's own validation moved from `domain/sequence` to `domain/symbol`, which is where it belonged anyway: a sequence is the one composition rule a node map carries, and it now sits beside the parent and stencil checks rather than in the namespace that happens to build lanes. That also breaks the cycle — sequence can require clip and bring, and nothing below it requires sequence. Preconditions still check only the LANE's shape: refusing an exposure edit over an unrelated defect elsewhere in the symbol would be this command answering for a part of the document it never touches. The cel strip gains reuse, duplicate and make unique, the last shown only where the selected exposure actually shares its drawing. Drawing on twos is also now under test: exposure length is the cadence, the lane's transform has its own clock, and it still moves on every frame — stepping it would be the cel cadence leaking into continuous motion. 397 tests, 5,561 assertions. `test/browser/sequence.mjs` drives the three new commands through the real editor and checks that three exposures are still one row. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:30:59 -04:00
`[:source]` channel. What a lane IS lives in `arthur.domain.symbol` beside the
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
other rules about a node map; `arthur.domain.lane` holds the commands over
A position is an argument, not another command Everything could only be added to the end, because `append` computed its own position — the max end of the lane — and so had no opinion to state. Insert is not a new command; it is the argument that function was missing. `:at` takes a lane frame or `:end`, `:end` is the position where nothing has to move, and appending stops being a separate operation from inserting. New, reused and duplicated drawings all take it, because there was only ever one placement rule. Placing ripples: occurrences at or after the position move later by the new exposure's duration, and `:keep` against `:grow-symbol` still decides what happens at the shot's end. OVERWRITE is deliberately not a policy argument yet. Taking frames away from the occurrence already there is TRIMMING, and an argument whose second value is unimplemented is worse than an argument that is not there. A position strictly inside an existing exposure refuses and names `split`, rather than splitting on the quiet: one command performing two is how a command stops being predictable. Then split, which turned out to cost almost nothing, and that is the interesting part. The two pieces keep ONE `:time` and differ only in `:span`. The right piece's own frames therefore carry on exactly where the left's stopped, so its source clock, its keys and its corrections go on meaning what they meant: a held drawing holds the same frame either side of the cut, and a playing insert plays through it without a seam. There is no arithmetic on in-points to get wrong, and no shot-length question, since the pieces occupy the frames the one exposure occupied. The test samples every frame before and after and asserts the picture is identical — for a hold, for an exposure with a correction of its own, and for a playing insert. That is not a clever split. It is `:span` being in the node's OWN coordinates, which was decided long before there were lanes, paying for something it was not designed for. The same property is why extending a hold leaves lane keys alone. Both new commands act at the playhead, which needed `lane-frame` — the symbol's frame as a frame of the lane's own time, nil through a stepped or looping lane where one is not the other. Nil refuses; it does not snap to a nearby frame. Two smaller things found while doing it. `placeable` promised "a whole lane frame" in its refusal and then accepted 2.5, so both it and `split` now require an integer, as `extend-hold` already did for its delta. And `lane-end` is private: `:end` is the only way to ask for it. 401 tests, 5,612 assertions. The browser flow now splits an exposure at the playhead and puts a drawing in the gap, and checks that six exposures are still one row. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:56:26 -04:00
one — add lane, place a drawing (new, reused or duplicated), make unique, split,
The shot is as long as somebody said it was Trim, move and blank, and the decision they all three walked into: is the shot's length authored, or derived from what is in it? AUTHORED. `:frames` is the symbol's window — how long the shot IS — and the occupied extent of its lanes is a different fact, read off the occurrences. A command grows the window when the caller says `:grow-symbol` and NEVER shrinks it, so blanking the end of a shot leaves a shot with empty frames at the end. That is a true statement about what somebody authored, and the alternative is deleting the last drawing and quietly shortening the film. `finish` had the right behaviour by accident — `(apply max (:frames sym) ...)` — and now says which number is which: `needed` is where the occurrences reach, `:frames` is what was authored, and the only thing that makes the second follow the first is a caller asking. The three commands turned out to be one piece of geometry, which is `split`'s. A `:span` is in the occurrence's OWN frames and `:time` says where those land in the lane, so moving an edge of an exposure is ONE WRITE to `:span` and `:time` and `:playback` are never touched. `local` and `edged` are the whole of it, and split now goes through them too. trim narrows one edge and moves nothing else. Lengthening is `extend-hold`, which carries a ripple policy and a shot-length policy because it needs them; letting trim grow as well would give one gesture two sets of rules and a way to overlap its neighbour. move one write to `:time :at`, and a destination that would overlap is REFUSED rather than rippled. Moving a drawing and re-timing the ones around it are different intentions, and a move that pushed the rest would be the second wearing the first one's name. Clear the room first. blank leaves a gap and does not close it. Wholly inside the range goes, overlapping an end is trimmed to it, spanning the range is split — the one case that needs an ID, and it asks for one instead of inventing it. Because the source clock is untouched, trimming the front of a playing insert starts it LATER INTO its animation rather than restarting it, which is the difference between trimming and slipping and the reason they stay two commands. The test samples the frames it kept and asserts they show what they showed. Blanking leaves the drawings in the library. A lane does not own its content, and a drawing whose last exposure is gone is still a drawing somebody made. Overwrite is now `blank` then `place` and needs no policy argument of its own, which is why it still is not one. 424 tests, 5,749 assertions. The browser flow trims an exposure at the playhead, moves it into the gap that made, blanks it, and checks the shot is still as long as it was authored. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 16:32:59 -04:00
trim, move, blank and extend hold. Each is one history step, and each refuses rather than
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
half-applying. The timeline draws a lane's cels as cel blocks on the
lane's own row, and offers Make unique only where the selected cel actually
A position is an argument, not another command Everything could only be added to the end, because `append` computed its own position — the max end of the lane — and so had no opinion to state. Insert is not a new command; it is the argument that function was missing. `:at` takes a lane frame or `:end`, `:end` is the position where nothing has to move, and appending stops being a separate operation from inserting. New, reused and duplicated drawings all take it, because there was only ever one placement rule. Placing ripples: occurrences at or after the position move later by the new exposure's duration, and `:keep` against `:grow-symbol` still decides what happens at the shot's end. OVERWRITE is deliberately not a policy argument yet. Taking frames away from the occurrence already there is TRIMMING, and an argument whose second value is unimplemented is worse than an argument that is not there. A position strictly inside an existing exposure refuses and names `split`, rather than splitting on the quiet: one command performing two is how a command stops being predictable. Then split, which turned out to cost almost nothing, and that is the interesting part. The two pieces keep ONE `:time` and differ only in `:span`. The right piece's own frames therefore carry on exactly where the left's stopped, so its source clock, its keys and its corrections go on meaning what they meant: a held drawing holds the same frame either side of the cut, and a playing insert plays through it without a seam. There is no arithmetic on in-points to get wrong, and no shot-length question, since the pieces occupy the frames the one exposure occupied. The test samples every frame before and after and asserts the picture is identical — for a hold, for an exposure with a correction of its own, and for a playing insert. That is not a clever split. It is `:span` being in the node's OWN coordinates, which was decided long before there were lanes, paying for something it was not designed for. The same property is why extending a hold leaves lane keys alone. Both new commands act at the playhead, which needed `lane-frame` — the symbol's frame as a frame of the lane's own time, nil through a stepped or looping lane where one is not the other. Nil refuses; it does not snap to a nearby frame. Two smaller things found while doing it. `placeable` promised "a whole lane frame" in its refusal and then accepted 2.5, so both it and `split` now require an integer, as `extend-hold` already did for its delta. And `lane-end` is private: `:end` is the only way to ask for it. 401 tests, 5,612 assertions. The browser flow now splits an exposure at the playhead and puts a drawing in the gap, and checks that six exposures are still one row. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:56:26 -04:00
shares its drawing.
There is ONE placement function and a position argument, so appending is not a
different operation from inserting: `:end` is a position like any other, the one
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
where nothing has to move. Placing ripples — cels at or after the
position move later by the new cel's duration — and `:keep` versus
A position is an argument, not another command Everything could only be added to the end, because `append` computed its own position — the max end of the lane — and so had no opinion to state. Insert is not a new command; it is the argument that function was missing. `:at` takes a lane frame or `:end`, `:end` is the position where nothing has to move, and appending stops being a separate operation from inserting. New, reused and duplicated drawings all take it, because there was only ever one placement rule. Placing ripples: occurrences at or after the position move later by the new exposure's duration, and `:keep` against `:grow-symbol` still decides what happens at the shot's end. OVERWRITE is deliberately not a policy argument yet. Taking frames away from the occurrence already there is TRIMMING, and an argument whose second value is unimplemented is worse than an argument that is not there. A position strictly inside an existing exposure refuses and names `split`, rather than splitting on the quiet: one command performing two is how a command stops being predictable. Then split, which turned out to cost almost nothing, and that is the interesting part. The two pieces keep ONE `:time` and differ only in `:span`. The right piece's own frames therefore carry on exactly where the left's stopped, so its source clock, its keys and its corrections go on meaning what they meant: a held drawing holds the same frame either side of the cut, and a playing insert plays through it without a seam. There is no arithmetic on in-points to get wrong, and no shot-length question, since the pieces occupy the frames the one exposure occupied. The test samples every frame before and after and asserts the picture is identical — for a hold, for an exposure with a correction of its own, and for a playing insert. That is not a clever split. It is `:span` being in the node's OWN coordinates, which was decided long before there were lanes, paying for something it was not designed for. The same property is why extending a hold leaves lane keys alone. Both new commands act at the playhead, which needed `lane-frame` — the symbol's frame as a frame of the lane's own time, nil through a stepped or looping lane where one is not the other. Nil refuses; it does not snap to a nearby frame. Two smaller things found while doing it. `placeable` promised "a whole lane frame" in its refusal and then accepted 2.5, so both it and `split` now require an integer, as `extend-hold` already did for its delta. And `lane-end` is private: `:end` is the only way to ask for it. 401 tests, 5,612 assertions. The browser flow now splits an exposure at the playhead and puts a drawing in the gap, and checks that six exposures are still one row. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:56:26 -04:00
`:grow-symbol` still decides what happens at the shot's end. OVERWRITE is not a
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
policy argument yet, deliberately: taking frames away from the cel
A position is an argument, not another command Everything could only be added to the end, because `append` computed its own position — the max end of the lane — and so had no opinion to state. Insert is not a new command; it is the argument that function was missing. `:at` takes a lane frame or `:end`, `:end` is the position where nothing has to move, and appending stops being a separate operation from inserting. New, reused and duplicated drawings all take it, because there was only ever one placement rule. Placing ripples: occurrences at or after the position move later by the new exposure's duration, and `:keep` against `:grow-symbol` still decides what happens at the shot's end. OVERWRITE is deliberately not a policy argument yet. Taking frames away from the occurrence already there is TRIMMING, and an argument whose second value is unimplemented is worse than an argument that is not there. A position strictly inside an existing exposure refuses and names `split`, rather than splitting on the quiet: one command performing two is how a command stops being predictable. Then split, which turned out to cost almost nothing, and that is the interesting part. The two pieces keep ONE `:time` and differ only in `:span`. The right piece's own frames therefore carry on exactly where the left's stopped, so its source clock, its keys and its corrections go on meaning what they meant: a held drawing holds the same frame either side of the cut, and a playing insert plays through it without a seam. There is no arithmetic on in-points to get wrong, and no shot-length question, since the pieces occupy the frames the one exposure occupied. The test samples every frame before and after and asserts the picture is identical — for a hold, for an exposure with a correction of its own, and for a playing insert. That is not a clever split. It is `:span` being in the node's OWN coordinates, which was decided long before there were lanes, paying for something it was not designed for. The same property is why extending a hold leaves lane keys alone. Both new commands act at the playhead, which needed `lane-frame` — the symbol's frame as a frame of the lane's own time, nil through a stepped or looping lane where one is not the other. Nil refuses; it does not snap to a nearby frame. Two smaller things found while doing it. `placeable` promised "a whole lane frame" in its refusal and then accepted 2.5, so both it and `split` now require an integer, as `extend-hold` already did for its delta. And `lane-end` is private: `:end` is the only way to ask for it. 401 tests, 5,612 assertions. The browser flow now splits an exposure at the playhead and puts a drawing in the gap, and checks that six exposures are still one row. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:56:26 -04:00
already there is trimming, and until `trim` exists, placement that would need it
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
refuses instead of approximating it. A position inside an existing cel
A position is an argument, not another command Everything could only be added to the end, because `append` computed its own position — the max end of the lane — and so had no opinion to state. Insert is not a new command; it is the argument that function was missing. `:at` takes a lane frame or `:end`, `:end` is the position where nothing has to move, and appending stops being a separate operation from inserting. New, reused and duplicated drawings all take it, because there was only ever one placement rule. Placing ripples: occurrences at or after the position move later by the new exposure's duration, and `:keep` against `:grow-symbol` still decides what happens at the shot's end. OVERWRITE is deliberately not a policy argument yet. Taking frames away from the occurrence already there is TRIMMING, and an argument whose second value is unimplemented is worse than an argument that is not there. A position strictly inside an existing exposure refuses and names `split`, rather than splitting on the quiet: one command performing two is how a command stops being predictable. Then split, which turned out to cost almost nothing, and that is the interesting part. The two pieces keep ONE `:time` and differ only in `:span`. The right piece's own frames therefore carry on exactly where the left's stopped, so its source clock, its keys and its corrections go on meaning what they meant: a held drawing holds the same frame either side of the cut, and a playing insert plays through it without a seam. There is no arithmetic on in-points to get wrong, and no shot-length question, since the pieces occupy the frames the one exposure occupied. The test samples every frame before and after and asserts the picture is identical — for a hold, for an exposure with a correction of its own, and for a playing insert. That is not a clever split. It is `:span` being in the node's OWN coordinates, which was decided long before there were lanes, paying for something it was not designed for. The same property is why extending a hold leaves lane keys alone. Both new commands act at the playhead, which needed `lane-frame` — the symbol's frame as a frame of the lane's own time, nil through a stepped or looping lane where one is not the other. Nil refuses; it does not snap to a nearby frame. Two smaller things found while doing it. `placeable` promised "a whole lane frame" in its refusal and then accepted 2.5, so both it and `split` now require an integer, as `extend-hold` already did for its delta. And `lane-end` is private: `:end` is the only way to ask for it. 401 tests, 5,612 assertions. The browser flow now splits an exposure at the playhead and puts a drawing in the gap, and checks that six exposures are still one row. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:56:26 -04:00
refuses too, and names `split` — one command does not quietly perform two.
Splitting turned out to cost almost nothing, which is evidence for the
representation rather than for the command. The two pieces keep ONE `:time` and
differ only in `:span`, so the right piece's own frames carry on where the
left's stopped and its source clock, keys and corrections go on meaning what
they meant — a held drawing holds the same frame either side, a playing insert
plays through the cut without a seam, and the test for it samples every frame
before and after and asserts the picture is identical. That falls out of `:span`
being in the node's own coordinates; it is not something split arranges.
Reuse, duplicate and make unique: deciding what is shared The model's whole claim is that content and its occurrences are different things, and until now nothing in the editor could tell them apart: you could make a drawing and time it, but not expose one drawing twice, and so never find out whether an edit arrives in two places. That is the first proof obligation in the lane model and it was the one the commands could not reach. Three commands, and the distinctions between them are the point: reuse another occurrence of the same drawing. A decision to share, made on purpose, because sharing discovered later — when an edit turns up somewhere you did not expect — is the bad version. duplicate a copy of the drawing, appended, for when what is on screen is the starting point for the next one. make unique this occurrence gets a private copy; the others keep sharing. The undo of reuse, and refused when nothing else uses the drawing: a copy nobody asked for is a second identical symbol in the library for no reason a person could see. Duplicate copies the CONTENT and not the exposure. Its new occurrence is a plain one-frame hold, not a copy of the source occurrence's transform or corrections, because those belong to that use of the drawing — carrying them over would make duplicating a drawing quietly duplicate the treatment of one exposure of it. A copy is SHALLOW by default and keeps its references to other symbols, so a head built out of reusable eyes still uses those eyes. `:deep? true` copies everything it places with new ids throughout. The lane model asks for both and says why: never promise decoupling while leaving the edited object shared, and only the deep copy can keep that promise. `bring/symbols` already did the reachability walk and the id remapping, so the deep copy is that function pointed at its own clip. `node/sources` was still being read as a SET at five call sites, each with a comment about a lane that cuts between several drawings — the keyed source that no longer exists. An occurrence names one symbol, so they now ask `node/source`, and `placed-frame` answers with `:symbol` rather than `:of`, which was the last echo of the retired field name. To let the commands use `clip/free-id` and the copy machinery, the lane's own validation moved from `domain/sequence` to `domain/symbol`, which is where it belonged anyway: a sequence is the one composition rule a node map carries, and it now sits beside the parent and stencil checks rather than in the namespace that happens to build lanes. That also breaks the cycle — sequence can require clip and bring, and nothing below it requires sequence. Preconditions still check only the LANE's shape: refusing an exposure edit over an unrelated defect elsewhere in the symbol would be this command answering for a part of the document it never touches. The cel strip gains reuse, duplicate and make unique, the last shown only where the selected exposure actually shares its drawing. Drawing on twos is also now under test: exposure length is the cadence, the lane's transform has its own clock, and it still moves on every frame — stepping it would be the cel cadence leaking into continuous motion. 397 tests, 5,561 assertions. `test/browser/sequence.mjs` drives the three new commands through the real editor and checks that three exposures are still one row. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:30:59 -04:00
Content copies are shallow by default and keep their references to other
symbols; `:deep? true` is the explicit copy that shares nothing, so the promise
of independence is only made where it is kept.
The shot is as long as somebody said it was Trim, move and blank, and the decision they all three walked into: is the shot's length authored, or derived from what is in it? AUTHORED. `:frames` is the symbol's window — how long the shot IS — and the occupied extent of its lanes is a different fact, read off the occurrences. A command grows the window when the caller says `:grow-symbol` and NEVER shrinks it, so blanking the end of a shot leaves a shot with empty frames at the end. That is a true statement about what somebody authored, and the alternative is deleting the last drawing and quietly shortening the film. `finish` had the right behaviour by accident — `(apply max (:frames sym) ...)` — and now says which number is which: `needed` is where the occurrences reach, `:frames` is what was authored, and the only thing that makes the second follow the first is a caller asking. The three commands turned out to be one piece of geometry, which is `split`'s. A `:span` is in the occurrence's OWN frames and `:time` says where those land in the lane, so moving an edge of an exposure is ONE WRITE to `:span` and `:time` and `:playback` are never touched. `local` and `edged` are the whole of it, and split now goes through them too. trim narrows one edge and moves nothing else. Lengthening is `extend-hold`, which carries a ripple policy and a shot-length policy because it needs them; letting trim grow as well would give one gesture two sets of rules and a way to overlap its neighbour. move one write to `:time :at`, and a destination that would overlap is REFUSED rather than rippled. Moving a drawing and re-timing the ones around it are different intentions, and a move that pushed the rest would be the second wearing the first one's name. Clear the room first. blank leaves a gap and does not close it. Wholly inside the range goes, overlapping an end is trimmed to it, spanning the range is split — the one case that needs an ID, and it asks for one instead of inventing it. Because the source clock is untouched, trimming the front of a playing insert starts it LATER INTO its animation rather than restarting it, which is the difference between trimming and slipping and the reason they stay two commands. The test samples the frames it kept and asserts they show what they showed. Blanking leaves the drawings in the library. A lane does not own its content, and a drawing whose last exposure is gone is still a drawing somebody made. Overwrite is now `blank` then `place` and needs no policy argument of its own, which is why it still is not one. 424 tests, 5,749 assertions. The browser flow trims an exposure at the playhead, moves it into the gap that made, blanks it, and checks the shot is still as long as it was authored. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 16:32:59 -04:00
THE SHOT LENGTH IS AUTHORED, which is the decision the range commands forced.
`:frames` is the symbol's window — how long the shot IS — and the occupied
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
extent of its lanes is a different fact derived from the cels. A command
The shot is as long as somebody said it was Trim, move and blank, and the decision they all three walked into: is the shot's length authored, or derived from what is in it? AUTHORED. `:frames` is the symbol's window — how long the shot IS — and the occupied extent of its lanes is a different fact, read off the occurrences. A command grows the window when the caller says `:grow-symbol` and NEVER shrinks it, so blanking the end of a shot leaves a shot with empty frames at the end. That is a true statement about what somebody authored, and the alternative is deleting the last drawing and quietly shortening the film. `finish` had the right behaviour by accident — `(apply max (:frames sym) ...)` — and now says which number is which: `needed` is where the occurrences reach, `:frames` is what was authored, and the only thing that makes the second follow the first is a caller asking. The three commands turned out to be one piece of geometry, which is `split`'s. A `:span` is in the occurrence's OWN frames and `:time` says where those land in the lane, so moving an edge of an exposure is ONE WRITE to `:span` and `:time` and `:playback` are never touched. `local` and `edged` are the whole of it, and split now goes through them too. trim narrows one edge and moves nothing else. Lengthening is `extend-hold`, which carries a ripple policy and a shot-length policy because it needs them; letting trim grow as well would give one gesture two sets of rules and a way to overlap its neighbour. move one write to `:time :at`, and a destination that would overlap is REFUSED rather than rippled. Moving a drawing and re-timing the ones around it are different intentions, and a move that pushed the rest would be the second wearing the first one's name. Clear the room first. blank leaves a gap and does not close it. Wholly inside the range goes, overlapping an end is trimmed to it, spanning the range is split — the one case that needs an ID, and it asks for one instead of inventing it. Because the source clock is untouched, trimming the front of a playing insert starts it LATER INTO its animation rather than restarting it, which is the difference between trimming and slipping and the reason they stay two commands. The test samples the frames it kept and asserts they show what they showed. Blanking leaves the drawings in the library. A lane does not own its content, and a drawing whose last exposure is gone is still a drawing somebody made. Overwrite is now `blank` then `place` and needs no policy argument of its own, which is why it still is not one. 424 tests, 5,749 assertions. The browser flow trims an exposure at the playhead, moves it into the gap that made, blanks it, and checks the shot is still as long as it was authored. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 16:32:59 -04:00
grows the window only when the caller says `:grow-symbol`, and never shrinks it:
blanking the end of a shot leaves a shot with empty frames at the end, because
that is a true statement about what somebody authored, and deriving the window
from the extent would make deleting the last drawing quietly shorten the film.
`finish` keeps the two numbers apart by name now rather than by a `max` that
read like an accident.
Trim NARROWS one edge and moves nothing else; lengthening is `extend-hold`,
which carries the ripple and shot-length policies because it needs them.
Move is one write to `:time :at` and REFUSES a destination that would overlap,
because moving a drawing and re-timing the ones around it are different
intentions — clear the room with `blank` or `trim` first, which is the
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
composition. Blank leaves a gap and does not close it; a cel wholly inside
The shot is as long as somebody said it was Trim, move and blank, and the decision they all three walked into: is the shot's length authored, or derived from what is in it? AUTHORED. `:frames` is the symbol's window — how long the shot IS — and the occupied extent of its lanes is a different fact, read off the occurrences. A command grows the window when the caller says `:grow-symbol` and NEVER shrinks it, so blanking the end of a shot leaves a shot with empty frames at the end. That is a true statement about what somebody authored, and the alternative is deleting the last drawing and quietly shortening the film. `finish` had the right behaviour by accident — `(apply max (:frames sym) ...)` — and now says which number is which: `needed` is where the occurrences reach, `:frames` is what was authored, and the only thing that makes the second follow the first is a caller asking. The three commands turned out to be one piece of geometry, which is `split`'s. A `:span` is in the occurrence's OWN frames and `:time` says where those land in the lane, so moving an edge of an exposure is ONE WRITE to `:span` and `:time` and `:playback` are never touched. `local` and `edged` are the whole of it, and split now goes through them too. trim narrows one edge and moves nothing else. Lengthening is `extend-hold`, which carries a ripple policy and a shot-length policy because it needs them; letting trim grow as well would give one gesture two sets of rules and a way to overlap its neighbour. move one write to `:time :at`, and a destination that would overlap is REFUSED rather than rippled. Moving a drawing and re-timing the ones around it are different intentions, and a move that pushed the rest would be the second wearing the first one's name. Clear the room first. blank leaves a gap and does not close it. Wholly inside the range goes, overlapping an end is trimmed to it, spanning the range is split — the one case that needs an ID, and it asks for one instead of inventing it. Because the source clock is untouched, trimming the front of a playing insert starts it LATER INTO its animation rather than restarting it, which is the difference between trimming and slipping and the reason they stay two commands. The test samples the frames it kept and asserts they show what they showed. Blanking leaves the drawings in the library. A lane does not own its content, and a drawing whose last exposure is gone is still a drawing somebody made. Overwrite is now `blank` then `place` and needs no policy argument of its own, which is why it still is not one. 424 tests, 5,749 assertions. The browser flow trims an exposure at the playhead, moves it into the gap that made, blanks it, and checks the shot is still as long as it was authored. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 16:32:59 -04:00
the range goes, one overlapping an end is trimmed to it, and the one spanning
the range is split. Their drawings stay in the library, since a lane does not
own its content.
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
All three are the same geometry as `split`: a `:span` is in the cel's own
The shot is as long as somebody said it was Trim, move and blank, and the decision they all three walked into: is the shot's length authored, or derived from what is in it? AUTHORED. `:frames` is the symbol's window — how long the shot IS — and the occupied extent of its lanes is a different fact, read off the occurrences. A command grows the window when the caller says `:grow-symbol` and NEVER shrinks it, so blanking the end of a shot leaves a shot with empty frames at the end. That is a true statement about what somebody authored, and the alternative is deleting the last drawing and quietly shortening the film. `finish` had the right behaviour by accident — `(apply max (:frames sym) ...)` — and now says which number is which: `needed` is where the occurrences reach, `:frames` is what was authored, and the only thing that makes the second follow the first is a caller asking. The three commands turned out to be one piece of geometry, which is `split`'s. A `:span` is in the occurrence's OWN frames and `:time` says where those land in the lane, so moving an edge of an exposure is ONE WRITE to `:span` and `:time` and `:playback` are never touched. `local` and `edged` are the whole of it, and split now goes through them too. trim narrows one edge and moves nothing else. Lengthening is `extend-hold`, which carries a ripple policy and a shot-length policy because it needs them; letting trim grow as well would give one gesture two sets of rules and a way to overlap its neighbour. move one write to `:time :at`, and a destination that would overlap is REFUSED rather than rippled. Moving a drawing and re-timing the ones around it are different intentions, and a move that pushed the rest would be the second wearing the first one's name. Clear the room first. blank leaves a gap and does not close it. Wholly inside the range goes, overlapping an end is trimmed to it, spanning the range is split — the one case that needs an ID, and it asks for one instead of inventing it. Because the source clock is untouched, trimming the front of a playing insert starts it LATER INTO its animation rather than restarting it, which is the difference between trimming and slipping and the reason they stay two commands. The test samples the frames it kept and asserts they show what they showed. Blanking leaves the drawings in the library. A lane does not own its content, and a drawing whose last exposure is gone is still a drawing somebody made. Overwrite is now `blank` then `place` and needs no policy argument of its own, which is why it still is not one. 424 tests, 5,749 assertions. The browser flow trims an exposure at the playhead, moves it into the gap that made, blanks it, and checks the shot is still as long as it was authored. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 16:32:59 -04:00
frames, so moving an edge is one write and `:time` and `:playback` are never
touched. That is why trimming the front of a playing insert starts it later into
its animation instead of restarting it — the difference between trimming and
slipping, and the reason they stay separate commands.
A correction is a layer, and a layer's values are a channel `:over` was specified in animation-model.md, refused in two places, and produced by nothing: `check-unimplemented!` threw on read and `channel/problems` reported it. It reads now. This is the part of the model the rotoscoping half depends on — generate motion, correct it by hand, turn the knob, keep the correction — and it was the last thing in the design that had never been tried. The shape that made it small: A LAYER'S VALUES ARE A CHANNEL. {:id :nudge :support [88 98] :op :offset :values {:animated? true :interp :linear :keys {88 [2 0], 96 [0 0]}}} So the three commands the lane model asks for over a selected range — a constant adjustment, a ramp, a return motion — are one mechanism and not three: framed values say the same thing on every frame they cover, keyed values move, and neither needs a new way to say what a value is over time. A layer reads through `value-at` and `cursor` like any channel, which is also what stopped blending from becoming two implementations: `over-at` is shared, and the specification and the playback path differ only in how they READ a layer — recursively through `value-at`, or through a reading head of its own. One level deep; a layer's values may not carry layers, which the stack already orders. That was the risk worth spiking for. A cursor that drifts produces the wrong pose rather than an error, and a stack means several reading heads per channel where there was one. The agreement test that holds the cursor to the specification in forward, backward and random frame order now covers stacked channels too — including a layer whose head is asked for nothing across the long stretches outside its support and then asked again, which is where drift would hide. `:support` is half-open and explicit. Outside it the base evaluates exactly as it did before, which is the whole difference between a bounded correction and inserting boundary keys: the latter alters the neighbouring segments, and the lane model says so. A LAYER HAS NO TIME SPACE OF ITS OWN, and this is the design question the doc left open. Its support and its values' keys are in the frames the base channel's keys are in — the node's. A correction on a lane is therefore in lane frames and reaches across the drawings exposed beneath it; one on a single occurrence is in that occurrence's frames and travels with it when the exposure moves. Ownership had already answered it, so there is no field to disagree with, and both halves are under test at lane level. Two things cost nothing, which is worth recording. A channel is ONE LEAF, so a correction persists inside it with no codec change at all. And `node/problems` already reports every channel's problems, so a malformed layer surfaces at the document level and in the sequence commands' post-check without plumbing. What is still missing is a command that MAKES one, and with it the question of how a view offers a constant, a ramp and a return over a selected range. The evaluator no longer has an opinion about that, which was the point. `offset` adds component-wise and never writes into a dense value, which is a view onto the block itself; a shape mismatch throws rather than being dropped, since a correction that silently does not take is the failure this design exists to prevent. `replace` can supply a value over an absent base and `offset` cannot, as animation-model.md required. 408 tests, 5,655 assertions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 16:07:38 -04:00
Correction layers EVALUATE. `channel/problems` used to refuse an `:over` stack
and `value-at`/`cursor` used to throw on one; both now read it, and the
agreement test that holds the optimized cursor to the specification covers
stacked channels in forward, backward and random frame order. A layer's values
are themselves a channel, so a constant adjustment, a ramp and a return motion
are one mechanism; `:support` is half-open and a layer is inactive outside it;
and a layer has no time space of its own, because the node its channel is on
already has one. Nothing had to change in the codec — a channel is one leaf, so
a correction persists inside it — and nothing had to change in validation
plumbing, since `node/problems` already reports every channel's problems.
Both halves of ownership are under test at lane level: a three-frame correction
on the girl's lane reaches across the drawing boundary beneath it and leaves
every frame outside its support identical, and a correction owned by one
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
cel travels with that cel when a hold before it grows.
A correction is a layer, and a layer's values are a channel `:over` was specified in animation-model.md, refused in two places, and produced by nothing: `check-unimplemented!` threw on read and `channel/problems` reported it. It reads now. This is the part of the model the rotoscoping half depends on — generate motion, correct it by hand, turn the knob, keep the correction — and it was the last thing in the design that had never been tried. The shape that made it small: A LAYER'S VALUES ARE A CHANNEL. {:id :nudge :support [88 98] :op :offset :values {:animated? true :interp :linear :keys {88 [2 0], 96 [0 0]}}} So the three commands the lane model asks for over a selected range — a constant adjustment, a ramp, a return motion — are one mechanism and not three: framed values say the same thing on every frame they cover, keyed values move, and neither needs a new way to say what a value is over time. A layer reads through `value-at` and `cursor` like any channel, which is also what stopped blending from becoming two implementations: `over-at` is shared, and the specification and the playback path differ only in how they READ a layer — recursively through `value-at`, or through a reading head of its own. One level deep; a layer's values may not carry layers, which the stack already orders. That was the risk worth spiking for. A cursor that drifts produces the wrong pose rather than an error, and a stack means several reading heads per channel where there was one. The agreement test that holds the cursor to the specification in forward, backward and random frame order now covers stacked channels too — including a layer whose head is asked for nothing across the long stretches outside its support and then asked again, which is where drift would hide. `:support` is half-open and explicit. Outside it the base evaluates exactly as it did before, which is the whole difference between a bounded correction and inserting boundary keys: the latter alters the neighbouring segments, and the lane model says so. A LAYER HAS NO TIME SPACE OF ITS OWN, and this is the design question the doc left open. Its support and its values' keys are in the frames the base channel's keys are in — the node's. A correction on a lane is therefore in lane frames and reaches across the drawings exposed beneath it; one on a single occurrence is in that occurrence's frames and travels with it when the exposure moves. Ownership had already answered it, so there is no field to disagree with, and both halves are under test at lane level. Two things cost nothing, which is worth recording. A channel is ONE LEAF, so a correction persists inside it with no codec change at all. And `node/problems` already reports every channel's problems, so a malformed layer surfaces at the document level and in the sequence commands' post-check without plumbing. What is still missing is a command that MAKES one, and with it the question of how a view offers a constant, a ramp and a return over a selected range. The evaluator no longer has an opinion about that, which was the point. `offset` adds component-wise and never writes into a dense value, which is a view onto the block itself; a shape mismatch throws rather than being dropped, since a correction that silently does not take is the failure this design exists to prevent. `replace` can supply a value over an absent base and `offset` cannot, as animation-model.md required. 408 tests, 5,655 assertions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 16:07:38 -04:00
Regenerate the base, keep the hand work, and say when you cannot The loop the layer design exists for, tested for the first time: correct a generated channel by hand, turn the generator's knob, and get the new base with the correction still on it. `replace-feature` already carried `:over` across — somebody anticipated this — so the feature path needed a test and not a fix. The head path needed a fix, and there was a second fault of my own making. `regenerate-head` leaves the head's authored channels alone once somebody has placed it by hand, and decided that by `(= (:channels old) (:measured old))`. Sound, until a correction exists: an `:over` layer makes those unequal, so the FIRST correction anyone made would have stopped the head following re-measurement for good — the exact opposite of what a layer is for. It compares the channels without their layers now. The test fails against the old guard, which is how I know the bug was real and not a story about one. The other fault was mine, from the commit before this one. An `:offset` whose shape does not match its base threw, which is right for authored data — the validator catches it — but WRONG for the case the model actually names: turn the mouth's `:verts` knob and the re-freeze gives it a different number of points, so a correction that was correct when it was made stops fitting through nobody's error, and a throw in the read path takes the stage down. So a base that has outgrown a correction is a CONFLICT, and a conflict is the third thing beside applied and discarded. The regeneration records `:conflict` on the layer; the layer stays exactly where it is; `over-at` skips it, so the picture is the base meanwhile; and `clip/conflicts` lists them for a view to offer. A later regeneration that restores the shape clears the mark, so resolving one can be as simple as putting the knob back. Deliberately NOT `problems`. A document with a conflict loads, evaluates and saves — it contains a decision nobody has made yet, and refusing to open it would be the persistence layer taking a side in an editing question. The distinction in the validator is one line: a shape mismatch nobody has recorded is an authoring bug, and one a regeneration recorded is a conflict. `channel/conflict-with` is the single rule for "can this layer apply to this base", used by the validator, by `conflicts`, and by the regeneration that marks them. Only `:offset` can conflict, since `:replace` states a whole value and has nothing to agree with; a shape that cannot be read yet — an empty key map — is not a disagreement. `value-shape` answers it without sampling anything. 414 tests, 5,696 assertions, and `:verts` in the test is a real topology change rather than a synthetic one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 16:22:02 -04:00
Regeneration keeps them, which is the obligation the layer design exists to
meet: `rebased` replaces a base and carries its corrections across, and a
correction the new base no longer fits is MARKED rather than dropped or
misapplied — `clip/conflicts` lists those for a view to offer, separately from
`problems`, because a conflict is a decision nobody has made yet and not a
document that will not load. Turning the mouth's `:verts` knob is a real
topology change and is what the test uses. Two latent faults turned up there and
are fixed: `regenerate-head` compared authored channels to measured ones
directly, so the first correction on the head would have stopped it following
re-measurement for good; and an incompatible offset threw in the read path,
which would have taken the stage down on exactly the case the model says to
report.
The shot is as long as somebody said it was Trim, move and blank, and the decision they all three walked into: is the shot's length authored, or derived from what is in it? AUTHORED. `:frames` is the symbol's window — how long the shot IS — and the occupied extent of its lanes is a different fact, read off the occurrences. A command grows the window when the caller says `:grow-symbol` and NEVER shrinks it, so blanking the end of a shot leaves a shot with empty frames at the end. That is a true statement about what somebody authored, and the alternative is deleting the last drawing and quietly shortening the film. `finish` had the right behaviour by accident — `(apply max (:frames sym) ...)` — and now says which number is which: `needed` is where the occurrences reach, `:frames` is what was authored, and the only thing that makes the second follow the first is a caller asking. The three commands turned out to be one piece of geometry, which is `split`'s. A `:span` is in the occurrence's OWN frames and `:time` says where those land in the lane, so moving an edge of an exposure is ONE WRITE to `:span` and `:time` and `:playback` are never touched. `local` and `edged` are the whole of it, and split now goes through them too. trim narrows one edge and moves nothing else. Lengthening is `extend-hold`, which carries a ripple policy and a shot-length policy because it needs them; letting trim grow as well would give one gesture two sets of rules and a way to overlap its neighbour. move one write to `:time :at`, and a destination that would overlap is REFUSED rather than rippled. Moving a drawing and re-timing the ones around it are different intentions, and a move that pushed the rest would be the second wearing the first one's name. Clear the room first. blank leaves a gap and does not close it. Wholly inside the range goes, overlapping an end is trimmed to it, spanning the range is split — the one case that needs an ID, and it asks for one instead of inventing it. Because the source clock is untouched, trimming the front of a playing insert starts it LATER INTO its animation rather than restarting it, which is the difference between trimming and slipping and the reason they stay two commands. The test samples the frames it kept and asserts they show what they showed. Blanking leaves the drawings in the library. A lane does not own its content, and a drawing whose last exposure is gone is still a drawing somebody made. Overwrite is now `blank` then `place` and needs no policy argument of its own, which is why it still is not one. 424 tests, 5,749 assertions. The browser flow trims an exposure at the playhead, moves it into the gap that made, blanks it, and checks the shot is still as long as it was authored. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 16:32:59 -04:00
Still unbuilt: overwrite as a placement policy (which is now `blank` then
`place`, composed inside one transaction), slip source, retime, and deleting
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
reused content. A lane cannot hold AUDIO cels — `lane-problems`
The shot is as long as somebody said it was Trim, move and blank, and the decision they all three walked into: is the shot's length authored, or derived from what is in it? AUTHORED. `:frames` is the symbol's window — how long the shot IS — and the occupied extent of its lanes is a different fact, read off the occurrences. A command grows the window when the caller says `:grow-symbol` and NEVER shrinks it, so blanking the end of a shot leaves a shot with empty frames at the end. That is a true statement about what somebody authored, and the alternative is deleting the last drawing and quietly shortening the film. `finish` had the right behaviour by accident — `(apply max (:frames sym) ...)` — and now says which number is which: `needed` is where the occurrences reach, `:frames` is what was authored, and the only thing that makes the second follow the first is a caller asking. The three commands turned out to be one piece of geometry, which is `split`'s. A `:span` is in the occurrence's OWN frames and `:time` says where those land in the lane, so moving an edge of an exposure is ONE WRITE to `:span` and `:time` and `:playback` are never touched. `local` and `edged` are the whole of it, and split now goes through them too. trim narrows one edge and moves nothing else. Lengthening is `extend-hold`, which carries a ripple policy and a shot-length policy because it needs them; letting trim grow as well would give one gesture two sets of rules and a way to overlap its neighbour. move one write to `:time :at`, and a destination that would overlap is REFUSED rather than rippled. Moving a drawing and re-timing the ones around it are different intentions, and a move that pushed the rest would be the second wearing the first one's name. Clear the room first. blank leaves a gap and does not close it. Wholly inside the range goes, overlapping an end is trimmed to it, spanning the range is split — the one case that needs an ID, and it asks for one instead of inventing it. Because the source clock is untouched, trimming the front of a playing insert starts it LATER INTO its animation rather than restarting it, which is the difference between trimming and slipping and the reason they stay two commands. The test samples the frames it kept and asserts they show what they showed. Blanking leaves the drawings in the library. A lane does not own its content, and a drawing whose last exposure is gone is still a drawing somebody made. Overwrite is now `blank` then `place` and needs no policy argument of its own, which is why it still is not one. 424 tests, 5,749 assertions. The browser flow trims an exposure at the playhead, moves it into the gap that made, blanks it, and checks the shot is still as long as it was authored. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 16:32:59 -04:00
requires visual ones, though this document says a lane may hold either and
should reject only a mixture.
A correction is a layer, and a layer's values are a channel `:over` was specified in animation-model.md, refused in two places, and produced by nothing: `check-unimplemented!` threw on read and `channel/problems` reported it. It reads now. This is the part of the model the rotoscoping half depends on — generate motion, correct it by hand, turn the knob, keep the correction — and it was the last thing in the design that had never been tried. The shape that made it small: A LAYER'S VALUES ARE A CHANNEL. {:id :nudge :support [88 98] :op :offset :values {:animated? true :interp :linear :keys {88 [2 0], 96 [0 0]}}} So the three commands the lane model asks for over a selected range — a constant adjustment, a ramp, a return motion — are one mechanism and not three: framed values say the same thing on every frame they cover, keyed values move, and neither needs a new way to say what a value is over time. A layer reads through `value-at` and `cursor` like any channel, which is also what stopped blending from becoming two implementations: `over-at` is shared, and the specification and the playback path differ only in how they READ a layer — recursively through `value-at`, or through a reading head of its own. One level deep; a layer's values may not carry layers, which the stack already orders. That was the risk worth spiking for. A cursor that drifts produces the wrong pose rather than an error, and a stack means several reading heads per channel where there was one. The agreement test that holds the cursor to the specification in forward, backward and random frame order now covers stacked channels too — including a layer whose head is asked for nothing across the long stretches outside its support and then asked again, which is where drift would hide. `:support` is half-open and explicit. Outside it the base evaluates exactly as it did before, which is the whole difference between a bounded correction and inserting boundary keys: the latter alters the neighbouring segments, and the lane model says so. A LAYER HAS NO TIME SPACE OF ITS OWN, and this is the design question the doc left open. Its support and its values' keys are in the frames the base channel's keys are in — the node's. A correction on a lane is therefore in lane frames and reaches across the drawings exposed beneath it; one on a single occurrence is in that occurrence's frames and travels with it when the exposure moves. Ownership had already answered it, so there is no field to disagree with, and both halves are under test at lane level. Two things cost nothing, which is worth recording. A channel is ONE LEAF, so a correction persists inside it with no codec change at all. And `node/problems` already reports every channel's problems, so a malformed layer surfaces at the document level and in the sequence commands' post-check without plumbing. What is still missing is a command that MAKES one, and with it the question of how a view offers a constant, a ramp and a return over a selected range. The evaluator no longer has an opinion about that, which was the point. `offset` adds component-wise and never writes into a dense value, which is a view onto the block itself; a shape mismatch throws rather than being dropped, since a correction that silently does not take is the failure this design exists to prevent. `replace` can supply a value over an absent base and `offset` cannot, as animation-model.md required. 408 tests, 5,655 assertions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 16:07:38 -04:00
What is NOT implemented is a command that produces a layer — the doc's Constant
adjustment, Ramp and Return motion — and with it the question of how a view
Regenerate the base, keep the hand work, and say when you cannot The loop the layer design exists for, tested for the first time: correct a generated channel by hand, turn the generator's knob, and get the new base with the correction still on it. `replace-feature` already carried `:over` across — somebody anticipated this — so the feature path needed a test and not a fix. The head path needed a fix, and there was a second fault of my own making. `regenerate-head` leaves the head's authored channels alone once somebody has placed it by hand, and decided that by `(= (:channels old) (:measured old))`. Sound, until a correction exists: an `:over` layer makes those unequal, so the FIRST correction anyone made would have stopped the head following re-measurement for good — the exact opposite of what a layer is for. It compares the channels without their layers now. The test fails against the old guard, which is how I know the bug was real and not a story about one. The other fault was mine, from the commit before this one. An `:offset` whose shape does not match its base threw, which is right for authored data — the validator catches it — but WRONG for the case the model actually names: turn the mouth's `:verts` knob and the re-freeze gives it a different number of points, so a correction that was correct when it was made stops fitting through nobody's error, and a throw in the read path takes the stage down. So a base that has outgrown a correction is a CONFLICT, and a conflict is the third thing beside applied and discarded. The regeneration records `:conflict` on the layer; the layer stays exactly where it is; `over-at` skips it, so the picture is the base meanwhile; and `clip/conflicts` lists them for a view to offer. A later regeneration that restores the shape clears the mark, so resolving one can be as simple as putting the knob back. Deliberately NOT `problems`. A document with a conflict loads, evaluates and saves — it contains a decision nobody has made yet, and refusing to open it would be the persistence layer taking a side in an editing question. The distinction in the validator is one line: a shape mismatch nobody has recorded is an authoring bug, and one a regeneration recorded is a conflict. `channel/conflict-with` is the single rule for "can this layer apply to this base", used by the validator, by `conflicts`, and by the regeneration that marks them. Only `:offset` can conflict, since `:replace` states a whole value and has nothing to agree with; a shape that cannot be read yet — an empty key map — is not a disagreement. `value-shape` answers it without sampling anything. 414 tests, 5,696 assertions, and `:verts` in the test is a real topology change rather than a synthetic one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 16:22:02 -04:00
offers those three over a selected range, and how it offers a conflict for
resolution. Overwrite, the range and retiming
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
commands (blank, trim, move, slip source, retime) and the cel-sheet view
A correction is a layer, and a layer's values are a channel `:over` was specified in animation-model.md, refused in two places, and produced by nothing: `check-unimplemented!` threw on read and `channel/problems` reported it. It reads now. This is the part of the model the rotoscoping half depends on — generate motion, correct it by hand, turn the knob, keep the correction — and it was the last thing in the design that had never been tried. The shape that made it small: A LAYER'S VALUES ARE A CHANNEL. {:id :nudge :support [88 98] :op :offset :values {:animated? true :interp :linear :keys {88 [2 0], 96 [0 0]}}} So the three commands the lane model asks for over a selected range — a constant adjustment, a ramp, a return motion — are one mechanism and not three: framed values say the same thing on every frame they cover, keyed values move, and neither needs a new way to say what a value is over time. A layer reads through `value-at` and `cursor` like any channel, which is also what stopped blending from becoming two implementations: `over-at` is shared, and the specification and the playback path differ only in how they READ a layer — recursively through `value-at`, or through a reading head of its own. One level deep; a layer's values may not carry layers, which the stack already orders. That was the risk worth spiking for. A cursor that drifts produces the wrong pose rather than an error, and a stack means several reading heads per channel where there was one. The agreement test that holds the cursor to the specification in forward, backward and random frame order now covers stacked channels too — including a layer whose head is asked for nothing across the long stretches outside its support and then asked again, which is where drift would hide. `:support` is half-open and explicit. Outside it the base evaluates exactly as it did before, which is the whole difference between a bounded correction and inserting boundary keys: the latter alters the neighbouring segments, and the lane model says so. A LAYER HAS NO TIME SPACE OF ITS OWN, and this is the design question the doc left open. Its support and its values' keys are in the frames the base channel's keys are in — the node's. A correction on a lane is therefore in lane frames and reaches across the drawings exposed beneath it; one on a single occurrence is in that occurrence's frames and travels with it when the exposure moves. Ownership had already answered it, so there is no field to disagree with, and both halves are under test at lane level. Two things cost nothing, which is worth recording. A channel is ONE LEAF, so a correction persists inside it with no codec change at all. And `node/problems` already reports every channel's problems, so a malformed layer surfaces at the document level and in the sequence commands' post-check without plumbing. What is still missing is a command that MAKES one, and with it the question of how a view offers a constant, a ramp and a return over a selected range. The evaluator no longer has an opinion about that, which was the point. `offset` adds component-wise and never writes into a dense value, which is a view onto the block itself; a shape mismatch throws rather than being dropped, since a correction that silently does not take is the failure this design exists to prevent. `replace` can supply a value over an absent base and `offset` cannot, as animation-model.md required. 408 tests, 5,655 assertions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 16:07:38 -04:00
are also not implemented; a refusal is the current behavior where the model
The shot is as long as somebody said it was Trim, move and blank, and the decision they all three walked into: is the shot's length authored, or derived from what is in it? AUTHORED. `:frames` is the symbol's window — how long the shot IS — and the occupied extent of its lanes is a different fact, read off the occurrences. A command grows the window when the caller says `:grow-symbol` and NEVER shrinks it, so blanking the end of a shot leaves a shot with empty frames at the end. That is a true statement about what somebody authored, and the alternative is deleting the last drawing and quietly shortening the film. `finish` had the right behaviour by accident — `(apply max (:frames sym) ...)` — and now says which number is which: `needed` is where the occurrences reach, `:frames` is what was authored, and the only thing that makes the second follow the first is a caller asking. The three commands turned out to be one piece of geometry, which is `split`'s. A `:span` is in the occurrence's OWN frames and `:time` says where those land in the lane, so moving an edge of an exposure is ONE WRITE to `:span` and `:time` and `:playback` are never touched. `local` and `edged` are the whole of it, and split now goes through them too. trim narrows one edge and moves nothing else. Lengthening is `extend-hold`, which carries a ripple policy and a shot-length policy because it needs them; letting trim grow as well would give one gesture two sets of rules and a way to overlap its neighbour. move one write to `:time :at`, and a destination that would overlap is REFUSED rather than rippled. Moving a drawing and re-timing the ones around it are different intentions, and a move that pushed the rest would be the second wearing the first one's name. Clear the room first. blank leaves a gap and does not close it. Wholly inside the range goes, overlapping an end is trimmed to it, spanning the range is split — the one case that needs an ID, and it asks for one instead of inventing it. Because the source clock is untouched, trimming the front of a playing insert starts it LATER INTO its animation rather than restarting it, which is the difference between trimming and slipping and the reason they stay two commands. The test samples the frames it kept and asserts they show what they showed. Blanking leaves the drawings in the library. A lane does not own its content, and a drawing whose last exposure is gone is still a drawing somebody made. Overwrite is now `blank` then `place` and needs no policy argument of its own, which is why it still is not one. 424 tests, 5,749 assertions. The browser flow trims an exposure at the playhead, moves it into the gap that made, blanks it, and checks the shot is still as long as it was authored. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 16:32:59 -04:00
demands an explicit choice nobody has made yet. The suite stands at 424 tests
and 5,749 assertions, with `frontend/test/browser/sequence.mjs` driving the
A correction is a layer, and a layer's values are a channel `:over` was specified in animation-model.md, refused in two places, and produced by nothing: `check-unimplemented!` threw on read and `channel/problems` reported it. It reads now. This is the part of the model the rotoscoping half depends on — generate motion, correct it by hand, turn the knob, keep the correction — and it was the last thing in the design that had never been tried. The shape that made it small: A LAYER'S VALUES ARE A CHANNEL. {:id :nudge :support [88 98] :op :offset :values {:animated? true :interp :linear :keys {88 [2 0], 96 [0 0]}}} So the three commands the lane model asks for over a selected range — a constant adjustment, a ramp, a return motion — are one mechanism and not three: framed values say the same thing on every frame they cover, keyed values move, and neither needs a new way to say what a value is over time. A layer reads through `value-at` and `cursor` like any channel, which is also what stopped blending from becoming two implementations: `over-at` is shared, and the specification and the playback path differ only in how they READ a layer — recursively through `value-at`, or through a reading head of its own. One level deep; a layer's values may not carry layers, which the stack already orders. That was the risk worth spiking for. A cursor that drifts produces the wrong pose rather than an error, and a stack means several reading heads per channel where there was one. The agreement test that holds the cursor to the specification in forward, backward and random frame order now covers stacked channels too — including a layer whose head is asked for nothing across the long stretches outside its support and then asked again, which is where drift would hide. `:support` is half-open and explicit. Outside it the base evaluates exactly as it did before, which is the whole difference between a bounded correction and inserting boundary keys: the latter alters the neighbouring segments, and the lane model says so. A LAYER HAS NO TIME SPACE OF ITS OWN, and this is the design question the doc left open. Its support and its values' keys are in the frames the base channel's keys are in — the node's. A correction on a lane is therefore in lane frames and reaches across the drawings exposed beneath it; one on a single occurrence is in that occurrence's frames and travels with it when the exposure moves. Ownership had already answered it, so there is no field to disagree with, and both halves are under test at lane level. Two things cost nothing, which is worth recording. A channel is ONE LEAF, so a correction persists inside it with no codec change at all. And `node/problems` already reports every channel's problems, so a malformed layer surfaces at the document level and in the sequence commands' post-check without plumbing. What is still missing is a command that MAKES one, and with it the question of how a view offers a constant, a ramp and a return over a selected range. The evaluator no longer has an opinion about that, which was the point. `offset` adds component-wise and never writes into a dense value, which is a view onto the block itself; a shape mismatch throws rather than being dropped, since a correction that silently does not take is the failure this design exists to prevent. `replace` can supply a value over an absent base and `offset` cannot, as animation-model.md required. 408 tests, 5,655 assertions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 16:07:38 -04:00
editor through create, hold, overflow, undo, reuse, make unique, duplicate,
The shot is as long as somebody said it was Trim, move and blank, and the decision they all three walked into: is the shot's length authored, or derived from what is in it? AUTHORED. `:frames` is the symbol's window — how long the shot IS — and the occupied extent of its lanes is a different fact, read off the occurrences. A command grows the window when the caller says `:grow-symbol` and NEVER shrinks it, so blanking the end of a shot leaves a shot with empty frames at the end. That is a true statement about what somebody authored, and the alternative is deleting the last drawing and quietly shortening the film. `finish` had the right behaviour by accident — `(apply max (:frames sym) ...)` — and now says which number is which: `needed` is where the occurrences reach, `:frames` is what was authored, and the only thing that makes the second follow the first is a caller asking. The three commands turned out to be one piece of geometry, which is `split`'s. A `:span` is in the occurrence's OWN frames and `:time` says where those land in the lane, so moving an edge of an exposure is ONE WRITE to `:span` and `:time` and `:playback` are never touched. `local` and `edged` are the whole of it, and split now goes through them too. trim narrows one edge and moves nothing else. Lengthening is `extend-hold`, which carries a ripple policy and a shot-length policy because it needs them; letting trim grow as well would give one gesture two sets of rules and a way to overlap its neighbour. move one write to `:time :at`, and a destination that would overlap is REFUSED rather than rippled. Moving a drawing and re-timing the ones around it are different intentions, and a move that pushed the rest would be the second wearing the first one's name. Clear the room first. blank leaves a gap and does not close it. Wholly inside the range goes, overlapping an end is trimmed to it, spanning the range is split — the one case that needs an ID, and it asks for one instead of inventing it. Because the source clock is untouched, trimming the front of a playing insert starts it LATER INTO its animation rather than restarting it, which is the difference between trimming and slipping and the reason they stay two commands. The test samples the frames it kept and asserts they show what they showed. Blanking leaves the drawings in the library. A lane does not own its content, and a drawing whose last exposure is gone is still a drawing somebody made. Overwrite is now `blank` then `place` and needs no policy argument of its own, which is why it still is not one. 424 tests, 5,749 assertions. The browser flow trims an exposure at the playhead, moves it into the gap that made, blanks it, and checks the shot is still as long as it was authored. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 16:32:59 -04:00
split, insert, trim, move and blank. Rewrite tests that encode superseded
behavior rather than preserving behavior to keep them green.
An occurrence is a node, with a clock of its own A lane's drawings were going to be one instance whose source was a KEYED channel: frame 0 says `:drawing-a`, frame 4 says `:drawing-b`, and the cels of a row are that channel's keys. Two things followed from it, and both were wrong. The first is that playback meant whichever shape the channel happened to have. A framed source played its symbol; a keyed source froze the selected frame. So `node/placed-at` read animation out of storage, and adding an ordinary key to a still turned it into an animation — the last-key bug, which was not a bug in the code so much as the rule working as written. But WHICH drawing is used and HOW time runs inside it are independent questions, and all four combinations are ordinary: hold one drawing, play one animation, cut between held drawings, cut between playing ones. So an occurrence names one symbol in `:source {:symbol ...}` and says how its source time advances in `:playback {:in :speed :end}` — `source = in + speed * f`, a hold being speed 0, with `:stop`, `:hold` or `:loop` at the end named rather than guessed. `node/placed-frame` samples it forwards, which works for holds too, and `node/source-time` is the separate, invertible edit map, nil where inversion is meaningless. The two were one function before, and a hold had to lie about one of them. The second is that a keyed source only looked necessary because an occurrence was assumed to need a ROW. It does not. A lane is a group with `:layout :sequence`, its occurrences are ordinary instances in the same flat node map, and `timeline/rows` draws them as cel blocks on the lane's own row: twelve exposures, one row, each cel still separately selectable and addressable. The vertical growth that justified the keyed source is a presentation question, and it is answered in the view. `arthur.domain.sequence` holds the first commands over that shape — add lane, append drawing, extend hold — each one history step, each refusing rather than half-applying. Extending a hold leaves the lane's keys at their authored times, because you are adjusting drawings underneath timed motion; a correction owned by an occurrence travels with it. Ownership does that work, so no key needs a flag saying what it follows. Ripple past the symbol's end is refused with the frame count it would need, and `:extent :grow-symbol` is the caller saying yes. `clip/blank` no longer carries `:subjects {} :features {} :groups {}`. Empty maps write no leaf, so a blank document could not survive its own round trip — `leaf/leaves` promises exactness and was the only honest side of that. Documents are schema 3. A version 2 document is not read; nothing here converts one. `docs/lane-model.md` is the design, and says which of its parts are built. 392 tests, 5,525 assertions, and `test/browser/sequence.mjs` drives the editor through create, hold, explicit overflow and undo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:20:23 -04:00
Build small adversarial documents and test their domain operations before
expanding the interface:
| Scenario | Required invariant |
| --- | --- |
| Same drawing exposed twice, then one made unique | Linked edits affect both before copying and only the selected content after |
| Holds, playing inserts, nonzero in-points, and gaps on one lane | Source behavior is independent of property key count and channel encoding |
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
| Adjacent cels, final hold, split, trim, ripple, and overwrite | Exact boundaries, stable IDs, deterministic collision handling |
| Extend a hold under lane keys and cel-local corrections | Lane key times remain fixed; later cel corrections travel with their owners; source playback origins survive |
An occurrence is a node, with a clock of its own A lane's drawings were going to be one instance whose source was a KEYED channel: frame 0 says `:drawing-a`, frame 4 says `:drawing-b`, and the cels of a row are that channel's keys. Two things followed from it, and both were wrong. The first is that playback meant whichever shape the channel happened to have. A framed source played its symbol; a keyed source froze the selected frame. So `node/placed-at` read animation out of storage, and adding an ordinary key to a still turned it into an animation — the last-key bug, which was not a bug in the code so much as the rule working as written. But WHICH drawing is used and HOW time runs inside it are independent questions, and all four combinations are ordinary: hold one drawing, play one animation, cut between held drawings, cut between playing ones. So an occurrence names one symbol in `:source {:symbol ...}` and says how its source time advances in `:playback {:in :speed :end}` — `source = in + speed * f`, a hold being speed 0, with `:stop`, `:hold` or `:loop` at the end named rather than guessed. `node/placed-frame` samples it forwards, which works for holds too, and `node/source-time` is the separate, invertible edit map, nil where inversion is meaningless. The two were one function before, and a hold had to lie about one of them. The second is that a keyed source only looked necessary because an occurrence was assumed to need a ROW. It does not. A lane is a group with `:layout :sequence`, its occurrences are ordinary instances in the same flat node map, and `timeline/rows` draws them as cel blocks on the lane's own row: twelve exposures, one row, each cel still separately selectable and addressable. The vertical growth that justified the keyed source is a presentation question, and it is answered in the view. `arthur.domain.sequence` holds the first commands over that shape — add lane, append drawing, extend hold — each one history step, each refusing rather than half-applying. Extending a hold leaves the lane's keys at their authored times, because you are adjusting drawings underneath timed motion; a correction owned by an occurrence travels with it. Ownership does that work, so no key needs a flag saying what it follows. Ripple past the symbol's end is refused with the frame count it would need, and `:extent :grow-symbol` is the caller saying yes. `clip/blank` no longer carries `:subjects {} :features {} :groups {}`. Empty maps write no leaf, so a blank document could not survive its own round trip — `leaf/leaves` promises exactness and was the only honest side of that. Documents are schema 3. A version 2 document is not read; nothing here converts one. `docs/lane-model.md` is the design, and says which of its parts are built. 392 tests, 5,525 assertions, and `test/browser/sequence.mjs` drives the editor through create, hold, explicit overflow and undo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:20:23 -04:00
| Ripple beyond the symbol end | Explicit extent policy; refusal leaves the document unchanged; resizing and retiming undo together |
| Girl on twos over a moving background | Drawing cadence does not quantize either lane's continuous properties |
| Three-frame correction crossing a drawing boundary | Exact support, same result outside it, one undo step |
| Nested retiming, holds, loops, and fractional sampling | Explicit time spaces; ambiguous inverse edits cannot silently choose a target |
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
| Audio inside changing source cels | Only active intervals sound, with correct trim and source timing |
An occurrence is a node, with a clock of its own A lane's drawings were going to be one instance whose source was a KEYED channel: frame 0 says `:drawing-a`, frame 4 says `:drawing-b`, and the cels of a row are that channel's keys. Two things followed from it, and both were wrong. The first is that playback meant whichever shape the channel happened to have. A framed source played its symbol; a keyed source froze the selected frame. So `node/placed-at` read animation out of storage, and adding an ordinary key to a still turned it into an animation — the last-key bug, which was not a bug in the code so much as the rule working as written. But WHICH drawing is used and HOW time runs inside it are independent questions, and all four combinations are ordinary: hold one drawing, play one animation, cut between held drawings, cut between playing ones. So an occurrence names one symbol in `:source {:symbol ...}` and says how its source time advances in `:playback {:in :speed :end}` — `source = in + speed * f`, a hold being speed 0, with `:stop`, `:hold` or `:loop` at the end named rather than guessed. `node/placed-frame` samples it forwards, which works for holds too, and `node/source-time` is the separate, invertible edit map, nil where inversion is meaningless. The two were one function before, and a hold had to lie about one of them. The second is that a keyed source only looked necessary because an occurrence was assumed to need a ROW. It does not. A lane is a group with `:layout :sequence`, its occurrences are ordinary instances in the same flat node map, and `timeline/rows` draws them as cel blocks on the lane's own row: twelve exposures, one row, each cel still separately selectable and addressable. The vertical growth that justified the keyed source is a presentation question, and it is answered in the view. `arthur.domain.sequence` holds the first commands over that shape — add lane, append drawing, extend hold — each one history step, each refusing rather than half-applying. Extending a hold leaves the lane's keys at their authored times, because you are adjusting drawings underneath timed motion; a correction owned by an occurrence travels with it. Ownership does that work, so no key needs a flag saying what it follows. Ripple past the symbol's end is refused with the frame count it would need, and `:extent :grow-symbol` is the caller saying yes. `clip/blank` no longer carries `:subjects {} :features {} :groups {}`. Empty maps write no leaf, so a blank document could not survive its own round trip — `leaf/leaves` promises exactness and was the only honest side of that. Documents are schema 3. A version 2 document is not read; nothing here converts one. `docs/lane-model.md` is the design, and says which of its parts are built. 392 tests, 5,525 assertions, and `test/browser/sequence.mjs` drives the editor through create, hold, explicit overflow and undo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:20:23 -04:00
| Regenerate with corrections and a topology change | Compatible edits survive; incompatible ones produce actionable conflicts |
| Reference cycles and deletion of reused content | Inactive references are validated too; no dangling references |
| Save/load and command undo/redo | Identity, source maps, corrections, and evaluation round-trip |
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
| Timeline and cel-sheet invocation of one command | Identical document changes and selection targets |
An occurrence is a node, with a clock of its own A lane's drawings were going to be one instance whose source was a KEYED channel: frame 0 says `:drawing-a`, frame 4 says `:drawing-b`, and the cels of a row are that channel's keys. Two things followed from it, and both were wrong. The first is that playback meant whichever shape the channel happened to have. A framed source played its symbol; a keyed source froze the selected frame. So `node/placed-at` read animation out of storage, and adding an ordinary key to a still turned it into an animation — the last-key bug, which was not a bug in the code so much as the rule working as written. But WHICH drawing is used and HOW time runs inside it are independent questions, and all four combinations are ordinary: hold one drawing, play one animation, cut between held drawings, cut between playing ones. So an occurrence names one symbol in `:source {:symbol ...}` and says how its source time advances in `:playback {:in :speed :end}` — `source = in + speed * f`, a hold being speed 0, with `:stop`, `:hold` or `:loop` at the end named rather than guessed. `node/placed-frame` samples it forwards, which works for holds too, and `node/source-time` is the separate, invertible edit map, nil where inversion is meaningless. The two were one function before, and a hold had to lie about one of them. The second is that a keyed source only looked necessary because an occurrence was assumed to need a ROW. It does not. A lane is a group with `:layout :sequence`, its occurrences are ordinary instances in the same flat node map, and `timeline/rows` draws them as cel blocks on the lane's own row: twelve exposures, one row, each cel still separately selectable and addressable. The vertical growth that justified the keyed source is a presentation question, and it is answered in the view. `arthur.domain.sequence` holds the first commands over that shape — add lane, append drawing, extend hold — each one history step, each refusing rather than half-applying. Extending a hold leaves the lane's keys at their authored times, because you are adjusting drawings underneath timed motion; a correction owned by an occurrence travels with it. Ownership does that work, so no key needs a flag saying what it follows. Ripple past the symbol's end is refused with the frame count it would need, and `:extent :grow-symbol` is the caller saying yes. `clip/blank` no longer carries `:subjects {} :features {} :groups {}`. Empty maps write no leaf, so a blank document could not survive its own round trip — `leaf/leaves` promises exactness and was the only honest side of that. Documents are schema 3. A version 2 document is not read; nothing here converts one. `docs/lane-model.md` is the design, and says which of its parts are built. 392 tests, 5,525 assertions, and `test/browser/sequence.mjs` drives the editor through create, hold, explicit overflow and undo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:20:23 -04:00
| Random forward/backward seeks and export | Reference and optimized evaluation agree, including defaults and absence |
| Concurrent commands on overlapping and disjoint targets | Transactions remain valid; conflicts are explicit and undo preserves others' work |
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
Implementation order: cel ownership and playback semantics; shared
An occurrence is a node, with a clock of its own A lane's drawings were going to be one instance whose source was a KEYED channel: frame 0 says `:drawing-a`, frame 4 says `:drawing-b`, and the cels of a row are that channel's keys. Two things followed from it, and both were wrong. The first is that playback meant whichever shape the channel happened to have. A framed source played its symbol; a keyed source froze the selected frame. So `node/placed-at` read animation out of storage, and adding an ordinary key to a still turned it into an animation — the last-key bug, which was not a bug in the code so much as the rule working as written. But WHICH drawing is used and HOW time runs inside it are independent questions, and all four combinations are ordinary: hold one drawing, play one animation, cut between held drawings, cut between playing ones. So an occurrence names one symbol in `:source {:symbol ...}` and says how its source time advances in `:playback {:in :speed :end}` — `source = in + speed * f`, a hold being speed 0, with `:stop`, `:hold` or `:loop` at the end named rather than guessed. `node/placed-frame` samples it forwards, which works for holds too, and `node/source-time` is the separate, invertible edit map, nil where inversion is meaningless. The two were one function before, and a hold had to lie about one of them. The second is that a keyed source only looked necessary because an occurrence was assumed to need a ROW. It does not. A lane is a group with `:layout :sequence`, its occurrences are ordinary instances in the same flat node map, and `timeline/rows` draws them as cel blocks on the lane's own row: twelve exposures, one row, each cel still separately selectable and addressable. The vertical growth that justified the keyed source is a presentation question, and it is answered in the view. `arthur.domain.sequence` holds the first commands over that shape — add lane, append drawing, extend hold — each one history step, each refusing rather than half-applying. Extending a hold leaves the lane's keys at their authored times, because you are adjusting drawings underneath timed motion; a correction owned by an occurrence travels with it. Ownership does that work, so no key needs a flag saying what it follows. Ripple past the symbol's end is refused with the frame count it would need, and `:extent :grow-symbol` is the caller saying yes. `clip/blank` no longer carries `:subjects {} :features {} :groups {}`. Empty maps write no leaf, so a blank document could not survive its own round trip — `leaf/leaves` promises exactness and was the only honest side of that. Documents are schema 3. A version 2 document is not read; nothing here converts one. `docs/lane-model.md` is the design, and says which of its parts are built. 392 tests, 5,525 assertions, and `test/browser/sequence.mjs` drives the editor through create, hold, explicit overflow and undo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:20:23 -04:00
commands and validation; correction layers and time-addressing contracts; then
One word for one thing: it is a cel Four words had accumulated for a node that puts a symbol inside another symbol. `instance` was the document's, from the model. `placement` was the stage and export work's. `occurrence` came in with the lane model. `exposure` came in with me, because it is what an animator would say. Three bodies of work each brought a word and none of them retired anybody else's, which is how you get a codebase that reads like three people describing the same object over each other. It is a CEL. One drawing, held for some duration. `cel` was already the view's word — `.tl-cel`, the cel strip — so choosing it was also the smallest change, and the app already says "drawing" for the content, which is what frees the word up: historically a cel IS the celluloid with the drawing on it, and that sense has somewhere else to live here. instance the `:kind`. The general thing, anywhere in a document. cel an instance in a lane. UI labels, command names, prose. lane the group with `:layout :sequence`. drawing the content a cel names. placement kept ONLY for where a node sits — `nest/placement` and the transform that puts a face on the stage. Retired as a noun for the node itself. occurrence gone. AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already existed and means something else entirely: how many frames each step of a subtree lasts, which is what shooting on twos is. Had the block been called an exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure` would have been permanently confusable with it. Choosing `cel` lets the word `exposure` keep the thing it actually names, and every remaining use of it in `src` is now that one. `:layout :sequence` stays as the field, and it is the one place two words are kept deliberately: the layout names the RULE — children follow one another and may not overlap — and a group carrying it is called a lane. `node/lane?` says so where the two meet. The second view is traditionally the exposure sheet. It will be the CEL SHEET, for one vocabulary. Renamed with a script and then read, because a blind pass does real damage: it produced "an cel" thirty times, renamed the `::exposure` sub that is about the `:expose` grid, and turned an "exposure grid" into a "cel grid" in two docstrings. All three classes are fixed. `arthur.domain.sequence` is now `arthur.domain.lane`, which is what its test file was already called. 424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`, renamed too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 19:43:18 -04:00
breadcrumb, cel strip, and a cel-sheet projection. Use those two temporal
An occurrence is a node, with a clock of its own A lane's drawings were going to be one instance whose source was a KEYED channel: frame 0 says `:drawing-a`, frame 4 says `:drawing-b`, and the cels of a row are that channel's keys. Two things followed from it, and both were wrong. The first is that playback meant whichever shape the channel happened to have. A framed source played its symbol; a keyed source froze the selected frame. So `node/placed-at` read animation out of storage, and adding an ordinary key to a still turned it into an animation — the last-key bug, which was not a bug in the code so much as the rule working as written. But WHICH drawing is used and HOW time runs inside it are independent questions, and all four combinations are ordinary: hold one drawing, play one animation, cut between held drawings, cut between playing ones. So an occurrence names one symbol in `:source {:symbol ...}` and says how its source time advances in `:playback {:in :speed :end}` — `source = in + speed * f`, a hold being speed 0, with `:stop`, `:hold` or `:loop` at the end named rather than guessed. `node/placed-frame` samples it forwards, which works for holds too, and `node/source-time` is the separate, invertible edit map, nil where inversion is meaningless. The two were one function before, and a hold had to lie about one of them. The second is that a keyed source only looked necessary because an occurrence was assumed to need a ROW. It does not. A lane is a group with `:layout :sequence`, its occurrences are ordinary instances in the same flat node map, and `timeline/rows` draws them as cel blocks on the lane's own row: twelve exposures, one row, each cel still separately selectable and addressable. The vertical growth that justified the keyed source is a presentation question, and it is answered in the view. `arthur.domain.sequence` holds the first commands over that shape — add lane, append drawing, extend hold — each one history step, each refusing rather than half-applying. Extending a hold leaves the lane's keys at their authored times, because you are adjusting drawings underneath timed motion; a correction owned by an occurrence travels with it. Ownership does that work, so no key needs a flag saying what it follows. Ripple past the symbol's end is refused with the frame count it would need, and `:extent :grow-symbol` is the caller saying yes. `clip/blank` no longer carries `:subjects {} :features {} :groups {}`. Empty maps write no leaf, so a blank document could not survive its own round trip — `leaf/leaves` promises exactness and was the only honest side of that. Documents are schema 3. A version 2 document is not read; nothing here converts one. `docs/lane-model.md` is the design, and says which of its parts are built. 392 tests, 5,525 assertions, and `test/browser/sequence.mjs` drives the editor through create, hold, explicit overflow and undo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-30 15:20:23 -04:00
views plus direct stage editing to prove the model before broadening the UI.
A new presentation should not require duplicate animation state. A genuinely new
authoring capability may require new domain data. The model is successful when
such additions have a clear owner and compose with existing operations, not when
it can claim that no future feature will ever need another field.