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
@@ -22,6 +22,7 @@ export type {
|
||||
CuttableConfig,
|
||||
DragAction,
|
||||
EditorCtx,
|
||||
GeometryContext,
|
||||
HostableConfig,
|
||||
IconRef,
|
||||
Issue,
|
||||
|
||||
@@ -1,7 +1,31 @@
|
||||
import type { ComponentType } from 'react'
|
||||
import type { Object3D } from 'three'
|
||||
import type { ZodObject, z } from 'zod'
|
||||
import type { AnyNode, AnyNodeId } from '../schema/types'
|
||||
|
||||
// ─── GeometryContext ─────────────────────────────────────────────────
|
||||
//
|
||||
// Read-only scene access passed to `def.geometry(node, ctx)`. Most kinds'
|
||||
// builders ignore `ctx` and read only `node` (shelf, item, spawn). Kinds
|
||||
// whose meshes reference other nodes by ID — wall miters with siblings,
|
||||
// door cutouts read parent wall — use `ctx` to resolve those references
|
||||
// without importing `useScene`. Builders stay pure and unit-testable.
|
||||
//
|
||||
// Future extension: `levelData?: { miters?: ... }` for level-scoped batch
|
||||
// data (wall mitering across an entire level). Decided alongside the wall
|
||||
// migration off its dedicated system (Phase 3+).
|
||||
|
||||
export type GeometryContext = {
|
||||
/** Look up any node by ID. Returns undefined if the node doesn't exist. */
|
||||
resolve: <N = AnyNode>(id: AnyNodeId) => N | undefined
|
||||
/** Resolved children of this node (filters out unresolvable IDs). */
|
||||
children: AnyNode[]
|
||||
/** Same kind, same parent — drives wall mitering / endpoint-match. */
|
||||
siblings: AnyNode[]
|
||||
/** Resolved parent (null for root-level nodes). */
|
||||
parent: AnyNode | null
|
||||
}
|
||||
|
||||
// ─── Plugin manifest ─────────────────────────────────────────────────
|
||||
|
||||
export type Plugin = {
|
||||
@@ -39,6 +63,19 @@ export type NodeDefinition<S extends ZodObject<any>> = {
|
||||
* already null-guard on `def.renderer` so omitting it is safe.
|
||||
*/
|
||||
renderer?: RendererSource<z.infer<S>>
|
||||
/**
|
||||
* Pure geometry builder. When set, the framework's generic
|
||||
* `<GeometrySystem>` calls this on every dirty mark — `nodes` keyed by
|
||||
* `def.geometry`'s presence are picked up; the returned `Object3D`'s
|
||||
* children replace the registered group's children. Together with
|
||||
* `<ParametricNodeRenderer>` this lets a kind ship without per-kind
|
||||
* `renderer.tsx` or `system.tsx` files (see
|
||||
* `wiki/architecture/node-definitions.md`). Combine with `renderer` if
|
||||
* you want JSX-side composition (drei, `<Html>`, GLB) AND parametric
|
||||
* rebuilds; combine with `system` if you also need per-frame imperative
|
||||
* work (animations, named-mesh material poking).
|
||||
*/
|
||||
geometry?: (node: z.infer<S>, ctx: GeometryContext) => Object3D
|
||||
system?: SystemContribution
|
||||
tool?: LazyComponent
|
||||
affordances?: Affordance<z.infer<S>>[]
|
||||
|
||||
Reference in New Issue
Block a user