This commit is contained in:
Andras Schmelczer 2026-09-11 16:58:58 +01:00
commit 62a385e4c9

View file

@ -1,10 +1,10 @@
import { useId } from 'react';
import type { FlooringPlan, Geometry, Point, Room, SkirtingPlan, WallEdge } from '../lib/types';
import { area, mm } from '../lib/format';
import { area, cutMm, mm } from '../lib/format';
import { signedArea } from '../lib/geometry';
import { isSkirtingCut } from '../lib/cut-list';
import { skirtingPolygon } from '../lib/skirting-polygon';
import { dimensionLabels } from '../lib/dimension-labels';
import { dimensionLabels, wallDimensionBox } from '../lib/dimension-labels';
import type { DimensionLabel, LabelBox } from '../lib/dimension-labels';
import { ClosedBadge } from './Fields';
@ -113,6 +113,9 @@ interface Props {
selectedPiece: string | null;
select?: (id: string) => void;
interactive?: boolean;
completedBoards?: string[];
highlightedPieces?: string[];
searchActive?: boolean;
}
export function FloorPlan({
@ -123,6 +126,9 @@ export function FloorPlan({
selectedPiece,
select,
interactive = true,
completedBoards = [],
highlightedPieces = [],
searchActive = false,
}: Props) {
const hatch = useId().replaceAll(':', '');
const allPoints = geometry.edges.flatMap((edge) => [edge.start, edge.end]);
@ -145,14 +151,17 @@ export function FloorPlan({
const y = center.y - ((orientation * (piece.end.x - piece.start.x)) / length) * span * 0.032;
skirtLabels.set(piece.id, { x, y });
reserved.push({
minX: x - font * 1.2,
maxX: x + font * 1.2,
minX: x - font * 1.4,
maxX: x + font * 1.4,
minY: y - font * 0.7,
maxY: y + font * 0.7,
});
}
const labels = dimensionLabels(geometry.edges, span, orientation, reserved);
const boxes = [...reserved, ...labels.map((label) => label.box)];
const boxes = [
...reserved,
...labels.map((label, i) => wallDimensionBox(geometry.edges[i], label, span)),
];
const left = Math.min(minX, ...boxes.map((b) => b.minX)) - span * 0.035;
const top = Math.min(minY, ...boxes.map((b) => b.minY)) - span * 0.035;
const width = Math.max(maxX, ...boxes.map((b) => b.maxX)) + span * 0.035 - left;
@ -161,7 +170,10 @@ export function FloorPlan({
<section className="drawing-card" aria-label="Floor plan preview">
<div className="drawing-header">
<h2>
Floor plan <small>{area(geometry.area)} m²</small>
Floor plan{' '}
<small>
{room.name} · {area(geometry.area)} m²
</small>
</h2>
<ClosedBadge closed={geometry.closed && geometry.valid} />
</div>
@ -212,6 +224,8 @@ export function FloorPlan({
{geometry.closed && <polygon points={points(geometry.vertices)} fill="#fff" />}
{floorPieces.map((piece) => {
const cut = piece.cut || piece.reused;
const done = completedBoards.includes(`floor:${piece.stockId}`);
const highlighted = highlightedPieces.includes(piece.id);
const x =
(Math.min(...piece.polygon.map((p) => p.x)) +
Math.max(...piece.polygon.map((p) => p.x))) /
@ -223,8 +237,11 @@ export function FloorPlan({
return (
<g
key={piece.id}
className={`floor-piece ${cut ? 'svg-cut floor-cut' : 'uncut'}`}
className={`floor-piece ${cut ? 'svg-cut floor-cut' : 'uncut'} ${done ? 'is-done' : ''} ${highlighted ? 'is-highlighted' : searchActive ? 'search-muted' : ''}`}
data-material="flooring"
data-board-key={`floor:${piece.stockId}`}
data-done={done}
data-highlighted={highlighted}
data-piece-id={piece.id}
role={cut && interactive ? 'button' : undefined}
tabIndex={cut && interactive ? 0 : undefined}
@ -246,11 +263,30 @@ export function FloorPlan({
<polygon
points={points(piece.polygon)}
fill={
cut ? (selectedPiece === piece.id ? '#ceddf9' : `url(#${hatch})`) : '#f5f6f8'
done
? '#dbe9e7'
: cut
? selectedPiece === piece.id
? '#ceddf9'
: `url(#${hatch})`
: '#f5f6f8'
}
stroke={cut ? '#1647ab' : '#9baabd'}
strokeWidth={span * (selectedPiece === piece.id ? 0.003 : 0.0012)}
/>
{done && !cut && (
<text
className="completion-mark"
x={x}
y={y}
textAnchor="middle"
dominantBaseline="middle"
fontSize={font}
fill="#20594f"
>
✓
</text>
)}
{cut && (
<>
<g
@ -258,11 +294,11 @@ export function FloorPlan({
pointerEvents="none"
>
<rect
x={-font * 1.1}
x={-font * 1.3}
y={-font * 0.55}
width={font * 2.2}
width={font * 2.6}
height={font * 1.1}
fill="#1647ab"
fill={done ? '#27655d' : '#1647ab'}
/>
<text
textAnchor="middle"
@ -272,10 +308,11 @@ export function FloorPlan({
fontWeight="600"
>
{piece.id}
{done ? ' ✓' : ''}
</text>
</g>
<title>
{piece.id}: {mm(piece.length)} × {mm(piece.width)} mm
{piece.id}: {cutMm(piece.length)} × {cutMm(piece.width)} mm
</title>
</>
)}
@ -287,6 +324,8 @@ export function FloorPlan({
))}
{skirtPieces.map((piece) => {
const cut = isSkirtingCut(piece, skirting);
const done = completedBoards.includes(`skirting:${piece.stockId}`);
const highlighted = highlightedPieces.includes(piece.id);
const label = skirtLabels.get(piece.id);
// Each template uses its true wall-facing and room-facing edges, including mitres.
const thickness = piece.template ? Math.max(...piece.template.map((p) => p.y)) : 0;
@ -294,8 +333,11 @@ export function FloorPlan({
return (
<g
key={piece.id}
className={`skirt-piece ${cut ? 'svg-cut skirt-cut' : 'uncut'}`}
className={`skirt-piece ${cut ? 'svg-cut skirt-cut' : 'uncut'} ${done ? 'is-done' : ''} ${highlighted ? 'is-highlighted' : searchActive ? 'search-muted' : ''}`}
data-material="skirting"
data-board-key={`skirting:${piece.stockId}`}
data-done={done}
data-highlighted={highlighted}
data-piece-id={piece.id}
role={cut && interactive ? 'button' : undefined}
tabIndex={cut && interactive ? 0 : undefined}
@ -316,7 +358,15 @@ export function FloorPlan({
>
<polygon
points={points(polygon)}
fill={cut ? (selectedPiece === piece.id ? '#17263f' : '#b74900') : '#d8d5cd'}
fill={
done
? '#27655d'
: cut
? selectedPiece === piece.id
? '#17263f'
: '#b74900'
: '#d8d5cd'
}
stroke={cut ? '#8d3800' : '#79756b'}
strokeWidth={span * 0.001}
/>
@ -330,14 +380,27 @@ export function FloorPlan({
strokeWidth={span * 0.025}
/>
)}
{done && !cut && (
<text
className="completion-mark"
x={(piece.start.x + piece.end.x) / 2}
y={(piece.start.y + piece.end.y) / 2}
textAnchor="middle"
dominantBaseline="middle"
fontSize={font}
fill="#20594f"
>
✓
</text>
)}
{label && (
<g className="skirting-code" transform={`translate(${label.x} ${label.y})`}>
<rect
x={-font * 1.1}
x={-font * 1.3}
y={-font * 0.58}
width={font * 2.2}
width={font * 2.6}
height={font * 1.16}
fill="#b74900"
fill={done ? '#27655d' : '#b74900'}
stroke="#8d3800"
strokeWidth={span * 0.0012}
/>
@ -349,12 +412,13 @@ export function FloorPlan({
fontWeight="700"
>
{piece.id}
{done ? ' ✓' : ''}
</text>
</g>
)}
{cut && (
<title>
{piece.id}: {mm(piece.length)} mm · {mm(piece.leftMitre)}° /{' '}
{piece.id}: {cutMm(piece.length)} mm · {mm(piece.leftMitre)}° /{' '}
{mm(piece.rightMitre)}°
</title>
)}