@kanzo-tech/graph 0.13.1 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/README.md +12 -15
  2. package/dist/core/categories.d.ts +18 -6
  3. package/dist/core/categories.d.ts.map +1 -1
  4. package/dist/core/categories.js +41 -10
  5. package/dist/core/categories.js.map +1 -1
  6. package/dist/core/channels.d.ts +10 -5
  7. package/dist/core/channels.d.ts.map +1 -1
  8. package/dist/core/channels.js +11 -13
  9. package/dist/core/channels.js.map +1 -1
  10. package/dist/core/detail.d.ts +14 -8
  11. package/dist/core/detail.d.ts.map +1 -1
  12. package/dist/core/detail.js +33 -15
  13. package/dist/core/detail.js.map +1 -1
  14. package/dist/core/filter.d.ts +3 -1
  15. package/dist/core/filter.d.ts.map +1 -1
  16. package/dist/core/filter.js +54 -50
  17. package/dist/core/filter.js.map +1 -1
  18. package/dist/core/load.d.ts +58 -0
  19. package/dist/core/load.d.ts.map +1 -0
  20. package/dist/core/load.js +115 -0
  21. package/dist/core/load.js.map +1 -0
  22. package/dist/core/state.d.ts +31 -56
  23. package/dist/core/state.d.ts.map +1 -1
  24. package/dist/core/store.d.ts +1 -3
  25. package/dist/core/store.d.ts.map +1 -1
  26. package/dist/core/store.js +162 -172
  27. package/dist/core/store.js.map +1 -1
  28. package/dist/core/types.d.ts +21 -17
  29. package/dist/core/types.d.ts.map +1 -1
  30. package/dist/index.d.ts +5 -7
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +19 -25
  33. package/dist/index.js.map +1 -1
  34. package/dist/parts/gesture.d.ts +1 -10
  35. package/dist/parts/gesture.d.ts.map +1 -1
  36. package/dist/parts/gesture.js +26 -28
  37. package/dist/parts/gesture.js.map +1 -1
  38. package/dist/parts/graph-canvas.d.ts.map +1 -1
  39. package/dist/parts/graph-canvas.js +128 -106
  40. package/dist/parts/graph-canvas.js.map +1 -1
  41. package/dist/parts/graph-inspector.d.ts +4 -4
  42. package/dist/parts/graph-inspector.d.ts.map +1 -1
  43. package/dist/parts/graph-inspector.js +55 -57
  44. package/dist/parts/graph-inspector.js.map +1 -1
  45. package/dist/parts/graph-legend.d.ts +4 -4
  46. package/dist/parts/graph-legend.d.ts.map +1 -1
  47. package/dist/parts/graph-legend.js +21 -21
  48. package/dist/parts/graph-legend.js.map +1 -1
  49. package/dist/parts/graph-toolbar.d.ts +1 -1
  50. package/dist/parts/graph-toolbar.d.ts.map +1 -1
  51. package/dist/parts/graph-toolbar.js +40 -40
  52. package/dist/parts/graph-toolbar.js.map +1 -1
  53. package/dist/parts/overlays.d.ts +6 -5
  54. package/dist/parts/overlays.d.ts.map +1 -1
  55. package/dist/parts/overlays.js +70 -68
  56. package/dist/parts/overlays.js.map +1 -1
  57. package/dist/react/use-graph-state.d.ts +4 -2
  58. package/dist/react/use-graph-state.d.ts.map +1 -1
  59. package/dist/react/use-graph-state.js +10 -6
  60. package/dist/react/use-graph-state.js.map +1 -1
  61. package/dist/react/use-graph.d.ts +1 -4
  62. package/dist/react/use-graph.d.ts.map +1 -1
  63. package/dist/react/use-graph.js +38 -49
  64. package/dist/react/use-graph.js.map +1 -1
  65. package/dist/render/graph-looks.d.ts.map +1 -1
  66. package/dist/render/graph-looks.js.map +1 -1
  67. package/dist/render/graph-model.d.ts +6 -8
  68. package/dist/render/graph-model.d.ts.map +1 -1
  69. package/dist/render/graph-model.js +51 -57
  70. package/dist/render/graph-model.js.map +1 -1
  71. package/dist/render/graph-sim.d.ts +2 -3
  72. package/dist/render/graph-sim.d.ts.map +1 -1
  73. package/dist/render/graph-sim.js.map +1 -1
  74. package/dist/render/renderer.d.ts +10 -10
  75. package/dist/render/renderer.d.ts.map +1 -1
  76. package/dist/render/renderer.js +121 -177
  77. package/dist/render/renderer.js.map +1 -1
  78. package/dist/section.d.ts.map +1 -1
  79. package/dist/section.js +3 -7
  80. package/dist/section.js.map +1 -1
  81. package/package.json +29 -10
  82. package/dist/core/refine.d.ts +0 -14
  83. package/dist/core/refine.d.ts.map +0 -1
  84. package/dist/core/refine.js +0 -20
  85. package/dist/core/refine.js.map +0 -1
  86. package/dist/core/resident.d.ts +0 -79
  87. package/dist/core/resident.d.ts.map +0 -1
  88. package/dist/core/resident.js +0 -44
  89. package/dist/core/resident.js.map +0 -1
  90. package/dist/core/scheduler.d.ts +0 -42
  91. package/dist/core/scheduler.d.ts.map +0 -1
  92. package/dist/core/scheduler.js +0 -77
  93. package/dist/core/scheduler.js.map +0 -1
  94. package/dist/core/tile-matrix.d.ts +0 -37
  95. package/dist/core/tile-matrix.d.ts.map +0 -1
  96. package/dist/core/tile-matrix.js +0 -49
  97. package/dist/core/tile-matrix.js.map +0 -1
  98. package/dist/core/tile.d.ts +0 -54
  99. package/dist/core/tile.d.ts.map +0 -1
  100. package/dist/core/tile.js +0 -76
  101. package/dist/core/tile.js.map +0 -1
  102. package/dist/core/tileset.d.ts +0 -59
  103. package/dist/core/tileset.d.ts.map +0 -1
  104. package/dist/core/tileset.js +0 -177
  105. package/dist/core/tileset.js.map +0 -1
  106. package/dist/render/adaptive.d.ts +0 -27
  107. package/dist/render/adaptive.d.ts.map +0 -1
  108. package/dist/render/adaptive.js +0 -25
  109. package/dist/render/adaptive.js.map +0 -1
  110. package/dist/render/compose.d.ts +0 -51
  111. package/dist/render/compose.d.ts.map +0 -1
  112. package/dist/render/compose.js +0 -107
  113. package/dist/render/compose.js.map +0 -1
  114. package/dist/render/encode.d.ts +0 -42
  115. package/dist/render/encode.d.ts.map +0 -1
  116. package/dist/render/encode.js +0 -68
  117. package/dist/render/encode.js.map +0 -1
  118. package/dist/render/when-ready.d.ts +0 -35
  119. package/dist/render/when-ready.d.ts.map +0 -1
  120. package/dist/render/when-ready.js +0 -17
  121. package/dist/render/when-ready.js.map +0 -1
@@ -1,5 +1,5 @@
1
1
  import { Graph } from '@cosmos.gl/graph';
