arthur/docs/clipboard-plan.md

140 lines
6.9 KiB
Markdown
Raw Normal View History

2026-10-02 16:13:03 -04:00
# Selection and clipboard
This is the implementation contract for multi-selection, copy, cut, paste,
duplicate, and duplicate unique. It deliberately replaces any incidental older
behavior. The document model is the authority; the stage and timeline are two
views of the same editor state.
## One selection
`[:ui :selections]` is the ordered selection set. Its last member is the primary
selection in `[:ui :selection]`, used by the inspector and single-subject tools.
Every member is an occurrence address:
```clojure
[:node owner-symbol-id node-id row-path]
```
The row path distinguishes two occurrences of shared content. Commands that
write the document canonicalize those addresses before acting:
- invalid and non-node addresses are ignored;
- the same owned node, `[owner-symbol-id node-id]`, is acted on once;
- when one selected row path is below another selected row path, only the
ancestor is a clipboard root. Its ordinary parent-pointer subtree comes with
it, and an instance already displays the symbol it references, so also
materializing the visibly nested selection would duplicate it twice.
Plain click replaces the selection. Shift-click toggles membership, on both the
stage and timeline. A stage marquee replaces, or with Shift adds to, the same
set. Timeline rows, bars, and cel blocks render membership from that same set;
the primary member gets the inspector/focus treatment.
Targeting remains separate. Clicking a timeline label both selects it and aims
it. Shift-click changes the selection set and makes the clicked row the one
target. Clicking a bar/cel or the stage changes selection without silently
changing the target. A target is singular even when selection is plural.
## Clipboard value
The clipboard is editor state, not document state and not history. Copy records
a detached snapshot of each canonical root and its complete parent-pointer
subtree. It records source occurrence paths and authored node data, but normal
copy deliberately keeps referenced symbol identities. Therefore a pasted
instance is another use of the same symbol. The clipboard survives cutting its
nodes because it contains the node snapshot, not merely their addresses.
This first implementation is the application's clipboard, not the operating
system clipboard. It is consequently project-local and has no serialization or
cross-project identity collision policy hidden inside it.
## Paste
Paste always uses the explicit insertion target and the playhead:
- no target: paste at the top of the open symbol;
- aimed ordinary node: paste beside it in the symbol that owns it;
- aimed instance, including an instance whose source is drawn as a lane: paste
inside the symbol it places;
- the open lane's own row: paste directly into that lane.
The destination is resolved structurally from the target. It does not require
the target instance to be visible at the playhead. Being outside the target
symbol's authored window is legal: content can exist there and be cropped by
the window. Lane paste may grow the lane/window because a sequence cannot store
an overlapping or unreachable tail and pretend the edit succeeded.
The earliest finite start among the copied roots is aligned with the playhead
in the destination. All other root starts retain their offset from it. Roots
without a finite span remain timeless; paste does not invent a span for a shape
that was authored for the whole symbol. Parent/child timing, transforms,
channels, corrections, playback, stencil links, and relative root order are
otherwise copied exactly. Every node receives a new identity, and all internal
parent and stencil references are remapped.
In an ordinary composition overlap is valid. In a lane the pasted finite spans
claim their intervals using the lane's existing overwrite rule: covered cels
are removed, crossing cels are trimmed or split, and the pasted roots do not
overlap one another. Pasting a timeless root into a lane is refused. Validation
is all-or-nothing.
After paste, the new roots are the selection, in clipboard order, and the last
one is primary.
## Cut
Cut first takes exactly the same snapshot as copy, then deletes every canonical
root and its parent-pointer subtree. The clipboard write is editor state; the
whole document deletion is one history transaction. The selection is cleared.
Undo restores the deleted document nodes. It does not roll back the clipboard,
which matches ordinary editor behavior.
## Duplicate and Duplicate Unique
Duplicate does not read the insertion target or playhead. It is a local
operation beside the selected material:
- in an ordinary composition, copies keep the originals' parent, transform,
timing, and span, and are stacked immediately in front;
- for direct children of a lane, the selected temporal envelope is repeated
immediately after itself. Relative timing and gaps inside the selected set are
preserved, and later cels ripple forward by the envelope duration. This is
the lane's useful "duplicate forward" behavior; it is not a second command.
Selections in several owners are handled per owner in one command. Thus two
lane selections repeat in their respective lanes, while a selected composition
node duplicates in place, all as one history step.
Normal Duplicate preserves symbol references, just like normal copy/paste.
Duplicate Unique performs the same placement but deep-copies the complete graph
of every referenced symbol. One shared remap table is used for the whole batch,
so two duplicated instances that shared a nested part still share one new copy
with each other, while sharing nothing mutable with the originals. Immutable
media/store blocks may remain shared.
After either duplicate command, the new roots replace the selection.
## History, refusal, and stale state
Cut, paste, duplicate, and duplicate unique each call one domain command and
commit through one `edit/transaction`; each is exactly one undo/redo step no
matter how many nodes or symbols it touches. Copy, aiming, and selection do not
touch history. A refusal changes no document leaves and creates no history step.
Undo/redo filters the complete selection set against the restored document and
repairs the primary selection. Clipboard payloads remain snapshots. Paste
validates the destination and all remapped references at commit time, so a stale
target or a newly impossible symbol cycle refuses rather than partially editing.
## Required tests
Domain tests cover canonical ancestor/descendant selection, subtree ID remaps,
normal shared references, deep unique graph remaps, multi-root relative timing,
composition overlap, lane overwrite, lane forward duplication and ripple,
mixed-owner duplication, cycle refusal, stale targets, and all-or-nothing
failure. Event tests cover copy without history, atomic cut/paste/duplicates,
resulting multi-selection, target resolution, pasting beyond a symbol window,
and one undo plus redo of each mutation. Browser tests cover mirrored stage and
timeline selection, Shift-toggle on labels/bars/cels, singular aiming, and the
keyboard commands.