diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 00000000..f5ad7046 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,7 @@ +{ + "permissions": { + "allow": [ + "Bash(npx turbo:*)" + ] + } +} diff --git a/packages/core/src/systems/wall/wall-mitering.ts b/packages/core/src/systems/wall/wall-mitering.ts index 217adca1..f73af172 100644 --- a/packages/core/src/systems/wall/wall-mitering.ts +++ b/packages/core/src/systems/wall/wall-mitering.ts @@ -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 -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 +// Map of junctionKey -> WallIntersections +type JunctionData = Map // ============================================================================ // 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 { - const junctionMap = new Map() + const junctions = new Map() 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() - 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 { } // ============================================================================ -// 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 { - const { point, walls } = junction - const result = new Map() +): 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() + 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) - } - - // 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 - }) - } - - return result -} - -// ============================================================================ -// T-JUNCTION HANDLING -// ============================================================================ - -/** - * Finds T-junctions where a wall endpoint meets another wall's side - */ -function findTJunctions(walls: WallNode[]): Map { - const tJunctions = new Map() - - 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)! - if (!entry.junction.walls.some((w) => w.wall.id === wall.id && w.endType === endType)) { - entry.junction.walls.push({ wall, endType }) - } - } - } + // 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 } + + 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, + } + + 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, {}) + } + wallIntersections.get(wall1.wallId)!.left = p + + if (!wallIntersections.has(wall2.wallId)) { + wallIntersections.set(wall2.wallId, {}) + } + wallIntersections.get(wall2.wallId)!.right = p } - 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 { - const { point, walls } = junction - const result = new Map() - - // 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, - }) + 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 result + return wallIntersections } // ============================================================================ // MAIN EXPORT // ============================================================================ +export interface WallMiterData { + // Junction data keyed by junction position key + junctionData: JunctionData + // All junctions for quick lookup + junctions: Map +} + /** * 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 { // 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