6.7 KiB
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:
[: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.
Creation targeting is derived rather than stored. The primary (last-selected)
occurrence is the preferred row; the playhead validates it and, when necessary,
walks outward to the nearest valid containing occurrence. A target is singular
even when selection is plural. See docs/creating-in.md for the resolver
contract.
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 resolves one destination from the primary selection and playhead. An ordinary occurrence is usable only while the playhead maps through every enclosing occurrence and lies within its extent. Otherwise resolution walks outward, with the open symbol as the total fallback. A selected lane is an insertion surface only while all occurrences enclosing its parent symbol are valid. Multi-selection supplies one ordered payload, not several destinations; only its primary member anchors this resolution.
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 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 resolved destination and all remapped references at commit time, so a stale selection 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 fallback at the playhead, and one undo plus redo of each mutation. Browser tests cover mirrored stage and timeline selection, Shift-toggle on labels/bars/cels, singular derived creation targeting, and the keyboard commands.