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>
545 lines
32 KiB
Markdown
545 lines
32 KiB
Markdown
# The Lane Model
|
||
|
||
Revised 2026-09-30. Target design. Occurrence ownership, source playback, the
|
||
content and exposure commands, placement anywhere in a lane, a one-row cel strip
|
||
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
|
||
[Proof obligations](#proof-obligations-and-implementation-order).
|
||
|
||
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
|
||
on the stage, arranging clips, timing exposures, 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, occurrences, 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 |
|
||
| Occurrence | One use of content, its interval, source playback, and local treatment | `a2` exposed on frames 12–16 |
|
||
| Lane | A sequence of occurrences and shared properties | The girl's drawings and the girl's overall transform |
|
||
| View row or column | Presentation and editor state | Timeline row, exposure-sheet column, or property curve |
|
||
|
||
Use the existing instance/node identity mechanism for occurrences. An exposure
|
||
should not acquire a second identity system just because it is shown as a cel.
|
||
Lanes group occurrences; 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 occurrence has a stable ID. Moving it, changing its hold, swapping its source,
|
||
or trimming it preserves that ID. Repeating it creates a new occurrence 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.
|
||
|
||
Occurrences 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 occurrence objects.
|
||
|
||
A sequence lane has non-overlapping half-open occurrence 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 occurrence, or the lane. For example:
|
||
|
||
- Rotate the reusable drawing: all its uses change.
|
||
- Rotate one occurrence: only that exposure changes.
|
||
- Animate the lane's rotation: whichever drawing is showing follows it.
|
||
|
||
An occurrence 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 sequence group and ordinary instances
|
||
|
||
A lane is a group node with `:layout :sequence`. Its occurrences 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; occurrence source references and playback have
|
||
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}}}}
|
||
|
||
:exposure-a
|
||
{:id :exposure-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}}
|
||
|
||
:exposure-b
|
||
{:id :exposure-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 occurrence's channels read occurrence
|
||
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
|
||
occurrence 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 sequence group may contain visual occurrences or audio occurrences; its
|
||
capability must reject an incompatible mixture rather than infer it per frame.
|
||
|
||
A source reference is fixed within an occurrence. The lane changes content when
|
||
another occurrence 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.
|
||
|
||
An occurrence names content and describes how its source time is sampled. In the
|
||
basic case, after mapping lane time into occurrence time:
|
||
|
||
```text
|
||
source_time = in_point + speed × occurrence_time
|
||
```
|
||
|
||
A newly created occurrence 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 occurrences, 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 occurrence 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 occurrences, 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, occurrence, 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 occurrences to one source time. APIs must distinguish forward sampling
|
||
from inverse editing, and expose enough context to resolve an occurrence 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 occurrence'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 exposure 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.
|
||
|
||
An occurrence interval is authored. Lane content extent is derived from its
|
||
occurrences, 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 exposure extension.
|
||
|
||
An edit target identifies the symbol, occurrence 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 occurrence.
|
||
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 exposure, 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 an occurrence. Blank range removes
|
||
content coverage without inventing a hidden drawing. These are different actions.
|
||
Reuse creates another occurrence pointing at existing content. Duplicate creates
|
||
a new content identity. Make unique rebinds the selected occurrence 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: exposure edits preserve lane keys
|
||
|
||
Working default from the follow-up discussion: extending a drawing's hold changes
|
||
exposure 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 an occurrence
|
||
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 `:exposure-a` by two lane frames with ripple:
|
||
|
||
| Fact | Before | After |
|
||
| --- | --- | --- |
|
||
| Exposure A's lane interval | `[0,4)` | `[0,6)` |
|
||
| Exposure 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 exposures underneath timed motion.
|
||
The position correction stays attached to drawing B's occurrence. Neither the
|
||
background's keys nor audio on another lane moves.
|
||
|
||
The command contract for this edit names the symbol and occurrence, a delta in
|
||
lane frames, `:ripple` behavior, and an explicit scope of exposure timing. It
|
||
extends A's local support by the delta converted through A's placement rate,
|
||
and shifts subsequent occurrence 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 occurrences 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 occurrences
|
||
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 an exposure ripple.
|
||
|
||
The initial UI should default stage transforms to the lane when drawing in a cel
|
||
workflow, so movement usually remains independent of exposure timing. The
|
||
inspector names the target: lane motion, this occurrence, 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 occurrences. 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 occurrence 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 occurrence 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/occurrence/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
|
||
|
||
1. In `main`, create a girl lane and a new drawing. Draw; use New drawing (`N`)
|
||
to create the next one with the previous exposure ghosted behind it.
|
||
2. Use Duplicate drawing (`D`) when the current shapes are the starting point.
|
||
Use Reuse drawing for a deliberately linked exposure. The UI shows the
|
||
difference before an edit can change other uses.
|
||
3. Time the performance. Hold longer (`H`) extends the selected occurrence and
|
||
ripples later occurrences in the explicitly targeted lane. A trim gesture
|
||
can use overwrite instead. The preview shows which boundaries will move.
|
||
4. Choose a two-frame default exposure for newly created drawings, or run a
|
||
separate Retime exposures command on a selected range. This does not quantize
|
||
lane transforms or silently retime already authored exposures.
|
||
5. Place the background in a lane below. Its source holds one frame throughout
|
||
its occurrence. Key the lane's X position at the beginning and end and choose
|
||
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
|
||
occurrence's source playback to advance. No lane conversion is required.
|
||
|
||
The timeline shows named exposure blocks with property marks and optional curve
|
||
subrows. The exposure sheet shows the same occurrences by frame and lane. The
|
||
graph editor edits the same properties; the stage resolves the same document.
|
||
Onion skin is configurable and counts neighboring exposure 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: an occurrence 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.sequence` holds the commands over
|
||
one — add lane, place a drawing (new, reused or duplicated), make unique, split,
|
||
and extend hold. Each is one history step, and each refuses rather than
|
||
half-applying. The timeline draws a lane's occurrences as cel blocks on the
|
||
lane's own row, and offers Make unique only where the selected exposure 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 — occurrences at or after the
|
||
position move later by the new exposure'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 occurrence
|
||
already there is trimming, and until `trim` exists, placement that would need it
|
||
refuses instead of approximating it. A position inside an existing exposure
|
||
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.
|
||
|
||
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
|
||
exposure travels with that exposure 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.
|
||
|
||
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
|
||
offers those three over a selected range, and how it offers a conflict for
|
||
resolution. Overwrite, the range and retiming
|
||
commands (blank, trim, move, slip source, retime) and the exposure-sheet view
|
||
are also not implemented; a refusal is the current behavior where the model
|
||
demands an explicit choice nobody has made yet. The suite stands at 414 tests
|
||
and 5,696 assertions, with `frontend/test/browser/sequence.mjs` driving the
|
||
editor through create, hold, overflow, undo, reuse, make unique, duplicate,
|
||
split and insert. 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 occurrences, final hold, split, trim, ripple, and overwrite | Exact boundaries, stable IDs, deterministic collision handling |
|
||
| Extend a hold under lane keys and occurrence-local corrections | Lane key times remain fixed; later occurrence 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 occurrences | 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 exposure-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: occurrence ownership and playback semantics; shared
|
||
commands and validation; correction layers and time-addressing contracts; then
|
||
breadcrumb, cel strip, and an exposure-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.
|