import { emitter, type GridEvent, useScene, WallNode } from '@pascal-app/core' import { useViewer } from '@pascal-app/viewer' import { useEffect, useRef, useMemo } from 'react' import { DoubleSide, type Mesh, Vector3, Shape, ShapeGeometry } from 'three' const WALL_HEIGHT = 2.5 const WALL_THICKNESS = 0.15 /** * Snap point to 45° angle increments relative to start point * Also snaps end point to 0.5 grid */ const snapTo45Degrees = (start: Vector3, cursor: Vector3): Vector3 => { const dx = cursor.x - start.x const dz = cursor.z - start.z // Calculate angle in radians const angle = Math.atan2(dz, dx) // Round to nearest 45° (π/4 radians) const snappedAngle = Math.round(angle / (Math.PI / 4)) * (Math.PI / 4) // Calculate distance from start to cursor const distance = Math.sqrt(dx * dx + dz * dz) // Project end point along snapped angle let snappedX = start.x + Math.cos(snappedAngle) * distance let snappedZ = start.z + Math.sin(snappedAngle) * distance // Snap to 0.5 grid snappedX = Math.round(snappedX * 2) / 2 snappedZ = Math.round(snappedZ * 2) / 2 return new Vector3(snappedX, cursor.y, snappedZ) } /** * Update wall preview mesh geometry to create a vertical plane between two points */ const updateWallPreview = (mesh: Mesh, start: Vector3, end: Vector3) => { // Calculate direction and perpendicular for wall thickness const direction = new Vector3(end.x - start.x, 0, end.z - start.z) const length = direction.length() if (length < 0.01) { mesh.visible = false return } mesh.visible = true direction.normalize() // Perpendicular vector for thickness const perpendicular = new Vector3(-direction.z, 0, direction.x).multiplyScalar(WALL_THICKNESS / 2) // Create wall shape (vertical rectangle in XY plane) const shape = new Shape() shape.moveTo(0, 0) shape.lineTo(length, 0) shape.lineTo(length, WALL_HEIGHT) shape.lineTo(0, WALL_HEIGHT) shape.closePath() // Create geometry const geometry = new ShapeGeometry(shape) // Calculate rotation angle // Negate the angle to fix the opposite direction issue const angle = -Math.atan2(direction.z, direction.x) // Position at start point and rotate mesh.position.set(start.x, start.y, start.z) mesh.rotation.y = angle // Dispose old geometry and assign new one if (mesh.geometry) { mesh.geometry.dispose() } mesh.geometry = geometry } const commitWallDrawing = (start: [number, number], end: [number, number]) => { const currentLevelId = useViewer.getState().selection.levelId const { createNode } = useScene.getState() if (!currentLevelId) return const wall = WallNode.parse({ start, end }) createNode(wall, currentLevelId) } export const WallTool: React.FC = () => { const cursorRef = useRef(null) const wallPreviewRef = useRef(null!) const startingPoint = useRef(new Vector3(0, 0, 0)) const endingPoint = useRef(new Vector3(0, 0, 0)) const buildingState = useRef(0) useEffect(() => { let gridPosition: [number, number] = [0, 0] const onGridMove = (event: GridEvent) => { if (!cursorRef.current || !wallPreviewRef.current) return gridPosition = [Math.round(event.position[0] * 2) / 2, Math.round(event.position[2] * 2) / 2] const cursorPosition = new Vector3(gridPosition[0], event.position[1], gridPosition[1]) cursorRef.current.position.set(gridPosition[0], event.position[1], gridPosition[1]) if (buildingState.current === 1) { // Snap to 45° angles const snapped = snapTo45Degrees(startingPoint.current, cursorPosition) endingPoint.current.copy(snapped) // Update wall preview geometry updateWallPreview(wallPreviewRef.current, startingPoint.current, endingPoint.current) } } const onGridClick = (event: GridEvent) => { if (buildingState.current === 0) { startingPoint.current.set(gridPosition[0], event.position[1], gridPosition[1]) buildingState.current = 1 wallPreviewRef.current.visible = true } else if (buildingState.current === 1) { commitWallDrawing( [startingPoint.current.x, startingPoint.current.z], [endingPoint.current.x, endingPoint.current.z] ) wallPreviewRef.current.visible = false buildingState.current = 0 } } emitter.on('grid:move', onGridMove) emitter.on('grid:click', onGridClick) return () => { emitter.off('grid:move', onGridMove) emitter.off('grid:click', onGridClick) } }, []) return ( {/* Cursor indicator */} {/* Wall preview */} ) }