Kacemath's picture
feat: update with latest changes
47bba68
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<string, string>(), pathEdges: [] as { from: string; to: string; color: string }[] };
}
const pathColorMap = new Map<string, string>();
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 (
<div className="flex items-center justify-center h-full" style={{ backgroundColor: '#0a0a0b' }}>
<div className="text-center">
<div className="w-20 h-20 rounded-2xl bg-zinc-900 border border-zinc-800 flex items-center justify-center mx-auto mb-6">
<MapIcon className="w-10 h-10 text-zinc-700" />
</div>
<p className="text-zinc-400 text-sm font-medium">No Grid Generated</p>
<p className="text-zinc-600 text-xs mt-2">Configure and generate a grid from the sidebar</p>
</div>
</div>
);
}
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 (
<div className="w-full h-full flex items-center justify-center overflow-auto" style={{ backgroundColor: '#0a0a0b' }}>
<svg
width={width}
height={height}
viewBox={`0 0 ${width} ${height}`}
style={{ minWidth: Math.min(width, 500), minHeight: Math.min(height, 400) }}
>
<defs>
{/* Glows for different states */}
<filter id="glow-current" x="-100%" y="-100%" width="300%" height="300%">
<feGaussianBlur stdDeviation="6" result="blur"/>
<feFlood floodColor={COLORS.current} result="color"/>
<feComposite in="color" in2="blur" operator="in" result="glow"/>
<feMerge>
<feMergeNode in="glow"/>
<feMergeNode in="glow"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
<filter id="glow-path" x="-100%" y="-100%" width="300%" height="300%">
<feGaussianBlur stdDeviation="4" result="blur"/>
<feFlood floodColor={COLORS.path} result="color"/>
<feComposite in="color" in2="blur" operator="in" result="glow"/>
<feMerge>
<feMergeNode in="glow"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
<filter id="glow-frontier" x="-100%" y="-100%" width="300%" height="300%">
<feGaussianBlur stdDeviation="3" result="blur"/>
<feFlood floodColor={COLORS.frontier} result="color"/>
<feComposite in="color" in2="blur" operator="in" result="glow"/>
<feMerge>
<feMergeNode in="glow"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
<filter id="glow-tunnel" x="-100%" y="-100%" width="300%" height="300%">
<feGaussianBlur stdDeviation="4" result="blur"/>
<feFlood floodColor={COLORS.tunnel} result="color"/>
<feComposite in="color" in2="blur" operator="in" result="glow"/>
<feMerge>
<feMergeNode in="glow"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
{/* Tunnel gradient */}
<linearGradient id="tunnel-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stopColor={COLORS.tunnel} stopOpacity="0.3"/>
<stop offset="50%" stopColor={COLORS.tunnelLight} stopOpacity="0.6"/>
<stop offset="100%" stopColor={COLORS.tunnel} stopOpacity="0.3"/>
</linearGradient>
{/* Blocked pattern */}
<pattern id="blocked-hatch" patternUnits="userSpaceOnUse" width="6" height="6" patternTransform="rotate(45)">
<line x1="0" y1="0" x2="0" y2="6" stroke={COLORS.blocked} strokeWidth="1" opacity="0.3"/>
</pattern>
</defs>
{/* Background */}
<rect x={0} y={0} width={width} height={height} fill={COLORS.background} rx={8}/>
{/* Grid cells background */}
{Array.from({ length: grid.width }).map((_, x) =>
Array.from({ length: grid.height }).map((_, y) => (
<rect
key={`cell-bg-${x}-${y}`}
x={PADDING + x * CELL_SIZE + 2}
y={PADDING + (grid.height - 1 - y) * CELL_SIZE + 2}
width={CELL_SIZE - 4}
height={CELL_SIZE - 4}
fill={COLORS.gridLine}
opacity={0.3}
rx={4}
/>
))
)}
{/* 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 (
<g key={`seg-${i}`}>
{/* Blocked edge line */}
<line
x1={x1} y1={y1} x2={x2} y2={y2}
stroke={COLORS.blocked}
strokeWidth={2}
strokeDasharray="8,6"
opacity={0.25}
/>
{/* Blocked indicator */}
<g transform={`translate(${midX}, ${midY})`}>
<circle r={14} fill={COLORS.blockedBg} stroke={COLORS.blocked} strokeWidth={1.5} opacity={0.9}/>
<line x1={-5} y1={-5} x2={5} y2={5} stroke={COLORS.blocked} strokeWidth={2} strokeLinecap="round"/>
<line x1={5} y1={-5} x2={-5} y2={5} stroke={COLORS.blocked} strokeWidth={2} strokeLinecap="round"/>
</g>
</g>
);
}
return (
<g key={`seg-${i}`}>
{/* Edge line */}
<line
x1={x1} y1={y1} x2={x2} y2={y2}
stroke={style.color}
strokeWidth={style.width}
strokeLinecap="round"
opacity={style.opacity}
/>
{/* Cost badge */}
<g transform={`translate(${midX}, ${midY})`}>
<rect
x={-10} y={-10}
width={20} height={20}
fill={COLORS.background}
stroke={style.color}
strokeWidth={1}
rx={4}
opacity={0.95}
/>
<text
textAnchor="middle"
dominantBaseline="middle"
fill={style.color}
fontSize={11}
fontWeight="600"
fontFamily="ui-monospace, monospace"
>
{segment.traffic}
</text>
</g>
</g>
);
})}
{/* 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 (
<g key={`tunnel-${i}`}>
{/* Tunnel glow background */}
<path
d={`M ${x1} ${y1} Q ${midX + perpX} ${midY + perpY} ${x2} ${y2}`}
stroke={COLORS.tunnelLight}
strokeWidth={12}
fill="none"
opacity={0.15}
strokeLinecap="round"
/>
{/* Tunnel path */}
<path
d={`M ${x1} ${y1} Q ${midX + perpX} ${midY + perpY} ${x2} ${y2}`}
stroke="url(#tunnel-gradient)"
strokeWidth={4}
fill="none"
strokeLinecap="round"
/>
{/* Animated dashes */}
<path
d={`M ${x1} ${y1} Q ${midX + perpX} ${midY + perpY} ${x2} ${y2}`}
stroke={COLORS.tunnel}
strokeWidth={2}
strokeDasharray="4,8"
fill="none"
opacity={0.8}
strokeLinecap="round"
>
<animate
attributeName="stroke-dashoffset"
from="0"
to="24"
dur="1.5s"
repeatCount="indefinite"
/>
</path>
{/* Portal entrance 1 */}
<g transform={`translate(${x1}, ${y1})`} filter="url(#glow-tunnel)">
<circle r={16} fill={COLORS.background} stroke={COLORS.tunnel} strokeWidth={2}/>
<circle r={10} fill={COLORS.tunnel} opacity={0.3}/>
<circle r={5} fill={COLORS.tunnelLight}/>
</g>
{/* Portal entrance 2 */}
<g transform={`translate(${x2}, ${y2})`} filter="url(#glow-tunnel)">
<circle r={16} fill={COLORS.background} stroke={COLORS.tunnel} strokeWidth={2}/>
<circle r={10} fill={COLORS.tunnel} opacity={0.3}/>
<circle r={5} fill={COLORS.tunnelLight}/>
</g>
{/* Cost label */}
<g transform={`translate(${midX + perpX * 0.6}, ${midY + perpY * 0.6})`}>
<rect
x={-18} y={-11}
width={36} height={22}
fill={COLORS.background}
stroke={COLORS.tunnel}
strokeWidth={1.5}
rx={6}
/>
<text
textAnchor="middle"
dominantBaseline="middle"
fill={COLORS.tunnelLight}
fontSize={11}
fontWeight="600"
fontFamily="ui-monospace, monospace"
>
T:{tunnel.cost}
</text>
</g>
</g>
);
})}
{/* 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 (
<line
key={`plan-edge-${i}`}
x1={x1} y1={y1} x2={x2} y2={y2}
stroke={edge.color}
strokeWidth={5}
strokeLinecap="round"
opacity={0.8}
/>
);
})}
{/* 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 (
<g key={`node-${x}-${y}`}>
<circle
cx={cx}
cy={cy}
r={radius}
fill={fill}
stroke={stroke}
strokeWidth={strokeWidth}
filter={filter}
opacity={state === 'default' ? 0.4 : 1}
style={{ transition: 'all 0.15s ease-out' }}
/>
{state === 'current' && (
<circle
cx={cx}
cy={cy}
r={radius + 6}
fill="none"
stroke={COLORS.current}
strokeWidth={1.5}
opacity={0.4}
>
<animate
attributeName="r"
from={radius + 4}
to={radius + 12}
dur="1s"
repeatCount="indefinite"
/>
<animate
attributeName="opacity"
from="0.5"
to="0"
dur="1s"
repeatCount="indefinite"
/>
</circle>
)}
</g>
);
})
)}
{/* Stores */}
{grid.stores.map((store) => {
const cx = toSvgX(store.position.x);
const cy = toSvgY(store.position.y);
return (
<g key={`store-${store.id}`}>
<rect
x={cx - 18}
y={cy - 18}
width={36}
height={36}
fill={COLORS.store}
stroke={COLORS.storeLight}
strokeWidth={2}
rx={8}
/>
<rect
x={cx - 12}
y={cy - 6}
width={24}
height={4}
fill={COLORS.storeLight}
rx={2}
/>
<text
x={cx}
y={cy + 8}
textAnchor="middle"
dominantBaseline="middle"
fill={COLORS.text}
fontSize={11}
fontWeight="700"
fontFamily="ui-monospace, monospace"
>
S{store.id}
</text>
</g>
);
})}
{/* Destinations */}
{grid.destinations.map((dest) => {
const cx = toSvgX(dest.position.x);
const cy = toSvgY(dest.position.y);
return (
<g key={`dest-${dest.id}`}>
<circle
cx={cx}
cy={cy}
r={18}
fill={COLORS.destination}
stroke={COLORS.destinationLight}
strokeWidth={2}
/>
{/* Target rings */}
<circle cx={cx} cy={cy} r={12} fill="none" stroke={COLORS.destinationLight} strokeWidth={1.5} opacity={0.5}/>
<circle cx={cx} cy={cy} r={6} fill={COLORS.destinationLight} opacity={0.8}/>
<text
x={cx}
y={cy + 28}
textAnchor="middle"
fill={COLORS.textMuted}
fontSize={10}
fontWeight="600"
fontFamily="ui-monospace, monospace"
>
D{dest.id}
</text>
</g>
);
})}
{/* Axis labels */}
{Array.from({ length: grid.width }).map((_, x) => (
<text
key={`x-${x}`}
x={toSvgX(x)}
y={height - 25}
textAnchor="middle"
fontSize={11}
fill={COLORS.textDim}
fontFamily="ui-monospace, monospace"
fontWeight="500"
>
{x}
</text>
))}
{Array.from({ length: grid.height }).map((_, y) => (
<text
key={`y-${y}`}
x={25}
y={toSvgY(y) + 4}
textAnchor="middle"
fontSize={11}
fill={COLORS.textDim}
fontFamily="ui-monospace, monospace"
fontWeight="500"
>
{y}
</text>
))}
</svg>
</div>
);
};
export default Grid;