OptionalactionDefaults?: {OptionalcloneNode?: (id: NodeId, offset: { dx: number; dy: number }) => { id: NodeId }OptionalduplicateOffset?: { dx: number; dy: number }Per-clone offset for the duplicate default. Default {dx:8,dy:8}.
OptionalnudgeShiftStep?: numberShifted nudge step. Default 10.
OptionalnudgeStep?: numberBase nudge step. Default 1.
Optional Experimentalactions?: ActionsPropOverride / disable / extend the default action set. Resolution rules:
see docs/superpowers/specs/2026-05-09-actions-registry-design.md §D.
Pass null to disable all defaults.
OptionalalphaFor?: (id: string) => numberOptional per-id alpha multiplier for the scene-render slot. When
supplied, each node's draw output is wrapped in a GroupDrawCommand
with the returned alpha so the renderer applies the multiplier.
Values equal to 1 are a no-op (no wrapper emitted). Typical use:
scoping-dim integration dims non-active nodes during a mode transition.
Defaults to () => 1 (no effect).
Optionalambient?: AnyTool[]Always-on tools to register alongside the internal default select.
Use this for wheel/keyboard zoom + pan tools that should run alongside
the default select. If you supply your own tools prop, this is
ignored — wire ambient through your own useTools call instead.
Optionalanimator?: AnimatorOptional animator to bind for per-frame redraws. When supplied,
SceneCanvas subscribes to animator.onTick and requests a redraw on
every active animation frame. This is the supported way to drive
repaints when an animation's effect is read from a non-scene channel
(e.g. a custom drawOne consults animator.colorOverrides) — scene
mutations trigger repaints automatically, but colorOverrides writes
do not.
Omit when no animation channel touches the render pipeline; idle frames don't cost anything if no animations are active (the animator's subscriber list stays quiet).
OptionalbackgroundFill?: FillStyleFillStyle applied to the full canvas behind the scene. Accepts the kit's
FillStyle union (solid / pattern / linear-gradient / radial-gradient /
conic-gradient) so consumers don't have to author a background node
just to colorize the canvas. Rendered as a screen-space layer slotted
before 'scene' — independent of pan / zoom.
Optionalchildren?: ReactNodeChildren rendered alongside the canvas. Useful for siblings that need
the same <ActionsProvider> scope (e.g. shortcuts overlays, probes).
OptionalchromeVisibility?: VisibilityRulesChrome-caps visibility overrides, keyed by chrome id (selection.outline,
selection.rotation-handle, gesture.marquee, …). Each entry is a
composable Condition built from the
cond() builder. Merged on top of the kit's defaultVisibilityRules;
unspecified ids fall through to the defaults.
Set an id to never to suppress a chrome element entirely (also
unhittable). Set to always to force-show. Mix cond(...) chains
(e.g. selectionIs(1).and(focused).andNot(gesturing)) for the in-
between cases.
OptionalcursorCoordsHud?: booleanDev HUD: when true, mounts a fixed-position widget in the top-left of the viewport showing live cursor coords in both viewport (client) and canvas (world) frames. Useful for diagnosing pointer- coord drift / pan-zoom misalignment without instrumenting events.
Optionaldebug?: { slops?: boolean }Dev overlay flags. slops: true renders translucent halos at every
affordance hit zone.
OptionaldefaultTools?: readonly BuiltinToolId[]Which built-in tools SceneCanvas registers in its internal useTools.
Default: ['select', 'rotate'] (plus 'hand' when the
viewport feature is on). Pass a smaller array to slim — e.g.
['select'] for move-only. Wins over toolBundle when both are
passed. Ignored when the consumer supplies their own tools prop.
Optional ExperimentaldescribeKind?: (node: SceneNode<TData, TLayer, TPose>) => string | undefinedOptional resolver: given a scene node, return a short human-readable
"kind" label (e.g. 'rectangle', 'path', 'sticky note'). When
supplied, the kit publishes per-id kinds into any surrounding
<SelectionContextProvider> so non-canvas UI (palette, status bar)
can render type-aware copy. Return undefined to skip an entry.
Default behavior when omitted: containers report 'group', paths
(poses with a kind property) report 'path', everything else is
left unlabelled.
Optionaldevice?: Partial<DeviceProfile>Override detected device facts. Merged over what matchMedia reports;
targetScale is re-derived from the merged coarsePointer unless you
override it explicitly.
Reach for this in three cases: tests that need a coarse profile without
stubbing matchMedia, demos that want to show touch-sized chrome on a
desktop, and hybrid devices where the media query guesses wrong.
OptionalenableGestureDispatcher?: booleanAuto-mount the gesture dispatcher (useGestureDispatcher) inside
<SceneCanvas>. Default true. When false, the dispatcher is not
wired — useful in tests or demos that drive actions through alternative
mechanisms, or that want to call useGestureDispatcher themselves.
The dispatcher reads registered actions' defaultBinding fields and
routes matching window keydown / canvas pointer / wheel events to the
corresponding invoker.run (or invoker.start for ongoing gestures).
OptionalenableKeybindings?: booleanAuto-wire keyboard shortcuts. Default true. When false, SceneCanvas
still mounts its tools but routes no keyboard input to them: it neither
subscribes the legacy useKeybindings hook (tool hotkeys) nor lets the
gesture dispatcher attach its keydown/keyup listeners (modern
keyboard-bound actions like delete / escape / nudge). Pointer, wheel, and
contextmenu interactions are unaffected. Leaves the consumer free to call
useKeybindings(tools, { ... }) themselves (e.g. with disable,
overrides, or defaultTool).
Optionalgeometry?: {OptionalboundsOf?: (id: string) => Bounds | nullOptionalpickEvery?: (worldX: number, worldY: number) => string | string[] | nullHit-test override. Return the topmost id, the full back-to-front hit
stack (string[]), or null for empty space. The stack form lets a
consumer with domain overlap ordering (e.g. children over their
container) feed the kit's pickTopMostHit the true order instead of
pre-collapsing to one id. Matches Canvas's pickEvery shape.
Optionalpicking?: "pose" | "shape"What "the pointer is on this node" means for the default body-pick.
'pose' — the node's pose rect, rotation honored. What every
consumer got before 'shape' existed, and now the opt-out.'shape' (default) — the pose rect as a pre-filter, then the ink the painter
actually lays down: its silhouette (findShapeSilhouette) filled
or not per the painter's ink, plus its outline widened by the
stroke half-width and pickTolerancePx. A click in the concave
notch of a star, in the corner outside an ellipse, or in the blank
half of a text box falls through to whatever is beneath; a click on
the thin outline of an unfilled shape hits it.Painters with no silhouette are unaffected — they keep the pose-rect answer either way, so this can never make a node unreachable.
Ignored when pickEvery is supplied: that override owns the test.
OptionalpickTolerancePx?: numberGrab slop around a shape's outline, in screen pixels. Default 4.
Screen pixels rather than world units so the target keeps its
apparent size at any zoom. It widens the outline test under
picking: 'shape' (a 1px hairline is otherwise a half-world-unit
target, which is unhittable), and it grows the pose-rect pre-filter
so those outline hits survive it. Set 0 for exact geometry.
OptionalgeometryProjection?: GeometryProjectionOptional consumer seam for eager geometry sync: lets pose-transform
actions (move, resize, nudge, flip — NOT rotate) also rewrite a node's
data-held geometry. Given a node and the affine m applied to its pose,
transform(node, m) returns updated data (geometry mapped by m) or
null for nodes with no data-held geometry.
Strictly opt-in: when absent, the kit emits only the pose op and leaves
data untouched. Consumers wire this via geometryProjection={myProjection}.
OptionalgetActiveMode?: () => { allowedCapabilities: ReadonlySet<string>; id: string }Returns the active mode id + the capability tags the mode allows.
Defaults to { id: 'normal', allowedCapabilities: new Set() } when
omitted. Apps using the modality machine should derive this from
modality.machine.registry.current() (mode.id + mode.allows union
with implicit capability tags).
Threading this through enables mode-aware chrome (selection outline, resize handles, rotation handle are off in path-edit mode) and the dispatcher's eligibility filter in later phases.
OptionalgetFocused?: () => booleanOptional live focus getter for chrome-caps' focused ctx field.
SceneCanvas does not own focus state by default — wire this when
your visibility rules read the focused atom (e.g. the kit's
default selection.rotation-handle rule requires focus). Omit
to default focused to true (rule fires regardless of focus).
Optionalingestion?: {External-content ingestion (OS drop / clipboard paste / picker).
handlers are consumer content handlers registered for this canvas's
lifetime (priority 0 by default — they beat the kit's image/* /
image/svg+xml handlers at -100/-90). resolveSrc overrides the
image handler's data:-URI embed (e.g. upload to an asset store,
return the URL). svg.unpack makes the kit SVG handler parse dropped
SVG files into native scene nodes instead of keeping each one a
single embedded-image node.
clipboard configures the kit weasel-JSON paste handler: enabled by
default (pastes of weasel clipboard payloads re-materialize through
this canvas's adapter); reviver restores JSON-unfriendly values the
copying side encoded via jsonReplacer (typed arrays etc.);
enabled: false opts the canvas out — but note the kit handler
still consumes weasel-matching items at match time; on a disabled
canvas it declines inert (a dwarn, nothing ingested) rather than
falling through. Only items that never match (non-weasel text)
flow on to other handlers.
Memoize handlers (useState/useMemo/module const) — an inline array
literal re-registers the handlers on every render. The same applies
to clipboard.reviver: an inline function identity-churns the
memoized clipboard ctx each render (harmless but wasteful).
OptionalinitialActiveTool?: stringInitial active-slot tool id. Default: 'select'. Must be one of the
registered tools (via defaultTools / toolBundle). Useful for
demos / consumers that want to land on a non-select tool — e.g. the
lasso demo starts with initialActiveTool="lasso". Ignored when the
consumer supplies their own tools prop.
OptionalinsertNodeFactories?: Record<string, InsertNodeFactory>Consumer node factories for the insert action, keyed by tool kind.
Each factory receives the drag AABB + tool extras and returns the
node's data (in this canvas's own data shape) plus an optional pose.
A factory for a kit kind (rect, line, …) replaces the kit's default
{ path, fill } node for that kind; a factory for a novel kind (e.g.
text) adds insert support the kit doesn't ship. The dep supplies id,
layer, and the undoable op. Return null to reject an insert.
OptionalinsertTool?: {OptionalisPointerInteractive?: (id: string) => booleanOptional per-id pointer-interactivity predicate. When supplied, ids
for which the predicate returns false are excluded from hit-test
results — getNodeAtPoint returns null for those positions.
Typical use: scoping-dim integration suppresses pointer events for
non-active nodes during a mode transition.
Defaults to () => true (all nodes are interactive).
Optionallayers?: LayersMap<SceneNode<TData, TLayer, TPose>, TPose>Layer configuration. When omitted, SceneCanvas applies kit defaults
(a scene slot that paints node.data.color rects + a default
selection overlay). Partial slot configs deep-merge with the
defaults; pass slot: null to suppress a default explicitly.
Optionallayouts?: SceneToAdapterOptions<TData, TLayer, TPose>["layouts"]Layout strategies keyed by container node id (or a resolver). Forwarded
to sceneToAdapter so useMove's layout pass runs on configured
containers (reflow on enter, reparent + reflow on commit).
OptionalmodalityHud?: boolean | { modeId?: string }Dev HUD: when true (or object), mounts a fixed-position widget below
the pick HUD showing the active modality mode, active-slot tool, and
hotkey stack. Pass { modeId } to populate the mode line.
OptionalonDoubleClick?: (hit: SceneCanvasHit | null) => voidCalled when the user double-clicks the canvas. Receives the hit node
(id + kind) at the double-click position, or null when the click
lands on empty canvas. Wired internally via a dblclick listener on
the canvas element so it doesn't interfere with the pointer-gesture
pipeline.
Typical use: modality dispatch — enter path-edit on a path node, isolation on a group, text-edit on a text node.
OptionalonToolsCreated?: (tools: ToolsApi) => voidCalled once after SceneCanvas constructs (or receives) its
ToolsApi. Useful for introspection — e.g. the toolkit-builder
dev surface walks tools.registry to render the live route table.
Fires with the consumer-supplied tools prop when present, or with
the internally-synthesized one otherwise.
OptionalpickHud?: booleanDev HUD: when true, mounts a fixed-position widget just below the
cursor-coords HUD listing the ids returned by pickEvery(world)
under the cursor. Useful for diagnosing hit-test order and
container/leaf overlap during select-tool work.
Optionalrouting?: readonly NodeRoutingEntry[]Routing-trait classifiers — list of NodeRoutingEntry entries. The kit
constructs a NodeRouting registry per-<SceneCanvas> from this prop,
then uses the resulting classifier to derive each hit's kind when
building getNodeAtPoint. Tool routing tables (e.g.
{ target: 'rect', actionId: 'move' }) match against the produced
kind strings.
Pass defaultNodeRouting to pick up the kit's built-in shape kinds
(rect, ellipse, polygon, …) for data: { kind: '<shape>' } nodes.
Spread additional entries for consumer-defined kinds.
See docs/superpowers/specs/2026-05-24-node-traits-reframe-design.md.
Memoize the routing value. The kit memoizes the registry on the
prop's reference identity. Passing a fresh array each render
(e.g. routing={[...defaultNodeRouting, custom]} inline) rebuilds the
registry and cascades into a new adapter, churning gesture state.
Define the list as a module-level constant, or wrap it in useMemo.
(defaultNodeRouting alone is a stable module-level constant; spreading
it with extras is what needs the memo.)
A Scene (typically from useScene) — or a SerializedScene
JSON object, which SceneCanvas bakes into a Scene internally on
first render. The serialized form is read once; subsequent
changes to the prop are ignored. Pass a key prop on
<SceneCanvas> to force a fresh canvas from updated JSON.
The accepted JSON shape is intentionally relaxed (version: number rather than 1 literal) so a import json from './x.json'
result satisfies the type without an as cast — Vite infers
number-literal types as number from JSON, which the strict
SerializedScene<…> would reject.
Optionalselection?: SelectionApiOptionalselectionMode?: CanvasSelectionModeHigh-level selection semantics. Controls whether canvas interactions mutate selection and whether multi-select chrome (union AABB) activates.
'single' (default) — click selects one id.'multi' — shift-click extends/toggles.'none' — canvas interactions never update selection.
See CanvasSelectionMode.OptionalselectionOptions?: UseSelectionOptionsOptionalselectTool?: {OptionalhandleHitRadius?: numberOptionalmove?: UseMoveOptions<TPose>OptionalpickBest?: (Override the body-pick used on click/pointerdown. Alt-aware: receives the live alt state + current selection so consumers can implement alt-cycling through an overlapping stack. Default: top-most hit (alt ignored).
Optionalresize?: UseResizeOptions<TPose>Optionalrotate?: UseRotateOptions<TPose> | falseRotation options, or false to disable rotation entirely — drops the
rotate action AND hides the selection rotation-handle chrome, so a
consumer whose objects don't rotate (e.g. a floor-plan / garden
editor) opts out with a single switch instead of pairing
actions={{ rotate: null }} with a chromeVisibility override.
Optionalsnap?: SnapStrategy<TPose>Optionalshaders?: ShaderProgramHandle[]Custom shader programs to compile on the renderer. Forwarded directly
to <Canvas shaders={...} />. See CanvasProps.shaders for details.
OptionaltoolBundle?: ToolBundleNamed preset for the built-in tool set: 'minimal' (select + hand),
'standard' (select + rotate + hand + rect + ellipse +
line + pencil), or 'exhaustive' (every built-in including polygon,
star, lasso, text, clone). When set, defines the starting set;
defaultTools (if also passed) overrides it. Ignored when the
consumer supplies their own tools prop.
OptionaltoolOptions?: BuiltinToolOptionsPer-tool option overrides for the built-in shape/lasso/clone tools.
Each entry is a narrow subset of the underlying hook's options
surface — lasso.mode, clone.cloneSelection, etc.
Optionaltools?: ToolsApi | Record<string, AnyTool | true | false>Extra tools or overrides keyed by id, or a full ToolsApi takeover.
Patch form (Record<string, AnyTool | true | false>): merged
into the built-in registry on top of whatever defaultTools /
toolBundle already selected.
true pulls in the built-in for this id ('pen', 'lasso',
'rotate', …) even when it's outside the active tier — useful
for toolBundle: 'minimal' + tools={{ pen: true }}. Unknown
built-in ids warn in dev and are ignored.AnyTool adds a new id or replaces an existing one
(dev-only warning on replace).false omits a bundled tool entirely.
Auto-wiring (keybindings, dispatcher, action registry) still runs.Takeover form (ToolsApi): the internal default useSelectTool
is bypassed and this tools value is forwarded to Canvas as-is —
the consumer owns active-slot management. Keybindings are still
auto-wired against the supplied registry; pass enableKeybindings={false}
to opt out.
Optionalviewport?: {Viewport feature wiring.
inertia, pinchZoom, animatedZoom are opt-in: pass true
for defaults or an object to tune. Omitted means off.pan (wheel pan) and zoom (Cmd+wheel + Cmd+=/-/0) are opt-OUT:
on by default; pass false to disable. They are wired by registering
the kit's viewport.pan / viewport.zoom action descriptors with
the actions registry — disabling via the actions prop
(actions: { 'viewport.wheelPan': null }) also works and runs after this.When omitted entirely, no hand/pinch tools are registered but the
default wheel pan + Cmd+wheel/key zoom remain wired (canvas-first
default). Pass { pan: false, zoom: false } to opt out entirely.
OptionalanimatedZoom?: Optionalinertia?: Optionalpan?: booleanOptionalpinchZoom?: boolean | { max?: number; min?: number }Optionalrecenter?: () => voidCallback invoked by Cmd-0 (viewport.zoom action's reset branch).
When supplied, replaces the default reset-to-identity behavior —
consumers typically refit the document page into the workspace
via fitViewToBounds. The callback owns its own bounds + host
dims and dispatches the resulting view via onViewChange.
Optionalzoom?: boolean | ViewportZoomOptionsWheel/keyboard zoom. true/omitted = default Cmd+wheel zoom with the
kit's 0.1–8 clamp; false disables. Pass a ViewportZoomOptions
object to bind zoom to plain wheel (wheel: 'plain', pair with
pan: false) and/or set min/max scale clamps.
Deprecated
unused after legacy-bridge removal; will be deleted