Symbols, not timelines; no symbol is special

Everything that holds nodes is a symbol (domain/timeline -> domain/symbol,
:timelines -> :symbols) and a node that places one is :kind :instance. The
reserved :main root is gone: which symbol is on screen is editor state
([:ui :open]), every domain function that needs a symbol is told which, and
a document opens on the longest symbol nothing else places.

Saved projects move to schema 2 through migration 0007, which rewrites leaf
paths, instance kinds and the feature :symbol key; the client refuses a
schema it does not read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Olive Vaughn 2026-09-29 12:46:42 -04:00
parent 179770d7d4
commit 5dff490162
61 changed files with 1587 additions and 1431 deletions

View file

@ -116,8 +116,14 @@ projects the server holds; the built-in scenes are under their own heading,
italic, and are not projects — they are compiled into the bundle and the server
has never heard of them.
Selection lives in app-db under `:ui`, as `[:node <timeline> <node>]`,
`[:timeline <id>]` or `[:subject|:feature|:group <id>]` — four panes ask what is
Everything that holds nodes is a **symbol**, and none is special: a new document
has one called `main` because it has to be called something. Which symbol is on
screen is editor state, `[:ui :open]`, not a fact about the document — the stage
draws it, the timeline lists it, the transport plays it and a new shape goes into
it. A document opens on the longest symbol nothing else places.
Selection lives in app-db under `:ui`, as `[:node <symbol> <node>]`,
`[:symbol <id>]` or `[:subject|:feature|:group <id>]` — four panes ask what is
selected, and a ratom private to one of them can only be shared by making the
other three require it.
@ -125,12 +131,12 @@ other three require it.
into detection. Dragging a symbol out of the pool onto the stage places an
instance of it at the playhead.
The timeline's rows are the open clip's nodes, front-most first, with a dot per
The timeline's rows are the open symbol's nodes, front-most first, with a dot per
keyframe and a bar over the frames the node exists on; a dense channel is hatched
rather than ticked, because one value per frame is a solid block that says less
than the bar does. Opening a row shows its channels; opening a **symbol** row
shows the timeline it instances, with every frame number mapped back into the
stage's own frame space — see the namespace docstring in `ui/timeline.cljs`, which
than the bar does. Opening a row shows its channels; opening an **instance** row
shows the symbol it places, with every frame number mapped back into the open
symbol's frame space — see the namespace docstring in `ui/timeline.cljs`, which
is where that mapping is argued.
### Paint sketch
@ -168,9 +174,9 @@ and real footage use `src/arthur/flow/take.cljs` for the measurement order and
`src/arthur/flow/freeze.cljs` for the landmark-to-channel conversion.
**8625 stage study**, in the open menu, loads the locally saved `IMG_8625.MOV` project and places its
post-processed timeline twice. The stage layout is
post-processed face symbol twice. The stage layout is
`src/arthur/demo/stage_8625.edn`: the right picture and sound start at frame 48,
and the two pictures overlap slightly in stage space. Audio has its own timeline
and the two pictures overlap slightly in stage space. Audio has its own
nodes, linked to the picture instances but with independent spans and gain
channels. The right sound swells and pans across the stage, then fades out at
frame 260 while its picture continues to
@ -357,12 +363,12 @@ them is `clips/templates/clips/index.html`.
## Two evaluators, on purpose
`domain/timeline` has both `eval-frame` and `resolver`, and they are not
`domain/symbol` has both `eval-frame` and `resolver`, and they are not
alternatives:
- **`(eval-frame timeline f store)`** is the specification. Allocating, order-free,
- **`(eval-frame symbol f store)`** is the specification. Allocating, order-free,
obviously correct. Tests and one-off renders use it.
- **`(resolver timeline store)` -> `(fn [f] ops)`** is what playback uses. It caches
- **`(resolver symbol store)` -> `(fn [f] ops)`** is what playback uses. It caches
the topological order and the z paths, holds a cursor per channel and reuses
one point buffer per node, so a frame allocates the op maps and nothing else.