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>
93 lines
4.2 KiB
Markdown
93 lines
4.2 KiB
Markdown
# 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](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.
|
|
|
|
```tsx
|
|
// 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 logic** → `packages/core/src/systems/`
|
|
- **Viewer rendering side-effect** → `packages/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:
|
|
```tsx
|
|
// 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.
|