Launch OpenGPEX

Editor Overview

A developer-oriented tour of the OpenGPEX v2 workspace — how the WebGPU editor is assembled, how panels negotiate space, and where every visible surface comes from.


Entry Chain

The editor mounts through a fully client-side chain (SSR is disabled because the component tree depends on Workers, Canvas, and IndexedDB):

src/app/page.tsx
  └─ EditorClient
       └─ dynamic(() => import("@opengpex/editor/workspace"), { ssr: false })
            └─ <EditorProvider><WorkspaceController /></EditorProvider>
                 └─ Workspace.tsx        (the real workspace assembly)

Path alias: @opengpex/editor/* → ./src/lib/opengpex/*.


Boot Lifecycle

useEditorStatus derives two global states: BOOTING (!state.isLoaded) and READY. Under the hood, EditorProvider.init() walks five stages:

Stage Behavior
checking Detects existing State_V1 / State_V2 records
migrating Runs v1→v2 migration + healing (installs a beforeunload guard)
restoring storage.restore() races a RESTORE_TIMEOUT_MS (8s) timer
finalizing Dispatches HYDRATE(savedState), or SET_LOADED for a fresh session (a timed-out/failed restore also raises the core.restoreTimedOut signal, blocking auto-save)
READY fonts.hydrate() fires fire-and-forget; advanced command set is dynamically imported

Boot progress is written to state.interaction.signals via actions.setStateSignal('core.bootStage', ...) — signals never enter persistence. While BOOTING, a FancyOverlay (z = POPOVER + 10) shows fixed copy plus a per-stage subtitle; it exits with a pure CSS animation and is unmounted on onAnimationEnd.


Screen Layout

The v2 workspace is a floating layout — sidebars overlay the canvas instead of squeezing it:

┌────────────────────────────────────────────────────────────────┐
│  ToolMenu (pinned 44px rail or floating, top-left)   ExtendButton (top-right, XTEND_SLOT)
│ ┌──┐        ┌──────────────────────────────────┐        ┌──┐  │
│ │ L│        │  OptionBar (top pill, 48px)       │        │ R│  │
│ │ e│  left  │                                  │  right │ e│  │
│ │ f│  drawer│   Viewport                        │  drawer│ f│  │
│ │ t│  320px │   CanvasBackdrop / CanvasStage    │  320px │ t│  │
│ │  │        │   ViewportHUD (4 corners) + DOCK  │        │  │  │
│ └──┘        └──────────────────────────────────┘        └──┘  │
│  GlobalUI (HUD toasts, confirm/choice dialogs, portal root)    │

Geometry constants live in workspace/Workspace.styles.ts:

Constant Value Purpose
TOOLBAR_WIDTH / DRAWER_BAR_WIDTH 40px Icon rails on both edges
SIDEBAR_WIDTH 320px Floating drawer width (plugins may override via preferredWidth)
HEADER_HEIGHT 48px OptionBar height
TOOL_MENU_WIDTH 44px Pinned ToolMenu rail

Layout Negotiation

Floating UI and the canvas reconcile through workspace/LayoutContext.tsx:

  1. Regions call registerSlot({ id, role, width?, height? }) with roles LEFT_PUSH | RIGHT_PUSH | TOP_PUSH | BOTTOM_PUSH | OVERLAY.
  2. Per-side maxima produce a safeRect; a IDLE → MEASURING → STABLE state machine marks stability after 300ms of silence.
  3. useLayoutSync writes the stable safeRect back to Redux (theme.config.insets), which is injected as --v-offset-* CSS variables consumed by corner HUDs and overlays.

Plugin Slots

Nearly every visible region is a plugin slot. Plugins are self-registering modules (no prop assembly); the core registry is generated by pnpm sync-plugins. Key slots:

Slot Screen location Examples
SIDE_BAR Floating left/right drawers Layers, Adjustment, Craft, ImageInfo, AnimatedImages, AITools
OPTION_BAR Top pill, switches with active tool ColorOptions, ClipOptions, ViewportOptions
TOOL_MENU / TOOL_SETTINGS ToolMenu panel and submenu SmartGuides, SettingsPanel, StorageInfoPanel
XTEND_SLOT ExtendButton popover (top right) CloudMenu
DOCK Viewport-area overlay, frame-lifecycle independent TabDock
TL/TR/BL/BR Viewport corners Zoom controls
VIEWPORT_OVERLAY Above the canvas transform SmartGuides
STAGE_OVERLAY Tool interaction overlays Brush, Eraser, Text, Clip, LayerOverlay
STAGE_GIZMOS Inside the artboard clip div (moves with canvas) In-canvas gizmos
ROOT_OVERLAY Workspace root FileLoader, Onboarding
HIDDEN Background services FontLoader, TimeTraveler

Slot rendering (PluginSlot.tsx) collects by slot + contributions, orders by order (multiples of 100 insert separators), wraps in PluginErrorBoundary + Suspense, and gates visibility via plugins.isPluginVisible(p, { hasActiveFrame }). ?safe-mode=true loads core plugins only.


Viewport Stack

Inside ViewportInner (bottom to top, all inside a reveal gate):

  1. CanvasBackdrop — checkerboard / rotated background
  2. Stage transform div → artboard clip div → STAGE_GIZMOS slot
  3. CanvasStage — the WebGPU canvas plane (z = 5, pointer-events none, driven by useFastSync)
  4. STAGE_OVERLAY slot — tool interaction layers

There are no scrollbars: pan/zoom is a camera model (useViewportScroll, useCameraInit). First paint is reveal-gated on imagesLoaded and engine.onFirstPaint (each with a 1500ms fallback) so the viewport never shows an empty shell.


Z-Index Policy

Single source of truth: core/helpers/config.ts.

EDITOR_Z_INDEX = {
  STAGE: { BACKDROP: 0, CONTENT: 100, GIZMOS: 1000, SYSTEM_TOOLS: 2000, DEVELOPER_ZONE: 3000 },
  UI:    { WORKSPACE_BASE: 2000, OVERLAY: 4000, POPOVER: 5000, MODAL: 6000, TOOLTIP: 10100 },
}

New floating UI must derive from EDITOR_Z_INDEX plus a small offset — never introduce an independent magnitude.


State and Context Supply

EditorProvider splits context in two:

  • EditorStateContext — state, activeFrame, activeLayer; new object per change, subscribers re-render.
  • EditorServiceContext — actions and singleton services (store, geometry, assets, WorkerBridge, files, pixelFacade, layers, plugins, stateStorage, clipboard, fonts); referentially stable.

Common hooks: useEditorServices (per-plugin scoped executeCommand), useEditorState, usePluginSignals, usePluginConfig, and useVolatileInteraction (a useSyncExternalStore fast path that bypasses reducer-wide updates for high-frequency interaction state).


GlobalUI

GlobalUI mounts unconditionally at the workspace root and owns:

  • EditorHUD — the single error feedback channel for the entire product (all command rejections converge here; error toasts auto-clear after 1200ms)
  • FancyConfirm / FancyChoice dialogs driven by state.confirm / state.choice
  • The portal root (#editor-portal-root, z = POPOVER + 100)

Last updated: 2026-10-12