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 { 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 ( {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 ( ); } 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 ? ( ) : ( ); } if (visual.type === "rectangle") { const center = anchorPoint(size, visual.center); return ( ); } if (visual.type === "ellipse") { const center = anchorPoint(size, visual.center); return ( ); } if (visual.type === "circle") { const center = anchorPoint(size, visual.center); return ( ); } const position = anchorPoint(size, visual.position); return ( {visual.text} ); })} ); }