fix wall mitering
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(npx turbo:*)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,24 +15,11 @@ interface LineEquation {
|
|||||||
c: number // ax + by + c = 0
|
c: number // ax + by + c = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WallEndpoint {
|
// Map of wallId -> { left?: Point2D, right?: Point2D } for each junction
|
||||||
wall: WallNode
|
type WallIntersections = Map<string, { left?: Point2D; right?: Point2D }>
|
||||||
endType: 'start' | 'end'
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Junction {
|
// Map of junctionKey -> WallIntersections
|
||||||
point: Point2D
|
type JunctionData = Map<string, WallIntersections>
|
||||||
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 }>
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// UTILITY FUNCTIONS
|
// UTILITY FUNCTIONS
|
||||||
@@ -52,72 +39,40 @@ function createLineFromPointAndVector(p: Point2D, v: Point2D): LineEquation {
|
|||||||
return { a, b, c }
|
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)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
/**
|
interface Junction {
|
||||||
* Finds all junctions where wall endpoints meet
|
meetingPoint: Point2D
|
||||||
*/
|
connectedWalls: Array<{ wall: WallNode; endType: 'start' | 'end' }>
|
||||||
|
}
|
||||||
|
|
||||||
function findJunctions(walls: WallNode[]): Map<string, Junction> {
|
function findJunctions(walls: WallNode[]): Map<string, Junction> {
|
||||||
const junctionMap = new Map<string, Junction>()
|
const junctions = new Map<string, Junction>()
|
||||||
|
|
||||||
for (const wall of walls) {
|
for (const wall of walls) {
|
||||||
const startPt: Point2D = { x: wall.start[0], y: wall.start[1] }
|
const startPt: Point2D = { x: wall.start[0], y: wall.start[1] }
|
||||||
const endPt: Point2D = { x: wall.end[0], y: wall.end[1] }
|
const endPt: Point2D = { x: wall.end[0], y: wall.end[1] }
|
||||||
|
|
||||||
const startKey = pointToKey(startPt)
|
const keyStart = pointToKey(startPt)
|
||||||
const endKey = pointToKey(endPt)
|
const keyEnd = pointToKey(endPt)
|
||||||
|
|
||||||
if (!junctionMap.has(startKey)) {
|
if (!junctions.has(keyStart)) {
|
||||||
junctionMap.set(startKey, { point: startPt, walls: [] })
|
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)) {
|
if (!junctions.has(keyEnd)) {
|
||||||
junctionMap.set(endKey, { point: endPt, walls: [] })
|
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>()
|
const actualJunctions = new Map<string, Junction>()
|
||||||
for (const [key, junction] of junctionMap) {
|
for (const [key, junction] of junctions.entries()) {
|
||||||
if (junction.walls.length >= 2) {
|
if (junction.connectedWalls.length >= 2) {
|
||||||
actualJunctions.set(key, junction)
|
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 {
|
interface ProcessedWall {
|
||||||
wallId: string
|
wallId: string
|
||||||
endType: 'start' | 'end'
|
|
||||||
angle: number
|
angle: number
|
||||||
edgeA: LineEquation // Left edge (CCW from outgoing direction)
|
edgeA: LineEquation // Left edge
|
||||||
edgeB: LineEquation // Right edge (CW from outgoing direction)
|
edgeB: LineEquation // Right edge
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function calculateJunctionIntersections(
|
||||||
* 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(
|
|
||||||
junction: Junction,
|
junction: Junction,
|
||||||
getThickness: (wall: WallNode) => number,
|
getThickness: (wall: WallNode) => number,
|
||||||
): Map<string, MiterData> {
|
): WallIntersections {
|
||||||
const { point, walls } = junction
|
const { meetingPoint, connectedWalls } = junction
|
||||||
const result = new Map<string, MiterData>()
|
|
||||||
const processedWalls: ProcessedWall[] = []
|
const processedWalls: ProcessedWall[] = []
|
||||||
|
|
||||||
// Process each wall at this junction
|
for (const { wall, endType } of connectedWalls) {
|
||||||
for (const { wall, endType } of walls) {
|
|
||||||
const halfT = getThickness(wall) / 2
|
const halfT = getThickness(wall) / 2
|
||||||
|
|
||||||
// Outgoing vector (pointing away from junction)
|
// Outgoing vector (pointing away from junction)
|
||||||
@@ -171,226 +114,100 @@ function calculateJunctionMiters(
|
|||||||
const nUnit = { x: -v.y / L, y: v.x / L }
|
const nUnit = { x: -v.y / L, y: v.x / L }
|
||||||
|
|
||||||
// Points on left (A) and right (B) edges at the junction
|
// 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 pA = { x: meetingPoint.x + nUnit.x * halfT, y: meetingPoint.y + nUnit.y * halfT }
|
||||||
const pB = { x: point.x - nUnit.x * halfT, y: point.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 edgeA = createLineFromPointAndVector(pA, v)
|
||||||
const edgeB = createLineFromPointAndVector(pB, v)
|
const edgeB = createLineFromPointAndVector(pB, v)
|
||||||
|
|
||||||
// Angle for sorting
|
// Angle for sorting
|
||||||
const angle = Math.atan2(v.y, v.x)
|
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
|
// Sort by outgoing angle
|
||||||
processedWalls.sort((a, b) => a.angle - b.angle)
|
processedWalls.sort((a, b) => a.angle - b.angle)
|
||||||
|
|
||||||
const n = processedWalls.length
|
console.log(`\n=== Junction at (${meetingPoint.x.toFixed(2)}, ${meetingPoint.y.toFixed(2)}) ===`)
|
||||||
if (n < 2) return result
|
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 wallIntersections = new Map<string, { left?: Point2D; right?: Point2D }>()
|
||||||
const intersections: 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++) {
|
for (let i = 0; i < n; i++) {
|
||||||
const wall1 = processedWalls[i]!
|
const wall1 = processedWalls[i]!
|
||||||
const wall2 = processedWalls[(i + 1) % n]!
|
const wall2 = processedWalls[(i + 1) % n]!
|
||||||
|
|
||||||
// Intersect left edge of wall1 with right edge of wall2
|
// 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
|
// If lines are parallel (det ≈ 0), skip this intersection - walls will use defaults
|
||||||
intersections.push(intersection ?? point)
|
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
|
const p = {
|
||||||
// wall[k].left = intersection[k], wall[k].right = intersection[k-1]
|
x: (wall1.edgeA.b * wall2.edgeB.c - wall2.edgeB.b * wall1.edgeA.c) / det,
|
||||||
for (let k = 0; k < n; k++) {
|
y: (wall2.edgeB.a * wall1.edgeA.c - wall1.edgeA.a * wall2.edgeB.c) / det,
|
||||||
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
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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`)
|
||||||
|
|
||||||
// ============================================================================
|
// Assign intersection to both walls (exactly like demo)
|
||||||
// T-JUNCTION HANDLING
|
if (!wallIntersections.has(wall1.wallId)) {
|
||||||
// ============================================================================
|
wallIntersections.set(wall1.wallId, {})
|
||||||
|
}
|
||||||
|
wallIntersections.get(wall1.wallId)!.left = p
|
||||||
|
|
||||||
/**
|
if (!wallIntersections.has(wall2.wallId)) {
|
||||||
* Finds T-junctions where a wall endpoint meets another wall's side
|
wallIntersections.set(wall2.wallId, {})
|
||||||
*/
|
}
|
||||||
function findTJunctions(walls: WallNode[]): Map<string, { junction: Junction; hostWall: WallNode }> {
|
wallIntersections.get(wall2.wallId)!.right = p
|
||||||
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,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const entry = tJunctions.get(key)!
|
console.log('Final wall intersections:')
|
||||||
if (!entry.junction.walls.some((w) => w.wall.id === wall.id && w.endType === endType)) {
|
for (const [id, data] of wallIntersections) {
|
||||||
entry.junction.walls.push({ wall, endType })
|
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
|
return wallIntersections
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// MAIN EXPORT
|
// 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
|
* Calculates miter data for all walls on a level
|
||||||
*/
|
*/
|
||||||
export function calculateLevelMiters(walls: WallNode[]): WallMiterMap {
|
export function calculateLevelMiters(walls: WallNode[]): WallMiterData {
|
||||||
const miterMap: WallMiterMap = new Map()
|
|
||||||
const getThickness = (wall: WallNode) => wall.thickness ?? 0.1
|
const getThickness = (wall: WallNode) => wall.thickness ?? 0.1
|
||||||
|
|
||||||
// Process regular junctions (2+ walls meeting at endpoints)
|
|
||||||
const junctions = findJunctions(walls)
|
const junctions = findJunctions(walls)
|
||||||
for (const [, junction] of junctions) {
|
const junctionData: JunctionData = new Map()
|
||||||
const miters = calculateJunctionMiters(junction, getThickness)
|
|
||||||
|
|
||||||
for (const { wall, endType } of junction.walls) {
|
for (const [key, junction] of junctions.entries()) {
|
||||||
const miterData = miters.get(wall.id)
|
const wallIntersections = calculateJunctionIntersections(junction, getThickness)
|
||||||
if (!miterData) continue
|
junctionData.set(key, wallIntersections)
|
||||||
|
|
||||||
if (!miterMap.has(wall.id)) {
|
|
||||||
miterMap.set(wall.id, {})
|
|
||||||
}
|
|
||||||
miterMap.get(wall.id)![endType] = miterData
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process T-junctions (wall endpoint on another wall's side)
|
return { junctionData, junctions }
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -423,17 +240,6 @@ export function getAdjacentWallIds(allWalls: WallNode[], dirtyWallIds: Set<strin
|
|||||||
startKey === dirtyEndKey ||
|
startKey === dirtyEndKey ||
|
||||||
endKey === dirtyStartKey ||
|
endKey === dirtyStartKey ||
|
||||||
endKey === dirtyEndKey
|
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)
|
adjacent.add(wall.id)
|
||||||
}
|
}
|
||||||
@@ -442,3 +248,6 @@ export function getAdjacentWallIds(allWalls: WallNode[], dirtyWallIds: Set<strin
|
|||||||
|
|
||||||
return adjacent
|
return adjacent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Re-export for backwards compatibility
|
||||||
|
export { pointToKey }
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import useScene from '../../store/use-scene'
|
|||||||
import {
|
import {
|
||||||
calculateLevelMiters,
|
calculateLevelMiters,
|
||||||
getAdjacentWallIds,
|
getAdjacentWallIds,
|
||||||
type MiterData,
|
pointToKey,
|
||||||
type Point2D,
|
type Point2D,
|
||||||
type WallMiterMap,
|
type WallMiterData,
|
||||||
} from './wall-mitering'
|
} from './wall-mitering'
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -40,13 +40,13 @@ export const WallSystem = () => {
|
|||||||
// Process each level that has dirty walls
|
// Process each level that has dirty walls
|
||||||
for (const [levelId, dirtyWallIds] of dirtyWallsByLevel) {
|
for (const [levelId, dirtyWallIds] of dirtyWallsByLevel) {
|
||||||
const levelWalls = getLevelWalls(levelId)
|
const levelWalls = getLevelWalls(levelId)
|
||||||
const miterMap = calculateLevelMiters(levelWalls)
|
const miterData = calculateLevelMiters(levelWalls)
|
||||||
|
|
||||||
// Update dirty walls
|
// Update dirty walls
|
||||||
for (const wallId of dirtyWallIds) {
|
for (const wallId of dirtyWallIds) {
|
||||||
const mesh = sceneRegistry.nodes.get(wallId) as THREE.Mesh
|
const mesh = sceneRegistry.nodes.get(wallId) as THREE.Mesh
|
||||||
if (mesh) {
|
if (mesh) {
|
||||||
updateWallGeometry(wallId, miterMap)
|
updateWallGeometry(wallId, miterData)
|
||||||
}
|
}
|
||||||
clearDirty(wallId as AnyNodeId)
|
clearDirty(wallId as AnyNodeId)
|
||||||
}
|
}
|
||||||
@@ -57,7 +57,7 @@ export const WallSystem = () => {
|
|||||||
if (!dirtyWallIds.has(wallId)) {
|
if (!dirtyWallIds.has(wallId)) {
|
||||||
const mesh = sceneRegistry.nodes.get(wallId) as THREE.Mesh
|
const mesh = sceneRegistry.nodes.get(wallId) as THREE.Mesh
|
||||||
if (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
|
* 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']]
|
const node = useScene.getState().nodes[wallId as WallNode['id']]
|
||||||
if (!node || node.type !== 'wall') return
|
if (!node || node.type !== 'wall') return
|
||||||
|
|
||||||
@@ -102,8 +102,7 @@ function updateWallGeometry(wallId: string, miterMap: WallMiterMap) {
|
|||||||
.map((childId) => useScene.getState().nodes[childId])
|
.map((childId) => useScene.getState().nodes[childId])
|
||||||
.filter((n): n is AnyNode => n !== undefined)
|
.filter((n): n is AnyNode => n !== undefined)
|
||||||
|
|
||||||
const miters = miterMap.get(wallId)
|
const newGeo = generateExtrudedWall(node, childrenNodes, miterData)
|
||||||
const newGeo = generateExtrudedWall(node, childrenNodes, miters)
|
|
||||||
|
|
||||||
mesh.geometry.dispose()
|
mesh.geometry.dispose()
|
||||||
mesh.geometry = newGeo
|
mesh.geometry = newGeo
|
||||||
@@ -111,7 +110,7 @@ function updateWallGeometry(wallId: string, miterMap: WallMiterMap) {
|
|||||||
// Update collision mesh
|
// Update collision mesh
|
||||||
const collisionMesh = mesh.getObjectByName('collision-mesh') as THREE.Mesh
|
const collisionMesh = mesh.getObjectByName('collision-mesh') as THREE.Mesh
|
||||||
if (collisionMesh) {
|
if (collisionMesh) {
|
||||||
const collisionGeo = generateExtrudedWall(node, [], miters)
|
const collisionGeo = generateExtrudedWall(node, [], miterData)
|
||||||
collisionMesh.geometry.dispose()
|
collisionMesh.geometry.dispose()
|
||||||
collisionMesh.geometry = collisionGeo
|
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:
|
* Key insight from demo: polygon is built in WORLD coordinates first,
|
||||||
* - Shape is drawn on XY plane (X = along wall, Y = height)
|
* then we transform to wall-local for the 3D mesh.
|
||||||
* - 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
|
|
||||||
*/
|
*/
|
||||||
export function generateExtrudedWall(
|
export function generateExtrudedWall(
|
||||||
wallNode: WallNode,
|
wallNode: WallNode,
|
||||||
_childrenNodes: AnyNode[], // TODO: Use for hole cutting (doors/windows)
|
_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 { junctionData } = miterData
|
||||||
const end = new THREE.Vector2(wallNode.end[0], wallNode.end[1])
|
|
||||||
const length = start.distanceTo(end)
|
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 height = wallNode.height ?? 2.5
|
||||||
const thickness = wallNode.thickness ?? 0.1
|
const thickness = wallNode.thickness ?? 0.1
|
||||||
const halfT = thickness / 2
|
const halfT = thickness / 2
|
||||||
|
|
||||||
console.log(`\n=== generateExtrudedWall: ${wallNode.id} ===`)
|
// Wall direction and normal (exactly like demo)
|
||||||
console.log('Wall:', { start: wallNode.start, end: wallNode.end, length, thickness })
|
const v = { x: wallEnd.x - wallStart.x, y: wallEnd.y - wallStart.y }
|
||||||
console.log('Miters received:', miters)
|
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
|
// Get junction data for start and end (exactly like demo)
|
||||||
const wallAngle = Math.atan2(end.y - start.y, end.x - start.x)
|
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 cosA = Math.cos(-wallAngle)
|
||||||
const sinA = Math.sin(-wallAngle)
|
const sinA = Math.sin(-wallAngle)
|
||||||
|
|
||||||
// Transform world point to wall-local space
|
|
||||||
const worldToLocal = (worldPt: Point2D): { x: number; z: number } => {
|
const worldToLocal = (worldPt: Point2D): { x: number; z: number } => {
|
||||||
const dx = worldPt.x - wallNode.start[0]
|
const dx = worldPt.x - wallStart.x
|
||||||
const dy = worldPt.y - wallNode.start[1]
|
const dy = worldPt.y - wallStart.y
|
||||||
return {
|
return {
|
||||||
x: dx * cosA - dy * sinA,
|
x: dx * cosA - dy * sinA,
|
||||||
z: dx * sinA + dy * cosA,
|
z: dx * sinA + dy * cosA,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default miter points (no junction - simple rectangle)
|
// Convert polygon to local coordinates
|
||||||
const defaultStart = {
|
const localPoints = polyPoints.map(worldToLocal)
|
||||||
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,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply miter data if available
|
// Build THREE.js shape
|
||||||
let startMiter = defaultStart
|
// Shape uses (x, y) where we map: shape.x = local.x, shape.y = -local.z
|
||||||
let endMiter = defaultEnd
|
// The negation is needed because after rotateX(-PI/2), shape.y becomes -geometry.z
|
||||||
|
|
||||||
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]
|
|
||||||
const footprint = new THREE.Shape()
|
const footprint = new THREE.Shape()
|
||||||
|
footprint.moveTo(localPoints[0]!.x, -localPoints[0]!.z)
|
||||||
// Start from start-right, go to end-right
|
for (let i = 1; i < localPoints.length; i++) {
|
||||||
footprint.moveTo(startMiter.right.x, -startMiter.right.z)
|
footprint.lineTo(localPoints[i]!.x, -localPoints[i]!.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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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()
|
footprint.closePath()
|
||||||
|
|
||||||
// Extrude along Z by height
|
// Extrude along Z by height
|
||||||
|
|||||||
Reference in New Issue
Block a user