af8b9fe00f
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
173 lines
4.6 KiB
TypeScript
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>
|
|
);
|
|
}
|