# Patch editor

> How does a creator wire a graph on the canvas, and what guarantees hold for every gesture? The patch editor is browser UI over React Flow and the shared authoring operations: every

Source: https://docs.tune.toys/contributors/patch-editor/

How does a creator wire a graph on the canvas, and what guarantees hold for every gesture? The patch editor is browser UI over React Flow and the shared authoring operations: every accepted edit is one validated, undoable authoring batch, and no canvas interaction changes the portable Toy model or DSP semantics.

**Status:** implemented browser editing UI. Cable brush selection, arbitrary multi-port drag splicing and broad physical-device qualification remain separate work; full browser/DAW qualification is tracked in [implementation status](https://github.com/joshstovall/TuneToys/blob/main/docs/delivery/implementation-status.md).

## One rule for every gesture

```mermaid
%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#FAFAF7', 'primaryTextColor': '#1C1C1B', 'primaryBorderColor': '#1C1C1B', 'lineColor': '#64645F', 'fontFamily': 'DM Sans, Inter, system-ui, sans-serif'}}}%%
flowchart LR
  GESTURE["Gesture or menu action<br/>drag · drop · key · context menu"] --> PREVIEW["Inert preview<br/>no source, no history, no audio"]
  PREVIEW -->|Escape · cancel · blur · outside| NOTHING["No change"]
  PREVIEW -->|release / confirm| ADMIT{"Full source admission<br/>channels · occupancy · cycles · ownership"}
  ADMIT -->|rejected| EXPLAIN["Visible reason<br/>graph unchanged"]
  ADMIT -->|accepted| BATCH["One authoring batch<br/>one Undo step"]
  BATCH --> HOST["Runtime prepares next generation<br/>DSP keeps running meanwhile"]
  classDef shared fill:#DCEBDD,stroke:#1C1C1B,color:#1C1C1B
  classDef source fill:#F0442E,stroke:#1C1C1B,color:#FAFAF7
  classDef host fill:#DED8F0,stroke:#1C1C1B,color:#1C1C1B
  class GESTURE,PREVIEW,ADMIT,EXPLAIN,NOTHING shared
  class BATCH source
  class HOST host
```

The canvas fills the workspace between document tabs and the transport. Undo/redo, node discovery, selection/panning, visual grouping, subpatch creation, arrange, zoom, minimap and an optional public-control inspector are canvas overlays. The shared [app shell](https://github.com/joshstovall/TuneToys/blob/main/docs/architecture/settings-library.md) keeps a persistent component palette beside the patch on desktop; palette and Spotlight share cycle-free catalog entries, and the compact navigation drawer provides tap-to-add on narrow screens.

## Open views

`EditorTabs.tsx` provides closable, keyboard-accessible document tabs: Left/Right and Home/End switch, Delete closes a non-pinned view, the main patch stays open. Each nested patch tab carries its definition ID and full instance path, so two instances of one reusable definition open independently and keep separate cameras. Tabs, the active view, cameras, node selections, Frame layer selections and inspector visibility restore after reload from browser-local `tunetoys.workspace.v1:<id>` storage; invalid paths, missing members, foreign document state and malformed cameras are discarded. Closing a tab never removes source. The tab menu opens Frame design, applet preview and portable source views. Snapshot links open the applet once; the first source edit clears the URL fragment so reload uses recovery data.

Loose components are stored in `Definition.widgets`, positioned directly on the Patch, and rendered by the same `WidgetView` as Frame members. Empty-canvas drops preserve the pointer position; only dropping on a Frame gives it membership. Dragging across a Frame boundary converts bounds and transfers ownership in one undoable batch. Negative coordinates, Properties editing, resizing, selection restoration and same-Patch component duplication use this source model.

`PatchCanvas.tsx` renders each explicitly defined Frame as a movable, resizable canvas card that holds its components, like a group: each component is drawn through the same shared renderer used in published playback at its Frame position, addresses the active instance, and is moved or resized right on the card, writing the Frame itself. The Patch lock chooses between arranging (unlocked: components select, drag and resize; their controls do not respond) and playing (locked: controls take input; structure and layout stay fixed). Design opens the Frame designer and Preview opens a dedicated applet tab without a second renderer or DSP copy. Selecting a component edits it in Properties, a dashed binding link joins its outlet to every parameter row its control drives, and dragging that outlet onto another row re-points the control ([web app](https://github.com/joshstovall/TuneToys/blob/main/apps/web/src/README.md#components-on-the-patch)). The [customizable workspace](https://docs.tune.toys/contributors/workspace-layout/) owns splitting, docking and floating of these views.

## Context actions

Dragging a Frame's header or empty background moves the whole Frame with its contents in one undoable layout edit. Member coordinates and bindings stay unchanged; dragging a component still moves that component independently. Escape cancels the move, and Patch lock prevents it.

Double-clicking empty space opens Spotlight and places the chosen node at that flow coordinate at any zoom/pan. Right-click opens a menu for the targeted canvas, node, cable, visual group or Frame card; a right-click on a selected node preserves the selection, another node becomes the sole target. Menus route through the same authoring operations and history as toolbar actions. Right-button dragging does not pan; use the pan tool, middle button or trackpad. Native context menus remain in editable node fields. Frame design has canvas insertion/settings actions and shared layer property, duplication, stacking, visibility, lock and removal actions in both the canvas and Layers panel; locked layers keep their edit protections and Play mode keeps live controls.

The editor's Mantine adapter uses the semantic color roles and partial component styles without a global reset. Tooltips support hover and focus; menus delegate positioning, focus and dismissal to Mantine; the view picker uses Popover. Searchable selectors preserve durable option values and call the same admission callbacks. Authentication, profile and library forms use Mantine `useForm` over the existing validators and versioned mutations; the shared Notifications host replaces notice timers; Media Dropzone keeps whole-selection rejection and the existing file limits and cancellation.

## Nodes and cables

| Element      | Behavior                                                                                                                                                                                                                                                                                                                                                                                                         |
| ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Node card    | One `AudioNode` renderer shared with picker and drag previews; category-tinted header, compact numeric values without range sliders, 30 px desktop headers, 8 px gutters, 3 px parameter row padding; coarse pointers get taller targets; choices share the value box (`ADSR ⌄` like `80 ms`)                                                                                                                    |
| Values       | Public controls send live host gestures and commit source defaults at gesture end; unexposed literals use `node.value`; `parameterDefaultOperation` follows aliases to the owning public default; controls clamp to admitted domains and log controls use shared Frame normalization                                                                                                                             |
| Folding      | Chevron folds controls/footer to the header; H folds a mixed selection or expands a fully folded one; body stops rendering while DSP keeps running; port anchors stay mounted; header inlet/outlet buttons open connectable port lists                                                                                                                                                                           |
| Appearance   | `layout.node` writes optional display name, color and width in `extensions["toys.editor.nodes"][definitionId][nodeId]` through ordinary history; display name is independent of the durable ID; invalid imported appearance is ignored                                                                                                                                                                           |
| Ports        | Declared signal type and width; hollow inlets, filled outlets; audio cyan circles, control amber diamonds (parameter inlets too), MIDI red squares, video blue and data violet circles. `PORT_TYPES` (`portTypes.ts`) sets every glyph, cable and width label from the themed `--port-*` tokens; every port shows its width (`2ch`, `1 lane`, `1 stream`, `Auto`), which follows the admitted multichannel graph |
| Cables       | Draw/reconnect through shared source validation (channel compatibility, occupied destinations, cycles); compatible targets highlight, blocked ports explain; Escape cancels; reconnection excludes the replaced edge from admission                                                                                                                                                                              |
| Add to Frame | Each parameter has Add/Show in Frame; an unexposed value becomes a public control and an explicit Frame member in one batch via `frameControlOperations`, and from a Patch its card lands beside the node and opens in Properties; multiple Frames prompt for a destination, none creates one                                                                                                                    |
| Inlets       | Every parameter row carries its control inlet (`@name`); a collapsed card lists only cabled inlets. A control cable from any control outlet, or a Frame control's binding outlet, drops on the row                                                                                                                                                                                                               |
| Properties   | Same value/ownership model as cards, real controls, declared/resolved port contracts, attached cables, nearest compatible peer search, replace/disconnect and peer reveal; display/default resets are explicit source edits                                                                                                                                                                                      |

Collapse state is stored per browser view/instance path. Clipboard paste remaps appearance, grouping moves child entries and deletion cleans them. React Flow remeasures after folding, opening port lists or changing widths. Group open stays a separate action.

Dropping a cable on empty canvas opens compatible node discovery at that location. A cable's Insert node action (also in the inspector) opens a reviewed port plan; contextual results inspect before Add and multiple viable socket pairs are selectable. The original cable stays until the entire node/default/wire/layout batch is admitted; stale requests fail without changing source. Delete and reconnect bridges only selected nodes with one input, one output and one connected input, and complete validation must accept every resulting wire. Node removal cleans orphan public controls, presets and Frame members. The picker itself is described in [node picker](https://github.com/joshstovall/TuneToys/blob/main/docs/architecture/node-picker.md).

## Selection clipboard, gestures and keyboard commands

| Command                                 | Behavior                                                                                                                                                                                                                                                                                                                    |
| --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Cmd/Ctrl+D, copy/cut/paste              | Bounded `tune-toys-selection` payload: selected nodes, internal cables, complete public-control owners, required nested definitions/Frames, layout and visual regions; paste imports fresh identities, boundary cables do not travel; partial public controls become literals; one history edit                             |
| Option/Alt-drag on a node header        | Drags a copy of the node or selection with inert previews at the receiving view's zoom; release inside the canvas commits the paste batch with preserved relative positions and selects the copies; under four pixels creates nothing; Escape, pointer cancel, lost capture, blur, source change or release outside cancels |
| Arrow keys                              | Preview a nudge of 4 canvas units (40 with Shift), commit once on release; Escape discards                                                                                                                                                                                                                                  |
| Shift+2 / Shift+1 / Shift+0 / Shift++ − | Fit selection / fit patch / 100% / zoom                                                                                                                                                                                                                                                                                     |
| Shift-drag a cable, then Shift-click    | Batch wiring from one origin; release Shift or Escape to finish; each cable is one undoable edit                                                                                                                                                                                                                            |
| `?` or the keyboard button              | Searchable reference of implemented shortcuts and gestures                                                                                                                                                                                                                                                                  |

Async clipboard work cannot mutate an unmounted patch; a same-tab fallback retains copies when clipboard APIs are unavailable. Inputs, dialogs and menus keep their local keyboard and clipboard behavior. Shared Frame knobs use vertical normalized motion, Shift fine adjustment, keyboard stepping and double-click reset with host ownership guards.

Copying a Receive retains its bus name without copying the external Send or its upstream graph. Paste requires a same-named Send in the destination definition, with signal types and channel widths checked before commit. When a selection includes the Send, its copied Send and Receives use an independent bus name if the destination already uses the original name. Nested definitions keep their own bus scope and must remain complete.

## Two kinds of grouping

| Kind         | Storage and operations                                                                                                                                                                                                                                                                                                                           | Effect on DSP                                     |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------- |
| Visual group | Named movable region; member IDs in `extensions["toys.editor.groups"][definitionId]`; `layout.group.put` / `layout.group.remove`; a node belongs to at most one region; moving a region writes member positions through `layout`; removing or nesting a member cleans membership; malformed imports ignored                                      | None                                              |
| Subpatch     | The `group` operation moves selected nodes into a reusable definition, rewires boundary ports and parent cables, keeps public IDs and explicit Frame membership; group nodes open the addressed instance in a tab; Rename subpatch uses `definition.rename`; breadcrumbs open ancestor instances; nesting repeats without a separate write model | Topology change through the same validated source |

## Notes, routes, cable tools and finding nodes

| Tool                | Behavior                                                                                                                                                                                                                                                                         |
| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Notes and waypoints | Editable draggable notes and cable waypoints in bounded `toys.editor.annotations` metadata keyed by definition and durable IDs; never processing nodes, never routing; drag/delete/double-click remove; Escape cancels; obsolete routes pruned; copies and grouping carry routes |
| Cut cables          | Explicitly armed overlay; drag previews the crossed-cable count; release removes them in one validated batch; sampling prepared once per gesture with a bounded hit set per segment, honoring zoom and routed curves                                                             |
| Shift-drag splice   | An unattached single-input/single-output node over a cable previews a splice; release with Shift held combines the move and both connections in one batch under full admission; Escape restores layout; connected or multi-port nodes keep ordinary moves                        |
| Find node in Toy    | Searches names, types and nested instance paths and reveals the instance                                                                                                                                                                                                         |
| Patch detail        | Full by default, with an explicit Compact option; zoom preserves node detail and cables                                                                                                                                                                                          |
| Arrange             | Works on the selection with measured dimensions and preserves unrelated positions; without a selection arranges the patch and Frame cards                                                                                                                                        |

Reusable component capture and insertion are in [components](https://github.com/joshstovall/TuneToys/blob/main/docs/architecture/reusable-components.md).

## Canvas structure and render model

`PatchCanvas.tsx` composes one Patch view from cohesive modules in [`apps/web/src/patch/`](https://github.com/joshstovall/TuneToys/blob/main/apps/web/src/patch/README.md): a pure derived graph (`graphModel.tsx`), identity reconciliation (`graphIdentity.ts`), and hooks for selection, node drags, cables, clipboard, keyboard and nudges, palette drops, plus the context menu, toolbars and dialogs.

- **React Flow owns node state between revisions.** Nodes are uncontrolled: a drag or arrow-key nudge moves cards inside the Flow store, so a pointer move re-renders only the moved card and its cables, never the canvas component. Release writes one `layout` batch (with a Shift splice when armed); Escape or a revision change mid-gesture restores the starting positions and writes nothing. Each new revision's derived graph is reconciled into the store.
- **Derived objects keep identity.** Authoring clones the document per revision, so the graph is rebuilt, then compared by content: unchanged nodes and cables keep their previous objects and React Flow skips them. Callbacks in node and edge data are latest-bound — a single action object dispatches by durable id against the current document — so callback props never force a rebuild and content comparison is exact.
- **Card-local gestures survive unrelated edits.** Graphical editors and resize handles are keyed by their node's own values, not the document revision.
- **Inspector edits stay one Undo step.** Parameter number boxes send live host values while typing or scrubbing and commit once; style and appearance controls preview through `editPreview.tsx` and commit once.

## Provenance and verification

Layout references were synth.ist's `features/canvas/CanvasFlow.tsx` and `ui/nodes/GraphNode.tsx` and Hotwire console's `lib/editor/DocumentEditor.svelte`: canvas as the main surface, editable node bodies, typed edge handles, floating graph tools. This is an original implementation; no source or build dependency was imported.

Authoring fixtures cover graph preservation, invalid/overlapping membership, cleanup, rename/undo, atomic Frame exposure and binding reuse, and alias/instance isolation. Browser-workspace fixtures (`patch-interactions`, `patch-graph-identity`, `cable-feedback`, `node-workflows`, `creation-tools`, `editor-workspace`) cover cable reasons and reconnection, per-instance cameras/selections, snapshot defaults, storage failures and stale-parent fallback. `npm run test:creation-ui --workspace @tunetoys/web` runs browser checks after a build. Root `npm run check` remains required.

## Implementation

- [apps/web](https://github.com/joshstovall/TuneToys/blob/main/apps/web/README.md) — `PatchUi.tsx`, `PatchCanvas.tsx` and [`patch/`](https://github.com/joshstovall/TuneToys/blob/main/apps/web/src/patch/README.md), `GraphNodes.tsx`, `NodeInspector.tsx`, `PatchCable.tsx`, `PatchCutTool.tsx`, `PatchNote.tsx`, `NodeCopyDrag.tsx`, `EditorTabs.tsx`, `ContextMenu.tsx`, `ShortcutHelp.tsx`
- [packages/authoring](https://github.com/joshstovall/TuneToys/blob/main/packages/authoring/README.md) — `selection.ts`, `frame-controls.ts`, `node-appearance.ts`, `patch-annotations.ts`, grouping operations
- [packages/frame-runtime](https://github.com/joshstovall/TuneToys/blob/main/packages/frame-runtime/README.md) — knob gestures and numeric controls shared with the inspector

## Related

- [Customizable editor workspace](https://docs.tune.toys/contributors/workspace-layout/)
- [App shell, Settings and Library](https://github.com/joshstovall/TuneToys/blob/main/docs/architecture/settings-library.md)
- [Node picker](https://github.com/joshstovall/TuneToys/blob/main/docs/architecture/node-picker.md)
- [Compact resizable nodes and shared interface components](https://docs.tune.toys/contributors/interface-components/)
- [Multichannel audio bundles](https://docs.tune.toys/contributors/multichannel-audio/) — the channel rules cables obey
- [Revisioned authoring operations](https://github.com/joshstovall/TuneToys/blob/main/docs/specs/operations.md)
