From dad6d79740f37992139caeb50254f9f8465c0bf6 Mon Sep 17 00:00:00 2001 From: PhatPhuckDave Date: Mon, 11 Aug 2025 19:45:30 +0200 Subject: [PATCH] feat(RegionMap): introduce interaction state machine for improved event handling --- frontend/src/components/RegionMap.tsx | 52 ++++++++++++++++++++++++--- 1 file changed, 48 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/RegionMap.tsx b/frontend/src/components/RegionMap.tsx index c31ce70..b95b5ad 100644 --- a/frontend/src/components/RegionMap.tsx +++ b/frontend/src/components/RegionMap.tsx @@ -126,6 +126,10 @@ export const RegionMap = ({ regionName, focusSystem, isCompact = false, isWormho const [shiftCenter, setShiftCenter] = useState(null); const [shiftRadius, setShiftRadius] = useState(0); + // Interaction state machine (lightweight) + type InteractionMode = 'idle' | 'holding' | 'panning' | 'selecting' | 'shiftSelecting'; + const [mode, setMode] = useState('idle'); + // When focusSystem changes, set an expiry 20s in the future useEffect(() => { if (focusSystem) { @@ -535,6 +539,17 @@ export const RegionMap = ({ regionName, focusSystem, isCompact = false, isWormho const handleMouseDown = useCallback((e: React.MouseEvent) => { if (!svgRef.current) return; + // If context menu is open, left-click closes it and no selection should happen + if (contextMenu) { + if (e.button === 0) setContextMenu(null); + clearSelectTimer(); + setIsSelecting(false); + setIsPanning(false); + setShiftSelecting(false); + setMode('idle'); + return; + } + mouseButtonRef.current = e.button; // SHIFT + VIA mode: start circle selection (left button only) @@ -545,6 +560,7 @@ export const RegionMap = ({ regionName, focusSystem, isCompact = false, isWormho setShiftSelecting(true); setShiftCenter(svgPt); setShiftRadius(0); + setMode('shiftSelecting'); // cancel any hold-to-select/pan intents setIsSelecting(false); setIsPanning(false); @@ -557,6 +573,7 @@ export const RegionMap = ({ regionName, focusSystem, isCompact = false, isWormho if (e.button !== 0) { clearSelectTimer(); setIsSelecting(false); + setMode('idle'); return; } @@ -572,11 +589,13 @@ export const RegionMap = ({ regionName, focusSystem, isCompact = false, isWormho // start delayed select mode timer setIsSelecting(false); + setMode('holding'); clearSelectTimer(); selectTimerRef.current = window.setTimeout(() => { setIsSelecting(true); + setMode('selecting'); }, SELECT_HOLD_MS); - }, [positions, systems, viaMode]); + }, [positions, systems, viaMode, contextMenu]); const handleMouseMove = useCallback((e: React.MouseEvent) => { // if dragging node, delegate @@ -590,6 +609,7 @@ export const RegionMap = ({ regionName, focusSystem, isCompact = false, isWormho const dx = svgPt.x - shiftCenter.x; const dy = svgPt.y - shiftCenter.y; setShiftRadius(Math.sqrt(dx * dx + dy * dy)); + setMode('shiftSelecting'); return; } @@ -601,10 +621,11 @@ export const RegionMap = ({ regionName, focusSystem, isCompact = false, isWormho const deltaY = (lastPanPoint.y - currentPoint.y) * (viewBox.height / rect.height); setViewBox(prev => ({ ...prev, x: prev.x + deltaX, y: prev.y + deltaY })); setLastPanPoint(currentPoint); + setMode('panning'); return; } - // determine if we should start panning + // determine if we should start panning (from holding) const down = downClientPointRef.current; if (down && !isSelecting) { const dx = e.clientX - down.x; @@ -616,6 +637,7 @@ export const RegionMap = ({ regionName, focusSystem, isCompact = false, isWormho setIsSelecting(false); setIndicatedSystem(null); setIsPanning(true); + setMode('panning'); // seed pan origin with current const currentPoint = { x: e.clientX - rect.left, y: e.clientY - rect.top }; setLastPanPoint(currentPoint); @@ -628,6 +650,7 @@ export const RegionMap = ({ regionName, focusSystem, isCompact = false, isWormho const svgPt = clientToSvg(e.clientX, e.clientY); const near = findNearestSystem(svgPt.x, svgPt.y); setIndicatedSystem(near); + setMode('selecting'); } }, [draggingNode, isPanning, lastPanPoint, viewBox.width, viewBox.height, isSelecting, positions, systems, shiftSelecting, shiftCenter]); @@ -635,6 +658,22 @@ export const RegionMap = ({ regionName, focusSystem, isCompact = false, isWormho // if dragging node, delegate if (draggingNode) { if (e) handleSvgMouseUp(e); return; } + // If context menu open, left click should just close it; do not select + if (contextMenu && mouseButtonRef.current === 0) { + setContextMenu(null); + clearSelectTimer(); + setIsPanning(false); + setIsSelecting(false); + setIndicatedSystem(null); + setShiftSelecting(false); + setShiftCenter(null); + setShiftRadius(0); + downClientPointRef.current = null; + mouseButtonRef.current = null; + setMode('idle'); + return; + } + // Commit shift selection if active (only if left button initiated) if (shiftSelecting) { if (mouseButtonRef.current === 0) { @@ -649,6 +688,7 @@ export const RegionMap = ({ regionName, focusSystem, isCompact = false, isWormho setIndicatedSystem(null); downClientPointRef.current = null; mouseButtonRef.current = null; + setMode('idle'); return; } @@ -660,6 +700,7 @@ export const RegionMap = ({ regionName, focusSystem, isCompact = false, isWormho setIndicatedSystem(null); downClientPointRef.current = null; mouseButtonRef.current = null; + setMode('idle'); return; } @@ -668,6 +709,7 @@ export const RegionMap = ({ regionName, focusSystem, isCompact = false, isWormho if (isPanning) { setIsPanning(false); mouseButtonRef.current = null; + setMode('idle'); return; } @@ -687,7 +729,8 @@ export const RegionMap = ({ regionName, focusSystem, isCompact = false, isWormho setIndicatedSystem(null); downClientPointRef.current = null; mouseButtonRef.current = null; - }, [draggingNode, isPanning, indicatedSystem, positions, systems, shiftSelecting, commitShiftSelection]); + setMode('idle'); + }, [draggingNode, isPanning, indicatedSystem, positions, systems, shiftSelecting, commitShiftSelection, contextMenu]); const handleWheel = useCallback((e: React.WheelEvent) => { e.preventDefault(); @@ -870,6 +913,7 @@ export const RegionMap = ({ regionName, focusSystem, isCompact = false, isWormho setShiftRadius(0); downClientPointRef.current = null; mouseButtonRef.current = null; + setMode('idle'); }; window.addEventListener('mouseup', onWindowMouseUp); return () => window.removeEventListener('mouseup', onWindowMouseUp); @@ -908,7 +952,7 @@ export const RegionMap = ({ regionName, focusSystem, isCompact = false, isWormho width="100%" height="100%" viewBox={`${viewBox.x} ${viewBox.y} ${viewBox.width} ${viewBox.height}`} - className={`${(isSelecting || shiftSelecting) ? 'cursor-crosshair' : (isPanning ? 'cursor-grabbing' : 'cursor-grab')}`} + className={`${(mode === 'selecting' || mode === 'shiftSelecting') ? 'cursor-crosshair' : (mode === 'panning' ? 'cursor-grabbing' : 'cursor-grab')}`} onMouseDown={handleMouseDown} onMouseMove={handleMouseMove} onMouseUp={(e) => handleMouseUp(e)}