wall snapping to 45°

This commit is contained in:
wass08
2026-02-05 15:20:21 +09:00
parent 40e3ce8d84
commit 74b70b7877
+118 -36
View File
@@ -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>
)
}