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:
2026-04-12 20:52:49 +03:00
parent d8a914bf2a
commit 521ea5e85b
34 changed files with 1278 additions and 162 deletions
@@ -1,5 +1,6 @@
import type { ReactNode } from 'react';
import { Group, Rect, Line, Text } from 'react-konva';
import { useCanvasColors } from '../utils/canvasThemeColors';
import type {
ProjectedOpening,
ProjectedElectrical,
@@ -7,6 +8,7 @@ import type {
} from '../utils/projectionMapping';
import { projectionToPixel } from '../utils/projectionMapping';
import { DEFAULT_OUTLET_WIDTH, DEFAULT_OUTLET_HEIGHT } from '@house-plan-maker/shared';
import { getOutletInvertCoordX, getOutletInvertCoordY } from '../symbols/electrical';
interface ProjectionMeasurementsProps {
readonly projectedOpenings: readonly ProjectedOpening[];
@@ -25,7 +27,7 @@ interface ProjectionMeasurementsProps {
/** Dimension line with arrows and text. */
function DimensionLine({
x1, y1, x2, y2, label, offset, horizontal,
x1, y1, x2, y2, label, offset, horizontal, lineColor, textColor,
}: {
readonly x1: number;
readonly y1: number;
@@ -34,6 +36,8 @@ function DimensionLine({
readonly label: string;
readonly offset: number;
readonly horizontal: boolean;
readonly lineColor: string;
readonly textColor: string;
}) {
const arrowSize = 4;
@@ -43,19 +47,19 @@ function DimensionLine({
return (
<Group>
{/* Extension lines */}
<Line points={[x1, y1, x1, lineY]} stroke="#94a3b8" strokeWidth={0.5} />
<Line points={[x2, y2, x2, lineY]} stroke="#94a3b8" strokeWidth={0.5} />
<Line points={[x1, y1, x1, lineY]} stroke={lineColor} strokeWidth={0.5} />
<Line points={[x2, y2, x2, lineY]} stroke={lineColor} strokeWidth={0.5} />
{/* Main line */}
<Line points={[x1, lineY, x2, lineY]} stroke="#94a3b8" strokeWidth={0.75} />
<Line points={[x1, lineY, x2, lineY]} stroke={lineColor} strokeWidth={0.75} />
{/* Arrows */}
<Line
points={[x1, lineY, x1 + arrowSize, lineY - arrowSize / 2, x1 + arrowSize, lineY + arrowSize / 2]}
fill="#94a3b8"
fill={lineColor}
closed
/>
<Line
points={[x2, lineY, x2 - arrowSize, lineY - arrowSize / 2, x2 - arrowSize, lineY + arrowSize / 2]}
fill="#94a3b8"
fill={lineColor}
closed
/>
{/* Label */}
@@ -66,7 +70,7 @@ function DimensionLine({
text={label}
align="center"
fontSize={9}
fill="#64748b"
fill={textColor}
/>
</Group>
);
@@ -77,17 +81,17 @@ function DimensionLine({
const midY = (y1 + y2) / 2;
return (
<Group>
<Line points={[x1, y1, lineX, y1]} stroke="#94a3b8" strokeWidth={0.5} />
<Line points={[x1, y2, lineX, y2]} stroke="#94a3b8" strokeWidth={0.5} />
<Line points={[lineX, y1, lineX, y2]} stroke="#94a3b8" strokeWidth={0.75} />
<Line points={[x1, y1, lineX, y1]} stroke={lineColor} strokeWidth={0.5} />
<Line points={[x1, y2, lineX, y2]} stroke={lineColor} strokeWidth={0.5} />
<Line points={[lineX, y1, lineX, y2]} stroke={lineColor} strokeWidth={0.75} />
<Line
points={[lineX, y1, lineX - arrowSize / 2, y1 + arrowSize, lineX + arrowSize / 2, y1 + arrowSize]}
fill="#94a3b8"
fill={lineColor}
closed
/>
<Line
points={[lineX, y2, lineX - arrowSize / 2, y2 - arrowSize, lineX + arrowSize / 2, y2 - arrowSize]}
fill="#94a3b8"
fill={lineColor}
closed
/>
<Text
@@ -95,7 +99,7 @@ function DimensionLine({
y={midY - 5}
text={label}
fontSize={9}
fill="#64748b"
fill={textColor}
/>
</Group>
);
@@ -118,6 +122,7 @@ export function ProjectionMeasurements({
outletHeight = DEFAULT_OUTLET_HEIGHT,
showWallDimensions = true,
}: ProjectionMeasurementsProps) {
const colors = useCanvasColors();
const elements: ReactNode[] = [];
if (showWallDimensions) {
@@ -134,6 +139,8 @@ export function ProjectionMeasurements({
label={formatM(wallLen)}
offset={18}
horizontal
lineColor={colors.dimensionLine}
textColor={colors.dimensionText}
/>,
);
@@ -150,6 +157,8 @@ export function ProjectionMeasurements({
label={formatM(wallHeight)}
offset={18}
horizontal={false}
lineColor={colors.dimensionLine}
textColor={colors.dimensionText}
/>,
);
}
@@ -171,6 +180,8 @@ export function ProjectionMeasurements({
label={formatM(rect.height)}
offset={-14}
horizontal={false}
lineColor={colors.dimensionLine}
textColor={colors.dimensionText}
/>,
);
@@ -187,6 +198,8 @@ export function ProjectionMeasurements({
label={formatM(rect.y)}
offset={-14}
horizontal={false}
lineColor={colors.dimensionLine}
textColor={colors.dimensionText}
/>,
);
}
@@ -203,6 +216,8 @@ export function ProjectionMeasurements({
label={formatM(rect.width)}
offset={-12}
horizontal
lineColor={colors.dimensionLine}
textColor={colors.dimensionText}
/>,
);
}
@@ -229,7 +244,14 @@ export function ProjectionMeasurements({
halfWidthPx = (safeCount * outletWidth * scale) / 2;
}
const coordLabel = `(${pe.position.alongWall.toFixed(2)}; ${pe.elevation.toFixed(2)})`;
// Outlets can opt into inverted coordinate read-outs per axis — useful
// when the user measures distances from the opposite wall edge or from
// the ceiling. Display-only: the stored position is untouched.
const invertX = pe.item.type === 'OUTLET' && getOutletInvertCoordX(pe.item.metadata);
const invertY = pe.item.type === 'OUTLET' && getOutletInvertCoordY(pe.item.metadata);
const displayX = invertX ? wallLen - pe.position.alongWall : pe.position.alongWall;
const displayY = invertY ? wallHeight - pe.elevation : pe.elevation;
const coordLabel = `(${displayX.toFixed(2)}; ${displayY.toFixed(2)})`;
const labelX = center.x + halfWidthPx + 6;
const labelY = center.y - 6;
// Rough text-width estimate (monospace-ish): ~5.5px per char at fontSize 9.
@@ -241,7 +263,7 @@ export function ProjectionMeasurements({
y={labelY - 1}
width={labelWidth}
height={12}
fill="rgba(255, 255, 255, 0.85)"
fill={colors.coordLabelBg}
cornerRadius={2}
listening={false}
/>
@@ -250,7 +272,7 @@ export function ProjectionMeasurements({
y={labelY}
text={coordLabel}
fontSize={9}
fill="#475569"
fill={colors.coordLabelText}
listening={false}
/>
</Group>,
@@ -274,7 +296,7 @@ export function ProjectionMeasurements({
y={openingCenter.y - 22}
text={formatM(rect.x + rect.width / 2)}
fontSize={8}
fill="#94a3b8"
fill={colors.openingLabel}
align="center"
width={32}
/>,
@@ -313,6 +335,8 @@ export function ProjectionMeasurements({
label={formatM(rect.width)}
offset={32}
horizontal
lineColor={colors.dimensionLine}
textColor={colors.dimensionText}
/>,
);
if (rect.x > 0.001) {
@@ -327,6 +351,8 @@ export function ProjectionMeasurements({
label={formatM(rect.x)}
offset={46}
horizontal
lineColor={colors.dimensionLine}
textColor={colors.dimensionText}
/>,
);
}
@@ -342,6 +368,8 @@ export function ProjectionMeasurements({
label={formatM(rect.width)}
offset={14}
horizontal
lineColor={colors.dimensionLine}
textColor={colors.dimensionText}
/>,
);
// Inline start-offset (distance from wall start to the left edge),
@@ -359,6 +387,8 @@ export function ProjectionMeasurements({
label={formatM(rect.x)}
offset={-6}
horizontal
lineColor={colors.dimensionLine}
textColor={colors.dimensionText}
/>,
);
}
@@ -381,6 +411,8 @@ export function ProjectionMeasurements({
label={formatM(rect.height)}
offset={-32}
horizontal={false}
lineColor={colors.dimensionLine}
textColor={colors.dimensionText}
/>,
);
if (rect.y > 0.001) {
@@ -395,6 +427,8 @@ export function ProjectionMeasurements({
label={formatM(rect.y)}
offset={-46}
horizontal={false}
lineColor={colors.dimensionLine}
textColor={colors.dimensionText}
/>,
);
}
@@ -410,6 +444,8 @@ export function ProjectionMeasurements({
label={formatM(rect.height)}
offset={-14}
horizontal={false}
lineColor={colors.dimensionLine}
textColor={colors.dimensionText}
/>,
);
// Inline elevation (distance from floor to the bottom of the item),
@@ -427,6 +463,8 @@ export function ProjectionMeasurements({
label={formatM(rect.y)}
offset={6}
horizontal={false}
lineColor={colors.dimensionLine}
textColor={colors.dimensionText}
/>,
);
}