'use client' import { EDITOR_LAYER } from '@pascal-app/editor' import { useEffect, useMemo } from 'react' import type { Material } from 'three' import { generateTree, treeSpecOf } from './geometry' import type { TreeNode } from './schema' import { naturalHeight } from './variant-utils' /** * Translucent placement ghost — a single ez-tree (not instanced) scaled to the * node's height, following the cursor. Clones each material for the see-through * look and disables raycast so the ghost never intercepts the cursor ray (which * would freeze `grid:move`). */ export default function TreePreview({ node }: { node: TreeNode }) { const built = useMemo(() => { const tree = generateTree(treeSpecOf(node)) tree.scale.setScalar(node.height / naturalHeight(tree)) // Overlay layer keeps the ghost out of export/snapshot passes. Layers // don't inherit, so every object in the built tree needs it. tree.traverse((obj) => obj.layers.set(EDITOR_LAYER)) return tree }, [node]) useEffect(() => { const cloned: Material[] = [] built.traverse((obj) => { ;(obj as unknown as { raycast: () => void }).raycast = () => {} const mesh = obj as { material?: Material | Material[] } if (!mesh.material) return const ghost = (mat: Material): Material => { const c = mat.clone() c.transparent = true c.opacity = 0.5 c.depthWrite = false cloned.push(c) return c } mesh.material = Array.isArray(mesh.material) ? mesh.material.map(ghost) : ghost(mesh.material) }) return () => { for (const c of cloned) c.dispose() } }, [built]) return }