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>
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.geometryand 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
dirtyin 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
-
Decide the scope:
- Domain logic →
packages/core/src/systems/ - Viewer rendering side-effect →
packages/viewer/src/systems/— mount inpackages/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>
- Domain logic →
-
Create
<name>-system.tsxin the appropriate directory. -
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>
- Viewer-internal systems go in
-
Mount order matters. Most viewer systems run after renderers in the JSX tree — they consume
sceneRegistrydata that renderers populate on mount. Only place a system before renderers if it explicitly does not read the registry.