/** * WhiteboardCanvas — SVG-based whiteboard for classroom scene rendering. * * Supports incremental drawing operations: * - Text (positioned labels) * - Shapes (rectangles, circles, arrows) * - Charts (bar/line/pie via simple SVG) * - LaTeX (rendered as styled text blocks) */ import { useCallback } from 'react'; import type { SceneAction } from '../../types/classroom'; interface WhiteboardCanvasProps { items: WhiteboardItem[]; width?: number; height?: number; } export interface WhiteboardItem { type: string; data: SceneAction; } export function WhiteboardCanvas({ items, width = 800, height = 600, }: WhiteboardCanvasProps) { const renderItem = useCallback((item: WhiteboardItem, index: number) => { switch (item.type) { case 'whiteboard_draw_text': return ; case 'whiteboard_draw_shape': return ; case 'whiteboard_draw_chart': return ; case 'whiteboard_draw_latex': return ; default: return null; } }, []); return (
{/* Grid background */} {/* Rendered items */} {items.map((item, i) => renderItem(item, i))}
); } // --------------------------------------------------------------------------- // Sub-components // --------------------------------------------------------------------------- interface TextDrawData { type: 'whiteboard_draw_text'; x: number; y: number; text: string; fontSize?: number; color?: string; } function TextItem({ data }: { data: TextDrawData }) { return ( {data.text} ); } interface ShapeDrawData { type: 'whiteboard_draw_shape'; shape: string; x: number; y: number; width: number; height: number; fill?: string; } function ShapeItem({ data }: { data: ShapeDrawData }) { switch (data.shape) { case 'circle': return ( ); case 'arrow': return ( ); default: // rectangle return ( ); } } interface ChartDrawData { type: 'whiteboard_draw_chart'; chartType: string; data: Record; x: number; y: number; width: number; height: number; } function ChartItem({ data }: { data: ChartDrawData }) { const chartData = data.data; const labels = (chartData?.labels as string[]) ?? []; const values = (chartData?.values as number[]) ?? []; if (labels.length === 0 || values.length === 0) return null; switch (data.chartType) { case 'bar': return ; case 'line': return ; default: return ; } } function BarChart({ data, labels, values }: { data: ChartDrawData; labels: string[]; values: number[]; }) { const maxVal = Math.max(...values, 1); const barWidth = data.width / (labels.length * 2); const chartHeight = data.height - 30; return ( {values.map((val, i) => { const barHeight = (val / maxVal) * chartHeight; return ( {labels[i]} ); })} ); } function LineChart({ data, labels, values }: { data: ChartDrawData; labels: string[]; values: number[]; }) { const maxVal = Math.max(...values, 1); const chartHeight = data.height - 30; const stepX = data.width / Math.max(labels.length - 1, 1); const points = values.map((val, i) => { const x = i * stepX; const y = chartHeight - (val / maxVal) * chartHeight; return `${x},${y}`; }).join(' '); return ( {values.map((val, i) => { const x = i * stepX; const y = chartHeight - (val / maxVal) * chartHeight; return ( {labels[i]} ); })} ); } interface LatexDrawData { type: 'whiteboard_draw_latex'; latex: string; x: number; y: number; } function LatexItem({ data }: { data: LatexDrawData }) { return ( {data.latex} ); }