Phase 2 spike: spawn migration (flagged) + new shelf node
The first time registry-driven nodes actually run in the editor. Spawn migration (under NEXT_PUBLIC_USE_REGISTRY_FOR_SPAWN flag): - New packages/nodes/src/spawn/ folder with renderer, tool, schema (re-exported from core), parametrics, definition, index. - Spawn definition appended to builtinPlugin.nodes only when the flag is set. With the flag off, the Phase 0 shims fall through and the legacy SpawnRenderer / SpawnTool keep ownership. - New no-props SpawnTool reads activeLevelId from useViewer directly, matches legacy placement behavior (half-meter snap, singleton-per- level, replace-on-reclick). - Structural parity test (9 cases) validates definition shape + schema identity. Pixel-diff defers to Phase 4 alongside more nodes. New shelf node (no legacy — registered unconditionally): - ShelfNode schema in core/schema/nodes/shelf.ts (hand-maintained AnyNode union for now; Phase 6 derives the union from the registry and moves the schema fully into nodes/shelf/). - packages/nodes/src/shelf/ folder: pure geometry builder (buildShelfGeometry returns a Three.js Group of top board + brackets), R3F renderer that mounts the built group, no-props placement tool, parametrics descriptor (width/depth/thickness/ height/bracketStyle/color), definition with surfaces.top stackable surface for future stacking, and presentation metadata for the palette. - 13 unit tests across schema bounds and geometry behavior. - Palette wiring: 'shelf' added to StructureTool union + an entry in the structure-tools array (placeholder icon, replaced in Phase 4 when palette is registry-driven). Framework changes: - @pascal-app/viewer now exports useNodeEvents from its public barrel so node bundles in @pascal-app/nodes can subscribe to node-specific pointer events. (Used by spawn renderer; shelf renderer skips it for now since useNodeEvents has a hardcoded kind list — Phase 4 generalizes it via the registry.) - @pascal-app/nodes gains @pascal-app/viewer as a peer + dev dep so node bundles can import from it. 630 tests pass across 76 files (22 new this phase). Editor app continues to ship green with both legacy spawn and the new shelf node co-existing through the Phase 0 dispatch shims. To validate end-to-end in dev: - bun dev:community → open editor → click 'Shelf' in structure toolbar → click to place. Confirms full registry path (NodeRenderer dispatch + ToolManager dispatch + sceneRegistry byType Proxy). - Set NEXT_PUBLIC_USE_REGISTRY_FOR_SPAWN=1, restart dev, place spawn → visually identical to legacy. Confirms parity. 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
e8cf85313b
commit
b6d77206b4
@@ -0,0 +1,65 @@
|
||||
import { BoxGeometry, type BufferGeometry, Color, Group, Mesh, MeshStandardMaterial } from 'three'
|
||||
import type { ShelfNode } from './schema'
|
||||
|
||||
/**
|
||||
* Pure shelf geometry builder. Takes a `ShelfNode` and returns a `Group`
|
||||
* containing the top board + bracket meshes — no React, no scene access.
|
||||
*
|
||||
* Two reasons this is its own pure function (not inlined into the renderer):
|
||||
*
|
||||
* 1. **Geometry parity testing.** Phase 4's pixel-diff test compares the
|
||||
* BufferGeometry vertex/index arrays returned by this function against
|
||||
* a snapshot — pure functions are trivial to test, JSX is not.
|
||||
* 2. **AI-authored nodes.** This is the file an AI is most likely to
|
||||
* generate. Pure, deterministic, takes typed input, returns Three.js
|
||||
* primitives. No React or registry knowledge required.
|
||||
*/
|
||||
export function buildShelfGeometry(node: ShelfNode): Group {
|
||||
const group = new Group()
|
||||
group.name = 'shelf-geometry'
|
||||
|
||||
const material = new MeshStandardMaterial({
|
||||
color: new Color(node.color),
|
||||
roughness: 0.65,
|
||||
metalness: 0.05,
|
||||
})
|
||||
|
||||
// Top board, centered at (0, height + thickness/2, 0)
|
||||
const topBoardGeometry: BufferGeometry = new BoxGeometry(node.width, node.thickness, node.depth)
|
||||
const topBoard = new Mesh(topBoardGeometry, material)
|
||||
topBoard.name = 'shelf-top'
|
||||
topBoard.position.set(0, node.height + node.thickness / 2, 0)
|
||||
group.add(topBoard)
|
||||
|
||||
// Brackets — two below the top, near each end. Style varies the look.
|
||||
for (const sign of [-1, 1] as const) {
|
||||
const bracket = buildBracket(node, sign, material)
|
||||
if (bracket) {
|
||||
bracket.name = `shelf-bracket-${sign === -1 ? 'left' : 'right'}`
|
||||
group.add(bracket)
|
||||
}
|
||||
}
|
||||
|
||||
return group
|
||||
}
|
||||
|
||||
function buildBracket(node: ShelfNode, sign: -1 | 1, material: MeshStandardMaterial): Mesh | null {
|
||||
// 'hidden' style: skip visible brackets entirely.
|
||||
if (node.bracketStyle === 'hidden') return null
|
||||
|
||||
const inset = Math.min(0.12, node.width / 6)
|
||||
const x = sign * (node.width / 2 - inset)
|
||||
// Bracket height: from floor (0) up to the underside of the top board.
|
||||
const bracketHeight = Math.max(0.01, node.height)
|
||||
|
||||
const bracketWidth =
|
||||
node.bracketStyle === 'industrial'
|
||||
? Math.max(0.04, node.depth * 0.2)
|
||||
: Math.max(0.02, node.depth * 0.12)
|
||||
const bracketDepth = node.bracketStyle === 'industrial' ? node.depth * 0.95 : node.depth * 0.7
|
||||
|
||||
const geometry = new BoxGeometry(bracketWidth, bracketHeight, bracketDepth)
|
||||
const mesh = new Mesh(geometry, material)
|
||||
mesh.position.set(x, bracketHeight / 2, 0)
|
||||
return mesh
|
||||
}
|
||||
Reference in New Issue
Block a user