import React, { useMemo } from 'react'; import { useGridStore } from '../../store/gridStore'; import type { ColoredPath } from '../../store/gridStore'; import { Map as MapIcon } from 'lucide-react'; import type { SearchStep } from '../../types'; const CELL_SIZE = 90; const PADDING = 70; const NODE_RADIUS = 14; const COLORS = { background: '#0a0a0b', gridLine: '#1f1f23', // Node states nodeDefault: '#27272a', nodeDefaultStroke: '#3f3f46', frontier: '#0ea5e9', frontierLight: '#38bdf8', explored: '#52525b', exploredLight: '#71717a', current: '#f59e0b', currentLight: '#fbbf24', path: '#10b981', pathLight: '#34d399', // Entities store: '#3b82f6', storeLight: '#60a5fa', destination: '#10b981', destinationLight: '#34d399', tunnel: '#a855f7', tunnelLight: '#c084fc', // Edges edge1: '#a1a1aa', edge2: '#71717a', edge3: '#52525b', edge4: '#3f3f46', blocked: '#ef4444', blockedBg: '#450a0a', // Text text: '#fafafa', textMuted: '#a1a1aa', textDim: '#52525b', }; export const Grid: React.FC = () => { const { grid, steps, currentStep, searchResult, showPlanPaths, planPaths } = useGridStore(); const stepData = useMemo(() => { const step: SearchStep | null = steps[currentStep] || null; return { step, exploredSet: new Set(step?.explored.map(p => `${p.x},${p.y}`) || []), frontierSet: new Set(step?.frontier.map(p => `${p.x},${p.y}`) || []), pathSet: new Set(step?.currentPath.map(p => `${p.x},${p.y}`) || []), finalPathSet: new Set((searchResult?.path || []).map(p => `${p.x},${p.y}`)), }; }, [steps, currentStep, searchResult]); // Build a map of position -> color for plan paths visualization const planPathData = useMemo(() => { if (!showPlanPaths || planPaths.length === 0) { return { pathColorMap: new Map(), pathEdges: [] as { from: string; to: string; color: string }[] }; } const pathColorMap = new Map(); const pathEdges: { from: string; to: string; color: string }[] = []; planPaths.forEach((coloredPath: ColoredPath) => { const path = coloredPath.path; for (let i = 0; i < path.length; i++) { const key = `${path[i].x},${path[i].y}`; // First path to claim a position gets to color it if (!pathColorMap.has(key)) { pathColorMap.set(key, coloredPath.color); } // Build edges if (i < path.length - 1) { const fromKey = `${path[i].x},${path[i].y}`; const toKey = `${path[i + 1].x},${path[i + 1].y}`; pathEdges.push({ from: fromKey, to: toKey, color: coloredPath.color }); } } }); return { pathColorMap, pathEdges }; }, [showPlanPaths, planPaths]); if (!grid) { return (

No Grid Generated

Configure and generate a grid from the sidebar

); } const width = grid.width * CELL_SIZE + PADDING * 2; const height = grid.height * CELL_SIZE + PADDING * 2; const { step, exploredSet, frontierSet, pathSet, finalPathSet } = stepData; const { pathColorMap, pathEdges } = planPathData; const getNodeState = (x: number, y: number): { state: string; color?: string } => { const key = `${x},${y}`; if (step?.currentNode.x === x && step?.currentNode.y === y) return { state: 'current' }; if (pathSet.has(key) || (!step && finalPathSet.has(key))) return { state: 'path' }; if (frontierSet.has(key)) return { state: 'frontier' }; if (exploredSet.has(key)) return { state: 'explored' }; // Check if this node is part of a plan path if (showPlanPaths && pathColorMap.has(key)) { return { state: 'planPath', color: pathColorMap.get(key) }; } return { state: 'default' }; }; const getEdgeStyle = (traffic: number) => { if (traffic === 0) return { color: COLORS.blocked, width: 2, opacity: 0.4 }; const colors = [COLORS.edge1, COLORS.edge2, COLORS.edge3, COLORS.edge4]; const widths = [3, 2.5, 2, 1.5]; return { color: colors[traffic - 1], width: widths[traffic - 1], opacity: 0.7 }; }; const toSvgY = (gridY: number) => PADDING + (grid.height - 1 - gridY) * CELL_SIZE + CELL_SIZE / 2; const toSvgX = (gridX: number) => PADDING + gridX * CELL_SIZE + CELL_SIZE / 2; return (
{/* Glows for different states */} {/* Tunnel gradient */} {/* Blocked pattern */} {/* Background */} {/* Grid cells background */} {Array.from({ length: grid.width }).map((_, x) => Array.from({ length: grid.height }).map((_, y) => ( )) )} {/* Edges */} {grid.segments.map((segment, i) => { const x1 = toSvgX(segment.src.x); const y1 = toSvgY(segment.src.y); const x2 = toSvgX(segment.dst.x); const y2 = toSvgY(segment.dst.y); const midX = (x1 + x2) / 2; const midY = (y1 + y2) / 2; const isBlocked = segment.traffic === 0; const style = getEdgeStyle(segment.traffic); if (isBlocked) { return ( {/* Blocked edge line */} {/* Blocked indicator */} ); } return ( {/* Edge line */} {/* Cost badge */} {segment.traffic} ); })} {/* Tunnels */} {grid.tunnels.map((tunnel, i) => { const x1 = toSvgX(tunnel.entrance1.x); const y1 = toSvgY(tunnel.entrance1.y); const x2 = toSvgX(tunnel.entrance2.x); const y2 = toSvgY(tunnel.entrance2.y); const midX = (x1 + x2) / 2; const midY = (y1 + y2) / 2; const dx = x2 - x1; const dy = y2 - y1; const len = Math.sqrt(dx * dx + dy * dy); const curveOffset = Math.min(60, len / 2.5); const perpX = -dy / len * curveOffset; const perpY = dx / len * curveOffset; return ( {/* Tunnel glow background */} {/* Tunnel path */} {/* Animated dashes */} {/* Portal entrance 1 */} {/* Portal entrance 2 */} {/* Cost label */} T:{tunnel.cost} ); })} {/* Plan Path Edges (rendered before nodes) */} {showPlanPaths && pathEdges.map((edge, i) => { const [fromX, fromY] = edge.from.split(',').map(Number); const [toX, toY] = edge.to.split(',').map(Number); const x1 = toSvgX(fromX); const y1 = toSvgY(fromY); const x2 = toSvgX(toX); const y2 = toSvgY(toY); return ( ); })} {/* Nodes */} {Array.from({ length: grid.width }).map((_, x) => Array.from({ length: grid.height }).map((_, y) => { const cx = toSvgX(x); const cy = toSvgY(y); const nodeState = getNodeState(x, y); const state = nodeState.state; let fill = COLORS.nodeDefault; let stroke = COLORS.nodeDefaultStroke; let filter = ''; let radius = NODE_RADIUS; let strokeWidth = 1.5; switch (state) { case 'current': fill = COLORS.current; stroke = COLORS.currentLight; filter = 'url(#glow-current)'; radius = NODE_RADIUS + 2; strokeWidth = 2; break; case 'path': fill = COLORS.path; stroke = COLORS.pathLight; filter = 'url(#glow-path)'; strokeWidth = 2; break; case 'frontier': fill = COLORS.frontier; stroke = COLORS.frontierLight; filter = 'url(#glow-frontier)'; break; case 'explored': fill = COLORS.explored; stroke = COLORS.exploredLight; break; case 'planPath': fill = nodeState.color || COLORS.path; stroke = nodeState.color || COLORS.pathLight; strokeWidth = 2; break; } return ( {state === 'current' && ( )} ); }) )} {/* Stores */} {grid.stores.map((store) => { const cx = toSvgX(store.position.x); const cy = toSvgY(store.position.y); return ( S{store.id} ); })} {/* Destinations */} {grid.destinations.map((dest) => { const cx = toSvgX(dest.position.x); const cy = toSvgY(dest.position.y); return ( {/* Target rings */} D{dest.id} ); })} {/* Axis labels */} {Array.from({ length: grid.width }).map((_, x) => ( {x} ))} {Array.from({ length: grid.height }).map((_, y) => ( {y} ))}
); }; export default Grid;