From d4c3fe2afb86a01730b3ca6d8d8924894e0b4b2c Mon Sep 17 00:00:00 2001 From: wass08 Date: Mon, 26 Jan 2026 18:17:46 +0900 Subject: [PATCH] perfect wall mitering --- .../core/src/systems/wall/wall-mitering.ts | 140 ++++++++++++++---- 1 file changed, 111 insertions(+), 29 deletions(-) diff --git a/packages/core/src/systems/wall/wall-mitering.ts b/packages/core/src/systems/wall/wall-mitering.ts index f73af172..f5dce879 100644 --- a/packages/core/src/systems/wall/wall-mitering.ts +++ b/packages/core/src/systems/wall/wall-mitering.ts @@ -39,18 +39,52 @@ function createLineFromPointAndVector(p: Point2D, v: Point2D): LineEquation { 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) // ============================================================================ interface Junction { meetingPoint: Point2D - connectedWalls: Array<{ wall: WallNode; endType: 'start' | 'end' }> + connectedWalls: Array<{ wall: WallNode; endType: 'start' | 'end' | 'passthrough' }> } function findJunctions(walls: WallNode[]): Map { const junctions = new Map() + // First pass: group walls by their endpoints 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] } @@ -69,6 +103,19 @@ function findJunctions(walls: WallNode[]): Map { 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 const actualJunctions = new Map() for (const [key, junction] of junctions.entries()) { @@ -89,6 +136,7 @@ interface ProcessedWall { angle: number edgeA: LineEquation // Left edge edgeB: LineEquation // Right edge + isPassthrough: boolean // True if wall passes through junction (T-junction) } function calculateJunctionIntersections( @@ -101,30 +149,46 @@ function calculateJunctionIntersections( for (const { wall, endType } of connectedWalls) { const halfT = getThickness(wall) / 2 - // Outgoing vector (pointing away from junction) - 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] } + 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 } - const L = Math.sqrt(v.x * v.x + v.y * v.y) - if (L < 1e-9) continue + for (const v of [v1, v2]) { + const L = Math.sqrt(v.x * v.x + v.y * v.y) + 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 } + 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 } - // 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 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) - // Edge lines (direction parallel to wall) - const edgeA = createLineFromPointAndVector(pA, v) - const edgeB = createLineFromPointAndVector(pB, v) + processedWalls.push({ wallId: wall.id, angle, edgeA, edgeB, isPassthrough: true }) + } + } else { + // Normal wall endpoint (start or end) + 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] } - // Angle for sorting - const angle = Math.atan2(v.y, v.x) + const L = Math.sqrt(v.x * v.x + v.y * v.y) + if (L < 1e-9) continue - processedWalls.push({ wallId: wall.id, angle, edgeA, edgeB }) + 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: false }) + } } // 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('Walls sorted by angle:') 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() @@ -161,18 +225,24 @@ function calculateJunctionIntersections( } 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) - if (!wallIntersections.has(wall1.wallId)) { - wallIntersections.set(wall1.wallId, {}) + // 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)) { + wallIntersections.set(wall1.wallId, {}) + } + wallIntersections.get(wall1.wallId)!.left = p } - wallIntersections.get(wall1.wallId)!.left = p - if (!wallIntersections.has(wall2.wallId)) { - wallIntersections.set(wall2.wallId, {}) + if (!wall2.isPassthrough) { + console.log(` -> ${wall2.wallId}.right = p`) + if (!wallIntersections.has(wall2.wallId)) { + wallIntersections.set(wall2.wallId, {}) + } + wallIntersections.get(wall2.wallId)!.right = p } - wallIntersections.get(wall2.wallId)!.right = p } console.log('Final wall intersections:') @@ -229,7 +299,7 @@ export function getAdjacentWallIds(allWalls: WallNode[], dirtyWallIds: Set