import { Group, Circle, Line, Rect, Text, Arc } from 'react-konva'; import type { ProjectedElectrical } from '../utils/projectionMapping'; import { projectionToPixel } from '../utils/projectionMapping'; import { DEFAULT_OUTLET_WIDTH, DEFAULT_OUTLET_HEIGHT } from '@house-plan-maker/shared'; import { getWallLightStyle, getOutletDirection } from '../symbols/electrical'; import { useCanvasColors } from '../utils/canvasThemeColors'; interface ProjectionElectricalProps { readonly projected: ProjectedElectrical; readonly wallHeight: number; readonly scale: number; readonly padding: number; readonly isSelected: boolean; readonly isDragging?: boolean; readonly dragFromFloor?: number; readonly dragAlongWall?: number; /** Physical width of a single outlet face plate (meters). */ readonly outletWidth?: number; /** Physical height of a single outlet face plate (meters). */ readonly outletHeight?: number; readonly onClick: () => void; readonly onDragStart?: (itemId: string, evt: MouseEvent) => void; } const SYMBOL_SIZE = 12; /** Render a wall-mounted electrical item in wall elevation view. */ export function ProjectionElectrical({ projected, wallHeight, scale, padding, isSelected, isDragging = false, dragFromFloor, dragAlongWall, outletWidth = DEFAULT_OUTLET_WIDTH, outletHeight = DEFAULT_OUTLET_HEIGHT, onClick, onDragStart, }: ProjectionElectricalProps) { const colors = useCanvasColors(); const { position, item } = projected; const displayFromFloor = isDragging && dragFromFloor != null ? dragFromFloor : position.fromFloor; const displayAlongWall = isDragging && dragAlongWall != null ? dragAlongWall : position.alongWall; const center = projectionToPixel( displayAlongWall, displayFromFloor, wallHeight, scale, padding, ); const strokeColor = isSelected ? colors.selectedStroke : colors.electricalStroke; const fillColor = isSelected ? colors.selectedFill : colors.electricalFill; const half = SYMBOL_SIZE / 2; return ( { if (onDragStart && e.evt.button === 0) { onDragStart(item.id, e.evt); } }} style={{ cursor: onDragStart ? 'grab' : 'default' }} > {/* Drag ghost outline */} {isDragging && ( )} {item.type === 'OUTLET' && (() => { const safeCount = Math.max(1, Math.round(item.count)); const direction = getOutletDirection(item.metadata); const isVertical = direction === 'vertical'; // Convert physical outlet dims to projection-pixel dims. const wPx = outletWidth * scale; const hPx = outletHeight * scale; // Anchor offset to bounding-box center, in projection pixels. // Horizontal axis = along-wall (positive right), vertical axis = up the wall. // In screen coords +y is down, so vertical='top' anchor means center is BELOW (positive y). // // When the projection axis is flipped (the canonical direction // runs opposite to the wall's stored start→end), we mirror the // horizontal anchor so "left" still refers to the same physical // side of the wall in both 3D and projection views. Without this // an outlet anchored "left" on a flipped wall would appear on // opposite sides of the two views. const anchor = item.positionAnchor; const mirroredHorizontal = projected.axisFlipped ? anchor.horizontal === 'left' ? 'right' : anchor.horizontal === 'right' ? 'left' : 'middle' : anchor.horizontal; // Bounding box of the full outlet group depends on layout direction. const totalW = isVertical ? wPx : safeCount * wPx; const totalH = isVertical ? safeCount * hPx : hPx; const offX = mirroredHorizontal === 'left' ? totalW / 2 : mirroredHorizontal === 'right' ? -totalW / 2 : 0; const offY = anchor.vertical === 'top' ? totalH / 2 : anchor.vertical === 'bottom' ? -totalH / 2 : 0; const cx = center.x + offX; const cy = center.y + offY; const groupLeft = cx - totalW / 2; const groupTop = cy - totalH / 2; const cellMin = Math.min(wPx, hPx); const faceR = cellMin * 0.32; const prongL = cellMin * 0.18; const prongG = cellMin * 0.12; return ( <> {Array.from({ length: safeCount }).map((_, i) => { const cellLeft = isVertical ? groupLeft : groupLeft + i * wPx; const cellTop = isVertical ? groupTop + i * hPx : groupTop; const cellCx = cellLeft + wPx / 2; const cellCy = cellTop + hPx / 2; return ( ); })} ); })()} {item.type === 'SWITCH' && ( <> {/* IEC switch symbol: circle with diagonal line */} )} {item.type === 'LIGHT_WALL' && (() => { const wallLightStyle = getWallLightStyle(item.metadata); if (wallLightStyle === 'pendant-globe') { // Pendant globe: gooseneck line + hanging cord + circle globe const armTop = center.y - half * 1.8; const cordBottom = center.y + half * 0.6; return ( <> {/* Wall mount dot */} {/* Gooseneck arm going up */} {/* Arm curving outward */} {/* Cord hanging down */} {/* Glass globe */} ); } if (wallLightStyle === 'sconce-up') { // Sconce up: trapezoid pointing up return ( <> {/* Light rays upward */} ); } if (wallLightStyle === 'sconce-down') { // Sconce down: trapezoid pointing down return ( <> {/* Light rays downward */} ); } // Classic: original circle with light rays return ( <> ); })()} {/* Fallback for other wall-mounted types */} {item.type !== 'OUTLET' && item.type !== 'SWITCH' && item.type !== 'LIGHT_WALL' && ( )} {/* Type label below symbol — uses the user's custom label if set, otherwise falls back to the short type code. */} 0 ? item.label : item.type === 'OUTLET' ? 'OUT' : item.type === 'SWITCH' ? 'SW' : 'WL' } align="center" fontSize={8} fill={colors.electricalLabel} /> ); }