Files
editor/wiki/architecture/systems.md
T
de13119f86 docs(wiki): vertical-model architecture page; drop stale SlabSystem rows (#528)
Documents the shipped vertical building model (#526): stored truth table,
resolution helpers, clamp rules, pointer-decided placement, the load
migration that lives in migrateNodes indefinitely, and the gotchas. The
systems tables stop listing the deleted SlabSystem and point at the
registry geometry path instead.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 14:44:03 -04:00

4.4 KiB

Systems

Core and viewer systems architecture.

Applies to: packages/core/src/systems/**, packages/viewer/src/systems/**.

Systems own business logic, geometry generation, and constraints. They run in the Three.js frame loop and are never rendered directly.

For registry-driven kinds, prefer no per-kind system. If your kind's only job is "rebuild geometry on dirty", set def.geometry and let the framework's <GeometrySystem> handle the rebuild loop. Per-kind systems remain for extra responsibilities — animations, cross-kind dirty cascades, named-mesh material poking. See node-definitions.md.

Two Kinds of Systems

Core Systems — packages/core/src/systems/

Pure logic: no rendering, no Three.js objects. They read nodes from useScene, compute derived values (geometry, constraints), and write results back.

System Responsibility
WallSystem Wall mitering, corner joints
CeilingSystem Polygon-based ceiling generation
RoofSystem Pitched roof shape
DoorSystem Placement constraints on walls
WindowSystem Placement constraints on walls
ItemSystem Item transforms, collision

Slab geometry has no dedicated system: it renders through the registry def.geometry (packages/nodes/src/slab/geometry.ts, calling the pure generators in packages/viewer/src/systems/slab/slab-system.tsx) with a small def.system for dirty tracking.

Viewer Systems — packages/viewer/src/systems/

Access Three.js objects (via useRegistry) and manage rendering side-effects.

System Responsibility
LevelSystem Stacked / exploded / solo / manual level positions
WallCutout Cuts door/window holes in wall geometry
ZoneSystem Zone display and label placement
InteractiveSystem Item toggles and sliders in the scene
GuideSystem Temporary helper geometry
ScanSystem Point cloud rendering

Pattern

Systems are React components that render nothing (return null) and use useFrame for per-frame logic.

// packages/core/src/systems/my-system.tsx
import { useFrame } from '@react-three/fiber'
import { useScene } from '../store/use-scene'

export function MySystem() {
  const nodes = useScene(s => s.nodes)

  useFrame(() => {
    // compute and write back derived state
  })

  return null
}

Core and viewer systems are mounted inside <Viewer> alongside renderers. See packages/viewer/src/components/viewer/index.tsx for the mount order.

Systems are a customization point. Any consumer of <Viewer> — the editor app, an embed, a read-only preview — can inject its own systems as children. This is how editor-specific behaviour (space detection, tool feedback) is added without touching the viewer package.

Rules

  • Core systems must not import Three.js — they work with plain data.
  • Viewer systems must not contain business logic — delegate to core if the rule is domain-level.
  • Never duplicate logic between a system and a renderer — if the renderer needs it, the system should compute and store it, and the renderer reads the result.
  • Systems should be idempotent: given the same nodes, they produce the same output.
  • Mark nodes as dirty in the scene store to signal that a system should re-run. Avoid running expensive logic every frame without a dirty check.

Adding a New System

  1. Decide the scope:

    • Domain logicpackages/core/src/systems/
    • Viewer rendering side-effectpackages/viewer/src/systems/ — mount in packages/viewer/src/components/viewer/index.tsx
    • Editor-specific or integration-specific → keep it in the consuming app (e.g. apps/editor/components/systems/) and inject it as a child of <Viewer>
  2. Create <name>-system.tsx in the appropriate directory.

  3. Mount it in the right place:

    • Viewer-internal systems go in packages/viewer/src/components/viewer/index.tsx
    • App-specific systems are injected as children from outside:
      // apps/editor — editor injects its own systems without modifying the viewer
      <Viewer>
        <MyEditorSystem />
        <ToolManager />
      </Viewer>
      
  4. Mount order matters. Most viewer systems run after renderers in the JSX tree — they consume sceneRegistry data that renderers populate on mount. Only place a system before renderers if it explicitly does not read the registry.