diff --git a/packages/core/src/systems/wall/wall-mitering.ts b/packages/core/src/systems/wall/wall-mitering.ts index ebfd51e0..217adca1 100644 --- a/packages/core/src/systems/wall/wall-mitering.ts +++ b/packages/core/src/systems/wall/wall-mitering.ts @@ -28,6 +28,7 @@ interface Junction { export interface MiterData { left: Point2D right: Point2D + center: Point2D // The junction meeting point } // Map of wallId -> { start?: MiterData, end?: MiterData } @@ -44,13 +45,6 @@ function pointToKey(p: Point2D, tolerance = TOLERANCE): string { return `${Math.round(p.x * snap)},${Math.round(p.y * snap)}` } -function getOutgoingVector(wall: WallNode, endType: 'start' | 'end'): Point2D { - if (endType === 'start') { - return { x: wall.end[0] - wall.start[0], y: wall.end[1] - wall.start[1] } - } - return { x: wall.start[0] - wall.end[0], y: wall.start[1] - wall.end[1] } -} - function createLineFromPointAndVector(p: Point2D, v: Point2D): LineEquation { const a = -v.y const b = v.x @@ -66,19 +60,10 @@ function intersectLines(l1: LineEquation, l2: LineEquation): Point2D | null { return { x, y } } -function normalize(v: Point2D): Point2D { - const len = Math.sqrt(v.x * v.x + v.y * v.y) - if (len < 1e-9) return { x: 0, y: 0 } - return { x: v.x / len, y: v.y / len } -} - function dot(a: Point2D, b: Point2D): number { return a.x * b.x + a.y * b.y } -/** - * Check if a point lies on a wall segment (excluding endpoints) - */ function pointOnWallSegment( point: Point2D, wallStart: Point2D, @@ -90,14 +75,10 @@ function pointOnWallSegment( if (wallLen < 1e-9) return false const toPoint = { x: point.x - wallStart.x, y: point.y - wallStart.y } - - // Project point onto wall line const t = dot(toPoint, wallVec) / (wallLen * wallLen) - // Check if within segment (with margin to exclude endpoints) if (t <= tolerance / wallLen || t >= 1 - tolerance / wallLen) return false - // Check perpendicular distance 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) @@ -109,15 +90,10 @@ function pointOnWallSegment( // JUNCTION DETECTION // ============================================================================ -interface JunctionResult { - junctions: Map - throughWalls: Map // junctionKey -> host wall that the junction lies on -} - /** - * Finds all junctions (where wall endpoints meet, including T-junctions on wall segments) + * Finds all junctions where wall endpoints meet */ -function findCornerJunctions(walls: WallNode[]): JunctionResult { +function findJunctions(walls: WallNode[]): Map { const junctionMap = new Map() for (const wall of walls) { @@ -138,29 +114,6 @@ function findCornerJunctions(walls: WallNode[]): JunctionResult { junctionMap.get(endKey)!.walls.push({ wall, endType: 'end' }) } - // For each junction point, check if it lies on any wall's segment (T-junction) - // Store this info separately - the host wall should NOT be modified - const throughWallsAtJunction = new Map() // junctionKey -> host wall - - for (const [key, junction] of junctionMap) { - const wallIdsInJunction = new Set(junction.walls.map((w) => w.wall.id)) - - for (const wall of walls) { - if (wallIdsInJunction.has(wall.id)) continue - - const wallStart: Point2D = { x: wall.start[0], y: wall.start[1] } - const wallEnd: Point2D = { x: wall.end[0], y: wall.end[1] } - - // Check if junction point lies on this wall's segment - if (pointOnWallSegment(junction.point, wallStart, wallEnd)) { - // Store the through wall separately - don't add to junction.walls - // The host wall should NOT get miter data - throughWallsAtJunction.set(key, wall) - break // Only need one through wall per junction - } - } - } - // Only keep junctions with 2+ walls const actualJunctions = new Map() for (const [key, junction] of junctionMap) { @@ -169,14 +122,112 @@ function findCornerJunctions(walls: WallNode[]): JunctionResult { } } - return { junctions: actualJunctions, throughWalls: throughWallsAtJunction } + return actualJunctions } +// ============================================================================ +// MITER CALCULATION (Simple approach from prototype) +// ============================================================================ + +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) +} + +/** + * 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, + getThickness: (wall: WallNode) => number, +): Map { + const { point, walls } = junction + const result = new Map() + const processedWalls: ProcessedWall[] = [] + + // Process each wall at this junction + for (const { wall, endType } of walls) { + 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] } + + 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 } + + // 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 } + + // Edge lines + 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 }) + } + + // Sort by outgoing angle + processedWalls.sort((a, b) => a.angle - b.angle) + + const n = processedWalls.length + if (n < 2) return result + + // Calculate intersections between adjacent walls + const intersections: Point2D[] = [] + 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) + + // 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() +function findTJunctions(walls: WallNode[]): Map { + const tJunctions = new Map() for (const wall of walls) { const endpoints: { pt: Point2D; endType: 'start' | 'end' }[] = [ @@ -187,36 +238,28 @@ function findTJunctions(walls: WallNode[]): Map { for (const { pt, endType } of endpoints) { const key = pointToKey(pt) - // Skip if this is already a corner junction - // (will be handled by findCornerJunctions) - 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] } - // Check if endpoint touches the other wall's endpoints - const touchesStart = pointToKey(pt) === pointToKey(otherStart) - const touchesEnd = pointToKey(pt) === pointToKey(otherEnd) - if (touchesStart || touchesEnd) continue + // 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, { point: pt, walls: [] }) - } - const junction = tJunctions.get(key)! - - // Add the incoming wall if not already present - if (!junction.walls.some((w) => w.wall.id === wall.id && w.endType === endType)) { - junction.walls.push({ wall, endType }) + tJunctions.set(key, { + junction: { point: pt, walls: [] }, + hostWall: otherWall, + }) } - // Add the host wall as a "through" wall (we'll handle it specially) - // Use 'start' as a convention for through walls - if (!junction.walls.some((w) => w.wall.id === otherWall.id)) { - junction.walls.push({ wall: otherWall, endType: 'start' }) + 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 }) } } } @@ -226,253 +269,75 @@ function findTJunctions(walls: WallNode[]): Map { return tJunctions } -// ============================================================================ -// MITER CALCULATION -// ============================================================================ - /** - * Calculates mitered corners for a junction (including T-junctions with through walls) - * @param throughWall - Optional wall that the junction lies on (for T-junctions) - */ -function calculateCornerMiters( - junction: Junction, - getThickness: (wall: WallNode) => number, - throughWall?: WallNode, -): Map { - const { point, walls } = junction - const result = new Map() - - // If there's a through wall, handle as combined corner + T-junction - // The through wall is NOT modified - only incoming walls get miter data - if (throughWall) { - const hostHalfT = getThickness(throughWall) / 2 - const hostDir = normalize({ - x: throughWall.end[0] - throughWall.start[0], - y: throughWall.end[1] - throughWall.start[1], - }) - const hostNormal = { x: -hostDir.y, y: hostDir.x } - - // Host wall edge points at junction - 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, hostDir) - const hostEdgeRight = createLineFromPointAndVector(hostRight, hostDir) - - // Build processed list for incoming walls - const incomingProcessed: { - wallId: string - angle: number - edgeLeft: LineEquation - edgeRight: LineEquation - defaultLeft: Point2D - defaultRight: Point2D - approachDot: number - }[] = [] - - for (const { wall, endType } of walls) { - const halfT = getThickness(wall) / 2 - const v = getOutgoingVector(wall, endType) - const vNorm = normalize(v) - - if (Math.abs(vNorm.x) < 1e-9 && Math.abs(vNorm.y) < 1e-9) continue - - const normal = { x: -vNorm.y, y: vNorm.x } - 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 } - - const incomingDir = { x: -vNorm.x, y: -vNorm.y } - const approachDot = dot(incomingDir, hostNormal) - - incomingProcessed.push({ - wallId: wall.id, - angle: Math.atan2(v.y, v.x), - edgeLeft: createLineFromPointAndVector(leftPt, v), - edgeRight: createLineFromPointAndVector(rightPt, v), - defaultLeft: leftPt, - defaultRight: rightPt, - approachDot, - }) - } - - // Initialize all walls with default values - for (const w of incomingProcessed) { - result.set(w.wallId, { left: w.defaultLeft, right: w.defaultRight }) - } - - // Group walls by side, then process each side separately - // Walls at a T-junction don't form a closed loop - they all face toward the host - const leftSideWalls = incomingProcessed.filter((w) => w.approachDot > 0) - const rightSideWalls = incomingProcessed.filter((w) => w.approachDot <= 0) - - for (const sideWalls of [leftSideWalls, rightSideWalls]) { - if (sideWalls.length === 0) continue - - // Determine which host edge this side approaches - const targetHostEdge = sideWalls[0]!.approachDot > 0 ? hostEdgeRight : hostEdgeLeft - - // For T-junctions: ALL edges of ALL walls on this side meet the host surface - // This ensures walls stop at the host and don't go through it - for (const w of sideWalls) { - const leftInt = intersectLines(w.edgeLeft, targetHostEdge) - const rightInt = intersectLines(w.edgeRight, targetHostEdge) - if (leftInt) result.get(w.wallId)!.left = leftInt - if (rightInt) result.get(w.wallId)!.right = rightInt - } - } - - return result - } - - // Standard corner junction processing (no through wall) - const processed: { - wallId: string - angle: number - edgeLeft: LineEquation - edgeRight: LineEquation - defaultLeft: Point2D - defaultRight: Point2D - }[] = [] - - for (const { wall, endType } of walls) { - const halfT = getThickness(wall) / 2 - const v = getOutgoingVector(wall, endType) - const vNorm = normalize(v) - - if (Math.abs(vNorm.x) < 1e-9 && Math.abs(vNorm.y) < 1e-9) continue - - const normal = { x: -vNorm.y, y: vNorm.x } - 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 } - - processed.push({ - wallId: wall.id, - angle: Math.atan2(v.y, v.x), - edgeLeft: createLineFromPointAndVector(leftPt, v), - edgeRight: createLineFromPointAndVector(rightPt, v), - defaultLeft: leftPt, - defaultRight: rightPt, - }) - } - - // Sort by angle for proper adjacency - processed.sort((a, b) => a.angle - b.angle) - - const n = processed.length - if (n < 2) return result - - // Initialize with defaults - for (const p of processed) { - result.set(p.wallId, { left: p.defaultLeft, right: p.defaultRight }) - } - - // Calculate intersections between adjacent walls - for (let i = 0; i < n; i++) { - const curr = processed[i]! - const next = processed[(i + 1) % n]! - - const intersection = intersectLines(curr.edgeLeft, next.edgeRight) - - if (intersection) { - result.get(curr.wallId)!.left = intersection - result.get(next.wallId)!.right = intersection - } - } - - return result -} - -/** - * Calculates miter for a T-junction (wall endpoint meeting another wall's side) + * 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() - // Separate incoming walls (those with endpoint at junction) from host wall - const incomingWalls: WallEndpoint[] = [] - let hostWall: WallNode | null = null - - for (const { wall, endType } of walls) { - const wallStart: Point2D = { x: wall.start[0], y: wall.start[1] } - const wallEnd: Point2D = { x: wall.end[0], y: wall.end[1] } - - const startKey = pointToKey(wallStart) - const endKey = pointToKey(wallEnd) - const junctionKey = pointToKey(point) - - if (startKey === junctionKey || endKey === junctionKey) { - incomingWalls.push({ wall, endType }) - } else { - hostWall = wall - } - } - - if (!hostWall || incomingWalls.length === 0) return result - - // If there are multiple incoming walls, use corner miter logic with throughWall - // This handles cases where walls meet at a T-junction point but weren't grouped as a corner junction - if (incomingWalls.length >= 2) { - const cornerJunction: Junction = { point, walls: incomingWalls } - return calculateCornerMiters(cornerJunction, getThickness, hostWall) - } - - // Single incoming wall: handle as simple T-junction - // Get host wall direction and normal - const hostDir = normalize({ + // Host wall direction and normal + const hostDir = { x: hostWall.end[0] - hostWall.start[0], y: hostWall.end[1] - hostWall.start[1], - }) - const hostNormal = { x: -hostDir.y, y: hostDir.x } + } + 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 points at junction + // 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 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 the host wall's edges - for (const { wall, endType } of incomingWalls) { + // For each incoming wall, extend to meet host wall's edge + for (const { wall, endType } of walls) { const halfT = getThickness(wall) / 2 - const v = getOutgoingVector(wall, endType) - const vNorm = normalize(v) - if (Math.abs(vNorm.x) < 1e-9 && Math.abs(vNorm.y) < 1e-9) continue + // 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 } - // Default corner points + // 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 } - // Create edge lines for incoming wall + // Edge lines const edgeLeft = createLineFromPointAndVector(leftPt, v) const edgeRight = createLineFromPointAndVector(rightPt, v) - // Determine which side of the host wall the incoming wall approaches from - // Use the OPPOSITE of outgoing direction (incoming direction) dotted with host normal + // 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 the host edge facing the incoming wall - // If dot > 0, wall approaches from the opposite side of hostNormal, use hostRight (near surface) - // If dot < 0, wall approaches from the hostNormal side, use hostLeft (near surface) - const targetHostEdge = - approachDot > 0 - ? createLineFromPointAndVector(hostRight, hostDir) - : createLineFromPointAndVector(hostLeft, hostDir) + // Pick host edge facing the incoming wall + const targetHostEdge = approachDot > 0 ? hostEdgeRight : hostEdgeLeft - // Both edges of incoming wall meet the same host edge - const leftIntersection = intersectLines(edgeLeft, targetHostEdge) - const rightIntersection = intersectLines(edgeRight, targetHostEdge) + // Both edges meet the same host edge + const leftInt = intersectLines(edgeLeft, targetHostEdge) + const rightInt = intersectLines(edgeRight, targetHostEdge) result.set(wall.id, { - left: leftIntersection || leftPt, - right: rightIntersection || rightPt, + left: leftInt ?? leftPt, + right: rightInt ?? rightPt, + center: point, }) } @@ -490,12 +355,10 @@ export function calculateLevelMiters(walls: WallNode[]): WallMiterMap { const miterMap: WallMiterMap = new Map() const getThickness = (wall: WallNode) => wall.thickness ?? 0.1 - // Process corner junctions - const { junctions: cornerJunctions, throughWalls } = findCornerJunctions(walls) - for (const [key, junction] of cornerJunctions) { - // Pass the through wall (if any) for T-junction handling - const throughWall = throughWalls.get(key) - const miters = calculateCornerMiters(junction, getThickness, throughWall) + // Process regular junctions (2+ walls meeting at endpoints) + const junctions = findJunctions(walls) + for (const [, junction] of junctions) { + const miters = calculateJunctionMiters(junction, getThickness) for (const { wall, endType } of junction.walls) { const miterData = miters.get(wall.id) @@ -508,16 +371,16 @@ export function calculateLevelMiters(walls: WallNode[]): WallMiterMap { } } - // Process T-junctions + // Process T-junctions (wall endpoint on another wall's side) const tJunctions = findTJunctions(walls) - for (const [, junction] of tJunctions) { - const miters = calculateTJunctionMiters(junction, getThickness) + 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 corner junction miters + // Don't overwrite existing miter data if (miterMap.get(wall.id)?.[endType]) continue if (!miterMap.has(wall.id)) { diff --git a/packages/core/src/systems/wall/wall-system.tsx b/packages/core/src/systems/wall/wall-system.tsx index 6ff3eaad..42533846 100644 --- a/packages/core/src/systems/wall/wall-system.tsx +++ b/packages/core/src/systems/wall/wall-system.tsx @@ -132,7 +132,7 @@ function updateWallGeometry(wallId: string, miterMap: WallMiterMap) { */ export function generateExtrudedWall( wallNode: WallNode, - childrenNodes: AnyNode[], + _childrenNodes: AnyNode[], // TODO: Use for hole cutting (doors/windows) miters?: { start?: MiterData; end?: MiterData }, ) { const start = new THREE.Vector2(wallNode.start[0], wallNode.start[1]) @@ -142,6 +142,10 @@ export function generateExtrudedWall( 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 angle for coordinate transforms const wallAngle = Math.atan2(end.y - start.y, end.x - start.x) const cosA = Math.cos(-wallAngle) @@ -157,155 +161,103 @@ export function generateExtrudedWall( } } - // Calculate miter offsets at start and end - // These determine how far the wall extends/retracts at each end for proper joints - let startLeftZ = halfT - let startRightZ = -halfT - let endLeftZ = halfT - let endRightZ = -halfT + // 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, + } - // Miter offset along the wall's X axis (for angled cuts) - let startLeftX = 0 - let startRightX = 0 - let endLeftX = length - let endRightX = length + // 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) - startLeftZ = left.z - startRightZ = right.z - startLeftX = left.x - startRightX = right.x + const center = worldToLocal(miters.start.center) + startMiter = { left, right, center, hasJunction: true } } if (miters?.end) { - // At end, left/right are relative to outgoing direction (reversed) + // At end, left/right are swapped because outgoing direction is reversed const left = worldToLocal(miters.end.right) const right = worldToLocal(miters.end.left) - endLeftZ = left.z - endRightZ = right.z - endLeftX = left.x - endRightX = right.x + const center = worldToLocal(miters.end.center) + endMiter = { left, right, center, hasJunction: true } } - // Create the main wall shape (XY plane: X = along wall, Y = height) - const shape = new THREE.Shape() - shape.moveTo(0, 0) - shape.lineTo(length, 0) - shape.lineTo(length, height) - shape.lineTo(0, height) - shape.closePath() - - // Process holes (doors/windows) - const wallStart: [number, number] = [wallNode.start[0], wallNode.start[1]] - const wallMesh = sceneRegistry.nodes.get(wallNode.id) as THREE.Mesh - const wallWorldY = wallMesh?.getWorldPosition(new THREE.Vector3()).y ?? 0 - - childrenNodes.forEach((child) => { - if (child.type !== 'item') return - - const childMesh = sceneRegistry.nodes.get(child.id) - if (!childMesh) return - - const cutoutMesh = childMesh.getObjectByName('cutout') as THREE.Mesh - if (!cutoutMesh) return - - const holePath = createPathFromCutout(cutoutMesh, wallStart, wallAngle, wallWorldY) - if (holePath) { - shape.holes.push(holePath) - } - }) - - // Create custom extrude geometry with mitered ends - const geometry = createMiteredExtrudeGeometry( - shape, - height, - { - leftZ: startLeftZ, - rightZ: startRightZ, - leftX: startLeftX, - rightX: startRightX, - }, - { - leftZ: endLeftZ, - rightZ: endRightZ, - leftX: endLeftX, - rightX: endRightX, - }, - ) + // 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 an extruded geometry with mitered (angled) ends + * 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( - shape: THREE.Shape, height: number, - startMiter: { leftZ: number; rightZ: number; leftX: number; rightX: number }, - endMiter: { leftZ: number; rightZ: number; leftX: number; rightX: number }, + startMiter: MiterEnd, + endMiter: MiterEnd, ): THREE.BufferGeometry { - // First, create standard extrude geometry - const thickness = Math.max( - Math.abs(startMiter.leftZ - startMiter.rightZ), - Math.abs(endMiter.leftZ - endMiter.rightZ), - 0.1, - ) + // 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 geometry = new THREE.ExtrudeGeometry(shape, { - depth: thickness, + // 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) + } + + // 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 + const geometry = new THREE.ExtrudeGeometry(footprint, { + depth: height, bevelEnabled: false, }) - // Translate so center is at Z=0 - geometry.translate(0, 0, -thickness / 2) - - // Get position attribute for modification - const positions = geometry.attributes.position - const vertices = positions.array as Float32Array - - // Modify vertex positions for mitering - for (let i = 0; i < positions.count; i++) { - const x = vertices[i * 3]! - const y = vertices[i * 3 + 1]! - const z = vertices[i * 3 + 2]! - - // Get shape bounds to determine which end we're at - const shapePoints = shape.getPoints() - const minX = Math.min(...shapePoints.map((p: THREE.Vector2) => p.x)) - const maxX = Math.max(...shapePoints.map((p: THREE.Vector2) => p.x)) - const wallLength = maxX - minX - - // Determine position along wall (0 to 1) - const t = wallLength > 0 ? (x - minX) / wallLength : 0 - - // Interpolate Z offset based on position along wall and which side (left/right) - const isLeftSide = z > 0 - const startZ = isLeftSide ? startMiter.leftZ : startMiter.rightZ - const endZ = isLeftSide ? endMiter.leftZ : endMiter.rightZ - - // Linear interpolation of Z offset - const newZ = startZ + t * (endZ - startZ) - - // Also adjust X for angled cuts at ends - let newX = x - if (t < 0.01) { - // Near start - const startX = isLeftSide ? startMiter.leftX : startMiter.rightX - newX = startX - } else if (t > 0.99) { - // Near end - const endX = isLeftSide ? endMiter.leftX : endMiter.rightX - newX = endX - } - - vertices[i * 3] = newX - vertices[i * 3 + 2] = newZ - } - - positions.needsUpdate = true + // Rotate so extrusion direction (Z) becomes height direction (Y) + geometry.rotateX(-Math.PI / 2) geometry.computeVertexNormals() return geometry @@ -313,8 +265,9 @@ function createMiteredExtrudeGeometry( /** * Creates a Path from a cutout mesh for door/window holes + * TODO: Integrate with mitered wall geometry */ -function createPathFromCutout( +function _createPathFromCutout( cutoutMesh: THREE.Mesh, wallStart: [number, number], wallAngle: number, diff --git a/packages/viewer/src/components/renderers/wall/wall-renderer.tsx b/packages/viewer/src/components/renderers/wall/wall-renderer.tsx index d5d51ae6..3895c20c 100644 --- a/packages/viewer/src/components/renderers/wall/wall-renderer.tsx +++ b/packages/viewer/src/components/renderers/wall/wall-renderer.tsx @@ -15,7 +15,7 @@ export const WallRenderer = ({ node }: { node: WallNode }) => { {/* WallSystem will replace this geometry in the next frame */} - +