perfect wall mitering
This commit is contained in:
@@ -39,18 +39,52 @@ function createLineFromPointAndVector(p: Point2D, v: Point2D): LineEquation {
|
|||||||
return { a, b, c }
|
return { a, b, c }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a point lies on a wall segment (not at its endpoints)
|
||||||
|
*/
|
||||||
|
function pointOnWallSegment(point: Point2D, wall: WallNode, tolerance = TOLERANCE): boolean {
|
||||||
|
const start: Point2D = { x: wall.start[0], y: wall.start[1] }
|
||||||
|
const end: Point2D = { x: wall.end[0], y: wall.end[1] }
|
||||||
|
|
||||||
|
// Check if point is at endpoints (those are handled separately)
|
||||||
|
if (pointToKey(point, tolerance) === pointToKey(start, tolerance)) return false
|
||||||
|
if (pointToKey(point, tolerance) === pointToKey(end, tolerance)) return false
|
||||||
|
|
||||||
|
// Vector from start to end
|
||||||
|
const v = { x: end.x - start.x, y: end.y - start.y }
|
||||||
|
const L = Math.sqrt(v.x * v.x + v.y * v.y)
|
||||||
|
if (L < 1e-9) return false
|
||||||
|
|
||||||
|
// Vector from start to point
|
||||||
|
const w = { x: point.x - start.x, y: point.y - start.y }
|
||||||
|
|
||||||
|
// Project point onto wall line (t is parametric position along segment)
|
||||||
|
const t = (v.x * w.x + v.y * w.y) / (L * L)
|
||||||
|
|
||||||
|
// Check if projection is within segment (not at endpoints)
|
||||||
|
if (t < tolerance || t > 1 - tolerance) return false
|
||||||
|
|
||||||
|
// Check distance from point to line
|
||||||
|
const projX = start.x + t * v.x
|
||||||
|
const projY = start.y + t * v.y
|
||||||
|
const dist = Math.sqrt((point.x - projX) ** 2 + (point.y - projY) ** 2)
|
||||||
|
|
||||||
|
return dist < tolerance
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// JUNCTION DETECTION (exactly like demo)
|
// JUNCTION DETECTION (exactly like demo)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
interface Junction {
|
interface Junction {
|
||||||
meetingPoint: Point2D
|
meetingPoint: Point2D
|
||||||
connectedWalls: Array<{ wall: WallNode; endType: 'start' | 'end' }>
|
connectedWalls: Array<{ wall: WallNode; endType: 'start' | 'end' | 'passthrough' }>
|
||||||
}
|
}
|
||||||
|
|
||||||
function findJunctions(walls: WallNode[]): Map<string, Junction> {
|
function findJunctions(walls: WallNode[]): Map<string, Junction> {
|
||||||
const junctions = new Map<string, Junction>()
|
const junctions = new Map<string, Junction>()
|
||||||
|
|
||||||
|
// First pass: group walls by their endpoints
|
||||||
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] }
|
||||||
@@ -69,6 +103,19 @@ function findJunctions(walls: WallNode[]): Map<string, Junction> {
|
|||||||
junctions.get(keyEnd)!.connectedWalls.push({ wall, endType: 'end' })
|
junctions.get(keyEnd)!.connectedWalls.push({ wall, endType: 'end' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Second pass: detect T-junctions (walls passing through junction points)
|
||||||
|
for (const [_key, junction] of junctions.entries()) {
|
||||||
|
for (const wall of walls) {
|
||||||
|
// Skip if wall already in this junction
|
||||||
|
if (junction.connectedWalls.some((cw) => cw.wall.id === wall.id)) continue
|
||||||
|
|
||||||
|
// Check if junction point lies on this wall's segment (not at endpoints)
|
||||||
|
if (pointOnWallSegment(junction.meetingPoint, wall)) {
|
||||||
|
junction.connectedWalls.push({ wall, endType: 'passthrough' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Filter to only 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 junctions.entries()) {
|
for (const [key, junction] of junctions.entries()) {
|
||||||
@@ -89,6 +136,7 @@ interface ProcessedWall {
|
|||||||
angle: number
|
angle: number
|
||||||
edgeA: LineEquation // Left edge
|
edgeA: LineEquation // Left edge
|
||||||
edgeB: LineEquation // Right edge
|
edgeB: LineEquation // Right edge
|
||||||
|
isPassthrough: boolean // True if wall passes through junction (T-junction)
|
||||||
}
|
}
|
||||||
|
|
||||||
function calculateJunctionIntersections(
|
function calculateJunctionIntersections(
|
||||||
@@ -101,7 +149,28 @@ function calculateJunctionIntersections(
|
|||||||
for (const { wall, endType } of connectedWalls) {
|
for (const { wall, endType } of connectedWalls) {
|
||||||
const halfT = getThickness(wall) / 2
|
const halfT = getThickness(wall) / 2
|
||||||
|
|
||||||
// Outgoing vector (pointing away from junction)
|
if (endType === 'passthrough') {
|
||||||
|
// For passthrough walls (T-junctions), add both directions
|
||||||
|
// This allows walls meeting the middle of this wall to miter against it
|
||||||
|
const v1 = { x: wall.end[0] - wall.start[0], y: wall.end[1] - wall.start[1] }
|
||||||
|
const v2 = { x: -v1.x, y: -v1.y }
|
||||||
|
|
||||||
|
for (const v of [v1, v2]) {
|
||||||
|
const L = Math.sqrt(v.x * v.x + v.y * v.y)
|
||||||
|
if (L < 1e-9) continue
|
||||||
|
|
||||||
|
const nUnit = { x: -v.y / L, y: v.x / L }
|
||||||
|
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 }
|
||||||
|
|
||||||
|
const edgeA = createLineFromPointAndVector(pA, v)
|
||||||
|
const edgeB = createLineFromPointAndVector(pB, v)
|
||||||
|
const angle = Math.atan2(v.y, v.x)
|
||||||
|
|
||||||
|
processedWalls.push({ wallId: wall.id, angle, edgeA, edgeB, isPassthrough: true })
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Normal wall endpoint (start or end)
|
||||||
const v =
|
const v =
|
||||||
endType === 'start'
|
endType === 'start'
|
||||||
? { x: wall.end[0] - wall.start[0], y: wall.end[1] - wall.start[1] }
|
? { x: wall.end[0] - wall.start[0], y: wall.end[1] - wall.start[1] }
|
||||||
@@ -110,21 +179,16 @@ function calculateJunctionIntersections(
|
|||||||
const L = Math.sqrt(v.x * v.x + v.y * v.y)
|
const L = Math.sqrt(v.x * v.x + v.y * v.y)
|
||||||
if (L < 1e-9) continue
|
if (L < 1e-9) continue
|
||||||
|
|
||||||
// Perpendicular unit vector (90° CCW = "left" of outgoing direction)
|
|
||||||
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
|
|
||||||
const pA = { x: meetingPoint.x + nUnit.x * halfT, y: meetingPoint.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 }
|
const pB = { x: meetingPoint.x - nUnit.x * halfT, y: meetingPoint.y - nUnit.y * halfT }
|
||||||
|
|
||||||
// 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
|
|
||||||
const angle = Math.atan2(v.y, v.x)
|
const angle = Math.atan2(v.y, v.x)
|
||||||
|
|
||||||
processedWalls.push({ wallId: wall.id, angle, edgeA, edgeB })
|
processedWalls.push({ wallId: wall.id, angle, edgeA, edgeB, isPassthrough: false })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort by outgoing angle
|
// Sort by outgoing angle
|
||||||
@@ -133,7 +197,7 @@ function calculateJunctionIntersections(
|
|||||||
console.log(`\n=== Junction at (${meetingPoint.x.toFixed(2)}, ${meetingPoint.y.toFixed(2)}) ===`)
|
console.log(`\n=== Junction at (${meetingPoint.x.toFixed(2)}, ${meetingPoint.y.toFixed(2)}) ===`)
|
||||||
console.log('Walls sorted by angle:')
|
console.log('Walls sorted by angle:')
|
||||||
for (const w of processedWalls) {
|
for (const w of processedWalls) {
|
||||||
console.log(` ${w.wallId}: angle=${((w.angle * 180) / Math.PI).toFixed(1)}°`)
|
console.log(` ${w.wallId}: angle=${((w.angle * 180) / Math.PI).toFixed(1)}°${w.isPassthrough ? ' (passthrough)' : ''}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const wallIntersections = new Map<string, { left?: Point2D; right?: Point2D }>()
|
const wallIntersections = new Map<string, { left?: Point2D; right?: Point2D }>()
|
||||||
@@ -161,19 +225,25 @@ function calculateJunctionIntersections(
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Intersection ${i}: wall1=${wall1.wallId}.edgeA ∩ wall2=${wall2.wallId}.edgeB = (${p.x.toFixed(3)}, ${p.y.toFixed(3)})`)
|
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)
|
// Only assign intersection to non-passthrough walls
|
||||||
|
// Passthrough walls don't receive junction data (their geometry doesn't change)
|
||||||
|
if (!wall1.isPassthrough) {
|
||||||
|
console.log(` -> ${wall1.wallId}.left = p`)
|
||||||
if (!wallIntersections.has(wall1.wallId)) {
|
if (!wallIntersections.has(wall1.wallId)) {
|
||||||
wallIntersections.set(wall1.wallId, {})
|
wallIntersections.set(wall1.wallId, {})
|
||||||
}
|
}
|
||||||
wallIntersections.get(wall1.wallId)!.left = p
|
wallIntersections.get(wall1.wallId)!.left = p
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!wall2.isPassthrough) {
|
||||||
|
console.log(` -> ${wall2.wallId}.right = p`)
|
||||||
if (!wallIntersections.has(wall2.wallId)) {
|
if (!wallIntersections.has(wall2.wallId)) {
|
||||||
wallIntersections.set(wall2.wallId, {})
|
wallIntersections.set(wall2.wallId, {})
|
||||||
}
|
}
|
||||||
wallIntersections.get(wall2.wallId)!.right = p
|
wallIntersections.get(wall2.wallId)!.right = p
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
console.log('Final wall intersections:')
|
console.log('Final wall intersections:')
|
||||||
for (const [id, data] of wallIntersections) {
|
for (const [id, data] of wallIntersections) {
|
||||||
@@ -229,7 +299,7 @@ export function getAdjacentWallIds(allWalls: WallNode[], dirtyWallIds: Set<strin
|
|||||||
const wallStart: Point2D = { x: wall.start[0], y: wall.start[1] }
|
const wallStart: Point2D = { x: wall.start[0], y: wall.start[1] }
|
||||||
const wallEnd: Point2D = { x: wall.end[0], y: wall.end[1] }
|
const wallEnd: Point2D = { x: wall.end[0], y: wall.end[1] }
|
||||||
|
|
||||||
// Check corner connections
|
// Check corner connections (endpoints meeting)
|
||||||
const startKey = pointToKey(wallStart)
|
const startKey = pointToKey(wallStart)
|
||||||
const endKey = pointToKey(wallEnd)
|
const endKey = pointToKey(wallEnd)
|
||||||
const dirtyStartKey = pointToKey(dirtyStart)
|
const dirtyStartKey = pointToKey(dirtyStart)
|
||||||
@@ -242,6 +312,18 @@ export function getAdjacentWallIds(allWalls: WallNode[], dirtyWallIds: Set<strin
|
|||||||
endKey === dirtyEndKey
|
endKey === dirtyEndKey
|
||||||
) {
|
) {
|
||||||
adjacent.add(wall.id)
|
adjacent.add(wall.id)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check T-junction connections (dirty wall endpoint on other wall's segment)
|
||||||
|
if (pointOnWallSegment(dirtyStart, wall) || pointOnWallSegment(dirtyEnd, wall)) {
|
||||||
|
adjacent.add(wall.id)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check reverse T-junction (other wall endpoint on dirty wall's segment)
|
||||||
|
if (pointOnWallSegment(wallStart, dirtyWall) || pointOnWallSegment(wallEnd, dirtyWall)) {
|
||||||
|
adjacent.add(wall.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user