Layer System

How rendering layers are stacked and managed.

Overview

The canvas is rendered as a stack of layers, each responsible for a different visual concern. Plugins register layers to participate in rendering.

Layer Interface

interface Layer {
  id: string;
  order: number;
  render: (ctx: LayerRenderContext) => ReactElement | null;
  interactable?: boolean;
  fixed?: boolean;
}
FieldDescription
idUnique layer identifier.
orderRendering order. Lower values render behind higher values.
renderReturns JSX for this layer. Receives the current render context.
interactableReserved for future use. Intended to control whether the layer receives pointer events.
fixedIf true, the layer is not affected by viewport pan/zoom.

LayerRenderContext

interface LayerRenderContext {
  viewport: Viewport;                       // Current pan & zoom
  shapes: ReadonlyMap<string, ShapeData>;   // All shapes on the board
  shapesSorted: readonly ShapeData[];       // Shapes sorted by zIndex (back to front)
  selection: ReadonlySet<string>;           // Currently selected shape IDs
  hoveredShapeId: string | null;            // Id of the hovered shape, or null
  theme: Theme;                             // Active theme
  renderMode: RenderMode;                   // Current LOD render mode
  viewportBounds: BoundingBox;              // Visible region in world coords (per-shape viewport LOD/culling)
}

Standard Layer Stack

The typical rendering order:

OrderLayerSource
Canvas background (set via container CSS)Core
10Background (grid, dots)Plugin
50Shapes (main content)Core
80UI (selection handles, resize grips)Plugin
85Guides (snap lines, alignment)Plugin
100Transient (cursors, effects)Core

LayerManager API

interface LayerManager {
  register(layer: Layer): void;
  unregister(layerId: string): void;
  getLayers(): readonly Layer[];  // Sorted by order
}

Example: Background Grid

setup(ctx: PluginContext) {
  ctx.layers.register({
    id: "bg-grid",
    order: 10,
    render: (renderCtx) => (
      <GridPattern viewport={renderCtx.viewport} />
    ),
  });
}

Example: Selection Handles

setup(ctx: PluginContext) {
  ctx.layers.register({
    id: "select-handles",
    order: 80,
    fixed: true,
    render: (renderCtx) => (
      <SelectionHandles
        shapes={renderCtx.shapes}
        selection={renderCtx.selection}
        viewport={renderCtx.viewport}
      />
    ),
  });
}

Replacing the selection UI

The default selection UI (handles, bounding box, marquee) is not registered through ctx.layers directly — it is mounted by a separate, priority-aware registry so apps and plugins can swap it out cleanly. See Selection Foreground for the replacement API.