Wire shelf + spawn placement polish: SFX, cursor, sidebar, selection
User-visible follow-ups after first running the Phase 2 spike.
Spawn tool now matches legacy UX:
- CursorSphere from @pascal-app/editor for the placement indicator
(ring + line + tool-icon tooltip) — was a plain sphere mesh.
- Emits sfx:structure-build on commit + setTool(null) + setMode
('select') to exit build mode, matching legacy spawn-tool.
Shelf tool placement:
- Emits sfx:structure-build on commit.
- Cursor preview now shows top board + brackets (was just the top),
matching what gets placed.
Shelf selectable from the 3D canvas:
- ShelfEvent type added to @pascal-app/core/events/bus.
- 'shelf' added to NodeConfig in useNodeEvents.
- ShelfRenderer wires `useNodeEvents(node, 'shelf')` handlers onto
every mesh. Clicks/hovers now bubble through the editor's selection
manager and update useViewer.selection.
Shelf appears in the sidebar:
- ShelfTreeNode component (mirrors spawn-tree-node's shape +
selection/hover/rename wiring; lucide Layers icon).
- TreeNode dispatcher adds a `case 'shelf':` arm.
Framework changes:
- @pascal-app/editor exports CursorSphere alongside triggerSFX.
- @pascal-app/nodes now declares @pascal-app/editor as peer/dev dep.
Pre-existing typecheck errors in @pascal-app/editor (ceiling-tree-node,
fence-tree-node, slab-tree-node, scene.ts) are unchanged — present on
main and not introduced by this commit.
630 tests still pass.
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
a89a1efccf
commit
76794ceb7d
@@ -1,26 +1,11 @@
|
||||
'use client'
|
||||
|
||||
import { emitter, type GridEvent, SpawnNode, sceneRegistry, useScene } from '@pascal-app/core'
|
||||
import { CursorSphere, triggerSFX, useEditor } from '@pascal-app/editor'
|
||||
import { useViewer } from '@pascal-app/viewer'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { type Group, Vector3 } from 'three'
|
||||
|
||||
/**
|
||||
* Registry-driven spawn placement tool. No props — reads `activeLevelId` from
|
||||
* `useViewer` directly and broadcasts placement events through the store.
|
||||
*
|
||||
* Behavior parity with the legacy tool in
|
||||
* `@pascal-app/editor/components/tools/spawn/spawn-tool.tsx`:
|
||||
* - Grid-snap to half-meter increments on X/Z
|
||||
* - Project click position into the active level's local frame
|
||||
* - Singleton: if a spawn already exists for this level, reuse it and clean
|
||||
* up any duplicates
|
||||
* - On commit: select the placed spawn and exit build mode
|
||||
*
|
||||
* Mounted by `ToolManager`'s registry-first dispatch (Phase 0 shim) when
|
||||
* `nodeRegistry.has('spawn')` and the active tool is 'spawn'.
|
||||
*/
|
||||
|
||||
const roundToHalf = (value: number) => Math.round(value * 2) / 2
|
||||
const worldVector = new Vector3()
|
||||
|
||||
@@ -47,22 +32,26 @@ function getLevelLocalPosition(levelId: string, event: GridEvent): [number, numb
|
||||
return [roundToHalf(worldVector.x), worldVector.y, roundToHalf(worldVector.z)]
|
||||
}
|
||||
|
||||
/**
|
||||
* Registry-driven spawn placement tool. Reads `activeLevelId` from useViewer
|
||||
* directly (no props), broadcasts placement via store updates + SFX, and
|
||||
* uses the shared CursorSphere from @pascal-app/editor for visual parity
|
||||
* with legacy placement tools.
|
||||
*/
|
||||
const SpawnTool = () => {
|
||||
const activeLevelId = useViewer((state) => state.selection.levelId)
|
||||
const [, setCursor] = useState<[number, number, number] | null>(null)
|
||||
const cursorRef = useRef<Group>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeLevelId) return
|
||||
|
||||
const onGridMove = (event: GridEvent) => {
|
||||
const next: [number, number, number] = [
|
||||
roundToHalf(event.localPosition[0]),
|
||||
event.localPosition[1],
|
||||
roundToHalf(event.localPosition[2]),
|
||||
]
|
||||
setCursor(next)
|
||||
cursorRef.current?.position.set(next[0], next[1], next[2])
|
||||
// Cursor lives in the ToolManager's building-local group. Use
|
||||
// event.localPosition directly (already building-local) with the
|
||||
// same half-meter snap the legacy tool uses.
|
||||
const nextX = roundToHalf(event.localPosition[0])
|
||||
const nextZ = roundToHalf(event.localPosition[2])
|
||||
cursorRef.current?.position.set(nextX, event.localPosition[1], nextZ)
|
||||
}
|
||||
|
||||
const onGridClick = (event: GridEvent) => {
|
||||
@@ -91,11 +80,9 @@ const SpawnTool = () => {
|
||||
}
|
||||
|
||||
useViewer.getState().setSelection({ selectedIds: [placedId] })
|
||||
// Note: legacy tool also emits sfx:structure-build and resets the editor
|
||||
// tool/mode. We rely on the legacy ToolManager to do the latter via the
|
||||
// build-tool exit path; this commit doesn't replicate the SFX since the
|
||||
// registry doesn't yet bridge to the editor's sfx-emitter. Phase 4's
|
||||
// command surface adds a clean path.
|
||||
triggerSFX('sfx:structure-build')
|
||||
useEditor.getState().setTool(null)
|
||||
useEditor.getState().setMode('select')
|
||||
}
|
||||
|
||||
emitter.on('grid:move', onGridMove)
|
||||
@@ -109,20 +96,7 @@ const SpawnTool = () => {
|
||||
|
||||
if (!activeLevelId) return null
|
||||
|
||||
// Visible marker for the cursor — using a simple group + box. The legacy
|
||||
// tool used a CursorSphere component from @pascal-app/editor; here we keep
|
||||
// the dependency arrow flowing nodes→editor (which is allowed by the layer
|
||||
// rules) but use a minimal inline mesh to avoid the dependency entirely for
|
||||
// the spike. Phase 4 ports CursorSphere to the editor framework so node
|
||||
// tools can reuse it.
|
||||
return (
|
||||
<group ref={cursorRef}>
|
||||
<mesh position={[0, 1.1, 0]}>
|
||||
<sphereGeometry args={[0.18, 16, 12]} />
|
||||
<meshStandardMaterial color="#60a5fa" transparent opacity={0.6} />
|
||||
</mesh>
|
||||
</group>
|
||||
)
|
||||
return <CursorSphere color="#60a5fa" height={2.2} ref={cursorRef} />
|
||||
}
|
||||
|
||||
export default SpawnTool
|
||||
|
||||
Reference in New Issue
Block a user