Add preview slot to NodeDefinition; show translucent shape during move

User feedback: the move tool's CursorSphere is just a vertical line,
hard to tell what you're moving. Placement gets a translucent shape
that follows the cursor; move should too.

NodeDefinition.preview?: () => Promise<{ default: ComponentType<{ node }> }>
opt-in lazy component that renders a translucent ghost of the node.
Used by:
- The placement tool (ShelfTool) — renders the preview at the cursor
  position so the user sees the shape they're placing.
- The move tool (MoveRegistryNodeTool) — renders the preview at the
  drag target alongside the CursorSphere. Plus the original node is
  also dragged via live transforms, so the user sees both: the actual
  node moving + a translucent ghost at the same spot.

Implementation:
- New nodes/shelf/preview.tsx: ShelfPreview component. Renders the
  same shape as ShelfRenderer but `transparent: true, opacity: 0.5`.
- shelfDefinition.preview = () => import('./preview').
- ShelfTool's placement preview now uses <ShelfPreview node={defaults} />
  instead of an inline copy of the box geometry.
- MoveRegistryNodeTool lazy-loads `def.preview` (cached by loader,
  Suspense-wrapped). If a kind doesn't define `preview`, only the
  CursorSphere shows — matches today's behavior.

Phase 4 may merge `preview` with `renderer` behind an `opacity` prop
so kinds don't duplicate JSX between the solid and translucent
versions; until then defining `preview` is opt-in and one extra file.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Wassim SAMAD
2026-05-14 16:01:32 -04:00
co-authored by Claude Opus 4.7
parent a090c22f42
commit e84b1ec8bc
5 changed files with 130 additions and 52 deletions
+50
View File
@@ -0,0 +1,50 @@
'use client'
import { useMemo } from 'react'
import { Color } from 'three'
import type { ShelfNode } from './schema'
/**
* Translucent preview of a shelf. Used by:
* - The placement tool's cursor (ShelfTool) — at the cursor position
* - The move tool (MoveRegistryNodeTool) — at the drag target position
*
* Renders the same primitives as the actual ShelfRenderer, but with
* `transparent: true, opacity: 0.5` so the user can see what they're
* placing/moving without it being a hard solid.
*/
const ShelfPreview = ({ node }: { node: ShelfNode }) => {
const color = useMemo(() => new Color(node.color), [node.color])
const topY = node.height + node.thickness / 2
const inset = Math.min(0.12, node.width / 6)
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
return (
<group>
<mesh position={[0, topY, 0]}>
<boxGeometry args={[node.width, node.thickness, node.depth]} />
<meshStandardMaterial color={color} transparent opacity={0.5} />
</mesh>
{node.bracketStyle !== 'hidden' && (
<>
<mesh position={[-(node.width / 2 - inset), bracketHeight / 2, 0]}>
<boxGeometry args={[bracketWidth, bracketHeight, bracketDepth]} />
<meshStandardMaterial color={color} transparent opacity={0.5} />
</mesh>
<mesh position={[node.width / 2 - inset, bracketHeight / 2, 0]}>
<boxGeometry args={[bracketWidth, bracketHeight, bracketDepth]} />
<meshStandardMaterial color={color} transparent opacity={0.5} />
</mesh>
</>
)}
</group>
)
}
export default ShelfPreview