260 lines
8.8 KiB
TypeScript
260 lines
8.8 KiB
TypeScript
import { Fragment } from 'react';
|
||
import type { FlooringPiece, SkirtingPiece } from '../lib/types';
|
||
import { cutMm, degree } from '../lib/format';
|
||
import {
|
||
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}>
|
||
{degree(dimension.angle)}
|
||
</tspan>
|
||
</>
|
||
)}
|
||
</text>
|
||
</g>
|
||
))}
|
||
</g>
|
||
);
|
||
}
|
||
|
||
export function FlooringPreview({
|
||
piece,
|
||
compact = false,
|
||
}: {
|
||
piece: FlooringPiece;
|
||
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(dimensions?.box ?? 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"
|
||
/>
|
||
{dimensions && <Dimensions dimensions={dimensions.dimensions} font={dimensions.font} />}
|
||
</svg>
|
||
{!compact && (
|
||
<figcaption>
|
||
{cutMm(piece.length)} × {cutMm(piece.width)} mm
|
||
{dimensions?.dimensions.some((dimension) => dimension.angle !== undefined) &&
|
||
' · angles from horizontal'}
|
||
</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 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 },
|
||
{ 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(dimensions!.box)}
|
||
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"
|
||
/>
|
||
<Dimensions dimensions={dimensions!.dimensions} font={dimensions!.font} />
|
||
</svg>
|
||
<figcaption>
|
||
{cutMm(piece.length)} × {cutMm(thickness)} mm · wall side above
|
||
</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'} ·{' '}
|
||
{degree(index === 0 ? piece.leftMitre : piece.rightMitre)}{' '}
|
||
{index === 0 ? piece.leftType : piece.rightType}
|
||
</figcaption>
|
||
)}
|
||
</EndWrapper>
|
||
{index === 0 && (
|
||
<span className="board-break" aria-hidden="true">
|
||
//
|
||
</span>
|
||
)}
|
||
</Fragment>
|
||
))}
|
||
</Grid>
|
||
</Wrapper>
|
||
);
|
||
}
|