Граф: не перестраивать при ресайзе окна

- onWidth/HeightChanged теперь только clampPositions()+requestPaint, без force-симуляции и без сброса зума/панорамы/позиций
- nodes/edges меняются парой -> дебаунс 120мс (requestRelayout), добавлен onEdgesChanged
- relayout больше не трогает zoom/pan (resetView только по двойному клику), убран Math.random в пользу детерминированного сдвига, guard на нулевой размер
This commit is contained in:
2026-09-06 17:38:02 +03:00
parent 6e49c4b5f8
commit 9c8215b707
+41 -9
View File
@@ -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() }
}
}