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:
parent
76106d36ee
commit
598c186c4f
21 changed files with 427 additions and 419 deletions
|
|
@ -822,7 +822,7 @@ collaborator's keying. The fix is addressing, not an algorithm:
|
|||
|
||||
```
|
||||
palette
|
||||
sequence/:sid
|
||||
lane/:sid
|
||||
clip/:cid/timing clip rate
|
||||
clip/:cid/subject/:sid tracked subject and settings
|
||||
clip/:cid/feature/:fid tracked feature and settings
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# The Lane Model
|
||||
|
||||
Revised 2026-09-30. Target design. Occurrence ownership, source playback, the
|
||||
content and exposure commands, placement anywhere in a lane, a one-row cel strip
|
||||
Revised 2026-09-30. Target design. Cel ownership, source playback, the
|
||||
content and cel commands, placement anywhere in a lane, a one-row cel strip
|
||||
and the correction-layer evaluator are implemented; the commands that produce a
|
||||
correction, overwrite, the retiming commands and the remaining views are not.
|
||||
See the status note under
|
||||
|
|
@ -21,7 +21,7 @@ and conflicting proposals in `animation-model.md`, `timing-model.md`, and
|
|||
## Goal and compatibility policy
|
||||
|
||||
Arthur is one animation document with several ways to see and edit it: drawing
|
||||
on the stage, arranging clips, timing exposures, editing curves, and generating
|
||||
on the stage, arranging clips, timing cels, editing curves, and generating
|
||||
motion from footage. Each view exposes relevant facts and invokes shared editing
|
||||
operations. Switching views must preserve the meaning of the work.
|
||||
|
||||
|
|
@ -48,61 +48,61 @@ rules. Minimizing field count is secondary to representing independent choices.
|
|||
export, and optimized cursors must agree with it.
|
||||
- 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:
|
||||
|
||||
| Concept | Owns | Example |
|
||||
| --- | --- | --- |
|
||||
| Content | Reusable nodes and their animation | Drawing `a2`, an animated head, or a sound asset |
|
||||
| Occurrence | One use of content, its interval, source playback, and local treatment | `a2` exposed on frames 12–16 |
|
||||
| Lane | A sequence of occurrences and shared properties | The girl's drawings and the girl's overall transform |
|
||||
| View row or column | Presentation and editor state | Timeline row, exposure-sheet column, or property curve |
|
||||
| Cel | One use of content, its interval, source playback, and local treatment | `a2` exposed on frames 12–16 |
|
||||
| Lane | A sequence of cels and shared properties | The girl's drawings and the girl's overall transform |
|
||||
| View row or column | Presentation and editor state | Timeline row, cel-sheet column, or property curve |
|
||||
|
||||
Use the existing instance/node identity mechanism for 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.
|
||||
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
|
||||
boundaries are part of the design. It is now the implemented shape, and the field
|
||||
spellings below are the ones the runtime reads.
|
||||
|
||||
Each occurrence has a stable ID. Moving it, changing its hold, swapping its source,
|
||||
or trimming it preserves that ID. Repeating it creates a new occurrence that may
|
||||
Each cel has a stable ID. Moving it, changing its hold, swapping its source,
|
||||
or trimming it preserves that ID. Repeating it creates a new cel that may
|
||||
reference the same content. A split retains the original ID on the left and gives
|
||||
the right piece a new ID; commands return the resulting selection explicitly.
|
||||
|
||||
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
|
||||
editable copy of the schedule. This replaces the earlier proposal that every cel
|
||||
must be represented solely as a source key. Ordinary property animation still
|
||||
uses lightweight keys; it does not need occurrence objects.
|
||||
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.
|
||||
Compositing and simultaneous sounds are represented by multiple lanes or ordinary
|
||||
scene composition; an accidental overlap never silently selects a winner.
|
||||
Transitions, if added, need explicit overlap and mixing semantics.
|
||||
|
||||
Properties can belong to content, one occurrence, or the lane. For example:
|
||||
Properties can belong to content, one cel, or the lane. For example:
|
||||
|
||||
- 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.
|
||||
|
||||
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
|
||||
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
|
||||
flat node map. The sequence constraint is document semantics; which rows the UI
|
||||
expands remains editor state. Ordinary groups retain unconstrained composition.
|
||||
|
||||
This example is a document the runtime accepts, built and evaluated by
|
||||
`frontend/test/arthur/domain/lane_test.cljs`. Times here are zero-based. Channels
|
||||
use the existing representation; occurrence source references and playback have
|
||||
use the existing representation; cel source references and playback have
|
||||
replaced the `[:source]` channel, which no longer exists.
|
||||
|
||||
```clojure
|
||||
|
|
@ -112,14 +112,14 @@ replaced the `[:source]` channel, which no longer exists.
|
|||
:channels {[:xform :rot]
|
||||
{:animated? true :interp :linear :keys {0 0, 6 30, 12 0}}}}
|
||||
|
||||
:exposure-a
|
||||
{:id :exposure-a :kind :instance :parent :girl :z "a"
|
||||
:cel-a
|
||||
{:id :cel-a :kind :instance :parent :girl :z "a"
|
||||
:time {:at 0 :rate 1} :span [0 4]
|
||||
:source {:symbol :drawing-a}
|
||||
:playback {:in 0 :speed 0 :end :stop}}
|
||||
|
||||
:exposure-b
|
||||
{:id :exposure-b :kind :instance :parent :girl :z "b"
|
||||
:cel-b
|
||||
{:id :cel-b :kind :instance :parent :girl :z "b"
|
||||
:time {:at 4 :rate 1} :span [0 4]
|
||||
:source {:symbol :drawing-b}
|
||||
:playback {:in 0 :speed 0 :end :stop}
|
||||
|
|
@ -133,20 +133,20 @@ replaced the `[:source]` channel, which no longer exists.
|
|||
: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
|
||||
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.
|
||||
The interval in lane time is derived through `:time`; do not also store parent
|
||||
start/end values. Sequence children require finite intervals and positive
|
||||
placement rates. Ordering and overlap checks use the mapped intervals, not `:z`.
|
||||
The sequence group may contain visual occurrences or audio occurrences; its
|
||||
The sequence group may contain visual cels or audio cels; its
|
||||
capability must reject an incompatible mixture rather than infer it per frame.
|
||||
|
||||
A source reference is fixed within an occurrence. The lane changes content when
|
||||
another occurrence becomes active. This is a deliberate revision of the original
|
||||
A source reference is fixed within a cel. The lane changes content when
|
||||
another cel becomes active. This is a deliberate revision of the original
|
||||
diagnosis that making `:of` a channel was necessary to avoid vertical growth:
|
||||
multiple instances can occupy one row when the view presents their containing
|
||||
sequence. A lane-level source schedule is therefore derived, not authored twice.
|
||||
|
|
@ -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.
|
||||
Adding or removing a key must not turn a still into an animation or vice versa.
|
||||
|
||||
An occurrence names content and describes how its source time is sampled. In the
|
||||
basic case, after mapping lane time into occurrence time:
|
||||
A cel names content and describes how its source time is sampled. In the
|
||||
basic case, after mapping lane time into cel time:
|
||||
|
||||
```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
|
||||
resetting that origin; split pieces likewise preserve the source and property
|
||||
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 |
|
||||
| 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 |
|
||||
|
||||
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
|
||||
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
|
||||
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.
|
||||
|
||||
## Time spaces and sampling
|
||||
|
||||
Name the relevant space whenever an API accepts a time or range: project,
|
||||
symbol/lane, occurrence, or source. Store authored frame coordinates exactly;
|
||||
symbol/lane, cel, or source. Store authored frame coordinates exactly;
|
||||
avoid cumulative rounding when moving through nested mappings. Quantize at a
|
||||
declared sampling boundary, not at every traversal step. Audio also needs its
|
||||
continuous clock/sample space rather than visual frame quantization.
|
||||
|
||||
A hold is an evaluable time map with no unique inverse. A loop can map many
|
||||
displayed occurrences to one source time. APIs must distinguish forward sampling
|
||||
from inverse editing, and expose enough context to resolve an occurrence or
|
||||
displayed cels to one source time. APIs must distinguish forward sampling
|
||||
from inverse editing, and expose enough context to resolve a cel or
|
||||
explicitly refuse an ambiguous operation. Do not report a missing time map merely
|
||||
because inversion is unavailable.
|
||||
|
||||
Separate invertible placement timing from source sampling. A zero source speed
|
||||
can mean hold without making the 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
|
||||
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.
|
||||
|
|
@ -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,
|
||||
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.
|
||||
Identity timing can be implicit; remove `:time :mode` if it only duplicates that.
|
||||
|
||||
An occurrence interval is authored. Lane content extent is derived from its
|
||||
occurrences, including the explicit end of the last one. A separately authored
|
||||
A cel interval is authored. Lane content extent is derived from its
|
||||
cels, including the explicit end of the last one. A separately authored
|
||||
container trim/window is legitimate when it intentionally gates children. Do not
|
||||
conflate that window with occupied extent or infer a final hold from the next key
|
||||
when no next key exists. A range of frame numbers alone cannot encode visibility
|
||||
|
|
@ -236,23 +236,23 @@ or a missing measurement.
|
|||
Every view issues the same domain commands. A command accepts an explicit target
|
||||
and edit policy, computes a valid change, and returns the change, resulting
|
||||
selection, and any refusal reason. A button and a drag must not implement two
|
||||
versions of exposure extension.
|
||||
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
|
||||
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
|
||||
different symbol: retain the explicit content target until navigation changes it.
|
||||
|
||||
Core commands include new drawing, reuse drawing, duplicate drawing, make unique,
|
||||
blank range, split, trim, move, extend exposure, slip source, retime, and apply a
|
||||
blank range, split, trim, move, extend cel, slip source, retime, and apply a
|
||||
bounded property edit. Ripple/overwrite policy and the set of affected lanes are
|
||||
explicit command arguments. Preview consequences before committing a gesture.
|
||||
|
||||
New drawing creates fresh empty content and 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.
|
||||
Reuse creates another occurrence pointing at existing content. Duplicate creates
|
||||
a new content identity. Make unique rebinds the selected occurrence only.
|
||||
Reuse creates another cel pointing at existing content. Duplicate creates
|
||||
a new content identity. Make unique rebinds the selected cel only.
|
||||
|
||||
Copy semantics must specify nested sharing. A normal content copy duplicates its
|
||||
owned nodes and channels while preserving references to other reusable symbols.
|
||||
|
|
@ -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
|
||||
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
|
||||
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
|
||||
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
|
||||
changes where their existing local times land. No per-key attachment flag is
|
||||
needed to recover ownership that the document already expresses.
|
||||
|
||||
For the concrete example, extend `:exposure-a` by two lane frames with ripple:
|
||||
For the concrete example, extend `:cel-a` by two lane frames with ripple:
|
||||
|
||||
| Fact | Before | After |
|
||||
| --- | --- | --- |
|
||||
| Exposure A's lane interval | `[0,4)` | `[0,6)` |
|
||||
| Exposure B's lane interval | `[4,8)` | `[6,10)` |
|
||||
| Cel A's lane interval | `[0,4)` | `[0,6)` |
|
||||
| Cel B's lane interval | `[4,8)` | `[6,10)` |
|
||||
| Animated insert's lane interval | `[8,12)` | `[10,14)` |
|
||||
| Girl's rotation peak | Lane frame 6 | Lane frame 6 |
|
||||
| B's position change | B frame 1, lane frame 5 | B frame 1, lane frame 7 |
|
||||
| Insert's first source frame | Source frame 3 | Source frame 3 |
|
||||
|
||||
The rotation peak now coincides with a different point in the drawing sequence.
|
||||
That is the intended consequence of changing exposures underneath timed motion.
|
||||
The position correction stays attached to drawing B's occurrence. Neither the
|
||||
That is the intended consequence of changing cels underneath timed motion.
|
||||
The position correction stays attached to drawing B's cel. Neither the
|
||||
background's keys nor audio on another lane moves.
|
||||
|
||||
The command contract for this edit names the symbol and occurrence, a delta in
|
||||
lane frames, `:ripple` behavior, and an explicit scope of exposure timing. It
|
||||
The command contract for this edit names the symbol and cel, a delta in
|
||||
lane frames, `:ripple` behavior, and an explicit scope of cel timing. It
|
||||
extends A's local support by the delta converted through A's placement rate,
|
||||
and shifts subsequent 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
|
||||
nonpositive resulting duration. Validate and commit the entire change together.
|
||||
|
||||
The symbol's authored end is another explicit boundary: preview an overflow and
|
||||
offer to extend the symbol or cancel. A command can request that extension as
|
||||
part of its transaction; it must not silently truncate later occurrences or grow
|
||||
part of its transaction; it must not silently truncate later cels or grow
|
||||
other uses of a shared symbol. In the example, a 12-frame symbol needs an explicit
|
||||
extension to 14 frames or the edit must be refused without partial changes.
|
||||
|
||||
Retime performance is a separate operation over explicitly selected occurrences
|
||||
Retime performance is a separate operation over explicitly selected cels
|
||||
and channels. It applies the same time transformation to their relevant clocks,
|
||||
keys, and correction supports. Stretching an interval requires a defined warp
|
||||
and interpolation behavior; it is not merely moving keys whose frame numbers
|
||||
happen to lie inside the selection. Until supported, refuse this operation
|
||||
rather than approximating it with 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
|
||||
workflow, so movement usually remains independent of exposure timing. The
|
||||
inspector names the target: lane motion, this occurrence, or shared drawing.
|
||||
workflow, so movement usually remains independent of cel timing. The
|
||||
inspector names the target: lane motion, this cel, or shared drawing.
|
||||
Changing that scope is explicit. It changes what the edit means, not just which
|
||||
panel happens to be open.
|
||||
|
||||
|
|
@ -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
|
||||
convenience, not ownership or deletion authority. Reference discovery for cycle
|
||||
validation, copying, and deletion examines all structural references, including
|
||||
currently inactive occurrences. Authored folders, favorites, and labels remain
|
||||
currently inactive cels. Authored folders, favorites, and labels remain
|
||||
legitimate user data even when the UI could have suggested defaults.
|
||||
|
||||
## UX: location, selection, and controls
|
||||
|
||||
The breadcrumb sits above the timeline and states the editing location, shared
|
||||
content identity, and 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
|
||||
description instead of a fictitious unique global frame.
|
||||
|
||||
Creation controls next to the breadcrumb act in that explicit location. Selection
|
||||
does not secretly change where a new symbol goes. A shared drawing indicates its
|
||||
reuse and offers Make this occurrence unique. Names help identify content;
|
||||
reuse and offers Make this cel unique. Names help identify content;
|
||||
linked-use indicators must rely on IDs, because different drawings can share names.
|
||||
|
||||
| Surface | Primary scope and controls |
|
||||
|
|
@ -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 |
|
||||
| Lane header | Lane selection, lock, mute/solo where applicable, onion settings, expansion |
|
||||
| Stage tools | Drawing and transform modes, active target and scope |
|
||||
| Inspector | Selected content/occurrence/lane properties and valid key controls |
|
||||
| Inspector | Selected content/cel/lane properties and valid key controls |
|
||||
|
||||
Cel actions have visible contextual buttons, shortcuts, a context menu, and
|
||||
command-palette entries. These are different entrances to the same commands.
|
||||
|
|
@ -411,52 +411,52 @@ preferences can be saved separately.
|
|||
## A session, revised
|
||||
|
||||
1. In `main`, create a girl lane and a new drawing. Draw; use New drawing (`N`)
|
||||
to create the next one with the previous exposure ghosted behind it.
|
||||
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.
|
||||
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.
|
||||
3. Time the performance. Hold longer (`H`) extends the selected occurrence and
|
||||
ripples later occurrences in the explicitly targeted lane. A trim gesture
|
||||
3. Time the performance. Hold longer (`H`) extends the selected cel and
|
||||
ripples later cels in the explicitly targeted lane. A trim gesture
|
||||
can use overwrite instead. The preview shows which boundaries will move.
|
||||
4. Choose a two-frame default exposure for newly created drawings, or run a
|
||||
separate Retime exposures command on a selected range. This does not quantize
|
||||
lane transforms or silently retime already authored exposures.
|
||||
4. Choose a two-frame default cel for newly created drawings, or run a
|
||||
separate Retime cels command on a selected range. This does not quantize
|
||||
lane transforms or silently retime already authored cels.
|
||||
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.
|
||||
6. Select three frames on the girl's lane, choose Return motion, and rotate to
|
||||
the desired peak. A bounded rotation correction affects the girl across any
|
||||
drawing boundaries in that range. Existing motion survives outside it.
|
||||
7. Insert a playing animated symbol among the girl's held drawings. Set that
|
||||
occurrence's source playback to advance. No lane conversion is required.
|
||||
cel's source playback to advance. No lane conversion is required.
|
||||
|
||||
The timeline shows named exposure blocks with property marks and optional curve
|
||||
subrows. The exposure sheet shows the same occurrences by frame and lane. The
|
||||
The timeline shows named cel blocks with property marks and optional curve
|
||||
subrows. The cel sheet shows the same cels by frame and lane. The
|
||||
graph editor edits the same properties; the stage resolves the same document.
|
||||
Onion skin is configurable and counts neighboring 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
|
||||
drawing remain distinct events. Deduplicating identical ghosts is a display option.
|
||||
|
||||
## Proof obligations and implementation order
|
||||
|
||||
The source-channel prototype has been removed: an occurrence names one symbol
|
||||
The source-channel prototype has been removed: a cel names one symbol
|
||||
and carries its own playback clock, and `node/problems` rejects the old
|
||||
`[:source]` channel. What a lane IS lives in `arthur.domain.symbol` beside the
|
||||
other rules about a node map; `arthur.domain.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,
|
||||
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
|
||||
lane's own row, and offers Make unique only where the selected exposure actually
|
||||
half-applying. The timeline draws a lane's cels as cel blocks on the
|
||||
lane's own row, and offers Make unique only where the selected cel actually
|
||||
shares its drawing.
|
||||
|
||||
There is ONE placement function and a position argument, so appending is not a
|
||||
different operation from inserting: `:end` is a position like any other, the one
|
||||
where nothing has to move. Placing ripples — occurrences at or after the
|
||||
position move later by the new exposure's duration — and `:keep` versus
|
||||
where nothing has to move. Placing ripples — cels at or after the
|
||||
position move later by the new cel's duration — and `:keep` versus
|
||||
`:grow-symbol` still decides what happens at the shot's end. OVERWRITE is not a
|
||||
policy argument yet, deliberately: taking frames away from the occurrence
|
||||
policy argument yet, deliberately: taking frames away from the cel
|
||||
already there is trimming, and until `trim` exists, placement that would need it
|
||||
refuses instead of approximating it. A position inside an existing exposure
|
||||
refuses instead of approximating it. A position inside an existing cel
|
||||
refuses too, and names `split` — one command does not quietly perform two.
|
||||
|
||||
Splitting turned out to cost almost nothing, which is evidence for the
|
||||
|
|
@ -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.
|
||||
`: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:
|
||||
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
|
||||
|
|
@ -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,
|
||||
because moving a drawing and re-timing the ones around it are different
|
||||
intentions — clear the room with `blank` or `trim` first, which is the
|
||||
composition. Blank leaves a gap and does not close it; 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 is split. Their drawings stay in the library, since a lane does not
|
||||
own its content.
|
||||
|
||||
All three are the same geometry as `split`: a `:span` is in the 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
|
||||
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
|
||||
|
|
@ -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
|
||||
on the girl's lane reaches across the drawing boundary beneath it and leaves
|
||||
every frame outside its support identical, and a correction owned by one
|
||||
exposure travels with that exposure when a hold before it grows.
|
||||
cel travels with that cel when a hold before it grows.
|
||||
|
||||
Regeneration keeps them, which is the obligation the layer design exists to
|
||||
meet: `rebased` replaces a base and carries its corrections across, and a
|
||||
|
|
@ -529,7 +529,7 @@ report.
|
|||
|
||||
Still unbuilt: overwrite as a placement policy (which is now `blank` then
|
||||
`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
|
||||
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
|
||||
offers those three over a selected range, and how it offers a conflict for
|
||||
resolution. Overwrite, the range and retiming
|
||||
commands (blank, trim, move, slip source, retime) and the exposure-sheet view
|
||||
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
|
||||
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
|
||||
|
|
@ -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 |
|
||||
| Holds, playing inserts, nonzero in-points, and gaps on one lane | Source behavior is independent of property key count and channel encoding |
|
||||
| Adjacent occurrences, final hold, split, trim, ripple, and overwrite | Exact boundaries, stable IDs, deterministic collision handling |
|
||||
| Extend a hold under lane keys and occurrence-local corrections | Lane key times remain fixed; later occurrence corrections travel with their owners; source playback origins survive |
|
||||
| Adjacent cels, final hold, split, trim, ripple, and overwrite | Exact boundaries, stable IDs, deterministic collision handling |
|
||||
| Extend a hold under lane keys and cel-local corrections | Lane key times remain fixed; later cel corrections travel with their owners; source playback origins survive |
|
||||
| Ripple beyond the symbol end | Explicit extent policy; refusal leaves the document unchanged; resizing and retiming undo together |
|
||||
| Girl on twos over a moving background | Drawing cadence does not quantize either lane's continuous properties |
|
||||
| Three-frame correction crossing a drawing boundary | Exact support, same result outside it, one undo step |
|
||||
| Nested retiming, holds, loops, and fractional sampling | Explicit time spaces; ambiguous inverse edits cannot silently choose a target |
|
||||
| Audio inside changing source 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 |
|
||||
| Reference cycles and deletion of reused content | Inactive references are validated too; no dangling references |
|
||||
| Save/load and command undo/redo | Identity, source maps, corrections, and evaluation round-trip |
|
||||
| Timeline and exposure-sheet invocation of one command | Identical document changes and selection targets |
|
||||
| Timeline and cel-sheet invocation of one command | Identical document changes and selection targets |
|
||||
| Random forward/backward seeks and export | Reference and optimized evaluation agree, including defaults and absence |
|
||||
| Concurrent commands on overlapping and disjoint targets | Transactions remain valid; conflicts are explicit and undo preserves others' work |
|
||||
|
||||
Implementation order: occurrence ownership and playback semantics; shared
|
||||
Implementation order: cel ownership and playback semantics; shared
|
||||
commands and validation; correction layers and time-addressing contracts; then
|
||||
breadcrumb, cel strip, and an exposure-sheet projection. Use those two temporal
|
||||
breadcrumb, cel strip, and a cel-sheet projection. Use those two temporal
|
||||
views plus direct stage editing to prove the model before broadening the UI.
|
||||
|
||||
A new presentation should not require duplicate animation state. A genuinely new
|
||||
|
|
|
|||
|
|
@ -22,10 +22,10 @@
|
|||
(defn compose
|
||||
"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
|
||||
`: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
|
||||
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
|
||||
|
|
@ -44,7 +44,7 @@
|
|||
by-id (into {} (map (juxt :id :uuid)) (concat instances audio))
|
||||
uuid-of (fn [what 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
|
||||
:known (vec (sort-by str (keys by-id)))}))))
|
||||
nodes (into
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
`other` to its id here.
|
||||
|
||||
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,
|
||||
which is how a symbol made from footage is called what the person typed rather
|
||||
than `:main`.
|
||||
|
|
@ -36,7 +36,7 @@
|
|||
(some #{%} (vals ids)))]
|
||||
(assoc ids sid (clip/free-id taken? (get wanted sid sid)))))
|
||||
{} (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.
|
||||
repoint (fn [n ids]
|
||||
(if (node/source n)
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@
|
|||
;; 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
|
||||
;; 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
|
||||
;; field to get wrong.
|
||||
|
||||
|
|
|
|||
|
|
@ -190,7 +190,7 @@
|
|||
own (symbol/resolver sym store palette
|
||||
(assoc opts :pose-tracks pose-tracks
|
||||
:source-fps (:fps clip)))
|
||||
;; Each occurrence owns its source resolver and mutable buffers.
|
||||
;; Each cel owns its source resolver and mutable buffers.
|
||||
children (into {}
|
||||
(for [[id n] nodes
|
||||
:when (= :instance (:kind n))
|
||||
|
|
@ -282,7 +282,7 @@
|
|||
middle goes there; without one — a drop on the timeline — the drawing stays
|
||||
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
|
||||
generating one in here would make this function's result depend on when it was
|
||||
called, and this namespace is the pure one.
|
||||
|
|
@ -307,7 +307,7 @@
|
|||
:name (symbol-name clip sid)
|
||||
:kind :instance
|
||||
: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.
|
||||
:z (str "z" (js/Date.now) "-" (name sid))
|
||||
:span [0 (:frames target)]
|
||||
|
|
@ -406,7 +406,7 @@
|
|||
missing (remove (:symbols clip) (node/sources n))]
|
||||
(str "symbol " (pr-str sid) " instance " (pr-str id)
|
||||
" 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)
|
||||
[id n] (:nodes sym)
|
||||
:when (= :instance (:kind n))
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
(ns arthur.domain.sequence
|
||||
"The commands over a lane of occurrences: make one, put drawings in it, change
|
||||
(ns arthur.domain.lane
|
||||
"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.
|
||||
|
||||
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
|
||||
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
|
||||
`{: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
|
||||
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
|
||||
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."
|
||||
|
|
@ -40,19 +40,19 @@
|
|||
|
||||
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
|
||||
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
|
||||
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
|
||||
last drawing silently shorten the film.
|
||||
|
||||
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."
|
||||
[clip sid nodes selection extent]
|
||||
(let [sym (clip/symbol clip sid)
|
||||
reach (for [[id n] nodes :when (node/sequence? n)
|
||||
child (symbol/sequence-members nodes id)
|
||||
reach (for [[id n] nodes :when (node/lane? n)
|
||||
child (symbol/lane-cels nodes id)
|
||||
:let [m (lane-map nodes id)
|
||||
end (second (node/placed-span child))]]
|
||||
(when m (+ (:at m) (/ end (:rate m)))))
|
||||
|
|
@ -70,13 +70,13 @@
|
|||
: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
|
||||
;; land in the lane. So moving an edge of an exposure is one write to `:span`,
|
||||
;; A `:span` is in the cel's OWN frames and its `:time` says where those
|
||||
;; 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
|
||||
;; 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.
|
||||
|
||||
(defn- local
|
||||
|
|
@ -91,8 +91,8 @@
|
|||
(assoc-in n [:span (case which :in 0 :out 1)] (local n f)))
|
||||
|
||||
(defn extend-hold
|
||||
"Change one held occurrence's duration by `delta` lane frames and ripple its
|
||||
later siblings. Lane channels, occurrence channels and source clocks stay put.
|
||||
"Change one held cel's duration by `delta` lane frames and ripple its
|
||||
later siblings. Lane channels, cel channels and source clocks stay put.
|
||||
Returns {:clip :selection} or {:refused :required-frames?}; never partially edits."
|
||||
[clip sid id delta {:keys [extent] :or {extent :keep}}]
|
||||
(let [nodes (get-in clip [:symbols sid :nodes])
|
||||
|
|
@ -101,21 +101,21 @@
|
|||
rate (:rate (node/time-of n))
|
||||
span (:span n)
|
||||
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
|
||||
;; 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
|
||||
(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}
|
||||
(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"}
|
||||
(nil? m) {:refused "exposure timing through a stepped or looping lane is not supported"}
|
||||
(<= (+ (second span) (* rate delta)) (first span)) {:refused "a drawing must keep a positive exposure"}
|
||||
(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 cel"}
|
||||
:else
|
||||
(let [[_ boundary] (node/placed-span n)
|
||||
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 (reduce (fn [ns sibling]
|
||||
(update-in ns [(:id sibling) :time :at] (fnil + 0) delta))
|
||||
|
|
@ -123,7 +123,7 @@
|
|||
(finish clip sid nodes id extent)))))
|
||||
|
||||
(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`.
|
||||
|
||||
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
|
||||
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
|
||||
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.
|
||||
|
||||
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)
|
||||
[lo hi] (or (node/placed-span n) [nil nil])]
|
||||
(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"}
|
||||
(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)))
|
||||
{:refused (str "frame " cut " is not inside this exposure")}
|
||||
{:refused (str "frame " cut " is not inside this cel")}
|
||||
:else
|
||||
(let [nodes (-> nodes
|
||||
(assoc id (edged n :out cut))
|
||||
|
|
@ -157,10 +157,10 @@
|
|||
(finish clip sid nodes new-id :keep)))))
|
||||
|
||||
(defn trim
|
||||
"Move one edge of occurrence `id` to lane frame `to`, without disturbing a
|
||||
single other occurrence.
|
||||
"Move one edge of cel `id` to lane frame `to`, without disturbing a
|
||||
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
|
||||
well would give one gesture two sets of rules and a way to overlap its
|
||||
neighbour. `edge` is `:in` or `:out`.
|
||||
|
|
@ -174,15 +174,15 @@
|
|||
lane (get nodes (:parent n))
|
||||
[lo hi] (or (node/placed-span n) [nil nil])]
|
||||
(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 (integer? to)) {:refused "an edge goes to a whole lane frame"}
|
||||
(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))))
|
||||
|
||||
(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.
|
||||
|
||||
One write to `:time :at`. A destination that would overlap a neighbour is
|
||||
|
|
@ -196,40 +196,40 @@
|
|||
lane (get nodes (:parent n))
|
||||
{:keys [at]} (node/time-of n)]
|
||||
(cond
|
||||
(not (node/sequence? lane)) {:refused "select an occurrence in a sequence lane"}
|
||||
(not (integer? to)) {:refused "an exposure moves to a whole lane frame"}
|
||||
(nil? (node/placed-span n)) {:refused "an occurrence needs a span to move"}
|
||||
(not (node/lane? lane)) {:refused "select a cel in a lane"}
|
||||
(not (integer? to)) {:refused "a cel moves to a whole lane frame"}
|
||||
(nil? (node/placed-span n)) {:refused "a cel needs a span to move"}
|
||||
:else
|
||||
(let [moved (update-in n [:time :at] (fnil + 0) (- to (first (node/placed-span n))))]
|
||||
(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))))))
|
||||
|
||||
(defn blank
|
||||
"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
|
||||
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.
|
||||
|
||||
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
|
||||
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
|
||||
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]}]
|
||||
(let [nodes (get-in clip [:symbols sid :nodes])
|
||||
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
|
||||
(first (filter #(let [[lo hi] (node/placed-span %)] (and (< lo a) (> hi b)))
|
||||
members)))]
|
||||
(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)))
|
||||
{:refused "a range to blank is whole lane frames, and not empty"}
|
||||
(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
|
||||
(let [nodes (reduce
|
||||
(fn [ns n]
|
||||
|
|
@ -258,10 +258,10 @@
|
|||
;; putting drawings in a lane
|
||||
|
||||
(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
|
||||
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
|
||||
animation and holding its first drawing the same gesture."
|
||||
[id lane-id drawing-id at]
|
||||
|
|
@ -281,16 +281,16 @@
|
|||
"Where lane `lane-id`'s occupied frames stop, in its own time."
|
||||
[nodes lane-id]
|
||||
(apply max 0 (map #(second (node/placed-span %))
|
||||
(symbol/sequence-members nodes lane-id))))
|
||||
(symbol/lane-cels nodes lane-id))))
|
||||
|
||||
(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.
|
||||
|
||||
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
|
||||
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.
|
||||
|
||||
`: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)
|
||||
[lo hi] (node/placed-span n)
|
||||
later (filter #(>= (first (node/placed-span %)) lo)
|
||||
(symbol/sequence-members nodes lane-id))
|
||||
(symbol/lane-cels nodes lane-id))
|
||||
nodes (reduce (fn [ns sibling]
|
||||
(update-in ns [(:id sibling) :time :at] (fnil + 0) (- hi lo)))
|
||||
(assoc nodes id n) later)
|
||||
|
|
@ -311,31 +311,31 @@
|
|||
(:clip result) (assoc :frame (+ (:at m) (/ at (:rate m)))))))
|
||||
|
||||
(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]
|
||||
(let [nodes (get-in clip [:symbols sid :nodes])
|
||||
lane (get nodes lane-id)
|
||||
;; INSIDE an exposure is not a position for another one. Splitting that
|
||||
;; exposure is what makes it two, and doing it here would be one command
|
||||
;; INSIDE a cel is not a position for another one. Splitting that
|
||||
;; 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.
|
||||
inside (when (number? at)
|
||||
(some (fn [n] (let [[lo hi] (node/placed-span n)]
|
||||
(when (< lo at hi) n)))
|
||||
(symbol/sequence-members nodes lane-id)))]
|
||||
(symbol/lane-cels nodes lane-id)))]
|
||||
(cond
|
||||
(not (node/sequence? lane)) "select a sequence lane"
|
||||
(contains? nodes id) "the new occurrence ID is already used"
|
||||
(not (node/lane? lane)) "select a lane"
|
||||
(contains? nodes id) "the new cel ID is already used"
|
||||
(not (or (= :end at) (and (integer? at) (not (neg? at)))))
|
||||
"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"
|
||||
:else (first (symbol/sequence-problems nodes)))))
|
||||
:else (first (symbol/lane-problems nodes)))))
|
||||
|
||||
(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.
|
||||
|
||||
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
|
||||
from a gap."
|
||||
[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)))
|
||||
|
||||
(defn reuse-drawing
|
||||
"Append a held occurrence of content the document ALREADY has, so the same
|
||||
drawing is exposed twice and editing it changes both exposures.
|
||||
"Append a held cel of content the document ALREADY has, so the same
|
||||
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
|
||||
commands: reuse is a decision to share, and sharing is not something to
|
||||
|
|
@ -381,17 +381,17 @@
|
|||
:id id})))
|
||||
|
||||
(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 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
|
||||
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."
|
||||
[clip sid id new-id {:keys [at extent deep?] :or {extent :keep at :end}}]
|
||||
(let [n (get-in clip [:symbols sid :nodes id])
|
||||
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")
|
||||
(placeable clip sid (:parent n) new-id at))]
|
||||
{:refused why}
|
||||
|
|
@ -399,10 +399,10 @@
|
|||
(place c sid (:parent n) new-id copy at extent)))))
|
||||
|
||||
(defn make-unique
|
||||
"Point occurrence `id` at a private copy of its content, leaving every other
|
||||
occurrence of that drawing sharing the original.
|
||||
"Point cel `id` at a private copy of its content, leaving every other
|
||||
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
|
||||
in the library for no reason a person could see."
|
||||
[clip sid id {:keys [deep?]}]
|
||||
|
|
@ -412,7 +412,7 @@
|
|||
[oid on] (:nodes osym)
|
||||
:when (and (= from (node/source on)) (not= [sid id] [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 (empty? elsewhere) "nothing else uses this drawing"))]
|
||||
{:refused why}
|
||||
|
|
@ -63,7 +63,7 @@
|
|||
local (symbol/frame-of r id)
|
||||
inst? (= :instance (:kind (get nodes id)))
|
||||
;; 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
|
||||
;; is its animation at its own in-point and speed.
|
||||
shown (when (number? local)
|
||||
|
|
@ -124,7 +124,7 @@
|
|||
(partition 2 pts)))))))
|
||||
|
||||
(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,
|
||||
an output interval, and automation mapped into the open symbol's time."
|
||||
[clip sid]
|
||||
|
|
@ -298,7 +298,7 @@
|
|||
only a move that keeps the PICTURE needs a frame, for the matrix."
|
||||
[clip sid path]
|
||||
(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
|
||||
;; would make where a row lives depend on the playhead.
|
||||
only (fn [sid id]
|
||||
|
|
|
|||
|
|
@ -136,13 +136,13 @@
|
|||
;; ---------------------------------------------------------------------------
|
||||
;; 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
|
||||
;; at the edges.
|
||||
|
||||
(defn expose
|
||||
"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
|
||||
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
|
||||
`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
|
||||
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
|
||||
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."
|
||||
|
|
@ -207,13 +207,13 @@
|
|||
;; what an instance places
|
||||
;;
|
||||
(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."
|
||||
[n]
|
||||
(when (= :instance (:kind n)) (get-in n [:source :symbol])))
|
||||
|
||||
(defn sources
|
||||
"Structural references, including occurrences outside the playhead."
|
||||
"Structural references, including cels outside the playhead."
|
||||
[n]
|
||||
(if-let [sid (source n)] #{sid} #{}))
|
||||
|
||||
|
|
@ -221,7 +221,7 @@
|
|||
(merge {:in 0 :speed 1 :end :stop} (:playback n)))
|
||||
|
||||
(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."
|
||||
[n]
|
||||
(let [{:keys [in speed end]} (playback-of n)]
|
||||
|
|
@ -229,9 +229,9 @@
|
|||
{:at (- (/ in speed)) :rate speed})))
|
||||
|
||||
(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'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."
|
||||
[n f length]
|
||||
(when-let [sid (source n)]
|
||||
|
|
@ -245,7 +245,14 @@
|
|||
(when (and (<= 0 frame) (< frame length))
|
||||
{: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))))
|
||||
|
||||
(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
|
||||
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.
|
||||
|
||||
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
|
||||
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."
|
||||
|
|
@ -425,7 +432,7 @@
|
|||
(finite-number? speed) (<= 0 speed)
|
||||
(#{:stop :hold :loop} end)))))
|
||||
(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")
|
||||
(and (= k :audio) (not (some (:source n) [:footage :sound])))
|
||||
(conj "an audio node needs a :source :footage or :sound")
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
(:require [arthur.domain.node :as node]))
|
||||
|
||||
(defn prepare
|
||||
"Sort exposure tracks once when building a resolver."
|
||||
"Sort pose tracks once when building a resolver."
|
||||
[tracks]
|
||||
(into {}
|
||||
(map (fn [[group entries]]
|
||||
|
|
@ -37,8 +37,8 @@
|
|||
untouched."
|
||||
[clip sid instance group at source]
|
||||
(let [inst (get-in clip [:symbols sid :nodes instance])
|
||||
;; A cut is checked against the ONE symbol this occurrence places. Which
|
||||
;; drawing a lane shows is a question about the lane's other occurrences,
|
||||
;; A cut is checked against the ONE symbol this cel places. Which
|
||||
;; 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.
|
||||
placed (get-in clip [:symbols (node/source inst)])
|
||||
length (or (:frames placed) 0)
|
||||
|
|
@ -70,7 +70,7 @@
|
|||
clip)))
|
||||
|
||||
(defn problems
|
||||
"Errors in one symbol instance's exposure tracks."
|
||||
"Errors in one symbol instance's pose tracks."
|
||||
[tracks source-frames groups]
|
||||
(cond
|
||||
(nil? tracks) []
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@
|
|||
[arthur.domain.wire :as wire]))
|
||||
|
||||
(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."
|
||||
3)
|
||||
|
||||
|
|
|
|||
|
|
@ -90,8 +90,8 @@
|
|||
[nodes id]
|
||||
(dec (count (lineage nodes id))))
|
||||
|
||||
(defn sequence-members
|
||||
"The occurrences of lane `lane`, in the order they are exposed.
|
||||
(defn lane-cels
|
||||
"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
|
||||
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 %))))
|
||||
vec))
|
||||
|
||||
(defn sequence-problems
|
||||
(defn lane-problems
|
||||
"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
|
||||
the parent and stencil references, rather than wherever a command happens to
|
||||
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
|
||||
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."
|
||||
|
|
@ -116,7 +116,7 @@
|
|||
(vec
|
||||
(mapcat
|
||||
(fn [[id lane]]
|
||||
(when (node/sequence? lane)
|
||||
(when (node/lane? lane)
|
||||
(let [children (filter #(= id (:parent %)) (vals nodes))
|
||||
valid? (fn [n]
|
||||
(and (= :instance (:kind n))
|
||||
|
|
@ -126,9 +126,9 @@
|
|||
intervals (sort-by first (map node/placed-span (filter valid? children)))]
|
||||
(concat
|
||||
(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))
|
||||
[(str "sequence " id " has overlapping occurrences")])))))
|
||||
[(str "lane " id " has overlapping cels")])))))
|
||||
nodes)))
|
||||
|
||||
(defn order
|
||||
|
|
@ -613,7 +613,7 @@
|
|||
(if-not (map? nodes)
|
||||
[":nodes must be a map of id -> node"]
|
||||
(-> []
|
||||
(into (sequence-problems nodes))
|
||||
(into (lane-problems nodes))
|
||||
(into (for [[id n] nodes
|
||||
:when (not= id (:id n))]
|
||||
(str "node under key " (pr-str id) " has :id " (pr-str (:id n)))))
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
[arthur.domain.gesture :as gesture]
|
||||
[arthur.domain.nest :as nest]
|
||||
[arthur.domain.node :as node]
|
||||
[arthur.domain.sequence :as sequence]
|
||||
[arthur.domain.lane :as lane]
|
||||
[arthur.events.edit :as edit]
|
||||
[arthur.events.paint :as paint]
|
||||
[arthur.events.playback :as playback]
|
||||
|
|
@ -27,7 +27,7 @@
|
|||
[:clip :symbols sid :nodes id :kind]))]
|
||||
(cond-> (-> db
|
||||
(assoc-in [:ui :selection] selection)
|
||||
(update :ui dissoc :points :sequence-retry))
|
||||
(update :ui dissoc :points :lane-retry))
|
||||
(and (= :node kind) path (not sound?))
|
||||
(update-in [:ui :expanded] (fnil into #{}) (rest (reductions conj [] (pop path))))))))
|
||||
|
||||
|
|
@ -35,33 +35,33 @@
|
|||
::set-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
|
||||
leaves the document and history untouched; an overflow offers an explicit retry."
|
||||
[db sid result retry]
|
||||
(if-let [why (:refused result)]
|
||||
(-> db
|
||||
(assoc-in [:project :status] why)
|
||||
(assoc-in [:ui :sequence-retry]
|
||||
(assoc-in [:ui :lane-retry]
|
||||
(when (:required-frames result) retry)))
|
||||
(let [[_ selected-sid _ path] (get-in db [:ui :selection])
|
||||
prefix (if (and (= sid selected-sid) (seq path)) (pop path) [])]
|
||||
(-> db
|
||||
(edit/transaction (constantly (:clip 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
|
||||
::new-lane
|
||||
(fn [db _]
|
||||
(let [clip (:clip (store/entry (:clip/current db)))
|
||||
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
|
||||
"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
|
||||
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."
|
||||
|
|
@ -71,23 +71,23 @@
|
|||
{:keys [at rate]} (:time (nest/inside clip st (get-in db [:ui :open])
|
||||
(if (seq path) (pop path) [])
|
||||
(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)
|
||||
(assoc :dispatch [::playback/seek (+ at (/ (:frame result) rate))]))))
|
||||
|
||||
(defn- selected-lane
|
||||
"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]
|
||||
(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
|
||||
::append-drawing
|
||||
(fn [{:keys [db]} [_ extent]]
|
||||
(let [clip (:clip (store/entry (:clip/current db)))
|
||||
[_ 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)
|
||||
{:extent (or extent :keep)})]
|
||||
(committed db sid result [::append-drawing :grow-symbol]))))
|
||||
|
|
@ -97,7 +97,7 @@
|
|||
(fn [{:keys [db]} [_ extent]]
|
||||
(let [clip (:clip (store/entry (:clip/current db)))
|
||||
[_ 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]))
|
||||
{:extent (or extent :keep)})]
|
||||
(committed db sid result [::reuse-drawing :grow-symbol]))))
|
||||
|
|
@ -107,7 +107,7 @@
|
|||
(fn [{:keys [db]} [_ extent deep?]]
|
||||
(let [clip (:clip (store/entry (:clip/current db)))
|
||||
[_ 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?})]
|
||||
(committed db sid result [::duplicate-drawing :grow-symbol deep?]))))
|
||||
|
||||
|
|
@ -120,67 +120,67 @@
|
|||
(let [clip (:clip (store/entry (:clip/current db)))
|
||||
[_ sid id] (get-in db [:ui :selection])
|
||||
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
|
||||
(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)})
|
||||
{:refused "this lane's frames are not the open symbol's"})]
|
||||
(committed db sid result [::insert-drawing :grow-symbol]))))
|
||||
|
||||
(rf/reg-event-db
|
||||
::split-exposure
|
||||
::split-cel
|
||||
(fn [db _]
|
||||
(let [clip (:clip (store/entry (:clip/current db)))
|
||||
[_ 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]))]
|
||||
(apply-sequence-command
|
||||
(apply-lane-command
|
||||
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"})
|
||||
nil))))
|
||||
|
||||
(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."
|
||||
[db]
|
||||
(let [clip (:clip (store/entry (:clip/current db)))
|
||||
[_ sid id] (get-in db [:ui :selection])
|
||||
n (get-in clip [:symbols sid :nodes id])]
|
||||
{: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
|
||||
::trim-exposure
|
||||
::trim-cel
|
||||
(fn [db [_ edge]]
|
||||
(let [{:keys [clip sid id at]} (at-playhead db)]
|
||||
(apply-sequence-command
|
||||
(apply-lane-command
|
||||
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"})
|
||||
nil))))
|
||||
|
||||
(rf/reg-event-db
|
||||
::move-exposure
|
||||
::move-cel
|
||||
(fn [db _]
|
||||
(let [{:keys [clip sid id at]} (at-playhead db)]
|
||||
(apply-sequence-command
|
||||
(apply-lane-command
|
||||
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"})
|
||||
nil))))
|
||||
|
||||
(rf/reg-event-db
|
||||
::blank-exposure
|
||||
;; The selected exposure's own frames, so the range needs no second gesture and
|
||||
;; the case that would split an exposure cannot arise.
|
||||
::blank-cel
|
||||
;; The selected cel's own frames, so the range needs no second gesture and
|
||||
;; the case that would split a cel cannot arise.
|
||||
(fn [db _]
|
||||
(let [{:keys [clip sid node]} (at-playhead db)
|
||||
span (node/placed-span node)]
|
||||
(apply-sequence-command
|
||||
(apply-lane-command
|
||||
db sid (if (and span (every? integer? span))
|
||||
(sequence/blank clip sid (:parent node) span {})
|
||||
{:refused "select an exposure that starts and ends on whole lane frames"})
|
||||
(lane/blank clip sid (:parent node) span {})
|
||||
{:refused "select a cel that starts and ends on whole lane frames"})
|
||||
nil))))
|
||||
|
||||
(rf/reg-event-db
|
||||
|
|
@ -188,21 +188,21 @@
|
|||
(fn [db [_ deep?]]
|
||||
(let [clip (:clip (store/entry (:clip/current db)))
|
||||
[_ 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
|
||||
::extend-hold
|
||||
(fn [db [_ delta extent]]
|
||||
(let [clip (:clip (store/entry (:clip/current db)))
|
||||
[_ sid id] (get-in db [:ui :selection])
|
||||
result (sequence/extend-hold clip sid id delta {:extent (or extent :keep)})]
|
||||
(apply-sequence-command db sid result [::extend-hold delta :grow-symbol]))))
|
||||
result (lane/extend-hold clip sid id delta {:extent (or extent :keep)})]
|
||||
(apply-lane-command db sid result [::extend-hold delta :grow-symbol]))))
|
||||
|
||||
(rf/reg-event-fx
|
||||
::sequence-retry
|
||||
::lane-retry
|
||||
(fn [{:keys [db]} _]
|
||||
(if-let [event (get-in db [:ui :sequence-retry])]
|
||||
{:db (update db :ui dissoc :sequence-retry) :dispatch event}
|
||||
(if-let [event (get-in db [:ui :lane-retry])]
|
||||
{:db (update db :ui dissoc :lane-retry) :dispatch event}
|
||||
{})))
|
||||
|
||||
(rf/reg-event-db
|
||||
|
|
|
|||
|
|
@ -84,14 +84,14 @@
|
|||
Four things, and each for its own reason:
|
||||
|
||||
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;
|
||||
everything BELOW it, because a group instance is its children;
|
||||
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.
|
||||
|
||||
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."
|
||||
[nodes id]
|
||||
(let [up (loop [i id acc #{}]
|
||||
|
|
@ -117,9 +117,9 @@
|
|||
"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 symbol a placement plays. Rooting at `:sym/face-8625` renders the drawing in
|
||||
its own time, identically for all seven placements. Isolating one placement
|
||||
renders the STAGE — its length, its rate, the placement's span, drift and scale
|
||||
the symbol an instance plays. Rooting at `:sym/face-8625` renders the drawing in
|
||||
its own time, identically for all seven instances. Isolating one instance
|
||||
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
|
||||
on the stage, and they are different deliverables."
|
||||
[sym id]
|
||||
|
|
|
|||
|
|
@ -74,10 +74,11 @@
|
|||
::exposure
|
||||
:<- [::symbol]
|
||||
(fn [sym _]
|
||||
;; Exposure lives on the symbol's root node and is INHERITED, so reading it
|
||||
;; there is reading it everywhere. The transport shows it so that `exposure 2`
|
||||
;; is visibly doing something at the transport rather than only inside the
|
||||
;; document.
|
||||
;; NOT a cel. This is `:time :expose` — how many frames each step of a
|
||||
;; subtree lasts, which is what shooting on twos means — and it lives on the
|
||||
;; symbol's root node and is INHERITED, so reading it there is reading it
|
||||
;; 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)))
|
||||
|
||||
(rf/reg-sub
|
||||
|
|
@ -115,7 +116,7 @@
|
|||
(defn- placed?
|
||||
"Does row `path` from symbol `sid` still name an instance, all the way down?"
|
||||
[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
|
||||
;; is the structural answer, and does not move as the lane cuts.
|
||||
(reduce (fn [sid id]
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
[re-frame.core :as rf]))
|
||||
|
||||
(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 ::tool (fn [db _] (get-in db [:ui :tool])))
|
||||
(rf/reg-sub ::draft (fn [db _] (get-in db [:ui :draft])))
|
||||
|
|
|
|||
|
|
@ -215,7 +215,7 @@
|
|||
[facts
|
||||
"name" (or (:name n) (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.
|
||||
"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
|
||||
|
|
@ -239,7 +239,7 @@
|
|||
;; tracing a face
|
||||
;;
|
||||
;; 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`.
|
||||
;;
|
||||
;; Whether the footage is SHOWING is deliberately not here. It is a viewing aid
|
||||
|
|
@ -435,11 +435,11 @@
|
|||
node @(rf/subscribe [::sub/selected-node])
|
||||
tracked? (seq (owners clip))
|
||||
;; 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.
|
||||
;; That last case had no section at all before, and it is the one where
|
||||
;; 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
|
||||
;; showing now would move the section under the playhead.
|
||||
placed (node/source (peek node))
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@
|
|||
(:require [clojure.string :as str]
|
||||
[arthur.domain.node :as node]
|
||||
[arthur.domain.nest :as nest]
|
||||
[arthur.domain.sequence :as sequence]
|
||||
[arthur.domain.lane :as lane]
|
||||
[arthur.domain.symbol :as symbol]
|
||||
[arthur.domain.trace :as trace]
|
||||
[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
|
||||
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
|
||||
is where the row should show it."
|
||||
[n]
|
||||
|
|
@ -140,16 +140,16 @@
|
|||
(distinct))
|
||||
(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
|
||||
;; 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
|
||||
;; necessary. The occurrence is still the thing selected
|
||||
;; necessary. The cel is still the thing selected
|
||||
;; 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
|
||||
(node/sequence? n)
|
||||
(node/lane? n)
|
||||
(assoc :cels
|
||||
(mapv (fn [child]
|
||||
{:id (:id child)
|
||||
|
|
@ -158,7 +158,7 @@
|
|||
:source (node/source child)
|
||||
:span (mapv self (node/placed-span 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?
|
||||
[row]
|
||||
(-> [row]
|
||||
|
|
@ -175,7 +175,7 @@
|
|||
|
||||
(defn sound-rows
|
||||
"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]
|
||||
(if-not (get-in clip [:symbols sid]) []
|
||||
(vec
|
||||
|
|
@ -228,19 +228,19 @@
|
|||
clip @(rf/subscribe [::render/clip])
|
||||
[_ sid id] @(rf/subscribe [::sub/selection])
|
||||
n (get-in clip [:symbols sid :nodes id])
|
||||
lane (if (node/sequence? n) n (get-in clip [:symbols sid :nodes (:parent n)]))
|
||||
lane? (node/sequence? lane)
|
||||
lane (if (node/lane? n) n (get-in clip [:symbols sid :nodes (:parent n)]))
|
||||
lane? (node/lane? lane)
|
||||
cel? (and lane? (= :instance (:kind n)) (some? (node/source n)))
|
||||
held? (and cel? (zero? (:speed (node/playback-of n))))
|
||||
;; 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)
|
||||
[_ other] (:nodes sym)
|
||||
:when (= (node/source n) (node/source other))]
|
||||
other))))
|
||||
;; Both act at the playhead, so both are offered only where the playhead
|
||||
;; 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))
|
||||
splittable? (and cel? (integer? at)
|
||||
(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"
|
||||
:on-click #(rf/dispatch [::ui/insert-drawing])} "insert"]
|
||||
[:button {:disabled (not splittable?)
|
||||
:title "cut this exposure in two at the playhead; the picture does not change"
|
||||
:on-click #(rf/dispatch [::ui/split-exposure])} "split"]
|
||||
:title "cut this cel in two at the playhead; the picture does not change"
|
||||
:on-click #(rf/dispatch [::ui/split-cel])} "split"]
|
||||
[: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"]
|
||||
[:button {:disabled (not cel?)
|
||||
:title "append a copy of this drawing, to draw the next one over it"
|
||||
:on-click #(rf/dispatch [::ui/duplicate-drawing])} "duplicate"]
|
||||
[: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"]
|
||||
[:button {:disabled (not splittable?)
|
||||
:title "start this exposure at the playhead; nothing else moves"
|
||||
:on-click #(rf/dispatch [::ui/trim-exposure :in])} "trim in"]
|
||||
:title "start this cel at the playhead; nothing else moves"
|
||||
:on-click #(rf/dispatch [::ui/trim-cel :in])} "trim in"]
|
||||
[:button {:disabled (not splittable?)
|
||||
:title "end this exposure at the playhead; nothing else moves"
|
||||
:on-click #(rf/dispatch [::ui/trim-exposure :out])} "trim out"]
|
||||
:title "end this cel at the playhead; nothing else moves"
|
||||
:on-click #(rf/dispatch [::ui/trim-cel :out])} "trim out"]
|
||||
[:button {:disabled (not (and cel? (integer? at)))
|
||||
:title "put this exposure at the playhead; refused if something is there"
|
||||
:on-click #(rf/dispatch [::ui/move-exposure])} "move here"]
|
||||
:title "put this cel at the playhead; refused if something is there"
|
||||
:on-click #(rf/dispatch [::ui/move-cel])} "move here"]
|
||||
[:button {:disabled (not cel?)
|
||||
:title "clear this exposure's frames, leaving a gap; later drawings stay put"
|
||||
:on-click #(rf/dispatch [::ui/blank-exposure])} "blank"]
|
||||
:title "clear this cel's frames, leaving a gap; later drawings stay put"
|
||||
:on-click #(rf/dispatch [::ui/blank-cel])} "blank"]
|
||||
[: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 −"]
|
||||
[: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 +"]
|
||||
(when @(rf/subscribe [::sub/sequence-retry])
|
||||
[:button {:on-click #(rf/dispatch [::ui/sequence-retry])} "extend shot and apply"])
|
||||
(when @(rf/subscribe [::sub/lane-retry])
|
||||
[:button {:on-click #(rf/dispatch [::ui/lane-retry])} "extend shot and apply"])
|
||||
[:span.spacer]
|
||||
[:span.dim (str frame " / " frames)]
|
||||
;; Measured in the loop, not derived from the clock — the whole question
|
||||
|
|
@ -475,7 +475,7 @@
|
|||
:when (< in out)]
|
||||
^{:key (str id)}
|
||||
[: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)
|
||||
:width (str (* 100 (/ (- out in) (max 1 frames))) "%")
|
||||
:top "2px" :bottom "2px" :overflow "hidden" :padding "0 3px"}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
[arthur.domain.node :as node]
|
||||
[arthur.domain.palette :as pal]
|
||||
[arthur.domain.pick :as pick]
|
||||
[arthur.domain.sequence :as sequence]
|
||||
[arthur.domain.lane :as lane]
|
||||
[arthur.domain.symbol :as symbol]))
|
||||
|
||||
(defn drawing [id x frames]
|
||||
|
|
@ -18,17 +18,17 @@
|
|||
:channels {[:geom :size] (ch/framed 4)
|
||||
[: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)
|
||||
:source {:symbol source} :playback {:in 0 :speed speed :end :stop}
|
||||
:time {:at at :rate 1} :span [0 duration]})
|
||||
|
||||
(defn document []
|
||||
(let [a (occurrence :a :drawing-a 0 4 0)
|
||||
b (assoc-in (occurrence :b :drawing-b 4 4 0)
|
||||
(let [a (cel :a :drawing-a 0 4 0)
|
||||
b (assoc-in (cel :b :drawing-b 4 4 0)
|
||||
[:channels [:xform :pos]] (ch/keyed {0 [0 0] 1 [2 0]} :hold))
|
||||
insert (assoc-in (occurrence :insert :wave 8 4 1) [:playback :in] 3)]
|
||||
{:name "exposures" :fps 24 :width 320 :height 200
|
||||
insert (assoc-in (cel :insert :wave 8 4 1) [:playback :in] 3)]
|
||||
{:name "cels" :fps 24 :width 320 :height 200
|
||||
:symbols
|
||||
{:main {:id :main :frames 12
|
||||
: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)))
|
||||
(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)
|
||||
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)
|
||||
nodes (get-in after [:symbols :main :nodes])]
|
||||
(is (= :a (:selection result)))
|
||||
|
|
@ -78,19 +78,19 @@
|
|||
(is (= 320 (get-in at [10 [:insert :mark]])) "insert starts on source frame 3"))
|
||||
(is (empty? (clip/problems after)))
|
||||
(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")))
|
||||
|
||||
(deftest overflow-and-invalid-edits-are-atomic
|
||||
(let [doc (document)
|
||||
result (sequence/extend-hold doc :main :a 2 {})]
|
||||
result (lane/extend-hold doc :main :a 2 {})]
|
||||
(is (:refused result))
|
||||
(is (= 14 (:required-frames result)))
|
||||
(is (not (contains? result :clip)))
|
||||
(doseq [delta [0 -4 0.5 js/NaN]]
|
||||
(is (:refused (sequence/extend-hold doc :main :a delta {}))))
|
||||
(is (:refused (sequence/extend-hold doc :main :insert 1 {})))
|
||||
(is (:refused (sequence/extend-hold doc :main :missing 1 {})))))
|
||||
(is (:refused (lane/extend-hold doc :main :a delta {}))))
|
||||
(is (:refused (lane/extend-hold doc :main :insert 1 {})))
|
||||
(is (:refused (lane/extend-hold doc :main :missing 1 {})))))
|
||||
|
||||
(deftest a-gap-is-an-uncovered-interval
|
||||
(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 (= 139 (get-in (sample edited [0 4]) [4 [:b :mark]]))))))
|
||||
|
||||
(deftest occurrence-identities-and-playback-round-trip
|
||||
(let [doc (:clip (sequence/extend-hold (document) :main :a 2 {:extent :grow-symbol}))
|
||||
(deftest cel-identities-and-playback-round-trip
|
||||
(let [doc (:clip (lane/extend-hold (document) :main :a 2 {:extent :grow-symbol}))
|
||||
leaves (leaf/leaves :project doc)]
|
||||
(is (= doc (leaf/clip :project leaves)))
|
||||
(is (contains? leaves "clip/project/symbol/main/node/a"))
|
||||
|
|
@ -163,7 +163,7 @@
|
|||
|
||||
(deftest one-transaction-undoes-the-ripple-and-shot-extension
|
||||
(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)
|
||||
after-leaves (leaf/leaves :p after)
|
||||
h (-> nil history/hold (history/record before-leaves after-leaves 0) history/settle)
|
||||
|
|
@ -174,19 +174,19 @@
|
|||
(is (= after-leaves (:leaves redo)))))
|
||||
|
||||
(deftest create-lane-and-append-drawings
|
||||
(let [doc (:clip (sequence/add-lane (clip/blank) :main :girl))
|
||||
a (:clip (sequence/append-drawing doc :main :girl :a :drawing-a {}))
|
||||
b (:clip (sequence/append-drawing a :main :girl :b :drawing-b {}))]
|
||||
(let [doc (:clip (lane/add-lane (clip/blank) :main :girl))
|
||||
a (:clip (lane/append-drawing doc :main :girl :a :drawing-a {}))
|
||||
b (:clip (lane/append-drawing a :main :girl :b :drawing-b {}))]
|
||||
(is (empty? (clip/problems 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 (: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
|
||||
(let [doc (-> (document)
|
||||
(assoc-in [:symbols :main :nodes :a :time :rate] 2)
|
||||
(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 (= [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))
|
||||
"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"}
|
||||
:span [0 10]
|
||||
:channels {[:audio :gain] (ch/keyed {0 0 5 1} :linear)}}
|
||||
|
|
@ -208,7 +208,7 @@
|
|||
(is (= [8 12] (node/placed-span track)))
|
||||
(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])))
|
||||
(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)]
|
||||
(is (= [10 14] (node/placed-span track)))
|
||||
(is (= [3 7] (:span track))))
|
||||
|
|
@ -231,56 +231,56 @@
|
|||
|
||||
(deftest enclosing-retiming-is-respected-when-extending-the-shot
|
||||
(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 (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])))))
|
||||
|
||||
(deftest reuse-shares-content-and-make-unique-decouples-one-exposure
|
||||
(deftest reuse-shares-content-and-make-unique-decouples-one-cel
|
||||
(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}))
|
||||
edit (fn [c sym x]
|
||||
(assoc-in c [:symbols sym :nodes :mark :channels [:xform :pos]]
|
||||
(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")
|
||||
(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 (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])]
|
||||
(is (= 99 (get-in at [0 [:a :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 (= (:nodes (get-in shared [:symbols :drawing-a]))
|
||||
(:nodes (get-in unique [:symbols :drawing-a-2])))
|
||||
"a copy of the same drawing, not an empty one")
|
||||
(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])]
|
||||
(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])]
|
||||
(is (= 10 (get-in at [0 [:a :mark]])) "and does not reach back"))
|
||||
(is (empty? (clip/problems unique))))
|
||||
;; Nothing else places drawing-b, so there is nothing to decouple from.
|
||||
(is (:refused (sequence/make-unique doc :main :b {})))
|
||||
(is (:refused (sequence/make-unique doc :main :girl {}))
|
||||
(is (:refused (lane/make-unique doc :main :b {})))
|
||||
(is (:refused (lane/make-unique doc :main :girl {}))
|
||||
"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)
|
||||
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])]
|
||||
(is (= :drawing-b-2 (node/source n)))
|
||||
(is (= (:nodes (get-in doc [:symbols :drawing-b]))
|
||||
(:nodes (get-in made [:symbols :drawing-b-2]))))
|
||||
(is (= [12 13] (node/placed-span 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])
|
||||
(get-in made [:symbols :main :nodes :b]))
|
||||
"the drawing duplicated is left as it was")
|
||||
|
|
@ -293,7 +293,7 @@
|
|||
{:id :part :kind :instance :z "b" :span [0 1]
|
||||
:time {:at 0 :rate 1} :source {:symbol :wave}
|
||||
: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))))
|
||||
shallow (copy {})
|
||||
deep (copy {:deep? true})]
|
||||
|
|
@ -306,19 +306,19 @@
|
|||
|
||||
(deftest reuse-refuses-what-would-not-be-a-document
|
||||
(let [doc (document)]
|
||||
(is (:refused (sequence/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 :nothing-here {})))
|
||||
(is (:refused (lane/reuse-drawing doc :main :girl :c :main {:extent :grow-symbol}))
|
||||
"a symbol cannot go inside itself")
|
||||
(is (:refused (sequence/reuse-drawing doc :main :girl :a :drawing-a {:extent :grow-symbol}))
|
||||
"an occurrence ID in use is not free")
|
||||
(is (:refused (sequence/reuse-drawing doc :main :plate :c :drawing-a {})))
|
||||
(is (:refused (sequence/duplicate-drawing doc :main :girl :d {})))))
|
||||
(is (:refused (lane/reuse-drawing doc :main :girl :a :drawing-a {:extent :grow-symbol}))
|
||||
"a cel ID in use is not free")
|
||||
(is (:refused (lane/reuse-drawing doc :main :plate :c :drawing-a {})))
|
||||
(is (:refused (lane/duplicate-drawing doc :main :girl :d {})))))
|
||||
|
||||
(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.
|
||||
;; 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)
|
||||
(update-in [:symbols :main :nodes] dissoc :a :b :insert)
|
||||
(update-in [:symbols :main :nodes] merge
|
||||
|
|
@ -337,7 +337,7 @@
|
|||
|
||||
(defn- drawn
|
||||
"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]
|
||||
(let [at (sample doc fs)]
|
||||
(mapv #(sort (vals (get at %))) fs)))
|
||||
|
|
@ -347,7 +347,7 @@
|
|||
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])))
|
||||
[: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})]
|
||||
(is (= [[0 4] [4 5] [5 9] [9 13]] (spans (:clip r))))
|
||||
(is (= 13 (get-in r [:clip :symbols :main :frames])))
|
||||
|
|
@ -356,45 +356,45 @@
|
|||
(is (= 4 (:frame r)))
|
||||
(is (empty? (clip/problems (:clip r))))
|
||||
;; 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.
|
||||
(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})))))
|
||||
;; 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"
|
||||
(: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}))))
|
||||
(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})))
|
||||
(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})))
|
||||
;; Reuse and duplicate take a position too; it is one placement rule.
|
||||
(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})
|
||||
[:clip :symbols :main :nodes :n]))))
|
||||
(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})
|
||||
[: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)
|
||||
fs (range 12)
|
||||
before (drawn doc fs)]
|
||||
(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]]]
|
||||
(testing label
|
||||
(let [r (sequence/split doc :main id cut :right)
|
||||
(let [r (lane/split doc :main id cut :right)
|
||||
after (:clip r)]
|
||||
(is (= :right (:selection r)))
|
||||
(is (= before (drawn after fs)) "the same picture, frame for frame")
|
||||
(is (= (node/placed-span (get-in doc [: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])))])
|
||||
"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])))
|
||||
(first (node/placed-span (get-in after [:symbols :main :nodes :right])))))
|
||||
(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 (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)]
|
||||
(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 (sequence/split doc :main :girl 2 :right)) "a lane is not an exposure")
|
||||
(is (:refused (sequence/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 :b cut :right)) (str "cut at " (pr-str cut))))
|
||||
(is (:refused (lane/split doc :main :girl 2 :right)) "a lane is not a cel")
|
||||
(is (:refused (lane/split doc :main :plate 2 :right)) "nor is a shape outside one")
|
||||
(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
|
||||
;; The two commands the doc asks for, composed: neither one guesses.
|
||||
(let [doc (document)
|
||||
cut (:clip (sequence/split doc :main :a 2 :right))
|
||||
r (sequence/append-drawing cut :main :girl :n :drawing-n
|
||||
cut (:clip (lane/split doc :main :a 2 :right))
|
||||
r (lane/append-drawing cut :main :girl :n :drawing-n
|
||||
{:at 2 :extent :grow-symbol})
|
||||
after (:clip r)]
|
||||
(is (= [[0 2] [2 3] [3 5] [5 9] [9 13]]
|
||||
|
|
@ -459,14 +459,14 @@
|
|||
(is (= b (:leaves (history/undo h a))))
|
||||
(is (= c (leaf/clip :p a)) "a correction needs no codec of its own"))))
|
||||
|
||||
(deftest a-correction-on-one-exposure-travels-with-it
|
||||
;; The other half of ownership: a layer on an occurrence is in that
|
||||
;; occurrence's own frames, so moving the exposure moves the correction and
|
||||
(deftest a-correction-on-one-cel-travels-with-it
|
||||
;; The other half of ownership: a layer on a cel is in that
|
||||
;; cel's own frames, so moving the cel moves the correction and
|
||||
;; nothing has to say so.
|
||||
(let [beat (ch/layer :beat [0 2] :offset (ch/framed [7 0]))
|
||||
doc (update-in (document) [:symbols :main :nodes :b :channels [:xform :pos] :over]
|
||||
(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,
|
||||
;; so the claim is about WHERE the layer applies and not about arithmetic.
|
||||
(let [nudge (fn [with without f]
|
||||
|
|
@ -475,7 +475,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")
|
||||
(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}))
|
||||
%)
|
||||
[6 7 8 9]))
|
||||
|
|
@ -488,9 +488,9 @@
|
|||
(defn- spans [clip 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)
|
||||
r (sequence/trim doc :main :b :out 6)
|
||||
r (lane/trim doc :main :b :out 6)
|
||||
after (:clip r)]
|
||||
(is (= [[0 4] [4 6] [8 12]] (spans after [:a :b :insert])))
|
||||
(is (= :b (:selection r)))
|
||||
|
|
@ -505,7 +505,7 @@
|
|||
;; were, so the frames that survive show exactly what they showed.
|
||||
(let [doc (document)
|
||||
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 (= (:playback (get-in doc [:symbols :main :nodes :insert]))
|
||||
(:playback (get-in after [:symbols :main :nodes :insert]))))
|
||||
|
|
@ -520,13 +520,13 @@
|
|||
["past its end" :out 9]
|
||||
["before its start" :in 2]
|
||||
["off a whole frame" :out 5.5]]]
|
||||
(is (:refused (sequence/trim doc :main :b edge to)) label))
|
||||
(is (:refused (sequence/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 :b edge to)) label))
|
||||
(is (:refused (lane/trim doc :main :b :middle 6)))
|
||||
(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)
|
||||
r (sequence/move doc :main :insert 4)
|
||||
r (lane/move doc :main :insert 4)
|
||||
after (:clip r)]
|
||||
(is (= [[0 4] [4 8]] (spans after [:a :insert])))
|
||||
(is (= :insert (:selection r)))
|
||||
|
|
@ -539,19 +539,19 @@
|
|||
|
||||
(deftest a-move-onto-an-occupied-frame-is-refused-rather-than-rippled
|
||||
(let [doc (document)]
|
||||
(is (:refused (sequence/move doc :main :insert 6)) "it would overlap B")
|
||||
(is (:refused (sequence/move doc :main :insert 4.5)))
|
||||
(is (:refused (sequence/move doc :main :girl 2)))
|
||||
(is (:refused (lane/move doc :main :insert 6)) "it would overlap B")
|
||||
(is (:refused (lane/move doc :main :insert 4.5)))
|
||||
(is (:refused (lane/move doc :main :girl 2)))
|
||||
;; Clearing the room first is the composition, and then it goes.
|
||||
(let [cleared (:clip (sequence/blank doc :main :girl [4 8] {}))]
|
||||
(is (= [[0 4] [4 8]] (spans (:clip (sequence/move cleared :main :insert 4))
|
||||
(let [cleared (:clip (lane/blank doc :main :girl [4 8] {}))]
|
||||
(is (= [[0 4] [4 8]] (spans (:clip (lane/move cleared :main :insert 4))
|
||||
[:a :insert]))))))
|
||||
|
||||
(deftest blanking-leaves-a-gap-and-does-not-close-it
|
||||
(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)]
|
||||
;; 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 (= :rest (:selection r)))
|
||||
(let [at (sample after [4 5 6 7])]
|
||||
|
|
@ -562,9 +562,9 @@
|
|||
(is (= 12 (get-in after [:symbols :main :frames])))
|
||||
(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)
|
||||
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 (= [[0 4] [8 12]] (spans after [:a :insert])) "and moves nothing")
|
||||
(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
|
||||
(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 (nil? (get-in after [:symbols :main :nodes :b])))
|
||||
(is (= (get-in (sample doc [9]) [9 [:insert :mark]])
|
||||
|
|
@ -583,28 +583,28 @@
|
|||
|
||||
(deftest blank-refuses-what-it-cannot-do-in-one-piece
|
||||
(let [doc (document)]
|
||||
(is (re-find #"free ID" (:refused (sequence/blank doc :main :girl [5 7] {})))
|
||||
"splitting an exposure needs an ID for the remainder")
|
||||
(is (:refused (sequence/blank doc :main :girl [5 7] {:id :a})) "and a free one")
|
||||
(is (:refused (sequence/blank doc :main :girl [7 5] {})))
|
||||
(is (:refused (sequence/blank doc :main :girl [5 5] {})))
|
||||
(is (:refused (sequence/blank doc :main :girl [5 6.5] {})))
|
||||
(is (:refused (sequence/blank doc :main :plate [0 2] {})))))
|
||||
(is (re-find #"free ID" (:refused (lane/blank doc :main :girl [5 7] {})))
|
||||
"splitting a cel needs an ID for the remainder")
|
||||
(is (:refused (lane/blank doc :main :girl [5 7] {:id :a})) "and a free one")
|
||||
(is (:refused (lane/blank doc :main :girl [7 5] {})))
|
||||
(is (:refused (lane/blank doc :main :girl [5 5] {})))
|
||||
(is (:refused (lane/blank doc :main :girl [5 6.5] {})))
|
||||
(is (:refused (lane/blank doc :main :plate [0 2] {})))))
|
||||
|
||||
(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 last half is a shot somebody authored that long, and deleting the last
|
||||
;; drawing must not quietly shorten the film.
|
||||
(let [doc (document)
|
||||
empty-lane (:clip (sequence/blank doc :main :girl [0 12] {}))]
|
||||
(is (empty? (symbol/sequence-members (get-in empty-lane [:symbols :main :nodes]) :girl)))
|
||||
empty-lane (:clip (lane/blank doc :main :girl [0 12] {}))]
|
||||
(is (empty? (symbol/lane-cels (get-in empty-lane [:symbols :main :nodes]) :girl)))
|
||||
(is (= 12 (get-in empty-lane [:symbols :main :frames])))
|
||||
(is (empty? (clip/problems empty-lane)))
|
||||
;; 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 (= 21 (get-in (sequence/append-drawing empty-lane :main :girl :n :drawing-n
|
||||
(is (:refused (lane/append-drawing empty-lane :main :girl :n :drawing-n {:at 20})))
|
||||
(is (= 21 (get-in (lane/append-drawing empty-lane :main :girl :n :drawing-n
|
||||
{:at 20 :extent :grow-symbol})
|
||||
[: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]))
|
||||
"and trimming the last exposure leaves the window where it was")))
|
||||
"and trimming the last cel leaves the window where it was")))
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
(ns arthur.events.sequence-test
|
||||
(ns arthur.events.lane-test
|
||||
(:require [cljs.test :refer [deftest is]]
|
||||
[arthur.domain.lane-test :as fixture]
|
||||
[arthur.domain.sequence :as sequence]
|
||||
[arthur.domain.lane :as lane]
|
||||
[arthur.events.ui :as ui]
|
||||
[arthur.domain.history :as history]
|
||||
[arthur.domain.leaf :as leaf]
|
||||
[arthur.footage.store :as store]
|
||||
[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)
|
||||
rows (timeline/rows doc :main #{})
|
||||
lane (first (filter :cels rows))]
|
||||
|
|
@ -24,15 +24,15 @@
|
|||
id (store/install! {:clip doc :store {}} "sequence-test")
|
||||
db {:clip/current id :paint/revision 0
|
||||
:ui {:open :main :selection [:node :main :a [:a]]}}
|
||||
refused (ui/apply-sequence-command db :main
|
||||
(sequence/extend-hold doc :main :a 1 {}) [:retry])]
|
||||
refused (ui/apply-lane-command db :main
|
||||
(lane/extend-hold doc :main :a 1 {}) [:retry])]
|
||||
(is (= doc (:clip (store/entry id))))
|
||||
(is (nil? (:history (store/entry id))))
|
||||
(is (= [:retry] (get-in refused [:ui :sequence-retry])))
|
||||
(let [r1 (sequence/extend-hold doc :main :a 1 {:extent :grow-symbol})
|
||||
db1 (ui/apply-sequence-command db :main r1 nil)
|
||||
r2 (sequence/extend-hold (:clip r1) :main :a 1 {:extent :grow-symbol})
|
||||
db2 (ui/apply-sequence-command db1 :main r2 nil)
|
||||
(is (= [:retry] (get-in refused [:ui :lane-retry])))
|
||||
(let [r1 (lane/extend-hold doc :main :a 1 {:extent :grow-symbol})
|
||||
db1 (ui/apply-lane-command db :main r1 nil)
|
||||
r2 (lane/extend-hold (:clip r1) :main :a 1 {:extent :grow-symbol})
|
||||
db2 (ui/apply-lane-command db1 :main r2 nil)
|
||||
h (:history (store/entry id))
|
||||
undo (history/undo h (leaf/leaves "u" (:clip r2)))
|
||||
undo2 (history/undo (:history undo) (:leaves undo))]
|
||||
|
|
@ -59,7 +59,7 @@ try {
|
|||
await evaluate(`(() => {
|
||||
const k = cljs.core.keyword;
|
||||
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 entry = arthur.footage.store.entry(cljs.core.get(db, k('clip/current')));
|
||||
return cljs.core.clj__GT_js(entry);
|
||||
|
|
@ -75,7 +75,7 @@ try {
|
|||
})()`), `enabled button: ${label}`);
|
||||
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')
|
||||
.sort((a, b) => a.time.at - b.time.at);
|
||||
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(`cljs.core.get_in(cljs.core.deref(re_frame.db.app_db),
|
||||
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.
|
||||
await evaluate(`(() => {
|
||||
|
|
@ -112,9 +112,9 @@ try {
|
|||
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 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.
|
||||
await evaluate(`(() => {
|
||||
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(await evaluate('document.querySelectorAll(".tl-cel").length'), 3);
|
||||
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);
|
||||
await click('make unique');
|
||||
s = await shot();
|
||||
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');
|
||||
await click('duplicate');
|
||||
s = await shot();
|
||||
cels = instances(s);
|
||||
assert.equal(cels.length, 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');
|
||||
|
||||
// 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]]);
|
||||
await evaluate(`document.querySelector('.tl-cel').click()`);
|
||||
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');
|
||||
assert.equal(await enabled('split'), true);
|
||||
await click('split');
|
||||
s = await shot();
|
||||
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');
|
||||
s = await shot();
|
||||
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');
|
||||
assert.equal(await evaluate('document.querySelectorAll(".tl-cel").length'), 6);
|
||||
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);
|
||||
|
||||
// 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 sleep(200);
|
||||
assert.deepEqual(placed(await shot()).slice(2, 4), [[3, 5], [5, 6]]);
|
||||
|
|
@ -185,7 +185,7 @@ try {
|
|||
await click('trim out');
|
||||
s = await shot();
|
||||
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);
|
||||
await click('move here');
|
||||
s = await shot();
|
||||
Loading…
Add table
Add a link
Reference in a new issue