"use client"; import { useId } from "react"; export interface GridPoint { x: number; y: number; label?: string; tone?: "solid" | "target" | "wrong" | "muted"; } export interface GridSegment { from: { x: number; y: number }; to: { x: number; y: number }; } export interface GridArrow { from: { x: number; y: number }; to: { x: number; y: number }; dashed?: boolean; } interface CartesianGridProps { points?: GridPoint[]; segments?: GridSegment[]; arrows?: GridArrow[]; /** Draws dashed guide lines from this point to the axes. `axis` picks which (default both). */ highlight?: { x: number; y: number; axis?: "x" | "y" | "both" }; onGridClick?: (x: number, y: number) => void; } const MIN = -10; const MAX = 10; const UNIT = 20; const MARGIN = 20; const SIZE = MARGIN * 2 + (MAX - MIN) * UNIT; // 440 const sx = (x: number) => MARGIN + (x - MIN) * UNIT; const sy = (y: number) => MARGIN + (MAX - y) * UNIT; const toneColor: Record, string> = { solid: "var(--unit-7)", target: "var(--correct)", wrong: "var(--incorrect)", muted: "var(--muted)", }; export function CartesianGrid({ points = [], segments = [], arrows = [], highlight, onGridClick, }: CartesianGridProps) { const arrowHeadId = useId(); const axisArrowId = useId(); const ticks: number[] = []; for (let v = MIN; v <= MAX; v++) ticks.push(v); const labelTicks = ticks.filter((v) => v !== 0 && v % 2 === 0); function handleClick(e: React.MouseEvent) { if (!onGridClick) return; const rect = e.currentTarget.getBoundingClientRect(); const px = ((e.clientX - rect.left) / rect.width) * SIZE; const py = ((e.clientY - rect.top) / rect.height) * SIZE; const x = Math.round((px - MARGIN) / UNIT + MIN); const y = Math.round(MAX - (py - MARGIN) / UNIT); if (x < MIN || x > MAX || y < MIN || y > MAX) return; onGridClick(x, y); } return ( {/* Light gridlines */} {ticks.map((v) => ( ))} {/* Axes */} x y {/* Axis labels */} {labelTicks.map((v) => ( {v} ))} {labelTicks.map((v) => ( {v} ))} 0 {/* Guide lines */} {highlight && ( {(highlight.axis ?? "both") !== "y" && ( )} {(highlight.axis ?? "both") !== "x" && ( )} )} {/* Segments */} {segments.map((s, i) => ( ))} {/* Movement arrows */} {arrows.map((a, i) => ( ))} {/* Points */} {points.map((p, i) => { const color = toneColor[p.tone ?? "solid"]; return ( {p.label && ( {p.label} )} ); })} ); }