reviewed
This commit is contained in:
parent
62a385e4c9
commit
7959fe85cc
1 changed files with 1679 additions and 498 deletions
|
|
@ -5,8 +5,8 @@ import { signedArea } from '../lib/geometry';
|
|||
import { isSkirtingCut } from '../lib/cut-list';
|
||||
import { skirtingPolygon } from '../lib/skirting-polygon';
|
||||
import { dimensionLabels, wallDimensionBox } from '../lib/dimension-labels';
|
||||
import { floorPieceLabels } from '../lib/piece-labels';
|
||||
import type { DimensionLabel, LabelBox } from '../lib/dimension-labels';
|
||||
import { ClosedBadge } from './Fields';
|
||||
|
||||
const points = (path: Point[]) => path.map((p) => `${p.x},${p.y}`).join(' ');
|
||||
const midpoint = (a: Point, b: Point): Point => ({ x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 });
|
||||
|
|
@ -141,57 +141,82 @@ export function FloorPlan({
|
|||
const orientation = Math.sign(signedArea(geometry.vertices)) || 1;
|
||||
const floorPieces = flooring.pieces.filter((p) => p.roomId === room.id);
|
||||
const skirtPieces = skirting.pieces.filter((p) => p.roomId === room.id);
|
||||
const skirtLabels = new Map<string, Point>();
|
||||
const skirtLabels = new Map<string, Point & { width: number; text: string }>();
|
||||
const reserved: LabelBox[] = [];
|
||||
for (const piece of skirtPieces) {
|
||||
if (!isSkirtingCut(piece, skirting)) continue;
|
||||
const center = midpoint(piece.start, piece.end);
|
||||
const length = Math.hypot(piece.end.x - piece.start.x, piece.end.y - piece.start.y) || 1;
|
||||
const x = center.x + ((orientation * (piece.end.y - piece.start.y)) / length) * span * 0.032;
|
||||
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.4,
|
||||
maxX: x + font * 1.4,
|
||||
minY: y - font * 0.7,
|
||||
maxY: y + font * 0.7,
|
||||
});
|
||||
const text = piece.id + (completedBoards.includes(`skirting:${piece.stockId}`) ? ' ✓' : '');
|
||||
const width = Math.max(font * 1.8, text.length * font * 0.8 * 0.64 + font * 0.55);
|
||||
let box!: LabelBox;
|
||||
for (let lane = 0; lane < 24; lane++) {
|
||||
const offset = span * (0.032 + lane * 0.035);
|
||||
const x = center.x + ((orientation * (piece.end.y - piece.start.y)) / length) * offset;
|
||||
const y = center.y - ((orientation * (piece.end.x - piece.start.x)) / length) * offset;
|
||||
box = {
|
||||
minX: x - width / 2,
|
||||
maxX: x + width / 2,
|
||||
minY: y - font * 0.7,
|
||||
maxY: y + font * 0.7,
|
||||
};
|
||||
skirtLabels.set(piece.id, { x, y, width, text });
|
||||
if (
|
||||
!reserved.some(
|
||||
(other) =>
|
||||
box.minX < other.maxX &&
|
||||
box.maxX > other.minX &&
|
||||
box.minY < other.maxY &&
|
||||
box.maxY > other.minY,
|
||||
)
|
||||
)
|
||||
break;
|
||||
}
|
||||
reserved.push(box);
|
||||
}
|
||||
const labels = dimensionLabels(geometry.edges, span, orientation, reserved);
|
||||
const floorLabels = floorPieceLabels(
|
||||
floorPieces,
|
||||
geometry.vertices,
|
||||
room.direction,
|
||||
font,
|
||||
[...reserved, ...labels.map((label) => label.box)],
|
||||
completedBoards,
|
||||
);
|
||||
const boxes = [
|
||||
...reserved,
|
||||
...labels.map((label, i) => wallDimensionBox(geometry.edges[i], label, span)),
|
||||
...floorLabels.map((label) => label.box),
|
||||
];
|
||||
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;
|
||||
const height = Math.max(maxY, ...boxes.map((b) => b.maxY)) + span * 0.035 - top;
|
||||
return (
|
||||
<section className="drawing-card" aria-label="Floor plan preview">
|
||||
<section
|
||||
className="drawing-card"
|
||||
aria-label="Floor plan preview"
|
||||
data-search-active={searchActive}
|
||||
>
|
||||
<div className="drawing-header">
|
||||
<h2>
|
||||
Floor plan{' '}
|
||||
<small>
|
||||
{room.name} · {area(geometry.area)} m²
|
||||
</small>
|
||||
{room.name} <small>{area(geometry.area)} m²</small>
|
||||
</h2>
|
||||
<ClosedBadge closed={geometry.closed && geometry.valid} />
|
||||
<div className="drawing-legend">
|
||||
<span>
|
||||
<i className="floor-key" />
|
||||
Flooring cuts
|
||||
</span>
|
||||
<span>
|
||||
<i className="skirt-key" />
|
||||
Skirting cuts
|
||||
</span>
|
||||
<span>
|
||||
<i className="uncut-key" />
|
||||
Uncut
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="drawing-legend">
|
||||
<span>
|
||||
<i className="floor-key" />
|
||||
Flooring cuts
|
||||
</span>
|
||||
<span>
|
||||
<i className="skirt-key" />
|
||||
Skirting cuts
|
||||
</span>
|
||||
<span>
|
||||
<i className="uncut-key" />
|
||||
Uncut
|
||||
</span>
|
||||
</div>
|
||||
{interactive && <p className="drawing-help">Select a numbered piece for cut details.</p>}
|
||||
<div className="drawing-viewport">
|
||||
<svg
|
||||
className="room-svg"
|
||||
|
|
@ -289,30 +314,9 @@ export function FloorPlan({
|
|||
)}
|
||||
{cut && (
|
||||
<>
|
||||
<g
|
||||
transform={`translate(${x} ${y})${room.direction === 90 ? ' rotate(90)' : ''}`}
|
||||
pointerEvents="none"
|
||||
>
|
||||
<rect
|
||||
x={-font * 1.3}
|
||||
y={-font * 0.55}
|
||||
width={font * 2.6}
|
||||
height={font * 1.1}
|
||||
fill={done ? '#27655d' : '#1647ab'}
|
||||
/>
|
||||
<text
|
||||
textAnchor="middle"
|
||||
dominantBaseline="middle"
|
||||
fill="#fff"
|
||||
fontSize={font * 0.76}
|
||||
fontWeight="600"
|
||||
>
|
||||
{piece.id}
|
||||
{done ? ' ✓' : ''}
|
||||
</text>
|
||||
</g>
|
||||
<title>
|
||||
{piece.id}: {cutMm(piece.length)} × {cutMm(piece.width)} mm
|
||||
{piece.id}: {cutMm(piece.length, piece.blankLength)} ×{' '}
|
||||
{cutMm(piece.width, piece.blankWidth)} mm
|
||||
</title>
|
||||
</>
|
||||
)}
|
||||
|
|
@ -326,7 +330,6 @@ export function FloorPlan({
|
|||
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;
|
||||
const polygon = skirtingPolygon(piece, thickness, orientation);
|
||||
|
|
@ -393,33 +396,10 @@ export function FloorPlan({
|
|||
✓
|
||||
</text>
|
||||
)}
|
||||
{label && (
|
||||
<g className="skirting-code" transform={`translate(${label.x} ${label.y})`}>
|
||||
<rect
|
||||
x={-font * 1.3}
|
||||
y={-font * 0.58}
|
||||
width={font * 2.6}
|
||||
height={font * 1.16}
|
||||
fill={done ? '#27655d' : '#b74900'}
|
||||
stroke="#8d3800"
|
||||
strokeWidth={span * 0.0012}
|
||||
/>
|
||||
<text
|
||||
textAnchor="middle"
|
||||
dominantBaseline="middle"
|
||||
fill="#fff"
|
||||
fontSize={font * 0.8}
|
||||
fontWeight="700"
|
||||
>
|
||||
{piece.id}
|
||||
{done ? ' ✓' : ''}
|
||||
</text>
|
||||
</g>
|
||||
)}
|
||||
{cut && (
|
||||
<title>
|
||||
{piece.id}: {cutMm(piece.length)} mm · {mm(piece.leftMitre)}° /{' '}
|
||||
{mm(piece.rightMitre)}°
|
||||
{piece.id}: {cutMm(piece.length, piece.blankLength)} mm · {mm(piece.leftMitre)}°
|
||||
/ {mm(piece.rightMitre)}°
|
||||
</title>
|
||||
)}
|
||||
</g>
|
||||
|
|
@ -436,6 +416,97 @@ export function FloorPlan({
|
|||
strokeDasharray={`${span * 0.015} ${span * 0.008}`}
|
||||
/>
|
||||
)}
|
||||
<g className="skirting-labels" aria-hidden="true">
|
||||
{skirtPieces.map((piece) => {
|
||||
const label = skirtLabels.get(piece.id);
|
||||
if (!label) return null;
|
||||
const done = completedBoards.includes(`skirting:${piece.stockId}`);
|
||||
const highlighted = highlightedPieces.includes(piece.id);
|
||||
return (
|
||||
<g
|
||||
key={piece.id}
|
||||
className={`skirting-code ${highlighted ? 'is-highlighted' : searchActive ? 'search-muted' : ''}`}
|
||||
data-piece-id={piece.id}
|
||||
transform={`translate(${label.x} ${label.y})`}
|
||||
onClick={interactive ? () => select?.(piece.id) : undefined}
|
||||
>
|
||||
<rect
|
||||
x={-label.width / 2}
|
||||
y={-font * 0.58}
|
||||
width={label.width}
|
||||
height={font * 1.16}
|
||||
fill={done ? '#27655d' : '#b74900'}
|
||||
stroke="#8d3800"
|
||||
strokeWidth={span * 0.0012}
|
||||
/>
|
||||
<text
|
||||
textAnchor="middle"
|
||||
dominantBaseline="middle"
|
||||
fill="#fff"
|
||||
fontSize={font * 0.8}
|
||||
fontWeight="700"
|
||||
>
|
||||
{label.text}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
</g>
|
||||
<g className="floor-labels" aria-hidden="true">
|
||||
{floorLabels.map((label) => {
|
||||
const piece = floorPieces.find((piece) => piece.id === label.id)!;
|
||||
const done = completedBoards.includes(`floor:${piece.stockId}`);
|
||||
const highlighted = highlightedPieces.includes(label.id);
|
||||
return (
|
||||
<g
|
||||
key={label.id}
|
||||
className={`floor-code ${highlighted ? 'is-highlighted' : searchActive ? 'search-muted' : ''}`}
|
||||
data-piece-id={label.id}
|
||||
onClick={interactive ? () => select?.(label.id) : undefined}
|
||||
>
|
||||
{label.leader && (
|
||||
<>
|
||||
<line
|
||||
className="piece-label-leader"
|
||||
x1={label.anchor.x}
|
||||
y1={label.anchor.y}
|
||||
x2={label.x}
|
||||
y2={label.y}
|
||||
stroke="#1647ab"
|
||||
strokeWidth={span * 0.0015}
|
||||
pointerEvents="none"
|
||||
/>
|
||||
<circle
|
||||
cx={label.anchor.x}
|
||||
cy={label.anchor.y}
|
||||
r={font * 0.09}
|
||||
fill="#1647ab"
|
||||
pointerEvents="none"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<g transform={`translate(${label.x} ${label.y}) rotate(${label.angle})`}>
|
||||
<rect
|
||||
x={-label.width / 2}
|
||||
y={-label.height / 2}
|
||||
width={label.width}
|
||||
height={label.height}
|
||||
fill={done ? '#27655d' : '#1647ab'}
|
||||
/>
|
||||
<text
|
||||
textAnchor="middle"
|
||||
dominantBaseline="middle"
|
||||
fill="#fff"
|
||||
fontSize={label.font}
|
||||
fontWeight="600"
|
||||
>
|
||||
{label.text}
|
||||
</text>
|
||||
</g>
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
|||
Loading…
Reference in a new issue