feat: add outlet direction (horizontal/vertical), wall light styles, floor textures, and stretch ceiling
- Add configurable outlet direction (horizontal/vertical) stored in metadata - Add wall light style variants (classic, pendant-globe, sconce-up, sconce-down) - Add PBR floor textures including natural oak - Add stretch ceiling offset support with DB migration - Add furniture surface texture selection - Add canvas theme colors utility for dark mode support - Update projection views with improved rendering - Add EN and RU translations for all new properties
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
import { Group, Circle, Line, Rect, Text } from 'react-konva';
|
||||
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;
|
||||
@@ -37,6 +39,7 @@ export function ProjectionElectrical({
|
||||
onClick,
|
||||
onDragStart,
|
||||
}: ProjectionElectricalProps) {
|
||||
const colors = useCanvasColors();
|
||||
const { position, item } = projected;
|
||||
|
||||
const displayFromFloor = isDragging && dragFromFloor != null ? dragFromFloor : position.fromFloor;
|
||||
@@ -50,8 +53,8 @@ export function ProjectionElectrical({
|
||||
padding,
|
||||
);
|
||||
|
||||
const strokeColor = isSelected ? '#2563eb' : '#64748b';
|
||||
const fillColor = isSelected ? '#dbeafe' : '#f1f5f9';
|
||||
const strokeColor = isSelected ? colors.selectedStroke : colors.electricalStroke;
|
||||
const fillColor = isSelected ? colors.selectedFill : colors.electricalFill;
|
||||
const half = SYMBOL_SIZE / 2;
|
||||
|
||||
return (
|
||||
@@ -71,7 +74,7 @@ export function ProjectionElectrical({
|
||||
y={center.y - half - 2}
|
||||
width={SYMBOL_SIZE + 4}
|
||||
height={SYMBOL_SIZE + 4}
|
||||
stroke="#2563eb"
|
||||
stroke={colors.selectedStroke}
|
||||
strokeWidth={1}
|
||||
dash={[3, 3]}
|
||||
fill="transparent"
|
||||
@@ -79,6 +82,8 @@ export function ProjectionElectrical({
|
||||
)}
|
||||
{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;
|
||||
@@ -100,15 +105,17 @@ export function ProjectionElectrical({
|
||||
? 'left'
|
||||
: 'middle'
|
||||
: anchor.horizontal;
|
||||
const totalW = safeCount * wPx;
|
||||
// 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' ? hPx / 2 : anchor.vertical === 'bottom' ? -hPx / 2 : 0;
|
||||
anchor.vertical === 'top' ? totalH / 2 : anchor.vertical === 'bottom' ? -totalH / 2 : 0;
|
||||
const cx = center.x + offX;
|
||||
const cy = center.y + offY;
|
||||
const left = cx - totalW / 2;
|
||||
const top = cy - hPx / 2;
|
||||
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;
|
||||
@@ -116,14 +123,15 @@ export function ProjectionElectrical({
|
||||
return (
|
||||
<>
|
||||
{Array.from({ length: safeCount }).map((_, i) => {
|
||||
const cellLeft = left + i * wPx;
|
||||
const cellLeft = isVertical ? groupLeft : groupLeft + i * wPx;
|
||||
const cellTop = isVertical ? groupTop + i * hPx : groupTop;
|
||||
const cellCx = cellLeft + wPx / 2;
|
||||
const cellCy = top + hPx / 2;
|
||||
const cellCy = cellTop + hPx / 2;
|
||||
return (
|
||||
<Group key={i}>
|
||||
<Rect
|
||||
x={cellLeft}
|
||||
y={top}
|
||||
y={cellTop}
|
||||
width={wPx}
|
||||
height={hPx}
|
||||
cornerRadius={Math.max(1, cellMin * 0.12)}
|
||||
@@ -178,35 +186,110 @@ export function ProjectionElectrical({
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{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}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{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 */}
|
||||
<Circle x={center.x} y={center.y} radius={3} fill="#b8860b" stroke={strokeColor} strokeWidth={1} />
|
||||
{/* Gooseneck arm going up */}
|
||||
<Line points={[center.x, center.y, center.x, armTop]} stroke="#b8860b" strokeWidth={1.5} />
|
||||
{/* Arm curving outward */}
|
||||
<Line points={[center.x, armTop, center.x + half * 0.8, armTop]} stroke="#b8860b" strokeWidth={1.5} />
|
||||
{/* Cord hanging down */}
|
||||
<Line points={[center.x + half * 0.8, armTop, center.x + half * 0.8, cordBottom]} stroke="#d4c090" strokeWidth={1} />
|
||||
{/* Glass globe */}
|
||||
<Circle
|
||||
x={center.x + half * 0.8}
|
||||
y={cordBottom + half * 0.5}
|
||||
radius={half * 0.7}
|
||||
fill="rgba(254, 249, 195, 0.4)"
|
||||
stroke="#eab308"
|
||||
strokeWidth={1}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
if (wallLightStyle === 'sconce-up') {
|
||||
// Sconce up: trapezoid pointing up
|
||||
return (
|
||||
<>
|
||||
<Line
|
||||
points={[
|
||||
center.x - half * 0.8, center.y + half * 0.5,
|
||||
center.x - half * 0.4, center.y - half,
|
||||
center.x + half * 0.4, center.y - half,
|
||||
center.x + half * 0.8, center.y + half * 0.5,
|
||||
]}
|
||||
closed
|
||||
fill="#fef9c3"
|
||||
stroke={strokeColor}
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
{/* Light rays upward */}
|
||||
<Line points={[center.x, center.y - half, center.x, center.y - half * 1.8]} stroke="#eab308" strokeWidth={1} />
|
||||
<Line points={[center.x - half * 0.3, center.y - half, center.x - half * 0.5, center.y - half * 1.6]} stroke="#eab308" strokeWidth={1} />
|
||||
<Line points={[center.x + half * 0.3, center.y - half, center.x + half * 0.5, center.y - half * 1.6]} stroke="#eab308" strokeWidth={1} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
if (wallLightStyle === 'sconce-down') {
|
||||
// Sconce down: trapezoid pointing down
|
||||
return (
|
||||
<>
|
||||
<Line
|
||||
points={[
|
||||
center.x - half * 0.8, center.y - half * 0.5,
|
||||
center.x - half * 0.4, center.y + half,
|
||||
center.x + half * 0.4, center.y + half,
|
||||
center.x + half * 0.8, center.y - half * 0.5,
|
||||
]}
|
||||
closed
|
||||
fill="#fef9c3"
|
||||
stroke={strokeColor}
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
{/* Light rays downward */}
|
||||
<Line points={[center.x, center.y + half, center.x, center.y + half * 1.8]} stroke="#eab308" strokeWidth={1} />
|
||||
<Line points={[center.x - half * 0.3, center.y + half, center.x - half * 0.5, center.y + half * 1.6]} stroke="#eab308" strokeWidth={1} />
|
||||
<Line points={[center.x + half * 0.3, center.y + half, center.x + half * 0.5, center.y + half * 1.6]} stroke="#eab308" strokeWidth={1} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
// Classic: original circle with light rays
|
||||
return (
|
||||
<>
|
||||
<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
|
||||
@@ -236,7 +319,7 @@ export function ProjectionElectrical({
|
||||
}
|
||||
align="center"
|
||||
fontSize={8}
|
||||
fill="#94a3b8"
|
||||
fill={colors.electricalLabel}
|
||||
/>
|
||||
</Group>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user