Phase 4: generic GeometrySystem + ParametricNodeRenderer; shelf ports off renderer/system files
Lands the three-checkbox composition runtime documented in wiki/architecture/node-definitions.md. A kind with only a pure geometry function now needs zero per-kind React or system code. Type-side additions (packages/core/src/registry/types.ts): - New `GeometryContext` (resolve / children / siblings / parent) — read- only scene access for builders that reference other nodes by ID (wall miters, door cutouts). Most kinds ignore it. - New `geometry?: (node, ctx) => Object3D` field on NodeDefinition, independent of renderer/system. Three orthogonal opt-ins replace the v0 RendererSource union. - Re-exported via packages/core/src/registry/index.ts (consumed by nodes packages through `export * from './registry'`). Runtime (packages/viewer): - New <GeometrySystem> (systems/geometry/geometry-system.tsx) walks dirtyNodes, builds a GeometryContext per dirty node, calls def.geometry, disposes old children, attaches new ones, clearDirty. Frame priority 2 (matches the priority shelf's per-kind system had). Mounted in viewer/index.tsx alongside <RegisteredSystems>. - New <ParametricNodeRenderer> (components/renderers/parametric-node- renderer.tsx) — empty <group> + useRegistry + useNodeEvents + markDirty-on-mount + useLiveTransforms. Mounts hosted children via <NodeRenderer> recursively. The default renderer for any registered kind without a custom def.renderer. - <NodeRenderer> dispatch updated: custom renderer wins, else geometry-only kinds fall through to ParametricNodeRenderer, else null (legacy switch fallback). Documented inline. Shelf migration (proof of the boilerplate collapse): - Deleted nodes/src/shelf/renderer.tsx (was 45 lines of registry + handler boilerplate). - Deleted nodes/src/shelf/system.tsx (was 60 lines of dirty-loop + dispose plumbing). - shelfDefinition now: `geometry: buildShelfGeometry`. One line. buildShelfGeometry is the pure function from geometry.ts that already existed. End-to-end effect: registry-driven shelf now mounts via the framework's generic renderer + system. Parametric edits flow through the same dirty-driven rebuild path, but the kind ships ~100 fewer lines of boilerplate. Every future kind that fits the same shape (item, fence segment, column, etc. as they migrate in Phase 5) follows the same "one line, one pure function" pattern. Wall stays on its dedicated def.renderer + def.system — its mitering needs level-batch context (`ctx.levelData?.miters`, future extension) that the generic system doesn't yet provide. Decided at Phase 3+, not blocking Phase 4 acceptance. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
60117e848b
commit
3f3818f3b0
@@ -1,4 +1,5 @@
|
||||
import type { NodeDefinition } from '@pascal-app/core'
|
||||
import { buildShelfGeometry } from './geometry'
|
||||
import { shelfParametrics } from './parametrics'
|
||||
import { ShelfNode } from './schema'
|
||||
|
||||
@@ -42,14 +43,15 @@ export const shelfDefinition: NodeDefinition<typeof ShelfNode> = {
|
||||
|
||||
parametrics: shelfParametrics,
|
||||
|
||||
renderer: {
|
||||
kind: 'parametric',
|
||||
module: () => import('./renderer'),
|
||||
},
|
||||
system: {
|
||||
module: () => import('./system'),
|
||||
priority: 5,
|
||||
},
|
||||
// Three-checkbox composition: shelf needs only a pure geometry function.
|
||||
// The framework's <ParametricNodeRenderer> mounts an empty group + wires
|
||||
// events / registry / dirty-on-mount; the global <GeometrySystem> calls
|
||||
// `buildShelfGeometry(node)` on every dirty mark and swaps the group's
|
||||
// children. No `renderer.tsx`, no `system.tsx` — see
|
||||
// `wiki/architecture/node-definitions.md`. Shelf is the reference port
|
||||
// proving Phase 4's boilerplate collapse end-to-end.
|
||||
geometry: buildShelfGeometry,
|
||||
|
||||
preview: () => import('./preview'),
|
||||
tool: () => import('./tool'),
|
||||
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useLiveTransforms, useRegistry, useScene } from '@pascal-app/core'
|
||||
import { useNodeEvents } from '@pascal-app/viewer'
|
||||
import { useLayoutEffect, useRef } from 'react'
|
||||
import type { Group } from 'three'
|
||||
import type { ShelfNode } from './schema'
|
||||
|
||||
/**
|
||||
* Thin shelf renderer. Mounts an empty `<group>`, registers it with
|
||||
* `sceneRegistry`, and marks the node dirty so `ShelfSystem` populates it
|
||||
* with geometry on the next frame.
|
||||
*
|
||||
* Mirrors the door/item pattern (see `wiki/architecture/renderers.md`):
|
||||
* "Renderers must not run geometry generation logic (that belongs in a
|
||||
* System)." Keeping the renderer tiny means parametric edits don't re-run
|
||||
* any React work — only the system's `useFrame` rebuilds the meshes.
|
||||
*/
|
||||
const ShelfRenderer = ({ node }: { node: ShelfNode }) => {
|
||||
const ref = useRef<Group>(null!)
|
||||
const handlers = useNodeEvents(node, 'shelf')
|
||||
const liveTransform = useLiveTransforms((state) => state.get(node.id))
|
||||
|
||||
useRegistry(node.id, 'shelf', ref)
|
||||
|
||||
// Mark dirty on mount and whenever the node identity changes so the system
|
||||
// builds (or rebuilds) geometry. Subsequent parametric edits set dirty via
|
||||
// the store's updateNode → dirtyNodes wiring.
|
||||
useLayoutEffect(() => {
|
||||
useScene.getState().markDirty(node.id)
|
||||
}, [node.id])
|
||||
|
||||
return (
|
||||
<group
|
||||
position={liveTransform?.position ?? node.position}
|
||||
ref={ref}
|
||||
rotation={liveTransform?.rotation ? [0, liveTransform.rotation, 0] : node.rotation}
|
||||
visible={node.visible}
|
||||
{...handlers}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default ShelfRenderer
|
||||
@@ -1,62 +0,0 @@
|
||||
import { type AnyNodeId, sceneRegistry, useScene } from '@pascal-app/core'
|
||||
import { useFrame } from '@react-three/fiber'
|
||||
import type { Group } from 'three'
|
||||
import { buildShelfGeometry } from './geometry'
|
||||
import type { ShelfNode } from './schema'
|
||||
|
||||
/**
|
||||
* Imperative shelf system. Mirrors the pattern used by door/wall/item systems
|
||||
* (see `wiki/architecture/systems.md` and `renderers.md`): geometry generation
|
||||
* lives here, the renderer is a thin mount point.
|
||||
*
|
||||
* On every frame, walks `dirtyNodes`, finds the registered group for each
|
||||
* dirty shelf in `sceneRegistry`, swaps its children with the result of
|
||||
* `buildShelfGeometry(node)`, then clears the dirty flag. No React re-render
|
||||
* is involved in the rebuild, so parametric edits stay smooth even when the
|
||||
* inspector emits an `updateNode` every pointermove.
|
||||
*/
|
||||
export const ShelfSystem = () => {
|
||||
const dirtyNodes = useScene((s) => s.dirtyNodes)
|
||||
const clearDirty = useScene((s) => s.clearDirty)
|
||||
|
||||
useFrame(() => {
|
||||
if (dirtyNodes.size === 0) return
|
||||
const nodes = useScene.getState().nodes
|
||||
|
||||
dirtyNodes.forEach((id) => {
|
||||
const node = nodes[id]
|
||||
if (!node || node.type !== 'shelf') return
|
||||
|
||||
const group = sceneRegistry.nodes.get(id) as Group | undefined
|
||||
if (!group) return // mount hasn't run yet — keep dirty for next frame
|
||||
|
||||
// Clear previous geometry. Disposing materials/geometries here keeps
|
||||
// long shelf-editing sessions from leaking GPU resources.
|
||||
for (const child of [...group.children]) {
|
||||
group.remove(child)
|
||||
if ('geometry' in child && (child as { geometry?: { dispose: () => void } }).geometry) {
|
||||
;(child as { geometry: { dispose: () => void } }).geometry.dispose()
|
||||
}
|
||||
if ('material' in child) {
|
||||
const m = (child as { material: unknown }).material
|
||||
if (Array.isArray(m)) {
|
||||
for (const mat of m) (mat as { dispose: () => void }).dispose()
|
||||
} else if (m && typeof (m as { dispose?: () => void }).dispose === 'function') {
|
||||
;(m as { dispose: () => void }).dispose()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const built = buildShelfGeometry(node as ShelfNode)
|
||||
for (const child of [...built.children]) {
|
||||
group.add(child)
|
||||
}
|
||||
|
||||
clearDirty(id as AnyNodeId)
|
||||
})
|
||||
}, 2)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export default ShelfSystem
|
||||
Reference in New Issue
Block a user