feat: complete advanced Sudoku workbench
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user