Files
sudoku-tools/src/components/SafeVisualLayer.tsx
T

140 lines
3.8 KiB
TypeScript

import type { SVGAttributes } from "react";
import type {
SafeVisualAnchor,
SafeVisualPrimitive,
SafeVisualStyle,
} from "../formats";
export interface SafeVisualLayerProps {
readonly size: number;
readonly visuals: readonly SafeVisualPrimitive[];
readonly layer: "underlay" | "overlay";
}
function anchorPoint(size: number, anchor: SafeVisualAnchor) {
if (anchor.kind === "coordinate") {
return { x: anchor.x, y: anchor.y };
}
return {
x: (anchor.cell % size) + 0.5 + (anchor.offsetX ?? 0),
y: Math.floor(anchor.cell / size) + 0.5 + (anchor.offsetY ?? 0),
};
}
function visualStyle(
style: SafeVisualStyle | undefined,
): SVGAttributes<SVGElement> {
return {
stroke: style?.stroke ?? "transparent",
fill: style?.fill ?? "transparent",
...(style?.strokeWidth === undefined
? {}
: { strokeWidth: style.strokeWidth }),
...(style?.opacity === undefined ? {} : { opacity: style.opacity }),
};
}
/**
* Render already-normalized inert drawings with typed SVG properties only.
* Text is supplied as a React text node; no source markup is ever interpreted.
*/
export function SafeVisualLayer({
size,
visuals,
layer,
}: SafeVisualLayerProps) {
return (
<g
className={`safe-visual-layer safe-visual-layer--${layer}`}
data-visual-layer={layer}
>
{visuals.map((visual, index) => {
if (visual.layer !== layer) return null;
const common = {
...visualStyle(visual.style),
className: `source-visual source-visual--${visual.type}`,
"data-visual": index,
};
if (visual.type === "line") {
const start = anchorPoint(size, visual.start);
const end = anchorPoint(size, visual.end);
return (
<line
key={index}
{...common}
x1={start.x}
y1={start.y}
x2={end.x}
y2={end.y}
/>
);
}
if (visual.type === "polyline") {
const points = visual.points
.map((anchor) => anchorPoint(size, anchor))
.map(({ x, y }) => `${String(x)},${String(y)}`)
.join(" ");
return visual.closed === true ? (
<polygon key={index} {...common} points={points} />
) : (
<polyline key={index} {...common} points={points} />
);
}
if (visual.type === "rectangle") {
const center = anchorPoint(size, visual.center);
return (
<rect
key={index}
{...common}
x={center.x - visual.width / 2}
y={center.y - visual.height / 2}
width={visual.width}
height={visual.height}
rx={visual.cornerRadius}
/>
);
}
if (visual.type === "ellipse") {
const center = anchorPoint(size, visual.center);
return (
<ellipse
key={index}
{...common}
cx={center.x}
cy={center.y}
rx={visual.radiusX}
ry={visual.radiusY}
/>
);
}
if (visual.type === "circle") {
const center = anchorPoint(size, visual.center);
return (
<circle
key={index}
{...common}
cx={center.x}
cy={center.y}
r={visual.radius}
/>
);
}
const position = anchorPoint(size, visual.position);
return (
<text
key={index}
{...common}
x={position.x}
y={position.y}
fontSize={visual.style?.fontSize}
textAnchor="middle"
dominantBaseline="central"
>
{visual.text}
</text>
);
})}
</g>
);
}