diff --git a/qml/panels/GraphView.qml b/qml/panels/GraphView.qml index d84e41c..577b8d5 100644 --- a/qml/panels/GraphView.qml +++ b/qml/panels/GraphView.qml @@ -23,9 +23,37 @@ Canvas { return null } + // Дебаунс перестройки на случай если nodes и edges меняются парой + Timer { + id: layoutDebounce + interval: 120 + repeat: false + onTriggered: g.relayout() + } + function requestRelayout() { layoutDebounce.restart() } + + // При ресайзе только подрезаем координаты и перерисовываем — без force-симуляции + function clampPositions() { + let changed = false + for (const k in pos) { + const p = pos[k] + const nx = Math.max(30, Math.min(width - 30, p.x)) + const ny = Math.max(30, Math.min(height - 30, p.y)) + if (nx !== p.x || ny !== p.y) { + pos[k] = { x: nx, y: ny } + changed = true + } + } + // присваивание нужно только если что-то вылезло за границы, + // requestPaint вызываем в любом случае из обработчиков размера + if (changed) pos = Object.assign({}, pos) + } + function relayout() { const n = nodes.length if (n === 0) { pos = {}; requestPaint(); return } + // Размер ещё не известен (создание компонента) — откладываем + if (width < 50 || height < 50) { layoutDebounce.restart(); return } if (n > 600) { // Слишком много узлов — показываем упрощённо без симуляции чтобы не заморозить UI const cx = width / 2, cy = height / 2 @@ -35,16 +63,17 @@ Canvas { const a = (i / n) * 2 * Math.PI - Math.PI / 2 newPos[nodes[i].key] = { x: cx + rad * Math.cos(a), y: cy + rad * Math.sin(a) } } - pos = newPos; zoom = 1.0; panX = 0; panY = 0; requestPaint(); return + pos = newPos; requestPaint(); return } const cx = width / 2, cy = height / 2 const rad = Math.min(width, height) / 2 - 36 let newPos = {} for (let i = 0; i < n; ++i) { const a = (i / n) * 2 * Math.PI - Math.PI / 2 - // небольшой джиттер чтобы не накладывались ровно - const jitter = (Math.random() - 0.5) * 8 - newPos[nodes[i].key] = { x: cx + rad * Math.cos(a) + jitter, y: cy + rad * Math.sin(a) + jitter } + // детерминированный сдвиг вместо Math.random — раскладка не прыгает при каждом ребилде + const jitterX = ((i * 37) % 9) - 4 + const jitterY = ((i * 53) % 9) - 4 + newPos[nodes[i].key] = { x: cx + rad * Math.cos(a) + jitterX, y: cy + rad * Math.sin(a) + jitterY } } const iterCount = n > 200 ? 25 : (n > 100 ? 40 : 80) // простая force-симуляция для разведения узлов (репульсия + притяжение по рёбрам) @@ -89,13 +118,16 @@ Canvas { } } pos = newPos - zoom = 1.0; panX = 0; panY = 0 requestPaint() } - onNodesChanged: relayout() - onWidthChanged: relayout() - onHeightChanged: relayout() + function resetView() { zoom = 1.0; panX = 0; panY = 0; requestPaint() } + + onNodesChanged: requestRelayout() + onEdgesChanged: requestRelayout() + // Ресайз окна: только перерисовка, БЕЗ перестройки графа и БЕЗ сброса зума/панорамы + onWidthChanged: { clampPositions(); requestPaint() } + onHeightChanged: { clampPositions(); requestPaint() } // пустое состояние Text { @@ -213,6 +245,6 @@ Canvas { g.dragKey = "" g.requestPaint() } - onDoubleClicked: { g.zoom = 1.0; g.panX = 0; g.panY = 0; g.relayout() } + onDoubleClicked: { g.resetView(); g.relayout() } } }