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,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}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user