This commit is contained in:
Andras Schmelczer 2026-09-11 16:09:28 +01:00
commit d8f2407ddf
44 changed files with 10377 additions and 0 deletions

View file

@ -0,0 +1,166 @@
import { Fragment } from 'react';
import type { FlooringPiece, SkirtingPiece } from '../lib/types';
import {
exactMm,
flooringTemplate,
fullPreviewBox,
skirtingEndBoxes,
skirtingTemplate,
viewBox,
} from '../lib/cut-preview';
export function FlooringPreview({
piece,
compact = false,
}: {
piece: FlooringPiece;
compact?: boolean;
}) {
const template = flooringTemplate(piece);
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))}
preserveAspectRatio="xMidYMid meet"
aria-hidden={compact || undefined}
role={compact ? undefined : 'img'}
aria-label={compact ? undefined : `Flooring shape ${piece.id}`}
>
<polygon
points={template.map((point) => `${point.x},${point.y}`).join(' ')}
fill="#dbeafe"
stroke="#2563eb"
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>
))}
</svg>
{!compact && (
<figcaption>
Length {exactMm(piece.length)} mm × width {exactMm(piece.width)} mm · top view, to scale
</figcaption>
)}
</Wrapper>
);
}
export function SkirtingPreview({
piece,
thickness,
compact = false,
}: {
piece: SkirtingPiece;
thickness: number;
compact?: boolean;
}) {
const template = skirtingTemplate(piece, thickness);
const Wrapper = compact ? 'span' : 'div';
const EndWrapper = compact ? 'span' : 'figure';
const Grid = compact ? 'span' : 'div';
if (template.length !== 4) return <span className="helper">Shape template unavailable</span>;
const boxes = skirtingEndBoxes(template, thickness);
const points = template.map((point) => `${point.x},${point.y}`).join(' ');
const ends = [
{ side: 'left', back: template[0], front: template[3], box: boxes.left },
{ side: 'right', back: template[1], front: template[2], box: boxes.right },
] as const;
return (
<Wrapper className={`skirting-preview ${compact ? 'compact-preview' : 'exact-preview'}`}>
{!compact && (
<figure className="skirting-full-preview">
<svg
viewBox={viewBox(fullPreviewBox(piece.length, thickness))}
preserveAspectRatio="xMidYMid meet"
role="img"
aria-label={`Full skirting shape ${piece.id}`}
>
<polygon
points={points}
fill="#ffedd5"
stroke="#c2410c"
strokeWidth={1}
vectorEffect="non-scaling-stroke"
/>
</svg>
<figcaption>
Full length {exactMm(piece.length)} mm × thickness {exactMm(thickness)} mm · top view,
to scale
</figcaption>
</figure>
)}
<Grid className="skirting-end-grid">
{ends.map((end, index) => (
<Fragment key={end.side}>
<EndWrapper className={`skirting-end ${end.side}-end`}>
<svg
className="skirting-end-preview"
data-end={end.side}
viewBox={viewBox(end.box)}
preserveAspectRatio="xMidYMid meet"
overflow="hidden"
aria-hidden={compact || undefined}
role={compact ? undefined : 'img'}
aria-label={
compact
? undefined
: `${end.side === 'left' ? 'Left' : 'Right'} mitre close-up ${piece.id}`
}
>
<polygon
points={points}
fill="#ffedd5"
stroke="#c2410c"
strokeWidth={1}
vectorEffect="non-scaling-stroke"
/>
<line
className="skirting-cut-edge"
data-end={end.side}
x1={end.back.x}
y1={end.back.y}
x2={end.front.x}
y2={end.front.y}
stroke="#9a3412"
strokeWidth={compact ? 1.5 : 2.5}
vectorEffect="non-scaling-stroke"
/>
</svg>
{!compact && (
<figcaption>
{index === 0 ? 'Left' : 'Right'} end ·{' '}
{exactMm(Math.abs(index === 0 ? piece.leftMitre : piece.rightMitre))}°
</figcaption>
)}
</EndWrapper>
{index === 0 && (
<span className="board-break" aria-hidden="true">
//
</span>
)}
</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>
);
}