wall snapping to 45°
This commit is contained in:
@@ -1,7 +1,83 @@
|
|||||||
import { emitter, type GridEvent, useScene, WallNode } from '@pascal-app/core'
|
import { emitter, type GridEvent, useScene, WallNode } from '@pascal-app/core'
|
||||||
import { useViewer } from '@pascal-app/viewer'
|
import { useViewer } from '@pascal-app/viewer'
|
||||||
import { useEffect, useRef } from 'react'
|
import { useEffect, useRef, useMemo } from 'react'
|
||||||
import { type Line, type Mesh, Vector3 } from 'three'
|
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 commitWallDrawing = (start: [number, number], end: [number, number]) => {
|
||||||
const currentLevelId = useViewer.getState().selection.levelId
|
const currentLevelId = useViewer.getState().selection.levelId
|
||||||
@@ -16,36 +92,43 @@ const commitWallDrawing = (start: [number, number], end: [number, number]) => {
|
|||||||
|
|
||||||
export const WallTool: React.FC = () => {
|
export const WallTool: React.FC = () => {
|
||||||
const cursorRef = useRef<Mesh>(null)
|
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(() => {
|
useEffect(() => {
|
||||||
let buildingState = 0
|
|
||||||
const startingPoint = new Vector3(0, 0, 0)
|
|
||||||
const endingPoint = new Vector3(0, 0, 0)
|
|
||||||
let gridPosition: [number, number] = [0, 0]
|
let gridPosition: [number, number] = [0, 0]
|
||||||
|
|
||||||
drawingLineRef.current.geometry.setFromPoints([startingPoint, endingPoint])
|
|
||||||
|
|
||||||
const onGridMove = (event: GridEvent) => {
|
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]
|
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])
|
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) => {
|
const onGridClick = (event: GridEvent) => {
|
||||||
if (buildingState === 0) {
|
if (buildingState.current === 0) {
|
||||||
startingPoint.set(gridPosition[0], event.position[1], gridPosition[1])
|
startingPoint.current.set(gridPosition[0], event.position[1], gridPosition[1])
|
||||||
buildingState = 1
|
buildingState.current = 1
|
||||||
console.log('starting building at:', startingPoint)
|
wallPreviewRef.current.visible = true
|
||||||
drawingLineRef.current.visible = true
|
} else if (buildingState.current === 1) {
|
||||||
} else if (buildingState === 1) {
|
commitWallDrawing(
|
||||||
commitWallDrawing([startingPoint.x, startingPoint.z], [endingPoint.x, endingPoint.z])
|
[startingPoint.current.x, startingPoint.current.z],
|
||||||
drawingLineRef.current.visible = false
|
[endingPoint.current.x, endingPoint.current.z]
|
||||||
buildingState = 0
|
)
|
||||||
|
wallPreviewRef.current.visible = false
|
||||||
|
buildingState.current = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,25 +143,24 @@ export const WallTool: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<group>
|
<group>
|
||||||
|
{/* Cursor indicator */}
|
||||||
<mesh ref={cursorRef}>
|
<mesh ref={cursorRef}>
|
||||||
<boxGeometry args={[0.2, 0.2, 0.2]} />
|
<boxGeometry args={[0.2, 0.2, 0.2]} />
|
||||||
<meshStandardMaterial color="red" />
|
<meshStandardMaterial color="red" />
|
||||||
</mesh>
|
</mesh>
|
||||||
<group>
|
|
||||||
{/* @ts-ignore */}
|
{/* Wall preview */}
|
||||||
<line ref={drawingLineRef} frustumCulled={false} renderOrder={1} visible={false}>
|
<mesh ref={wallPreviewRef} visible={false} renderOrder={1}>
|
||||||
<bufferGeometry />
|
<shapeGeometry />
|
||||||
<lineDashedNodeMaterial
|
<meshBasicMaterial
|
||||||
color="blue"
|
color="#3b82f6"
|
||||||
linewidth={4}
|
transparent
|
||||||
linecap="round"
|
opacity={0.5}
|
||||||
depthTest={false}
|
side={DoubleSide}
|
||||||
depthWrite={false}
|
depthTest={false}
|
||||||
dashSize={2}
|
depthWrite={false}
|
||||||
gapSize={0.1}
|
/>
|
||||||
/>
|
</mesh>
|
||||||
</line>
|
|
||||||
</group>
|
|
||||||
</group>
|
</group>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user