Files
editor/wiki/architecture/systems.md
T
Wassim SAMADandClaude Opus 4.7 7b946ce8b7 wiki: add node-definitions doc for three-checkbox composition model
New page covering the geometry/renderer/system trio that registry-driven
kinds opt into. Documents:

- The three optional fields on NodeDefinition and when each applies
- Generic <GeometrySystem> + <ParametricNodeRenderer> runtime
- GeometryContext shape (resolve / children / siblings / parent)
- Combination matrix for shelf / spawn / zone / door / window / GLB items
- Migration recipe from custom renderer+system files to def.geometry
- Rules around purity, dispose-on-rebuild, register-once

renderers.md and systems.md gain "prefer registry-driven" banners and
link out to the new page. Architecture README adds the page to the
index so review-architecture skill picks it up.

The pattern was validated by the shelf spike: inline-JSX geometry was
visibly laggy on parametric edits; moving to a per-kind system reading
dirtyNodes (mirroring door/wall/item) restored smoothness. The three-
checkbox model generalises that win so most future kinds need only a
pure geometry function.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 08:29:54 -04:00

4.2 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
SlabSystem Polygon-based floor/roof generation
CeilingSystem Polygon-based ceiling generation
RoofSystem Pitched roof shape
DoorSystem Placement constraints on walls
WindowSystem Placement constraints on walls
ItemSystem Item transforms, collision

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.