fix: use WALL_MIN_LENGTH constant to prevent ghost walls (#168)

* Increase minimum wall length from 0.01 to 0.5

* fix: use WALL_MIN_LENGTH constant in wall-tool.tsx
This commit is contained in:
Thiago Löpes
2026-03-26 00:30:04 -04:00
committed by GitHub
parent 04e9feb78e
commit e4e64e22e3
2 changed files with 4 additions and 28 deletions
@@ -1,40 +1,32 @@
import { useScene, type WallNode, WallNode as WallSchema } from '@pascal-app/core' import { useScene, type WallNode, WallNode as WallSchema } from '@pascal-app/core'
import { useViewer } from '@pascal-app/viewer' import { useViewer } from '@pascal-app/viewer'
import { sfxEmitter } from '../../../lib/sfx-bus' import { sfxEmitter } from '../../../lib/sfx-bus'
export type WallPlanPoint = [number, number] export type WallPlanPoint = [number, number]
export const WALL_GRID_STEP = 0.5 export const WALL_GRID_STEP = 0.5
export const WALL_JOIN_SNAP_RADIUS = 0.35 export const WALL_JOIN_SNAP_RADIUS = 0.35
export const WALL_MIN_LENGTH = 0.01 export const WALL_MIN_LENGTH = 0.5
function distanceSquared(a: WallPlanPoint, b: WallPlanPoint): number { function distanceSquared(a: WallPlanPoint, b: WallPlanPoint): number {
const dx = a[0] - b[0] const dx = a[0] - b[0]
const dz = a[1] - b[1] const dz = a[1] - b[1]
return dx * dx + dz * dz return dx * dx + dz * dz
} }
function snapScalarToGrid(value: number, step = WALL_GRID_STEP): number { function snapScalarToGrid(value: number, step = WALL_GRID_STEP): number {
return Math.round(value / step) * step return Math.round(value / step) * step
} }
export function snapPointToGrid(point: WallPlanPoint, step = WALL_GRID_STEP): WallPlanPoint { export function snapPointToGrid(point: WallPlanPoint, step = WALL_GRID_STEP): WallPlanPoint {
return [snapScalarToGrid(point[0], step), snapScalarToGrid(point[1], step)] return [snapScalarToGrid(point[0], step), snapScalarToGrid(point[1], step)]
} }
export function snapPointTo45Degrees(start: WallPlanPoint, cursor: WallPlanPoint): WallPlanPoint { export function snapPointTo45Degrees(start: WallPlanPoint, cursor: WallPlanPoint): WallPlanPoint {
const dx = cursor[0] - start[0] const dx = cursor[0] - start[0]
const dz = cursor[1] - start[1] const dz = cursor[1] - start[1]
const angle = Math.atan2(dz, dx) const angle = Math.atan2(dz, dx)
const snappedAngle = Math.round(angle / (Math.PI / 4)) * (Math.PI / 4) const snappedAngle = Math.round(angle / (Math.PI / 4)) * (Math.PI / 4)
const distance = Math.sqrt(dx * dx + dz * dz) const distance = Math.sqrt(dx * dx + dz * dz)
return snapPointToGrid([ return snapPointToGrid([
start[0] + Math.cos(snappedAngle) * distance, start[0] + Math.cos(snappedAngle) * distance,
start[1] + Math.sin(snappedAngle) * distance, start[1] + Math.sin(snappedAngle) * distance,
]) ])
} }
function projectPointOntoWall(point: WallPlanPoint, wall: WallNode): WallPlanPoint | null { function projectPointOntoWall(point: WallPlanPoint, wall: WallNode): WallPlanPoint | null {
const [x1, z1] = wall.start const [x1, z1] = wall.start
const [x2, z2] = wall.end const [x2, z2] = wall.end
@@ -44,15 +36,12 @@ function projectPointOntoWall(point: WallPlanPoint, wall: WallNode): WallPlanPoi
if (lengthSquared < 1e-9) { if (lengthSquared < 1e-9) {
return null return null
} }
const t = ((point[0] - x1) * dx + (point[1] - z1) * dz) / lengthSquared const t = ((point[0] - x1) * dx + (point[1] - z1) * dz) / lengthSquared
if (t <= 0 || t >= 1) { if (t <= 0 || t >= 1) {
return null return null
} }
return [x1 + dx * t, z1 + dz * t] return [x1 + dx * t, z1 + dz * t]
} }
export function findWallSnapTarget( export function findWallSnapTarget(
point: WallPlanPoint, point: WallPlanPoint,
walls: WallNode[], walls: WallNode[],
@@ -62,12 +51,10 @@ export function findWallSnapTarget(
const radiusSquared = (options?.radius ?? WALL_JOIN_SNAP_RADIUS) ** 2 const radiusSquared = (options?.radius ?? WALL_JOIN_SNAP_RADIUS) ** 2
let bestTarget: WallPlanPoint | null = null let bestTarget: WallPlanPoint | null = null
let bestDistanceSquared = Number.POSITIVE_INFINITY let bestDistanceSquared = Number.POSITIVE_INFINITY
for (const wall of walls) { for (const wall of walls) {
if (ignoreWallIds.has(wall.id)) { if (ignoreWallIds.has(wall.id)) {
continue continue
} }
const candidates: Array<WallPlanPoint | null> = [ const candidates: Array<WallPlanPoint | null> = [
wall.start, wall.start,
wall.end, wall.end,
@@ -77,7 +64,6 @@ export function findWallSnapTarget(
if (!candidate) { if (!candidate) {
continue continue
} }
const candidateDistanceSquared = distanceSquared(point, candidate) const candidateDistanceSquared = distanceSquared(point, candidate)
if ( if (
candidateDistanceSquared > radiusSquared || candidateDistanceSquared > radiusSquared ||
@@ -85,15 +71,12 @@ export function findWallSnapTarget(
) { ) {
continue continue
} }
bestTarget = candidate bestTarget = candidate
bestDistanceSquared = candidateDistanceSquared bestDistanceSquared = candidateDistanceSquared
} }
} }
return bestTarget return bestTarget
} }
export function snapWallDraftPoint(args: { export function snapWallDraftPoint(args: {
point: WallPlanPoint point: WallPlanPoint
walls: WallNode[] walls: WallNode[]
@@ -103,38 +86,31 @@ export function snapWallDraftPoint(args: {
}): WallPlanPoint { }): WallPlanPoint {
const { point, walls, start, angleSnap = false, ignoreWallIds } = args const { point, walls, start, angleSnap = false, ignoreWallIds } = args
const basePoint = start && angleSnap ? snapPointTo45Degrees(start, point) : snapPointToGrid(point) const basePoint = start && angleSnap ? snapPointTo45Degrees(start, point) : snapPointToGrid(point)
return ( return (
findWallSnapTarget(basePoint, walls, { findWallSnapTarget(basePoint, walls, {
ignoreWallIds, ignoreWallIds,
}) ?? basePoint }) ?? basePoint
) )
} }
export function isWallLongEnough(start: WallPlanPoint, end: WallPlanPoint): boolean { export function isWallLongEnough(start: WallPlanPoint, end: WallPlanPoint): boolean {
return distanceSquared(start, end) >= WALL_MIN_LENGTH * WALL_MIN_LENGTH return distanceSquared(start, end) >= WALL_MIN_LENGTH * WALL_MIN_LENGTH
} }
export function createWallOnCurrentLevel( export function createWallOnCurrentLevel(
start: WallPlanPoint, start: WallPlanPoint,
end: WallPlanPoint, end: WallPlanPoint,
): WallNode | null { ): WallNode | null {
const currentLevelId = useViewer.getState().selection.levelId const currentLevelId = useViewer.getState().selection.levelId
const { createNode, nodes } = useScene.getState() const { createNode, nodes } = useScene.getState()
if (!(currentLevelId && isWallLongEnough(start, end))) { if (!(currentLevelId && isWallLongEnough(start, end))) {
return null return null
} }
const wallCount = Object.values(nodes).filter((node) => node.type === 'wall').length const wallCount = Object.values(nodes).filter((node) => node.type === 'wall').length
const wall = WallSchema.parse({ const wall = WallSchema.parse({
name: `Wall ${wallCount + 1}`, name: `Wall ${wallCount + 1}`,
start, start,
end, end,
}) })
createNode(wall, currentLevelId) createNode(wall, currentLevelId)
sfxEmitter.emit('sfx:structure-build') sfxEmitter.emit('sfx:structure-build')
return wall return wall
} }
@@ -5,7 +5,7 @@ import { DoubleSide, type Group, type Mesh, Shape, ShapeGeometry, Vector3 } from
import { EDITOR_LAYER } from '../../../lib/constants' import { EDITOR_LAYER } from '../../../lib/constants'
import { sfxEmitter } from '../../../lib/sfx-bus' import { sfxEmitter } from '../../../lib/sfx-bus'
import { CursorSphere } from '../shared/cursor-sphere' import { CursorSphere } from '../shared/cursor-sphere'
import { createWallOnCurrentLevel, snapWallDraftPoint, type WallPlanPoint } from './wall-drafting' import { createWallOnCurrentLevel, snapWallDraftPoint, WALL_MIN_LENGTH, type WallPlanPoint } from './wall-drafting'
const WALL_HEIGHT = 2.5 const WALL_HEIGHT = 2.5
@@ -17,7 +17,7 @@ const updateWallPreview = (mesh: Mesh, start: Vector3, end: Vector3) => {
const direction = new Vector3(end.x - start.x, 0, end.z - start.z) const direction = new Vector3(end.x - start.x, 0, end.z - start.z)
const length = direction.length() const length = direction.length()
if (length < 0.01) { if (length < WALL_MIN_LENGTH) {
mesh.visible = false mesh.visible = false
return return
} }
@@ -142,7 +142,7 @@ export const WallTool: React.FC = () => {
endingPoint.current.set(snappedEnd[0], event.position[1], snappedEnd[1]) endingPoint.current.set(snappedEnd[0], event.position[1], snappedEnd[1])
const dx = endingPoint.current.x - startingPoint.current.x const dx = endingPoint.current.x - startingPoint.current.x
const dz = endingPoint.current.z - startingPoint.current.z const dz = endingPoint.current.z - startingPoint.current.z
if (dx * dx + dz * dz < 0.01 * 0.01) return if (dx * dx + dz * dz < WALL_MIN_LENGTH * WALL_MIN_LENGTH) return
createWallOnCurrentLevel( createWallOnCurrentLevel(
[startingPoint.current.x, startingPoint.current.z], [startingPoint.current.x, startingPoint.current.z],
[endingPoint.current.x, endingPoint.current.z], [endingPoint.current.x, endingPoint.current.z],