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
@@ -0,0 +1,80 @@
|
||||
'use client'
|
||||
|
||||
import { type ShelfNode, useScene } from '@pascal-app/core'
|
||||
import { useViewer } from '@pascal-app/viewer'
|
||||
import { Layers } from 'lucide-react'
|
||||
import { memo, useCallback, useState } from 'react'
|
||||
import useEditor from './../../../../../store/use-editor'
|
||||
import { InlineRenameInput } from './inline-rename-input'
|
||||
import { focusTreeNode, handleTreeSelection, TreeNodeWrapper } from './tree-node'
|
||||
import { TreeNodeActions } from './tree-node-actions'
|
||||
|
||||
interface ShelfTreeNodeProps {
|
||||
nodeId: ShelfNode['id']
|
||||
depth: number
|
||||
isLast?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Sidebar tree entry for shelf. Mirrors spawn-tree-node's shape so the
|
||||
* existing tree-node-wrapper / selection / hover / rename plumbing all work
|
||||
* unchanged. Phase 4 derives this row generically from
|
||||
* `definition.presentation` — until then, one file per kind.
|
||||
*/
|
||||
export const ShelfTreeNode = memo(function ShelfTreeNode({
|
||||
nodeId,
|
||||
depth,
|
||||
isLast,
|
||||
}: ShelfTreeNodeProps) {
|
||||
const [isEditing, setIsEditing] = useState(false)
|
||||
const isVisible = useScene((s) => s.nodes[nodeId]?.visible !== false)
|
||||
const isSelected = useViewer((state) => state.selection.selectedIds.includes(nodeId))
|
||||
const isHovered = useViewer((state) => state.hoveredId === nodeId)
|
||||
const setSelection = useViewer((state) => state.setSelection)
|
||||
const setHoveredId = useViewer((state) => state.setHoveredId)
|
||||
|
||||
const handleClick = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
const handled = handleTreeSelection(
|
||||
e,
|
||||
nodeId,
|
||||
useViewer.getState().selection.selectedIds,
|
||||
setSelection,
|
||||
)
|
||||
if (!handled && useEditor.getState().phase === 'furnish') {
|
||||
useEditor.getState().setPhase('structure')
|
||||
}
|
||||
},
|
||||
[nodeId, setSelection],
|
||||
)
|
||||
|
||||
return (
|
||||
<TreeNodeWrapper
|
||||
actions={<TreeNodeActions nodeId={nodeId} />}
|
||||
depth={depth}
|
||||
expanded={false}
|
||||
hasChildren={false}
|
||||
icon={<Layers size={14} />}
|
||||
isHovered={isHovered}
|
||||
isLast={isLast}
|
||||
isSelected={isSelected}
|
||||
isVisible={isVisible}
|
||||
label={
|
||||
<InlineRenameInput
|
||||
defaultName="Shelf"
|
||||
isEditing={isEditing}
|
||||
nodeId={nodeId}
|
||||
onStartEditing={() => setIsEditing(true)}
|
||||
onStopEditing={() => setIsEditing(false)}
|
||||
/>
|
||||
}
|
||||
nodeId={nodeId}
|
||||
onClick={handleClick}
|
||||
onDoubleClick={() => focusTreeNode(nodeId)}
|
||||
onMouseEnter={() => setHoveredId(nodeId)}
|
||||
onMouseLeave={() => setHoveredId(null)}
|
||||
onToggle={() => {}}
|
||||
/>
|
||||
)
|
||||
})
|
||||
@@ -63,6 +63,7 @@ import { FenceTreeNode } from './fence-tree-node'
|
||||
import { ItemTreeNode } from './item-tree-node'
|
||||
import { LevelTreeNode } from './level-tree-node'
|
||||
import { RoofTreeNode } from './roof-tree-node'
|
||||
import { ShelfTreeNode } from './shelf-tree-node'
|
||||
import { SlabTreeNode } from './slab-tree-node'
|
||||
import { SpawnTreeNode } from './spawn-tree-node'
|
||||
import { StairTreeNode } from './stair-tree-node'
|
||||
@@ -94,6 +95,8 @@ export const TreeNode = memo(function TreeNode({ nodeId, depth = 0, isLast }: Tr
|
||||
return <ElevatorTreeNode depth={depth} isLast={isLast} nodeId={nodeId} />
|
||||
case 'level':
|
||||
return <LevelTreeNode depth={depth} isLast={isLast} nodeId={nodeId as `level_${string}`} />
|
||||
case 'shelf':
|
||||
return <ShelfTreeNode depth={depth} isLast={isLast} nodeId={nodeId as `shelf_${string}`} />
|
||||
case 'slab':
|
||||
return <SlabTreeNode depth={depth} isLast={isLast} nodeId={nodeId} />
|
||||
case 'spawn':
|
||||
|
||||
@@ -4,6 +4,7 @@ export {
|
||||
type SnapshotCameraData,
|
||||
ThumbnailGenerator,
|
||||
} from './components/editor/thumbnail-generator'
|
||||
export { CursorSphere } from './components/tools/shared/cursor-sphere'
|
||||
export { CameraActions as ViewerToolbarRight } from './components/ui/action-menu/camera-actions'
|
||||
export { ViewToggles as ViewerToolbarLeft } from './components/ui/action-menu/view-toggles'
|
||||
export { useCommandPalette } from './components/ui/command-palette'
|
||||
|
||||
Reference in New Issue
Block a user