From 9c8215b707d0ef671497e6828d95dceb0567de8f Mon Sep 17 00:00:00 2001 From: grendervill Date: Sun, 6 Sep 2026 17:38:02 +0300 Subject: [PATCH] =?UTF-8?q?=D0=93=D1=80=D0=B0=D1=84:=20=D0=BD=D0=B5=20?= =?UTF-8?q?=D0=BF=D0=B5=D1=80=D0=B5=D1=81=D1=82=D1=80=D0=B0=D0=B8=D0=B2?= =?UTF-8?q?=D0=B0=D1=82=D1=8C=20=D0=BF=D1=80=D0=B8=20=D1=80=D0=B5=D1=81?= =?UTF-8?q?=D0=B0=D0=B9=D0=B7=D0=B5=20=D0=BE=D0=BA=D0=BD=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - onWidth/HeightChanged теперь только clampPositions()+requestPaint, без force-симуляции и без сброса зума/панорамы/позиций - nodes/edges меняются парой -> дебаунс 120мс (requestRelayout), добавлен onEdgesChanged - relayout больше не трогает zoom/pan (resetView только по двойному клику), убран Math.random в пользу детерминированного сдвига, guard на нулевой размер --- qml/panels/GraphView.qml | 50 ++++++++++++++++++++++++++++++++-------- 1 file changed, 41 insertions(+), 9 deletions(-) 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() } } }