Files
editor/packages/nodes/src/spawn/renderer.tsx
T

95 lines
3.1 KiB
TypeScript

'use client'
import {
type AnyNodeId,
type SpawnNode,
useLiveNodeOverrides,
useLiveTransforms,
useRegistry,
} from '@pascal-app/core'
import { createDefaultMaterial, useNodeEvents, useViewer } from '@pascal-app/viewer'
import { useMemo, useRef } from 'react'
import { Color, type Group, Shape } from 'three'
const SPAWN_COLOR = new Color('#818cf8')
/**
* Registry-driven spawn renderer. Behaviorally identical to the legacy
* `@pascal-app/viewer/components/renderers/spawn/spawn-renderer.tsx` — same
* geometry and event surface. When the spawn definition lands
* in `builtinPlugin.nodes`, the Phase 0 dispatch shims switch the renderer
* here and the legacy one is short-circuited.
*
* Lives in `@pascal-app/nodes` (not viewer) so the kind owns its own render
* code. Phase 5's batch migration applies the same pattern to every node.
*/
const SpawnRenderer = ({ node }: { node: SpawnNode }) => {
const ref = useRef<Group>(null!)
const handlers = useNodeEvents(node, 'spawn')
const liveOverride = useLiveNodeOverrides((state) => state.get(node.id as AnyNodeId))
const effectiveNode = useMemo(
() => (liveOverride ? ({ ...node, ...liveOverride } as SpawnNode) : node),
[node, liveOverride],
)
const liveTransform = useLiveTransforms((state) => state.get(node.id))
const walkthroughMode = useViewer((state) => state.walkthroughMode)
const shading = useViewer((state) => state.shading)
useRegistry(node.id, 'spawn', ref)
const material = useMemo(() => {
const next = createDefaultMaterial('#818cf8', 0.42, shading) as ReturnType<
typeof createDefaultMaterial
> & {
emissive?: Color
emissiveIntensity?: number
metalness?: number
}
next.emissive?.copy(SPAWN_COLOR)
if ('emissiveIntensity' in next) next.emissiveIntensity = 0.08
if ('metalness' in next) next.metalness = 0.03
next.needsUpdate = true
return next
}, [shading])
const arrowShape = useMemo(() => {
const shape = new Shape()
shape.moveTo(0, 0.24)
shape.lineTo(-0.18, -0.14)
shape.lineTo(0.18, -0.14)
shape.closePath()
return shape
}, [])
return (
<group
position={liveTransform?.position ?? effectiveNode.position}
ref={ref}
rotation={[0, liveTransform?.rotation ?? effectiveNode.rotation, 0]}
visible={!walkthroughMode && effectiveNode.visible !== false}
>
<mesh position={[0, 0.09, 0]} rotation={[-Math.PI / 2, 0, 0]} {...handlers}>
<ringGeometry args={[0.34, 0.48, 48]} />
<primitive attach="material" object={material} />
</mesh>
<mesh position={[0, 0.1, -0.52]} rotation={[-Math.PI / 2, 0, 0]} {...handlers}>
<shapeGeometry args={[arrowShape]} />
<primitive attach="material" object={material} />
</mesh>
<mesh position={[0, 0.41, 0]} {...handlers}>
<boxGeometry args={[0.3, 0.54, 0.16]} />
<primitive attach="material" object={material} />
</mesh>
<mesh position={[0, 0.83, 0]} {...handlers}>
<boxGeometry args={[0.18, 0.18, 0.18]} />
<primitive attach="material" object={material} />
</mesh>
</group>
)
}
export default SpawnRenderer