wall snapping to 45°
This commit is contained in:
@@ -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<Mesh>(null)
|
||||
const drawingLineRef = useRef<Line>(null!)
|
||||
const wallPreviewRef = useRef<Mesh>(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 (
|
||||
<group>
|
||||
{/* Cursor indicator */}
|
||||
<mesh ref={cursorRef}>
|
||||
<boxGeometry args={[0.2, 0.2, 0.2]} />
|
||||
<meshStandardMaterial color="red" />
|
||||
</mesh>
|
||||
<group>
|
||||
{/* @ts-ignore */}
|
||||
<line ref={drawingLineRef} frustumCulled={false} renderOrder={1} visible={false}>
|
||||
<bufferGeometry />
|
||||
<lineDashedNodeMaterial
|
||||
color="blue"
|
||||
linewidth={4}
|
||||
linecap="round"
|
||||
depthTest={false}
|
||||
depthWrite={false}
|
||||
dashSize={2}
|
||||
gapSize={0.1}
|
||||
/>
|
||||
</line>
|
||||
</group>
|
||||
|
||||
{/* Wall preview */}
|
||||
<mesh ref={wallPreviewRef} visible={false} renderOrder={1}>
|
||||
<shapeGeometry />
|
||||
<meshBasicMaterial
|
||||
color="#3b82f6"
|
||||
transparent
|
||||
opacity={0.5}
|
||||
side={DoubleSide}
|
||||
depthTest={false}
|
||||
depthWrite={false}
|
||||
/>
|
||||
</mesh>
|
||||
</group>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user