weasel API - v0.8.0
    Preparing search index...

    Variable SceneCanvasConst

    SceneCanvas: <TData, TLayer extends string, TPose>(
        props: Omit<
            CanvasProps<SceneNode<TData, TLayer, TPose>, TPose>,

                | "selection"
                | "snap"
                | "adapter"
                | "moveOptions"
                | "resizeOptions"
                | "rotateOptions"
                | "pickEvery"
                | "boundsOf"
                | "handleHitRadius"
                | "selectionOptions"
                | "tools"
                | "geometry"
                | "layers"
                | "onBackgroundClick"
                | "getIsVisible",
        > & {
            actionDefaults?: {
                cloneNode?: (
                    id: NodeId,
                    offset: { dx: number; dy: number },
                ) => { id: NodeId };
                duplicateOffset?: { dx: number; dy: number };
                nudgeShiftStep?: number;
                nudgeStep?: number;
            };
            actions?: ActionsProp;
            alphaFor?: (id: string) => number;
            ambient?: AnyTool[];
            animator?: Animator;
            backgroundFill?: FillStyle;
            children?: ReactNode;
            chromeVisibility?: Partial<Record<ChromeId, Rule | Condition>>;
            cursorCoordsHud?: boolean;
            debug?: { slops?: boolean };
            defaultTools?: readonly BuiltinToolId[];
            describeKind?: (
                node: SceneNode<TData, TLayer, TPose>,
            ) => string | undefined;
            device?: Partial<DeviceProfile>;
            enableGestureDispatcher?: boolean;
            enableKeybindings?: boolean;
            geometry?: {
                boundsOf?: (id: string) => Bounds | null;
                pickEvery?: (worldX: number, worldY: number) => string | string[] | null;
                picking?: "pose" | "shape";
                pickTolerancePx?: number;
            };
            geometryProjection?: GeometryProjection;
            getActiveMode?: () => {
                allowedCapabilities: ReadonlySet<string>;
                id: string;
            };
            getFocused?: () => boolean;
            ingestion?: {
                clipboard?: {
                    enabled?: boolean;
                    reviver?: (key: string, value: unknown) => unknown;
                };
                handlers?: ContentHandlerEntry[];
                resolveSrc?: (file: File) => Promise<string>;
                svg?: SvgIngestOptions;
            };
            initialActiveTool?: string;
            insertNodeFactories?: Record<string, InsertNodeFactory>;
            insertTool?: {
                create:
                    | (
                        (bounds: Bounds) => { data: TData; id?: string; pose: TPose } | null
                    )
                    | undefined;
                layer?: TLayer;
            };
            isPointerInteractive?: (id: string) => boolean;
            layers?: LayersMap<SceneNode<TData, TLayer, TPose>, TPose>;
            layouts?:
                | Record<string, LayoutStrategy<TPose>>
                | ((containerId: string) => LayoutStrategy<TPose> | null);
            modalityHud?: boolean | { modeId?: string };
            onDoubleClick?: (hit: SceneCanvasHit | null) => void;
            onToolsCreated?: (tools: ToolsApi) => void;
            pickHud?: boolean;
            routing?: readonly NodeRoutingEntry[];
            scene:
                | {
                    nodes: readonly unknown[];
                    systemLayers?: readonly { id: string }[];
                    version: number;
                }
                | Scene<TData, TLayer, TPose>
                | SerializedScene<TData, TLayer, TPose>;
            selection?: SelectionApi;
            selectionMode?: CanvasSelectionMode;
            selectionOptions?: UseSelectionOptions;
            selectTool?: {
                handleHitRadius?: number;
                move?: UseMoveOptions<TPose>;
                pickBest?: (
                    worldX: number,
                    worldY: number,
                    alt: boolean,
                    sel: readonly string[],
                ) => string | null;
                resize?: UseResizeOptions<TPose>;
                rotate?: false | UseRotateOptions<TPose>;
                snap?: SnapStrategy<TPose>;
            };
            shaders?: ShaderProgramHandle[];
            toolBundle?: ToolBundle;
            toolOptions?: BuiltinToolOptions;
            tools?: ToolsApi
            | Record<string, boolean | AnyTool>;
            viewport?: {
                animatedZoom?:
                    | boolean
                    | {
                        duration?: number;
                        easing?: (t: number) => number;
                        resetDuration?: number;
                    };
                inertia?: | boolean
                | {
                    boundary?: "stop"
                    | "bounce"
                    | "spring";
                    bounds?: PanBounds;
                    friction?: number;
                    minSpeed?: number;
                };
                pan?: boolean;
                pinchZoom?: boolean
                | { max?: number; min?: number };
                recenter?: () => void;
                zoom?: boolean | ViewportZoomOptions;
            };
        } & { ref?: Ref<SceneCanvasApi> },
    ) => Element = ...

    Type Declaration

      • <TData, TLayer extends string, TPose>(
            props: Omit<
                CanvasProps<SceneNode<TData, TLayer, TPose>, TPose>,

                    | "selection"
                    | "snap"
                    | "adapter"
                    | "moveOptions"
                    | "resizeOptions"
                    | "rotateOptions"
                    | "pickEvery"
                    | "boundsOf"
                    | "handleHitRadius"
                    | "selectionOptions"
                    | "tools"
                    | "geometry"
                    | "layers"
                    | "onBackgroundClick"
                    | "getIsVisible",
            > & {
                actionDefaults?: {
                    cloneNode?: (
                        id: NodeId,
                        offset: { dx: number; dy: number },
                    ) => { id: NodeId };
                    duplicateOffset?: { dx: number; dy: number };
                    nudgeShiftStep?: number;
                    nudgeStep?: number;
                };
                actions?: ActionsProp;
                alphaFor?: (id: string) => number;
                ambient?: AnyTool[];
                animator?: Animator;
                backgroundFill?: FillStyle;
                children?: ReactNode;
                chromeVisibility?: Partial<Record<ChromeId, Rule | Condition>>;
                cursorCoordsHud?: boolean;
                debug?: { slops?: boolean };
                defaultTools?: readonly BuiltinToolId[];
                describeKind?: (
                    node: SceneNode<TData, TLayer, TPose>,
                ) => string | undefined;
                device?: Partial<DeviceProfile>;
                enableGestureDispatcher?: boolean;
                enableKeybindings?: boolean;
                geometry?: {
                    boundsOf?: (id: string) => Bounds | null;
                    pickEvery?: (worldX: number, worldY: number) => string | string[] | null;
                    picking?: "pose" | "shape";
                    pickTolerancePx?: number;
                };
                geometryProjection?: GeometryProjection;
                getActiveMode?: () => {
                    allowedCapabilities: ReadonlySet<string>;
                    id: string;
                };
                getFocused?: () => boolean;
                ingestion?: {
                    clipboard?: {
                        enabled?: boolean;
                        reviver?: (key: string, value: unknown) => unknown;
                    };
                    handlers?: ContentHandlerEntry[];
                    resolveSrc?: (file: File) => Promise<string>;
                    svg?: SvgIngestOptions;
                };
                initialActiveTool?: string;
                insertNodeFactories?: Record<string, InsertNodeFactory>;
                insertTool?: {
                    create:
                        | (
                            (bounds: Bounds) => { data: TData; id?: string; pose: TPose } | null
                        )
                        | undefined;
                    layer?: TLayer;
                };
                isPointerInteractive?: (id: string) => boolean;
                layers?: LayersMap<SceneNode<TData, TLayer, TPose>, TPose>;
                layouts?:
                    | Record<string, LayoutStrategy<TPose>>
                    | ((containerId: string) => LayoutStrategy<TPose> | null);
                modalityHud?: boolean | { modeId?: string };
                onDoubleClick?: (hit: SceneCanvasHit | null) => void;
                onToolsCreated?: (tools: ToolsApi) => void;
                pickHud?: boolean;
                routing?: readonly NodeRoutingEntry[];
                scene:
                    | {
                        nodes: readonly unknown[];
                        systemLayers?: readonly { id: string }[];
                        version: number;
                    }
                    | Scene<TData, TLayer, TPose>
                    | SerializedScene<TData, TLayer, TPose>;
                selection?: SelectionApi;
                selectionMode?: CanvasSelectionMode;
                selectionOptions?: UseSelectionOptions;
                selectTool?: {
                    handleHitRadius?: number;
                    move?: UseMoveOptions<TPose>;
                    pickBest?: (
                        worldX: number,
                        worldY: number,
                        alt: boolean,
                        sel: readonly string[],
                    ) => string | null;
                    resize?: UseResizeOptions<TPose>;
                    rotate?: false | UseRotateOptions<TPose>;
                    snap?: SnapStrategy<TPose>;
                };
                shaders?: ShaderProgramHandle[];
                toolBundle?: ToolBundle;
                toolOptions?: BuiltinToolOptions;
                tools?: ToolsApi
                | Record<string, boolean | AnyTool>;
                viewport?: {
                    animatedZoom?:
                        | boolean
                        | {
                            duration?: number;
                            easing?: (t: number) => number;
                            resetDuration?: number;
                        };
                    inertia?: | boolean
                    | {
                        boundary?: "stop"
                        | "bounce"
                        | "spring";
                        bounds?: PanBounds;
                        friction?: number;
                        minSpeed?: number;
                    };
                    pan?: boolean;
                    pinchZoom?: boolean
                    | { max?: number; min?: number };
                    recenter?: () => void;
                    zoom?: boolean | ViewportZoomOptions;
                };
            } & { ref?: Ref<SceneCanvasApi> },
        ): Element
      • Type Parameters

        • TData
        • TLayer extends string
        • TPose

        Parameters

        • props: Omit<
              CanvasProps<SceneNode<TData, TLayer, TPose>, TPose>,

                  | "selection"
                  | "snap"
                  | "adapter"
                  | "moveOptions"
                  | "resizeOptions"
                  | "rotateOptions"
                  | "pickEvery"
                  | "boundsOf"
                  | "handleHitRadius"
                  | "selectionOptions"
                  | "tools"
                  | "geometry"
                  | "layers"
                  | "onBackgroundClick"
                  | "getIsVisible",
          > & {
              actionDefaults?: {
                  cloneNode?: (
                      id: NodeId,
                      offset: { dx: number; dy: number },
                  ) => { id: NodeId };
                  duplicateOffset?: { dx: number; dy: number };
                  nudgeShiftStep?: number;
                  nudgeStep?: number;
              };
              actions?: ActionsProp;
              alphaFor?: (id: string) => number;
              ambient?: AnyTool[];
              animator?: Animator;
              backgroundFill?: FillStyle;
              children?: ReactNode;
              chromeVisibility?: Partial<Record<ChromeId, Rule | Condition>>;
              cursorCoordsHud?: boolean;
              debug?: { slops?: boolean };
              defaultTools?: readonly BuiltinToolId[];
              describeKind?: (
                  node: SceneNode<TData, TLayer, TPose>,
              ) => string | undefined;
              device?: Partial<DeviceProfile>;
              enableGestureDispatcher?: boolean;
              enableKeybindings?: boolean;
              geometry?: {
                  boundsOf?: (id: string) => Bounds | null;
                  pickEvery?: (worldX: number, worldY: number) => string | string[] | null;
                  picking?: "pose" | "shape";
                  pickTolerancePx?: number;
              };
              geometryProjection?: GeometryProjection;
              getActiveMode?: () => {
                  allowedCapabilities: ReadonlySet<string>;
                  id: string;
              };
              getFocused?: () => boolean;
              ingestion?: {
                  clipboard?: {
                      enabled?: boolean;
                      reviver?: (key: string, value: unknown) => unknown;
                  };
                  handlers?: ContentHandlerEntry[];
                  resolveSrc?: (file: File) => Promise<string>;
                  svg?: SvgIngestOptions;
              };
              initialActiveTool?: string;
              insertNodeFactories?: Record<string, InsertNodeFactory>;
              insertTool?: {
                  create:
                      | (
                          (bounds: Bounds) => { data: TData; id?: string; pose: TPose } | null
                      )
                      | undefined;
                  layer?: TLayer;
              };
              isPointerInteractive?: (id: string) => boolean;
              layers?: LayersMap<SceneNode<TData, TLayer, TPose>, TPose>;
              layouts?:
                  | Record<string, LayoutStrategy<TPose>>
                  | ((containerId: string) => LayoutStrategy<TPose> | null);
              modalityHud?: boolean | { modeId?: string };
              onDoubleClick?: (hit: SceneCanvasHit | null) => void;
              onToolsCreated?: (tools: ToolsApi) => void;
              pickHud?: boolean;
              routing?: readonly NodeRoutingEntry[];
              scene:
                  | {
                      nodes: readonly unknown[];
                      systemLayers?: readonly { id: string }[];
                      version: number;
                  }
                  | Scene<TData, TLayer, TPose>
                  | SerializedScene<TData, TLayer, TPose>;
              selection?: SelectionApi;
              selectionMode?: CanvasSelectionMode;
              selectionOptions?: UseSelectionOptions;
              selectTool?: {
                  handleHitRadius?: number;
                  move?: UseMoveOptions<TPose>;
                  pickBest?: (
                      worldX: number,
                      worldY: number,
                      alt: boolean,
                      sel: readonly string[],
                  ) => string | null;
                  resize?: UseResizeOptions<TPose>;
                  rotate?: false | UseRotateOptions<TPose>;
                  snap?: SnapStrategy<TPose>;
              };
              shaders?: ShaderProgramHandle[];
              toolBundle?: ToolBundle;
              toolOptions?: BuiltinToolOptions;
              tools?: ToolsApi
              | Record<string, boolean | AnyTool>;
              viewport?: {
                  animatedZoom?:
                      | boolean
                      | {
                          duration?: number;
                          easing?: (t: number) => number;
                          resetDuration?: number;
                      };
                  inertia?: | boolean
                  | {
                      boundary?: "stop"
                      | "bounce"
                      | "spring";
                      bounds?: PanBounds;
                      friction?: number;
                      minSpeed?: number;
                  };
                  pan?: boolean;
                  pinchZoom?: boolean
                  | { max?: number; min?: number };
                  recenter?: () => void;
                  zoom?: boolean | ViewportZoomOptions;
              };
          } & { ref?: Ref<SceneCanvasApi> }
          • OptionalactionDefaults?: {
                cloneNode?: (
                    id: NodeId,
                    offset: { dx: number; dy: number },
                ) => { id: NodeId };
                duplicateOffset?: { dx: number; dy: number };
                nudgeShiftStep?: number;
                nudgeStep?: number;
            }

            unused after legacy-bridge removal; will be deleted

            • 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?: number

              Shifted nudge step. Default 10.

            • OptionalnudgeStep?: number

              Base nudge step. Default 1.

          • Optional Experimentalactions?: ActionsProp

            Override / 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) => number

            Optional 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?: Animator

            Optional 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?: FillStyle

            FillStyle 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?: ReactNode

            Children rendered alongside the canvas. Useful for siblings that need the same <ActionsProvider> scope (e.g. shortcuts overlays, probes).

          • OptionalchromeVisibility?: Partial<Record<ChromeId, Rule | Condition>>

            Chrome-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?: boolean

            Dev 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 | undefined

            Optional 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?: boolean

            Auto-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?: boolean

            Auto-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?: {
                boundsOf?: (id: string) => Bounds | null;
                pickEvery?: (worldX: number, worldY: number) => string | string[] | null;
                picking?: "pose" | "shape";
                pickTolerancePx?: number;
            }
            • OptionalboundsOf?: (id: string) => Bounds | null
            • OptionalpickEvery?: (worldX: number, worldY: number) => string | string[] | null

              Hit-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?: number

              Grab 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?: GeometryProjection

            Optional 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}.

            GeometryProjection

          • 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?: () => boolean

            Optional 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?: {
                clipboard?: {
                    enabled?: boolean;
                    reviver?: (key: string, value: unknown) => unknown;
                };
                handlers?: ContentHandlerEntry[];
                resolveSrc?: (file: File) => Promise<string>;
                svg?: SvgIngestOptions;
            }

            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?: string

            Initial 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?: {
                create:
                    | (
                        (bounds: Bounds) => { data: TData; id?: string; pose: TPose } | null
                    )
                    | undefined;
                layer?: TLayer;
            }
          • OptionalisPointerInteractive?: (id: string) => boolean

            Optional 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?:
                | Record<string, LayoutStrategy<TPose>>
                | ((containerId: string) => LayoutStrategy<TPose> | null)

            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) => void

            Called 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) => void

            Called 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?: boolean

            Dev 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.)

          • scene:
                | {
                    nodes: readonly unknown[];
                    systemLayers?: readonly { id: string }[];
                    version: number;
                }
                | Scene<TData, TLayer, TPose>
                | SerializedScene<TData, TLayer, TPose>

            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?: SelectionApi
          • OptionalselectionMode?: CanvasSelectionMode

            High-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?: UseSelectionOptions
          • OptionalselectTool?: {
                handleHitRadius?: number;
                move?: UseMoveOptions<TPose>;
                pickBest?: (
                    worldX: number,
                    worldY: number,
                    alt: boolean,
                    sel: readonly string[],
                ) => string | null;
                resize?: UseResizeOptions<TPose>;
                rotate?: false | UseRotateOptions<TPose>;
                snap?: SnapStrategy<TPose>;
            }
            • OptionalhandleHitRadius?: number
            • Optionalmove?: UseMoveOptions<TPose>
            • OptionalpickBest?: (
                  worldX: number,
                  worldY: number,
                  alt: boolean,
                  sel: readonly string[],
              ) => string | null

              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?: false | UseRotateOptions<TPose>

              Rotation 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?: ToolBundle

            Named 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?: BuiltinToolOptions

            Per-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, boolean | AnyTool>

            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?: {
                animatedZoom?:
                    | boolean
                    | {
                        duration?: number;
                        easing?: (t: number) => number;
                        resetDuration?: number;
                    };
                inertia?: | boolean
                | {
                    boundary?: "stop"
                    | "bounce"
                    | "spring";
                    bounds?: PanBounds;
                    friction?: number;
                    minSpeed?: number;
                };
                pan?: boolean;
                pinchZoom?: boolean
                | { max?: number; min?: number };
                recenter?: () => void;
                zoom?: boolean | ViewportZoomOptions;
            }

            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?:
                  | boolean
                  | {
                      duration?: number;
                      easing?: (t: number) => number;
                      resetDuration?: number;
                  }
            • Optionalinertia?:
                  | boolean
                  | {
                      boundary?: "stop"
                      | "bounce"
                      | "spring";
                      bounds?: PanBounds;
                      friction?: number;
                      minSpeed?: number;
                  }
            • Optionalpan?: boolean
            • OptionalpinchZoom?: boolean | { max?: number; min?: number }
            • Optionalrecenter?: () => void

              Callback 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 | ViewportZoomOptions

              Wheel/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.

        Returns Element