Files
house-plan-maker/apps/client/src/components/editor/projection/ProjectionElectrical.tsx
T
alexei.dolgolyov af8b9fe00f feat: complete house plan maker application
Full-featured house/apartment floor plan editor with:

- Turborepo monorepo (React/Vite client, Fastify/Prisma server, shared Zod schemas)
- 2D room editor with walls, doors, windows, furniture, electrical elements
- 3D room preview with Three.js (auto-hide nearest walls, bird's eye default)
- Wall projection views with interactive drag (elevation, position)
- Apartment floor plan view with room positioning
- Copy/paste, alignment tools, measurement tool, annotations
- Item-attached annotations with leader lines (visible on projections)
- Door open direction (LEFT/RIGHT/INWARD/OUTWARD) with swing arc
- Floor type textures (wood, tile, concrete, laminate, herringbone)
- Wall color picker for 3D view
- Furniture: bed, desk, wardrobe, sofa, table, chair, shelf, nightstand, dresser, bookcase, TV (with stand toggle), AC unit
- Furniture elevation support (wall-mounted items)
- Auto-save with dirty state tracking, batch save API
- Rotation-aware collision detection (SAT/OBB) with 3D elevation check
- Rotation-aware hit testing
- i18n (English/Russian) with locale-aware number formatting
- Dark mode with system preference detection
- Undo/redo, keyboard shortcuts, scale bar
- PDF/PNG/JSON export and JSON import
- Focus trap modal, toast notifications, tooltips
- Responsive layout with overlay palettes
2026-04-05 22:34:03 +03:00

173 lines
4.6 KiB
TypeScript

import { Group, Circle, Line, Rect, Text } from 'react-konva';
import type { ProjectedElectrical } from '../utils/projectionMapping';
import { projectionToPixel } from '../utils/projectionMapping';
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;
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,
onClick,
onDragStart,
}: ProjectionElectricalProps) {
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 ? '#2563eb' : '#64748b';
const fillColor = isSelected ? '#dbeafe' : '#f1f5f9';
const half = SYMBOL_SIZE / 2;
return (
<Group
onClick={onClick}
onMouseDown={(e) => {
if (onDragStart && e.evt.button === 0) {
onDragStart(item.id, e.evt);
}
}}
style={{ cursor: onDragStart ? 'grab' : 'default' }}
>
{/* Drag ghost outline */}
{isDragging && (
<Rect
x={center.x - half - 2}
y={center.y - half - 2}
width={SYMBOL_SIZE + 4}
height={SYMBOL_SIZE + 4}
stroke="#2563eb"
strokeWidth={1}
dash={[3, 3]}
fill="transparent"
/>
)}
{item.type === 'OUTLET' && (
<>
{/* IEC outlet symbol: circle with two horizontal lines */}
<Circle
x={center.x}
y={center.y}
radius={half}
fill={fillColor}
stroke={strokeColor}
strokeWidth={1.5}
/>
<Line
points={[center.x - 3, center.y - 2, center.x + 3, center.y - 2]}
stroke={strokeColor}
strokeWidth={1.5}
/>
<Line
points={[center.x - 3, center.y + 2, center.x + 3, center.y + 2]}
stroke={strokeColor}
strokeWidth={1.5}
/>
</>
)}
{item.type === 'SWITCH' && (
<>
{/* IEC switch symbol: circle with diagonal line */}
<Circle
x={center.x}
y={center.y}
radius={half}
fill={fillColor}
stroke={strokeColor}
strokeWidth={1.5}
/>
<Line
points={[
center.x - half + 2,
center.y + half - 2,
center.x + half - 2,
center.y - half + 2,
]}
stroke={strokeColor}
strokeWidth={1.5}
/>
</>
)}
{item.type === 'LIGHT_WALL' && (
<>
{/* Wall light: semicircle shape */}
<Circle
x={center.x}
y={center.y}
radius={half}
fill="#fef9c3"
stroke={strokeColor}
strokeWidth={1.5}
/>
<Line
points={[
center.x - half, center.y,
center.x - half * 1.3, center.y + half * 0.8,
]}
stroke="#eab308"
strokeWidth={1}
/>
<Line
points={[
center.x + half, center.y,
center.x + half * 1.3, center.y + half * 0.8,
]}
stroke="#eab308"
strokeWidth={1}
/>
</>
)}
{/* Fallback for other wall-mounted types */}
{item.type !== 'OUTLET' && item.type !== 'SWITCH' && item.type !== 'LIGHT_WALL' && (
<Rect
x={center.x - half}
y={center.y - half}
width={SYMBOL_SIZE}
height={SYMBOL_SIZE}
fill={fillColor}
stroke={strokeColor}
strokeWidth={1.5}
/>
)}
{/* Type label below symbol */}
<Text
x={center.x - 20}
y={center.y + half + 2}
width={40}
text={item.type === 'OUTLET' ? 'OUT' : item.type === 'SWITCH' ? 'SW' : 'WL'}
align="center"
fontSize={8}
fill="#94a3b8"
/>
</Group>
);
}