/ concept-collection / ans-visualizer
Sign in
concept-collection / ans-visualizer
ans-visualizer / src / components / StateGrid.tsx
318 lines · 9.2 KBBlameHistoryRaw
1import { Box, Paper, Typography, TextField } from "@mui/material";
2import { useState, useEffect, useMemo, useRef, useCallback } from "react";
3import type { Symbol } from "../types";
4import {
5 generateStatePattern,
6 calculateL,
7 calculateEdgeChain,
8 calculateForwardEdges,
9} from "../utils/ansLogic";
10import {
11 clearCanvas,
12 drawStateBox,
13 drawEdge,
14 getBoxAtPosition,
15 type RenderConfig,
16} from "../utils/canvasRenderer";
18interface StateGridProps {
19 symbols: Symbol[];
22export default function StateGrid({ symbols }: StateGridProps) {
23 const canvasRef = useRef<HTMLCanvasElement>(null);
24 const [containerElement, setContainerElement] =
25 useState<HTMLDivElement | null>(null);
26 const [boxSize, setBoxSize] = useState(60);
27 const [boxesPerRow, setBoxesPerRow] = useState(0);
28 const [hoveredState, setHoveredState] = useState<number | null>(null);
29 const [numLeadingAs, setNumLeadingAs] = useState(0);
31 const L = useMemo(() => calculateL(symbols), [symbols]);
32 const maxStates = 5000; // Show first 5000 states
33 const states = useMemo(
34 () => generateStatePattern(symbols, maxStates),
35 [symbols, maxStates]
36 );
39 // Calculate edge chain on demand for hovered state
40 const edgeChain = useMemo(() => {
41 if (hoveredState === null) return [];
42 console.log("Calculating edge chain for state:", hoveredState);
43 return calculateEdgeChain(hoveredState, symbols, numLeadingAs);
44 }, [hoveredState, symbols, numLeadingAs]);
46 // Calculate forward edges on demand for hovered state
47 const forwardEdges = useMemo(() => {
48 if (hoveredState === null) return [];
49 console.log("Calculating forward edges for state:", hoveredState);
50 return calculateForwardEdges(hoveredState, symbols, maxStates);
51 }, [hoveredState, symbols, maxStates]);
53 // Calculate canvas dimensions
54 const canvasWidth = useMemo(() => {
55 if (!containerElement || boxesPerRow === 0) return 0;
56 return containerElement.clientWidth;
57 }, [containerElement, boxesPerRow]);
59 const canvasHeight = useMemo(() => {
60 if (boxesPerRow === 0) return 0;
61 const numRows = Math.ceil(states.length / boxesPerRow);
62 const boxHeight = Math.floor(boxSize * 0.6);
63 return numRows * boxHeight;
64 }, [states.length, boxesPerRow, boxSize]);
66 // Update layout based on container size
67 useEffect(() => {
68 const updateLayout = () => {
69 if (!containerElement) return;
71 const containerWidth = containerElement.clientWidth;
72 if (containerWidth === 0) return; // Container not ready yet
74 const desiredBoxSize = 24;
75 const gapSize = 6; // Space between groups
77 // Calculate how many complete groups can fit
78 // Each group has L boxes, and each group (except the last on a row) has a gap after it
79 let numGroups = 1;
80 while (true) {
81 const totalWidth =
82 numGroups * L * desiredBoxSize + (numGroups - 1) * gapSize;
83 if (totalWidth > containerWidth) break;
84 numGroups++;
85 }
86 numGroups = Math.max(1, numGroups - 1);
88 const adjustedBoxesPerRow = numGroups * L;
90 // Calculate actual box size to fill the width perfectly
91 const totalGapWidth = (numGroups - 1) * gapSize;
92 const availableWidthForBoxes = containerWidth - totalGapWidth;
93 const actualBoxSize = Math.floor(
94 availableWidthForBoxes / adjustedBoxesPerRow
95 );
97 setBoxesPerRow(adjustedBoxesPerRow);
98 setBoxSize(actualBoxSize);
99 };
101 updateLayout();
103 window.addEventListener("resize", updateLayout);
104 return () => {
105 window.removeEventListener("resize", updateLayout);
106 };
107 }, [L, containerElement]);
109 // Render canvas whenever state changes
110 useEffect(() => {
111 const canvas = canvasRef.current;
112 if (!canvas || boxesPerRow === 0) return;
114 const ctx = canvas.getContext("2d");
115 if (!ctx) return;
117 // Set canvas resolution
118 const dpr = window.devicePixelRatio || 1;
119 canvas.width = canvasWidth * dpr;
120 canvas.height = canvasHeight * dpr;
121 canvas.style.width = `${canvasWidth}px`;
122 canvas.style.height = `${canvasHeight}px`;
123 ctx.scale(dpr, dpr);
125 // Clear canvas
126 clearCanvas(ctx, canvasWidth, canvasHeight);
128 // Create render config
129 const config: RenderConfig = {
130 boxSize,
131 boxesPerRow,
132 L,
133 canvasWidth,
134 canvasHeight,
135 };
137 // Draw all state boxes
138 states.forEach((state) => {
139 const isHovered = state.index === hoveredState;
140 drawStateBox(ctx, state, config, isHovered);
141 });
143 // Draw backward edges (incoming) - black with arrow pointing to hovered state
144 if (edgeChain.length > 1) {
145 edgeChain.slice(0, -1).forEach((fromState, idx) => {
146 const toState = edgeChain[idx + 1];
147 // Draw from toState to fromState so arrow points to hovered
148 drawEdge(ctx, toState, fromState, config, "rgba(0, 0, 0, 0.85)", 3);
149 });
150 }
152 // Draw forward edges (outgoing) - blue with arrow pointing away from hovered state
153 if (forwardEdges.length > 0 && hoveredState !== null) {
154 forwardEdges.forEach((edge) => {
155 drawEdge(
156 ctx,
157 hoveredState,
158 edge.toState,
159 config,
160 "rgba(33, 150, 243, 0.85)",
161 3
162 );
163 });
164 }
165 }, [
166 states,
167 boxSize,
168 boxesPerRow,
169 L,
170 canvasWidth,
171 canvasHeight,
172 hoveredState,
173 edgeChain,
174 forwardEdges,
175 ]);
177 // Handle mouse move on canvas
178 const handleMouseMove = useCallback(
179 (e: React.MouseEvent<HTMLCanvasElement>) => {
180 const canvas = canvasRef.current;
181 if (!canvas || boxesPerRow === 0) return;
183 const rect = canvas.getBoundingClientRect();
184 const mouseX = e.clientX - rect.left;
185 const mouseY = e.clientY - rect.top;
187 const config: RenderConfig = {
188 boxSize,
189 boxesPerRow,
190 L,
191 canvasWidth,
192 canvasHeight,
193 };
195 const boxIndex = getBoxAtPosition(
196 mouseX,
197 mouseY,
198 states.length,
199 config
200 );
201 setHoveredState(boxIndex);
202 },
203 [boxSize, boxesPerRow, L, canvasWidth, canvasHeight, states.length]
204 );
206 // Handle mouse leave
207 const handleMouseLeave = useCallback(() => {
208 setHoveredState(null);
209 }, []);
211 if (symbols.length === 0) {
212 return (
213 <Paper elevation={2} sx={{ p: 3 }}>
214 <Typography color="text.secondary">
215 Add at least one symbol to see the state visualization.
216 </Typography>
217 </Paper>
218 );
219 }
221 if (boxesPerRow === 0) {
222 return (
223 <Paper elevation={2} sx={{ p: 3 }}>
224 <Typography variant="h6" gutterBottom>
225 State Visualization
226 </Typography>
227 <Box
228 ref={(elmt: HTMLDivElement | null) => setContainerElement(elmt)}
229 sx={{
230 border: "1px solid",
231 borderColor: "divider",
232 backgroundColor: "grey.50",
233 minHeight: 100,
234 display: "flex",
235 alignItems: "center",
236 justifyContent: "center",
237 }}
238 >
239 <Typography color="text.secondary" sx={{ p: 2 }}>
240 Loading state visualization...
241 </Typography>
242 </Box>
243 </Paper>
244 );
245 }
247 return (
248 <Paper elevation={2} sx={{ p: 2 }}>
249 <Typography variant="subtitle1" gutterBottom sx={{ fontWeight: 600 }}>
250 State Visualization
251 </Typography>
252 <Typography variant="body2" color="text.secondary" sx={{ mb: 1.5 }}>
253 Each box represents a state (integer).
254 </Typography>
256 <Box sx={{ mb: 1.5 }}>
257 <TextField
258 label="Number of Leading A's"
259 type="number"
260 value={numLeadingAs}
261 onChange={(e) => {
262 const value = parseInt(e.target.value, 10);
263 if (!isNaN(value) && value >= 0) {
264 setNumLeadingAs(value);
265 }
266 }}
267 size="small"
268 inputProps={{ min: 0, step: 1 }}
269 sx={{ width: 200 }}
270 />
271 </Box>
273 <Box sx={{ mb: 1.5, p: 1.5, backgroundColor: "grey.100", borderRadius: 1, height: 45, overflow: "hidden" }}>
274 {hoveredState !== null ? (
275 <Typography variant="body2">
276 <Box component="span">
277 <span style={{ fontWeight: "bold" }}>State:</span> {hoveredState}
278 </Box>{" | "}
279 <Box
280 component="span"
281 sx={{ fontWeight: "medium", fontFamily: "monospace" }}
282 >
283 <span style={{ fontWeight: "bold" }}>Encoded sequence:</span> {edgeChain
284 .slice()
285 .reverse()
286 .map((stateIdx) => states[stateIdx]?.symbol.name || "?")
287 .join(" → ")}
288 </Box>
289 </Typography>
290 ) : (
291 <Typography variant="body2" color="text.secondary" sx={{ fontStyle: "italic" }}>
292 Hover over a state to see its details
293 </Typography>
294 )}
295 </Box>
297 <Box
298 ref={(elmt: HTMLDivElement | null) => setContainerElement(elmt)}
299 sx={{
300 border: "1px solid",
301 borderColor: "divider",
302 backgroundColor: "grey.50",
303 position: "relative",
304 }}
305 >
306 <canvas
307 ref={canvasRef}
308 onMouseMove={handleMouseMove}
309 onMouseLeave={handleMouseLeave}
310 style={{
311 display: "block",
312 cursor: "pointer",
313 }}
314 />
315 </Box>
316 </Paper>
317 );
moveopenescclose