2
- import { Resident, VertexId } from '../core/resident';
2
+ import { VertexId } from '../core/types';
3
3
  export interface GraphOverlays {
4
4
  /** The box the overlays are positioned within — the canvas' own bounds. */
5
5
  hostRef: React.RefObject<HTMLDivElement | null>;
@@ -9,9 +9,12 @@ export interface GraphOverlays {
9
9
  labelRef: (vertex: VertexId) => (element: HTMLElement | null) => void;
10
10
  /** The labelled vertices, in the order the declutter pass should place them. */
11
11
  setLabelOrder: (vertices: VertexId[]) => void;
12
+ /** The vertex the card names; its box is measured again on the next paint. */
12
13
  setHovered: (vertex: VertexId | null) => void;
14
+ /** Where the hovered point is, in space — from the renderer, never read back from the GPU. */
15
+ hoverAt: (position: [number, number] | null) => void;
13
16
  /**
14
- * Re-register the tracked points with cosmos.gl.
17
+ * Re-register the labelled points with cosmos.gl.
15
18
  *
16
19
  * Call it after the graph exists and whenever the set of overlaid nodes changes. It has to be
17
20
  * driven from outside because effects run in declaration order, so this hook's own effects cannot
@@ -23,11 +26,9 @@ export interface GraphOverlays {
23
26
  schedule: () => void;
24
27
  }
25
28
  /**
26
- * The labels, the hover card and the grid, positioned from the renderer every frame: points are
27
- * tracked by index through the resident map, and every overlay is held by identity.
29
+ * The labels, the hover card and the grid, positioned from the renderer every frame.
28
30
  */
29
31
  export declare function useOverlays(api: {
30
32
  getGraph: () => Graph | null;
31
- getResident: () => Resident;
32
33
  }): GraphOverlays;
33
34
  //# sourceMappingURL=overlays.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"overlays.d.ts","sourceRoot":"","sources":["../../src/parts/overlays.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AA8C3D,MAAM,WAAW,aAAa;IAC5B,2EAA2E;IAC3E,OAAO,EAAE,KAAK,CAAC,SAAS,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;IAChD,OAAO,EAAE,KAAK,CAAC,SAAS,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;IAChD,OAAO,EAAE,KAAK,CAAC,SAAS,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;IAChD,mDAAmD;IACnD,QAAQ,EAAE,CAAC,MAAM,EAAE,QAAQ,KAAK,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI,KAAK,IAAI,CAAC;IACtE,gFAAgF;IAChF,aAAa,EAAE,CAAC,QAAQ,EAAE,QAAQ,EAAE,KAAK,IAAI,CAAC;IAC9C,UAAU,EAAE,CAAC,MAAM,EAAE,QAAQ,GAAG,IAAI,KAAK,IAAI,CAAC;IAC9C;;;;;;;OAOG;IACH,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,qEAAqE;IACrE,QAAQ,EAAE,MAAM,IAAI,CAAC;CACtB;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE;IAAE,QAAQ,EAAE,MAAM,KAAK,GAAG,IAAI,CAAC;IAAC,WAAW,EAAE,MAAM,QAAQ,CAAA;CAAE,GAAG,aAAa,CA+N7G"}
1
+ {"version":3,"file":"overlays.d.ts","sourceRoot":"","sources":["../../src/parts/overlays.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AA0C9C,MAAM,WAAW,aAAa;IAC5B,2EAA2E;IAC3E,OAAO,EAAE,KAAK,CAAC,SAAS,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;IAChD,OAAO,EAAE,KAAK,CAAC,SAAS,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;IAChD,OAAO,EAAE,KAAK,CAAC,SAAS,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;IAChD,mDAAmD;IACnD,QAAQ,EAAE,CAAC,MAAM,EAAE,QAAQ,KAAK,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI,KAAK,IAAI,CAAC;IACtE,gFAAgF;IAChF,aAAa,EAAE,CAAC,QAAQ,EAAE,QAAQ,EAAE,KAAK,IAAI,CAAC;IAC9C,8EAA8E;IAC9E,UAAU,EAAE,CAAC,MAAM,EAAE,QAAQ,GAAG,IAAI,KAAK,IAAI,CAAC;IAC9C,8FAA8F;IAC9F,OAAO,EAAE,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,KAAK,IAAI,CAAC;IACrD;;;;;;;OAOG;IACH,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,qEAAqE;IACrE,QAAQ,EAAE,MAAM,IAAI,CAAC;CACtB;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE;IAAE,QAAQ,EAAE,MAAM,KAAK,GAAG,IAAI,CAAA;CAAE,GAAG,aAAa,CAuNhF"}
@@ -1,98 +1,100 @@
1
1
  "use client";
2
- import { useRef as r, useCallback as x, useEffect as S } from "react";
3
- import { whenReady as Q } from "../render/when-ready.js";
4
- const T = 22, C = 14, w = 8, U = 10, V = 12, M = 3, F = (P, l, f) => Math.min(Math.max(P, l), Math.max(l, f));
5
- function tt(P) {
6
- const { getGraph: l, getResident: f } = P, O = r(null), $ = r(null), _ = r(null), A = r(/* @__PURE__ */ new Map()), L = r(/* @__PURE__ */ new Map()), m = r([]), k = r(null), E = r(null), B = r(null), p = r(0), W = x(() => {
7
- const t = l();
2
+ import { useRef as c, useCallback as h, useEffect as S } from "react";
3
+ const T = 22, C = 14, m = 8, Q = 10, U = 12, M = 3, F = (A, i, y) => Math.min(Math.max(A, i), Math.max(i, y));
4
+ function X(A) {
5
+ const { getGraph: i } = A, y = c(null), R = c(null), _ = c(null), P = c(/* @__PURE__ */ new Map()), $ = c(/* @__PURE__ */ new Map()), L = c([]), B = c(null), D = c(null), k = c(null), H = c(null), f = c(0), N = h(() => {
6
+ const t = i();
7
+ t && t.trackPointPositionsByIndices(L.current);
8
+ }, [i]), I = h(() => {
9
+ const t = i();
8
10
  if (!t) return;
9
- const o = k.current, i = o === null || m.current.includes(o) ? m.current : [...m.current, o];
10
- Q(t, (u) => u.trackPointPositionsByIndices(f().indicesOf(i)));
11
- }, [l, f]), D = x(() => {
12
- const t = l();
13
- if (!t) return;
14
- const o = f();
15
- let i = null;
16
- const u = (n) => {
17
- const e = o.indexOf(n);
18
- return e === void 0 ? null : (i ?? (i = t.getTrackedPointPositionsMap()), i.get(e) ?? null);
19
- }, H = [], g = B.current;
20
- for (const n of m.current) {
21
- const e = A.current.get(n);
22
- if (!e) continue;
23
- const a = u(n);
24
- if (!a) {
25
- e.style.opacity = "0";
11
+ let s = null;
12
+ const g = (e) => {
13
+ s ?? (s = t.getTrackedPointPositionsMap());
14
+ const n = s.get(e);
15
+ return n && !Number.isNaN(n[0]) ? n : null;
16
+ }, d = [], p = H.current;
17
+ for (const e of L.current) {
18
+ const n = P.current.get(e);
19
+ if (!n) continue;
20
+ const u = g(e);
21
+ if (!u) {
22
+ n.style.opacity = "0";
26
23
  continue;
27
24
  }
28
- const [c, d] = t.spaceToScreenPosition(a);
29
- let s = L.current.get(n);
30
- s === void 0 && (s = e.offsetWidth, L.current.set(n, s));
31
- const h = c - s / 2, G = h + s, b = d - U, R = b - V, K = g !== null && (G < 0 || b < 0 || h > g.width || R > g.height), N = H.some((v) => h < v[2] && G > v[0] && R < v[3] && b > v[1]);
32
- if (K || N) {
33
- e.style.opacity = "0";
25
+ const [o, a] = t.spaceToScreenPosition(u);
26
+ let r = $.current.get(e);
27
+ r === void 0 && (r = n.offsetWidth, $.current.set(e, r));
28
+ const l = o - r / 2, G = l + r, b = a - Q, w = b - U, J = p !== null && (G < 0 || b < 0 || l > p.width || w > p.height), K = d.some((v) => l < v[2] && G > v[0] && w < v[3] && b > v[1]);
29
+ if (J || K) {
30
+ n.style.opacity = "0";
34
31
  continue;
35
32
  }
36
- H.push([h - M, R - M, G + M, b + M]), e.style.transform = `translate(${Math.round(h)}px, ${Math.round(R)}px)`, e.style.opacity = "1";
33
+ d.push([l - M, w - M, G + M, b + M]), n.style.transform = `translate(${Math.round(l)}px, ${Math.round(w)}px)`, n.style.opacity = "1";
37
34
  }
38
- const z = $.current;
35
+ const z = R.current;
39
36
  if (z) {
40
- const n = t.getZoomLevel();
41
- if (n > 0) {
42
- const e = T * n / 2 ** Math.floor(Math.log2(n)), [a, c] = t.spaceToScreenPosition([0, 0]), d = (s) => (s % e + e) % e;
43
- z.style.backgroundSize = `${e}px ${e}px`, z.style.backgroundPosition = `${d(a)}px ${d(c)}px`;
37
+ const e = t.getZoomLevel();
38
+ if (e > 0) {
39
+ const n = T * e / 2 ** Math.floor(Math.log2(e)), [u, o] = t.spaceToScreenPosition([0, 0]), a = (r) => (r % n + n) % n;
40
+ z.style.backgroundSize = `${n}px ${n}px`, z.style.backgroundPosition = `${a(u)}px ${a(o)}px`;
44
41
  }
45
42
  }
46
- const y = _.current, I = k.current;
47
- if (y && I !== null && g) {
48
- const n = u(I);
49
- if (n) {
50
- const [e, a] = t.spaceToScreenPosition(n);
51
- let c = E.current;
52
- c || (c = { width: y.offsetWidth, height: y.offsetHeight }, E.current = c);
53
- const d = a - C - c.height, s = a + C, h = d >= w ? d : s;
54
- y.style.transform = `translate(${Math.round(
55
- F(e - c.width / 2, w, g.width - c.width - w)
56
- )}px, ${Math.round(F(h, w, g.height - c.height - w))}px)`, y.style.opacity = "1";
43
+ const x = _.current, j = B.current;
44
+ if (x && j !== null && p) {
45
+ const e = D.current;
46
+ if (e) {
47
+ const [n, u] = t.spaceToScreenPosition(e);
48
+ let o = k.current;
49
+ o || (o = { width: x.offsetWidth, height: x.offsetHeight }, k.current = o);
50
+ const a = u - C - o.height, r = u + C, l = a >= m ? a : r;
51
+ x.style.transform = `translate(${Math.round(
52
+ F(n - o.width / 2, m, p.width - o.width - m)
53
+ )}px, ${Math.round(F(l, m, p.height - o.height - m))}px)`, x.style.opacity = "1";
57
54
  }
58
55
  }
59
- }, [l, f]);
56
+ }, [i]);
60
57
  S(() => {
61
- const t = O.current;
58
+ const t = y.current;
62
59
  if (!t) return;
63
- const o = new ResizeObserver(([i]) => {
64
- const u = i == null ? void 0 : i.contentRect;
65
- u && (B.current = { width: u.width, height: u.height });
60
+ const s = new ResizeObserver(([g]) => {
61
+ const d = g == null ? void 0 : g.contentRect;
62
+ d && (H.current = { width: d.width, height: d.height });
66
63
  });
67
- return o.observe(t), () => o.disconnect();
64
+ return s.observe(t), () => s.disconnect();
68
65
  }, []), S(() => {
69
- const t = $.current;
66
+ const t = R.current;
70
67
  t && (t.style.backgroundSize = `${T}px ${T}px`);
71
68
  }, []);
72
- const q = x(() => {
73
- p.current || (p.current = requestAnimationFrame(() => {
74
- p.current = 0, D();
69
+ const E = h(() => {
70
+ f.current || (f.current = requestAnimationFrame(() => {
71
+ f.current = 0, I();
75
72
  }));
76
- }, [D]);
73
+ }, [I]);
77
74
  S(
78
75
  () => () => {
79
- p.current && cancelAnimationFrame(p.current), p.current = 0;
76
+ f.current && cancelAnimationFrame(f.current), f.current = 0;
80
77
  },
81
78
  []
82
79
  );
83
- const Z = x(
84
- (t) => (o) => {
85
- o ? A.current.set(t, o) : A.current.delete(t);
80
+ const O = h(
81
+ (t) => (s) => {
82
+ s ? P.current.set(t, s) : P.current.delete(t);
86
83
  },
87
84
  []
88
- ), j = x((t) => {
89
- m.current = t, L.current.clear();
90
- }, []), J = x((t) => {
91
- k.current = t, E.current = null;
92
- }, []);
93
- return { hostRef: O, gridRef: $, cardRef: _, labelRef: Z, setLabelOrder: j, setHovered: J, track: W, schedule: q };
85
+ ), W = h((t) => {
86
+ L.current = t, $.current.clear();
87
+ }, []), q = h((t) => {
88
+ B.current = t, k.current = null;
89
+ }, []), Z = h(
90
+ (t) => {
91
+ D.current = t, E();
92
+ },
93
+ [E]
94
+ );
95
+ return { hostRef: y, gridRef: R, cardRef: _, labelRef: O, setLabelOrder: W, setHovered: q, hoverAt: Z, track: N, schedule: E };
94
96
  }
95
97
  export {
96
- tt as useOverlays
98
+ X as useOverlays
97
99
  };
98
100
  //# sourceMappingURL=overlays.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"overlays.js","sources":["../../src/parts/overlays.ts"],"sourcesContent":["\"use client\";\n\nimport { useCallback, useEffect, useRef } from \"react\";\nimport type { Graph } from \"@cosmos.gl/graph\";\nimport type { Resident, VertexId } from \"../core/resident\";\nimport { whenReady } from \"../render/when-ready\";\n\n/**\n * Everything that floats over the canvas and has to keep up with it: the hub labels, the hover\n * card, and the grid's lock to the graph's own space.\n *\n * One rAF for all three, because they answer the same question — *where is the camera now* — and\n * three independent loops would read the same transform three times a frame. React never runs: the\n * overlays move by imperative style writes, and a re-render per frame would be a re-render per\n * frame.\n *\n * **An overlay is attached to a vertex, not to a slot.** Everything here outlives an answer — a\n * label element is kept across renders, a hover survives a query — so the tracked set is identities\n * and the buffer index is resolved through `Resident` at the moment of painting. Held as indices, a\n * label would keep its position and change which node it was naming the first time the resident set\n * moved, with the text and the dot disagreeing and nothing raised.\n *\n * This lived inside the canvas component among seven other concerns, and that is not a filing\n * detail: the scheduler below once kept a cancelled `requestAnimationFrame` handle in `frame`,\n * which silently disabled every overlay for the life of the page. It took a long time to find in a\n * 700-line component and would have been obvious here.\n */\n\n/**\n * Dot spacing at zoom 1. The painter keeps the on-screen spacing inside [GRID, 2·GRID).\n *\n * **Not on the barrel any more.** Both call sites that imported it wrote the same line —\n * `backgroundSize: \\`${GRID}px ${GRID}px\\`` — as the *initial* value of a style `paint` overwrites\n * on its first frame. So the number was public to spell a value this hook was about to replace, and\n * the effect below writes it instead: the element the hook owns is seeded by the hook that owns it.\n */\nconst GRID = 22;\n\n/** How far the hover card clears its node, and the margin it keeps from the canvas edge. */\nconst CARD_GAP = 14;\nconst CARD_EDGE = 8;\n\n/** How far a label floats above its node, how tall its box is, and the air it demands around it. */\nconst LABEL_LIFT = 10;\nconst LABEL_HEIGHT = 12;\nconst LABEL_GAP = 3;\n\nconst clamp = (value: number, low: number, high: number) =>\n Math.min(Math.max(value, low), Math.max(low, high));\n\nexport interface GraphOverlays {\n /** The box the overlays are positioned within — the canvas' own bounds. */\n hostRef: React.RefObject<HTMLDivElement | null>;\n gridRef: React.RefObject<HTMLDivElement | null>;\n cardRef: React.RefObject<HTMLDivElement | null>;\n /** A `ref` callback for a given vertex's label. */\n labelRef: (vertex: VertexId) => (element: HTMLElement | null) => void;\n /** The labelled vertices, in the order the declutter pass should place them. */\n setLabelOrder: (vertices: VertexId[]) => void;\n setHovered: (vertex: VertexId | null) => void;\n /**\n * Re-register the tracked points with cosmos.gl.\n *\n * Call it after the graph exists and whenever the set of overlaid nodes changes. It has to be\n * driven from outside because effects run in declaration order, so this hook's own effects cannot\n * see a graph that a later hook is about to construct — and that construction is exactly what\n * clears the registration.\n */\n track: () => void;\n /** Ask for a repaint. Coalesced — many calls in a frame cost one. */\n schedule: () => void;\n}\n\n/**\n * The labels, the hover card and the grid, positioned from the renderer every frame: points are\n * tracked by index through the resident map, and every overlay is held by identity.\n */\nexport function useOverlays(api: { getGraph: () => Graph | null; getResident: () => Resident }): GraphOverlays {\n // Both are built once by `useGraph` and are stable for the life of the component, which is what\n // makes them safe to name in the dependency arrays below.\n const { getGraph, getResident } = api;\n const hostRef = useRef<HTMLDivElement>(null);\n const gridRef = useRef<HTMLDivElement>(null);\n const cardRef = useRef<HTMLDivElement>(null);\n const labelEls = useRef(new Map<VertexId, HTMLElement>());\n /** Label widths, measured once each — reading `offsetWidth` every frame would force layout. */\n const widths = useRef(new Map<VertexId, number>());\n const order = useRef<VertexId[]>([]);\n const hoveredRef = useRef<VertexId | null>(null);\n /** The card's box, measured once per hover, for the same reason. */\n const cardSize = useRef<{ width: number; height: number } | null>(null);\n /** The canvas' own box, kept by a `ResizeObserver` — see the effect below. */\n const box = useRef<{ width: number; height: number } | null>(null);\n const frame = useRef(0);\n\n /**\n * Tell cosmos.gl which points the overlays are watching: the labelled ones, plus the hovered one.\n *\n * Registration is *not* self-maintaining. `Points.updatePositions()` ends in an argument-less\n * `trackPointsByIndices()` that clears it, and that runs whenever `isPointPositionsUpdateNeeded`\n * is set — which only `setPointPositions` does. So the registration survives every look change and\n * every slider, and is lost exactly once per graph: at construction, on the `render()` that\n * follows `setPointPositions`. Hence a caller-driven re-register rather than a one-shot.\n */\n const track = useCallback(() => {\n const graph = getGraph();\n if (!graph) return;\n const hovered = hoveredRef.current;\n const watched =\n hovered === null || order.current.includes(hovered)\n ? order.current\n : [...order.current, hovered];\n // Registration is a device call like any other, and this one is *only* reached before the\n // device in the case that matters: the canvas registers its labels the moment the first\n // composition lands, which is the same commit the graph is still being built in. Dropped there, the\n // tracked map stays empty and every overlay sits at `opacity: 0` for ever.\n whenReady(graph, (ready) => ready.trackPointPositionsByIndices(getResident().indicesOf(watched)));\n }, [getGraph, getResident]);\n\n const paint = useCallback(() => {\n const graph = getGraph();\n if (!graph) return;\n const resident = getResident();\n /**\n * Positions come from the tracking API, not from `getPointPositions()`.\n *\n * The difference is what gets read back per frame. `getPointPositions()` is a synchronous\n * `readPixels` of the *whole* position framebuffer — 10,000 bytes at this corpus size, plus an\n * O(n) array build — on every animation frame the simulation runs. Tracking reads a\n * `ceil(√k)²` texture for the k points that actually carry an overlay: 576 bytes for Atlas'\n * 26 labels and a hovered node. It also caches while the simulation is stopped, so a settled\n * graph costs no readback at all until something moves.\n */\n // Read lazily: with labels off and nothing hovered, the only overlay left is the grid, which\n // needs the transform and not the points.\n let positions: ReadonlyMap<number, [number, number]> | null = null;\n /**\n * Where a vertex is on screen, or `null` when it is not drawn at all.\n *\n * Two ways to be absent and they are one answer here: not resident — the query moved on and this\n * vertex is not in the current buffers — or resident and not yet tracked. Both mean *do not draw\n * an overlay for it*, and the alternative to asking is drawing it at whatever the stale index now\n * holds, which is a label on the wrong node.\n */\n const at = (vertex: VertexId): [number, number] | null => {\n const index = resident.indexOf(vertex);\n if (index === undefined) return null;\n positions ??= graph.getTrackedPointPositionsMap();\n return positions.get(index) ?? null;\n };\n\n // Placed boxes, in importance order. A label that would land on one already down is dropped\n // rather than drawn over it — an unreadable pile of overlapping names is worse than a sparser\n // set of legible ones.\n const placed: [number, number, number, number][] = [];\n const bounds = box.current;\n for (const vertex of order.current) {\n const element = labelEls.current.get(vertex);\n if (!element) continue;\n const point = at(vertex);\n if (!point) {\n element.style.opacity = \"0\";\n continue;\n }\n const [x, y] = graph.spaceToScreenPosition(point);\n let width = widths.current.get(vertex);\n if (width === undefined) {\n width = element.offsetWidth;\n widths.current.set(vertex, width);\n }\n const x1 = x - width / 2;\n const x2 = x1 + width;\n const y2 = y - LABEL_LIFT;\n const y1 = y2 - LABEL_HEIGHT;\n const offscreen =\n bounds !== null && (x2 < 0 || y2 < 0 || x1 > bounds.width || y1 > bounds.height);\n const collides = placed.some((r) => x1 < r[2] && x2 > r[0] && y1 < r[3] && y2 > r[1]);\n if (offscreen || collides) {\n element.style.opacity = \"0\";\n continue;\n }\n placed.push([x1 - LABEL_GAP, y1 - LABEL_GAP, x2 + LABEL_GAP, y2 + LABEL_GAP]);\n // Positioned at the box that was just tested, in pixels. The previous version measured a\n // rectangle here and then drew the label somewhere else — `translate(-50%, -160%)` offsets by\n // percentages of the element's own size, so the collision box sat about nine pixels below the\n // text it was meant to protect and neighbouring labels overlapped anyway.\n element.style.transform = `translate(${Math.round(x1)}px, ${Math.round(y1)}px)`;\n element.style.opacity = \"1\";\n }\n // The grid belongs to the graph's space, not to the viewport: it slides with a pan and\n // subdivides on zoom, so the spacing on screen never leaves [GRID, 2·GRID). Without that a\n // fixed grid reads as wallpaper and the canvas stops feeling like somewhere you can move.\n const grid = gridRef.current;\n if (grid) {\n const k = graph.getZoomLevel();\n if (k > 0) {\n const step = (GRID * k) / 2 ** Math.floor(Math.log2(k));\n const [ox, oy] = graph.spaceToScreenPosition([0, 0]);\n const wrap = (v: number) => ((v % step) + step) % step;\n grid.style.backgroundSize = `${step}px ${step}px`;\n grid.style.backgroundPosition = `${wrap(ox)}px ${wrap(oy)}px`;\n }\n }\n\n // The card sits above the node, flips below when the top runs out, and is held inside the\n // canvas on both axes. It used to be centred with percentage transforms, which cannot know\n // about an edge — and since this layer clips, a node near a border showed half a tooltip.\n const card = cardRef.current;\n const hovered = hoveredRef.current;\n if (card && hovered !== null && bounds) {\n const point = at(hovered);\n if (point) {\n const [x, y] = graph.spaceToScreenPosition(point);\n let size = cardSize.current;\n if (!size) {\n size = { width: card.offsetWidth, height: card.offsetHeight };\n cardSize.current = size;\n }\n const above = y - CARD_GAP - size.height;\n const below = y + CARD_GAP;\n const top = above >= CARD_EDGE ? above : below;\n card.style.transform = `translate(${Math.round(\n clamp(x - size.width / 2, CARD_EDGE, bounds.width - size.width - CARD_EDGE),\n )}px, ${Math.round(clamp(top, CARD_EDGE, bounds.height - size.height - CARD_EDGE))}px)`;\n card.style.opacity = \"1\";\n }\n }\n }, [getGraph, getResident]);\n\n // The canvas' box, measured when it changes rather than when it is read. `getBoundingClientRect()`\n // inside `paint` was one forced layout per animation frame, in a painter that caches `offsetWidth`\n // for exactly that reason.\n useEffect(() => {\n const host = hostRef.current;\n if (!host) return;\n const observer = new ResizeObserver(([entry]) => {\n const size = entry?.contentRect;\n if (size) box.current = { width: size.width, height: size.height };\n });\n observer.observe(host);\n return () => observer.disconnect();\n }, []);\n\n /**\n * The grid's spacing at rest, written before the camera has said anything.\n *\n * `paint` sets this every frame from the live zoom, but only once there is a graph and a zoom to\n * read — and the element is mounted well before that. The two hosts that drew a grid were each\n * writing this same line inline off an exported `GRID`, which is a constant published so a call\n * site could spell the value this hook was about to overwrite. Seeding it here is the same picture\n * with the number staying where the painter that maintains it lives.\n *\n * The `backgroundImage` is not seeded: what the dots are made of is the host's decision — the\n * border colour, a gradient, whatever the surface wants — and only the *spacing* has to agree with\n * the camera.\n */\n useEffect(() => {\n const grid = gridRef.current;\n if (grid) grid.style.backgroundSize = `${GRID}px ${GRID}px`;\n }, []);\n\n const schedule = useCallback(() => {\n if (frame.current) return;\n frame.current = requestAnimationFrame(() => {\n frame.current = 0;\n paint();\n });\n }, [paint]);\n\n useEffect(\n () => () => {\n if (frame.current) cancelAnimationFrame(frame.current);\n // Clearing the handle is the whole point of this cleanup, not the cancel. `frame` doubles as\n // the \"a paint is already queued\" flag, and StrictMode runs setup → cleanup → setup on the\n // same instance, so the refs survive. Leaving a cancelled handle behind made every later\n // `schedule()` believe a frame was still pending and return early — permanently.\n frame.current = 0;\n },\n [],\n );\n\n const labelRef = useCallback(\n (vertex: VertexId) => (element: HTMLElement | null) => {\n if (element) labelEls.current.set(vertex, element);\n else labelEls.current.delete(vertex);\n },\n [],\n );\n\n const setLabelOrder = useCallback((vertices: VertexId[]) => {\n order.current = vertices;\n widths.current.clear();\n }, []);\n\n const setHovered = useCallback((vertex: VertexId | null) => {\n hoveredRef.current = vertex;\n cardSize.current = null;\n }, []);\n\n return { hostRef, gridRef, cardRef, labelRef, setLabelOrder, setHovered, track, schedule };\n}\n"],"names":[],"mappings":";;;AAoCA;AAyCO;AAGL;AAyBE;AACA;AACA;AASA;AAAgG;AAIhG;AACA;AACA;AAaA;AASA;AACE;AACA;AAE+B;AAQjC;AACE;AACA;AACA;AACA;AACE;AACA;AAAA;AAEF;AACA;AACA;AAIA;AAOA;AACE;AACA;AAAA;AAEF;AAMwB;AAK1B;AACA;AACE;AACA;AACE;AAGA;AACyD;AAC3D;AAMF;AAEA;AACE;AACA;AACE;AACA;AACA;AAIA;AAGA;AAAyC;AACmC;AAEvD;AACvB;AACF;AAMF;AACE;AACA;AACA;AACE;AACA;AAA0D;AAE5D;AACsB;AAiBtB;AACA;AAAuD;AAGzD;AACE;AAEE;AACA;AACD;AAGH;AAAA;AAEI;AAKgB;AAClB;AACA;AAGF;AAAiB;AAEb;AACmC;AACrC;AACA;AAIA;AACe;AAIf;AACmB;AAGrB;AACF;;;;"}
1
+ {"version":3,"file":"overlays.js","sources":["../../src/parts/overlays.ts"],"sourcesContent":["\"use client\";\n\nimport { useCallback, useEffect, useRef } from \"react\";\nimport type { Graph } from \"@cosmos.gl/graph\";\nimport type { VertexId } from \"../core/types\";\n\n/**\n * Everything that floats over the canvas and has to keep up with it: the hub labels, the hover\n * card, and the grid's lock to the graph's own space.\n *\n * One rAF for all three, because they answer the same question — *where is the camera now* — and\n * three independent loops would read the same transform three times a frame. React never runs: the\n * overlays move by imperative style writes, and a re-render per frame would be a re-render per\n * frame.\n *\n * **An overlay is attached to a vertex**, and a vertex's id is its index in the buffers, so the\n * tracked set is the ids themselves. A vertex the page's filter hides has no position, and no overlay.\n *\n * This lived inside the canvas component among seven other concerns, and that is not a filing\n * detail: the scheduler below once kept a cancelled `requestAnimationFrame` handle in `frame`,\n * which silently disabled every overlay for the life of the page. It took a long time to find in a\n * 700-line component and would have been obvious here.\n */\n\n/**\n * Dot spacing at zoom 1. The painter keeps the on-screen spacing inside [GRID, 2·GRID).\n *\n * **Not on the barrel any more.** Both call sites that imported it wrote the same line —\n * `backgroundSize: \\`${GRID}px ${GRID}px\\`` — as the *initial* value of a style `paint` overwrites\n * on its first frame. So the number was public to spell a value this hook was about to replace, and\n * the effect below writes it instead: the element the hook owns is seeded by the hook that owns it.\n */\nconst GRID = 22;\n\n/** How far the hover card clears its node, and the margin it keeps from the canvas edge. */\nconst CARD_GAP = 14;\nconst CARD_EDGE = 8;\n\n/** How far a label floats above its node, how tall its box is, and the air it demands around it. */\nconst LABEL_LIFT = 10;\nconst LABEL_HEIGHT = 12;\nconst LABEL_GAP = 3;\n\nconst clamp = (value: number, low: number, high: number) =>\n Math.min(Math.max(value, low), Math.max(low, high));\n\nexport interface GraphOverlays {\n /** The box the overlays are positioned within — the canvas' own bounds. */\n hostRef: React.RefObject<HTMLDivElement | null>;\n gridRef: React.RefObject<HTMLDivElement | null>;\n cardRef: React.RefObject<HTMLDivElement | null>;\n /** A `ref` callback for a given vertex's label. */\n labelRef: (vertex: VertexId) => (element: HTMLElement | null) => void;\n /** The labelled vertices, in the order the declutter pass should place them. */\n setLabelOrder: (vertices: VertexId[]) => void;\n /** The vertex the card names; its box is measured again on the next paint. */\n setHovered: (vertex: VertexId | null) => void;\n /** Where the hovered point is, in space — from the renderer, never read back from the GPU. */\n hoverAt: (position: [number, number] | null) => void;\n /**\n * Re-register the labelled points with cosmos.gl.\n *\n * Call it after the graph exists and whenever the set of overlaid nodes changes. It has to be\n * driven from outside because effects run in declaration order, so this hook's own effects cannot\n * see a graph that a later hook is about to construct — and that construction is exactly what\n * clears the registration.\n */\n track: () => void;\n /** Ask for a repaint. Coalesced — many calls in a frame cost one. */\n schedule: () => void;\n}\n\n/**\n * The labels, the hover card and the grid, positioned from the renderer every frame.\n */\nexport function useOverlays(api: { getGraph: () => Graph | null }): GraphOverlays {\n // Built once by the canvas and stable for its life, which is what makes it safe to name in the\n // dependency arrays below.\n const { getGraph } = api;\n const hostRef = useRef<HTMLDivElement>(null);\n const gridRef = useRef<HTMLDivElement>(null);\n const cardRef = useRef<HTMLDivElement>(null);\n const labelEls = useRef(new Map<VertexId, HTMLElement>());\n /** Label widths, measured once each — reading `offsetWidth` every frame would force layout. */\n const widths = useRef(new Map<VertexId, number>());\n const order = useRef<VertexId[]>([]);\n const hoveredRef = useRef<VertexId | null>(null);\n const hoveredAt = useRef<[number, number] | null>(null);\n /** The card's box, measured once per hover, for the same reason. */\n const cardSize = useRef<{ width: number; height: number } | null>(null);\n /** The canvas' own box, kept by a `ResizeObserver` — see the effect below. */\n const box = useRef<{ width: number; height: number } | null>(null);\n const frame = useRef(0);\n\n /**\n * Tell cosmos.gl which points the labels are watching. The hovered one is not among them: a change\n * of tracked set costs a readback, and the renderer already reports where the hovered point is.\n *\n * Registration is *not* self-maintaining. `Points.updatePositions()` ends in an argument-less\n * `trackPointsByIndices()` that clears it, and that runs whenever `isPointPositionsUpdateNeeded`\n * is set — which only `setPointPositions` does. So the registration survives every look change and\n * every slider, and is lost exactly once per graph: at construction, on the `render()` that\n * follows `setPointPositions`. Hence a caller-driven re-register rather than a one-shot.\n */\n const track = useCallback(() => {\n const graph = getGraph();\n if (!graph) return;\n graph.trackPointPositionsByIndices(order.current);\n }, [getGraph]);\n\n const paint = useCallback(() => {\n const graph = getGraph();\n if (!graph) return;\n /**\n * Positions come from the tracking API, not from `getPointPositions()`.\n *\n * The difference is what gets read back per frame. `getPointPositions()` is a synchronous\n * `readPixels` of the *whole* position framebuffer — 10,000 bytes at this corpus size, plus an\n * O(n) array build — on every animation frame the simulation runs. Tracking reads a\n * `ceil(√k)²` texture for the k points that actually carry an overlay: 576 bytes for Atlas'\n * 26 labels and a hovered node. It also caches while the simulation is stopped, so a settled\n * graph costs no readback at all until something moves.\n */\n // Read lazily: with labels off and nothing hovered, the only overlay left is the grid, which\n // needs the transform and not the points.\n let positions: ReadonlyMap<number, [number, number]> | null = null;\n /** Where a vertex is in space, or `null` when it is not tracked yet or has no position. */\n const at = (vertex: VertexId): [number, number] | null => {\n positions ??= graph.getTrackedPointPositionsMap();\n const point = positions.get(vertex);\n return point && !Number.isNaN(point[0]) ? point : null;\n };\n\n // Placed boxes, in importance order. A label that would land on one already down is dropped\n // rather than drawn over it — an unreadable pile of overlapping names is worse than a sparser\n // set of legible ones.\n const placed: [number, number, number, number][] = [];\n const bounds = box.current;\n for (const vertex of order.current) {\n const element = labelEls.current.get(vertex);\n if (!element) continue;\n const point = at(vertex);\n if (!point) {\n element.style.opacity = \"0\";\n continue;\n }\n const [x, y] = graph.spaceToScreenPosition(point);\n let width = widths.current.get(vertex);\n if (width === undefined) {\n width = element.offsetWidth;\n widths.current.set(vertex, width);\n }\n const x1 = x - width / 2;\n const x2 = x1 + width;\n const y2 = y - LABEL_LIFT;\n const y1 = y2 - LABEL_HEIGHT;\n const offscreen =\n bounds !== null && (x2 < 0 || y2 < 0 || x1 > bounds.width || y1 > bounds.height);\n const collides = placed.some((r) => x1 < r[2] && x2 > r[0] && y1 < r[3] && y2 > r[1]);\n if (offscreen || collides) {\n element.style.opacity = \"0\";\n continue;\n }\n placed.push([x1 - LABEL_GAP, y1 - LABEL_GAP, x2 + LABEL_GAP, y2 + LABEL_GAP]);\n // Positioned at the box that was just tested, in pixels. The previous version measured a\n // rectangle here and then drew the label somewhere else — `translate(-50%, -160%)` offsets by\n // percentages of the element's own size, so the collision box sat about nine pixels below the\n // text it was meant to protect and neighbouring labels overlapped anyway.\n element.style.transform = `translate(${Math.round(x1)}px, ${Math.round(y1)}px)`;\n element.style.opacity = \"1\";\n }\n // The grid belongs to the graph's space, not to the viewport: it slides with a pan and\n // subdivides on zoom, so the spacing on screen never leaves [GRID, 2·GRID). Without that a\n // fixed grid reads as wallpaper and the canvas stops feeling like somewhere you can move.\n const grid = gridRef.current;\n if (grid) {\n const k = graph.getZoomLevel();\n if (k > 0) {\n const step = (GRID * k) / 2 ** Math.floor(Math.log2(k));\n const [ox, oy] = graph.spaceToScreenPosition([0, 0]);\n const wrap = (v: number) => ((v % step) + step) % step;\n grid.style.backgroundSize = `${step}px ${step}px`;\n grid.style.backgroundPosition = `${wrap(ox)}px ${wrap(oy)}px`;\n }\n }\n\n // The card sits above the node, flips below when the top runs out, and is held inside the\n // canvas on both axes. It used to be centred with percentage transforms, which cannot know\n // about an edge — and since this layer clips, a node near a border showed half a tooltip.\n const card = cardRef.current;\n const hovered = hoveredRef.current;\n if (card && hovered !== null && bounds) {\n const point = hoveredAt.current;\n if (point) {\n const [x, y] = graph.spaceToScreenPosition(point);\n let size = cardSize.current;\n if (!size) {\n size = { width: card.offsetWidth, height: card.offsetHeight };\n cardSize.current = size;\n }\n const above = y - CARD_GAP - size.height;\n const below = y + CARD_GAP;\n const top = above >= CARD_EDGE ? above : below;\n card.style.transform = `translate(${Math.round(\n clamp(x - size.width / 2, CARD_EDGE, bounds.width - size.width - CARD_EDGE),\n )}px, ${Math.round(clamp(top, CARD_EDGE, bounds.height - size.height - CARD_EDGE))}px)`;\n card.style.opacity = \"1\";\n }\n }\n }, [getGraph]);\n\n // The canvas' box, measured when it changes rather than when it is read. `getBoundingClientRect()`\n // inside `paint` was one forced layout per animation frame, in a painter that caches `offsetWidth`\n // for exactly that reason.\n useEffect(() => {\n const host = hostRef.current;\n if (!host) return;\n const observer = new ResizeObserver(([entry]) => {\n const size = entry?.contentRect;\n if (size) box.current = { width: size.width, height: size.height };\n });\n observer.observe(host);\n return () => observer.disconnect();\n }, []);\n\n /**\n * The grid's spacing at rest, written before the camera has said anything.\n *\n * `paint` sets this every frame from the live zoom, but only once there is a graph and a zoom to\n * read — and the element is mounted well before that. The two hosts that drew a grid were each\n * writing this same line inline off an exported `GRID`, which is a constant published so a call\n * site could spell the value this hook was about to overwrite. Seeding it here is the same picture\n * with the number staying where the painter that maintains it lives.\n *\n * The `backgroundImage` is not seeded: what the dots are made of is the host's decision — the\n * border colour, a gradient, whatever the surface wants — and only the *spacing* has to agree with\n * the camera.\n */\n useEffect(() => {\n const grid = gridRef.current;\n if (grid) grid.style.backgroundSize = `${GRID}px ${GRID}px`;\n }, []);\n\n const schedule = useCallback(() => {\n if (frame.current) return;\n frame.current = requestAnimationFrame(() => {\n frame.current = 0;\n paint();\n });\n }, [paint]);\n\n useEffect(\n () => () => {\n if (frame.current) cancelAnimationFrame(frame.current);\n // Clearing the handle is the whole point of this cleanup, not the cancel. `frame` doubles as\n // the \"a paint is already queued\" flag, and StrictMode runs setup → cleanup → setup on the\n // same instance, so the refs survive. Leaving a cancelled handle behind made every later\n // `schedule()` believe a frame was still pending and return early — permanently.\n frame.current = 0;\n },\n [],\n );\n\n const labelRef = useCallback(\n (vertex: VertexId) => (element: HTMLElement | null) => {\n if (element) labelEls.current.set(vertex, element);\n else labelEls.current.delete(vertex);\n },\n [],\n );\n\n const setLabelOrder = useCallback((vertices: VertexId[]) => {\n order.current = vertices;\n widths.current.clear();\n }, []);\n\n const setHovered = useCallback((vertex: VertexId | null) => {\n hoveredRef.current = vertex;\n cardSize.current = null;\n }, []);\n\n const hoverAt = useCallback(\n (position: [number, number] | null) => {\n hoveredAt.current = position;\n schedule();\n },\n [schedule],\n );\n\n return { hostRef, gridRef, cardRef, labelRef, setLabelOrder, setHovered, hoverAt, track, schedule };\n}\n"],"names":[],"mappings":";;AAgCA;AA2CO;AAGL;AA2BE;AACA;AACgD;AAIhD;AACA;AAaA;AAEA;AACE;AACA;AACA;AAAkD;AAQpD;AACE;AACA;AACA;AACA;AACE;AACA;AAAA;AAEF;AACA;AACA;AAIA;AAOA;AACE;AACA;AAAA;AAEF;AAMwB;AAK1B;AACA;AACE;AACA;AACE;AAGA;AACyD;AAC3D;AAMF;AAEA;AACE;AACA;AACE;AACA;AACA;AAIA;AAGA;AAAyC;AACmC;AAEvD;AACvB;AACF;AAMF;AACE;AACA;AACA;AACE;AACA;AAA0D;AAE5D;AACsB;AAiBtB;AACA;AAAuD;AAGzD;AACE;AAEE;AACA;AACD;AAGH;AAAA;AAEI;AAKgB;AAClB;AACA;AAGF;AAAiB;AAEb;AACmC;AACrC;AACA;AAIA;AACe;AAIf;AACmB;AAGL;AAEZ;AACA;AACF;AACS;AAGX;AACF;;;;"}
@@ -1,11 +1,13 @@
1
- import { GraphState } from '../core/store';
1
+ import { GraphSnapshot, GraphState } from '../core/store';
2
2
  /**
3
3
  * **A slice of the graph's state, and a render only when that slice moves** — TanStack Store's
4
4
  * `useStore(store, selector)`, over the root in context. The state changes at frame rate — a hover, a
5
- * tile, a tick of the layout — so reading all of it would re-render a toolbar on every hover.
5
+ * load, a tick of the layout — so reading all of it would re-render a toolbar on every hover.
6
6
  *
7
7
  * The selection is kept while the snapshot is the same object, and while `isEqual` says the new slice
8
8
  * equals the last one, so a selector may build an object as long as it passes a comparison for it.
9
9
  */
10
10
  export declare function useGraphState<T>(selector: (state: GraphState) => T, isEqual?: (a: T, b: T) => boolean): T;
11
+ /** The parts' selector over what only they and the renderer read. Not on the barrel. */
12
+ export declare function useGraphSnapshot<T>(selector: (state: GraphSnapshot) => T): T;
11
13
  //# sourceMappingURL=use-graph-state.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-graph-state.d.ts","sourceRoot":"","sources":["../../src/react/use-graph-state.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAGhD;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,CAAC,EAAE,OAAO,GAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,KAAK,OAAmB,GAAG,CAAC,CAgBpH"}
1
+ {"version":3,"file":"use-graph-state.d.ts","sourceRoot":"","sources":["../../src/react/use-graph-state.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAG/D;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,CAAC,EAAE,OAAO,GAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,KAAK,OAAmB,GAAG,CAAC,CAgBpH;AAED,wFAAwF;AACxF,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,CAAC,GAAG,CAAC,CAE5E"}
@@ -1,16 +1,20 @@
1
1
  "use client";
2
2
  import { useRef as c, useSyncExternalStore as l } from "react";
3
3
  import { useGraphContext as i } from "./graph-root.js";
4
- function p(s, o = Object.is) {
5
- const n = i(), r = c(null), a = () => {
6
- const e = n.getState(), t = r.current;
4
+ function p(r, n = Object.is) {
5
+ const s = i(), u = c(null), o = () => {
6
+ const e = s.getState(), t = u.current;
7
7
  if (t && t.state === e) return t.value;
8
- const u = s(e);
9
- return t && o(t.value, u) ? (r.current = { state: e, value: t.value }, t.value) : (r.current = { state: e, value: u }, u);
8
+ const a = r(e);
9
+ return t && n(t.value, a) ? (u.current = { state: e, value: t.value }, t.value) : (u.current = { state: e, value: a }, a);
10
10
  };
11
- return l(n.subscribe, a, a);
11
+ return l(s.subscribe, o, o);
12
+ }
13
+ function v(r) {
14
+ return p((n) => r(n));
12
15
  }
13
16
  export {
17
+ v as useGraphSnapshot,
14
18
  p as useGraphState
15
19
  };
16
20
  //# sourceMappingURL=use-graph-state.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-graph-state.js","sources":["../../src/react/use-graph-state.ts"],"sourcesContent":["\"use client\";\n\nimport { useRef, useSyncExternalStore } from \"react\";\nimport type { GraphState } from \"../core/store\";\nimport { useGraphContext } from \"./graph-root\";\n\n/**\n * **A slice of the graph's state, and a render only when that slice moves** — TanStack Store's\n * `useStore(store, selector)`, over the root in context. The state changes at frame rate — a hover, a\n * tile, a tick of the layout — so reading all of it would re-render a toolbar on every hover.\n *\n * The selection is kept while the snapshot is the same object, and while `isEqual` says the new slice\n * equals the last one, so a selector may build an object as long as it passes a comparison for it.\n */\nexport function useGraphState<T>(selector: (state: GraphState) => T, isEqual: (a: T, b: T) => boolean = Object.is): T {\n const api = useGraphContext();\n const memo = useRef<{ state: GraphState; value: T } | null>(null);\n const read = () => {\n const state = api.getState();\n const last = memo.current;\n if (last && last.state === state) return last.value;\n const value = selector(state);\n if (last && isEqual(last.value, value)) {\n memo.current = { state, value: last.value };\n return last.value;\n }\n memo.current = { state, value };\n return value;\n };\n return useSyncExternalStore(api.subscribe, read, read);\n}\n"],"names":[],"mappings":";;;AAcO;AACL;AAGE;AAEA;AACA;AACA;AAKO;AAET;AACF;;;;"}
1
+ {"version":3,"file":"use-graph-state.js","sources":["../../src/react/use-graph-state.ts"],"sourcesContent":["\"use client\";\n\nimport { useRef, useSyncExternalStore } from \"react\";\nimport type { GraphSnapshot, GraphState } from \"../core/store\";\nimport { useGraphContext } from \"./graph-root\";\n\n/**\n * **A slice of the graph's state, and a render only when that slice moves** — TanStack Store's\n * `useStore(store, selector)`, over the root in context. The state changes at frame rate — a hover, a\n * load, a tick of the layout — so reading all of it would re-render a toolbar on every hover.\n *\n * The selection is kept while the snapshot is the same object, and while `isEqual` says the new slice\n * equals the last one, so a selector may build an object as long as it passes a comparison for it.\n */\nexport function useGraphState<T>(selector: (state: GraphState) => T, isEqual: (a: T, b: T) => boolean = Object.is): T {\n const api = useGraphContext();\n const memo = useRef<{ state: GraphState; value: T } | null>(null);\n const read = () => {\n const state = api.getState();\n const last = memo.current;\n if (last && last.state === state) return last.value;\n const value = selector(state);\n if (last && isEqual(last.value, value)) {\n memo.current = { state, value: last.value };\n return last.value;\n }\n memo.current = { state, value };\n return value;\n };\n return useSyncExternalStore(api.subscribe, read, read);\n}\n\n/** The parts' selector over what only they and the renderer read. Not on the barrel. */\nexport function useGraphSnapshot<T>(selector: (state: GraphSnapshot) => T): T {\n return useGraphState((state) => selector(state as GraphSnapshot));\n}\n"],"names":[],"mappings":";;;AAcO;AACL;AAGE;AAEA;AACA;AACA;AAKO;AAET;AACF;AAGO;AACL;AACF;;;;;"}
@@ -1,6 +1,5 @@
1
- import { Resident, VertexId } from '../core/resident';
2
1
  import { GraphOptions, GraphState, GraphStore } from '../core/store';
3
- import { GraphCommands, SelectionSource, Tool } from '../core/types';
2
+ import { GraphCommands, SelectionSource, Tool, VertexId } from '../core/types';
4
3
  import { Renderer, RendererEvents } from '../render/renderer';
5
4
  export type UseGraphProps = GraphOptions;
6
5
  /**
@@ -16,8 +15,6 @@ export interface GraphApi extends GraphCommands {
16
15
  subscribe(listener: () => void): () => void;
17
16
  /** The state now, for a callback; a render reads it with `useGraphState`. */
18
17
  getState(): GraphState;
19
- /** Who is drawn right now, to resolve a buffer index to an identity. */
20
- getResident(): Resident;
21
18
  }
22
19
  /** What the parts in this package reach and a host does not: the element and the renderer. */
23
20
  interface Internals {
@@ -1 +1 @@
1
- {"version":3,"file":"use-graph.d.ts","sourceRoot":"","sources":["../../src/react/use-graph.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,EAAe,KAAK,YAAY,EAAE,KAAK,UAAU,EAAE,KAAK,UAAU,EAAE,MAAM,eAAe,CAAC;AACjG,OAAO,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AAC1E,OAAO,EAAkB,KAAK,QAAQ,EAAE,KAAK,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAExF,MAAM,MAAM,aAAa,GAAG,YAAY,CAAC;AAEzC;;;;;GAKG;AACH,MAAM,WAAW,QAAS,SAAQ,aAAa;IAC7C,MAAM,CAAC,QAAQ,EAAE,SAAS,QAAQ,EAAE,GAAG,IAAI,EAAE,MAAM,CAAC,EAAE,eAAe,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7F,QAAQ,CAAC,MAAM,EAAE,QAAQ,GAAG,IAAI,GAAG,IAAI,CAAC;IACxC,OAAO,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,CAAC;IAC1B,SAAS,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC;IAC5C,6EAA6E;IAC7E,QAAQ,IAAI,UAAU,CAAC;IACvB,wEAAwE;IACxE,WAAW,IAAI,QAAQ,CAAC;CACzB;AAED,8FAA8F;AAC9F,UAAU,SAAS;IACjB,KAAK,EAAE,UAAU,CAAC;IAClB,MAAM,CAAC,IAAI,EAAE,cAAc,EAAE,MAAM,CAAC,EAAE,cAAc,GAAG,MAAM,IAAI,CAAC;IAClE,QAAQ,IAAI,QAAQ,GAAG,IAAI,CAAC;CAC7B;AAID,wFAAwF;AACxF,wBAAgB,WAAW,CAAC,GAAG,EAAE,QAAQ,GAAG,SAAS,CAIpD;AAED;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,aAAa,GAAG,QAAQ,CAyBvD"}
1
+ {"version":3,"file":"use-graph.d.ts","sourceRoot":"","sources":["../../src/react/use-graph.ts"],"names":[],"mappings":"AAGA,OAAO,EAAe,KAAK,YAAY,EAAE,KAAK,UAAU,EAAE,KAAK,UAAU,EAAE,MAAM,eAAe,CAAC;AACjG,OAAO,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACpF,OAAO,EAAkB,KAAK,QAAQ,EAAE,KAAK,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAExF,MAAM,MAAM,aAAa,GAAG,YAAY,CAAC;AAEzC;;;;;GAKG;AACH,MAAM,WAAW,QAAS,SAAQ,aAAa;IAC7C,MAAM,CAAC,QAAQ,EAAE,SAAS,QAAQ,EAAE,GAAG,IAAI,EAAE,MAAM,CAAC,EAAE,eAAe,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7F,QAAQ,CAAC,MAAM,EAAE,QAAQ,GAAG,IAAI,GAAG,IAAI,CAAC;IACxC,OAAO,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,CAAC;IAC1B,SAAS,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC;IAC5C,6EAA6E;IAC7E,QAAQ,IAAI,UAAU,CAAC;CACxB;AAED,8FAA8F;AAC9F,UAAU,SAAS;IACjB,KAAK,EAAE,UAAU,CAAC;IAClB,MAAM,CAAC,IAAI,EAAE,cAAc,EAAE,MAAM,CAAC,EAAE,cAAc,GAAG,MAAM,IAAI,CAAC;IAClE,QAAQ,IAAI,QAAQ,GAAG,IAAI,CAAC;CAC7B;AAID,wFAAwF;AACxF,wBAAgB,WAAW,CAAC,GAAG,EAAE,QAAQ,GAAG,SAAS,CAIpD;AAED;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,aAAa,GAAG,QAAQ,CAyBvD"}
@@ -1,87 +1,76 @@
1
1
  "use client";
2
- import { useRef as R, useCallback as k, useState as v, useEffect as l } from "react";
3
- import { createGraph as N } from "../core/store.js";
4
- import { createRenderer as z } from "../render/renderer.js";
2
+ import { useRef as k, useCallback as R, useState as B, useEffect as i } from "react";
3
+ import { createGraph as E } from "../core/store.js";
4
+ import { createRenderer as G } from "../render/renderer.js";
5
5
  const f = /* @__PURE__ */ new WeakMap();
6
- function E(n) {
6
+ function N(n) {
7
7
  const e = f.get(n);
8
8
  if (!e) throw new Error("a graph part was given an api useGraph did not build");
9
9
  return e;
10
10
  }
11
- function D(n) {
12
- const e = R(n);
11
+ function A(n) {
12
+ const e = k(n);
13
13
  e.current = n;
14
- const s = k(
14
+ const r = R(
15
15
  (F) => ({
16
16
  ...F,
17
- onFailure: (u) => e.current.onFailure(u),
18
- onSelect: (u) => {
19
- var r, a;
20
- return (a = (r = e.current).onSelect) == null ? void 0 : a.call(r, u);
17
+ onFailure: (s) => e.current.onFailure(s),
18
+ onSelect: (s) => {
19
+ var l, a;
20
+ return (a = (l = e.current).onSelect) == null ? void 0 : a.call(l, s);
21
21
  },
22
- onFocus: (u) => {
23
- var r, a;
24
- return (a = (r = e.current).onFocus) == null ? void 0 : a.call(r, u);
22
+ onFocus: (s) => {
23
+ var l, a;
24
+ return (a = (l = e.current).onFocus) == null ? void 0 : a.call(l, s);
25
25
  }
26
26
  }),
27
27
  []
28
- ), [i] = v(() => G(N(s(n)))), { store: t } = E(i), { categories: c, corpus: o, fill: m, filterBy: p, limit: d, look: b, r: S, sim: g, simulate: h, stroke: y, symbol: B, title: O, type: w } = n;
29
- return l(() => {
30
- t.setOptions(s(e.current));
31
- }, [t, s, c, o, m, p, d, b, S, g, h, y, B, O, w]), l(() => t.subscribe(() => {
32
- }), [t]), i;
28
+ ), [c] = B(() => T(E(r(n)))), { store: t } = N(c), { categories: u, corpus: o, fill: m, filterBy: p, look: b, r: d, sim: S, simulate: h, stroke: g, symbol: w, title: y } = n;
29
+ return i(() => {
30
+ t.setOptions(r(e.current));
31
+ }, [t, r, u, o, m, p, b, d, S, h, g, w, y]), i(() => t.subscribe(() => {
32
+ }), [t]), c;
33
33
  }
34
- function G(n) {
34
+ function T(n) {
35
35
  let e = null;
36
- const s = (t) => (...c) => {
36
+ const r = (t) => (...u) => {
37
37
  var o;
38
- return (o = e == null ? void 0 : e[t]) == null ? void 0 : o.call(e, ...c);
39
- }, i = {
40
- zoomBy: s("zoomBy"),
41
- fit: s("fit"),
42
- frameBox: s("frameBox"),
43
- pause: s("pause"),
44
- resume: s("resume"),
45
- restart: s("restart"),
46
- unpin: s("unpin"),
38
+ return (o = e == null ? void 0 : e[t]) == null ? void 0 : o.call(e, ...u);
39
+ }, c = {
40
+ zoomBy: r("zoomBy"),
41
+ fit: r("fit"),
42
+ pause: r("pause"),
43
+ resume: r("resume"),
44
+ restart: r("restart"),
45
+ unpin: r("unpin"),
47
46
  // Selecting and focusing are state, and hold without a renderer; centring is the camera's.
48
47
  reveal: (t) => {
49
48
  if (e) return e.reveal(t);
50
49
  n.select([t], "node", "Node"), n.focus(t);
51
50
  },
52
- frameSelection: s("frameSelection"),
51
+ frameSelection: r("frameSelection"),
53
52
  clear: () => {
54
53
  n.select(null), n.focus(null);
55
54
  },
56
- select: (t, c, o) => n.select(t, c, o),
55
+ select: (t, u, o) => n.select(t, u, o),
57
56
  setFocus: (t) => n.focus(t),
58
57
  setTool: (t) => n.setTool(t),
59
58
  subscribe: (t) => n.subscribe(t),
60
- getState: () => n.getSnapshot(),
61
- getResident: () => (e == null ? void 0 : e.resident()) ?? T
59
+ getState: () => n.getSnapshot()
62
60
  };
63
- return f.set(i, {
61
+ return f.set(c, {
64
62
  store: n,
65
63
  renderer: () => e,
66
- attach(t, c) {
67
- const o = z(t, n, c);
64
+ attach(t, u) {
65
+ const o = G(t, n, u);
68
66
  return e = o, n.setRenderable(o !== null), () => {
69
67
  o == null || o.destroy(), e === o && (e = null);
70
68
  };
71
69
  }
72
- }), i;
70
+ }), c;
73
71
  }
74
- const T = {
75
- size: 0,
76
- indexOf: () => {
77
- },
78
- at: () => {
79
- },
80
- indicesOf: () => [],
81
- verticesAt: () => []
82
- };
83
72
  export {
84
- E as internalsOf,
85
- D as useGraph
73
+ N as internalsOf,
74
+ A as useGraph
86
75
  };
87
76
  //# sourceMappingURL=use-graph.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-graph.js","sources":["../../src/react/use-graph.ts"],"sourcesContent":["\"use client\";\n\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport type { Resident, VertexId } from \"../core/resident\";\nimport { createGraph, type GraphOptions, type GraphState, type GraphStore } from \"../core/store\";\nimport type { GraphCommands, SelectionSource, Tool } from \"../core/types\";\nimport { createRenderer, type Renderer, type RendererEvents } from \"../render/renderer\";\n\nexport type UseGraphProps = GraphOptions;\n\n/**\n * **The commands, and the door to the state — stable for the life of the root.** Zag's split of an\n * api from its state, in TanStack Store's shape: the state is read through `useGraphState(selector)`\n * so a part re-renders on what it shows, and a host holding the api never re-renders because a\n * vertex was hovered.\n */\nexport interface GraphApi extends GraphCommands {\n select(vertices: readonly VertexId[] | null, source?: SelectionSource, label?: string): void;\n setFocus(vertex: VertexId | null): void;\n setTool(tool: Tool): void;\n subscribe(listener: () => void): () => void;\n /** The state now, for a callback; a render reads it with `useGraphState`. */\n getState(): GraphState;\n /** Who is drawn right now, to resolve a buffer index to an identity. */\n getResident(): Resident;\n}\n\n/** What the parts in this package reach and a host does not: the element and the renderer. */\ninterface Internals {\n store: GraphStore;\n attach(host: HTMLDivElement, events?: RendererEvents): () => void;\n renderer(): Renderer | null;\n}\n\nconst INTERNALS = new WeakMap<GraphApi, Internals>();\n\n/** The parts' door to the renderer. Not on the barrel, and not on `GraphApi`'s type. */\nexport function internalsOf(api: GraphApi): Internals {\n const found = INTERNALS.get(api);\n if (!found) throw new Error(\"a graph part was given an api useGraph did not build\");\n return found;\n}\n\n/**\n * **`useBaseQuery`'s three moves**: the store is created once in `useState`, handed the latest props\n * with `setOptions` in an effect — whose dependencies are the props themselves, so a render that\n * changed none does not reach it — and read through `useSyncExternalStore`, in `useGraphState`.\n * Callbacks are read through a ref, so a host's inline `onFailure` is never a change of options.\n */\nexport function useGraph(props: UseGraphProps): GraphApi {\n const latest = useRef(props);\n latest.current = props;\n const forward = useCallback(\n (options: UseGraphProps): UseGraphProps => ({\n ...options,\n onFailure: (message) => latest.current.onFailure(message),\n onSelect: (selection) => latest.current.onSelect?.(selection),\n onFocus: (vertex) => latest.current.onFocus?.(vertex),\n }),\n [],\n );\n const [api] = useState<GraphApi>(() => build(createGraph(forward(props))));\n const { store } = internalsOf(api);\n\n const { categories, corpus, fill, filterBy, limit, look, r, sim, simulate, stroke, symbol, title, type } = props;\n useEffect(() => {\n store.setOptions(forward(latest.current));\n }, [store, forward, categories, corpus, fill, filterBy, limit, look, r, sim, simulate, stroke, symbol, title, type]);\n\n // Subscribed here as well as by the parts, so the store's first-subscriber and last-subscriber\n // moves follow the root's lifetime and not whichever part happened to mount first.\n useEffect(() => store.subscribe(() => {}), [store]);\n\n return api;\n}\n\nfunction build(store: GraphStore): GraphApi {\n let renderer: Renderer | null = null;\n const on =\n <K extends keyof GraphCommands>(name: K) =>\n (...args: Parameters<GraphCommands[K]>) =>\n (renderer?.[name] as ((...a: Parameters<GraphCommands[K]>) => void) | undefined)?.(...args);\n const api: GraphApi = {\n zoomBy: on(\"zoomBy\"),\n fit: on(\"fit\"),\n frameBox: on(\"frameBox\"),\n pause: on(\"pause\"),\n resume: on(\"resume\"),\n restart: on(\"restart\"),\n unpin: on(\"unpin\"),\n // Selecting and focusing are state, and hold without a renderer; centring is the camera's.\n reveal: (vertex) => {\n if (renderer) return renderer.reveal(vertex);\n store.select([vertex], \"node\", \"Node\");\n store.focus(vertex);\n },\n frameSelection: on(\"frameSelection\"),\n clear: () => {\n store.select(null);\n store.focus(null);\n },\n select: (vertices, source, label) => store.select(vertices, source, label),\n setFocus: (vertex) => store.focus(vertex),\n setTool: (tool) => store.setTool(tool),\n subscribe: (listener) => store.subscribe(listener),\n getState: () => store.getSnapshot(),\n getResident: () => renderer?.resident() ?? NOBODY,\n };\n INTERNALS.set(api, {\n store,\n renderer: () => renderer,\n attach(host, events) {\n const mounted = createRenderer(host, store, events);\n renderer = mounted;\n store.setRenderable(mounted !== null);\n return () => {\n mounted?.destroy();\n if (renderer === mounted) renderer = null;\n };\n },\n });\n return api;\n}\n\nconst NOBODY: Resident = {\n size: 0,\n indexOf: () => undefined,\n at: () => undefined,\n indicesOf: () => [],\n verticesAt: () => [],\n};\n"],"names":[],"mappings":";;;;AAkCA;AAGO;AACL;AACA;AACA;AACF;AAQO;AACL;AACA;AACA;AAAgB;AAC8B;AACvC;AACqD;;AAC/B;AAA0B;AAAA;;AAC9B;AAAyB;AAAA;AAAM;AAEtD;AAMF;AACE;AAAwC;AAKJ;AAGxC;AAEA;AACE;AACA;;AAGK;AAAqF;AACpE;AACD;AACN;AACU;AACN;AACE;AACE;AACJ;AAAA;AAGf;AACA;AACkB;AACpB;AACmC;AAEjC;AACgB;AAClB;AACyE;AACjC;AACH;AACY;AAC3B;AACqB;AAE7C;AAAmB;AACjB;AACgB;AAEd;AACA;AAGE;AACqC;AACvC;AACF;AAGJ;AAEA;AAAyB;AACjB;AACG;AAAA;AACL;AAAA;AACa;AAEnB;;;;;"}
1
+ {"version":3,"file":"use-graph.js","sources":["../../src/react/use-graph.ts"],"sourcesContent":["\"use client\";\n\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport { createGraph, type GraphOptions, type GraphState, type GraphStore } from \"../core/store\";\nimport type { GraphCommands, SelectionSource, Tool, VertexId } from \"../core/types\";\nimport { createRenderer, type Renderer, type RendererEvents } from \"../render/renderer\";\n\nexport type UseGraphProps = GraphOptions;\n\n/**\n * **The commands, and the door to the state — stable for the life of the root.** Zag's split of an\n * api from its state, in TanStack Store's shape: the state is read through `useGraphState(selector)`\n * so a part re-renders on what it shows, and a host holding the api never re-renders because a\n * vertex was hovered.\n */\nexport interface GraphApi extends GraphCommands {\n select(vertices: readonly VertexId[] | null, source?: SelectionSource, label?: string): void;\n setFocus(vertex: VertexId | null): void;\n setTool(tool: Tool): void;\n subscribe(listener: () => void): () => void;\n /** The state now, for a callback; a render reads it with `useGraphState`. */\n getState(): GraphState;\n}\n\n/** What the parts in this package reach and a host does not: the element and the renderer. */\ninterface Internals {\n store: GraphStore;\n attach(host: HTMLDivElement, events?: RendererEvents): () => void;\n renderer(): Renderer | null;\n}\n\nconst INTERNALS = new WeakMap<GraphApi, Internals>();\n\n/** The parts' door to the renderer. Not on the barrel, and not on `GraphApi`'s type. */\nexport function internalsOf(api: GraphApi): Internals {\n const found = INTERNALS.get(api);\n if (!found) throw new Error(\"a graph part was given an api useGraph did not build\");\n return found;\n}\n\n/**\n * **`useBaseQuery`'s three moves**: the store is created once in `useState`, handed the latest props\n * with `setOptions` in an effect — whose dependencies are the props themselves, so a render that\n * changed none does not reach it — and read through `useSyncExternalStore`, in `useGraphState`.\n * Callbacks are read through a ref, so a host's inline `onFailure` is never a change of options.\n */\nexport function useGraph(props: UseGraphProps): GraphApi {\n const latest = useRef(props);\n latest.current = props;\n const forward = useCallback(\n (options: UseGraphProps): UseGraphProps => ({\n ...options,\n onFailure: (message) => latest.current.onFailure(message),\n onSelect: (selection) => latest.current.onSelect?.(selection),\n onFocus: (vertex) => latest.current.onFocus?.(vertex),\n }),\n [],\n );\n const [api] = useState<GraphApi>(() => build(createGraph(forward(props))));\n const { store } = internalsOf(api);\n\n const { categories, corpus, fill, filterBy, look, r, sim, simulate, stroke, symbol, title } = props;\n useEffect(() => {\n store.setOptions(forward(latest.current));\n }, [store, forward, categories, corpus, fill, filterBy, look, r, sim, simulate, stroke, symbol, title]);\n\n // Subscribed here as well as by the parts, so the store's first-subscriber and last-subscriber\n // moves follow the root's lifetime and not whichever part happened to mount first.\n useEffect(() => store.subscribe(() => {}), [store]);\n\n return api;\n}\n\nfunction build(store: GraphStore): GraphApi {\n let renderer: Renderer | null = null;\n const on =\n <K extends keyof GraphCommands>(name: K) =>\n (...args: Parameters<GraphCommands[K]>) =>\n (renderer?.[name] as ((...a: Parameters<GraphCommands[K]>) => void) | undefined)?.(...args);\n const api: GraphApi = {\n zoomBy: on(\"zoomBy\"),\n fit: on(\"fit\"),\n pause: on(\"pause\"),\n resume: on(\"resume\"),\n restart: on(\"restart\"),\n unpin: on(\"unpin\"),\n // Selecting and focusing are state, and hold without a renderer; centring is the camera's.\n reveal: (vertex) => {\n if (renderer) return renderer.reveal(vertex);\n store.select([vertex], \"node\", \"Node\");\n store.focus(vertex);\n },\n frameSelection: on(\"frameSelection\"),\n clear: () => {\n store.select(null);\n store.focus(null);\n },\n select: (vertices, source, label) => store.select(vertices, source, label),\n setFocus: (vertex) => store.focus(vertex),\n setTool: (tool) => store.setTool(tool),\n subscribe: (listener) => store.subscribe(listener),\n getState: () => store.getSnapshot(),\n };\n INTERNALS.set(api, {\n store,\n renderer: () => renderer,\n attach(host, events) {\n const mounted = createRenderer(host, store, events);\n renderer = mounted;\n store.setRenderable(mounted !== null);\n return () => {\n mounted?.destroy();\n if (renderer === mounted) renderer = null;\n };\n },\n });\n return api;\n}\n\n"],"names":[],"mappings":";;;;AA+BA;AAGO;AACL;AACA;AACA;AACF;AAQO;AACL;AACA;AACA;AAAgB;AAC8B;AACvC;AACqD;;AAC/B;AAA0B;AAAA;;AAC9B;AAAyB;AAAA;AAAM;AAEtD;AAMF;AACE;AAAwC;AAKJ;AAGxC;AAEA;AACE;AACA;;AAGK;AAAqF;AACpE;AACD;AACN;AACI;AACE;AACE;AACJ;AAAA;AAGf;AACA;AACkB;AACpB;AACmC;AAEjC;AACgB;AAClB;AACyE;AACjC;AACH;AACY;AAC3B;AAExB;AAAmB;AACjB;AACgB;AAEd;AACA;AAGE;AACqC;AACvC;AACF;AAGJ;;;;;"}
@@ -1 +1 @@
1
- {"version":3,"file":"graph-looks.d.ts","sourceRoot":"","sources":["../../src/render/graph-looks.ts"],"names":[],"mappings":"AAmBA;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,KAAK,GAAG,QAAQ,GAAG,QAAQ,GAAG,UAAU,GAAG,SAAS,GAAG,OAAO,CAAC;AAE3E;;;;;;;;;GASG;AACH,eAAO,MAAM,WAAW,EAAE,MAAM,CAAC,KAAK,EAAE,MAAM,CAM7C,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,WAAW,EAAE,KAAK,EAAgD,CAAC;AAEhF;;;;;;GAMG;AACH,eAAO,MAAM,WAAW,EAAE,KAAe,CAAC;AAE1C;;;;;;GAMG;AACH,MAAM,WAAW,IAAI;IACnB,qEAAqE;IACrE,IAAI,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACvB,IAAI,EAAE;QACJ;;;;;WAKG;QACH,MAAM,EAAE,OAAO,CAAC;QAChB,OAAO,EAAE,MAAM,CAAC;QAChB,KAAK,EAAE,MAAM,CAAC;QACd;;;;;;WAMG;QACH,KAAK,EAAE,MAAM,CAAC;QACd;;;;;;;;;;WAUG;QACH,KAAK,EAAE,OAAO,CAAC;QACf;;;;;;WAMG;QACH,IAAI,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;KACxB,CAAC;IACF,mEAAmE;IACnE,MAAM,EAAE,MAAM,CAAC;IACf,+FAA+F;IAC/F,QAAQ,EAAE,OAAO,CAAC;IAClB;;;;;;OAMG;IACH,IAAI,EAAE,OAAO,CAAC;CAuBf;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,QAAQ,CAAC,MAAM,GAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAM,GAAG,IAAI,CA0BxF;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,YAAY,EAAE,IAAiB,CAAC;AAE7C;;;;;;GAMG;AACH,MAAM,WAAW,SAAU,SAAQ,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IAC5D,IAAI,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC;CAC9B;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,KAAK,CAAC,EAAE,SAAS,GAAG,IAAI,CAGnD;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,UAAU,EAAE,MAAM,CAAC,KAAK,EAAE,MAAM,CAQ5C,CAAC"}
1
+ {"version":3,"file":"graph-looks.d.ts","sourceRoot":"","sources":["../../src/render/graph-looks.ts"],"names":[],"mappings":"AAmBA;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,KAAK,GAAG,QAAQ,GAAG,QAAQ,GAAG,UAAU,GAAG,SAAS,GAAG,OAAO,CAAC;AAE3E;;;;;;;;;GASG;AACH,eAAO,MAAM,WAAW,EAAE,MAAM,CAAC,KAAK,EAAE,MAAM,CAM7C,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,WAAW,EAAE,KAAK,EAAgD,CAAC;AAEhF;;;;;;GAMG;AACH,eAAO,MAAM,WAAW,EAAE,KAAe,CAAC;AAE1C;;;;;;GAMG;AACH,MAAM,WAAW,IAAI;IACnB,qEAAqE;IACrE,IAAI,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACvB,IAAI,EAAE;QACJ;;;;;WAKG;QACH,MAAM,EAAE,OAAO,CAAC;QAChB,OAAO,EAAE,MAAM,CAAC;QAChB,KAAK,EAAE,MAAM,CAAC;QACd;;;;;;WAMG;QACH,KAAK,EAAE,MAAM,CAAC;QACd;;;;;;;;;;WAUG;QACH,KAAK,EAAE,OAAO,CAAC;QACf;;;;;;WAMG;QACH,IAAI,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;KACxB,CAAC;IACF,mEAAmE;IACnE,MAAM,EAAE,MAAM,CAAC;IACf,+FAA+F;IAC/F,QAAQ,EAAE,OAAO,CAAC;IAClB;;;;;;OAMG;IACH,IAAI,EAAE,OAAO,CAAC;CAsBf;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,QAAQ,CAAC,MAAM,GAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAM,GAAG,IAAI,CA0BxF;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,YAAY,EAAE,IAAiB,CAAC;AAE7C;;;;;;GAMG;AACH,MAAM,WAAW,SAAU,SAAQ,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IAC5D,IAAI,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC;CAC9B;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,KAAK,CAAC,EAAE,SAAS,GAAG,IAAI,CAGnD;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,UAAU,EAAE,MAAM,CAAC,KAAK,EAAE,MAAM,CAQ5C,CAAC"}