fix wall mitering

This commit is contained in:
wass08
2026-01-26 18:06:39 +09:00
parent e13005dcda
commit abd8e327fe
3 changed files with 191 additions and 390 deletions
+7
View File
@@ -0,0 +1,7 @@
{
"permissions": {
"allow": [
"Bash(npx turbo:*)"
]
}
}
+83 -274
View File
@@ -15,24 +15,11 @@ interface LineEquation {
c: number // ax + by + c = 0
}
interface WallEndpoint {
wall: WallNode
endType: 'start' | 'end'
}
// Map of wallId -> { left?: Point2D, right?: Point2D } for each junction
type WallIntersections = Map<string, { left?: Point2D; right?: Point2D }>
interface Junction {
point: Point2D
walls: WallEndpoint[]
}
export interface MiterData {
left: Point2D
right: Point2D
center: Point2D // The junction meeting point
}
// Map of wallId -> { start?: MiterData, end?: MiterData }
export type WallMiterMap = Map<string, { start?: MiterData; end?: MiterData }>
// Map of junctionKey -> WallIntersections
type JunctionData = Map<string, WallIntersections>
// ============================================================================
// UTILITY FUNCTIONS
@@ -52,72 +39,40 @@ function createLineFromPointAndVector(p: Point2D, v: Point2D): LineEquation {
return { a, b, c }
}
function intersectLines(l1: LineEquation, l2: LineEquation): Point2D | null {
const det = l1.a * l2.b - l2.a * l1.b
if (Math.abs(det) < 1e-9) return null
const x = (l1.b * l2.c - l2.b * l1.c) / det
const y = (l2.a * l1.c - l1.a * l2.c) / det
return { x, y }
}
function dot(a: Point2D, b: Point2D): number {
return a.x * b.x + a.y * b.y
}
function pointOnWallSegment(
point: Point2D,
wallStart: Point2D,
wallEnd: Point2D,
tolerance = TOLERANCE,
): boolean {
const wallVec = { x: wallEnd.x - wallStart.x, y: wallEnd.y - wallStart.y }
const wallLen = Math.sqrt(wallVec.x * wallVec.x + wallVec.y * wallVec.y)
if (wallLen < 1e-9) return false
const toPoint = { x: point.x - wallStart.x, y: point.y - wallStart.y }
const t = dot(toPoint, wallVec) / (wallLen * wallLen)
if (t <= tolerance / wallLen || t >= 1 - tolerance / wallLen) return false
const projX = wallStart.x + t * wallVec.x
const projY = wallStart.y + t * wallVec.y
const dist = Math.sqrt((point.x - projX) ** 2 + (point.y - projY) ** 2)
return dist < tolerance
}
// ============================================================================
// JUNCTION DETECTION
// JUNCTION DETECTION (exactly like demo)
// ============================================================================
/**
* Finds all junctions where wall endpoints meet
*/
interface Junction {
meetingPoint: Point2D
connectedWalls: Array<{ wall: WallNode; endType: 'start' | 'end' }>
}
function findJunctions(walls: WallNode[]): Map<string, Junction> {
const junctionMap = new Map<string, Junction>()
const junctions = new Map<string, Junction>()
for (const wall of walls) {
const startPt: Point2D = { x: wall.start[0], y: wall.start[1] }
const endPt: Point2D = { x: wall.end[0], y: wall.end[1] }
const startKey = pointToKey(startPt)
const endKey = pointToKey(endPt)
const keyStart = pointToKey(startPt)
const keyEnd = pointToKey(endPt)
if (!junctionMap.has(startKey)) {
junctionMap.set(startKey, { point: startPt, walls: [] })
if (!junctions.has(keyStart)) {
junctions.set(keyStart, { meetingPoint: startPt, connectedWalls: [] })
}
junctionMap.get(startKey)!.walls.push({ wall, endType: 'start' })
junctions.get(keyStart)!.connectedWalls.push({ wall, endType: 'start' })
if (!junctionMap.has(endKey)) {
junctionMap.set(endKey, { point: endPt, walls: [] })
if (!junctions.has(keyEnd)) {
junctions.set(keyEnd, { meetingPoint: endPt, connectedWalls: [] })
}
junctionMap.get(endKey)!.walls.push({ wall, endType: 'end' })
junctions.get(keyEnd)!.connectedWalls.push({ wall, endType: 'end' })
}
// Only keep junctions with 2+ walls
// Filter to only junctions with 2+ walls
const actualJunctions = new Map<string, Junction>()
for (const [key, junction] of junctionMap) {
if (junction.walls.length >= 2) {
for (const [key, junction] of junctions.entries()) {
if (junction.connectedWalls.length >= 2) {
actualJunctions.set(key, junction)
}
}
@@ -126,36 +81,24 @@ function findJunctions(walls: WallNode[]): Map<string, Junction> {
}
// ============================================================================
// MITER CALCULATION (Simple approach from prototype)
// MITER CALCULATION (exactly like demo)
// ============================================================================
interface ProcessedWall {
wallId: string
endType: 'start' | 'end'
angle: number
edgeA: LineEquation // Left edge (CCW from outgoing direction)
edgeB: LineEquation // Right edge (CW from outgoing direction)
edgeA: LineEquation // Left edge
edgeB: LineEquation // Right edge
}
/**
* Calculates miter intersections for a junction
* Simple algorithm from prototype:
* 1. Get outgoing vector for each wall (pointing away from junction)
* 2. Calculate left/right edge lines offset by halfThickness
* 3. Sort walls by outgoing angle
* 4. Intersect adjacent edges: wall[i].edgeA ∩ wall[i+1].edgeB
* 5. Assign: wall[k].left = intersection[k], wall[k].right = intersection[k-1]
*/
function calculateJunctionMiters(
function calculateJunctionIntersections(
junction: Junction,
getThickness: (wall: WallNode) => number,
): Map<string, MiterData> {
const { point, walls } = junction
const result = new Map<string, MiterData>()
): WallIntersections {
const { meetingPoint, connectedWalls } = junction
const processedWalls: ProcessedWall[] = []
// Process each wall at this junction
for (const { wall, endType } of walls) {
for (const { wall, endType } of connectedWalls) {
const halfT = getThickness(wall) / 2
// Outgoing vector (pointing away from junction)
@@ -171,226 +114,100 @@ function calculateJunctionMiters(
const nUnit = { x: -v.y / L, y: v.x / L }
// Points on left (A) and right (B) edges at the junction
const pA = { x: point.x + nUnit.x * halfT, y: point.y + nUnit.y * halfT }
const pB = { x: point.x - nUnit.x * halfT, y: point.y - nUnit.y * halfT }
const pA = { x: meetingPoint.x + nUnit.x * halfT, y: meetingPoint.y + nUnit.y * halfT }
const pB = { x: meetingPoint.x - nUnit.x * halfT, y: meetingPoint.y - nUnit.y * halfT }
// Edge lines
// Edge lines (direction parallel to wall)
const edgeA = createLineFromPointAndVector(pA, v)
const edgeB = createLineFromPointAndVector(pB, v)
// Angle for sorting
const angle = Math.atan2(v.y, v.x)
processedWalls.push({ wallId: wall.id, endType, angle, edgeA, edgeB })
processedWalls.push({ wallId: wall.id, angle, edgeA, edgeB })
}
// Sort by outgoing angle
processedWalls.sort((a, b) => a.angle - b.angle)
const n = processedWalls.length
if (n < 2) return result
console.log(`\n=== Junction at (${meetingPoint.x.toFixed(2)}, ${meetingPoint.y.toFixed(2)}) ===`)
console.log('Walls sorted by angle:')
for (const w of processedWalls) {
console.log(` ${w.wallId}: angle=${((w.angle * 180) / Math.PI).toFixed(1)}°`)
}
// Calculate intersections between adjacent walls
const intersections: Point2D[] = []
const wallIntersections = new Map<string, { left?: Point2D; right?: Point2D }>()
const n = processedWalls.length
if (n < 2) return wallIntersections
// Calculate intersections between adjacent walls (exactly like demo)
for (let i = 0; i < n; i++) {
const wall1 = processedWalls[i]!
const wall2 = processedWalls[(i + 1) % n]!
// Intersect left edge of wall1 with right edge of wall2
const intersection = intersectLines(wall1.edgeA, wall2.edgeB)
const det = wall1.edgeA.a * wall2.edgeB.b - wall2.edgeB.a * wall1.edgeA.b
// If parallel, use junction center
intersections.push(intersection ?? point)
// If lines are parallel (det ≈ 0), skip this intersection - walls will use defaults
if (Math.abs(det) < 1e-9) {
console.log(`Intersection ${i}: wall1=${wall1.wallId}.edgeA ∩ wall2=${wall2.wallId}.edgeB = PARALLEL (skipped)`)
continue
}
// Assign miter data to each wall
// wall[k].left = intersection[k], wall[k].right = intersection[k-1]
for (let k = 0; k < n; k++) {
const wall = processedWalls[k]!
const prevIdx = (k - 1 + n) % n
result.set(wall.wallId, {
left: intersections[k]!,
right: intersections[prevIdx]!,
center: point, // Junction center point
})
const p = {
x: (wall1.edgeA.b * wall2.edgeB.c - wall2.edgeB.b * wall1.edgeA.c) / det,
y: (wall2.edgeB.a * wall1.edgeA.c - wall1.edgeA.a * wall2.edgeB.c) / det,
}
return result
}
console.log(`Intersection ${i}: wall1=${wall1.wallId}.edgeA ∩ wall2=${wall2.wallId}.edgeB = (${p.x.toFixed(3)}, ${p.y.toFixed(3)})`)
console.log(` -> ${wall1.wallId}.left = p, ${wall2.wallId}.right = p`)
// ============================================================================
// T-JUNCTION HANDLING
// ============================================================================
// Assign intersection to both walls (exactly like demo)
if (!wallIntersections.has(wall1.wallId)) {
wallIntersections.set(wall1.wallId, {})
}
wallIntersections.get(wall1.wallId)!.left = p
/**
* Finds T-junctions where a wall endpoint meets another wall's side
*/
function findTJunctions(walls: WallNode[]): Map<string, { junction: Junction; hostWall: WallNode }> {
const tJunctions = new Map<string, { junction: Junction; hostWall: WallNode }>()
for (const wall of walls) {
const endpoints: { pt: Point2D; endType: 'start' | 'end' }[] = [
{ pt: { x: wall.start[0], y: wall.start[1] }, endType: 'start' },
{ pt: { x: wall.end[0], y: wall.end[1] }, endType: 'end' },
]
for (const { pt, endType } of endpoints) {
const key = pointToKey(pt)
for (const otherWall of walls) {
if (otherWall.id === wall.id) continue
const otherStart: Point2D = { x: otherWall.start[0], y: otherWall.start[1] }
const otherEnd: Point2D = { x: otherWall.end[0], y: otherWall.end[1] }
// Skip if touching endpoints (handled by regular junctions)
if (pointToKey(pt) === pointToKey(otherStart)) continue
if (pointToKey(pt) === pointToKey(otherEnd)) continue
// Check if endpoint lies on the other wall's segment
if (pointOnWallSegment(pt, otherStart, otherEnd)) {
if (!tJunctions.has(key)) {
tJunctions.set(key, {
junction: { point: pt, walls: [] },
hostWall: otherWall,
})
if (!wallIntersections.has(wall2.wallId)) {
wallIntersections.set(wall2.wallId, {})
}
wallIntersections.get(wall2.wallId)!.right = p
}
const entry = tJunctions.get(key)!
if (!entry.junction.walls.some((w) => w.wall.id === wall.id && w.endType === endType)) {
entry.junction.walls.push({ wall, endType })
}
}
}
}
console.log('Final wall intersections:')
for (const [id, data] of wallIntersections) {
console.log(` ${id}: left=(${data.left?.x.toFixed(3)}, ${data.left?.y.toFixed(3)}), right=(${data.right?.x.toFixed(3)}, ${data.right?.y.toFixed(3)})`)
}
return tJunctions
}
/**
* Calculates miter for T-junction (wall endpoint meeting another wall's side)
*/
function calculateTJunctionMiters(
junction: Junction,
hostWall: WallNode,
getThickness: (wall: WallNode) => number,
): Map<string, MiterData> {
const { point, walls } = junction
const result = new Map<string, MiterData>()
// Host wall direction and normal
const hostDir = {
x: hostWall.end[0] - hostWall.start[0],
y: hostWall.end[1] - hostWall.start[1],
}
const hostLen = Math.sqrt(hostDir.x * hostDir.x + hostDir.y * hostDir.y)
if (hostLen < 1e-9) return result
const hostDirNorm = { x: hostDir.x / hostLen, y: hostDir.y / hostLen }
const hostNormal = { x: -hostDirNorm.y, y: hostDirNorm.x }
const hostHalfT = getThickness(hostWall) / 2
// Host wall edge lines at the junction point
const hostLeft = { x: point.x + hostNormal.x * hostHalfT, y: point.y + hostNormal.y * hostHalfT }
const hostRight = { x: point.x - hostNormal.x * hostHalfT, y: point.y - hostNormal.y * hostHalfT }
const hostEdgeLeft = createLineFromPointAndVector(hostLeft, hostDirNorm)
const hostEdgeRight = createLineFromPointAndVector(hostRight, hostDirNorm)
// For each incoming wall, extend to meet host wall's edge
for (const { wall, endType } of walls) {
const halfT = getThickness(wall) / 2
// Outgoing vector
const v =
endType === 'start'
? { x: wall.end[0] - wall.start[0], y: wall.end[1] - wall.start[1] }
: { x: wall.start[0] - wall.end[0], y: wall.start[1] - wall.end[1] }
const L = Math.sqrt(v.x * v.x + v.y * v.y)
if (L < 1e-9) continue
const vNorm = { x: v.x / L, y: v.y / L }
const normal = { x: -vNorm.y, y: vNorm.x }
// Edge points
const leftPt = { x: point.x + normal.x * halfT, y: point.y + normal.y * halfT }
const rightPt = { x: point.x - normal.x * halfT, y: point.y - normal.y * halfT }
// Edge lines
const edgeLeft = createLineFromPointAndVector(leftPt, v)
const edgeRight = createLineFromPointAndVector(rightPt, v)
// Determine which host edge to intersect with
// Use incoming direction (opposite of outgoing) dotted with host normal
const incomingDir = { x: -vNorm.x, y: -vNorm.y }
const approachDot = dot(incomingDir, hostNormal)
// Pick host edge facing the incoming wall
const targetHostEdge = approachDot > 0 ? hostEdgeRight : hostEdgeLeft
// Both edges meet the same host edge
const leftInt = intersectLines(edgeLeft, targetHostEdge)
const rightInt = intersectLines(edgeRight, targetHostEdge)
result.set(wall.id, {
left: leftInt ?? leftPt,
right: rightInt ?? rightPt,
center: point,
})
}
return result
return wallIntersections
}
// ============================================================================
// MAIN EXPORT
// ============================================================================
export interface WallMiterData {
// Junction data keyed by junction position key
junctionData: JunctionData
// All junctions for quick lookup
junctions: Map<string, Junction>
}
/**
* Calculates miter data for all walls on a level
*/
export function calculateLevelMiters(walls: WallNode[]): WallMiterMap {
const miterMap: WallMiterMap = new Map()
export function calculateLevelMiters(walls: WallNode[]): WallMiterData {
const getThickness = (wall: WallNode) => wall.thickness ?? 0.1
// Process regular junctions (2+ walls meeting at endpoints)
const junctions = findJunctions(walls)
for (const [, junction] of junctions) {
const miters = calculateJunctionMiters(junction, getThickness)
const junctionData: JunctionData = new Map()
for (const { wall, endType } of junction.walls) {
const miterData = miters.get(wall.id)
if (!miterData) continue
if (!miterMap.has(wall.id)) {
miterMap.set(wall.id, {})
}
miterMap.get(wall.id)![endType] = miterData
}
for (const [key, junction] of junctions.entries()) {
const wallIntersections = calculateJunctionIntersections(junction, getThickness)
junctionData.set(key, wallIntersections)
}
// Process T-junctions (wall endpoint on another wall's side)
const tJunctions = findTJunctions(walls)
for (const [, { junction, hostWall }] of tJunctions) {
const miters = calculateTJunctionMiters(junction, hostWall, getThickness)
for (const { wall, endType } of junction.walls) {
const miterData = miters.get(wall.id)
if (!miterData) continue
// Don't overwrite existing miter data
if (miterMap.get(wall.id)?.[endType]) continue
if (!miterMap.has(wall.id)) {
miterMap.set(wall.id, {})
}
miterMap.get(wall.id)![endType] = miterData
}
}
return miterMap
return { junctionData, junctions }
}
/**
@@ -423,17 +240,6 @@ export function getAdjacentWallIds(allWalls: WallNode[], dirtyWallIds: Set<strin
startKey === dirtyEndKey ||
endKey === dirtyStartKey ||
endKey === dirtyEndKey
) {
adjacent.add(wall.id)
continue
}
// Check T-junction connections
if (
pointOnWallSegment(dirtyStart, wallStart, wallEnd) ||
pointOnWallSegment(dirtyEnd, wallStart, wallEnd) ||
pointOnWallSegment(wallStart, dirtyStart, dirtyEnd) ||
pointOnWallSegment(wallEnd, dirtyStart, dirtyEnd)
) {
adjacent.add(wall.id)
}
@@ -442,3 +248,6 @@ export function getAdjacentWallIds(allWalls: WallNode[], dirtyWallIds: Set<strin
return adjacent
}
// Re-export for backwards compatibility
export { pointToKey }
+96 -111
View File
@@ -6,9 +6,9 @@ import useScene from '../../store/use-scene'
import {
calculateLevelMiters,
getAdjacentWallIds,
type MiterData,
pointToKey,
type Point2D,
type WallMiterMap,
type WallMiterData,
} from './wall-mitering'
// ============================================================================
@@ -40,13 +40,13 @@ export const WallSystem = () => {
// Process each level that has dirty walls
for (const [levelId, dirtyWallIds] of dirtyWallsByLevel) {
const levelWalls = getLevelWalls(levelId)
const miterMap = calculateLevelMiters(levelWalls)
const miterData = calculateLevelMiters(levelWalls)
// Update dirty walls
for (const wallId of dirtyWallIds) {
const mesh = sceneRegistry.nodes.get(wallId) as THREE.Mesh
if (mesh) {
updateWallGeometry(wallId, miterMap)
updateWallGeometry(wallId, miterData)
}
clearDirty(wallId as AnyNodeId)
}
@@ -57,7 +57,7 @@ export const WallSystem = () => {
if (!dirtyWallIds.has(wallId)) {
const mesh = sceneRegistry.nodes.get(wallId) as THREE.Mesh
if (mesh) {
updateWallGeometry(wallId, miterMap)
updateWallGeometry(wallId, miterData)
}
}
}
@@ -90,7 +90,7 @@ function getLevelWalls(levelId: string): WallNode[] {
/**
* Updates the geometry for a single wall
*/
function updateWallGeometry(wallId: string, miterMap: WallMiterMap) {
function updateWallGeometry(wallId: string, miterData: WallMiterData) {
const node = useScene.getState().nodes[wallId as WallNode['id']]
if (!node || node.type !== 'wall') return
@@ -102,8 +102,7 @@ function updateWallGeometry(wallId: string, miterMap: WallMiterMap) {
.map((childId) => useScene.getState().nodes[childId])
.filter((n): n is AnyNode => n !== undefined)
const miters = miterMap.get(wallId)
const newGeo = generateExtrudedWall(node, childrenNodes, miters)
const newGeo = generateExtrudedWall(node, childrenNodes, miterData)
mesh.geometry.dispose()
mesh.geometry = newGeo
@@ -111,7 +110,7 @@ function updateWallGeometry(wallId: string, miterMap: WallMiterMap) {
// Update collision mesh
const collisionMesh = mesh.getObjectByName('collision-mesh') as THREE.Mesh
if (collisionMesh) {
const collisionGeo = generateExtrudedWall(node, [], miters)
const collisionGeo = generateExtrudedWall(node, [], miterData)
collisionMesh.geometry.dispose()
collisionMesh.geometry = collisionGeo
}
@@ -122,132 +121,118 @@ function updateWallGeometry(wallId: string, miterMap: WallMiterMap) {
}
/**
* Generates extruded wall geometry with mitering and holes
* Generates extruded wall geometry with mitering (exactly like demo)
*
* Geometry approach:
* - Shape is drawn on XY plane (X = along wall, Y = height)
* - Extruded by wall thickness along Z
* - This allows holes (doors/windows) to work correctly on the wall face
* - Mitering adjusts the extrusion offset at start/end
* Key insight from demo: polygon is built in WORLD coordinates first,
* then we transform to wall-local for the 3D mesh.
*/
export function generateExtrudedWall(
wallNode: WallNode,
_childrenNodes: AnyNode[], // TODO: Use for hole cutting (doors/windows)
miters?: { start?: MiterData; end?: MiterData },
miterData: WallMiterData,
) {
const start = new THREE.Vector2(wallNode.start[0], wallNode.start[1])
const end = new THREE.Vector2(wallNode.end[0], wallNode.end[1])
const length = start.distanceTo(end)
const { junctionData } = miterData
const wallStart: Point2D = { x: wallNode.start[0], y: wallNode.start[1] }
const wallEnd: Point2D = { x: wallNode.end[0], y: wallNode.end[1] }
const height = wallNode.height ?? 2.5
const thickness = wallNode.thickness ?? 0.1
const halfT = thickness / 2
console.log(`\n=== generateExtrudedWall: ${wallNode.id} ===`)
console.log('Wall:', { start: wallNode.start, end: wallNode.end, length, thickness })
console.log('Miters received:', miters)
// Wall direction and normal (exactly like demo)
const v = { x: wallEnd.x - wallStart.x, y: wallEnd.y - wallStart.y }
const L = Math.sqrt(v.x * v.x + v.y * v.y)
if (L < 1e-9) {
return new THREE.BufferGeometry()
}
const nUnit = { x: -v.y / L, y: v.x / L }
// Wall angle for coordinate transforms
const wallAngle = Math.atan2(end.y - start.y, end.x - start.x)
// Get junction data for start and end (exactly like demo)
const keyStart = pointToKey(wallStart)
const keyEnd = pointToKey(wallEnd)
const startJunction = junctionData.get(keyStart)?.get(wallNode.id)
const endJunction = junctionData.get(keyEnd)?.get(wallNode.id)
console.log(`\n=== Wall ${wallNode.id} ===`)
console.log('Start:', wallStart, 'End:', wallEnd, 'Thickness:', thickness)
console.log('Key start:', keyStart, 'Key end:', keyEnd)
console.log('Start junction data:', startJunction)
console.log('End junction data:', endJunction)
// Calculate polygon corners in world coordinates (exactly like demo)
// p_start_L = left side at start
// p_start_R = right side at start
// p_end_L = left side at end
// p_end_R = right side at end
const p_start_L: Point2D = startJunction?.left || {
x: wallStart.x + nUnit.x * halfT,
y: wallStart.y + nUnit.y * halfT,
}
const p_start_R: Point2D = startJunction?.right || {
x: wallStart.x - nUnit.x * halfT,
y: wallStart.y - nUnit.y * halfT,
}
// At end, SWAP left/right from junction data (exactly like demo)
// This is because junction stores left/right relative to OUTGOING direction,
// which is reversed at the end of the wall
const p_end_L: Point2D = endJunction?.right || {
x: wallEnd.x + nUnit.x * halfT,
y: wallEnd.y + nUnit.y * halfT,
}
const p_end_R: Point2D = endJunction?.left || {
x: wallEnd.x - nUnit.x * halfT,
y: wallEnd.y - nUnit.y * halfT,
}
console.log('Polygon corners (world coords):')
console.log(' p_start_L:', p_start_L, startJunction ? '(from junction)' : '(default)')
console.log(' p_start_R:', p_start_R, startJunction ? '(from junction)' : '(default)')
console.log(' p_end_L:', p_end_L, endJunction ? '(from junction, swapped)' : '(default)')
console.log(' p_end_R:', p_end_R, endJunction ? '(from junction, swapped)' : '(default)')
// Build polygon points (exactly like demo)
// Order: start-right -> end-right -> [end center] -> end-left -> start-left -> [start center]
const polyPoints: Point2D[] = [p_start_R, p_end_R]
if (endJunction) {
polyPoints.push(wallEnd) // Add center vertex at junction
}
polyPoints.push(p_end_L, p_start_L)
if (startJunction) {
polyPoints.push(wallStart) // Add center vertex at junction
}
console.log('Polygon order:', polyPoints.length, 'points')
console.log(' Has end junction:', !!endJunction, '| Has start junction:', !!startJunction)
// Transform world coordinates to wall-local coordinates
// Wall-local: x along wall, z perpendicular (thickness direction)
const wallAngle = Math.atan2(v.y, v.x)
const cosA = Math.cos(-wallAngle)
const sinA = Math.sin(-wallAngle)
// Transform world point to wall-local space
const worldToLocal = (worldPt: Point2D): { x: number; z: number } => {
const dx = worldPt.x - wallNode.start[0]
const dy = worldPt.y - wallNode.start[1]
const dx = worldPt.x - wallStart.x
const dy = worldPt.y - wallStart.y
return {
x: dx * cosA - dy * sinA,
z: dx * sinA + dy * cosA,
}
}
// Default miter points (no junction - simple rectangle)
const defaultStart = {
left: { x: 0, z: halfT },
right: { x: 0, z: -halfT },
center: { x: 0, z: 0 },
hasJunction: false,
}
const defaultEnd = {
left: { x: length, z: halfT },
right: { x: length, z: -halfT },
center: { x: length, z: 0 },
hasJunction: false,
}
// Convert polygon to local coordinates
const localPoints = polyPoints.map(worldToLocal)
// Apply miter data if available
let startMiter = defaultStart
let endMiter = defaultEnd
if (miters?.start) {
const left = worldToLocal(miters.start.left)
const right = worldToLocal(miters.start.right)
const center = worldToLocal(miters.start.center)
startMiter = { left, right, center, hasJunction: true }
}
if (miters?.end) {
// At end, left/right are swapped because outgoing direction is reversed
const left = worldToLocal(miters.end.right)
const right = worldToLocal(miters.end.left)
const center = worldToLocal(miters.end.center)
endMiter = { left, right, center, hasJunction: true }
}
// Create geometry
const geometry = createMiteredExtrudeGeometry(height, startMiter, endMiter)
return geometry
}
interface MiterPoint {
x: number
z: number
}
interface MiterEnd {
left: MiterPoint
right: MiterPoint
center: MiterPoint
hasJunction: boolean
}
/**
* Creates wall geometry using footprint polygon approach
*
* Footprint has 6 vertices - 3 on each thickness edge (start/end):
* - start-right, start-center (if junction), start-left
* - end-left, end-center (if junction), end-right
*
* Based on the prototype: center vertices are only added when there's a junction
*/
function createMiteredExtrudeGeometry(
height: number,
startMiter: MiterEnd,
endMiter: MiterEnd,
): THREE.BufferGeometry {
// Build footprint polygon (CCW winding, viewed from above)
// Following prototype: start-right -> end-right -> [end-center] -> end-left -> start-left -> [start-center]
// Build THREE.js shape
// Shape uses (x, y) where we map: shape.x = local.x, shape.y = -local.z
// The negation is needed because after rotateX(-PI/2), shape.y becomes -geometry.z
const footprint = new THREE.Shape()
// Start from start-right, go to end-right
footprint.moveTo(startMiter.right.x, -startMiter.right.z)
footprint.lineTo(endMiter.right.x, -endMiter.right.z)
// Add end-center if there's a junction at end
if (endMiter.hasJunction) {
footprint.lineTo(endMiter.center.x, -endMiter.center.z)
footprint.moveTo(localPoints[0]!.x, -localPoints[0]!.z)
for (let i = 1; i < localPoints.length; i++) {
footprint.lineTo(localPoints[i]!.x, -localPoints[i]!.z)
}
// Continue to end-left, then start-left
footprint.lineTo(endMiter.left.x, -endMiter.left.z)
footprint.lineTo(startMiter.left.x, -startMiter.left.z)
// Add start-center if there's a junction at start
if (startMiter.hasJunction) {
footprint.lineTo(startMiter.center.x, -startMiter.center.z)
}
footprint.closePath()
// Extrude along Z by height