diff --git a/apps/editor/components/tools/wall/wall-tool.tsx b/apps/editor/components/tools/wall/wall-tool.tsx index 23998980..935dea97 100644 --- a/apps/editor/components/tools/wall/wall-tool.tsx +++ b/apps/editor/components/tools/wall/wall-tool.tsx @@ -1,7 +1,83 @@ import { emitter, type GridEvent, useScene, WallNode } from '@pascal-app/core' import { useViewer } from '@pascal-app/viewer' -import { useEffect, useRef } from 'react' -import { type Line, type Mesh, Vector3 } from 'three' +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 @@ -16,36 +92,43 @@ const commitWallDrawing = (start: [number, number], end: [number, number]) => { export const WallTool: React.FC = () => { const cursorRef = useRef(null) - const drawingLineRef = 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 buildingState = 0 - const startingPoint = new Vector3(0, 0, 0) - const endingPoint = new Vector3(0, 0, 0) let gridPosition: [number, number] = [0, 0] - drawingLineRef.current.geometry.setFromPoints([startingPoint, endingPoint]) - const onGridMove = (event: GridEvent) => { - if (!cursorRef.current) return + 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 === 1) { - endingPoint.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) } - drawingLineRef.current.geometry.setFromPoints([startingPoint, endingPoint]) } + const onGridClick = (event: GridEvent) => { - if (buildingState === 0) { - startingPoint.set(gridPosition[0], event.position[1], gridPosition[1]) - buildingState = 1 - console.log('starting building at:', startingPoint) - drawingLineRef.current.visible = true - } else if (buildingState === 1) { - commitWallDrawing([startingPoint.x, startingPoint.z], [endingPoint.x, endingPoint.z]) - drawingLineRef.current.visible = false - buildingState = 0 + 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 } } @@ -60,25 +143,24 @@ export const WallTool: React.FC = () => { return ( + {/* Cursor indicator */} - - {/* @ts-ignore */} - - - - - + + {/* Wall preview */} + + + + ) }