This commit is contained in:
Andras Schmelczer 2026-09-11 16:58:56 +01:00
commit 9689bfcc18
23 changed files with 1815 additions and 955 deletions

View file

@ -2,12 +2,107 @@ import { Fragment } from 'react';
import type { FlooringPiece, SkirtingPiece } from '../lib/types';
import {
exactMm,
dimensionedOutline,
flooringTemplate,
fullPreviewBox,
skirtingEndBoxes,
skirtingTemplate,
viewBox,
} from '../lib/cut-preview';
import type { EdgeDimension } from '../lib/cut-preview';
function Dimensions({ dimensions, font }: { dimensions: EdgeDimension[]; font: number }) {
return (
<g className="cut-dimensions">
{dimensions.map((dimension) => {
const { start, end, lineStart, lineEnd, normal, label } = dimension;
const center = { x: (lineStart.x + lineEnd.x) / 2, y: (lineStart.y + lineEnd.y) / 2 };
const staggered =
Math.abs((label.x - center.x) * -normal.y + (label.y - center.y) * normal.x) > font * 0.1;
return (
<g
key={dimension.edge}
className="dimension-lines"
stroke="#637185"
strokeWidth="0.8"
fill="none"
>
{[
[start, lineStart],
[end, lineEnd],
].map(([point, projected], i) => (
<line
key={i}
className="dimension-extension"
x1={point.x + normal.x * font * 0.12}
y1={point.y + normal.y * font * 0.12}
x2={projected.x + normal.x * font * 0.12}
y2={projected.y + normal.y * font * 0.12}
vectorEffect="non-scaling-stroke"
/>
))}
<line
className="dimension-line"
x1={lineStart.x}
y1={lineStart.y}
x2={lineEnd.x}
y2={lineEnd.y}
vectorEffect="non-scaling-stroke"
/>
{[lineStart, lineEnd].map((point, i) => (
<line
key={i}
className="dimension-tick"
x1={point.x - (normal.x - normal.y) * font * 0.075}
y1={point.y - (normal.y + normal.x) * font * 0.075}
x2={point.x + (normal.x - normal.y) * font * 0.075}
y2={point.y + (normal.y + normal.x) * font * 0.075}
vectorEffect="non-scaling-stroke"
/>
))}
{staggered && (
<line
className="dimension-leader"
x1={center.x}
y1={center.y}
x2={label.x}
y2={label.y}
vectorEffect="non-scaling-stroke"
/>
)}
</g>
);
})}
{dimensions.map((dimension) => (
<g key={dimension.edge} className="dimension-caption" data-edge={dimension.edge}>
<rect {...dimension.labelBox} fill="white" />
<text
className="dimension-label"
x={dimension.label.x}
y={dimension.label.y}
textAnchor="middle"
dominantBaseline="central"
fontSize={font}
fill="#17263f"
>
{dimension.angle === undefined ? (
dimension.text
) : (
<>
<tspan x={dimension.label.x} dy={-font * 0.6}>
{dimension.text}
</tspan>{' '}
<tspan x={dimension.label.x} dy={font * 1.2}>
{exactMm(dimension.angle)}°
</tspan>
</>
)}
</text>
</g>
))}
</g>
);
}
export function FlooringPreview({
piece,
@ -17,12 +112,15 @@ export function FlooringPreview({
compact?: boolean;
}) {
const template = flooringTemplate(piece);
const dimensions = compact
? undefined
: dimensionedOutline(template, piece.shaped ? undefined : [0, 1]);
const Wrapper = compact ? 'span' : 'figure';
return (
<Wrapper className={`flooring-preview-figure ${compact ? 'compact-preview' : 'exact-preview'}`}>
<svg
className="flooring-preview"
viewBox={viewBox(fullPreviewBox(piece.length, piece.width))}
viewBox={viewBox(dimensions?.box ?? fullPreviewBox(piece.length, piece.width))}
preserveAspectRatio="xMidYMid meet"
aria-hidden={compact || undefined}
role={compact ? undefined : 'img'}
@ -35,24 +133,13 @@ export function FlooringPreview({
strokeWidth={compact ? 1 : 1.5}
vectorEffect="non-scaling-stroke"
/>
{!compact &&
piece.shaped &&
template.map((point, index) => (
<text
key={index}
x={point.x}
y={point.y - Math.max(piece.length, piece.width) * 0.015}
textAnchor="middle"
fontSize={Math.max(piece.length, piece.width) * 0.025}
fill="#1e40af"
>
{index + 1}
</text>
))}
{dimensions && <Dimensions dimensions={dimensions.dimensions} font={dimensions.font} />}
</svg>
{!compact && (
<figcaption>
Length {exactMm(piece.length)} mm × width {exactMm(piece.width)} mm · top view, to scale
{exactMm(piece.length)} × {exactMm(piece.width)} mm
{dimensions?.dimensions.some((dimension) => dimension.angle !== undefined) &&
' · angles from horizontal'}
</figcaption>
)}
</Wrapper>
@ -74,6 +161,18 @@ export function SkirtingPreview({
const Grid = compact ? 'span' : 'div';
if (template.length !== 4) return <span className="helper">Shape template unavailable</span>;
const boxes = skirtingEndBoxes(template, thickness);
const dimensions = compact
? undefined
: dimensionedOutline(
[template[0], template[1], { x: piece.length, y: thickness }, { x: 0, y: thickness }],
[0, 2],
);
// The lower dimension measures the overall blank, between its actual longest points.
const overall = dimensions?.dimensions.find((dimension) => dimension.edge === 2);
if (overall) {
overall.start = template.reduce((a, b) => (a.x > b.x ? a : b));
overall.end = template.reduce((a, b) => (a.x < b.x ? a : b));
}
const points = template.map((point) => `${point.x},${point.y}`).join(' ');
const ends = [
{ side: 'left', back: template[0], front: template[3], box: boxes.left },
@ -84,7 +183,7 @@ export function SkirtingPreview({
{!compact && (
<figure className="skirting-full-preview">
<svg
viewBox={viewBox(fullPreviewBox(piece.length, thickness))}
viewBox={viewBox(dimensions!.box)}
preserveAspectRatio="xMidYMid meet"
role="img"
aria-label={`Full skirting shape ${piece.id}`}
@ -96,10 +195,10 @@ export function SkirtingPreview({
strokeWidth={1}
vectorEffect="non-scaling-stroke"
/>
<Dimensions dimensions={dimensions!.dimensions} font={dimensions!.font} />
</svg>
<figcaption>
Full length {exactMm(piece.length)} mm × thickness {exactMm(thickness)} mm · top view,
to scale
{exactMm(piece.length)} × {exactMm(thickness)} mm · wall side above
</figcaption>
</figure>
)}
@ -142,8 +241,9 @@ export function SkirtingPreview({
</svg>
{!compact && (
<figcaption>
{index === 0 ? 'Left' : 'Right'} end ·{' '}
{exactMm(Math.abs(index === 0 ? piece.leftMitre : piece.rightMitre))}°
{index === 0 ? 'Left' : 'Right'} ·{' '}
{exactMm(Math.abs(index === 0 ? piece.leftMitre : piece.rightMitre))}°{' '}
{index === 0 ? piece.leftType : piece.rightType}
</figcaption>
)}
</EndWrapper>
@ -155,12 +255,6 @@ export function SkirtingPreview({
</Fragment>
))}
</Grid>
{!compact && (
<p className="helper">
End close-ups; the middle is omitted. The back edge faces the wall, the front edge faces
the room. Both axes use the same scale.
</p>
)}
</Wrapper>
);
}