UNPKG

ngraph.svg

Version:

SVG-based graph visualization library with adaptive rendering

1,413 lines (1,412 loc) 123 kB
var ut = Object.defineProperty; var mt = (a, t, e) => t in a ? ut(a, t, { enumerable: !0, configurable: !0, writable: !0, value: e }) : a[t] = e; var st = (a, t, e) => mt(a, typeof t != "symbol" ? t + "" : t, e); import pt from "ngraph.graph"; class gt { constructor() { this.viewBox = { left: -100, top: -100, right: 100, bottom: 100 }, this.transform = { scale: 1, x: 0, y: 0 }, this.width = 0, this.height = 0, this.pixelRatio = typeof window < "u" ? window.devicePixelRatio : 1, this._visibleBounds = { left: 0, top: 0, right: 0, bottom: 0 }, this._visibleBoundsDirty = !0, this._reusableScenePoint = { x: 0, y: 0 }, this._reusableScreenPoint = { x: 0, y: 0 }; } /** * Update the viewport dimensions */ setSize(t, e) { this.width = t, this.height = e, this._visibleBoundsDirty = !0; } /** * Update the viewBox bounds */ setViewBox(t, e, s, i) { this.viewBox.left = t, this.viewBox.top = e, this.viewBox.right = s, this.viewBox.bottom = i; } /** * Update the transform state */ setTransform(t, e, s) { this.transform.scale = t, this.transform.x = e, this.transform.y = s, this._visibleBoundsDirty = !0; } /** * Calculate how many pixels a node of given size occupies on screen. * This is the core metric for adaptive rendering decisions. */ getNodeScreenSize(t) { return t * this.transform.scale; } /** * Convert screen coordinates to scene coordinates */ screenToScene(t, e) { const s = this._reusableScenePoint; return s.x = (t - this.transform.x) / this.transform.scale, s.y = (e - this.transform.y) / this.transform.scale, s; } /** * Convert scene coordinates to screen coordinates */ sceneToScreen(t, e) { const s = this._reusableScreenPoint; return s.x = t * this.transform.scale + this.transform.x, s.y = e * this.transform.scale + this.transform.y, s; } /** * Get the visible bounds in scene coordinates */ /** * Returns the cached visible bounds object. Callers must not modify it. */ getVisibleBounds() { return this._updateVisibleBounds(), this._visibleBounds; } _updateVisibleBounds() { if (this._visibleBoundsDirty) { this._visibleBoundsDirty = !1; const t = this.transform.scale, e = this.transform.x, s = this.transform.y; this._visibleBounds.left = (0 - e) / t, this._visibleBounds.top = (0 - s) / t, this._visibleBounds.right = (this.width - e) / t, this._visibleBounds.bottom = (this.height - s) / t; } } /** * Check if a point with given radius is visible in the viewport */ isVisible(t, e, s = 0) { this._updateVisibleBounds(); const i = this._visibleBounds; return t + s >= i.left && t - s <= i.right && e + s >= i.top && e - s <= i.bottom; } /** * Get the SVG transform string for the current state */ getTransformString() { return `translate(${this.transform.x}, ${this.transform.y}) scale(${this.transform.scale})`; } } function yt(a, t) { const { onPanStart: e, onPanMove: s, onPanEnd: i, onZoom: n } = t; let o = !1, h = 0, l = 0; const c = []; function r(d, m, _, y) { d.addEventListener(m, _, y), c.push({ target: d, event: m, handler: _, options: y }); } function u(d) { d.button === 0 && (o = !0, h = d.clientX, l = d.clientY, e && e({ x: h, y: l, event: d }), d.preventDefault()); } function p(d) { if (!o) return; const m = d.clientX - h, _ = d.clientY - l; h = d.clientX, l = d.clientY, s && s({ dx: m, dy: _, x: h, y: l, event: d }); } function x(d) { o && (o = !1, i && i({ x: d.clientX, y: d.clientY, event: d })); } function g(d) { d.preventDefault(); let m = d.deltaY; d.deltaMode === 1 ? m *= 40 : d.deltaMode === 2 && (m *= 800); const _ = 1 - m * 3e-3, y = a.getBoundingClientRect(), v = d.clientX - y.left, b = d.clientY - y.top; n && n({ factor: _, x: v, y: b, event: d }); } function f(d) { const m = a.getBoundingClientRect(), _ = d.clientX - m.left, y = d.clientY - m.top; n && n({ factor: 2, x: _, y, event: d }); } return r(a, "mousedown", u), r(window, "mousemove", p), r(window, "mouseup", x), r(a, "wheel", g, { passive: !1 }), r(a, "dblclick", f), { dispose() { for (const { target: d, event: m, handler: _, options: y } of c) d.removeEventListener(m, _, y); c.length = 0; } }; } class xt { constructor(t) { this.id = t.identifier, this.x = t.clientX, this.y = t.clientY, this.lastX = this.x, this.lastY = this.y; } move(t) { this.lastX = this.x, this.lastY = this.y, this.x = t.clientX, this.y = t.clientY; } } function vt(a, t) { const { onPanStart: e, onPanMove: s, onPanEnd: i, onZoom: n } = t; let o = /* @__PURE__ */ new Map(), h = !1, l = 0, c = 0, r = null; a.addEventListener("touchstart", u, { passive: !1 }); function u(d) { h || (g(), h = !0); for (let m = 0; m < d.touches.length; m++) { const _ = d.touches[m]; o.has(_.identifier) || o.set(_.identifier, new xt(_)); } if (d.touches.length === 1) { const m = d.touches[0]; e && e({ x: m.clientX, y: m.clientY, event: d }); } d.preventDefault(), d.stopPropagation(); } function p(d) { const m = Date.now(); let _ = 0, y = 0, v = 0, b = 0, C = null, S = null; for (let Y = 0; Y < d.touches.length; Y++) { const X = d.touches[Y], P = o.get(X.identifier); P && (P.move(X), v += P.x, b += P.y, _ += P.x - P.lastX, y += P.y - P.lastY, C ? S || (S = P) : C = P); } const w = d.touches.length; if (w === 0) return; if (_ /= w, y /= w, v /= w, b /= w, C && S) { c = m; const Y = Math.hypot(S.x - C.x, S.y - C.y), X = Math.hypot(S.lastX - C.lastX, S.lastY - C.lastY); if (X > 0) { const P = Y / X, G = a.getBoundingClientRect(), H = v - G.left, R = b - G.top; n && n({ factor: P, x: H, y: R, event: d }); } d.preventDefault(), d.stopPropagation(); } const L = m - c; d.touches.length === 1 && L > 100 && (_ !== 0 || y !== 0) && s && s({ dx: _, dy: y, x: v, y: b, event: d }); } function x(d) { const m = Date.now(), _ = m - l; l = m; for (let y = 0; y < d.changedTouches.length; y++) { const v = d.changedTouches[y]; o.delete(v.identifier); } if (d.touches.length === 0) { if (o.clear(), h = !1, f(), d.changedTouches.length === 1 && m - c > 350) { const y = d.changedTouches[0]; if (_ < 350 && r && Math.hypot( y.clientX - r.clientX, y.clientY - r.clientY ) < 30) { const b = a.getBoundingClientRect(), C = y.clientX - b.left, S = y.clientY - b.top; n && n({ factor: 2, x: C, y: S, event: d }); } r = { clientX: y.clientX, clientY: y.clientY }; } i && i({ x: 0, y: 0, event: d }); } } function g() { document.addEventListener("touchmove", p, { passive: !1 }), document.addEventListener("touchend", x, { passive: !1 }), document.addEventListener("touchcancel", x, { passive: !1 }); } function f() { document.removeEventListener("touchmove", p, { passive: !1 }), document.removeEventListener("touchend", x, { passive: !1 }), document.removeEventListener("touchcancel", x, { passive: !1 }); } return { dispose() { a.removeEventListener("touchstart", u, { passive: !1 }), f(), o.clear(); } }; } function bt(a = {}) { const { friction: t = 0.92, minVelocity: e = 0.5, onUpdate: s = () => { } } = a; let i = 0, n = 0, o = 0, h = null, l = !1; const c = [], r = 5; function u(g, f) { const d = performance.now(); for (c.push({ dx: g, dy: f, time: d }); c.length > r; ) c.shift(); } function p() { if (c.length < 2) return { vx: 0, vy: 0 }; let g = 0, f = 0, d = 0; for (let m = 1; m < c.length; m++) { const _ = c[m].time - c[m - 1].time; _ > 0 && (g += c[m].dx, f += c[m].dy, d += _); } return d === 0 ? { vx: 0, vy: 0 } : { vx: g / d * 16, // Scale to approx per-frame velocity vy: f / d * 16 }; } function x() { h = null; const g = performance.now(), f = Math.min(g - o, 32); o = g; const d = f / 16, m = Math.pow(t, d); if (i *= m, n *= m, Math.sqrt(i * i + n * n) < e) { i = 0, n = 0; return; } s(i * d, n * d), h = requestAnimationFrame(x); } return { /** * Start tracking movements */ startTracking() { l = !0, c.length = 0, this.stop(); }, /** * Record a movement during pan */ track(g, f) { l && u(g, f); }, /** * Stop tracking and start kinetic animation */ release() { l = !1; const { vx: g, vy: f } = p(); i = g, n = f, c.length = 0, Math.sqrt(i * i + n * n) >= e && (o = performance.now(), h = requestAnimationFrame(x)); }, /** * Stop any ongoing animation */ stop() { h !== null && (cancelAnimationFrame(h), h = null), i = 0, n = 0; }, /** * Check if animation is running */ isAnimating() { return h !== null; }, /** * Dispose of the animation */ dispose() { this.stop(), c.length = 0; } }; } function Mt(a, t, e = {}) { const { onTransform: s = () => { } } = e; let i = e.minZoom || 0.1, n = e.maxZoom || 20, o = null, h = null, l = null, c = null, r = null; function u(f, d) { const { x: m, y: _, scale: y } = t.transform; t.setTransform(y, m + f, _ + d), s(); } function p(f, d, m) { const { x: _, y, scale: v } = t.transform; let b = v * f; if (b = Math.max(i, Math.min(n, b)), b === v) return; const C = b / v, S = d - (d - _) * C, w = m - (m - y) * C; t.setTransform(b, S, w), s(); } l = bt({ onUpdate(f, d) { u(f, d); } }); const x = { onPanStart() { l.startTracking(); }, onPanMove({ dx: f, dy: d }) { u(f, d), l.track(f, d); }, onPanEnd() { l.release(); }, onZoom({ factor: f, x: d, y: m }) { p(f, d, m); } }; o = yt(a, x), h = vt(a, x); function g() { c !== null && (cancelAnimationFrame(c), c = null), r && (r(), r = null); } return { /** * Set the zoom limits */ setZoomLimits(f, d) { i = f, n = d; }, /** * Get the current zoom level */ getZoom() { return t.transform.scale; }, /** * Set the zoom level */ setZoom(f, d = !1) { if (d) { const y = (t.width / 2 - t.transform.x) / t.transform.scale, v = (t.height / 2 - t.transform.y) / t.transform.scale; return this.flyTo(y, v, f); } const { width: m, height: _ } = t; p(f / t.transform.scale, m / 2, _ / 2); }, /** * Pan by a delta */ pan(f, d) { u(f, d); }, /** * Animate camera to a specific position */ flyTo(f, d, m, _ = 300) { return new Promise((y) => { l.stop(), g(), r = y; const v = t.transform.x, b = t.transform.y, C = t.transform.scale, S = Math.max(i, Math.min(n, m)), { width: w, height: L } = t, D = w / 2 - f * S, Y = L / 2 - d * S, X = performance.now(); function P() { const H = performance.now() - X, R = Math.min(H / _, 1), U = 1 - Math.pow(1 - R, 3), dt = v + (D - v) * U, ft = b + (Y - b) * U, _t = C + (S - C) * U; t.setTransform(_t, dt, ft), s(), R < 1 ? c = requestAnimationFrame(P) : (c = null, r = null, y()); } c = requestAnimationFrame(P); }); }, /** * Stop any ongoing animation */ stop() { l.stop(), g(); }, /** * Dispose of the pan/zoom controller */ dispose() { g(), o && (o.dispose(), o = null), h && (h.dispose(), h = null), l && (l.dispose(), l = null); } }; } const nt = "http://www.w3.org/2000/svg"; function Kt(a, t = {}) { const { viewBox: e = { left: -100, top: -100, right: 100, bottom: 100 }, panZoom: s = {} } = t, i = document.createElementNS(nt, "svg"); i.style.position = "relative", i.style.width = "100%", i.style.height = "100%", i.style.display = "block", i.style.overflow = "hidden", a.appendChild(i); const n = document.createElementNS(nt, "g"); n.setAttribute("class", "scene-transform"), i.appendChild(n); const o = new gt(); o.setViewBox(e.left, e.top, e.right, e.bottom); const h = [], l = { render: [], transform: [], resize: [] }; let c = null, r = !0; function u() { const _ = a.getBoundingClientRect(); o.setSize(_.width, _.height); const y = e.right - e.left, v = e.bottom - e.top, b = _.width / y, C = _.height / v, S = Math.min(b, C), w = _.width / 2, L = _.height / 2, D = (e.left + e.right) / 2, Y = (e.top + e.bottom) / 2; o.setTransform( S, w - D * S, L - Y * S ), p("resize", { width: _.width, height: _.height }), g(); } function p(_, y) { const v = l[_]; if (v) for (let b = 0; b < v.length; b++) v[b](y); } function x() { if (c = null, !!r) { r = !1, n.setAttribute("transform", o.getTransformString()); for (let _ = 0; _ < h.length; _++) h[_].render(o); p("render", o); } } function g() { r = !0, c === null && (c = requestAnimationFrame(x)); } let f = null; s.enabled !== !1 && (f = Mt(i, o, { minZoom: s.minZoom || 0.1, maxZoom: s.maxZoom || 20, onTransform: () => { p("transform", o.transform), g(); } })); const d = new ResizeObserver(() => { u(); }); d.observe(a), u(); const m = { /** * The SVG root element */ svg: i, /** * The transform group containing all scene content */ root: n, /** * The current draw context */ drawContext: o, /** * Add a collection to the scene */ addCollection(_) { h.push(_); const y = _.getRoot(); return y && n.appendChild(y), g(), _; }, /** * Remove a collection from the scene */ removeCollection(_) { const y = h.indexOf(_); if (y !== -1) { h.splice(y, 1); const v = _.getRoot(); v && v.parentNode && v.parentNode.removeChild(v), g(); } }, /** * Request a render on the next animation frame */ requestRender: g, /** * Add an event listener */ on(_, y) { return l[_] && l[_].push(y), m; }, /** * Remove an event listener */ off(_, y) { if (l[_]) { const v = l[_].indexOf(y); v !== -1 && l[_].splice(v, 1); } return m; }, /** * Get the pan/zoom controller */ getPanZoom() { return f; }, /** * Animate camera to a specific position */ flyTo(_, y, v, b = 300) { return f ? f.flyTo(_, y, v, b) : Promise.resolve(); }, /** * Fit the view to show all content */ fitToView(_, y = 20) { const { width: v, height: b } = o, C = _.right - _.left, S = _.bottom - _.top, w = (v - y * 2) / C, L = (b - y * 2) / S, D = Math.min(w, L), Y = (_.left + _.right) / 2, X = (_.top + _.bottom) / 2; return this.flyTo(Y, X, D); }, /** * Dispose of the scene and clean up resources */ dispose() { c !== null && (cancelAnimationFrame(c), c = null), d.disconnect(), f && (f.dispose(), f = null); for (let _ = 0; _ < h.length; _++) h[_].dispose && h[_].dispose(); h.length = 0; for (const _ in l) l[_].length = 0; i.parentNode && i.parentNode.removeChild(i); } }; return m; } function Ct(a, t, e, s, i) { lt(a, t, e || 0, s || a.length - 1, i || St); } function lt(a, t, e, s, i) { for (; s > e; ) { if (s - e > 600) { var n = s - e + 1, o = t - e + 1, h = Math.log(n), l = 0.5 * Math.exp(2 * h / 3), c = 0.5 * Math.sqrt(h * l * (n - l) / n) * (o - n / 2 < 0 ? -1 : 1), r = Math.max(e, Math.floor(t - o * l / n + c)), u = Math.min(s, Math.floor(t + (n - o) * l / n + c)); lt(a, t, r, u, i); } var p = a[t], x = e, g = s; for (N(a, e, t), i(a[s], p) > 0 && N(a, e, s); x < g; ) { for (N(a, x, g), x++, g--; i(a[x], p) < 0; ) x++; for (; i(a[g], p) > 0; ) g--; } i(a[e], p) === 0 ? N(a, e, g) : (g++, N(a, g, s)), g <= t && (e = g + 1), t <= g && (s = g - 1); } } function N(a, t, e) { var s = a[t]; a[t] = a[e], a[e] = s; } function St(a, t) { return a < t ? -1 : a > t ? 1 : 0; } class F { constructor(t = 9) { this._maxEntries = Math.max(4, t), this._minEntries = Math.max(2, Math.ceil(this._maxEntries * 0.4)), this.clear(); } all() { return this._all(this.data, []); } search(t) { let e = this.data; const s = []; if (!$(t, e)) return s; const i = this.toBBox, n = []; for (; e; ) { for (let o = 0; o < e.children.length; o++) { const h = e.children[o], l = e.leaf ? i(h) : h; $(t, l) && (e.leaf ? s.push(h) : j(t, l) ? this._all(h, s) : n.push(h)); } e = n.pop(); } return s; } collides(t) { let e = this.data; if (!$(t, e)) return !1; const s = []; for (; e; ) { for (let i = 0; i < e.children.length; i++) { const n = e.children[i], o = e.leaf ? this.toBBox(n) : n; if ($(t, o)) { if (e.leaf || j(t, o)) return !0; s.push(n); } } e = s.pop(); } return !1; } load(t) { if (!(t && t.length)) return this; if (t.length < this._minEntries) { for (let s = 0; s < t.length; s++) this.insert(t[s]); return this; } let e = this._build(t.slice(), 0, t.length - 1, 0); if (!this.data.children.length) this.data = e; else if (this.data.height === e.height) this._splitRoot(this.data, e); else { if (this.data.height < e.height) { const s = this.data; this.data = e, e = s; } this._insert(e, this.data.height - e.height - 1, !0); } return this; } insert(t) { return t && this._insert(t, this.data.height - 1), this; } clear() { return this.data = E([]), this; } remove(t, e) { if (!t) return this; let s = this.data; const i = this.toBBox(t), n = [], o = []; let h, l, c; for (; s || n.length; ) { if (s || (s = n.pop(), l = n[n.length - 1], h = o.pop(), c = !0), s.leaf) { const r = wt(t, s.children, e); if (r !== -1) return s.children.splice(r, 1), n.push(s), this._condense(n), this; } !c && !s.leaf && j(s, i) ? (n.push(s), o.push(h), h = 0, l = s, s = s.children[0]) : l ? (h++, s = l.children[h], c = !1) : s = null; } return this; } toBBox(t) { return t; } compareMinX(t, e) { return t.minX - e.minX; } compareMinY(t, e) { return t.minY - e.minY; } toJSON() { return this.data; } fromJSON(t) { return this.data = t, this; } _all(t, e) { const s = []; for (; t; ) t.leaf ? e.push(...t.children) : s.push(...t.children), t = s.pop(); return e; } _build(t, e, s, i) { const n = s - e + 1; let o = this._maxEntries, h; if (n <= o) return h = E(t.slice(e, s + 1)), T(h, this.toBBox), h; i || (i = Math.ceil(Math.log(n) / Math.log(o)), o = Math.ceil(n / Math.pow(o, i - 1))), h = E([]), h.leaf = !1, h.height = i; const l = Math.ceil(n / o), c = l * Math.ceil(Math.sqrt(o)); ot(t, e, s, c, this.compareMinX); for (let r = e; r <= s; r += c) { const u = Math.min(r + c - 1, s); ot(t, r, u, l, this.compareMinY); for (let p = r; p <= u; p += l) { const x = Math.min(p + l - 1, u); h.children.push(this._build(t, p, x, i - 1)); } } return T(h, this.toBBox), h; } _chooseSubtree(t, e, s, i) { for (; i.push(e), !(e.leaf || i.length - 1 === s); ) { let n = 1 / 0, o = 1 / 0, h; for (let l = 0; l < e.children.length; l++) { const c = e.children[l], r = K(c), u = Lt(t, c) - r; u < o ? (o = u, n = r < n ? r : n, h = c) : u === o && r < n && (n = r, h = c); } e = h || e.children[0]; } return e; } _insert(t, e, s) { const i = s ? t : this.toBBox(t), n = [], o = this._chooseSubtree(i, this.data, e, n); for (o.children.push(t), z(o, i); e >= 0 && n[e].children.length > this._maxEntries; ) this._split(n, e), e--; this._adjustParentBBoxes(i, n, e); } // split overflowed node into two _split(t, e) { const s = t[e], i = s.children.length, n = this._minEntries; this._chooseSplitAxis(s, n, i); const o = this._chooseSplitIndex(s, n, i), h = E(s.children.splice(o, s.children.length - o)); h.height = s.height, h.leaf = s.leaf, T(s, this.toBBox), T(h, this.toBBox), e ? t[e - 1].children.push(h) : this._splitRoot(s, h); } _splitRoot(t, e) { this.data = E([t, e]), this.data.height = t.height + 1, this.data.leaf = !1, T(this.data, this.toBBox); } _chooseSplitIndex(t, e, s) { let i, n = 1 / 0, o = 1 / 0; for (let h = e; h <= s - e; h++) { const l = A(t, 0, h, this.toBBox), c = A(t, h, s, this.toBBox), r = Xt(l, c), u = K(l) + K(c); r < n ? (n = r, i = h, o = u < o ? u : o) : r === n && u < o && (o = u, i = h); } return i || s - e; } // sorts node children by the best axis for split _chooseSplitAxis(t, e, s) { const i = t.leaf ? this.compareMinX : Pt, n = t.leaf ? this.compareMinY : Yt, o = this._allDistMargin(t, e, s, i), h = this._allDistMargin(t, e, s, n); o < h && t.children.sort(i); } // total margin of all possible split distributions where each node is at least m full _allDistMargin(t, e, s, i) { t.children.sort(i); const n = this.toBBox, o = A(t, 0, e, n), h = A(t, s - e, s, n); let l = V(o) + V(h); for (let c = e; c < s - e; c++) { const r = t.children[c]; z(o, t.leaf ? n(r) : r), l += V(o); } for (let c = s - e - 1; c >= e; c--) { const r = t.children[c]; z(h, t.leaf ? n(r) : r), l += V(h); } return l; } _adjustParentBBoxes(t, e, s) { for (let i = s; i >= 0; i--) z(e[i], t); } _condense(t) { for (let e = t.length - 1, s; e >= 0; e--) t[e].children.length === 0 ? e > 0 ? (s = t[e - 1].children, s.splice(s.indexOf(t[e]), 1)) : this.clear() : T(t[e], this.toBBox); } } function wt(a, t, e) { if (!e) return t.indexOf(a); for (let s = 0; s < t.length; s++) if (e(a, t[s])) return s; return -1; } function T(a, t) { A(a, 0, a.children.length, t, a); } function A(a, t, e, s, i) { i || (i = E(null)), i.minX = 1 / 0, i.minY = 1 / 0, i.maxX = -1 / 0, i.maxY = -1 / 0; for (let n = t; n < e; n++) { const o = a.children[n]; z(i, a.leaf ? s(o) : o); } return i; } function z(a, t) { return a.minX = Math.min(a.minX, t.minX), a.minY = Math.min(a.minY, t.minY), a.maxX = Math.max(a.maxX, t.maxX), a.maxY = Math.max(a.maxY, t.maxY), a; } function Pt(a, t) { return a.minX - t.minX; } function Yt(a, t) { return a.minY - t.minY; } function K(a) { return (a.maxX - a.minX) * (a.maxY - a.minY); } function V(a) { return a.maxX - a.minX + (a.maxY - a.minY); } function Lt(a, t) { return (Math.max(t.maxX, a.maxX) - Math.min(t.minX, a.minX)) * (Math.max(t.maxY, a.maxY) - Math.min(t.minY, a.minY)); } function Xt(a, t) { const e = Math.max(a.minX, t.minX), s = Math.max(a.minY, t.minY), i = Math.min(a.maxX, t.maxX), n = Math.min(a.maxY, t.maxY); return Math.max(0, i - e) * Math.max(0, n - s); } function j(a, t) { return a.minX <= t.minX && a.minY <= t.minY && t.maxX <= a.maxX && t.maxY <= a.maxY; } function $(a, t) { return t.minX <= a.maxX && t.minY <= a.maxY && t.maxX >= a.minX && t.maxY >= a.minY; } function E(a) { return { children: a, height: 1, leaf: !0, minX: 1 / 0, minY: 1 / 0, maxX: -1 / 0, maxY: -1 / 0 }; } function ot(a, t, e, s, i) { const n = [t, e]; for (; n.length; ) { if (e = n.pop(), t = n.pop(), e - t <= s) continue; const o = t + Math.ceil((e - t) / s / 2) * s; Ct(a, o, t, e, i), n.push(t, o, o, e); } } class It { constructor(t) { this._svg = t, this._elements = /* @__PURE__ */ new Map(), this._restoreTimer = 0, this._onSvgWheel = this._handleSvgWheel.bind(this), this._svg.addEventListener("wheel", this._onSvgWheel), this._container = this._createContainer(), this._injectStyle(); } _createContainer() { const t = document.createElement("div"); t.style.position = "absolute", t.style.top = "0", t.style.left = "0", t.style.width = "0", t.style.height = "0", t.style.transformOrigin = "0 0", t.style.pointerEvents = "none"; const e = this._svg.parentNode; return e && (getComputedStyle(e).position === "static" && (e.style.position = "relative"), e.insertBefore(t, this._svg.nextSibling)), t; } /** * Inject a one-off stylesheet rule that suppresses pointer events on all * children when the container has the suppressed class. The !important * overrides any inline pointer-events:auto set by the consumer. */ _injectStyle() { if (document.getElementById("dom-overlay-style")) return; const t = document.createElement("style"); t.id = "dom-overlay-style", t.textContent = ".dom-overlay--suppressed > * { pointer-events: none !important; }", document.head.appendChild(t); } /** * When the user wheels on the SVG (zooming), suppress interactivity on * all DOM overlay children so cards that drift under the cursor can't * capture subsequent wheel ticks. Restore after 150ms of no wheel events. */ _handleSvgWheel() { this._container.classList.add("dom-overlay--suppressed"), clearTimeout(this._restoreTimer), this._restoreTimer = setTimeout(() => { this._container.classList.remove("dom-overlay--suppressed"); }, 150); } /** * Sync the overlay transform with the SVG scene transform. * Called once per frame. */ syncTransform(t) { const e = t.transform; this._container.style.transform = `translate(${e.x}px,${e.y}px) scale(${e.scale})`; } /** * Get or create a DOM element for the given node. * `createFn(data, ctx)` is only called on first encounter. */ ensureElement(t, e, s, i) { let n = this._elements.get(t); if (!n) { const o = i(e, s); o.style.position = "absolute", o.style.left = "0", o.style.top = "0", o.style.transformOrigin = "0 0", o.style.pointerEvents = "none", n = { el: o, attached: !1, stateVersion: -1 }, this._elements.set(t, n); } return n; } /** * Call the update callback if the node's state version has changed. */ updateState(t, e, s, i, n) { const o = this._elements.get(t); !o || o.stateVersion === n || (o.stateVersion = n, i(e, s, o.el)); } /** * Set a DOM element's position in world coordinates with counter-scale. * contentScale matches NodeCollection's maxScale / zoom capping. * halfW/halfH offset the element so its center aligns with (worldX, worldY), * matching SVG node centering behavior. */ setPosition(t, e, s, i, n, o) { const h = this._elements.get(t); if (h) { const l = e - (n || 0) * i, c = s - (o || 0) * i; h.el.style.transform = `translate(${l}px,${c}px) scale(${i})`; } } /** * Attach the element to the overlay container (make visible). */ attach(t) { const e = this._elements.get(t); e && !e.attached && (this._container.appendChild(e.el), e.attached = !0); } /** * Detach the element from the overlay container (hide, but keep cached). */ detach(t) { const e = this._elements.get(t); e && e.attached && (this._container.removeChild(e.el), e.attached = !1); } /** * Remove a node's DOM element entirely (detach + delete from cache). */ remove(t) { const e = this._elements.get(t); e && (e.attached && e.el.parentNode && e.el.parentNode.removeChild(e.el), this._elements.delete(t)); } dispose() { clearTimeout(this._restoreTimer), this._svg.removeEventListener("wheel", this._onSvgWheel), this._container && this._container.parentNode && this._container.parentNode.removeChild(this._container), this._elements.clear(), this._container = null; } } const at = "http://www.w3.org/2000/svg"; function M(a, t, e) { return typeof a == "function" ? a(t, e) : a; } function Q(a) { return a ? String(a).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;") : ""; } function Dt(a) { let t; if (a.type && !a.layers) { const { minZoom: i, maxZoom: n, importance: o, hitArea: h, ...l } = a; t = { minZoom: i || 0, maxZoom: n, importance: o, hitArea: h, layers: [l] }; } else t = { minZoom: a.minZoom || 0, maxZoom: a.maxZoom, importance: a.importance, hitArea: a.hitArea, layers: a.layers || [] }; const e = t.layers.find((i) => i.type === "dom"); e && (t._domLayer = e); const s = t.layers.find((i) => i.type === "svg" && i.update); return s && (t._svgUpdate = s.update), t; } function Tt(a, t, e) { if (a.visible !== void 0 && !M(a.visible, t, e)) return ""; const s = a.type; if (s === "circle") { const i = M(a.radius, t, e) || 4, n = M(a.fill, t, e) || "none", o = M(a.stroke, t, e) || "none", h = M(a.strokeWidth, t, e) || 0, l = M(a.opacity, t, e), c = M(a.filter, t, e); let r = `r="${i}" fill="${n}"`; return o !== "none" && (r += ` stroke="${o}"`), h && (r += ` stroke-width="${h}"`), l != null && (r += ` opacity="${l}"`), c && (r += ` filter="${c}"`), `<circle ${r}/>`; } if (s === "rect") { const i = M(a.width, t, e) || 10, n = M(a.height, t, e) || 10, o = M(a.rx, t, e), h = M(a.ry, t, e), l = M(a.fill, t, e) || "none", c = M(a.stroke, t, e) || "none", r = M(a.strokeWidth, t, e) || 0, u = M(a.opacity, t, e), p = M(a.filter, t, e); let x = `x="${-i / 2}" y="${-n / 2}" width="${i}" height="${n}" fill="${l}"`; return o != null && (x += ` rx="${o}"`), h != null && (x += ` ry="${h}"`), c !== "none" && (x += ` stroke="${c}"`), r && (x += ` stroke-width="${r}"`), u != null && (x += ` opacity="${u}"`), p && (x += ` filter="${p}"`), `<rect ${x}/>`; } if (s === "text") { const i = M(a.text, t, e); if (i == null || i === "") return ""; const n = M(a.fontSize, t, e) || 10, o = M(a.fill, t, e) || "#000", h = M(a.fontFamily, t, e), l = M(a.fontWeight, t, e), c = M(a.anchor, t, e) || "center", r = M(a.offset, t, e) || [0, 0], u = M(a.opacity, t, e), p = M(a.maxWidth, t, e); let x = "middle", g = r[0], f = r[1]; c === "center" && (f += n * 0.35); let d = `x="${g}" y="${f}" text-anchor="${x}" font-size="${n}" fill="${o}"`; h && (d += ` font-family="${h}"`), l && (d += ` font-weight="${l}"`), u != null && (d += ` opacity="${u}"`); const m = String(i); if (p && m.length * n * 0.6 > p) { const _ = ct(m, n, p), y = n * 1.3; let v = ""; for (let b = 0; b < _.length; b++) b === 0 ? v += `<tspan x="${g}">${Q(_[b])}</tspan>` : v += `<tspan x="${g}" dy="${y}">${Q(_[b])}</tspan>`; return `<text ${d}>${v}</text>`; } return `<text ${d}>${Q(m)}</text>`; } return s === "svg" && typeof a.create == "function" && a.create(t, e) || ""; } function ct(a, t, e) { const s = t * 0.6, i = Math.max(1, Math.floor(e / s)), n = a.split(/\s+/), o = []; let h = ""; for (let l = 0; l < n.length; l++) { const c = n[l], r = h ? h + " " + c : c; r.length > i && h ? (o.push(h), h = c) : h = r; } return h && o.push(h), o; } function W(a, t, e) { const s = a.layers; if (!s || s.length === 0) return ""; let i = ""; for (let n = 0; n < s.length; n++) i += Tt(s[n], t, e); return i; } function J(a, t, e) { const s = a.layers; let i = 0, n = 0; for (let o = 0; o < s.length; o++) { const h = s[o]; if (h.visible !== void 0 && !M(h.visible, t, e)) continue; const l = h.type; if (l === "circle") { const r = (M(h.radius, t, e) || 4) * 2; r > i && (i = r), r > n && (n = r); } else if (l === "rect") { const c = M(h.width, t, e) || 10, r = M(h.height, t, e) || 10; c > i && (i = c), r > n && (n = r); } else if (l === "svg") { const c = M(h.width, t, e), r = M(h.height, t, e); c && c > i && (i = c), r && r > n && (n = r); } else if (l === "dom") { const c = M(h.width, t, e) || 0, r = M(h.height, t, e) || 0; c > i && (i = c), r > n && (n = r); } else if (l === "text") { const c = M(h.text, t, e); if (!c) continue; const r = M(h.fontSize, t, e) || 10, u = M(h.offset, t, e) || [0, 0], p = M(h.maxWidth, t, e), x = String(c); let g, f; if (p && x.length * r * 0.6 > p) { const d = ct(x, r, p); g = p + Math.abs(u[0]) * 2, f = d.length * r * 1.3 + Math.abs(u[1]); } else g = x.length * r * 0.6 + Math.abs(u[0]) * 2, f = r + Math.abs(u[1]); g > i && (i = g), n += f; } } if (a.hitArea) { const o = a.hitArea; o.type === "rect" && (i = M(o.width, t, e) || i, n = M(o.height, t, e) || n); } return { width: i, height: n }; } function Et(a, t, e) { const s = a.layers; for (let i = 0; i < s.length; i++) { const n = s[i]; if (n.type === "circle") return { type: "circle", radius: M(n.radius, t, e) || 4 }; if (n.type === "rect") return { type: "rect", width: M(n.width, t, e) || 10, height: M(n.height, t, e) || 10 }; } return null; } class jt { constructor(t = {}) { st(this, "_transitionRafId", null); this._dataFn = t.data || null, this._maxScale = t.maxScale ?? 1, this._levels = (t.levels || []).map(Dt), this._collisionTrees = /* @__PURE__ */ new Map(), this._spatialIndex = new F(), this._spatialValid = !1, this._positionsDirty = !0, this._attachedNodes = /* @__PURE__ */ new Set(), this._swapAttachedSet = /* @__PURE__ */ new Set(), this._maxNodeSize = 0, this._root = document.createElementNS(at, "g"), this._root.setAttribute("class", "node-collection"), this._nodes = [], this._nodeMap = /* @__PURE__ */ new Map(), this._freeIndices = [], this._batchDepth = 0, this._batchDirty = !1, this._elementPool = [], this._lastDrawContext = null, this._lastScale = 1, this._lastCollisionZoom = 0, this._lastCandidateLevel = void 0, this._state = /* @__PURE__ */ new Map(), this._resolvedLevels = /* @__PURE__ */ new Map(), this._prevResolvedLevels = /* @__PURE__ */ new Map(), this._transitions = /* @__PURE__ */ new Map(), this._transitionDuration = 150, this._reusableCtx = { zoom: 1 }, this._reusableCtxKeys = [], this._collisionInterval = t.collisionInterval ?? 200, this._lastCollisionTime = 0, this._collisionNodes = [], this._collisionCandidates = [], this._collisionStable = [], this._hasDomLayers = this._levels.some((e) => e._domLayer), this._domOverlay = null, this._graph = t.graph || null, this._graphChangeListener = null, this._graph && this._dataFn && this._bindGraph(); } /** * Get the root SVG group element */ getRoot() { return this._root; } /** * Add a node to the collection * @returns {Object} Node handle */ add(t) { const { id: e = this._generateId(), x: s = 0, y: i = 0, data: n = {} } = t, o = this._computeNodeSize(n); let h; this._freeIndices.length > 0 ? h = this._freeIndices.pop() : h = this._nodes.length; const l = { id: e, index: h, x: s, y: i, size: o, data: n, visible: !0, _element: null, _currentLevel: -1, _inDOM: !1, _collection: this, _stateVersion: 0, _renderedLevel: -1, _renderedStateVersion: -1 }; return this._nodes[h] = l, this._nodeMap.set(e, l), o > this._maxNodeSize && (this._maxNodeSize = o), this._createNodeElement(l), this._spatialValid = !1, this._batchDepth === 0 ? this._addNodeToScene(l, this._lastDrawContext) : this._batchDirty = !0, l; } /** * Remove a node from the collection */ remove(t) { const e = typeof t == "object" ? t : this._nodeMap.get(t); e && (e._element && (e._inDOM && (this._root.removeChild(e._element), e._inDOM = !1), this._elementPool.push(e._element), e._element = null), this._domOverlay && this._domOverlay.remove(e.id), this._freeIndices.push(e.index), this._nodes[e.index] = null, this._nodeMap.delete(e.id), this._attachedNodes.delete(e), this._state.delete(e.id), this._resolvedLevels.delete(e.id), this._prevResolvedLevels.delete(e.id), this._transitions.delete(e.id), this._spatialValid = !1, this._batchDepth > 0 && (this._batchDirty = !0)); } /** * Get a node by ID */ get(t) { return this._nodeMap.get(t); } /** * Set node position (GC-friendly) */ setPosition(t, e, s) { t.x = e, t.y = s, this._positionsDirty = !0, this._batchDepth === 0 && t._element && t._inDOM && this._applyTransform(t); } /** * Apply transform to a node element */ _applyTransform(t) { if (!t._element) return; const e = Math.min(1, this._maxScale / this._lastScale); e !== 1 ? t._element.setAttribute( "transform", `translate(${t.x}, ${t.y}) scale(${e})` ) : t._element.setAttribute("transform", `translate(${t.x}, ${t.y})`); } // ── Model-driven state ────────────────────────────────────────────── /** * Set a state key on a node. State keys are available in ctx for property functions. * Also applied as CSS classes for external styling. */ setState(t, e, s) { const i = typeof t == "object" ? t.id : t; let n = this._state.get(i); s ? (n || (n = /* @__PURE__ */ new Map(), this._state.set(i, n)), n.set(e, !0)) : n && (n.delete(e), n.size === 0 && this._state.delete(i)); const o = this._nodeMap.get(i); o && (o._stateVersion++, o._element && (s ? o._element.classList.add(e) : o._element.classList.remove(e))); } /** * Get a state value for a node */ getState(t, e) { const s = typeof t == "object" ? t.id : t, i = this._state.get(s); return i && i.get(e) || !1; } /** * Force re-render of all nodes on the next frame. * Useful when external data used by property callbacks (e.g. fontSize) * has changed without any level or state change. */ invalidateContent() { for (const t of this._nodeMap.values()) t._stateVersion++; } /** * Remove a state key from ALL nodes and sync CSS classes */ clearState(t) { for (const [e, s] of this._state) if (s.has(t)) { s.delete(t), s.size === 0 && this._state.delete(e); const i = this._nodeMap.get(e); i && (i._stateVersion++, i._element && i._element.classList.remove(t)); } } /** * Reapply all state keys as CSS classes on a node element. */ _reapplyState(t) { if (!t._element) return; t._element.setAttribute("class", "node"); const e = this._state.get(t.id); if (e) for (const s of e.keys()) t._element.classList.add(s); } /** * Build ctx object for a node's property functions. */ _buildCtx(t) { const e = this._reusableCtx, s = this._reusableCtxKeys; for (let n = 0; n < s.length; n++) e[s[n]] = void 0; s.length = 0, e.zoom = this._lastScale; const i = this._state.get(t); if (i) for (const [n, o] of i) e[n] = o, s.push(n); return e; } // ── Shape descriptors ─────────────────────────────────────────────── /** * Get the current shape of a node in world coordinates. */ getNodeShape(t) { const e = this._nodeMap.get(t); if (!e) return null; const s = this._resolvedLevels.get(t) ?? 0; if (s < 0 || s >= this._levels.length) return null; const i = this._levels[s], n = this._buildCtx(t), o = Et(i, e.data, n); if (!o) return null; const h = Math.min(1, this._maxScale / this._lastScale); return o.type === "circle" ? { type: "circle", radius: o.radius * h, x: e.x, y: e.y } : o.type === "rect" ? { type: "rect", width: o.width * h, height: o.height * h, x: e.x, y: e.y } : null; } /** * Get the current content scale factor. */ getContentScale() { return Math.min(1, this._maxScale / this._lastScale); } // ── Sync positions ───────────────────────────────────────────────── syncPositions(t) { this.beginBatch(); for (const [e, s] of t) { const i = this._nodeMap.get(e); i && (i.x = s.x, i.y = s.y, i.visible || (i.visible = !0)); } for (let e = 0; e < this._nodes.length; e++) { const s = this._nodes[e]; s && s.visible && !t.has(s.id) && (s.visible = !1); } this._positionsDirty = !0, this.endBatch(); } // ── Hit testing ──────────────────────────────────────────────────── getNodeAt(t, e, s) { if (s || (s = this._lastDrawContext), !s) return null; const i = s.screenToScene(t, e), n = Math.max(this._maxNodeSize, 80 / this._lastScale); this._spatialValid || (this._rebuildSpatialIndex(), this._spatialValid = !0); const o = this._spatialIndex.search({ minX: i.x - n, minY: i.y - n, maxX: i.x + n, maxY: i.y + n }); let h = null, l = 1 / 0; const c = Math.min(1, this._maxScale / this._lastScale), r = 10 / this._lastScale; for (let u = 0; u < o.length; u++) { const p = o[u].node, x = i.x - p.x, g = i.y - p.y, f = x * x + g * g; let d = !1; if (this._levels.length > 0) { const m = this._resolvedLevels.get(p.id) ?? 0; if (m >= 0 && m < this._levels.length) { const _ = this._levels[m], y = this._buildCtx(p.id), v = J(_, p.data, y), b = Math.max(v.width * c / 2, r), C = Math.max(v.height * c / 2, r); d = Math.abs(x) <= b && Math.abs(g) <= C; } } if (!d) { const m = this.getNodeShape(p.id); if (m && m.type === "circle") { const _ = Math.max(m.radius, r); d = f <= _ * _; } else if (m && m.type === "rect") { const _ = Math.max(m.width / 2, r), y = Math.max(m.height / 2, r); d = Math.abs(x) <= _ && Math.abs(g) <= y; } else { const _ = Math.max(r, p.size * 0.5); d = f <= _ * _; } } d && f < l && (l = f, h = p); } return h ? h.id : null; } // ── Batch updates ────────────────────────────────────────────────── beginBatch() { this._batchDepth++; } endBatch() { this._batchDepth > 0 && this._batchDepth--, this._batchDepth === 0 && this._batchDirty && (this._batchDirty = !1, this.render(this._lastDrawContext)); } // ── Iteration ────────────────────────────────────────────────────── forEach(t) { for (let e = 0; e < this._nodes.length; e++) { const s = this._nodes[e]; s && t(s, s.id); } } get count() { return this._nodeMap.size; } // ── Render ───────────────────────────────────────────────────────── render(t) { if (this._lastDrawContext = t, !t) return; const e = t.transform.scale, s = e !== this._lastScale; this._lastScale = e; const i = performance.now(), n = this._getCandidateLevelIndex(e), o = this._lastCandidateLevel !== void 0 && n !== this._lastCandidateLevel; this._lastCandidateLevel = n; const h = this._lastCollisionZoom > 0 ? e / this._lastCollisionZoom : 1 / 0, l = h > 1.2, c = h < 0.83; if (o || c || this._positionsDirty && i - this._lastCollisionTime >= this._collisionInterval ? (this._computeResolvedLevels(t, !1), this._lastCollisionTime = i, this._lastCollisionZoom = e) : l && (this._computeResolvedLevels(t, !0), this._lastCollisionTime = i, this._lastCollisionZoom = e), this._hasDomLayers && !this._domOverlay) { const r = this._root.ownerSVGElement; r && (this._domOverlay = new It(r)); } this._domOverlay && this._domOverlay.syncTransform(t), this._positionsDirty ? (this._positionsDirty = !1, this._spatialValid = !1, this._renderAllNodes(t, s)) : (this._spatialValid || (this._rebuildSpatialIndex(), this._spatialValid = !0), this._renderWithSpatialQuery(t, s)); } /** * Compute which level each node should render at, considering zoom and collision. * * For each node: * 1. Find the candidate level based on zoom (last level where minZoom <= zoom) * 2. If level has importance, check collision with more important nodes * 3. On collision, fall back to previous level */ _computeResolvedLevels(t, e = !1) { const s = this._lastScale, i = this._levels; if (i.length === 0) return; const n = this._prevResolvedLevels; this._prevResolvedLevels = this._resolvedLevels, this._resolvedLevels = n, this._resolvedLevels.clear(); const o = this._getCandidateLevelIndex(s), h = this._collisionNodes; h.length = 0; for (let f = 0; f < this._nodes.length; f++) { const d = this._nodes[f]; if (!d || !d.visible) continue; const m = Math.max(d.size, 50 / s); t.isVisible(d.x, d.y, m) && h.push(d); } const l = Math.min(1, this._maxScale / s), c = this._collisionCandidates, r = this._collisionStable, u = t.width / 2, p = t.height / 2, x = Math.hypot(u, p) || 1, g = { minX: 0, minY: 0, maxX: 0, maxY: 0 }; for (let f = o; f >= 0; f--) { const d = i[f]; if (s < d.minZoom || d.maxZoom !== void 0 && s >= d.maxZoom) continue; if (!d.importance) { for (let _ = 0; _ < h.length; _++) { const y = h[_]; this._resolvedLevels.has(y.id) || this._resolvedLevels.set(y.id, f); } break; } let m = this._collisionTrees.get(f); if (m ? m.clear() : (m = new F(), this._collisionTrees.set(f, m)), e) { r.length = 0; for (let _ = 0; _ < h.length; _++) { const y = h[_]; if (this._resolvedLevels.has(y.id) || this._prevResolvedLevels.get(y.id) !== f) continue; const v = this._buildCtx(y.id), b = t.sceneToScreen(y.x, y.y); v.centerProximity = 1 - Math.min(1, Math.hypot(b.x - u, b.y - p) / x); const C = M(d.importance, y.data, v); r.push(y, C || 0); } this._sortPairs(r); for (let _ = 0; _ < r.length; _ += 2) { const y = r[_]; this._fillBbox(g, y, d, l, s, t), m.collides(g) || (m.insert({ minX: g.minX, minY: g.minY, maxX: g.maxX, maxY: g.maxY }), this._resolvedLevels.set(y.id, f)); } } c.length = 0; for (let _ = 0; _ < h.length; _++) { const y = h[_]; if (this._resolvedLevels.has(y.id)) continue; const v = this._buildCtx(y.id), b = t.sceneToScreen(y.x, y.y); v.centerProximity = 1 - Math.min(1, Math.hypot(b.x - u, b.y - p) / x); const C = M(d.importance, y.data, v); c.push(y, C || 0); } this._sortPairs(c); for (let _ = 0; _ < c.length; _ += 2) { const y = c[_]; this._fillBbox(g, y, d, l, s, t), m.collides(g) || (m.insert({ minX: g.minX, minY: g.minY, maxX: g.maxX, maxY: g.maxY }), this._resolvedLevels.set(y.id, f)); } } for (let f = 0; f < h.length; f++) { const d = h[f]; this._resolvedLevels.has(d.id) || this._resolvedLevels.set(d.id, 0); } } /** * Find the highest candidate level index for the current zoom. */ _getCandidateLevelIndex(t) { const e = this._levels; let s = 0; for (let i = 0; i < e.length; i++) t >= e[i].minZoom && (e[i].maxZoom === void 0 || t < e[i].maxZoom) && (s = i); return s; } /** * O(N) render path — used during layout when positions change every frame. */ _renderAllNodes(t, e) { this._attachedNodes.clear(); for (let s = 0; s < this._nodes.length; s++) { const i = this._nodes[s]; if (!i || !i._element) continue; const n = Math.max(i.size, 50 / this._lastScale); i.visible && t.isVisible(i.x, i.y, n) ? (i._inDOM || (this._root.appendChild(i._element), i._inDOM = !0, this._reapplyState(i)), this._applyTransform(i), this._updateNodeContent(i, t, e), this._attachedNodes.add(i)) : (i._inDOM && (this._root.removeChild(i._element), i._inDOM = !1), this._domOverlay && this._domOverlay.detach(i.id)); } } /** * R-tree render path — used during pan/zoom when positions are stable. */ _renderWithSpatialQuery(t, e) { const s = t.getVisibleBounds(), i = Math.max(this._maxNodeSize, 100 / this._lastScale), n = this._spatialIndex.search({ minX: s.left - i, minY: s.top - i, maxX: s.right + i, maxY: s.bottom + i }), o = this._swapAttachedSet; o.clear(); for (let h = 0; h < n.length; h++) { const l = n[h].node; if (!l._element) continue; const c = Math.max(l.size, 50 / this._lastScale); t.isVisible(l.x, l.y, c) && (l._inDOM || (this._root.appendChild(l._element), l._inDOM = !0, this._reapplyState(l), this._applyTransform(l)), this._updateNodeContent(l, t, e), o.add(l)); } for (const h of this._attachedNodes) o.has(h) || (h._inDOM && (this._root.removeChild(h._element), h._inDOM = !1), this._domOverlay && this._domOverlay.detach(h.id)); this._swapAttachedSet = this._attachedNodes, this._attachedNodes = o; } _rebuildSpatialIndex() { const t = []; for (const e of this._nodes) !e || !e.visible || t.push({ minX: e.x, minY: e.y, maxX: e.x, maxY: e.y, node: e }); this._spatialIndex.clear(), t.length > 0 && this._spatialIndex.load(t); } _addNodeToScene(t, e) { if (!t._element || !e) return; const s = Math.max(t.size, 50 / this._lastScale); e.isVisible(t.x, t.y, s) && (t._inDOM || (this._root.appendChild(t._element), t._inDOM = !0, this._reapplyState(t), this._attachedNodes.add(t)), this._updateNodeContent(t, e)); } /** * Update node content based on resolved level. */ _updateNodeContent(t, e, s = !1) { if (!t._element || !e || (s && this._applyTransform(t), this._levels.length === 0)) return;