diff --git a/frontend/src/components/RegionMap.tsx b/frontend/src/components/RegionMap.tsx index e19ac43..81ecc1e 100644 --- a/frontend/src/components/RegionMap.tsx +++ b/frontend/src/components/RegionMap.tsx @@ -22,6 +22,9 @@ const INDICATED_RING_RADIUS = 20; const INDICATED_RING_COLOR = '#f59e0b'; const INDICATED_RING_ANIM_VALUES = '18;22;18'; const INDICATED_RING_ANIM_DUR = '1.2s'; +const SHIFT_SELECT_STROKE_COLOR = '#60a5fa'; +const SHIFT_SELECT_FILL_COLOR = 'rgba(96,165,250,0.12)'; +const SHIFT_SELECT_STROKE_WIDTH = 2; interface RegionMapProps { regionName: string; @@ -117,6 +120,11 @@ export const RegionMap = ({ regionName, focusSystem, isCompact = false, isWormho const selectTimerRef = useRef(null); const downClientPointRef = useRef<{ x: number; y: number } | null>(null); + // New: shift-drag circle selection state (VIA mode) + const [shiftSelecting, setShiftSelecting] = useState(false); + const [shiftCenter, setShiftCenter] = useState(null); + const [shiftRadius, setShiftRadius] = useState(0); + // When focusSystem changes, set an expiry 20s in the future useEffect(() => { if (focusSystem) { @@ -488,6 +496,32 @@ export const RegionMap = ({ regionName, focusSystem, isCompact = false, isWormho return nearestName; }; + // Commit shift selection: toggle all systems within radius + const commitShiftSelection = useCallback(() => { + if (!shiftCenter || shiftRadius <= 0) return; + const within: string[] = []; + Object.keys(positions).forEach(name => { + const pos = positions[name]; + if (!pos) return; + const dx = pos.x - shiftCenter.x; + const dy = pos.y - shiftCenter.y; + const dist = Math.sqrt(dx * dx + dy * dy); + if (dist <= shiftRadius) within.push(name); + }); + if (within.length === 0) return; + setViaQueue(prev => { + const prevSet = new Set(prev); + const toToggle = new Set(within); + // remove toggled ones that were present + const kept = prev.filter(n => !toToggle.has(n)); + // add new ones (those within but not previously present), preserve within order + const additions = within.filter(n => !prevSet.has(n)); + const next = kept.concat(additions); + toast({ title: 'VIA toggled', description: `${within.length} systems` }); + return next; + }); + }, [positions, shiftCenter, shiftRadius]); + const clearSelectTimer = () => { if (selectTimerRef.current !== null) { window.clearTimeout(selectTimerRef.current); @@ -500,6 +534,22 @@ export const RegionMap = ({ regionName, focusSystem, isCompact = false, isWormho const handleMouseDown = useCallback((e: React.MouseEvent) => { if (!svgRef.current) return; + // SHIFT + VIA mode: start circle selection + if (viaMode && e.shiftKey) { + e.preventDefault(); + e.stopPropagation(); + const svgPt = clientToSvg(e.clientX, e.clientY); + setShiftSelecting(true); + setShiftCenter(svgPt); + setShiftRadius(0); + // cancel any hold-to-select/pan intents + setIsSelecting(false); + setIsPanning(false); + clearSelectTimer(); + downClientPointRef.current = { x: e.clientX, y: e.clientY }; + return; + } + // record down point (client) and seed pan origin const rect = svgRef.current.getBoundingClientRect(); setLastPanPoint({ x: e.clientX - rect.left, y: e.clientY - rect.top }); @@ -516,7 +566,7 @@ export const RegionMap = ({ regionName, focusSystem, isCompact = false, isWormho selectTimerRef.current = window.setTimeout(() => { setIsSelecting(true); }, SELECT_HOLD_MS); - }, [positions, systems]); + }, [positions, systems, viaMode]); const handleMouseMove = useCallback((e: React.MouseEvent) => { // if dragging node, delegate @@ -524,6 +574,15 @@ export const RegionMap = ({ regionName, focusSystem, isCompact = false, isWormho if (!svgRef.current) return; + // Shift selection radius update + if (shiftSelecting && shiftCenter) { + const svgPt = clientToSvg(e.clientX, e.clientY); + const dx = svgPt.x - shiftCenter.x; + const dy = svgPt.y - shiftCenter.y; + setShiftRadius(Math.sqrt(dx * dx + dy * dy)); + return; + } + const rect = svgRef.current.getBoundingClientRect(); if (isPanning) { @@ -560,12 +619,26 @@ export const RegionMap = ({ regionName, focusSystem, isCompact = false, isWormho const near = findNearestSystem(svgPt.x, svgPt.y); setIndicatedSystem(near); } - }, [draggingNode, isPanning, lastPanPoint, viewBox.width, viewBox.height, isSelecting, positions, systems]); + }, [draggingNode, isPanning, lastPanPoint, viewBox.width, viewBox.height, isSelecting, positions, systems, shiftSelecting, shiftCenter]); const handleMouseUp = useCallback((e?: React.MouseEvent) => { // if dragging node, delegate if (draggingNode) { if (e) handleSvgMouseUp(e); return; } + // Commit shift selection if active + if (shiftSelecting) { + commitShiftSelection(); + setShiftSelecting(false); + setShiftCenter(null); + setShiftRadius(0); + clearSelectTimer(); + setIsPanning(false); + setIsSelecting(false); + setIndicatedSystem(null); + downClientPointRef.current = null; + return; + } + clearSelectTimer(); if (isPanning) { @@ -588,7 +661,7 @@ export const RegionMap = ({ regionName, focusSystem, isCompact = false, isWormho setIsSelecting(false); setIndicatedSystem(null); downClientPointRef.current = null; - }, [draggingNode, isPanning, indicatedSystem, positions, systems]); + }, [draggingNode, isPanning, indicatedSystem, positions, systems, shiftSelecting, commitShiftSelection]); const handleWheel = useCallback((e: React.WheelEvent) => { e.preventDefault(); @@ -758,15 +831,22 @@ export const RegionMap = ({ regionName, focusSystem, isCompact = false, isWormho useEffect(() => { const onWindowMouseUp = () => { + // if shift selection ongoing, commit on global mouseup as well + if (shiftSelecting) { + commitShiftSelection(); + } clearSelectTimer(); setIsPanning(false); setIsSelecting(false); setIndicatedSystem(null); + setShiftSelecting(false); + setShiftCenter(null); + setShiftRadius(0); downClientPointRef.current = null; }; window.addEventListener('mouseup', onWindowMouseUp); return () => window.removeEventListener('mouseup', onWindowMouseUp); - }, []); + }, [shiftSelecting, commitShiftSelection]); if (isLoading) { return ( @@ -801,7 +881,7 @@ export const RegionMap = ({ regionName, focusSystem, isCompact = false, isWormho width="100%" height="100%" viewBox={`${viewBox.x} ${viewBox.y} ${viewBox.width} ${viewBox.height}`} - className={`${isSelecting ? 'cursor-crosshair' : (isPanning ? 'cursor-grabbing' : 'cursor-grab')}`} + className={`${(isSelecting || shiftSelecting) ? 'cursor-crosshair' : (isPanning ? 'cursor-grabbing' : 'cursor-grab')}`} onMouseDown={handleMouseDown} onMouseMove={handleMouseMove} onMouseUp={(e) => handleMouseUp(e)} @@ -842,6 +922,20 @@ export const RegionMap = ({ regionName, focusSystem, isCompact = false, isWormho /> )} + {/* Shift selection circle (VIA mode) */} + {shiftSelecting && shiftCenter && ( + + + + )} + {/* Render existing systems */} {Array.from(systems.entries()).map(([key, system]) => (