82 lines
2.9 KiB
TypeScript

'use client'
import { emitter, type GridEvent, sceneRegistry, snapPointToGrid } from '@pascal-app/core'
import { isGridSnapActive, useEditor } from '@pascal-app/editor'
import { useEffect, useRef, useState } from 'react'
import { type Group, Vector3 } from 'three'
const worldVec = new Vector3()
/** Snap a planar position to the grid when grid snapping is the active mode —
* reading the same `isGridSnapActive()` toggle + `gridSnapStep` the built-in
* item/shelf tools use, so plants honour the snap mode like every other item. */
export function snapXZ(x: number, z: number): readonly [number, number] {
if (!isGridSnapActive()) return [x, z]
return snapPointToGrid([x, z], useEditor.getState().gridSnapStep)
}
/**
* Convert a world-space grid hit into the active level's local frame, the way
* the host stores node positions. Re-derived from the public `sceneRegistry`
* because the built-in `floor-placement` helpers aren't part of the public
* `@pascal-app/*` surface yet — a candidate for a future `@pascal-app/plugin-api`.
*/
export function toLevelLocal(
levelId: string,
world: [number, number, number],
): [number, number, number] {
const levelObject = sceneRegistry.nodes.get(levelId)
if (!levelObject) return [world[0], 0, world[2]]
worldVec.set(world[0], world[1], world[2])
levelObject.updateWorldMatrix(true, false)
levelObject.worldToLocal(worldVec)
return [worldVec.x, 0, worldVec.z]
}
/**
* Shared placement wiring for any plant tool: ghosts a preview at the snapped
* cursor on `grid:move`, and calls `onCommit` with the snapped level-local
* position on `grid:click`. Returns the cursor group ref + visibility for the
* tool to attach its preview to. `onCommit` is read through a ref so a tool can
* close over live brush state without re-subscribing every render.
*/
export function usePlacement(
activeLevelId: string | null,
onCommit: (levelLocalPosition: [number, number, number]) => void,
) {
const cursorRef = useRef<Group>(null)
const [cursorVisible, setCursorVisible] = useState(false)
const commitRef = useRef(onCommit)
commitRef.current = onCommit
useEffect(() => {
if (!activeLevelId) return
setCursorVisible(false)
let lastWorld: [number, number, number] | null = null
const onMove = (event: GridEvent) => {
setCursorVisible(true)
const [lx, , lz] = event.localPosition
const [sx, sz] = snapXZ(lx, lz)
cursorRef.current?.position.set(sx, 0, sz)
lastWorld = event.position
}
const onClick = (event: GridEvent) => {
const world = lastWorld ?? event.position
const [lx, , lz] = toLevelLocal(activeLevelId, world)
const [sx, sz] = snapXZ(lx, lz)
commitRef.current([sx, 0, sz])
}
emitter.on('grid:move', onMove)
emitter.on('grid:click', onClick)
return () => {
emitter.off('grid:move', onMove)
emitter.off('grid:click', onClick)
}
}, [activeLevelId])
return { cursorRef, cursorVisible }
}