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>
This commit is contained in:
Your Name 2026-09-30 19:43:18 -04:00
parent 76106d36ee
commit 598c186c4f
21 changed files with 427 additions and 419 deletions

View file

@ -822,7 +822,7 @@ collaborator's keying. The fix is addressing, not an algorithm:
``` ```
palette palette
sequence/:sid lane/:sid
clip/:cid/timing clip rate clip/:cid/timing clip rate
clip/:cid/subject/:sid tracked subject and settings clip/:cid/subject/:sid tracked subject and settings
clip/:cid/feature/:fid tracked feature and settings clip/:cid/feature/:fid tracked feature and settings

View file

@ -1,7 +1,7 @@
# The Lane Model # The Lane Model
Revised 2026-09-30. Target design. Occurrence ownership, source playback, the Revised 2026-09-30. Target design. Cel ownership, source playback, the
content and exposure commands, placement anywhere in a lane, a one-row cel strip content and cel commands, placement anywhere in a lane, a one-row cel strip
and the correction-layer evaluator are implemented; the commands that produce a and the correction-layer evaluator are implemented; the commands that produce a
correction, overwrite, the retiming commands and the remaining views are not. correction, overwrite, the retiming commands and the remaining views are not.
See the status note under See the status note under
@ -21,7 +21,7 @@ and conflicting proposals in `animation-model.md`, `timing-model.md`, and
## Goal and compatibility policy ## Goal and compatibility policy
Arthur is one animation document with several ways to see and edit it: drawing 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 on the stage, arranging clips, timing cels, editing curves, and generating
motion from footage. Each view exposes relevant facts and invokes shared editing motion from footage. Each view exposes relevant facts and invokes shared editing
operations. Switching views must preserve the meaning of the work. operations. Switching views must preserve the meaning of the work.
@ -48,61 +48,61 @@ rules. Minimizing field count is secondary to representing independent choices.
export, and optimized cursors must agree with it. export, and optimized cursors must agree with it.
- Existence, visibility, and missing measured data remain distinct facts. - Existence, visibility, and missing measured data remain distinct facts.
## Content, occurrences, lanes, and rows ## Content, cels, lanes, and rows
These have different identities and responsibilities: These have different identities and responsibilities:
| Concept | Owns | Example | | Concept | Owns | Example |
| --- | --- | --- | | --- | --- | --- |
| Content | Reusable nodes and their animation | Drawing `a2`, an animated head, or a sound asset | | 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 | | Cel | 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 | | 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, exposure-sheet column, or property curve | | View row or column | Presentation and editor state | Timeline row, cel-sheet column, or property curve |
Use the existing instance/node identity mechanism for occurrences. An exposure 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. 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. Lanes group cels; they do not introduce another node-holding content type.
The concrete candidate below uses existing group and instance nodes; its ownership 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 boundaries are part of the design. It is now the implemented shape, and the field
spellings below are the ones the runtime reads. spellings below are the ones the runtime reads.
Each occurrence has a stable ID. Moving it, changing its hold, swapping its source, 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 occurrence that may 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 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. the right piece a new ID; commands return the resulting selection explicitly.
Occurrences are the canonical authored arrangement. A source-at-time channel or 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 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 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 must be represented solely as a source key. Ordinary property animation still
uses lightweight keys; it does not need occurrence objects. uses lightweight keys; it does not need cel objects.
A sequence lane has non-overlapping half-open occurrence intervals `[start,end)` A lane has non-overlapping half-open cel intervals `[start,end)`
in its own time space. Uncovered intervals are gaps. Empty lanes are valid. in its own time space. Uncovered intervals are gaps. Empty lanes are valid.
Compositing and simultaneous sounds are represented by multiple lanes or ordinary Compositing and simultaneous sounds are represented by multiple lanes or ordinary
scene composition; an accidental overlap never silently selects a winner. scene composition; an accidental overlap never silently selects a winner.
Transitions, if added, need explicit overlap and mixing semantics. Transitions, if added, need explicit overlap and mixing semantics.
Properties can belong to content, one occurrence, or the lane. For example: Properties can belong to content, one cel, or the lane. For example:
- Rotate the reusable drawing: all its uses change. - Rotate the reusable drawing: all its uses change.
- Rotate one occurrence: only that exposure changes. - Rotate one cel: only that cel changes.
- Animate the lane's rotation: whichever drawing is showing follows it. - Animate the lane's rotation: whichever drawing is showing follows it.
An occurrence can have its own transform, gain, corrections, and source timing A cel can have its own transform, gain, corrections, and source timing
while remaining a block in the same timeline row. Independent treatment never while remaining a block in the same timeline row. Independent treatment never
requires a new row or an otherwise unnecessary wrapper symbol. requires a new row or an otherwise unnecessary wrapper symbol.
### Concrete candidate: a sequence group and ordinary instances ### Concrete candidate: a lane and ordinary instances
A lane is a group node with `:layout :sequence`. Its occurrences are ordinary 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 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 flat node map. The sequence constraint is document semantics; which rows the UI
expands remains editor state. Ordinary groups retain unconstrained composition. expands remains editor state. Ordinary groups retain unconstrained composition.
This example is a document the runtime accepts, built and evaluated by 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 `frontend/test/arthur/domain/lane_test.cljs`. Times here are zero-based. Channels
use the existing representation; occurrence source references and playback have use the existing representation; cel source references and playback have
replaced the `[:source]` channel, which no longer exists. replaced the `[:source]` channel, which no longer exists.
```clojure ```clojure
@ -112,14 +112,14 @@ replaced the `[:source]` channel, which no longer exists.
:channels {[:xform :rot] :channels {[:xform :rot]
{:animated? true :interp :linear :keys {0 0, 6 30, 12 0}}}} {:animated? true :interp :linear :keys {0 0, 6 30, 12 0}}}}
:exposure-a :cel-a
{:id :exposure-a :kind :instance :parent :girl :z "a" {:id :cel-a :kind :instance :parent :girl :z "a"
:time {:at 0 :rate 1} :span [0 4] :time {:at 0 :rate 1} :span [0 4]
:source {:symbol :drawing-a} :source {:symbol :drawing-a}
:playback {:in 0 :speed 0 :end :stop}} :playback {:in 0 :speed 0 :end :stop}}
:exposure-b :cel-b
{:id :exposure-b :kind :instance :parent :girl :z "b" {:id :cel-b :kind :instance :parent :girl :z "b"
:time {:at 4 :rate 1} :span [0 4] :time {:at 4 :rate 1} :span [0 4]
:source {:symbol :drawing-b} :source {:symbol :drawing-b}
:playback {:in 0 :speed 0 :end :stop} :playback {:in 0 :speed 0 :end :stop}
@ -133,20 +133,20 @@ replaced the `[:source]` channel, which no longer exists.
:playback {:in 3 :speed 1 :end :stop}}} :playback {:in 3 :speed 1 :end :stop}}}
``` ```
The lane's rotation reads lane time. Each occurrence's channels read occurrence 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 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 samples source frames 3, 4, 5, and 6. The group transform composes with the
occurrence transform and then the content's own transform. cel transform and then the content's own transform.
`:span` remains in the node's own coordinates, consistent with ordinary nodes. `: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 The interval in lane time is derived through `:time`; do not also store parent
start/end values. Sequence children require finite intervals and positive start/end values. Sequence children require finite intervals and positive
placement rates. Ordering and overlap checks use the mapped intervals, not `:z`. placement rates. Ordering and overlap checks use the mapped intervals, not `:z`.
The sequence group may contain visual occurrences or audio occurrences; its The sequence group may contain visual cels or audio cels; its
capability must reject an incompatible mixture rather than infer it per frame. 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 A source reference is fixed within a cel. The lane changes content when
another occurrence becomes active. This is a deliberate revision of the original another cel becomes active. This is a deliberate revision of the original
diagnosis that making `:of` a channel was necessary to avoid vertical growth: diagnosis that making `:of` a channel was necessary to avoid vertical growth:
multiple instances can occupy one row when the view presents their containing multiple instances can occupy one row when the view presents their containing
sequence. A lane-level source schedule is therefore derived, not authored twice. sequence. A lane-level source schedule is therefore derived, not authored twice.
@ -157,14 +157,14 @@ The current implementation makes framed sources play and keyed sources hold.
Retire that rule. Channel storage shape must not determine playback behavior. 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. 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 A cel names content and describes how its source time is sampled. In the
basic case, after mapping lane time into occurrence time: basic case, after mapping lane time into cel time:
```text ```text
source_time = in_point + speed × occurrence_time source_time = in_point + speed × cel_time
``` ```
A newly created occurrence starts at local time zero. Moving it preserves this 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 origin relative to its content. Trimming can narrow its local support without
resetting that origin; split pieces likewise preserve the source and property resetting that origin; split pieces likewise preserve the source and property
values at the cut. Trimming, slipping, and retiming are distinct operations with values at the cut. Trimming, slipping, and retiming are distinct operations with
@ -174,7 +174,7 @@ explicitly different effects on the interval and the source map.
| --- | --- | | --- | --- |
| Hold a drawing | Constant source frame, equivalently speed 0 | | Hold a drawing | Constant source frame, equivalently speed 0 |
| Play an animated symbol | Advancing source time, normally speed 1 | | Play an animated symbol | Advancing source time, normally speed 1 |
| Cut between animations | Several occurrences, each with its own in-point and speed | | 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 | | 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. The source reference itself is discrete and never numerically interpolated.
@ -188,28 +188,28 @@ source, hold an endpoint, or loop an explicit range. A still uses a valid consta
frame. A loop uses a nonempty half-open range and a defined modulo rule. Playback 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. never guesses these policies from whether a channel happens to have keys.
Audio shares occurrence arrangement, trimming, gain ownership, and clock mapping. Audio shares cel arrangement, trimming, gain ownership, and clock mapping.
It does not inherit visual frame-hold semantics: holding one audio sample is not 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. an audio freeze effect. Validate supported playback policies by media capability.
Actual audio scheduling must follow active occurrences, including gaps and cuts, Actual audio scheduling must follow active cels, including gaps and cuts,
rather than playing every sound reachable through a structural reference. rather than playing every sound reachable through a structural reference.
## Time spaces and sampling ## Time spaces and sampling
Name the relevant space whenever an API accepts a time or range: project, Name the relevant space whenever an API accepts a time or range: project,
symbol/lane, occurrence, or source. Store authored frame coordinates exactly; symbol/lane, cel, or source. Store authored frame coordinates exactly;
avoid cumulative rounding when moving through nested mappings. Quantize at a avoid cumulative rounding when moving through nested mappings. Quantize at a
declared sampling boundary, not at every traversal step. Audio also needs its declared sampling boundary, not at every traversal step. Audio also needs its
continuous clock/sample space rather than visual frame quantization. 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 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 displayed cels to one source time. APIs must distinguish forward sampling
from inverse editing, and expose enough context to resolve an occurrence or 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 explicitly refuse an ambiguous operation. Do not report a missing time map merely
because inversion is unavailable. because inversion is unavailable.
Separate invertible placement timing from source sampling. A zero source speed Separate invertible placement timing from source sampling. A zero source speed
can mean hold without making the occurrence's own edit clock non-invertible. can mean hold without making the cel's own edit clock non-invertible.
Reparenting through changing transforms or non-invertible timing must either 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 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. matrix captured at one frame does not prove preservation across the animation.
@ -220,12 +220,12 @@ transform. An explicit whole-subtree stepping operation can exist separately.
Share the quantization primitive where possible, but retain its units, phase, Share the quantization primitive where possible, but retain its units, phase,
rounding policy, and order relative to retiming and lead. The original suggestion rounding policy, and order relative to retiming and lead. The original suggestion
that exposure and picture-rate sampling are simply one floor is insufficient: that cel and picture-rate sampling are simply one floor is insufficient:
noninteger grids and source-frame quantization require specified behavior. noninteger grids and source-frame quantization require specified behavior.
Identity timing can be implicit; remove `:time :mode` if it only duplicates that. 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 A cel interval is authored. Lane content extent is derived from its
occurrences, including the explicit end of the last one. A separately authored cels, including the explicit end of the last one. A separately authored
container trim/window is legitimate when it intentionally gates children. Do not 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 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 when no next key exists. A range of frame numbers alone cannot encode visibility
@ -236,23 +236,23 @@ or a missing measurement.
Every view issues the same domain commands. A command accepts an explicit target 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 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 selection, and any refusal reason. A button and a drag must not implement two
versions of exposure extension. versions of cel extension.
An edit target identifies the symbol, occurrence path, selected entities or An edit target identifies the symbol, cel path, selected entities or
properties, and the time range with its space. Navigation also distinguishes properties, and the time range with its space. Navigation also distinguishes
editing shared content directly from editing it through a particular occurrence. 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 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. different symbol: retain the explicit content target until navigation changes it.
Core commands include new drawing, reuse drawing, duplicate drawing, make unique, Core commands include new drawing, reuse drawing, duplicate drawing, make unique,
blank range, split, trim, move, extend exposure, slip source, retime, and apply a 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 bounded property edit. Ripple/overwrite policy and the set of affected lanes are
explicit command arguments. Preview consequences before committing a gesture. explicit command arguments. Preview consequences before committing a gesture.
New drawing creates fresh empty content and an occurrence. Blank range removes New drawing creates fresh empty content and a cel. Blank range removes
content coverage without inventing a hidden drawing. These are different actions. content coverage without inventing a hidden drawing. These are different actions.
Reuse creates another occurrence pointing at existing content. Duplicate creates Reuse creates another cel pointing at existing content. Duplicate creates
a new content identity. Make unique rebinds the selected occurrence only. a new content identity. Make unique rebinds the selected cel only.
Copy semantics must specify nested sharing. A normal content copy duplicates its Copy semantics must specify nested sharing. A normal content copy duplicates its
owned nodes and channels while preserving references to other reusable symbols. owned nodes and channels while preserving references to other reusable symbols.
@ -268,57 +268,57 @@ 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 not solve simultaneous edits to different keys of that same channel; define a
conflict policy rather than claiming that granularity solves all collaboration. conflict policy rather than claiming that granularity solves all collaboration.
### Default timing behavior: exposure edits preserve lane keys ### Default timing behavior: cel edits preserve lane keys
Working default from the follow-up discussion: extending a drawing's hold changes 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 cel timing, leaving lane animation at its authored times. The user raised
keeping keyframes in place as a possibility; this is the proposed predictable keeping keyframes in place as a possibility; this is the proposed predictable
default, not a claim that they selected every timing policy below. default, not a claim that they selected every timing policy below.
Ownership supplies the remaining rule: properties attached to an occurrence Ownership supplies the remaining rule: properties attached to a cel
travel with it. Extending its end does not stretch those properties; moving it 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 changes where their existing local times land. No per-key attachment flag is
needed to recover ownership that the document already expresses. needed to recover ownership that the document already expresses.
For the concrete example, extend `:exposure-a` by two lane frames with ripple: For the concrete example, extend `:cel-a` by two lane frames with ripple:
| Fact | Before | After | | Fact | Before | After |
| --- | --- | --- | | --- | --- | --- |
| Exposure A's lane interval | `[0,4)` | `[0,6)` | | Cel A's lane interval | `[0,4)` | `[0,6)` |
| Exposure B's lane interval | `[4,8)` | `[6,10)` | | Cel B's lane interval | `[4,8)` | `[6,10)` |
| Animated insert's lane interval | `[8,12)` | `[10,14)` | | Animated insert's lane interval | `[8,12)` | `[10,14)` |
| Girl's rotation peak | Lane frame 6 | Lane frame 6 | | 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 | | 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 | | Insert's first source frame | Source frame 3 | Source frame 3 |
The rotation peak now coincides with a different point in the drawing sequence. The rotation peak now coincides with a different point in the drawing sequence.
That is the intended consequence of changing exposures underneath timed motion. That is the intended consequence of changing cels underneath timed motion.
The position correction stays attached to drawing B's occurrence. Neither the The position correction stays attached to drawing B's cel. Neither the
background's keys nor audio on another lane moves. background's keys nor audio on another lane moves.
The command contract for this edit names the symbol and occurrence, a delta in The command contract for this edit names the symbol and cel, a delta in
lane frames, `:ripple` behavior, and an explicit scope of exposure timing. It 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, 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 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 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. nonpositive resulting duration. Validate and commit the entire change together.
The symbol's authored end is another explicit boundary: preview an overflow and 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 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 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 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. extension to 14 frames or the edit must be refused without partial changes.
Retime performance is a separate operation over explicitly selected occurrences Retime performance is a separate operation over explicitly selected cels
and channels. It applies the same time transformation to their relevant clocks, and channels. It applies the same time transformation to their relevant clocks,
keys, and correction supports. Stretching an interval requires a defined warp keys, and correction supports. Stretching an interval requires a defined warp
and interpolation behavior; it is not merely moving keys whose frame numbers and interpolation behavior; it is not merely moving keys whose frame numbers
happen to lie inside the selection. Until supported, refuse this operation happen to lie inside the selection. Until supported, refuse this operation
rather than approximating it with an exposure ripple. rather than approximating it with a cel ripple.
The initial UI should default stage transforms to the lane when drawing in a cel 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 workflow, so movement usually remains independent of cel timing. The
inspector names the target: lane motion, this occurrence, or shared drawing. 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 Changing that scope is explicit. It changes what the edit means, not just which
panel happens to be open. panel happens to be open.
@ -368,19 +368,19 @@ merely two drawings that happen to look related.
Derived library grouping may collect drawings used by a single lane. This is a Derived library grouping may collect drawings used by a single lane. This is a
convenience, not ownership or deletion authority. Reference discovery for cycle convenience, not ownership or deletion authority. Reference discovery for cycle
validation, copying, and deletion examines all structural references, including validation, copying, and deletion examines all structural references, including
currently inactive occurrences. Authored folders, favorites, and labels remain currently inactive cels. Authored folders, favorites, and labels remain
legitimate user data even when the UI could have suggested defaults. legitimate user data even when the UI could have suggested defaults.
## UX: location, selection, and controls ## UX: location, selection, and controls
The breadcrumb sits above the timeline and states the editing location, shared The breadcrumb sits above the timeline and states the editing location, shared
content identity, and occurrence context when applicable. Show local time and 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 its project context where a useful mapping exists. Holds and loops need an honest
description instead of a fictitious unique global frame. description instead of a fictitious unique global frame.
Creation controls next to the breadcrumb act in that explicit location. Selection 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 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; reuse and offers Make this cel unique. Names help identify content;
linked-use indicators must rely on IDs, because different drawings can share names. linked-use indicators must rely on IDs, because different drawings can share names.
| Surface | Primary scope and controls | | Surface | Primary scope and controls |
@ -390,7 +390,7 @@ linked-use indicators must rely on IDs, because different drawings can share nam
| Cel action strip | New drawing, duplicate drawing, hold longer/shorter, blank range | | Cel action strip | New drawing, duplicate drawing, hold longer/shorter, blank range |
| Lane header | Lane selection, lock, mute/solo where applicable, onion settings, expansion | | Lane header | Lane selection, lock, mute/solo where applicable, onion settings, expansion |
| Stage tools | Drawing and transform modes, active target and scope | | Stage tools | Drawing and transform modes, active target and scope |
| Inspector | Selected content/occurrence/lane properties and valid key controls | | Inspector | Selected content/cel/lane properties and valid key controls |
Cel actions have visible contextual buttons, shortcuts, a context menu, and Cel actions have visible contextual buttons, shortcuts, a context menu, and
command-palette entries. These are different entrances to the same commands. command-palette entries. These are different entrances to the same commands.
@ -411,52 +411,52 @@ preferences can be saved separately.
## A session, revised ## A session, revised
1. In `main`, create a girl lane and a new drawing. Draw; use New drawing (`N`) 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. to create the next one with the previous cel ghosted behind it.
2. Use Duplicate drawing (`D`) when the current shapes are the starting point. 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 Use Reuse drawing for a deliberately linked cel. The UI shows the
difference before an edit can change other uses. difference before an edit can change other uses.
3. Time the performance. Hold longer (`H`) extends the selected occurrence and 3. Time the performance. Hold longer (`H`) extends the selected cel and
ripples later occurrences in the explicitly targeted lane. A trim gesture ripples later cels in the explicitly targeted lane. A trim gesture
can use overwrite instead. The preview shows which boundaries will move. 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 4. Choose a two-frame default cel for newly created drawings, or run a
separate Retime exposures command on a selected range. This does not quantize separate Retime cels command on a selected range. This does not quantize
lane transforms or silently retime already authored exposures. lane transforms or silently retime already authored cels.
5. Place the background in a lane below. Its source holds one frame throughout 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 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. 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 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 the desired peak. A bounded rotation correction affects the girl across any
drawing boundaries in that range. Existing motion survives outside it. drawing boundaries in that range. Existing motion survives outside it.
7. Insert a playing animated symbol among the girl's held drawings. Set that 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. cel's source playback to advance. No lane conversion is required.
The timeline shows named exposure blocks with property marks and optional curve The timeline shows named cel blocks with property marks and optional curve
subrows. The exposure sheet shows the same occurrences by frame and lane. The 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. graph editor edits the same properties; the stage resolves the same document.
Onion skin is configurable and counts neighboring exposure events, skipping gaps 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 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. drawing remain distinct events. Deduplicating identical ghosts is a display option.
## Proof obligations and implementation order ## Proof obligations and implementation order
The source-channel prototype has been removed: an occurrence names one symbol The source-channel prototype has been removed: a cel names one symbol
and carries its own playback clock, and `node/problems` rejects the old 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 `[: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 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, 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 trim, move, blank 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 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 exposure actually lane's own row, and offers Make unique only where the selected cel actually
shares its drawing. shares its drawing.
There is ONE placement function and a position argument, so appending is not a 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 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 where nothing has to move. Placing ripples — cels at or after the
position move later by the new exposure's duration — and `:keep` versus 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 `: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 policy argument yet, deliberately: taking frames away from the cel
already there is trimming, and until `trim` exists, placement that would need it 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 instead of approximating it. A position inside an existing cel
refuses too, and names `split` — one command does not quietly perform two. refuses too, and names `split` — one command does not quietly perform two.
Splitting turned out to cost almost nothing, which is evidence for the Splitting turned out to cost almost nothing, which is evidence for the
@ -474,7 +474,7 @@ of independence is only made where it is kept.
THE SHOT LENGTH IS AUTHORED, which is the decision the range commands forced. 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 `: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 occurrences. A command 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: 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 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 that is a true statement about what somebody authored, and deriving the window
@ -487,12 +487,12 @@ 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, 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 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 intentions — clear the room with `blank` or `trim` first, which is the
composition. Blank leaves a gap and does not close it; an exposure wholly inside 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 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 the range is split. Their drawings stay in the library, since a lane does not
own its content. own its content.
All three are the same geometry as `split`: a `:span` is in the occurrence's own 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 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 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 its animation instead of restarting it — the difference between trimming and
@ -512,7 +512,7 @@ plumbing, since `node/problems` already reports every channel's problems.
Both halves of ownership are under test at lane level: a three-frame correction 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 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 every frame outside its support identical, and a correction owned by one
exposure travels with that exposure when a hold before it grows. cel travels with that cel when a hold before it grows.
Regeneration keeps them, which is the obligation the layer design exists to Regeneration keeps them, which is the obligation the layer design exists to
meet: `rebased` replaces a base and carries its corrections across, and a meet: `rebased` replaces a base and carries its corrections across, and a
@ -529,7 +529,7 @@ report.
Still unbuilt: overwrite as a placement policy (which is now `blank` then Still unbuilt: overwrite as a placement policy (which is now `blank` then
`place`, composed inside one transaction), slip source, retime, and deleting `place`, composed inside one transaction), slip source, retime, and deleting
reused content. A lane cannot hold AUDIO occurrences — `sequence-problems` reused content. A lane cannot hold AUDIO cels — `lane-problems`
requires visual ones, though this document says a lane may hold either and requires visual ones, though this document says a lane may hold either and
should reject only a mixture. should reject only a mixture.
@ -537,7 +537,7 @@ What is NOT implemented is a command that produces a layer — the doc's Constan
adjustment, Ramp and Return motion — and with it the question of how a view 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 offers those three over a selected range, and how it offers a conflict for
resolution. Overwrite, the range and retiming resolution. Overwrite, the range and retiming
commands (blank, trim, move, slip source, retime) and the exposure-sheet view commands (blank, trim, move, slip source, retime) and the cel-sheet view
are also not implemented; a refusal is the current behavior where the model 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 424 tests 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 and 5,749 assertions, with `frontend/test/browser/sequence.mjs` driving the
@ -552,23 +552,23 @@ expanding the interface:
| --- | --- | | --- | --- |
| Same drawing exposed twice, then one made unique | Linked edits affect both before copying and only the selected content after | | 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 | | 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 | | Adjacent cels, 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 | | 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 | | 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 | | 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 | | 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 | | 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 | | 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 | | 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 | | 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 | | 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 | | 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 | | 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 | | 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 Implementation order: cel ownership and playback semantics; shared
commands and validation; correction layers and time-addressing contracts; then commands and validation; correction layers and time-addressing contracts; then
breadcrumb, cel strip, and an exposure-sheet projection. Use those two temporal 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. 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 A new presentation should not require duplicate animation state. A genuinely new

View file

@ -22,10 +22,10 @@
(defn compose (defn compose
"The authored layout plus a source clip -> the composed stage document. "The authored layout plus a source clip -> the composed stage document.
A PLACEMENT IS KEYED BY ITS :uuid, not by the authored id. The authored id AN INSTANCE IS KEYED BY ITS :uuid, not by the authored id. The authored id
(`:left`, `:voice-right`) is a handle for reading the EDN and for the (`:left`, `:voice-right`) is a handle for reading the EDN and for the
`:linked-to` written there; it does not appear in the document this returns. `:linked-to` written there; it does not appear in the document this returns.
What replaces it is an identity that means one placement and nothing else: seven What replaces it is an identity that means one instance and nothing else: seven
instances of one symbol are seven different things to name — to export on their instances of one symbol are seven different things to name — to export on their
own, to link a voice to, to point at later — and an id like `:left` is a own, to link a voice to, to point at later — and an id like `:left` is a
description of where a thing sits, which is exactly what changes when the stage description of where a thing sits, which is exactly what changes when the stage
@ -44,7 +44,7 @@
by-id (into {} (map (juxt :id :uuid)) (concat instances audio)) by-id (into {} (map (juxt :id :uuid)) (concat instances audio))
uuid-of (fn [what id] uuid-of (fn [what id]
(or (get by-id id) (or (get by-id id)
(throw (ex-info "the stage layout names a placement that is not there" (throw (ex-info "the stage layout names an instance that is not there"
{:in what :id id {:in what :id id
:known (vec (sort-by str (keys by-id)))})))) :known (vec (sort-by str (keys by-id)))}))))
nodes (into nodes (into

View file

@ -20,7 +20,7 @@
`other` to its id here. `other` to its id here.
AN ID THAT IS TAKEN IS RENAMED, never merged: two symbols that happen to share AN ID THAT IS TAKEN IS RENAMED, never merged: two symbols that happen to share
an id are two drawings, and an occurrence's `:source :symbol` inside the copy is an id are two drawings, and a cel's `:source :symbol` inside the copy is
rewritten to follow. `wanted` maps a root's id in `other` to the id it should preferably get, rewritten to follow. `wanted` maps a root's id in `other` to the id it should preferably get,
which is how a symbol made from footage is called what the person typed rather which is how a symbol made from footage is called what the person typed rather
than `:main`. than `:main`.
@ -36,7 +36,7 @@
(some #{%} (vals ids)))] (some #{%} (vals ids)))]
(assoc ids sid (clip/free-id taken? (get wanted sid sid))))) (assoc ids sid (clip/free-id taken? (get wanted sid sid)))))
{} (sort-by str reach)) {} (sort-by str reach))
;; Occurrence identity and timing stay put; content references follow ;; Cel identity and timing stay put; content references follow
;; the symbol IDs assigned in the destination document. ;; the symbol IDs assigned in the destination document.
repoint (fn [n ids] repoint (fn [n ids]
(if (node/source n) (if (node/source n)

View file

@ -111,7 +111,7 @@
;; A LAYER HAS NO TIME SPACE OF ITS OWN. Its `:support` and its values' keys are ;; A LAYER HAS NO TIME SPACE OF ITS OWN. Its `:support` and its values' keys are
;; in the frames the base channel's keys are in — the node's own. A correction on ;; in the frames the base channel's keys are in — the node's own. A correction on
;; a lane is therefore in lane frames and crosses the drawing boundaries under ;; a lane is therefore in lane frames and crosses the drawing boundaries under
;; it; a correction on one occurrence is in that occurrence's frames and travels ;; it; a correction on one cel is in that cel's frames and travels
;; with it when it moves. Ownership already answered the question, so there is no ;; with it when it moves. Ownership already answered the question, so there is no
;; field to get wrong. ;; field to get wrong.

View file

@ -190,7 +190,7 @@
own (symbol/resolver sym store palette own (symbol/resolver sym store palette
(assoc opts :pose-tracks pose-tracks (assoc opts :pose-tracks pose-tracks
:source-fps (:fps clip))) :source-fps (:fps clip)))
;; Each occurrence owns its source resolver and mutable buffers. ;; Each cel owns its source resolver and mutable buffers.
children (into {} children (into {}
(for [[id n] nodes (for [[id n] nodes
:when (= :instance (:kind n)) :when (= :instance (:kind n))
@ -282,7 +282,7 @@
middle goes there; without one — a drop on the timeline — the drawing stays middle goes there; without one — a drop on the timeline — the drawing stays
where it was drawn. where it was drawn.
THE UUID IS AN ARGUMENT. A placement's identity is the key it has in the node THE UUID IS AN ARGUMENT. An instance's identity is the key it has in the node
map — it is what `:linked-to`, an export target and a saved leaf all name — so map — it is what `:linked-to`, an export target and a saved leaf all name — so
generating one in here would make this function's result depend on when it was generating one in here would make this function's result depend on when it was
called, and this namespace is the pure one. called, and this namespace is the pure one.
@ -307,7 +307,7 @@
:name (symbol-name clip sid) :name (symbol-name clip sid)
:kind :instance :kind :instance
:parent nil :parent nil
;; Lexicographic draw order, as `domain/paint` does it: a placement made ;; Lexicographic draw order, as `domain/paint` does it: an instance made
;; later sits above one made earlier, and neither has to renumber. ;; later sits above one made earlier, and neither has to renumber.
:z (str "z" (js/Date.now) "-" (name sid)) :z (str "z" (js/Date.now) "-" (name sid))
:span [0 (:frames target)] :span [0 (:frames target)]
@ -406,7 +406,7 @@
missing (remove (:symbols clip) (node/sources n))] missing (remove (:symbols clip) (node/sources n))]
(str "symbol " (pr-str sid) " instance " (pr-str id) (str "symbol " (pr-str sid) " instance " (pr-str id)
" names missing symbol " (pr-str missing))) " names missing symbol " (pr-str missing)))
;; Pose tracks belong to this occurrence's single source symbol. ;; Pose tracks belong to this cel's single source symbol.
(for [[sid sym] (:symbols clip) (for [[sid sym] (:symbols clip)
[id n] (:nodes sym) [id n] (:nodes sym)
:when (= :instance (:kind n)) :when (= :instance (:kind n))

View file

@ -1,10 +1,10 @@
(ns arthur.domain.sequence (ns arthur.domain.lane
"The commands over a lane of occurrences: make one, put drawings in it, change "The commands over a lane of cels: make one, put drawings in it, change
how long they are exposed, and decide which of them share content. how long they are exposed, and decide which of them share content.
WHAT A LANE IS lives in `arthur.domain.symbol`, beside the other rules about a WHAT A LANE IS lives in `arthur.domain.symbol`, beside the other rules about a
node map: a group with `:layout :sequence`, whose children are non-overlapping node map: a group with `:layout :sequence`, whose children are non-overlapping
visual occurrences. This namespace only changes them. visual cels. This namespace only changes them.
EVERY COMMAND IS ONE STEP AND ALL OF IT. Each returns `{:clip :selection}` or EVERY COMMAND IS ONE STEP AND ALL OF IT. Each returns `{:clip :selection}` or
`{:refused reason}` — never a half-applied edit, and never a document that `{:refused reason}` — never a half-applied edit, and never a document that
@ -13,7 +13,7 @@
caller's `:extent`, and decoupling shared content is its own command instead caller's `:extent`, and decoupling shared content is its own command instead
of something an ordinary edit does silently. of something an ordinary edit does silently.
IDS FOR OCCURRENCES COME FROM THE CALLER, because an occurrence's identity is IDS FOR CELS COME FROM THE CALLER, because a cel's identity is
a uuid and this namespace is pure. Ids for new CONTENT are derived from the a uuid and this namespace is pure. Ids for new CONTENT are derived from the
drawing being copied — `clip/free-id` is pure too, and `drawing-a-2` says what drawing being copied — `clip/free-id` is pure too, and `drawing-a-2` says what
it came from in a way `symbol-7` does not." it came from in a way `symbol-7` does not."
@ -40,19 +40,19 @@
THE SHOT LENGTH IS AUTHORED. `:frames` is the symbol's window — how long the THE SHOT LENGTH IS AUTHORED. `:frames` is the symbol's window — how long the
shot IS — and the occupied extent of its lanes is a different fact derived shot IS — and the occupied extent of its lanes is a different fact derived
from the occurrences. A command may GROW the window when the caller says from the cels. A command may GROW the window when the caller says
`:grow-symbol`, and never shrinks it: emptying the end of a shot leaves a shot `:grow-symbol`, and never shrinks it: emptying the end of a shot leaves a shot
with empty frames at the end, which is a true statement about what somebody with empty frames at the end, which is a true statement about what somebody
authored. Deriving the window from the extent instead would make deleting the authored. Deriving the window from the extent instead would make deleting the
last drawing silently shorten the film. last drawing silently shorten the film.
So there are two numbers and this function keeps them apart: `needed` is where So there are two numbers and this function keeps them apart: `needed` is where
the occurrences reach, `:frames` is what was authored, and the only way the the cels reach, `:frames` is what was authored, and the only way the
second follows the first is a caller asking." second follows the first is a caller asking."
[clip sid nodes selection extent] [clip sid nodes selection extent]
(let [sym (clip/symbol clip sid) (let [sym (clip/symbol clip sid)
reach (for [[id n] nodes :when (node/sequence? n) reach (for [[id n] nodes :when (node/lane? n)
child (symbol/sequence-members nodes id) child (symbol/lane-cels nodes id)
:let [m (lane-map nodes id) :let [m (lane-map nodes id)
end (second (node/placed-span child))]] end (second (node/placed-span child))]]
(when m (+ (:at m) (/ end (:rate m))))) (when m (+ (:at m) (/ end (:rate m)))))
@ -70,13 +70,13 @@
:selection selection}))) :selection selection})))
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; the geometry every exposure edit is made of ;; the geometry every cel edit is made of
;; ;;
;; A `:span` is in the occurrence's OWN frames and its `:time` says where those ;; A `:span` is in the cel's OWN frames and its `:time` says where those
;; land in the lane. So moving an edge of an exposure is one write to `:span`, ;; land in the lane. So moving an edge of a cel is one write to `:span`,
;; and `:time` and `:playback` are untouched — which is why trimming the front ;; and `:time` and `:playback` are untouched — which is why trimming the front
;; of a playing insert starts it later in its source instead of resetting it, ;; of a playing insert starts it later in its source instead of resetting it,
;; and why the two halves of a split go on meaning what the one exposure meant. ;; and why the two halves of a split go on meaning what the one cel meant.
;; Trim, split and blank are all this one operation, applied differently. ;; Trim, split and blank are all this one operation, applied differently.
(defn- local (defn- local
@ -91,8 +91,8 @@
(assoc-in n [:span (case which :in 0 :out 1)] (local n f))) (assoc-in n [:span (case which :in 0 :out 1)] (local n f)))
(defn extend-hold (defn extend-hold
"Change one held occurrence's duration by `delta` lane frames and ripple its "Change one held cel's duration by `delta` lane frames and ripple its
later siblings. Lane channels, occurrence channels and source clocks stay put. later siblings. Lane channels, cel channels and source clocks stay put.
Returns {:clip :selection} or {:refused :required-frames?}; never partially edits." Returns {:clip :selection} or {:refused :required-frames?}; never partially edits."
[clip sid id delta {:keys [extent] :or {extent :keep}}] [clip sid id delta {:keys [extent] :or {extent :keep}}]
(let [nodes (get-in clip [:symbols sid :nodes]) (let [nodes (get-in clip [:symbols sid :nodes])
@ -101,21 +101,21 @@
rate (:rate (node/time-of n)) rate (:rate (node/time-of n))
span (:span n) span (:span n)
m (when lane (lane-map nodes (:id lane))) m (when lane (lane-map nodes (:id lane)))
;; The LANE's own shape, not the whole symbol's: refusing an exposure ;; The LANE's own shape, not the whole symbol's: refusing a cel
;; edit over some unrelated defect elsewhere in the symbol would be ;; edit over some unrelated defect elsewhere in the symbol would be
;; this command answering for a part of the document it never touches. ;; this command answering for a part of the document it never touches.
broken (first (symbol/sequence-problems nodes))] broken (first (symbol/lane-problems nodes))]
(cond (cond
(not (node/sequence? lane)) {:refused "select an occurrence in a sequence lane"} (not (node/lane? lane)) {:refused "select a cel in a lane"}
broken {:refused broken} broken {:refused broken}
(not (and (integer? delta) (not (zero? delta)))) {:refused "hold change must be a nonzero whole number of lane frames"} (not (and (integer? delta) (not (zero? delta)))) {:refused "hold change must be a nonzero whole number of lane frames"}
(not (zero? (:speed (node/playback-of n)))) {:refused "hold length applies to a held drawing"} (not (zero? (:speed (node/playback-of n)))) {:refused "hold length applies to a held drawing"}
(nil? m) {:refused "exposure timing through a stepped or looping lane is not supported"} (nil? m) {:refused "cel timing through a stepped or looping lane is not supported"}
(<= (+ (second span) (* rate delta)) (first span)) {:refused "a drawing must keep a positive exposure"} (<= (+ (second span) (* rate delta)) (first span)) {:refused "a drawing must keep a positive cel"}
:else :else
(let [[_ boundary] (node/placed-span n) (let [[_ boundary] (node/placed-span n)
later (filter #(>= (first (node/placed-span %)) boundary) later (filter #(>= (first (node/placed-span %)) boundary)
(symbol/sequence-members nodes (:id lane))) (symbol/lane-cels nodes (:id lane)))
nodes (assoc-in nodes [id :span 1] (+ (second span) (* rate delta))) nodes (assoc-in nodes [id :span 1] (+ (second span) (* rate delta)))
nodes (reduce (fn [ns sibling] nodes (reduce (fn [ns sibling]
(update-in ns [(:id sibling) :time :at] (fnil + 0) delta)) (update-in ns [(:id sibling) :time :at] (fnil + 0) delta))
@ -123,7 +123,7 @@
(finish clip sid nodes id extent))))) (finish clip sid nodes id extent)))))
(defn split (defn split
"Cut occurrence `id` in two at lane frame `cut`. The left piece keeps its "Cut cel `id` in two at lane frame `cut`. The left piece keeps its
identity; the right gets `new-id`. identity; the right gets `new-id`.
NOTHING BUT `:span` DIFFERS between the two pieces. They keep one `:time`, so NOTHING BUT `:span` DIFFERS between the two pieces. They keep one `:time`, so
@ -133,7 +133,7 @@
in-points that could be wrong. A held drawing holds the same frame on both in-points that could be wrong. A held drawing holds the same frame on both
sides; a playing insert plays on through the cut without a seam. That is what sides; a playing insert plays on through the cut without a seam. That is what
`:span` being in the node's OWN coordinates buys, and it is why splitting `:span` being in the node's OWN coordinates buys, and it is why splitting
needs no shot-length policy: the pieces occupy the frames the one exposure needs no shot-length policy: the pieces occupy the frames the one cel
occupied. occupied.
The right piece is the selection, because it is the piece that was made." The right piece is the selection, because it is the piece that was made."
@ -144,11 +144,11 @@
{:keys [at rate]} (node/time-of n) {:keys [at rate]} (node/time-of n)
[lo hi] (or (node/placed-span n) [nil nil])] [lo hi] (or (node/placed-span n) [nil nil])]
(cond (cond
(not (node/sequence? lane)) {:refused "select an occurrence in a sequence lane"} (not (node/lane? lane)) {:refused "select a cel in a lane"}
(not (integer? cut)) {:refused "a cut is a whole lane frame"} (not (integer? cut)) {:refused "a cut is a whole lane frame"}
(contains? nodes new-id) {:refused "the new occurrence ID is already used"} (contains? nodes new-id) {:refused "the new cel ID is already used"}
(not (and lo (< lo cut hi))) (not (and lo (< lo cut hi)))
{:refused (str "frame " cut " is not inside this exposure")} {:refused (str "frame " cut " is not inside this cel")}
:else :else
(let [nodes (-> nodes (let [nodes (-> nodes
(assoc id (edged n :out cut)) (assoc id (edged n :out cut))
@ -157,10 +157,10 @@
(finish clip sid nodes new-id :keep))))) (finish clip sid nodes new-id :keep)))))
(defn trim (defn trim
"Move one edge of occurrence `id` to lane frame `to`, without disturbing a "Move one edge of cel `id` to lane frame `to`, without disturbing a
single other occurrence. single other cel.
TRIM NARROWS. Lengthening an exposure is `extend-hold`, which carries a ripple TRIM NARROWS. Lengthening a cel is `extend-hold`, which carries a ripple
policy and a shot-length policy because it needs them; letting trim grow as 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 well would give one gesture two sets of rules and a way to overlap its
neighbour. `edge` is `:in` or `:out`. neighbour. `edge` is `:in` or `:out`.
@ -174,15 +174,15 @@
lane (get nodes (:parent n)) lane (get nodes (:parent n))
[lo hi] (or (node/placed-span n) [nil nil])] [lo hi] (or (node/placed-span n) [nil nil])]
(cond (cond
(not (node/sequence? lane)) {:refused "select an occurrence in a sequence lane"} (not (node/lane? lane)) {:refused "select a cel in a lane"}
(not (#{:in :out} edge)) {:refused "an edge is :in or :out"} (not (#{:in :out} edge)) {:refused "an edge is :in or :out"}
(not (integer? to)) {:refused "an edge goes to a whole lane frame"} (not (integer? to)) {:refused "an edge goes to a whole lane frame"}
(not (and lo (< lo to hi))) (not (and lo (< lo to hi)))
{:refused (str "frame " to " is not inside this exposure; trim narrows it")} {:refused (str "frame " to " is not inside this cel; trim narrows it")}
:else (finish clip sid (assoc nodes id (edged n edge to)) id :keep)))) :else (finish clip sid (assoc nodes id (edged n edge to)) id :keep))))
(defn move (defn move
"Put occurrence `id` at lane frame `to`, leaving every other occurrence and "Put cel `id` at lane frame `to`, leaving every other cel and
its own length, source and corrections alone. its own length, source and corrections alone.
One write to `:time :at`. A destination that would overlap a neighbour is One write to `:time :at`. A destination that would overlap a neighbour is
@ -196,40 +196,40 @@
lane (get nodes (:parent n)) lane (get nodes (:parent n))
{:keys [at]} (node/time-of n)] {:keys [at]} (node/time-of n)]
(cond (cond
(not (node/sequence? lane)) {:refused "select an occurrence in a sequence lane"} (not (node/lane? lane)) {:refused "select a cel in a lane"}
(not (integer? to)) {:refused "an exposure moves to a whole lane frame"} (not (integer? to)) {:refused "a cel moves to a whole lane frame"}
(nil? (node/placed-span n)) {:refused "an occurrence needs a span to move"} (nil? (node/placed-span n)) {:refused "a cel needs a span to move"}
:else :else
(let [moved (update-in n [:time :at] (fnil + 0) (- to (first (node/placed-span n))))] (let [moved (update-in n [:time :at] (fnil + 0) (- to (first (node/placed-span n))))]
(if (not= to (first (node/placed-span moved))) (if (not= to (first (node/placed-span moved)))
{:refused "exposure timing through a stepped or looping lane is not supported"} {:refused "cel timing through a stepped or looping lane is not supported"}
(finish clip sid (assoc nodes id moved) id :keep)))))) (finish clip sid (assoc nodes id moved) id :keep))))))
(defn blank (defn blank
"Clear lane frames `[a b)` of lane `lane-id`, leaving a GAP. "Clear lane frames `[a b)` of lane `lane-id`, leaving a GAP.
A gap is not a drawing. Nothing is invented to cover those frames and nothing A gap is not a drawing. Nothing is invented to cover those frames and nothing
closes the hole — the occurrences after it stay where they are, because closes the hole — the cels after it stay where they are, because
emptying frames and re-timing a performance are different intentions. emptying frames and re-timing a performance are different intentions.
What it does to each exposure it meets is the edge geometry above: one wholly What it does to each cel it meets is the edge geometry above: one wholly
inside is removed, one overlapping an end is trimmed to it, and the one that inside is removed, one overlapping an end is trimmed to it, and the one that
spans the whole range is split, which is the only case that needs `id`. Their spans the whole range is split, which is the only case that needs `id`. Their
drawings stay in the library — a lane does not own its content, and a drawing drawings stay in the library — a lane does not own its content, and a drawing
whose last exposure is gone is still a drawing somebody made." whose last cel is gone is still a drawing somebody made."
[clip sid lane-id [a b] {:keys [id]}] [clip sid lane-id [a b] {:keys [id]}]
(let [nodes (get-in clip [:symbols sid :nodes]) (let [nodes (get-in clip [:symbols sid :nodes])
lane (get nodes lane-id) lane (get nodes lane-id)
members (when (node/sequence? lane) (symbol/sequence-members nodes lane-id)) members (when (node/lane? lane) (symbol/lane-cels nodes lane-id))
spanning (when members spanning (when members
(first (filter #(let [[lo hi] (node/placed-span %)] (and (< lo a) (> hi b))) (first (filter #(let [[lo hi] (node/placed-span %)] (and (< lo a) (> hi b)))
members)))] members)))]
(cond (cond
(not (node/sequence? lane)) {:refused "select a sequence lane"} (not (node/lane? lane)) {:refused "select a lane"}
(not (and (integer? a) (integer? b) (< a b))) (not (and (integer? a) (integer? b) (< a b)))
{:refused "a range to blank is whole lane frames, and not empty"} {:refused "a range to blank is whole lane frames, and not empty"}
(and spanning (or (nil? id) (contains? nodes id))) (and spanning (or (nil? id) (contains? nodes id)))
{:refused "blanking inside one exposure splits it, which needs a free ID for the remainder"} {:refused "blanking inside one cel splits it, which needs a free ID for the remainder"}
:else :else
(let [nodes (reduce (let [nodes (reduce
(fn [ns n] (fn [ns n]
@ -258,10 +258,10 @@
;; putting drawings in a lane ;; putting drawings in a lane
(defn- held (defn- held
"A one-frame held occurrence of `drawing-id`, starting at lane frame `at`. "A one-frame held cel of `drawing-id`, starting at lane frame `at`.
Held rather than playing, and one frame rather than the length of what it Held rather than playing, and one frame rather than the length of what it
places: an exposure's duration is the lane's business — `extend-hold` is how places: a cel's duration is the lane's business — `extend-hold` is how
it changes — and reading it off the content would make placing a ten-frame it changes — and reading it off the content would make placing a ten-frame
animation and holding its first drawing the same gesture." animation and holding its first drawing the same gesture."
[id lane-id drawing-id at] [id lane-id drawing-id at]
@ -281,16 +281,16 @@
"Where lane `lane-id`'s occupied frames stop, in its own time." "Where lane `lane-id`'s occupied frames stop, in its own time."
[nodes lane-id] [nodes lane-id]
(apply max 0 (map #(second (node/placed-span %)) (apply max 0 (map #(second (node/placed-span %))
(symbol/sequence-members nodes lane-id)))) (symbol/lane-cels nodes lane-id))))
(defn- place (defn- place
"Put a held occurrence of `drawing-id` into `lane-id` at lane frame `at`, and "Put a held cel of `drawing-id` into `lane-id` at lane frame `at`, and
RIPPLE: everything starting at or after it moves later by its duration. RIPPLE: everything starting at or after it moves later by its duration.
There is one placement function and `:end` is a position like any other, so There is one placement function and `:end` is a position like any other, so
appending is not a different operation from inserting — the end is just where appending is not a different operation from inserting — the end is just where
nothing has to move. Overwriting is the other policy and is NOT this: taking nothing has to move. Overwriting is the other policy and is NOT this: taking
frames away from the occurrence already there is trimming, which is its own frames away from the cel already there is trimming, which is its own
command and not something placing a drawing should do on the quiet. command and not something placing a drawing should do on the quiet.
`:frame` in the result is where it landed, in the open symbol's time, for a `:frame` in the result is where it landed, in the open symbol's time, for a
@ -301,7 +301,7 @@
n (held id lane-id drawing-id at) n (held id lane-id drawing-id at)
[lo hi] (node/placed-span n) [lo hi] (node/placed-span n)
later (filter #(>= (first (node/placed-span %)) lo) later (filter #(>= (first (node/placed-span %)) lo)
(symbol/sequence-members nodes lane-id)) (symbol/lane-cels nodes lane-id))
nodes (reduce (fn [ns sibling] nodes (reduce (fn [ns sibling]
(update-in ns [(:id sibling) :time :at] (fnil + 0) (- hi lo))) (update-in ns [(:id sibling) :time :at] (fnil + 0) (- hi lo)))
(assoc nodes id n) later) (assoc nodes id n) later)
@ -311,31 +311,31 @@
(:clip result) (assoc :frame (+ (:at m) (/ at (:rate m))))))) (:clip result) (assoc :frame (+ (:at m) (/ at (:rate m)))))))
(defn- placeable (defn- placeable
"Why a held occurrence cannot go into `lane-id` at `at`, or nil." "Why a held cel cannot go into `lane-id` at `at`, or nil."
[clip sid lane-id id at] [clip sid lane-id id at]
(let [nodes (get-in clip [:symbols sid :nodes]) (let [nodes (get-in clip [:symbols sid :nodes])
lane (get nodes lane-id) lane (get nodes lane-id)
;; INSIDE an exposure is not a position for another one. Splitting that ;; INSIDE a cel is not a position for another one. Splitting that
;; exposure is what makes it two, and doing it here would be one command ;; cel is what makes it two, and doing it here would be one command
;; quietly performing two: the caller asks for `split` and then places. ;; quietly performing two: the caller asks for `split` and then places.
inside (when (number? at) inside (when (number? at)
(some (fn [n] (let [[lo hi] (node/placed-span n)] (some (fn [n] (let [[lo hi] (node/placed-span n)]
(when (< lo at hi) n))) (when (< lo at hi) n)))
(symbol/sequence-members nodes lane-id)))] (symbol/lane-cels nodes lane-id)))]
(cond (cond
(not (node/sequence? lane)) "select a sequence lane" (not (node/lane? lane)) "select a lane"
(contains? nodes id) "the new occurrence ID is already used" (contains? nodes id) "the new cel ID is already used"
(not (or (= :end at) (and (integer? at) (not (neg? at))))) (not (or (= :end at) (and (integer? at) (not (neg? at)))))
"a position is :end or a whole lane frame" "a position is :end or a whole lane frame"
inside (str "frame " at " is inside an exposure; split it first") inside (str "frame " at " is inside a cel; split it first")
(nil? (lane-map nodes lane-id)) "drawing creation through a stepped or looping lane is not supported" (nil? (lane-map nodes lane-id)) "drawing creation through a stepped or looping lane is not supported"
:else (first (symbol/sequence-problems nodes))))) :else (first (symbol/lane-problems nodes)))))
(defn append-drawing (defn append-drawing
"Append fresh empty content and a held occurrence of it. IDs come from the "Append fresh empty content and a held cel of it. IDs come from the
caller so a command is deterministic and replayable. caller so a command is deterministic and replayable.
Fresh content, not a blank range: a lane with no occurrence over a frame shows Fresh content, not a blank range: a lane with no cel over a frame shows
nothing there already, and a drawing nobody has drawn in is a different thing nothing there already, and a drawing nobody has drawn in is a different thing
from a gap." from a gap."
[clip sid lane-id id drawing-id {:keys [at extent] :or {extent :keep at :end}}] [clip sid lane-id id drawing-id {:keys [at extent] :or {extent :keep at :end}}]
@ -347,8 +347,8 @@
sid lane-id id drawing-id at extent))) sid lane-id id drawing-id at extent)))
(defn reuse-drawing (defn reuse-drawing
"Append a held occurrence of content the document ALREADY has, so the same "Append a held cel of content the document ALREADY has, so the same
drawing is exposed twice and editing it changes both exposures. drawing is exposed twice and editing it changes both cels.
This is the command `make-unique` is the undo of, and the reason they are two This is the command `make-unique` is the undo of, and the reason they are two
commands: reuse is a decision to share, and sharing is not something to commands: reuse is a decision to share, and sharing is not something to
@ -381,17 +381,17 @@
:id id}))) :id id})))
(defn duplicate-drawing (defn duplicate-drawing
"Append a held occurrence of a COPY of what occurrence `id` places, for when "Append a held cel of a COPY of what cel `id` places, for when
the drawing on screen is the starting point for the next one. the drawing on screen is the starting point for the next one.
The copy is of the content only. The new exposure is a plain one-frame hold The copy is of the content only. The new cel is a plain one-frame hold
rather than a copy of `id`'s own transform or corrections: those belong to rather than a copy of `id`'s own transform or corrections: those belong to
that exposure, and carrying them over would make duplicating a drawing quietly that cel, and carrying them over would make duplicating a drawing quietly
duplicate the treatment of one use of it." duplicate the treatment of one use of it."
[clip sid id new-id {:keys [at extent deep?] :or {extent :keep at :end}}] [clip sid id new-id {:keys [at extent deep?] :or {extent :keep at :end}}]
(let [n (get-in clip [:symbols sid :nodes id]) (let [n (get-in clip [:symbols sid :nodes id])
from (node/source n)] from (node/source n)]
(if-let [why (or (when-not from "select an occurrence to duplicate") (if-let [why (or (when-not from "select a cel to duplicate")
(when-not (clip/symbol clip from) "the drawing it places is missing") (when-not (clip/symbol clip from) "the drawing it places is missing")
(placeable clip sid (:parent n) new-id at))] (placeable clip sid (:parent n) new-id at))]
{:refused why} {:refused why}
@ -399,10 +399,10 @@
(place c sid (:parent n) new-id copy at extent))))) (place c sid (:parent n) new-id copy at extent)))))
(defn make-unique (defn make-unique
"Point occurrence `id` at a private copy of its content, leaving every other "Point cel `id` at a private copy of its content, leaving every other
occurrence of that drawing sharing the original. cel of that drawing sharing the original.
Refused when nothing else uses it: a drawing with one exposure is already Refused when nothing else uses it: a drawing with one cel is already
unique, and answering with a silent copy would leave a second identical symbol unique, and answering with a silent copy would leave a second identical symbol
in the library for no reason a person could see." in the library for no reason a person could see."
[clip sid id {:keys [deep?]}] [clip sid id {:keys [deep?]}]
@ -412,7 +412,7 @@
[oid on] (:nodes osym) [oid on] (:nodes osym)
:when (and (= from (node/source on)) (not= [sid id] [osid oid]))] :when (and (= from (node/source on)) (not= [sid id] [osid oid]))]
[osid oid])] [osid oid])]
(if-let [why (or (when-not from "select an occurrence to make unique") (if-let [why (or (when-not from "select a cel to make unique")
(when-not (clip/symbol clip from) "the drawing it places is missing") (when-not (clip/symbol clip from) "the drawing it places is missing")
(when (empty? elsewhere) "nothing else uses this drawing"))] (when (empty? elsewhere) "nothing else uses this drawing"))]
{:refused why} {:refused why}

View file

@ -63,7 +63,7 @@
local (symbol/frame-of r id) local (symbol/frame-of r id)
inst? (= :instance (:kind (get nodes id))) inst? (= :instance (:kind (get nodes id)))
;; WHICH symbol, and which frame of it, are both read off the ;; WHICH symbol, and which frame of it, are both read off the
;; occurrence: inside a held cel is its drawing on the frame ;; cel: inside a held cel is its drawing on the frame
;; the hold pins, not on `local`, and inside a playing insert ;; the hold pins, not on `local`, and inside a playing insert
;; is its animation at its own in-point and speed. ;; is its animation at its own in-point and speed.
shown (when (number? local) shown (when (number? local)
@ -124,7 +124,7 @@
(partition 2 pts))))))) (partition 2 pts)))))))
(defn audio-tracks (defn audio-tracks
"Flatten audible source intervals through occurrence and parent clocks. "Flatten audible source intervals through cel and parent clocks.
A held visual source is silent. Every returned track carries a source offset, A held visual source is silent. Every returned track carries a source offset,
an output interval, and automation mapped into the open symbol's time." an output interval, and automation mapped into the open symbol's time."
[clip sid] [clip sid]
@ -298,7 +298,7 @@
only a move that keeps the PICTURE needs a frame, for the matrix." only a move that keeps the PICTURE needs a frame, for the matrix."
[clip sid path] [clip sid path]
(let [;; Structurally, a row leads into a symbol only where it names one: (let [;; Structurally, a row leads into a symbol only where it names one:
;; an occurrence does, and the lane holding it does not, so the walk ;; a cel does, and the lane holding it does not, so the walk
;; stops at a lane rather than picking the drawing showing now — which ;; stops at a lane rather than picking the drawing showing now — which
;; would make where a row lives depend on the playhead. ;; would make where a row lives depend on the playhead.
only (fn [sid id] only (fn [sid id]

View file

@ -136,13 +136,13 @@
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; time maps ;; time maps
;; ;;
;; Exposure, mouth lead and a symbol instance's timing are ONE mechanism, and ;; Cel, mouth lead and a symbol instance's timing are ONE mechanism, and
;; seeing that is what keeps them from being three implementations that disagree ;; seeing that is what keeps them from being three implementations that disagree
;; at the edges. ;; at the edges.
(defn expose (defn expose
"Hold a frame back onto an exposure grid: 1 = on 1s, 2 = on 2s. Frame 5 at "Hold a frame back onto an exposure grid: 1 = on 1s, 2 = on 2s. Frame 5 at
exposure 2 reads the pose from frame 4. cel 2 reads the pose from frame 4.
FLOOR, NEVER ROUND. Rounding would let an output frame read a pose from the FLOOR, NEVER ROUND. Rounding would let an output frame read a pose from the
FUTURE, which is a lead — a separate control, applied after this one, for a FUTURE, which is a lead — a separate control, applied after this one, for a
@ -170,7 +170,7 @@
"A node's own time as the affine map it is: `{:at a :rate r}`, meaning a frame "A node's own time as the affine map it is: `{:at a :rate r}`, meaning a frame
`p` of its parent is frame `r·(p − a)` of its own. THE SAME FOR EVERY NODE. A `p` of its parent is frame `r·(p − a)` of its own. THE SAME FOR EVERY NODE. A
node with no time map is `{:at 0 :rate 1}`, reading its parent's frames as its node with no time map is `{:at 0 :rate 1}`, reading its parent's frames as its
own; a mouth lead's `:offset` is folded into `:at`. Exposure and picture own; a mouth lead's `:offset` is folded into `:at`. Cel and picture
sampling are floors, not part of the map, and are left out: this is the map a sampling are floors, not part of the map, and are left out: this is the map a
move preserves and a timeline row draws with, and `local-frame` is what reads move preserves and a timeline row draws with, and `local-frame` is what reads
a frame, floors and the lead in their load-bearing order." a frame, floors and the lead in their load-bearing order."
@ -207,13 +207,13 @@
;; what an instance places ;; what an instance places
;; ;;
(defn source (defn source
"The symbol used by this occurrence. Sequence groups arrange occurrences; "The symbol used by this cel. Sequence groups arrange cels;
a row is a view of that group, not one row per source." a row is a view of that group, not one row per source."
[n] [n]
(when (= :instance (:kind n)) (get-in n [:source :symbol]))) (when (= :instance (:kind n)) (get-in n [:source :symbol])))
(defn sources (defn sources
"Structural references, including occurrences outside the playhead." "Structural references, including cels outside the playhead."
[n] [n]
(if-let [sid (source n)] #{sid} #{})) (if-let [sid (source n)] #{sid} #{}))
@ -221,7 +221,7 @@
(merge {:in 0 :speed 1 :end :stop} (:playback n))) (merge {:in 0 :speed 1 :end :stop} (:playback n)))
(defn source-time (defn source-time
"Invertible occurrence -> source map, or nil for holds and endpoint policies. "Invertible cel -> source map, or nil for holds and endpoint policies.
Forward sampling remains available through `placed-frame` in every case." Forward sampling remains available through `placed-frame` in every case."
[n] [n]
(let [{:keys [in speed end]} (playback-of n)] (let [{:keys [in speed end]} (playback-of n)]
@ -229,9 +229,9 @@
{:at (- (/ in speed)) :rate speed}))) {:at (- (/ in speed)) :rate speed})))
(defn placed-frame (defn placed-frame
"Sample source time without changing the occurrence's property clock: "Sample source time without changing the cel's property clock:
`{:symbol :frame}`, the symbol shown and which of its frames. `length` is that `{:symbol :frame}`, the symbol shown and which of its frames. `length` is that
symbol's frame count. Nil means no source contribution — this occurrence symbol's frame count. Nil means no source contribution — this cel
places nothing, or its playback has run past what there is to show." places nothing, or its playback has run past what there is to show."
[n f length] [n f length]
(when-let [sid (source n)] (when-let [sid (source n)]
@ -245,7 +245,14 @@
(when (and (<= 0 frame) (< frame length)) (when (and (<= 0 frame) (< frame length))
{:symbol sid :frame frame}))))) {:symbol sid :frame frame})))))
(defn sequence? [n] (defn lane?
"Is this group a LANE — a succession of cels rather than a composition?
`:layout :sequence` is the field because it names the RULE: children follow
one another and may not overlap. A group carrying it is called a lane, which
is the one place two words are kept for one thing, and they are kept apart on
purpose — the layout says what the rule is, the noun says what the thing is."
[n]
(and (= :group (:kind n)) (= :sequence (:layout n)))) (and (= :group (:kind n)) (= :sequence (:layout n))))
(defn finite-number? [v] (and (number? v) (js/Number.isFinite v))) (defn finite-number? [v] (and (number? v) (js/Number.isFinite v)))
@ -255,11 +262,11 @@
ORDER IS LOAD-BEARING: expose first, then offset. Flooring onto a grid and ORDER IS LOAD-BEARING: expose first, then offset. Flooring onto a grid and
shifting against the clock do not commute — shift first and the floor discards shifting against the clock do not commute — shift first and the floor discards
it on most frames, so the lead slider reads as doing nothing at exposures above it on most frames, so the lead slider reads as doing nothing at cels above
1, which is indistinguishable from the slider being unwired. 1, which is indistinguishable from the slider being unwired.
Composed along the parent chain, outermost first, by symbol/eval-frame. Two Composed along the parent chain, outermost first, by symbol/eval-frame. Two
rules fall out and they are different rules: exposure INHERITS STRICTLY, rules fall out and they are different rules: cel INHERITS STRICTLY,
because a head cutting on odd frames against a mouth cutting on even ones reads because a head cutting on odd frames against a mouth cutting on even ones reads
as two performances; offset is PER-NODE by design, because mouth lead applies as two performances; offset is PER-NODE by design, because mouth lead applies
to performance nodes and not to the plate, which is the entire point of it." to performance nodes and not to the plate, which is the entire point of it."
@ -425,7 +432,7 @@
(finite-number? speed) (<= 0 speed) (finite-number? speed) (<= 0 speed)
(#{:stop :hold :loop} end))))) (#{:stop :hold :loop} end)))))
(conj "playback needs a nonnegative finite :in and :speed, and :end :stop, :hold or :loop") (conj "playback needs a nonnegative finite :in and :speed, and :end :stop, :hold or :loop")
(and (:layout n) (not (sequence? n))) (and (:layout n) (not (lane? n)))
(conj ":layout :sequence belongs to a group") (conj ":layout :sequence belongs to a group")
(and (= k :audio) (not (some (:source n) [:footage :sound]))) (and (= k :audio) (not (some (:source n) [:footage :sound])))
(conj "an audio node needs a :source :footage or :sound") (conj "an audio node needs a :source :footage or :sound")

View file

@ -6,7 +6,7 @@
(:require [arthur.domain.node :as node])) (:require [arthur.domain.node :as node]))
(defn prepare (defn prepare
"Sort exposure tracks once when building a resolver." "Sort pose tracks once when building a resolver."
[tracks] [tracks]
(into {} (into {}
(map (fn [[group entries]] (map (fn [[group entries]]
@ -37,8 +37,8 @@
untouched." untouched."
[clip sid instance group at source] [clip sid instance group at source]
(let [inst (get-in clip [:symbols sid :nodes instance]) (let [inst (get-in clip [:symbols sid :nodes instance])
;; A cut is checked against the ONE symbol this occurrence places. Which ;; A cut is checked against the ONE symbol this cel places. Which
;; drawing a lane shows is a question about the lane's other occurrences, ;; drawing a lane shows is a question about the lane's other cels,
;; and each of them owns its own tracks — so there is nothing to union. ;; and each of them owns its own tracks — so there is nothing to union.
placed (get-in clip [:symbols (node/source inst)]) placed (get-in clip [:symbols (node/source inst)])
length (or (:frames placed) 0) length (or (:frames placed) 0)
@ -70,7 +70,7 @@
clip))) clip)))
(defn problems (defn problems
"Errors in one symbol instance's exposure tracks." "Errors in one symbol instance's pose tracks."
[tracks source-frames groups] [tracks source-frames groups]
(cond (cond
(nil? tracks) [] (nil? tracks) []

View file

@ -30,7 +30,7 @@
[arthur.domain.wire :as wire])) [arthur.domain.wire :as wire]))
(def schema-version (def schema-version
"3 stores occurrence source references and explicit playback clocks. Older "3 stores cel source references and explicit playback clocks. Older
source-channel documents are unsupported; there is no compatibility conversion." source-channel documents are unsupported; there is no compatibility conversion."
3) 3)

View file

@ -90,8 +90,8 @@
[nodes id] [nodes id]
(dec (count (lineage nodes id)))) (dec (count (lineage nodes id))))
(defn sequence-members (defn lane-cels
"The occurrences of lane `lane`, in the order they are exposed. "The cels of lane `lane`, in the order they are exposed.
Sorted by where they START, not by `:z`: a lane's blocks follow one another in Sorted by where they START, not by `:z`: a lane's blocks follow one another in
time, and two of them cannot be in the same place for `:z` to decide between. time, and two of them cannot be in the same place for `:z` to decide between.
@ -102,13 +102,13 @@
(sort-by (juxt #(or (first (node/placed-span %)) 0) #(str (:id %)))) (sort-by (juxt #(or (first (node/placed-span %)) 0) #(str (:id %))))
vec)) vec))
(defn sequence-problems (defn lane-problems
"What makes a lane not a lane. A SEQUENCE is the one composition rule the node "What makes a lane not a lane. A SEQUENCE is the one composition rule the node
map carries — ordinary groups compose freely — so it is checked here, beside map carries — ordinary groups compose freely — so it is checked here, beside
the parent and stencil references, rather than wherever a command happens to the parent and stencil references, rather than wherever a command happens to
build one. build one.
Occurrences must be visual, finite and non-overlapping. An accidental overlap Cels must be visual, finite and non-overlapping. An accidental overlap
is refused rather than resolved by draw order: two drawings exposed on one is refused rather than resolved by draw order: two drawings exposed on one
frame of one lane is a document nobody meant to write, and picking a winner frame of one lane is a document nobody meant to write, and picking a winner
would hide it. Empty lanes are valid — a lane is made before it is filled." would hide it. Empty lanes are valid — a lane is made before it is filled."
@ -116,7 +116,7 @@
(vec (vec
(mapcat (mapcat
(fn [[id lane]] (fn [[id lane]]
(when (node/sequence? lane) (when (node/lane? lane)
(let [children (filter #(= id (:parent %)) (vals nodes)) (let [children (filter #(= id (:parent %)) (vals nodes))
valid? (fn [n] valid? (fn [n]
(and (= :instance (:kind n)) (and (= :instance (:kind n))
@ -126,9 +126,9 @@
intervals (sort-by first (map node/placed-span (filter valid? children)))] intervals (sort-by first (map node/placed-span (filter valid? children)))]
(concat (concat
(for [n children :when (not (valid? n))] (for [n children :when (not (valid? n))]
(str "sequence " id " needs finite visual occurrences: " (:id n))) (str "sequence " id " needs finite visual cels: " (:id n)))
(when (some (fn [[[_ b] [c _]]] (> b c)) (partition 2 1 intervals)) (when (some (fn [[[_ b] [c _]]] (> b c)) (partition 2 1 intervals))
[(str "sequence " id " has overlapping occurrences")]))))) [(str "lane " id " has overlapping cels")])))))
nodes))) nodes)))
(defn order (defn order
@ -613,7 +613,7 @@
(if-not (map? nodes) (if-not (map? nodes)
[":nodes must be a map of id -> node"] [":nodes must be a map of id -> node"]
(-> [] (-> []
(into (sequence-problems nodes)) (into (lane-problems nodes))
(into (for [[id n] nodes (into (for [[id n] nodes
:when (not= id (:id n))] :when (not= id (:id n))]
(str "node under key " (pr-str id) " has :id " (pr-str (:id n))))) (str "node under key " (pr-str id) " has :id " (pr-str (:id n)))))

View file

@ -9,7 +9,7 @@
[arthur.domain.gesture :as gesture] [arthur.domain.gesture :as gesture]
[arthur.domain.nest :as nest] [arthur.domain.nest :as nest]
[arthur.domain.node :as node] [arthur.domain.node :as node]
[arthur.domain.sequence :as sequence] [arthur.domain.lane :as lane]
[arthur.events.edit :as edit] [arthur.events.edit :as edit]
[arthur.events.paint :as paint] [arthur.events.paint :as paint]
[arthur.events.playback :as playback] [arthur.events.playback :as playback]
@ -27,7 +27,7 @@
[:clip :symbols sid :nodes id :kind]))] [:clip :symbols sid :nodes id :kind]))]
(cond-> (-> db (cond-> (-> db
(assoc-in [:ui :selection] selection) (assoc-in [:ui :selection] selection)
(update :ui dissoc :points :sequence-retry)) (update :ui dissoc :points :lane-retry))
(and (= :node kind) path (not sound?)) (and (= :node kind) path (not sound?))
(update-in [:ui :expanded] (fnil into #{}) (rest (reductions conj [] (pop path)))))))) (update-in [:ui :expanded] (fnil into #{}) (rest (reductions conj [] (pop path))))))))
@ -35,33 +35,33 @@
::set-tone ::set-tone
(fn [db [_ tone]] (assoc-in db [:ui :tone] tone))) (fn [db [_ tone]] (assoc-in db [:ui :tone] tone)))
(defn apply-sequence-command (defn apply-lane-command
"Commit a successful domain command as one history step. A refused command "Commit a successful domain command as one history step. A refused command
leaves the document and history untouched; an overflow offers an explicit retry." leaves the document and history untouched; an overflow offers an explicit retry."
[db sid result retry] [db sid result retry]
(if-let [why (:refused result)] (if-let [why (:refused result)]
(-> db (-> db
(assoc-in [:project :status] why) (assoc-in [:project :status] why)
(assoc-in [:ui :sequence-retry] (assoc-in [:ui :lane-retry]
(when (:required-frames result) retry))) (when (:required-frames result) retry)))
(let [[_ selected-sid _ path] (get-in db [:ui :selection]) (let [[_ selected-sid _ path] (get-in db [:ui :selection])
prefix (if (and (= sid selected-sid) (seq path)) (pop path) [])] prefix (if (and (= sid selected-sid) (seq path)) (pop path) [])]
(-> db (-> db
(edit/transaction (constantly (:clip result))) (edit/transaction (constantly (:clip result)))
(assoc-in [:ui :selection] [:node sid (:selection result) (conj prefix (:selection result))]) (assoc-in [:ui :selection] [:node sid (:selection result) (conj prefix (:selection result))])
(update :ui dissoc :sequence-retry))))) (update :ui dissoc :lane-retry)))))
(rf/reg-event-db (rf/reg-event-db
::new-lane ::new-lane
(fn [db _] (fn [db _]
(let [clip (:clip (store/entry (:clip/current db))) (let [clip (:clip (store/entry (:clip/current db)))
sid (get-in db [:ui :open])] sid (get-in db [:ui :open])]
(apply-sequence-command db sid (sequence/add-lane clip sid (random-uuid)) nil)))) (apply-lane-command db sid (lane/add-lane clip sid (random-uuid)) nil))))
(defn- committed (defn- committed
"One appending command, as effects: commit it, and look at what it made. "One appending command, as effects: commit it, and look at what it made.
Seeking is the whole reason these are `-fx` events. An appended exposure lands Seeking is the whole reason these are `-fx` events. An appended cel lands
past the end of the lane, off the playhead, and a drawing you cannot see is not past the end of the lane, off the playhead, and a drawing you cannot see is not
one you can draw in. An inserted one is already under the playhead and the one you can draw in. An inserted one is already under the playhead and the
seek is a no-op, which is the same rule and not a second one." seek is a no-op, which is the same rule and not a second one."
@ -71,23 +71,23 @@
{:keys [at rate]} (:time (nest/inside clip st (get-in db [:ui :open]) {:keys [at rate]} (:time (nest/inside clip st (get-in db [:ui :open])
(if (seq path) (pop path) []) (if (seq path) (pop path) [])
(get-in db [:playback :frame])))] (get-in db [:playback :frame])))]
(cond-> {:db (apply-sequence-command db sid result retry)} (cond-> {:db (apply-lane-command db sid result retry)}
(and (:clip result) (:frame result) rate) (and (:clip result) (:frame result) rate)
(assoc :dispatch [::playback/seek (+ at (/ (:frame result) rate))])))) (assoc :dispatch [::playback/seek (+ at (/ (:frame result) rate))]))))
(defn- selected-lane (defn- selected-lane
"The lane a command should act in: the selected lane itself, or the one "The lane a command should act in: the selected lane itself, or the one
holding the selected occurrence." holding the selected cel."
[clip sid id] [clip sid id]
(let [n (get-in clip [:symbols sid :nodes id])] (let [n (get-in clip [:symbols sid :nodes id])]
(if (node/sequence? n) id (:parent n)))) (if (node/lane? n) id (:parent n))))
(rf/reg-event-fx (rf/reg-event-fx
::append-drawing ::append-drawing
(fn [{:keys [db]} [_ extent]] (fn [{:keys [db]} [_ extent]]
(let [clip (:clip (store/entry (:clip/current db))) (let [clip (:clip (store/entry (:clip/current db)))
[_ sid id] (get-in db [:ui :selection]) [_ sid id] (get-in db [:ui :selection])
result (sequence/append-drawing clip sid (selected-lane clip sid id) result (lane/append-drawing clip sid (selected-lane clip sid id)
(random-uuid) (clip/fresh-id clip) (random-uuid) (clip/fresh-id clip)
{:extent (or extent :keep)})] {:extent (or extent :keep)})]
(committed db sid result [::append-drawing :grow-symbol])))) (committed db sid result [::append-drawing :grow-symbol]))))
@ -97,7 +97,7 @@
(fn [{:keys [db]} [_ extent]] (fn [{:keys [db]} [_ extent]]
(let [clip (:clip (store/entry (:clip/current db))) (let [clip (:clip (store/entry (:clip/current db)))
[_ sid id] (get-in db [:ui :selection]) [_ sid id] (get-in db [:ui :selection])
result (sequence/reuse-drawing clip sid (selected-lane clip sid id) (random-uuid) result (lane/reuse-drawing clip sid (selected-lane clip sid id) (random-uuid)
(node/source (get-in clip [:symbols sid :nodes id])) (node/source (get-in clip [:symbols sid :nodes id]))
{:extent (or extent :keep)})] {:extent (or extent :keep)})]
(committed db sid result [::reuse-drawing :grow-symbol])))) (committed db sid result [::reuse-drawing :grow-symbol]))))
@ -107,7 +107,7 @@
(fn [{:keys [db]} [_ extent deep?]] (fn [{:keys [db]} [_ extent deep?]]
(let [clip (:clip (store/entry (:clip/current db))) (let [clip (:clip (store/entry (:clip/current db)))
[_ sid id] (get-in db [:ui :selection]) [_ sid id] (get-in db [:ui :selection])
result (sequence/duplicate-drawing clip sid id (random-uuid) result (lane/duplicate-drawing clip sid id (random-uuid)
{:extent (or extent :keep) :deep? deep?})] {:extent (or extent :keep) :deep? deep?})]
(committed db sid result [::duplicate-drawing :grow-symbol deep?])))) (committed db sid result [::duplicate-drawing :grow-symbol deep?]))))
@ -120,67 +120,67 @@
(let [clip (:clip (store/entry (:clip/current db))) (let [clip (:clip (store/entry (:clip/current db)))
[_ sid id] (get-in db [:ui :selection]) [_ sid id] (get-in db [:ui :selection])
lane (selected-lane clip sid id) lane (selected-lane clip sid id)
at (sequence/lane-frame clip sid lane (get-in db [:playback :frame])) at (lane/lane-frame clip sid lane (get-in db [:playback :frame]))
result (if at result (if at
(sequence/append-drawing clip sid lane (random-uuid) (clip/fresh-id clip) (lane/append-drawing clip sid lane (random-uuid) (clip/fresh-id clip)
{:at at :extent (or extent :keep)}) {:at at :extent (or extent :keep)})
{:refused "this lane's frames are not the open symbol's"})] {:refused "this lane's frames are not the open symbol's"})]
(committed db sid result [::insert-drawing :grow-symbol])))) (committed db sid result [::insert-drawing :grow-symbol]))))
(rf/reg-event-db (rf/reg-event-db
::split-exposure ::split-cel
(fn [db _] (fn [db _]
(let [clip (:clip (store/entry (:clip/current db))) (let [clip (:clip (store/entry (:clip/current db)))
[_ sid id] (get-in db [:ui :selection]) [_ sid id] (get-in db [:ui :selection])
cut (sequence/lane-frame clip sid (:parent (get-in clip [:symbols sid :nodes id])) cut (lane/lane-frame clip sid (:parent (get-in clip [:symbols sid :nodes id]))
(get-in db [:playback :frame]))] (get-in db [:playback :frame]))]
(apply-sequence-command (apply-lane-command
db sid (if cut db sid (if cut
(sequence/split clip sid id cut (random-uuid)) (lane/split clip sid id cut (random-uuid))
{:refused "this lane's frames are not the open symbol's"}) {:refused "this lane's frames are not the open symbol's"})
nil)))) nil))))
(defn- at-playhead (defn- at-playhead
"The selected occurrence, its lane, and the playhead as a frame of that lane's "The selected cel, its lane, and the playhead as a frame of that lane's
own time — or a refusal in place of the frame where there is no single one." own time — or a refusal in place of the frame where there is no single one."
[db] [db]
(let [clip (:clip (store/entry (:clip/current db))) (let [clip (:clip (store/entry (:clip/current db)))
[_ sid id] (get-in db [:ui :selection]) [_ sid id] (get-in db [:ui :selection])
n (get-in clip [:symbols sid :nodes id])] n (get-in clip [:symbols sid :nodes id])]
{:clip clip :sid sid :id id :node n {:clip clip :sid sid :id id :node n
:at (sequence/lane-frame clip sid (:parent n) (get-in db [:playback :frame]))})) :at (lane/lane-frame clip sid (:parent n) (get-in db [:playback :frame]))}))
(rf/reg-event-db (rf/reg-event-db
::trim-exposure ::trim-cel
(fn [db [_ edge]] (fn [db [_ edge]]
(let [{:keys [clip sid id at]} (at-playhead db)] (let [{:keys [clip sid id at]} (at-playhead db)]
(apply-sequence-command (apply-lane-command
db sid (if at db sid (if at
(sequence/trim clip sid id edge at) (lane/trim clip sid id edge at)
{:refused "this lane's frames are not the open symbol's"}) {:refused "this lane's frames are not the open symbol's"})
nil)))) nil))))
(rf/reg-event-db (rf/reg-event-db
::move-exposure ::move-cel
(fn [db _] (fn [db _]
(let [{:keys [clip sid id at]} (at-playhead db)] (let [{:keys [clip sid id at]} (at-playhead db)]
(apply-sequence-command (apply-lane-command
db sid (if at db sid (if at
(sequence/move clip sid id at) (lane/move clip sid id at)
{:refused "this lane's frames are not the open symbol's"}) {:refused "this lane's frames are not the open symbol's"})
nil)))) nil))))
(rf/reg-event-db (rf/reg-event-db
::blank-exposure ::blank-cel
;; The selected exposure's own frames, so the range needs no second gesture and ;; The selected cel's own frames, so the range needs no second gesture and
;; the case that would split an exposure cannot arise. ;; the case that would split a cel cannot arise.
(fn [db _] (fn [db _]
(let [{:keys [clip sid node]} (at-playhead db) (let [{:keys [clip sid node]} (at-playhead db)
span (node/placed-span node)] span (node/placed-span node)]
(apply-sequence-command (apply-lane-command
db sid (if (and span (every? integer? span)) db sid (if (and span (every? integer? span))
(sequence/blank clip sid (:parent node) span {}) (lane/blank clip sid (:parent node) span {})
{:refused "select an exposure that starts and ends on whole lane frames"}) {:refused "select a cel that starts and ends on whole lane frames"})
nil)))) nil))))
(rf/reg-event-db (rf/reg-event-db
@ -188,21 +188,21 @@
(fn [db [_ deep?]] (fn [db [_ deep?]]
(let [clip (:clip (store/entry (:clip/current db))) (let [clip (:clip (store/entry (:clip/current db)))
[_ sid id] (get-in db [:ui :selection])] [_ sid id] (get-in db [:ui :selection])]
(apply-sequence-command db sid (sequence/make-unique clip sid id {:deep? deep?}) nil)))) (apply-lane-command db sid (lane/make-unique clip sid id {:deep? deep?}) nil))))
(rf/reg-event-db (rf/reg-event-db
::extend-hold ::extend-hold
(fn [db [_ delta extent]] (fn [db [_ delta extent]]
(let [clip (:clip (store/entry (:clip/current db))) (let [clip (:clip (store/entry (:clip/current db)))
[_ sid id] (get-in db [:ui :selection]) [_ sid id] (get-in db [:ui :selection])
result (sequence/extend-hold clip sid id delta {:extent (or extent :keep)})] result (lane/extend-hold clip sid id delta {:extent (or extent :keep)})]
(apply-sequence-command db sid result [::extend-hold delta :grow-symbol])))) (apply-lane-command db sid result [::extend-hold delta :grow-symbol]))))
(rf/reg-event-fx (rf/reg-event-fx
::sequence-retry ::lane-retry
(fn [{:keys [db]} _] (fn [{:keys [db]} _]
(if-let [event (get-in db [:ui :sequence-retry])] (if-let [event (get-in db [:ui :lane-retry])]
{:db (update db :ui dissoc :sequence-retry) :dispatch event} {:db (update db :ui dissoc :lane-retry) :dispatch event}
{}))) {})))
(rf/reg-event-db (rf/reg-event-db

View file

@ -84,14 +84,14 @@
Four things, and each for its own reason: Four things, and each for its own reason:
the node itself; the node itself;
everything ABOVE it, because a placement's transform is relative to its everything ABOVE it, because an instance's transform is relative to its
parent and dropping the chain would move the thing being isolated; parent and dropping the chain would move the thing being isolated;
everything BELOW it, because a group instance is its children; everything BELOW it, because a group instance is its children;
any audio track `:linked-to` it, because the link is the statement that this any audio track `:linked-to` it, because the link is the statement that this
sound belongs to that placement, and a face exported without its voice is sound belongs to that instance, and a face exported without its voice is
not the thing that was asked for. not the thing that was asked for.
Siblings go. That is the whole point: what comes out is one placement, where it Siblings go. That is the whole point: what comes out is one instance, where it
sits, in the symbol it sits in." sits, in the symbol it sits in."
[nodes id] [nodes id]
(let [up (loop [i id acc #{}] (let [up (loop [i id acc #{}]
@ -117,9 +117,9 @@
"The symbol with only `id` and its kin kept. `nil` leaves it alone. "The symbol with only `id` and its kin kept. `nil` leaves it alone.
The FRAME SPACE IS UNTOUCHED, which is what makes this different from exporting The FRAME SPACE IS UNTOUCHED, which is what makes this different from exporting
the symbol a placement plays. Rooting at `:sym/face-8625` renders the drawing in the symbol an instance plays. Rooting at `:sym/face-8625` renders the drawing in
its own time, identically for all seven placements. Isolating one placement its own time, identically for all seven instances. Isolating one instance
renders the STAGE — its length, its rate, the placement's span, drift and scale renders the STAGE — its length, its rate, the instance's span, drift and scale
— with the other six removed. The first is the drawing; the second is that face — with the other six removed. The first is the drawing; the second is that face
on the stage, and they are different deliverables." on the stage, and they are different deliverables."
[sym id] [sym id]

View file

@ -74,10 +74,11 @@
::exposure ::exposure
:<- [::symbol] :<- [::symbol]
(fn [sym _] (fn [sym _]
;; Exposure lives on the symbol's root node and is INHERITED, so reading it ;; NOT a cel. This is `:time :expose` — how many frames each step of a
;; there is reading it everywhere. The transport shows it so that `exposure 2` ;; subtree lasts, which is what shooting on twos means — and it lives on the
;; is visibly doing something at the transport rather than only inside the ;; symbol's root node and is INHERITED, so reading it there is reading it
;; document. ;; everywhere. The transport shows it so that `exposure 2` is visibly doing
;; something rather than only inside the document.
(or (get-in sym [:nodes :root :time :expose]) 1))) (or (get-in sym [:nodes :root :time :expose]) 1)))
(rf/reg-sub (rf/reg-sub
@ -115,7 +116,7 @@
(defn- placed? (defn- placed?
"Does row `path` from symbol `sid` still name an instance, all the way down?" "Does row `path` from symbol `sid` still name an instance, all the way down?"
[clip sid path] [clip sid path]
;; Only an occurrence names a symbol. A lane is a group, so a row path that ;; Only a cel names a symbol. A lane is a group, so a row path that
;; ends at the lane rather than at one of its cels names no placement — which ;; ends at the lane rather than at one of its cels names no placement — which
;; is the structural answer, and does not move as the lane cuts. ;; is the structural answer, and does not move as the lane cuts.
(reduce (fn [sid id] (reduce (fn [sid id]

View file

@ -12,7 +12,7 @@
[re-frame.core :as rf])) [re-frame.core :as rf]))
(rf/reg-sub ::selection (fn [db _] (get-in db [:ui :selection]))) (rf/reg-sub ::selection (fn [db _] (get-in db [:ui :selection])))
(rf/reg-sub ::sequence-retry (fn [db _] (get-in db [:ui :sequence-retry]))) (rf/reg-sub ::lane-retry (fn [db _] (get-in db [:ui :lane-retry])))
(rf/reg-sub ::tone (fn [db _] (get-in db [:ui :tone]))) (rf/reg-sub ::tone (fn [db _] (get-in db [:ui :tone])))
(rf/reg-sub ::tool (fn [db _] (get-in db [:ui :tool]))) (rf/reg-sub ::tool (fn [db _] (get-in db [:ui :tool])))
(rf/reg-sub ::draft (fn [db _] (get-in db [:ui :draft]))) (rf/reg-sub ::draft (fn [db _] (get-in db [:ui :draft])))

View file

@ -215,7 +215,7 @@
[facts [facts
"name" (or (:name n) (brief id)) "name" (or (:name n) (brief id))
"id" (brief id) "id" (brief id)
;; Which symbol an occurrence places. The one fact that makes an instance ;; Which symbol a cel places. The one fact that makes an instance
;; legible as an instance rather than as a node. ;; legible as an instance rather than as a node.
"of" (when (= :instance (:kind n)) (str (node/source n))) "of" (when (= :instance (:kind n)) (str (node/source n)))
;; A span is in the node's OWN frames and `at` is where its frame 0 sits ;; A span is in the node's OWN frames and `at` is where its frame 0 sits
@ -239,7 +239,7 @@
;; tracing a face ;; tracing a face
;; ;;
;; THE FACE'S OWN FACTS ONLY, and both of them are keyed to the face rather than ;; THE FACE'S OWN FACTS ONLY, and both of them are keyed to the face rather than
;; to a placement of it: which of its frames its drawings were made over, and ;; to an instance of it: which of its frames its drawings were made over, and
;; what its origin does between those frames. See `domain/trace`. ;; what its origin does between those frames. See `domain/trace`.
;; ;;
;; Whether the footage is SHOWING is deliberately not here. It is a viewing aid ;; Whether the footage is SHOWING is deliberately not here. It is a viewing aid
@ -435,11 +435,11 @@
node @(rf/subscribe [::sub/selected-node]) node @(rf/subscribe [::sub/selected-node])
tracked? (seq (owners clip)) tracked? (seq (owners clip))
;; The face the tracing section is about: the SELECTED PLACEMENT's symbol, ;; The face the tracing section is about: the SELECTED PLACEMENT's symbol,
;; or, when the selection is not a placement or there is none, the OPEN ;; or, when the selection is not an instance or there is none, the OPEN
;; symbol — which is the face itself when a face is open to be drawn over. ;; symbol — which is the face itself when a face is open to be drawn over.
;; That last case had no section at all before, and it is the one where ;; That last case had no section at all before, and it is the one where
;; the keys are actually being set. ;; the keys are actually being set.
;; The symbol this occurrence places, or nil for a lane: which face a ;; The symbol this cel places, or nil for a lane: which face a
;; lane traces is not a question with one answer, and naming the drawing ;; lane traces is not a question with one answer, and naming the drawing
;; showing now would move the section under the playhead. ;; showing now would move the section under the playhead.
placed (node/source (peek node)) placed (node/source (peek node))

View file

@ -23,7 +23,7 @@
(:require [clojure.string :as str] (:require [clojure.string :as str]
[arthur.domain.node :as node] [arthur.domain.node :as node]
[arthur.domain.nest :as nest] [arthur.domain.nest :as nest]
[arthur.domain.sequence :as sequence] [arthur.domain.lane :as lane]
[arthur.domain.symbol :as symbol] [arthur.domain.symbol :as symbol]
[arthur.domain.trace :as trace] [arthur.domain.trace :as trace]
[arthur.events.playback :as pb] [arthur.events.playback :as pb]
@ -48,7 +48,7 @@
frame 48 of the symbol it is in, and drawing it at 0 puts every placement's frame 48 of the symbol it is in, and drawing it at 0 puts every placement's
keys in the same place however staggered they are. keys in the same place however staggered they are.
Exposure is not inverted, because it is a floor and has no inverse: a key on a Cel is not inverted, because it is a floor and has no inverse: a key on a
frame the exposure grid never samples is still authored on that frame, and that frame the exposure grid never samples is still authored on that frame, and that
is where the row should show it." is where the row should show it."
[n] [n]
@ -140,16 +140,16 @@
(distinct)) (distinct))
(vals channels)) (vals channels))
:dense? (boolean (some :dense (vals channels)))}] :dense? (boolean (some :dense (vals channels)))}]
;; AN OCCURRENCE IS NOT A ROW. A lane's drawings are cel ;; AN CEL IS NOT A ROW. A lane's drawings are cel
;; blocks on the lane's own row, so a lane of twelve ;; blocks on the lane's own row, so a lane of twelve
;; exposures is one row and not twelve — which is the ;; cels is one row and not twelve — which is the
;; vertical growth that made a keyed source look ;; vertical growth that made a keyed source look
;; necessary. The occurrence is still the thing selected ;; necessary. The cel is still the thing selected
;; and addressed; only its presentation is shared. ;; and addressed; only its presentation is shared.
(if (node/sequence? (get-in sym [:nodes (:parent n)])) (if (node/lane? (get-in sym [:nodes (:parent n)]))
[] []
(let [row (cond-> row (let [row (cond-> row
(node/sequence? n) (node/lane? n)
(assoc :cels (assoc :cels
(mapv (fn [child] (mapv (fn [child]
{:id (:id child) {:id (:id child)
@ -158,7 +158,7 @@
:source (node/source child) :source (node/source child)
:span (mapv self (node/placed-span child)) :span (mapv self (node/placed-span child))
:select [:node sid (:id child) (conj path (:id child))]}) :select [:node sid (:id child) (conj path (:id child))]})
(symbol/sequence-members (:nodes sym) id))))] (symbol/lane-cels (:nodes sym) id))))]
(if-not open? (if-not open?
[row] [row]
(-> [row] (-> [row]
@ -175,7 +175,7 @@
(defn sound-rows (defn sound-rows
"Audio rows use the same flattened intervals as the mixer, including source "Audio rows use the same flattened intervals as the mixer, including source
in-points, occurrence speeds, parent timing, and silence beneath visual holds." in-points, cel speeds, parent timing, and silence beneath visual holds."
[clip sid expanded] [clip sid expanded]
(if-not (get-in clip [:symbols sid]) [] (if-not (get-in clip [:symbols sid]) []
(vec (vec
@ -228,19 +228,19 @@
clip @(rf/subscribe [::render/clip]) clip @(rf/subscribe [::render/clip])
[_ sid id] @(rf/subscribe [::sub/selection]) [_ sid id] @(rf/subscribe [::sub/selection])
n (get-in clip [:symbols sid :nodes id]) n (get-in clip [:symbols sid :nodes id])
lane (if (node/sequence? n) n (get-in clip [:symbols sid :nodes (:parent n)])) lane (if (node/lane? n) n (get-in clip [:symbols sid :nodes (:parent n)]))
lane? (node/sequence? lane) lane? (node/lane? lane)
cel? (and lane? (= :instance (:kind n)) (some? (node/source n))) cel? (and lane? (= :instance (:kind n)) (some? (node/source n)))
held? (and cel? (zero? (:speed (node/playback-of n)))) held? (and cel? (zero? (:speed (node/playback-of n))))
;; Shared use is shown rather than discovered: the button that decouples ;; Shared use is shown rather than discovered: the button that decouples
;; an exposure is only offered where there is something to decouple from. ;; a cel is only offered where there is something to decouple from.
shared? (and cel? (< 1 (count (for [[_ sym] (:symbols clip) shared? (and cel? (< 1 (count (for [[_ sym] (:symbols clip)
[_ other] (:nodes sym) [_ other] (:nodes sym)
:when (= (node/source n) (node/source other))] :when (= (node/source n) (node/source other))]
other)))) other))))
;; Both act at the playhead, so both are offered only where the playhead ;; Both act at the playhead, so both are offered only where the playhead
;; is somewhere they mean something. ;; is somewhere they mean something.
at (when lane? (sequence/lane-frame clip sid (:id lane) frame)) at (when lane? (lane/lane-frame clip sid (:id lane) frame))
insertable? (and lane? (integer? at)) insertable? (and lane? (integer? at))
splittable? (and cel? (integer? at) splittable? (and cel? (integer? at)
(let [[lo hi] (node/placed-span n)] (< lo at hi)))] (let [[lo hi] (node/placed-span n)] (< lo at hi)))]
@ -273,37 +273,37 @@
:title "a new drawing at the playhead; later drawings ripple later" :title "a new drawing at the playhead; later drawings ripple later"
:on-click #(rf/dispatch [::ui/insert-drawing])} "insert"] :on-click #(rf/dispatch [::ui/insert-drawing])} "insert"]
[:button {:disabled (not splittable?) [:button {:disabled (not splittable?)
:title "cut this exposure in two at the playhead; the picture does not change" :title "cut this cel in two at the playhead; the picture does not change"
:on-click #(rf/dispatch [::ui/split-exposure])} "split"] :on-click #(rf/dispatch [::ui/split-cel])} "split"]
[:button {:disabled (not cel?) [:button {:disabled (not cel?)
:title "expose this same drawing again — one drawing, two exposures" :title "expose this same drawing again — one drawing, two cels"
:on-click #(rf/dispatch [::ui/reuse-drawing])} "reuse"] :on-click #(rf/dispatch [::ui/reuse-drawing])} "reuse"]
[:button {:disabled (not cel?) [:button {:disabled (not cel?)
:title "append a copy of this drawing, to draw the next one over it" :title "append a copy of this drawing, to draw the next one over it"
:on-click #(rf/dispatch [::ui/duplicate-drawing])} "duplicate"] :on-click #(rf/dispatch [::ui/duplicate-drawing])} "duplicate"]
[:button {:disabled (not shared?) [:button {:disabled (not shared?)
:title "give this exposure its own copy; other exposures keep sharing" :title "give this cel its own copy; other cels keep sharing"
:on-click #(rf/dispatch [::ui/make-unique])} "make unique"] :on-click #(rf/dispatch [::ui/make-unique])} "make unique"]
[:button {:disabled (not splittable?) [:button {:disabled (not splittable?)
:title "start this exposure at the playhead; nothing else moves" :title "start this cel at the playhead; nothing else moves"
:on-click #(rf/dispatch [::ui/trim-exposure :in])} "trim in"] :on-click #(rf/dispatch [::ui/trim-cel :in])} "trim in"]
[:button {:disabled (not splittable?) [:button {:disabled (not splittable?)
:title "end this exposure at the playhead; nothing else moves" :title "end this cel at the playhead; nothing else moves"
:on-click #(rf/dispatch [::ui/trim-exposure :out])} "trim out"] :on-click #(rf/dispatch [::ui/trim-cel :out])} "trim out"]
[:button {:disabled (not (and cel? (integer? at))) [:button {:disabled (not (and cel? (integer? at)))
:title "put this exposure at the playhead; refused if something is there" :title "put this cel at the playhead; refused if something is there"
:on-click #(rf/dispatch [::ui/move-exposure])} "move here"] :on-click #(rf/dispatch [::ui/move-cel])} "move here"]
[:button {:disabled (not cel?) [:button {:disabled (not cel?)
:title "clear this exposure's frames, leaving a gap; later drawings stay put" :title "clear this cel's frames, leaving a gap; later drawings stay put"
:on-click #(rf/dispatch [::ui/blank-exposure])} "blank"] :on-click #(rf/dispatch [::ui/blank-cel])} "blank"]
[:button {:disabled (not held?) [:button {:disabled (not held?)
:title "shorten this exposure; ripple later drawings, keeping lane keys fixed" :title "shorten this cel; ripple later drawings, keeping lane keys fixed"
:on-click #(rf/dispatch [::ui/extend-hold -1])} "hold −"] :on-click #(rf/dispatch [::ui/extend-hold -1])} "hold −"]
[:button {:disabled (not held?) [:button {:disabled (not held?)
:title "extend this exposure; ripple later drawings, keeping lane keys fixed" :title "extend this cel; ripple later drawings, keeping lane keys fixed"
:on-click #(rf/dispatch [::ui/extend-hold 1])} "hold +"] :on-click #(rf/dispatch [::ui/extend-hold 1])} "hold +"]
(when @(rf/subscribe [::sub/sequence-retry]) (when @(rf/subscribe [::sub/lane-retry])
[:button {:on-click #(rf/dispatch [::ui/sequence-retry])} "extend shot and apply"]) [:button {:on-click #(rf/dispatch [::ui/lane-retry])} "extend shot and apply"])
[:span.spacer] [:span.spacer]
[:span.dim (str frame " / " frames)] [:span.dim (str frame " / " frames)]
;; Measured in the loop, not derived from the clock — the whole question ;; Measured in the loop, not derived from the clock — the whole question
@ -475,7 +475,7 @@
:when (< in out)] :when (< in out)]
^{:key (str id)} ^{:key (str id)}
[:button.tl-cel [:button.tl-cel
{:title (str label " · select occurrence; double-click to edit shared drawing") {:title (str label " · select cel; double-click to edit shared drawing")
:style {:position "absolute" :left (edge% in frames) :style {:position "absolute" :left (edge% in frames)
:width (str (* 100 (/ (- out in) (max 1 frames))) "%") :width (str (* 100 (/ (- out in) (max 1 frames))) "%")
:top "2px" :bottom "2px" :overflow "hidden" :padding "0 3px"} :top "2px" :bottom "2px" :overflow "hidden" :padding "0 3px"}

View file

@ -9,7 +9,7 @@
[arthur.domain.node :as node] [arthur.domain.node :as node]
[arthur.domain.palette :as pal] [arthur.domain.palette :as pal]
[arthur.domain.pick :as pick] [arthur.domain.pick :as pick]
[arthur.domain.sequence :as sequence] [arthur.domain.lane :as lane]
[arthur.domain.symbol :as symbol])) [arthur.domain.symbol :as symbol]))
(defn drawing [id x frames] (defn drawing [id x frames]
@ -18,17 +18,17 @@
:channels {[:geom :size] (ch/framed 4) :channels {[:geom :size] (ch/framed 4)
[:xform :pos] (ch/framed [x 0])}}}}) [:xform :pos] (ch/framed [x 0])}}}})
(defn occurrence [id source at duration speed] (defn cel [id source at duration speed]
{:id id :kind :instance :parent :girl :z (name id) {:id id :kind :instance :parent :girl :z (name id)
:source {:symbol source} :playback {:in 0 :speed speed :end :stop} :source {:symbol source} :playback {:in 0 :speed speed :end :stop}
:time {:at at :rate 1} :span [0 duration]}) :time {:at at :rate 1} :span [0 duration]})
(defn document [] (defn document []
(let [a (occurrence :a :drawing-a 0 4 0) (let [a (cel :a :drawing-a 0 4 0)
b (assoc-in (occurrence :b :drawing-b 4 4 0) b (assoc-in (cel :b :drawing-b 4 4 0)
[:channels [:xform :pos]] (ch/keyed {0 [0 0] 1 [2 0]} :hold)) [:channels [:xform :pos]] (ch/keyed {0 [0 0] 1 [2 0]} :hold))
insert (assoc-in (occurrence :insert :wave 8 4 1) [:playback :in] 3)] insert (assoc-in (cel :insert :wave 8 4 1) [:playback :in] 3)]
{:name "exposures" :fps 24 :width 320 :height 200 {:name "cels" :fps 24 :width 320 :height 200
:symbols :symbols
{:main {:id :main :frames 12 {:main {:id :main :frames 12
:nodes {:girl {:id :girl :kind :group :layout :sequence :z "b" :nodes {:girl {:id :girl :kind :group :layout :sequence :z "b"
@ -58,9 +58,9 @@
(into {} (map (fn [[f ops]] [f (js/Math.round (:plate ops))])) at))) (into {} (map (fn [[f ops]] [f (js/Math.round (:plate ops))])) at)))
(is (nil? (get-in at [4 [:a :mark]])) "half-open cuts have a single owner"))) (is (nil? (get-in at [4 [:a :mark]])) "half-open cuts have a single owner")))
(deftest exposure-ripple-keeps-lane-keys-and-moves-occurrence-corrections (deftest cel-ripple-keeps-lane-keys-and-moves-cel-corrections
(let [doc (document) (let [doc (document)
result (sequence/extend-hold doc :main :a 2 {:extent :grow-symbol}) result (lane/extend-hold doc :main :a 2 {:extent :grow-symbol})
after (:clip result) after (:clip result)
nodes (get-in after [:symbols :main :nodes])] nodes (get-in after [:symbols :main :nodes])]
(is (= :a (:selection result))) (is (= :a (:selection result)))
@ -78,19 +78,19 @@
(is (= 320 (get-in at [10 [:insert :mark]])) "insert starts on source frame 3")) (is (= 320 (get-in at [10 [:insert :mark]])) "insert starts on source frame 3"))
(is (empty? (clip/problems after))) (is (empty? (clip/problems after)))
(is (= (assoc-in doc [:symbols :main :frames] 14) (is (= (assoc-in doc [:symbols :main :frames] 14)
(:clip (sequence/extend-hold after :main :a -2 {}))) (:clip (lane/extend-hold after :main :a -2 {})))
"shrinking restores content, except the explicitly grown shot"))) "shrinking restores content, except the explicitly grown shot")))
(deftest overflow-and-invalid-edits-are-atomic (deftest overflow-and-invalid-edits-are-atomic
(let [doc (document) (let [doc (document)
result (sequence/extend-hold doc :main :a 2 {})] result (lane/extend-hold doc :main :a 2 {})]
(is (:refused result)) (is (:refused result))
(is (= 14 (:required-frames result))) (is (= 14 (:required-frames result)))
(is (not (contains? result :clip))) (is (not (contains? result :clip)))
(doseq [delta [0 -4 0.5 js/NaN]] (doseq [delta [0 -4 0.5 js/NaN]]
(is (:refused (sequence/extend-hold doc :main :a delta {})))) (is (:refused (lane/extend-hold doc :main :a delta {}))))
(is (:refused (sequence/extend-hold doc :main :insert 1 {}))) (is (:refused (lane/extend-hold doc :main :insert 1 {})))
(is (:refused (sequence/extend-hold doc :main :missing 1 {}))))) (is (:refused (lane/extend-hold doc :main :missing 1 {})))))
(deftest a-gap-is-an-uncovered-interval (deftest a-gap-is-an-uncovered-interval
(let [doc (update-in (document) [:symbols :main :nodes] dissoc :b) (let [doc (update-in (document) [:symbols :main :nodes] dissoc :b)
@ -148,8 +148,8 @@
(is (= 99 (get-in (sample edited [0 4]) [0 [:a :mark]]))) (is (= 99 (get-in (sample edited [0 4]) [0 [:a :mark]])))
(is (= 139 (get-in (sample edited [0 4]) [4 [:b :mark]])))))) (is (= 139 (get-in (sample edited [0 4]) [4 [:b :mark]]))))))
(deftest occurrence-identities-and-playback-round-trip (deftest cel-identities-and-playback-round-trip
(let [doc (:clip (sequence/extend-hold (document) :main :a 2 {:extent :grow-symbol})) (let [doc (:clip (lane/extend-hold (document) :main :a 2 {:extent :grow-symbol}))
leaves (leaf/leaves :project doc)] leaves (leaf/leaves :project doc)]
(is (= doc (leaf/clip :project leaves))) (is (= doc (leaf/clip :project leaves)))
(is (contains? leaves "clip/project/symbol/main/node/a")) (is (contains? leaves "clip/project/symbol/main/node/a"))
@ -163,7 +163,7 @@
(deftest one-transaction-undoes-the-ripple-and-shot-extension (deftest one-transaction-undoes-the-ripple-and-shot-extension
(let [doc (document) (let [doc (document)
after (:clip (sequence/extend-hold doc :main :a 2 {:extent :grow-symbol})) after (:clip (lane/extend-hold doc :main :a 2 {:extent :grow-symbol}))
before-leaves (leaf/leaves :p doc) before-leaves (leaf/leaves :p doc)
after-leaves (leaf/leaves :p after) after-leaves (leaf/leaves :p after)
h (-> nil history/hold (history/record before-leaves after-leaves 0) history/settle) h (-> nil history/hold (history/record before-leaves after-leaves 0) history/settle)
@ -174,19 +174,19 @@
(is (= after-leaves (:leaves redo))))) (is (= after-leaves (:leaves redo)))))
(deftest create-lane-and-append-drawings (deftest create-lane-and-append-drawings
(let [doc (:clip (sequence/add-lane (clip/blank) :main :girl)) (let [doc (:clip (lane/add-lane (clip/blank) :main :girl))
a (:clip (sequence/append-drawing doc :main :girl :a :drawing-a {})) a (:clip (lane/append-drawing doc :main :girl :a :drawing-a {}))
b (:clip (sequence/append-drawing a :main :girl :b :drawing-b {}))] b (:clip (lane/append-drawing a :main :girl :b :drawing-b {}))]
(is (empty? (clip/problems b))) (is (empty? (clip/problems b)))
(is (= [1 2] (node/placed-span (get-in b [:symbols :main :nodes :b])))) (is (= [1 2] (node/placed-span (get-in b [:symbols :main :nodes :b]))))
(is (= 0 (get-in b [:symbols :main :nodes :b :playback :speed]))) (is (= 0 (get-in b [:symbols :main :nodes :b :playback :speed])))
(is (:refused (sequence/append-drawing b :main :girl :a :new {}))))) (is (:refused (lane/append-drawing b :main :girl :a :new {})))))
(deftest fractional-placement-rates-convert-the-hold-delta (deftest fractional-placement-rates-convert-the-hold-delta
(let [doc (-> (document) (let [doc (-> (document)
(assoc-in [:symbols :main :nodes :a :time :rate] 2) (assoc-in [:symbols :main :nodes :a :time :rate] 2)
(assoc-in [:symbols :main :nodes :a :span] [0 8])) (assoc-in [:symbols :main :nodes :a :span] [0 8]))
after (:clip (sequence/extend-hold doc :main :a 2 {:extent :grow-symbol}))] after (:clip (lane/extend-hold doc :main :a 2 {:extent :grow-symbol}))]
(is (= [0 12] (get-in after [:symbols :main :nodes :a :span]))) (is (= [0 12] (get-in after [:symbols :main :nodes :a :span])))
(is (= [6 10] (node/placed-span (get-in after [:symbols :main :nodes :b])))))) (is (= [6 10] (node/placed-span (get-in after [:symbols :main :nodes :b]))))))
@ -196,7 +196,7 @@
((symbol/resolver sym nil pal/index-of nil) 0)) ((symbol/resolver sym nil pal/index-of nil) 0))
"omitted style colour must not crash a missing cursor"))) "omitted style colour must not crash a missing cursor")))
(deftest audio-follows-only-the-playing-occurrence (deftest audio-follows-only-the-playing-cel
(let [voice {:id :voice :kind :audio :z "a" :source {:sound "voice"} (let [voice {:id :voice :kind :audio :z "a" :source {:sound "voice"}
:span [0 10] :span [0 10]
:channels {[:audio :gain] (ch/keyed {0 0 5 1} :linear)}} :channels {[:audio :gain] (ch/keyed {0 0 5 1} :linear)}}
@ -208,7 +208,7 @@
(is (= [8 12] (node/placed-span track))) (is (= [8 12] (node/placed-span track)))
(is (= [3 7] (:span track)) "the source in-point trims the audio too") (is (= [3 7] (:span track)) "the source in-point trims the audio too")
(is (= {5 0 10 1} (get-in track [:channels [:audio :gain] :keys]))) (is (= {5 0 10 1} (get-in track [:channels [:audio :gain] :keys])))
(let [moved (:clip (sequence/extend-hold doc :main :a 2 {:extent :grow-symbol})) (let [moved (:clip (lane/extend-hold doc :main :a 2 {:extent :grow-symbol}))
[track] (nest/audio-tracks moved :main)] [track] (nest/audio-tracks moved :main)]
(is (= [10 14] (node/placed-span track))) (is (= [10 14] (node/placed-span track)))
(is (= [3 7] (:span track)))) (is (= [3 7] (:span track))))
@ -231,56 +231,56 @@
(deftest enclosing-retiming-is-respected-when-extending-the-shot (deftest enclosing-retiming-is-respected-when-extending-the-shot
(let [doc (assoc-in (document) [:symbols :main :nodes :girl :time] {:at 8 :rate 2}) (let [doc (assoc-in (document) [:symbols :main :nodes :girl :time] {:at 8 :rate 2})
result (sequence/extend-hold doc :main :a 2 {})] result (lane/extend-hold doc :main :a 2 {})]
(is (= 15 (:required-frames result))) (is (= 15 (:required-frames result)))
(is (nil? (:clip result))) (is (nil? (:clip result)))
(is (= 15 (get-in (sequence/extend-hold doc :main :a 2 {:extent :grow-symbol}) (is (= 15 (get-in (lane/extend-hold doc :main :a 2 {:extent :grow-symbol})
[:clip :symbols :main :frames]))))) [:clip :symbols :main :frames])))))
(deftest reuse-shares-content-and-make-unique-decouples-one-exposure (deftest reuse-shares-content-and-make-unique-decouples-one-cel
(let [doc (document) (let [doc (document)
shared (:clip (sequence/reuse-drawing doc :main :girl :c :drawing-a shared (:clip (lane/reuse-drawing doc :main :girl :c :drawing-a
{:extent :grow-symbol})) {:extent :grow-symbol}))
edit (fn [c sym x] edit (fn [c sym x]
(assoc-in c [:symbols sym :nodes :mark :channels [:xform :pos]] (assoc-in c [:symbols sym :nodes :mark :channels [:xform :pos]]
(ch/framed [x 0])))] (ch/framed [x 0])))]
(is (:refused (sequence/reuse-drawing doc :main :girl :c :drawing-a {})) (is (:refused (lane/reuse-drawing doc :main :girl :c :drawing-a {}))
"the shot has to be extended on purpose") "the shot has to be extended on purpose")
(is (= :drawing-a (node/source (get-in shared [:symbols :main :nodes :c])))) (is (= :drawing-a (node/source (get-in shared [:symbols :main :nodes :c]))))
(is (= [12 13] (node/placed-span (get-in shared [:symbols :main :nodes :c])))) (is (= [12 13] (node/placed-span (get-in shared [:symbols :main :nodes :c]))))
(is (empty? (clip/problems shared))) (is (empty? (clip/problems shared)))
;; One drawing, two exposures: the edit arrives at both. ;; One drawing, two cels: the edit arrives at both.
(let [at (sample (edit shared :drawing-a 99) [0 12])] (let [at (sample (edit shared :drawing-a 99) [0 12])]
(is (= 99 (get-in at [0 [:a :mark]]))) (is (= 99 (get-in at [0 [:a :mark]])))
(is (= 99 (get-in at [12 [:c :mark]])))) (is (= 99 (get-in at [12 [:c :mark]]))))
(let [unique (:clip (sequence/make-unique shared :main :c {}))] (let [unique (:clip (lane/make-unique shared :main :c {}))]
(is (= :drawing-a-2 (node/source (get-in unique [:symbols :main :nodes :c])))) (is (= :drawing-a-2 (node/source (get-in unique [:symbols :main :nodes :c]))))
(is (= (:nodes (get-in shared [:symbols :drawing-a])) (is (= (:nodes (get-in shared [:symbols :drawing-a]))
(:nodes (get-in unique [:symbols :drawing-a-2]))) (:nodes (get-in unique [:symbols :drawing-a-2])))
"a copy of the same drawing, not an empty one") "a copy of the same drawing, not an empty one")
(is (= :drawing-a (node/source (get-in unique [:symbols :main :nodes :a]))) (is (= :drawing-a (node/source (get-in unique [:symbols :main :nodes :a])))
"the other exposure keeps the original") "the other cel keeps the original")
(let [at (sample (edit unique :drawing-a 99) [0 12])] (let [at (sample (edit unique :drawing-a 99) [0 12])]
(is (= 99 (get-in at [0 [:a :mark]]))) (is (= 99 (get-in at [0 [:a :mark]])))
(is (= 10 (get-in at [12 [:c :mark]])) "the exposure made unique is untouched")) (is (= 10 (get-in at [12 [:c :mark]])) "the cel made unique is untouched"))
(let [at (sample (edit unique :drawing-a-2 99) [0 12])] (let [at (sample (edit unique :drawing-a-2 99) [0 12])]
(is (= 10 (get-in at [0 [:a :mark]])) "and does not reach back")) (is (= 10 (get-in at [0 [:a :mark]])) "and does not reach back"))
(is (empty? (clip/problems unique)))) (is (empty? (clip/problems unique))))
;; Nothing else places drawing-b, so there is nothing to decouple from. ;; Nothing else places drawing-b, so there is nothing to decouple from.
(is (:refused (sequence/make-unique doc :main :b {}))) (is (:refused (lane/make-unique doc :main :b {})))
(is (:refused (sequence/make-unique doc :main :girl {})) (is (:refused (lane/make-unique doc :main :girl {}))
"a lane places nothing itself"))) "a lane places nothing itself")))
(deftest duplicate-copies-the-drawing-and-not-the-exposure (deftest duplicate-copies-the-drawing-and-not-the-cel
(let [doc (document) (let [doc (document)
made (:clip (sequence/duplicate-drawing doc :main :b :d {:extent :grow-symbol})) made (:clip (lane/duplicate-drawing doc :main :b :d {:extent :grow-symbol}))
n (get-in made [:symbols :main :nodes :d])] n (get-in made [:symbols :main :nodes :d])]
(is (= :drawing-b-2 (node/source n))) (is (= :drawing-b-2 (node/source n)))
(is (= (:nodes (get-in doc [:symbols :drawing-b])) (is (= (:nodes (get-in doc [:symbols :drawing-b]))
(:nodes (get-in made [:symbols :drawing-b-2])))) (:nodes (get-in made [:symbols :drawing-b-2]))))
(is (= [12 13] (node/placed-span n))) (is (= [12 13] (node/placed-span n)))
(is (= {:in 0 :speed 0 :end :stop} (:playback n))) (is (= {:in 0 :speed 0 :end :stop} (:playback n)))
(is (nil? (:channels n)) "B's own position correction belongs to B's exposure") (is (nil? (:channels n)) "B's own position correction belongs to B's cel")
(is (= (get-in doc [:symbols :main :nodes :b]) (is (= (get-in doc [:symbols :main :nodes :b])
(get-in made [:symbols :main :nodes :b])) (get-in made [:symbols :main :nodes :b]))
"the drawing duplicated is left as it was") "the drawing duplicated is left as it was")
@ -293,7 +293,7 @@
{:id :part :kind :instance :z "b" :span [0 1] {:id :part :kind :instance :z "b" :span [0 1]
:time {:at 0 :rate 1} :source {:symbol :wave} :time {:at 0 :rate 1} :source {:symbol :wave}
:playback {:in 0 :speed 0 :end :stop}}) :playback {:in 0 :speed 0 :end :stop}})
copy (fn [opts] (:clip (sequence/duplicate-drawing copy (fn [opts] (:clip (lane/duplicate-drawing
doc :main :a :d (merge {:extent :grow-symbol} opts)))) doc :main :a :d (merge {:extent :grow-symbol} opts))))
shallow (copy {}) shallow (copy {})
deep (copy {:deep? true})] deep (copy {:deep? true})]
@ -306,19 +306,19 @@
(deftest reuse-refuses-what-would-not-be-a-document (deftest reuse-refuses-what-would-not-be-a-document
(let [doc (document)] (let [doc (document)]
(is (:refused (sequence/reuse-drawing doc :main :girl :c :nothing-here {}))) (is (:refused (lane/reuse-drawing doc :main :girl :c :nothing-here {})))
(is (:refused (sequence/reuse-drawing doc :main :girl :c :main {:extent :grow-symbol})) (is (:refused (lane/reuse-drawing doc :main :girl :c :main {:extent :grow-symbol}))
"a symbol cannot go inside itself") "a symbol cannot go inside itself")
(is (:refused (sequence/reuse-drawing doc :main :girl :a :drawing-a {:extent :grow-symbol})) (is (:refused (lane/reuse-drawing doc :main :girl :a :drawing-a {:extent :grow-symbol}))
"an occurrence ID in use is not free") "a cel ID in use is not free")
(is (:refused (sequence/reuse-drawing doc :main :plate :c :drawing-a {}))) (is (:refused (lane/reuse-drawing doc :main :plate :c :drawing-a {})))
(is (:refused (sequence/duplicate-drawing doc :main :girl :d {}))))) (is (:refused (lane/duplicate-drawing doc :main :girl :d {})))))
(deftest drawing-on-twos-does-not-quantize-the-lane-transform (deftest drawing-on-twos-does-not-quantize-the-lane-transform
;; Exposure length IS the drawing cadence, and it is the only thing on twos ;; Cel length IS the drawing cadence, and it is the only thing on twos
;; here: the lane's transform has its own clock and keeps moving every frame. ;; here: the lane's transform has its own clock and keeps moving every frame.
;; Stepping it would be the cel cadence leaking into continuous motion. ;; Stepping it would be the cel cadence leaking into continuous motion.
(let [cel (fn [id source at] (occurrence id source at 2 0)) (let [cel (fn [id source at] (cel id source at 2 0))
doc (-> (document) doc (-> (document)
(update-in [:symbols :main :nodes] dissoc :a :b :insert) (update-in [:symbols :main :nodes] dissoc :a :b :insert)
(update-in [:symbols :main :nodes] merge (update-in [:symbols :main :nodes] merge
@ -337,7 +337,7 @@
(defn- drawn (defn- drawn
"What every frame draws, as sorted values, so a picture can be compared "What every frame draws, as sorted values, so a picture can be compared
without naming the occurrences that produced it." without naming the cels that produced it."
[doc fs] [doc fs]
(let [at (sample doc fs)] (let [at (sample doc fs)]
(mapv #(sort (vals (get at %))) fs))) (mapv #(sort (vals (get at %))) fs)))
@ -347,7 +347,7 @@
keys-of #(get-in % [:symbols :main :nodes :girl :channels [:xform :pos] :keys]) keys-of #(get-in % [:symbols :main :nodes :girl :channels [:xform :pos] :keys])
spans #(mapv (fn [id] (node/placed-span (get-in % [:symbols :main :nodes id]))) spans #(mapv (fn [id] (node/placed-span (get-in % [:symbols :main :nodes id])))
[:a :n :b :insert]) [:a :n :b :insert])
r (sequence/append-drawing doc :main :girl :n :drawing-n r (lane/append-drawing doc :main :girl :n :drawing-n
{:at 4 :extent :grow-symbol})] {:at 4 :extent :grow-symbol})]
(is (= [[0 4] [4 5] [5 9] [9 13]] (spans (:clip r)))) (is (= [[0 4] [4 5] [5 9] [9 13]] (spans (:clip r))))
(is (= 13 (get-in r [:clip :symbols :main :frames]))) (is (= 13 (get-in r [:clip :symbols :main :frames])))
@ -356,45 +356,45 @@
(is (= 4 (:frame r))) (is (= 4 (:frame r)))
(is (empty? (clip/problems (:clip r)))) (is (empty? (clip/problems (:clip r))))
;; The same command with no room refuses, and says how much it needs. ;; The same command with no room refuses, and says how much it needs.
(is (= 13 (:required-frames (sequence/append-drawing doc :main :girl :n :drawing-n {:at 4})))) (is (= 13 (:required-frames (lane/append-drawing doc :main :girl :n :drawing-n {:at 4}))))
;; At the very front everything moves. ;; At the very front everything moves.
(is (= [[1 5] [0 1] [5 9] [9 13]] (is (= [[1 5] [0 1] [5 9] [9 13]]
(spans (:clip (sequence/append-drawing doc :main :girl :n :drawing-n (spans (:clip (lane/append-drawing doc :main :girl :n :drawing-n
{:at 0 :extent :grow-symbol}))))) {:at 0 :extent :grow-symbol})))))
;; Inside an exposure is not a position for another one. ;; Inside a cel is not a position for another one.
(is (re-find #"split it first" (is (re-find #"split it first"
(:refused (sequence/append-drawing doc :main :girl :n :drawing-n (:refused (lane/append-drawing doc :main :girl :n :drawing-n
{:at 2 :extent :grow-symbol})))) {:at 2 :extent :grow-symbol}))))
(is (:refused (sequence/append-drawing doc :main :girl :n :drawing-n (is (:refused (lane/append-drawing doc :main :girl :n :drawing-n
{:at -1 :extent :grow-symbol}))) {:at -1 :extent :grow-symbol})))
(is (:refused (sequence/append-drawing doc :main :girl :n :drawing-n (is (:refused (lane/append-drawing doc :main :girl :n :drawing-n
{:at ##Inf :extent :grow-symbol}))) {:at ##Inf :extent :grow-symbol})))
;; Reuse and duplicate take a position too; it is one placement rule. ;; Reuse and duplicate take a position too; it is one placement rule.
(is (= [4 5] (node/placed-span (is (= [4 5] (node/placed-span
(get-in (sequence/reuse-drawing doc :main :girl :n :drawing-b (get-in (lane/reuse-drawing doc :main :girl :n :drawing-b
{:at 4 :extent :grow-symbol}) {:at 4 :extent :grow-symbol})
[:clip :symbols :main :nodes :n])))) [:clip :symbols :main :nodes :n]))))
(is (= [4 5] (node/placed-span (is (= [4 5] (node/placed-span
(get-in (sequence/duplicate-drawing doc :main :b :n (get-in (lane/duplicate-drawing doc :main :b :n
{:at 4 :extent :grow-symbol}) {:at 4 :extent :grow-symbol})
[:clip :symbols :main :nodes :n])))))) [:clip :symbols :main :nodes :n]))))))
(deftest splitting-an-exposure-changes-nothing-that-is-drawn (deftest splitting-an-cel-changes-nothing-that-is-drawn
(let [doc (document) (let [doc (document)
fs (range 12) fs (range 12)
before (drawn doc fs)] before (drawn doc fs)]
(doseq [[label id cut] [["a held drawing" :a 2] (doseq [[label id cut] [["a held drawing" :a 2]
["an exposure with a correction of its own" :b 6] ["a cel with a correction of its own" :b 6]
["a playing insert" :insert 10]]] ["a playing insert" :insert 10]]]
(testing label (testing label
(let [r (sequence/split doc :main id cut :right) (let [r (lane/split doc :main id cut :right)
after (:clip r)] after (:clip r)]
(is (= :right (:selection r))) (is (= :right (:selection r)))
(is (= before (drawn after fs)) "the same picture, frame for frame") (is (= before (drawn after fs)) "the same picture, frame for frame")
(is (= (node/placed-span (get-in doc [:symbols :main :nodes id])) (is (= (node/placed-span (get-in doc [:symbols :main :nodes id]))
[(first (node/placed-span (get-in after [:symbols :main :nodes id]))) [(first (node/placed-span (get-in after [:symbols :main :nodes id])))
(second (node/placed-span (get-in after [:symbols :main :nodes :right])))]) (second (node/placed-span (get-in after [:symbols :main :nodes :right])))])
"the pieces occupy the frames the exposure did") "the pieces occupy the frames the cel did")
(is (= cut (second (node/placed-span (get-in after [:symbols :main :nodes id]))) (is (= cut (second (node/placed-span (get-in after [:symbols :main :nodes id])))
(first (node/placed-span (get-in after [:symbols :main :nodes :right]))))) (first (node/placed-span (get-in after [:symbols :main :nodes :right])))))
(is (= (:time (get-in doc [:symbols :main :nodes id])) (is (= (:time (get-in doc [:symbols :main :nodes id]))
@ -405,19 +405,19 @@
(is (= 12 (get-in after [:symbols :main :frames])) "and no shot-length question") (is (= 12 (get-in after [:symbols :main :frames])) "and no shot-length question")
(is (empty? (clip/problems after)))))))) (is (empty? (clip/problems after))))))))
(deftest split-refuses-anything-that-is-not-one-cut-inside-one-exposure (deftest split-refuses-anything-that-is-not-one-cut-inside-one-cel
(let [doc (document)] (let [doc (document)]
(doseq [cut [0 4 8 12 -1 2.5 ##NaN nil]] (doseq [cut [0 4 8 12 -1 2.5 ##NaN nil]]
(is (:refused (sequence/split doc :main :b cut :right)) (str "cut at " (pr-str cut)))) (is (:refused (lane/split doc :main :b cut :right)) (str "cut at " (pr-str cut))))
(is (:refused (sequence/split doc :main :girl 2 :right)) "a lane is not an exposure") (is (:refused (lane/split doc :main :girl 2 :right)) "a lane is not a cel")
(is (:refused (sequence/split doc :main :plate 2 :right)) "nor is a shape outside one") (is (:refused (lane/split doc :main :plate 2 :right)) "nor is a shape outside one")
(is (:refused (sequence/split doc :main :a 2 :b)) "the new ID has to be free"))) (is (:refused (lane/split doc :main :a 2 :b)) "the new ID has to be free")))
(deftest split-then-place-puts-a-drawing-inside-a-hold (deftest split-then-place-puts-a-drawing-inside-a-hold
;; The two commands the doc asks for, composed: neither one guesses. ;; The two commands the doc asks for, composed: neither one guesses.
(let [doc (document) (let [doc (document)
cut (:clip (sequence/split doc :main :a 2 :right)) cut (:clip (lane/split doc :main :a 2 :right))
r (sequence/append-drawing cut :main :girl :n :drawing-n r (lane/append-drawing cut :main :girl :n :drawing-n
{:at 2 :extent :grow-symbol}) {:at 2 :extent :grow-symbol})
after (:clip r)] after (:clip r)]
(is (= [[0 2] [2 3] [3 5] [5 9] [9 13]] (is (= [[0 2] [2 3] [3 5] [5 9] [9 13]]
@ -459,14 +459,14 @@
(is (= b (:leaves (history/undo h a)))) (is (= b (:leaves (history/undo h a))))
(is (= c (leaf/clip :p a)) "a correction needs no codec of its own")))) (is (= c (leaf/clip :p a)) "a correction needs no codec of its own"))))
(deftest a-correction-on-one-exposure-travels-with-it (deftest a-correction-on-one-cel-travels-with-it
;; The other half of ownership: a layer on an occurrence is in that ;; The other half of ownership: a layer on a cel is in that
;; occurrence's own frames, so moving the exposure moves the correction and ;; cel's own frames, so moving the cel moves the correction and
;; nothing has to say so. ;; nothing has to say so.
(let [beat (ch/layer :beat [0 2] :offset (ch/framed [7 0])) (let [beat (ch/layer :beat [0 2] :offset (ch/framed [7 0]))
doc (update-in (document) [:symbols :main :nodes :b :channels [:xform :pos] :over] doc (update-in (document) [:symbols :main :nodes :b :channels [:xform :pos] :over]
(fnil conj []) beat) (fnil conj []) beat)
moved (:clip (sequence/extend-hold doc :main :a 2 {:extent :grow-symbol}))] moved (:clip (lane/extend-hold doc :main :a 2 {:extent :grow-symbol}))]
;; Stated as the difference from the same document without the correction, ;; Stated as the difference from the same document without the correction,
;; so the claim is about WHERE the layer applies and not about arithmetic. ;; so the claim is about WHERE the layer applies and not about arithmetic.
(let [nudge (fn [with without f] (let [nudge (fn [with without f]
@ -475,7 +475,7 @@
(is (= [7 7 0 0] (mapv #(nudge doc (document) %) [4 5 6 7])) (is (= [7 7 0 0] (mapv #(nudge doc (document) %) [4 5 6 7]))
"B's first two frames, which are lane frames 4 and 5") "B's first two frames, which are lane frames 4 and 5")
(is (= [7 7 0 0] (is (= [7 7 0 0]
(mapv #(nudge moved (:clip (sequence/extend-hold (document) :main :a 2 (mapv #(nudge moved (:clip (lane/extend-hold (document) :main :a 2
{:extent :grow-symbol})) {:extent :grow-symbol}))
%) %)
[6 7 8 9])) [6 7 8 9]))
@ -488,9 +488,9 @@
(defn- spans [clip ids] (defn- spans [clip ids]
(mapv #(node/placed-span (get-in clip [:symbols :main :nodes %])) ids)) (mapv #(node/placed-span (get-in clip [:symbols :main :nodes %])) ids))
(deftest trimming-narrows-one-exposure-and-moves-nothing-else (deftest trimming-narrows-one-cel-and-moves-nothing-else
(let [doc (document) (let [doc (document)
r (sequence/trim doc :main :b :out 6) r (lane/trim doc :main :b :out 6)
after (:clip r)] after (:clip r)]
(is (= [[0 4] [4 6] [8 12]] (spans after [:a :b :insert]))) (is (= [[0 4] [4 6] [8 12]] (spans after [:a :b :insert])))
(is (= :b (:selection r))) (is (= :b (:selection r)))
@ -505,7 +505,7 @@
;; were, so the frames that survive show exactly what they showed. ;; were, so the frames that survive show exactly what they showed.
(let [doc (document) (let [doc (document)
before (sample doc [10 11]) before (sample doc [10 11])
after (:clip (sequence/trim doc :main :insert :in 10))] after (:clip (lane/trim doc :main :insert :in 10))]
(is (= [10 12] (node/placed-span (get-in after [:symbols :main :nodes :insert])))) (is (= [10 12] (node/placed-span (get-in after [:symbols :main :nodes :insert]))))
(is (= (:playback (get-in doc [:symbols :main :nodes :insert])) (is (= (:playback (get-in doc [:symbols :main :nodes :insert]))
(:playback (get-in after [:symbols :main :nodes :insert])))) (:playback (get-in after [:symbols :main :nodes :insert]))))
@ -520,13 +520,13 @@
["past its end" :out 9] ["past its end" :out 9]
["before its start" :in 2] ["before its start" :in 2]
["off a whole frame" :out 5.5]]] ["off a whole frame" :out 5.5]]]
(is (:refused (sequence/trim doc :main :b edge to)) label)) (is (:refused (lane/trim doc :main :b edge to)) label))
(is (:refused (sequence/trim doc :main :b :middle 6))) (is (:refused (lane/trim doc :main :b :middle 6)))
(is (:refused (sequence/trim doc :main :girl :out 6)) "a lane is not an exposure"))) (is (:refused (lane/trim doc :main :girl :out 6)) "a lane is not a cel")))
(deftest moving-an-exposure-keeps-its-length-and-its-source-origin (deftest moving-an-cel-keeps-its-length-and-its-source-origin
(let [doc (update-in (document) [:symbols :main :nodes] dissoc :b) (let [doc (update-in (document) [:symbols :main :nodes] dissoc :b)
r (sequence/move doc :main :insert 4) r (lane/move doc :main :insert 4)
after (:clip r)] after (:clip r)]
(is (= [[0 4] [4 8]] (spans after [:a :insert]))) (is (= [[0 4] [4 8]] (spans after [:a :insert])))
(is (= :insert (:selection r))) (is (= :insert (:selection r)))
@ -539,19 +539,19 @@
(deftest a-move-onto-an-occupied-frame-is-refused-rather-than-rippled (deftest a-move-onto-an-occupied-frame-is-refused-rather-than-rippled
(let [doc (document)] (let [doc (document)]
(is (:refused (sequence/move doc :main :insert 6)) "it would overlap B") (is (:refused (lane/move doc :main :insert 6)) "it would overlap B")
(is (:refused (sequence/move doc :main :insert 4.5))) (is (:refused (lane/move doc :main :insert 4.5)))
(is (:refused (sequence/move doc :main :girl 2))) (is (:refused (lane/move doc :main :girl 2)))
;; Clearing the room first is the composition, and then it goes. ;; Clearing the room first is the composition, and then it goes.
(let [cleared (:clip (sequence/blank doc :main :girl [4 8] {}))] (let [cleared (:clip (lane/blank doc :main :girl [4 8] {}))]
(is (= [[0 4] [4 8]] (spans (:clip (sequence/move cleared :main :insert 4)) (is (= [[0 4] [4 8]] (spans (:clip (lane/move cleared :main :insert 4))
[:a :insert])))))) [:a :insert]))))))
(deftest blanking-leaves-a-gap-and-does-not-close-it (deftest blanking-leaves-a-gap-and-does-not-close-it
(let [doc (document) (let [doc (document)
r (sequence/blank doc :main :girl [5 7] {:id :rest}) r (lane/blank doc :main :girl [5 7] {:id :rest})
after (:clip r)] after (:clip r)]
;; B spanned the range, so it became two exposures with a hole between them. ;; B spanned the range, so it became two cels with a hole between them.
(is (= [[0 4] [4 5] [7 8] [8 12]] (spans after [:a :b :rest :insert]))) (is (= [[0 4] [4 5] [7 8] [8 12]] (spans after [:a :b :rest :insert])))
(is (= :rest (:selection r))) (is (= :rest (:selection r)))
(let [at (sample after [4 5 6 7])] (let [at (sample after [4 5 6 7])]
@ -562,9 +562,9 @@
(is (= 12 (get-in after [:symbols :main :frames]))) (is (= 12 (get-in after [:symbols :main :frames])))
(is (empty? (clip/problems after))))) (is (empty? (clip/problems after)))))
(deftest blanking-a-whole-exposure-removes-it-and-keeps-its-drawing (deftest blanking-a-whole-cel-removes-it-and-keeps-its-drawing
(let [doc (document) (let [doc (document)
after (:clip (sequence/blank doc :main :girl [4 8] {}))] after (:clip (lane/blank doc :main :girl [4 8] {}))]
(is (nil? (get-in after [:symbols :main :nodes :b]))) (is (nil? (get-in after [:symbols :main :nodes :b])))
(is (= [[0 4] [8 12]] (spans after [:a :insert])) "and moves nothing") (is (= [[0 4] [8 12]] (spans after [:a :insert])) "and moves nothing")
(is (= (get-in doc [:symbols :drawing-b]) (get-in after [:symbols :drawing-b])) (is (= (get-in doc [:symbols :drawing-b]) (get-in after [:symbols :drawing-b]))
@ -573,7 +573,7 @@
(deftest blanking-a-range-trims-what-it-only-partly-covers (deftest blanking-a-range-trims-what-it-only-partly-covers
(let [doc (document) (let [doc (document)
after (:clip (sequence/blank doc :main :girl [3 9] {}))] after (:clip (lane/blank doc :main :girl [3 9] {}))]
(is (= [[0 3] [9 12]] (spans after [:a :insert]))) (is (= [[0 3] [9 12]] (spans after [:a :insert])))
(is (nil? (get-in after [:symbols :main :nodes :b]))) (is (nil? (get-in after [:symbols :main :nodes :b])))
(is (= (get-in (sample doc [9]) [9 [:insert :mark]]) (is (= (get-in (sample doc [9]) [9 [:insert :mark]])
@ -583,28 +583,28 @@
(deftest blank-refuses-what-it-cannot-do-in-one-piece (deftest blank-refuses-what-it-cannot-do-in-one-piece
(let [doc (document)] (let [doc (document)]
(is (re-find #"free ID" (:refused (sequence/blank doc :main :girl [5 7] {}))) (is (re-find #"free ID" (:refused (lane/blank doc :main :girl [5 7] {})))
"splitting an exposure needs an ID for the remainder") "splitting a cel needs an ID for the remainder")
(is (:refused (sequence/blank doc :main :girl [5 7] {:id :a})) "and a free one") (is (:refused (lane/blank doc :main :girl [5 7] {:id :a})) "and a free one")
(is (:refused (sequence/blank doc :main :girl [7 5] {}))) (is (:refused (lane/blank doc :main :girl [7 5] {})))
(is (:refused (sequence/blank doc :main :girl [5 5] {}))) (is (:refused (lane/blank doc :main :girl [5 5] {})))
(is (:refused (sequence/blank doc :main :girl [5 6.5] {}))) (is (:refused (lane/blank doc :main :girl [5 6.5] {})))
(is (:refused (sequence/blank doc :main :plate [0 2] {}))))) (is (:refused (lane/blank doc :main :plate [0 2] {})))))
(deftest the-shot-length-is-authored-and-emptying-a-lane-does-not-shorten-it (deftest the-shot-length-is-authored-and-emptying-a-lane-does-not-shorten-it
;; The window and the occupied extent are two facts. A shot with nothing in ;; The window and the occupied extent are two facts. A shot with nothing in
;; the last half is a shot somebody authored that long, and deleting the last ;; the last half is a shot somebody authored that long, and deleting the last
;; drawing must not quietly shorten the film. ;; drawing must not quietly shorten the film.
(let [doc (document) (let [doc (document)
empty-lane (:clip (sequence/blank doc :main :girl [0 12] {}))] empty-lane (:clip (lane/blank doc :main :girl [0 12] {}))]
(is (empty? (symbol/sequence-members (get-in empty-lane [:symbols :main :nodes]) :girl))) (is (empty? (symbol/lane-cels (get-in empty-lane [:symbols :main :nodes]) :girl)))
(is (= 12 (get-in empty-lane [:symbols :main :frames]))) (is (= 12 (get-in empty-lane [:symbols :main :frames])))
(is (empty? (clip/problems empty-lane))) (is (empty? (clip/problems empty-lane)))
;; Growing is still the caller's word, and only ever grows. ;; Growing is still the caller's word, and only ever grows.
(is (:refused (sequence/append-drawing empty-lane :main :girl :n :drawing-n {:at 20}))) (is (:refused (lane/append-drawing empty-lane :main :girl :n :drawing-n {:at 20})))
(is (= 21 (get-in (sequence/append-drawing empty-lane :main :girl :n :drawing-n (is (= 21 (get-in (lane/append-drawing empty-lane :main :girl :n :drawing-n
{:at 20 :extent :grow-symbol}) {:at 20 :extent :grow-symbol})
[:clip :symbols :main :frames]))) [:clip :symbols :main :frames])))
(is (= 12 (get-in (:clip (sequence/trim doc :main :insert :out 9)) (is (= 12 (get-in (:clip (lane/trim doc :main :insert :out 9))
[:symbols :main :frames])) [:symbols :main :frames]))
"and trimming the last exposure leaves the window where it was"))) "and trimming the last cel leaves the window where it was")))

View file

@ -1,14 +1,14 @@
(ns arthur.events.sequence-test (ns arthur.events.lane-test
(:require [cljs.test :refer [deftest is]] (:require [cljs.test :refer [deftest is]]
[arthur.domain.lane-test :as fixture] [arthur.domain.lane-test :as fixture]
[arthur.domain.sequence :as sequence] [arthur.domain.lane :as lane]
[arthur.events.ui :as ui] [arthur.events.ui :as ui]
[arthur.domain.history :as history] [arthur.domain.history :as history]
[arthur.domain.leaf :as leaf] [arthur.domain.leaf :as leaf]
[arthur.footage.store :as store] [arthur.footage.store :as store]
[arthur.ui.timeline :as timeline])) [arthur.ui.timeline :as timeline]))
(deftest one-row-projects-all-occurrences-and-keeps-selection-addresses (deftest one-row-projects-all-cels-and-keeps-selection-addresses
(let [doc (fixture/document) (let [doc (fixture/document)
rows (timeline/rows doc :main #{}) rows (timeline/rows doc :main #{})
lane (first (filter :cels rows))] lane (first (filter :cels rows))]
@ -24,15 +24,15 @@
id (store/install! {:clip doc :store {}} "sequence-test") id (store/install! {:clip doc :store {}} "sequence-test")
db {:clip/current id :paint/revision 0 db {:clip/current id :paint/revision 0
:ui {:open :main :selection [:node :main :a [:a]]}} :ui {:open :main :selection [:node :main :a [:a]]}}
refused (ui/apply-sequence-command db :main refused (ui/apply-lane-command db :main
(sequence/extend-hold doc :main :a 1 {}) [:retry])] (lane/extend-hold doc :main :a 1 {}) [:retry])]
(is (= doc (:clip (store/entry id)))) (is (= doc (:clip (store/entry id))))
(is (nil? (:history (store/entry id)))) (is (nil? (:history (store/entry id))))
(is (= [:retry] (get-in refused [:ui :sequence-retry]))) (is (= [:retry] (get-in refused [:ui :lane-retry])))
(let [r1 (sequence/extend-hold doc :main :a 1 {:extent :grow-symbol}) (let [r1 (lane/extend-hold doc :main :a 1 {:extent :grow-symbol})
db1 (ui/apply-sequence-command db :main r1 nil) db1 (ui/apply-lane-command db :main r1 nil)
r2 (sequence/extend-hold (:clip r1) :main :a 1 {:extent :grow-symbol}) r2 (lane/extend-hold (:clip r1) :main :a 1 {:extent :grow-symbol})
db2 (ui/apply-sequence-command db1 :main r2 nil) db2 (ui/apply-lane-command db1 :main r2 nil)
h (:history (store/entry id)) h (:history (store/entry id))
undo (history/undo h (leaf/leaves "u" (:clip r2))) undo (history/undo h (leaf/leaves "u" (:clip r2)))
undo2 (history/undo (:history undo) (:leaves undo))] undo2 (history/undo (:history undo) (:leaves undo))]

View file

@ -59,7 +59,7 @@ try {
await evaluate(`(() => { await evaluate(`(() => {
const k = cljs.core.keyword; const k = cljs.core.keyword;
cljs.core.swap_BANG_(re_frame.db.app_db, db => cljs.core.assoc(db, k('route'), k('local-test'))); cljs.core.swap_BANG_(re_frame.db.app_db, db => cljs.core.assoc(db, k('route'), k('local-test')));
window.sequenceSnapshot = () => { window.laneSnapshot = () => {
const db = cljs.core.deref(re_frame.db.app_db); const db = cljs.core.deref(re_frame.db.app_db);
const entry = arthur.footage.store.entry(cljs.core.get(db, k('clip/current'))); const entry = arthur.footage.store.entry(cljs.core.get(db, k('clip/current')));
return cljs.core.clj__GT_js(entry); return cljs.core.clj__GT_js(entry);
@ -75,7 +75,7 @@ try {
})()`), `enabled button: ${label}`); })()`), `enabled button: ${label}`);
await sleep(180); await sleep(180);
}; };
const shot = async () => (await evaluate('sequenceSnapshot()')); const shot = async () => (await evaluate('laneSnapshot()'));
const instances = s => Object.values(s.clip.symbols.main.nodes).filter(n => n.kind === 'instance') const instances = s => Object.values(s.clip.symbols.main.nodes).filter(n => n.kind === 'instance')
.sort((a, b) => a.time.at - b.time.at); .sort((a, b) => a.time.at - b.time.at);
await click('+ lane'); await click('+ lane');
@ -90,7 +90,7 @@ try {
assert.equal(await evaluate('document.querySelectorAll(".tl-label:not(.tl-corner)").length'), 1); assert.equal(await evaluate('document.querySelectorAll(".tl-label:not(.tl-corner)").length'), 1);
assert.equal(await evaluate(`cljs.core.get_in(cljs.core.deref(re_frame.db.app_db), assert.equal(await evaluate(`cljs.core.get_in(cljs.core.deref(re_frame.db.app_db),
cljs.core.vector(cljs.core.keyword('playback'), cljs.core.keyword('frame')))`), 4, cljs.core.vector(cljs.core.keyword('playback'), cljs.core.keyword('frame')))`), 4,
'new drawing seeks to its occurrence'); 'new drawing seeks to its cel');
// Shorten this test shot to the occupied extent, purely in memory. // Shorten this test shot to the occupied extent, purely in memory.
await evaluate(`(() => { await evaluate(`(() => {
@ -112,9 +112,9 @@ try {
assert.equal(s.history.done.length, before.history.done.length + 1); assert.equal(s.history.done.length, before.history.done.length + 1);
await evaluate(`document.dispatchEvent(new KeyboardEvent('keydown', {key:'z', ctrlKey:true, bubbles:true}))`); await evaluate(`document.dispatchEvent(new KeyboardEvent('keydown', {key:'z', ctrlKey:true, bubbles:true}))`);
await sleep(250); await sleep(250);
assert.deepEqual((await shot()).clip, before.clip, 'one undo restores exposure, ripple, and shot length'); assert.deepEqual((await shot()).clip, before.clip, 'one undo restores cel, ripple, and shot length');
// Sharing: one drawing exposed twice, then one exposure decoupled. Room is // Sharing: one drawing exposed twice, then one cel decoupled. Room is
// made first so these assertions are about content and not about overflow. // made first so these assertions are about content and not about overflow.
await evaluate(`(() => { await evaluate(`(() => {
const k = cljs.core.keyword, db = cljs.core.deref(re_frame.db.app_db); const k = cljs.core.keyword, db = cljs.core.deref(re_frame.db.app_db);
@ -135,19 +135,19 @@ try {
assert.equal(cels[2].source.symbol, cels[0].source.symbol, 'reuse exposes the same drawing'); assert.equal(cels[2].source.symbol, cels[0].source.symbol, 'reuse exposes the same drawing');
assert.equal(await evaluate('document.querySelectorAll(".tl-cel").length'), 3); assert.equal(await evaluate('document.querySelectorAll(".tl-cel").length'), 3);
assert.equal(await evaluate('document.querySelectorAll(".tl-label:not(.tl-corner)").length'), 1, assert.equal(await evaluate('document.querySelectorAll(".tl-label:not(.tl-corner)").length'), 1,
'three exposures, still one row'); 'three cels, still one row');
assert.equal(await enabled('make unique'), true); assert.equal(await enabled('make unique'), true);
await click('make unique'); await click('make unique');
s = await shot(); s = await shot();
cels = instances(s); cels = instances(s);
assert.notEqual(cels[2].source.symbol, cels[0].source.symbol, 'that exposure has its own drawing'); assert.notEqual(cels[2].source.symbol, cels[0].source.symbol, 'that cel has its own drawing');
assert.equal(await enabled('make unique'), false, 'and is not shared any more'); assert.equal(await enabled('make unique'), false, 'and is not shared any more');
await click('duplicate'); await click('duplicate');
s = await shot(); s = await shot();
cels = instances(s); cels = instances(s);
assert.equal(cels.length, 4); assert.equal(cels.length, 4);
assert.equal(new Set(cels.map(n => n.source.symbol)).size, 4, assert.equal(new Set(cels.map(n => n.source.symbol)).size, 4,
'four exposures of four drawings: nothing is shared once every copy is made'); 'four cels of four drawings: nothing is shared once every copy is made');
assert.equal(s.history.done.length, before.history.done.length + 3, 'three more commands, three more steps'); assert.equal(s.history.done.length, before.history.done.length + 3, 'three more commands, three more steps');
// A drawing into the middle of a hold: split, then insert. Both act at the // A drawing into the middle of a hold: split, then insert. Both act at the
@ -158,25 +158,25 @@ try {
assert.deepEqual(placed(s), [[0, 4], [4, 5], [5, 6], [6, 7]]); assert.deepEqual(placed(s), [[0, 4], [4, 5], [5, 6], [6, 7]]);
await evaluate(`document.querySelector('.tl-cel').click()`); await evaluate(`document.querySelector('.tl-cel').click()`);
await sleep(200); await sleep(200);
assert.equal(await enabled('split'), false, 'the start of an exposure is not inside it'); assert.equal(await enabled('split'), false, 'the start of a cel is not inside it');
await click('+1'); await click('+1');
await click('+1'); await click('+1');
assert.equal(await enabled('split'), true); assert.equal(await enabled('split'), true);
await click('split'); await click('split');
s = await shot(); s = await shot();
assert.deepEqual(placed(s), [[0, 2], [2, 4], [4, 5], [5, 6], [6, 7]], assert.deepEqual(placed(s), [[0, 2], [2, 4], [4, 5], [5, 6], [6, 7]],
'one exposure became two, over the frames it had'); 'one cel became two, over the frames it had');
await click('insert'); await click('insert');
s = await shot(); s = await shot();
assert.deepEqual(placed(s), [[0, 2], [2, 3], [3, 5], [5, 6], [6, 7], [7, 8]], assert.deepEqual(placed(s), [[0, 2], [2, 3], [3, 5], [5, 6], [6, 7], [7, 8]],
'the new drawing took frame 2 and everything from there rippled later'); 'the new drawing took frame 2 and everything from there rippled later');
assert.equal(await evaluate('document.querySelectorAll(".tl-cel").length'), 6); assert.equal(await evaluate('document.querySelectorAll(".tl-cel").length'), 6);
assert.equal(await evaluate('document.querySelectorAll(".tl-label:not(.tl-corner)").length'), 1, assert.equal(await evaluate('document.querySelectorAll(".tl-label:not(.tl-corner)").length'), 1,
'six exposures, still one row'); 'six cels, still one row');
assert.equal(s.history.done.length, before.history.done.length + 5); assert.equal(s.history.done.length, before.history.done.length + 5);
// Trim, move and blank: three gestures that move nothing but their own // Trim, move and blank: three gestures that move nothing but their own
// exposure, and a shot whose length does not follow what is in it. // cel, and a shot whose length does not follow what is in it.
await evaluate(`[...document.querySelectorAll('.tl-cel')][2].click()`); await evaluate(`[...document.querySelectorAll('.tl-cel')][2].click()`);
await sleep(200); await sleep(200);
assert.deepEqual(placed(await shot()).slice(2, 4), [[3, 5], [5, 6]]); assert.deepEqual(placed(await shot()).slice(2, 4), [[3, 5], [5, 6]]);
@ -185,7 +185,7 @@ try {
await click('trim out'); await click('trim out');
s = await shot(); s = await shot();
assert.deepEqual(placed(s), [[0, 2], [2, 3], [3, 4], [5, 6], [6, 7], [7, 8]], assert.deepEqual(placed(s), [[0, 2], [2, 3], [3, 4], [5, 6], [6, 7], [7, 8]],
'it ends at the playhead and every other exposure stayed'); 'it ends at the playhead and every other cel stayed');
assert.equal(await enabled('move here'), true); assert.equal(await enabled('move here'), true);
await click('move here'); await click('move here');
s = await shot(); s = await shot();