import { memo, useMemo } from 'react'; import { Line, Group } from 'react-konva'; import type { Point, Wall } from '@house-plan-maker/shared'; import { polygonCentroid } from '../utils/geometry'; interface WallLayerProps { readonly walls: readonly Wall[]; readonly roomShape: readonly Point[]; readonly zoom: number; readonly panOffset: Point; readonly selectedIds: ReadonlySet; readonly plinthThickness?: number; } /** Wall fill color. */ const WALL_FILL = '#495057'; /** Wall stroke color. */ const WALL_STROKE = '#343a40'; /** Room interior fill color. */ const ROOM_FILL = '#f8f9fa'; /** Room interior stroke. */ const ROOM_STROKE = '#dee2e6'; /** Selected wall highlight. */ const WALL_SELECTED_STROKE = '#4c6ef5'; /** * Offset a polygon outward by a fixed distance. * Uses the angle bisector at each vertex to push the point outward. */ function offsetPolygonOutward(shape: readonly Point[], thickness: number, centroid: Point): Point[] { const n = shape.length; if (n < 3) return [...shape]; const result: Point[] = []; for (let i = 0; i < n; i++) { const prev = shape[(i - 1 + n) % n]; const curr = shape[i]; const next = shape[(i + 1) % n]; // Edge vectors const e1x = curr.x - prev.x; const e1y = curr.y - prev.y; const e2x = next.x - curr.x; const e2y = next.y - curr.y; // Outward normals for each edge (perpendicular, pointing away from centroid) let n1x = -e1y; let n1y = e1x; // Normalize const len1 = Math.sqrt(n1x * n1x + n1y * n1y) || 1; n1x /= len1; n1y /= len1; let n2x = -e2y; let n2y = e2x; const len2 = Math.sqrt(n2x * n2x + n2y * n2y) || 1; n2x /= len2; n2y /= len2; // Ensure normals point away from centroid const midX = curr.x; const midY = curr.y; const toCenterX = centroid.x - midX; const toCenterY = centroid.y - midY; if (n1x * toCenterX + n1y * toCenterY > 0) { n1x = -n1x; n1y = -n1y; } if (n2x * toCenterX + n2y * toCenterY > 0) { n2x = -n2x; n2y = -n2y; } // Average bisector direction let bx = n1x + n2x; let by = n1y + n2y; const bLen = Math.sqrt(bx * bx + by * by); if (bLen < 0.001) { // Parallel edges — just use one normal bx = n1x; by = n1y; } else { bx /= bLen; by /= bLen; // Scale by 1/cos(half-angle) to maintain thickness at the corner const cosHalf = bx * n1x + by * n1y; const scale = cosHalf > 0.1 ? 1 / cosHalf : 1; bx *= Math.min(scale, 3); // cap to prevent extreme spikes by *= Math.min(scale, 3); } result.push({ x: curr.x + bx * thickness, y: curr.y + by * thickness, }); } return result; } /** Plinth color. */ const PLINTH_COLOR = '#8b7355'; export const WallLayer = memo(function WallLayer({ walls, roomShape, zoom, panOffset, selectedIds, plinthThickness = 0.01, }: WallLayerProps) { // Get wall thickness (use first wall's thickness as representative) const wallThickness = walls.length > 0 ? walls[0].thickness : 0.1; // Convert room shape to screen coordinates const roomShapeScreen = useMemo(() => { if (roomShape.length < 3) return []; return roomShape.flatMap((p) => [ p.x * zoom + panOffset.x, p.y * zoom + panOffset.y, ]); }, [roomShape, zoom, panOffset]); // Compute outer wall boundary (room shape offset outward by wall thickness) const outerWallScreen = useMemo(() => { if (roomShape.length < 3) return []; const centroid = polygonCentroid(roomShape); const outer = offsetPolygonOutward(roomShape, wallThickness, centroid); return outer.flatMap((p) => [ p.x * zoom + panOffset.x, p.y * zoom + panOffset.y, ]); }, [roomShape, wallThickness, zoom, panOffset]); // Compute inner plinth boundary (room shape offset inward by plinth thickness) const plinthScreen = useMemo(() => { if (roomShape.length < 3 || plinthThickness <= 0) return []; const centroid = polygonCentroid(roomShape); // Offset inward (toward centroid) = negative outward offset const inner = offsetPolygonOutward(roomShape, -plinthThickness, centroid); return inner.flatMap((p) => [ p.x * zoom + panOffset.x, p.y * zoom + panOffset.y, ]); }, [roomShape, plinthThickness, zoom, panOffset]); // Selected wall segments for highlighting const selectedWallSegments = useMemo(() => { return walls .filter((w) => selectedIds.has(w.id)) .map((wall) => { const points = [ wall.startX * zoom + panOffset.x, wall.startY * zoom + panOffset.y, wall.endX * zoom + panOffset.x, wall.endY * zoom + panOffset.y, ]; return { wall, points }; }); }, [walls, selectedIds, zoom, panOffset]); return ( {/* Room interior fill */} {roomShapeScreen.length >= 6 && ( )} {/* Outer wall boundary — single continuous polygon, no corner gaps */} {outerWallScreen.length >= 6 && ( {/* Outer fill */} {/* Cut out room interior by drawing room shape on top with room fill */} )} {/* Plinth strip along inside of walls */} {plinthScreen.length >= 6 && roomShapeScreen.length >= 6 && ( {/* Draw room shape filled with plinth color */} {/* Cut out inner area (room minus plinth) */} )} {/* Selected wall highlights */} {selectedWallSegments.map(({ wall, points }) => ( ))} ); });