diff --git a/frontend/src/GraphView.jsx b/frontend/src/GraphView.jsx index 2a8700f..d10189f 100644 --- a/frontend/src/GraphView.jsx +++ b/frontend/src/GraphView.jsx @@ -17,6 +17,8 @@ function GraphView({ servers, connections }) { const zoomRef = useRef(null); const [isFullscreen, setIsFullscreen] = useState(false); const [transformState, setTransformState] = useState({ k: 1, x: 0, y: 0 }); + const [isPanningMode, setIsPanningMode] = useState(false); + const panModeRef = useRef(false); const ZOOM_MIN = 0.2; const ZOOM_MAX = 4; @@ -90,8 +92,9 @@ function GraphView({ servers, connections }) { .extent([[0, 0], [width, height]]) .translateExtent([[-10000, -10000], [10000, 10000]]) .on('zoom', (event) => { - g.attr('transform', event.transform); - setTransformState({ k: event.transform.k, x: event.transform.x, y: event.transform.y }); + const t = event.transform; + g.attr('transform', `translate(${t.x},${t.y}) scale(${t.k})`); + setTransformState({ k: t.k, x: t.x, y: t.y }); }); zoomRef.current = zoom; svg @@ -100,6 +103,30 @@ function GraphView({ servers, connections }) { dbg('zoom initialized', { width, height }); }, []); + // Горячая клавиша: пробел включает режим панорамирования (чтобы тянуть в любом месте, даже на узлах) + useEffect(() => { + const down = (e) => { + if (e.code === 'Space') { + e.preventDefault(); + setIsPanningMode(true); + panModeRef.current = true; + } + }; + const up = (e) => { + if (e.code === 'Space') { + e.preventDefault(); + setIsPanningMode(false); + panModeRef.current = false; + } + }; + window.addEventListener('keydown', down, { passive: false }); + window.addEventListener('keyup', up, { passive: false }); + return () => { + window.removeEventListener('keydown', down); + window.removeEventListener('keyup', up); + }; + }, []); + // Контролы масштабирования const zoomBy = (factor) => { if (!zoomRef.current || !svgRef.current) return; @@ -222,6 +249,8 @@ function GraphView({ servers, connections }) { // Обработчик начала перетаскивания const handleMouseDown = (e, serverIp) => { + // В режиме панорамирования — не начинаем перетаскивание узла, даём d3.zoom обработать drag + if (panModeRef.current) return; const target = gRef.current || svgRef.current; const pt = (gRef.current?.ownerSVGElement || svgRef.current).createSVGPoint(); pt.x = e.clientX; @@ -311,11 +340,10 @@ function GraphView({ servers, connections }) { ref={svgRef} width="100%" height="100%" - viewBox="0 0 900 600" onMouseMove={handleMouseMove} onMouseUp={handleMouseUp} onMouseLeave={handleMouseUp} - style={{ cursor: draggedNode ? 'grabbing' : 'default' }} + style={{ cursor: (draggedNode || isPanningMode) ? 'grabbing' : 'grab' }} > {/* Лёгкая сетка фона */} @@ -355,9 +383,9 @@ function GraphView({ servers, connections }) { - {/* Подложка сетки */} - + {/* Подложка сетки (внутри слоя зума) */} + {/* Связи */} {connections.map((connection, index) => { const fromServer = servers.find(s => s.ip === connection.from); @@ -397,7 +425,6 @@ function GraphView({ servers, connections }) { markerEnd={`url(#arrow-${connection.tunnelType})`} filter={isHovered ? 'url(#glow)' : undefined} opacity={isHovered ? 1 : 0.9} - style={{ vectorEffect: 'non-scaling-stroke' }} /> {/* Подпись связи: ярлык с подложкой и halo */}