A lane was a drawing lane: the only thing that could go in one was a one-frame held cel, and every other symbol instance stayed a permanent root row of its own. Those are not two kinds of timing, they are one kind with two creation policies. `lane/place-symbol` drops any library symbol in as a clip that plays naturally at speed one, `lane/adopt` moves an instance that is already in the document into a lane keeping its source, span, playback and corrections, and `append-drawing`/`overwrite-drawing` keep being the policy that makes a new empty symbol a one-frame hold. The child shape they produce is the same. Both new commands claim their interval through `blank` before they write, so the partition rule is unchanged and unduplicated: placing into occupied lane time trims, removes or splits the incumbents, and a lane still never stores an overlap. Real compositing overlap is another lane, where the order is explicit. Creating a symbol with nothing aimed now makes a lane and a clip in it instead of a loose root instance, and a pool drop prefers an explicitly targeted lane, then the selected one, and makes a lane only when there is neither. That is what stops the row-per-symbol growth coming back in through the drop path, and it is why `add-lane` now takes a z in front of the existing root nodes and calls what it makes a "lane" rather than "drawings". The timeline learned the two gestures that a generic lane needs. A clip body dragged over another lane's track previews there as a dashed block and lands through `::adopt-in-lane`; the track is found with `elementsFromPoint` and its selection read back off the element, because a pointer capture does not retarget. A pool drop over an existing lane previews as a dashed clip inside that lane instead of a temporary new row that appears and then vanishes -- which also needed the drag-leave check to be geometric, since inserting the preview changes the element under the pointer and Chromium then reports a leave with no related target. Lanes are renameable from their label, by double-click, F2, or the pencil, through `::rename-node`. `symbol/lane-cels` is `symbol/lane-clips`, and the vocabulary table in the handoff now separates the two words it had merged: a clip is an instance in a lane, and a cel is specifically the one-frame held source that drawing creation makes. Keeping `cel` for the policy is what lets the lane stop being about drawings at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
34 KiB
The Lane Model
Revised 2026-10-01. Clip ownership, source playback, one-row generic lanes, direct clip movement and edge editing, correction evaluation, and correction authoring for rotation and position are implemented. The former cel-sheet projection was removed: the timeline is the single timing interface. Sections below that describe a cel sheet are retained as design history and are superseded by this revision. Retiming commands are not. See the status note under Proof obligations.
Lane and cel handoff records what is built, the decisions that are settled, and what to do next.
This revises the Claude artifact The Lane Model.
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 on the stage, arranging clips, timing cels, editing curves, and generating 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.
Content, cels, lanes, and rows
These have different identities and responsibilities:
| Concept | Owns | Example |
|---|---|---|
| Content | Reusable nodes and their animation | Drawing a2, an animated head, or a sound asset |
| 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 |
Use the existing instance/node identity mechanism for cels. A cel should not acquire a second identity system just because it is shown as a cel. Lanes group cels; they do not introduce another node-holding content type. 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.
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 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.
Cels are the canonical authored arrangement. A source-at-time channel or 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 uses lightweight keys; it does not need cel objects.
A lane has non-overlapping half-open cel intervals [start,end)
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.
Properties can belong to content, one cel, or the lane. For example:
- Rotate the reusable drawing: all its uses change.
- Rotate one cel: only that cel changes.
- Animate the lane's rotation: whichever drawing is showing follows it.
A cel can have its own transform, gain, corrections, and source timing while remaining a block in the same timeline row. Independent treatment never requires a new row or an otherwise unnecessary wrapper symbol.
Concrete candidate: a lane and ordinary instances
A lane is a group node with :layout :sequence. Its cels are ordinary
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
use the existing representation; cel source references and playback have
replaced the [:source] channel, which no longer exists.
;; 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}}}}
:cel-a
{:id :cel-a :kind :instance :parent :girl :z "a"
:time {:at 0 :rate 1} :span [0 4]
:source {:symbol :drawing-a}
:playback {:in 0 :speed 0 :end :stop}}
:cel-b
{:id :cel-b :kind :instance :parent :girl :z "b"
: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}}}
The lane's rotation reads lane time. Each cel's channels read cel 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 cel transform and then the content's own transform.
: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.
The current sequence group contains visual symbol clips. Audio remains an
independent root node (and can be linked to picture); if audio lanes are added,
their capability must be explicit rather than inferred per frame.
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
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.
A cel names content and describes how its source time is sampled. In the basic case, after mapping lane time into cel time:
source_time = in_point + speed × cel_time
A newly created cel starts at local time zero. Moving it preserves this 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 |
| Cut between animations | Several cels, each with its own in-point and speed |
| 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.
Audio shares cel arrangement, trimming, gain ownership, and clock mapping. 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. Actual audio scheduling must follow active cels, including gaps and cuts, 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, symbol/lane, cel, or source. Store authored frame coordinates exactly; 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 displayed cels to one source time. APIs must distinguish forward sampling from inverse editing, and expose enough context to resolve a cel or 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 can mean hold without making the cel's own edit clock non-invertible. 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
that cel and picture-rate sampling are simply one floor is insufficient:
noninteger grids and source-frame quantization require specified behavior.
Identity timing can be implicit; remove :time :mode if it only duplicates that.
A cel interval is authored. Lane content extent is derived from its cels, including the explicit end of the last one. A separately authored 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 versions of cel extension.
An edit target identifies the symbol, cel path, selected entities or properties, and the time range with its space. Navigation also distinguishes editing shared content directly from editing it through a particular cel. 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, blank range, split, trim, move, extend cel, slip source, retime, and apply a bounded property edit. Ripple/overwrite policy and the set of affected lanes are explicit command arguments. Preview consequences before committing a gesture.
New drawing creates fresh empty content and a cel. Blank range removes content coverage without inventing a hidden drawing. These are different actions. Reuse creates another cel pointing at existing content. Duplicate creates a new content identity. Make unique rebinds the selected cel only.
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.
Default timing behavior: cel edits preserve lane keys
Working default from the follow-up discussion: extending a drawing's hold changes cel timing, leaving lane animation at its authored times. The user raised keeping keyframes in place as a possibility; this is the proposed predictable default, not a claim that they selected every timing policy below.
Ownership supplies the remaining rule: properties attached to a cel 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.
For the concrete example, extend :cel-a by two lane frames with ripple:
| Fact | Before | After |
|---|---|---|
| Cel A's lane interval | [0,4) |
[0,6) |
| Cel B's lane interval | [4,8) |
[6,10) |
| 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. That is the intended consequence of changing cels underneath timed motion. The position correction stays attached to drawing B's cel. Neither the background's keys nor audio on another lane moves.
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
extends A's local support by the delta converted through A's placement rate,
and shifts subsequent cel placements by that delta in lane time. It does
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 part of its transaction; it must not silently truncate later cels or grow 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.
Retime performance is a separate operation over explicitly selected cels 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 rather than approximating it with a cel ripple.
The initial UI should default stage transforms to the lane when drawing in a cel workflow, so movement usually remains independent of cel timing. The inspector names the target: lane motion, this cel, or shared drawing. 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 currently inactive cels. Authored folders, favorites, and labels remain 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 content identity, and cel context when applicable. Show local time and 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 reuse and offers Make this cel unique. Names help identify content; 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 |
| Inspector | Selected content/cel/lane properties and valid key controls |
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
- In
main, create a girl lane and a new drawing. Draw; use New drawing (N) to create the next one with the previous cel ghosted behind it. - Use Duplicate drawing (
D) when the current shapes are the starting point. Use Reuse drawing for a deliberately linked cel. The UI shows the difference before an edit can change other uses. - Time the performance. Hold longer (
H) extends the selected cel and ripples later cels in the explicitly targeted lane. A trim gesture can use overwrite instead. The preview shows which boundaries will move. - 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.
- Place the background in a lane below. Its source holds one frame throughout its cel. Key the lane's X position at the beginning and end and choose linear interpolation. The background slides while the girl's drawings cut.
- 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.
- Insert a playing animated symbol among the girl's held drawings. Set that cel's source playback to advance. No lane conversion is required.
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 graph editor edits the same properties; the stage resolves the same document. Onion skin is configurable and counts neighboring cel events, skipping gaps 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
The source-channel prototype has been removed: a cel names one symbol
and carries its own playback clock, and node/problems rejects the old
[:source] channel. What a lane IS lives in arthur.domain.symbol beside the
other rules about a node map; arthur.domain.lane holds the commands over
one — add lane, place a drawing (new, reused or duplicated), make unique, split,
trim, move, blank and extend hold. Each is one history step, and each refuses rather than
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
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
where nothing has to move. Placing ripples — cels at or after the
position move later by the new cel's duration — and :keep versus
:grow-symbol still decides what happens at the shot's end. OVERWRITE is not a
policy argument yet, deliberately: taking frames away from the cel
already there is trimming, and until trim exists, placement that would need it
refuses instead of approximating it. A position inside an existing cel
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.
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 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
extent of its lanes is a different fact derived from the cels. A command
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
composition. Blank leaves a gap and does not close it; a cel wholly inside
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.
All three are the same geometry as split: a :span is in the cel's own
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.
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 cel travels with that cel when a hold before it grows.
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.
Overwrite is blank followed by non-rippling placement, composed inside one
transaction; insertion keeps its ripple rule. Still unbuilt: slip source,
retime, and deleting reused content. A lane cannot hold AUDIO cels — lane-problems
requires visual ones, though this document says a lane may hold either and
should reject only a mixture.
domain/correction.cljs now produces Constant adjustment, Ramp, and Return
motion layers for rotation and position. The inspector exposes them on a selected
lane or cel using an explicit range in that owner's frames; this deliberately
leaves displayed-range dragging through nested or retimed owners for later. One
Apply is one undo step. Conflicted layers are listed, can be removed, and can be
retried when the complete ordered stack is compatible again. Validation,
conflict reporting, and regeneration share that ordered-stack rule, including
coverage by adjacent replacement layers. Slip source and retime are still not
implemented; a refusal is the current behavior where the model demands an
explicit choice nobody has made yet.
The cel sheet is the same projected cels and selection addresses with its axes
turned: frames down and lanes across, so commands selected there and in the
timeline have identical targets; a gap selects its column's lane rather than
retaining a stale selection from another column. The suite stands at 437 tests and 5,804
assertions, with frontend/test/browser/lane.mjs driving the editor through
create, hold, overflow, undo, reuse, make unique, duplicate, split, insert,
trim, move, blank, correction authoring, and two-lane sheet targeting. Rewrite tests that encode superseded
behavior rather than preserving behavior to keep them green.
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 |
| 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 |
| 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 |
| Audio inside changing source cels | Only active intervals sound, with correct trim and source timing |
| 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 |
| Timeline and cel-sheet invocation of one command | Identical document changes and selection targets |
| 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 |
Implementation order: cel ownership and playback semantics; shared commands and validation; correction layers and time-addressing contracts; then breadcrumb, cel strip, and a cel-sheet projection. Use those two temporal 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.