fix wall mitering (three-mesh-csg)

This commit is contained in:
wass08
2026-01-26 19:52:53 +09:00
parent f244f864ba
commit 54dd5d0541
4 changed files with 158 additions and 105 deletions
+55 -48
View File
@@ -41,15 +41,37 @@ const stripTransient = (meta: any) => {
// ============================================================================
/**
* Calculate rotation angle from wall normal vector
* The normal points outward from the wall surface
* Calculate cursor rotation in WORLD space from wall normal and orientation
*/
const calculateRotationFromNormal = (normal: [number, number, number] | undefined): number => {
const calculateCursorRotation = (
normal: [number, number, number] | undefined,
wallStart: [number, number],
wallEnd: [number, number],
): number => {
if (!normal) return 0
// Calculate angle in X-Z plane (top-down view)
// atan2(z, x) gives the angle the vector makes with the positive X axis
// Add π/2 to align item's forward direction with the wall normal
return Math.atan2(normal[2], normal[0]) - Math.PI / 2
// Wall direction angle in world XZ plane
const wallAngle = Math.atan2(wallEnd[1] - wallStart[1], wallEnd[0] - wallStart[0])
// In local wall space, front face has normal.z < 0, back face has normal.z > 0
if (normal[2] < 0) {
return -wallAngle
} else {
return Math.PI - wallAngle
}
}
/**
* Calculate item rotation in WALL-LOCAL space from normal
* Items are children of the wall mesh, so their rotation is relative to wall's local space
*/
const calculateItemRotation = (normal: [number, number, number] | undefined): number => {
if (!normal) return 0
// In wall-local space: X along wall, Y up, Z perpendicular (thickness)
// Front face (normal.z < 0): item faces -Z local → rotation = 0
// Back face (normal.z > 0): item faces +Z local → rotation = PI
return normal[2] < 0 ? 0 : Math.PI
}
/**
@@ -67,38 +89,20 @@ const getSideFromNormal = (normal: [number, number, number] | undefined): 'front
/**
* Check if the normal indicates a valid wall side face (front or back)
* Filters out top face and thickness edges
* @param normal - The face normal vector
* @param wallStart - Wall start point [x, z]
* @param wallEnd - Wall end point [x, z]
*
* In wall-local geometry space (after ExtrudeGeometry + rotateX):
* - X axis: along wall direction
* - Y axis: up (height)
* - Z axis: perpendicular to wall (thickness direction)
*
* So valid side faces have normals pointing in ±Z direction (local space)
*/
const isValidWallSideFace = (
normal: [number, number, number] | undefined,
wallStart: [number, number],
wallEnd: [number, number],
): boolean => {
const isValidWallSideFace = (normal: [number, number, number] | undefined): boolean => {
if (!normal) return false
// Filter out top/bottom faces (normal pointing up or down)
if (Math.abs(normal[1]) > 0.3) return false
// Calculate wall direction in X-Z plane
const wallDirX = wallEnd[0] - wallStart[0]
const wallDirZ = wallEnd[1] - wallStart[1]
const wallLength = Math.sqrt(wallDirX * wallDirX + wallDirZ * wallDirZ)
if (wallLength === 0) return false
// Normalize wall direction
const normWallDirX = wallDirX / wallLength
const normWallDirZ = wallDirZ / wallLength
// Dot product of normal (X-Z components) with wall direction
// Front/back faces: normal perpendicular to wall → dot ≈ 0
// Thickness edges: normal parallel to wall → dot ≈ ±1
const dotProduct = normal[0] * normWallDirX + normal[2] * normWallDirZ
// If dot product is high, it's a thickness edge (normal parallel to wall)
// Allow only faces where normal is mostly perpendicular to wall direction
return Math.abs(dotProduct) < 0.5
// Valid side faces have normals pointing in the local Z direction (perpendicular to wall)
// This filters out top faces (Y direction) and end caps/junctions (X direction)
return Math.abs(normal[2]) > 0.7
}
export const ItemTool: React.FC = () => {
@@ -223,8 +227,9 @@ export const ItemTool: React.FC = () => {
draftItem.current?.asset.attachTo === 'wall' ||
draftItem.current?.asset.attachTo === 'wall-side'
) {
console.log('Wall enter:', event.node.id, event.normal)
// Ignore top face and thickness edges
if (!isValidWallSideFace(event.normal, event.node.start, event.node.end)) return
if (!isValidWallSideFace(event.normal)) return
event.stopPropagation()
isOnWall.current = true
@@ -232,7 +237,8 @@ export const ItemTool: React.FC = () => {
// Determine side and rotation from normal
const side = getSideFromNormal(event.normal)
const rotation = calculateRotationFromNormal(event.normal)
const itemRotation = calculateItemRotation(event.normal)
const cursorRotation = calculateCursorRotation(event.normal, event.node.start, event.node.end)
gridPosition.current.set(
Math.round(event.localPosition[0] * 2) / 2,
@@ -241,15 +247,15 @@ export const ItemTool: React.FC = () => {
)
draftItem.current.parentId = event.node.id
draftItem.current.side = side
draftItem.current.rotation = [0, rotation, 0]
draftItem.current.rotation = [0, itemRotation, 0]
useScene.getState().updateNode(draftItem.current.id, {
position: [gridPosition.current.x, gridPosition.current.y, gridPosition.current.z],
parentId: event.node.id,
side,
rotation: [0, rotation, 0],
rotation: [0, itemRotation, 0],
})
cursorRef.current.rotation.y = rotation
cursorRef.current.rotation.y = cursorRotation
checkCanPlace()
}
}
@@ -273,7 +279,7 @@ export const ItemTool: React.FC = () => {
if (!isOnWall.current) return
// Ignore top face and thickness edges
if (!isValidWallSideFace(event.normal, event.node.start, event.node.end)) return
if (!isValidWallSideFace(event.normal)) return
event.stopPropagation()
@@ -305,13 +311,14 @@ export const ItemTool: React.FC = () => {
if (!draftItem.current) return
// Ignore top face and thickness edges
if (!isValidWallSideFace(event.normal, event.node.start, event.node.end)) return
if (!isValidWallSideFace(event.normal)) return
event.stopPropagation()
// Determine side and rotation from normal
const side = getSideFromNormal(event.normal)
const rotation = calculateRotationFromNormal(event.normal)
const itemRotation = calculateItemRotation(event.normal)
const cursorRotation = calculateCursorRotation(event.normal, event.node.start, event.node.end)
gridPosition.current.set(
Math.round(event.localPosition[0] * 2) / 2,
@@ -323,11 +330,11 @@ export const ItemTool: React.FC = () => {
Math.round(event.position[1] * 2) / 2,
Math.round(event.position[2] * 2) / 2,
)
cursorRef.current.rotation.y = rotation
cursorRef.current.rotation.y = cursorRotation
// Update draft item side and rotation
draftItem.current.side = side
draftItem.current.rotation = [0, rotation, 0]
draftItem.current.rotation = [0, itemRotation, 0]
const canPlace = checkCanPlace()
if (draftItem.current && canPlace) {
@@ -339,12 +346,12 @@ export const ItemTool: React.FC = () => {
const draftItemMesh = sceneRegistry.nodes.get(draftItem.current.id)
if (draftItemMesh) {
draftItemMesh.position.copy(gridPosition.current)
draftItemMesh.rotation.y = rotation
draftItemMesh.rotation.y = itemRotation
}
useScene.getState().updateNode(draftItem.current.id, {
side,
rotation: [0, rotation, 0],
rotation: [0, itemRotation, 0],
})
useScene.getState().dirtyNodes.add(event.node.id)
}