@kanzo-tech/graph 0.1.0 → 0.2.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 (53) hide show
  1. package/dist/bounded.d.ts +115 -40
  2. package/dist/bounded.d.ts.map +1 -1
  3. package/dist/bounded.js +13 -34
  4. package/dist/bounded.js.map +1 -1
  5. package/dist/duck-source.d.ts.map +1 -1
  6. package/dist/duck-source.js +165 -166
  7. package/dist/duck-source.js.map +1 -1
  8. package/dist/graph-canvas.d.ts +1 -1
  9. package/dist/graph-canvas.js.map +1 -1
  10. package/dist/graph-looks.d.ts +63 -18
  11. package/dist/graph-looks.d.ts.map +1 -1
  12. package/dist/graph-looks.js +37 -26
  13. package/dist/graph-looks.js.map +1 -1
  14. package/dist/graph-model.d.ts +2 -2
  15. package/dist/graph-model.d.ts.map +1 -1
  16. package/dist/graph-model.js +19 -14
  17. package/dist/graph-model.js.map +1 -1
  18. package/dist/graph-sim.d.ts +15 -1
  19. package/dist/graph-sim.d.ts.map +1 -1
  20. package/dist/graph-sim.js +17 -13
  21. package/dist/graph-sim.js.map +1 -1
  22. package/dist/index.d.ts +9 -9
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +33 -49
  25. package/dist/index.js.map +1 -1
  26. package/dist/memory-source.d.ts.map +1 -1
  27. package/dist/memory-source.js +99 -92
  28. package/dist/memory-source.js.map +1 -1
  29. package/dist/obligations.d.ts +1 -3
  30. package/dist/obligations.d.ts.map +1 -1
  31. package/dist/shape-glyph.d.ts +30 -0
  32. package/dist/shape-glyph.d.ts.map +1 -0
  33. package/dist/shape-glyph.js +15 -0
  34. package/dist/shape-glyph.js.map +1 -0
  35. package/dist/slice-client.d.ts.map +1 -1
  36. package/dist/slice-client.js +36 -36
  37. package/dist/slice-client.js.map +1 -1
  38. package/dist/use-graph-overlays.d.ts +21 -30
  39. package/dist/use-graph-overlays.d.ts.map +1 -1
  40. package/dist/use-graph-overlays.js +39 -37
  41. package/dist/use-graph-overlays.js.map +1 -1
  42. package/dist/use-graph.d.ts +38 -10
  43. package/dist/use-graph.d.ts.map +1 -1
  44. package/dist/use-graph.js +66 -65
  45. package/dist/use-graph.js.map +1 -1
  46. package/dist/use-query-loop.d.ts +6 -0
  47. package/dist/use-query-loop.d.ts.map +1 -1
  48. package/dist/use-query-loop.js +88 -70
  49. package/dist/use-query-loop.js.map +1 -1
  50. package/dist/use-renderer.d.ts.map +1 -1
  51. package/dist/use-renderer.js +69 -66
  52. package/dist/use-renderer.js.map +1 -1
  53. package/package.json +4 -4
@@ -1,27 +1,5 @@
1
- import { Graph } from '@cosmos.gl/graph';
2
- import { Resident, VertexId } from './resident';
3
- /**
4
- * Everything that floats over the canvas and has to keep up with it: the hub labels, the hover
5
- * card, and the grid's lock to the graph's own space.
6
- *
7
- * One rAF for all three, because they answer the same question — *where is the camera now* — and
8
- * three independent loops would read the same transform three times a frame. React never runs: the
9
- * overlays move by imperative style writes, and a re-render per frame would be a re-render per
10
- * frame.
11
- *
12
- * **An overlay is attached to a vertex, not to a slot.** Everything here outlives an answer — a
13
- * label element is kept across renders, a hover survives a query — so the tracked set is identities
14
- * and the buffer index is resolved through `Resident` at the moment of painting. Held as indices, a
15
- * label would keep its position and change which node it was naming the first time the resident set
16
- * moved, with the text and the dot disagreeing and nothing raised.
17
- *
18
- * This lived inside the canvas component among seven other concerns, and that is not a filing
19
- * detail: the scheduler below once kept a cancelled `requestAnimationFrame` handle in `frame`,
20
- * which silently disabled every overlay for the life of the page. It took a long time to find in a
21
- * 700-line component and would have been obvious here.
22
- */
23
- /** Dot spacing at zoom 1. The painter keeps the on-screen spacing inside [GRID, 2·GRID). */
24
- export declare const GRID = 22;
1
+ import { VertexId } from './resident';
2
+ import { GraphApi } from './use-graph';
25
3
  export interface GraphOverlays {
26
4
  /** The box the overlays are positioned within — the canvas' own bounds. */
27
5
  hostRef: React.RefObject<HTMLDivElement | null>;
@@ -44,10 +22,23 @@ export interface GraphOverlays {
44
22
  /** Ask for a repaint. Coalesced — many calls in a frame cost one. */
45
23
  schedule: () => void;
46
24
  }
47
- export interface GraphOverlayOptions {
48
- getGraph: () => Graph | null;
49
- /** Who is drawn right now, for turning a tracked vertex into the buffer index cosmos.gl wants. */
50
- getResident: () => Resident;
51
- }
52
- export declare function useGraphOverlays(options: GraphOverlayOptions): GraphOverlays;
25
+ /**
26
+ * **The api, not two getters off it.**
27
+ *
28
+ * This took `{ getGraph, getResident }` — an options object whose two members were copied out of
29
+ * `GraphApi` — and that shape is the reason `getGraph` and `getResident` are on the api at all
30
+ * beside the `slice` and `resident` values it already publishes. A host composing the two wrote the
31
+ * hook's argument by hand out of the object it had just been given, which is a re-statement rather
32
+ * than a decision: there is no useful call where the two come from different graphs.
33
+ *
34
+ * `GraphOverlayOptions` went with it. It named a shape a caller had to assemble, and what a caller
35
+ * has is the api.
36
+ *
37
+ * **The ordering follows, and it is the honest one.** This has to be called *after* `useGraph`,
38
+ * because it now takes what `useGraph` returns. The other half of the cycle — a look change owes the
39
+ * overlays a repaint, and a look change does not tick — stays where it was: `useGraph` takes a
40
+ * `schedule` callback, and a host bridges the two with one ref. One indirection, in the direction
41
+ * that genuinely needs one, instead of two accessors threaded around an object the host is holding.
42
+ */
43
+ export declare function useGraphOverlays(api: GraphApi): GraphOverlays;
53
44
  //# sourceMappingURL=use-graph-overlays.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-graph-overlays.d.ts","sourceRoot":"","sources":["../src/use-graph-overlays.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAGrD;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,4FAA4F;AAC5F,eAAO,MAAM,IAAI,KAAK,CAAC;AAcvB,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,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,MAAM,KAAK,GAAG,IAAI,CAAC;IAC7B,kGAAkG;IAClG,WAAW,EAAE,MAAM,QAAQ,CAAC;CAC7B;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,mBAAmB,GAAG,aAAa,CA2M5E"}
1
+ {"version":3,"file":"use-graph-overlays.d.ts","sourceRoot":"","sources":["../src/use-graph-overlays.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AA8C5C,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;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,QAAQ,GAAG,aAAa,CA+N7D"}
@@ -1,14 +1,14 @@
1
1
  "use client";
2
- import { useRef as r, useCallback as x, useEffect as H } from "react";
3
- import { whenReady as N } from "./when-ready.js";
4
- const Q = 22, I = 14, w = 8, U = 10, V = 12, M = 3, C = (P, l, f) => Math.min(Math.max(P, l), Math.max(l, f));
2
+ import { useRef as r, useCallback as x, useEffect as S } from "react";
3
+ import { whenReady as Q } from "./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
5
  function tt(P) {
6
- const { getGraph: l, getResident: f } = P, T = r(null), z = r(null), O = r(null), A = r(/* @__PURE__ */ new Map()), L = r(/* @__PURE__ */ new Map()), m = r([]), E = r(null), $ = r(null), S = r(null), p = r(0), F = x(() => {
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
7
  const t = l();
8
8
  if (!t) return;
9
- const o = E.current, i = o === null || m.current.includes(o) ? m.current : [...m.current, o];
10
- N(t, (u) => u.trackPointPositionsByIndices(f().indicesOf(i)));
11
- }, [l, f]), _ = x(() => {
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
12
  const t = l();
13
13
  if (!t) return;
14
14
  const o = f();
@@ -16,7 +16,7 @@ function tt(P) {
16
16
  const u = (n) => {
17
17
  const e = o.indexOf(n);
18
18
  return e === void 0 ? null : (i ?? (i = t.getTrackedPointPositionsMap()), i.get(e) ?? null);
19
- }, B = [], g = S.current;
19
+ }, H = [], g = B.current;
20
20
  for (const n of m.current) {
21
21
  const e = A.current.get(n);
22
22
  if (!e) continue;
@@ -25,72 +25,74 @@ function tt(P) {
25
25
  e.style.opacity = "0";
26
26
  continue;
27
27
  }
28
- const [c, h] = t.spaceToScreenPosition(a);
28
+ const [c, d] = t.spaceToScreenPosition(a);
29
29
  let s = L.current.get(n);
30
30
  s === void 0 && (s = e.offsetWidth, L.current.set(n, s));
31
- const d = c - s / 2, G = d + s, b = h - U, R = b - V, J = g !== null && (G < 0 || b < 0 || d > g.width || R > g.height), K = B.some((v) => d < v[2] && G > v[0] && R < v[3] && b > v[1]);
32
- if (J || K) {
31
+ const h = c - s / 2, z = h + s, b = d - U, R = b - V, K = g !== null && (z < 0 || b < 0 || h > g.width || R > g.height), N = H.some((v) => h < v[2] && z > v[0] && R < v[3] && b > v[1]);
32
+ if (K || N) {
33
33
  e.style.opacity = "0";
34
34
  continue;
35
35
  }
36
- B.push([d - M, R - M, G + M, b + M]), e.style.transform = `translate(${Math.round(d)}px, ${Math.round(R)}px)`, e.style.opacity = "1";
36
+ H.push([h - M, R - M, z + M, b + M]), e.style.transform = `translate(${Math.round(h)}px, ${Math.round(R)}px)`, e.style.opacity = "1";
37
37
  }
38
- const k = z.current;
39
- if (k) {
38
+ const G = $.current;
39
+ if (G) {
40
40
  const n = t.getZoomLevel();
41
41
  if (n > 0) {
42
- const e = Q * n / 2 ** Math.floor(Math.log2(n)), [a, c] = t.spaceToScreenPosition([0, 0]), h = (s) => (s % e + e) % e;
43
- k.style.backgroundSize = `${e}px ${e}px`, k.style.backgroundPosition = `${h(a)}px ${h(c)}px`;
42
+ const e = T * n / 2 ** Math.floor(Math.log2(n)), [a, c] = t.spaceToScreenPosition([0, 0]), d = (s) => (s % e + e) % e;
43
+ G.style.backgroundSize = `${e}px ${e}px`, G.style.backgroundPosition = `${d(a)}px ${d(c)}px`;
44
44
  }
45
45
  }
46
- const y = O.current, D = E.current;
47
- if (y && D !== null && g) {
48
- const n = u(D);
46
+ const y = _.current, I = k.current;
47
+ if (y && I !== null && g) {
48
+ const n = u(I);
49
49
  if (n) {
50
50
  const [e, a] = t.spaceToScreenPosition(n);
51
- let c = $.current;
52
- c || (c = { width: y.offsetWidth, height: y.offsetHeight }, $.current = c);
53
- const h = a - I - c.height, s = a + I, d = h >= w ? h : s;
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
54
  y.style.transform = `translate(${Math.round(
55
- C(e - c.width / 2, w, g.width - c.width - w)
56
- )}px, ${Math.round(C(d, w, g.height - c.height - w))}px)`, y.style.opacity = "1";
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";
57
57
  }
58
58
  }
59
59
  }, [l, f]);
60
- H(() => {
61
- const t = T.current;
60
+ S(() => {
61
+ const t = O.current;
62
62
  if (!t) return;
63
63
  const o = new ResizeObserver(([i]) => {
64
64
  const u = i == null ? void 0 : i.contentRect;
65
- u && (S.current = { width: u.width, height: u.height });
65
+ u && (B.current = { width: u.width, height: u.height });
66
66
  });
67
67
  return o.observe(t), () => o.disconnect();
68
+ }, []), S(() => {
69
+ const t = $.current;
70
+ t && (t.style.backgroundSize = `${T}px ${T}px`);
68
71
  }, []);
69
- const W = x(() => {
72
+ const q = x(() => {
70
73
  p.current || (p.current = requestAnimationFrame(() => {
71
- p.current = 0, _();
74
+ p.current = 0, D();
72
75
  }));
73
- }, [_]);
74
- H(
76
+ }, [D]);
77
+ S(
75
78
  () => () => {
76
79
  p.current && cancelAnimationFrame(p.current), p.current = 0;
77
80
  },
78
81
  []
79
82
  );
80
- const q = x(
83
+ const Z = x(
81
84
  (t) => (o) => {
82
85
  o ? A.current.set(t, o) : A.current.delete(t);
83
86
  },
84
87
  []
85
- ), Z = x((t) => {
88
+ ), j = x((t) => {
86
89
  m.current = t, L.current.clear();
87
- }, []), j = x((t) => {
88
- E.current = t, $.current = null;
90
+ }, []), J = x((t) => {
91
+ k.current = t, E.current = null;
89
92
  }, []);
90
- return { hostRef: T, gridRef: z, cardRef: O, labelRef: q, setLabelOrder: Z, setHovered: j, track: F, schedule: W };
93
+ return { hostRef: O, gridRef: $, cardRef: _, labelRef: Z, setLabelOrder: j, setHovered: J, track: W, schedule: q };
91
94
  }
92
95
  export {
93
- Q as GRID,
94
96
  tt as useGraphOverlays
95
97
  };
96
98
  //# sourceMappingURL=use-graph-overlays.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-graph-overlays.js","sources":["../src/use-graph-overlays.ts"],"sourcesContent":["\"use client\";\n\nimport { useCallback, useEffect, useRef } from \"react\";\nimport type { Graph } from \"@cosmos.gl/graph\";\nimport type { Resident, VertexId } from \"./resident\";\nimport { whenReady } from \"./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/** Dot spacing at zoom 1. The painter keeps the on-screen spacing inside [GRID, 2·GRID). */\nexport const 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\nexport interface GraphOverlayOptions {\n getGraph: () => Graph | null;\n /** Who is drawn right now, for turning a tracked vertex into the buffer index cosmos.gl wants. */\n getResident: () => Resident;\n}\n\nexport function useGraphOverlays(options: GraphOverlayOptions): GraphOverlays {\n const { getGraph, getResident } = options;\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: a host registers its labels the moment the first slice\n // 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 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":";;;AA6BO;AA2CA;AACL;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;AAGxB;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":"use-graph-overlays.js","sources":["../src/use-graph-overlays.ts"],"sourcesContent":["\"use client\";\n\nimport { useCallback, useEffect, useRef } from \"react\";\nimport type { VertexId } from \"./resident\";\nimport type { GraphApi } from \"./use-graph\";\nimport { whenReady } from \"./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 api, not two getters off it.**\n *\n * This took `{ getGraph, getResident }` — an options object whose two members were copied out of\n * `GraphApi` — and that shape is the reason `getGraph` and `getResident` are on the api at all\n * beside the `slice` and `resident` values it already publishes. A host composing the two wrote the\n * hook's argument by hand out of the object it had just been given, which is a re-statement rather\n * than a decision: there is no useful call where the two come from different graphs.\n *\n * `GraphOverlayOptions` went with it. It named a shape a caller had to assemble, and what a caller\n * has is the api.\n *\n * **The ordering follows, and it is the honest one.** This has to be called *after* `useGraph`,\n * because it now takes what `useGraph` returns. The other half of the cycle — a look change owes the\n * overlays a repaint, and a look change does not tick — stays where it was: `useGraph` takes a\n * `schedule` callback, and a host bridges the two with one ref. One indirection, in the direction\n * that genuinely needs one, instead of two accessors threaded around an object the host is holding.\n */\nexport function useGraphOverlays(api: GraphApi): 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: a host registers its labels the moment the first slice\n // 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;AAuDO;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,7 +1,7 @@
1
1
  import { Graph } from '@cosmos.gl/graph';
2
2
  import { RefObject } from 'react';
3
3
  import { BoundedSource, Slice } from './bounded';
4
- import { Look } from './graph-looks';
4
+ import { LookPatch } from './graph-looks';
5
5
  import { Resident, VertexId } from './resident';
6
6
  import { Sim } from './graph-sim';
7
7
  import { Motion } from './types';
@@ -11,10 +11,10 @@ import { Motion } from './types';
11
11
  * **This is Ark's `useX(props) → api`, and the reason it exists is a host that could not adopt
12
12
  * `GraphCanvas`.** The component owned the renderer, the query loop and the look, and published them
13
13
  * through a context — which a legend or an inspector can read, because those are `children`. What
14
- * cannot read a context is anything that sits *above* the element: `useGraphOverlays` wants
15
- * `getGraph` and `getResident`, the `events` block wants the same accessors, and both are arguments
16
- * to a hook called in the component that renders the canvas rather than inside it. The workspace
17
- * needed them in thirty places and so kept its own copy of all three.
14
+ * cannot read a context is anything that sits *above* the element: `useGraphOverlays` wants the api
15
+ * itself, the `events` block wants the accessors on it, and both are arguments to a hook called in
16
+ * the component that renders the canvas rather than inside it. The workspace needed them in thirty
17
+ * places and so kept its own copy of all three.
18
18
  *
19
19
  * The general answer to that circularity is Ark's, and it is a shape rather than a feature: the
20
20
  * factory builds the api where the host can hold it, the provider takes it and renders. What the
@@ -58,9 +58,23 @@ export interface UseGraphProps {
58
58
  * **One appearance input**, where there were two: a `Display` rode beside this with its own
59
59
  * defaults, spelling the edge layer, the backdrop and two multipliers over numbers the look
60
60
  * already computes. `lookFrom(values)` resolves the whole picture from the axes a person chose.
61
+ *
62
+ * **A patch, not a whole `Look` — which is what `DEFAULT_LOOK` was for.** This took the complete
63
+ * object, so a host that wanted the vignette on wrote `{ ...DEFAULT_LOOK, vignette: true }` and
64
+ * the package exported the default to make that expressible. That spread is a *copy*: the host
65
+ * now holds every number this package chose, and stops tracking any of them the moment one moves
66
+ * here. Passing `{ vignette: true }` says what the host decided and leaves the rest ours. `link`
67
+ * merges one level down, so `{ link: { render: false } }` keeps the measured opacity and width.
68
+ *
69
+ * Memoise it, or pass a literal only when it does not change: the reference is what the buffers
70
+ * are rebuilt on. Omitted entirely, it costs nothing — the shared default is returned by identity.
71
+ */
72
+ look?: LookPatch;
73
+ /**
74
+ * The force coefficients, as a patch over this package's own — `look`'s twin in every respect,
75
+ * including why `DEFAULT_SIM` is no longer exported to spread from.
61
76
  */
62
- look?: Look;
63
- sim?: Sim;
77
+ sim?: Partial<Sim>;
64
78
  /**
65
79
  * Off by default, and that is the correct default rather than a cautious one: a bounded source
66
80
  * hands back the coordinates its next spatial query is expressed in, so a force moves the picture
@@ -131,9 +145,9 @@ export interface UseGraphProps {
131
145
  * off. A host drawing no labels passes nothing.
132
146
  *
133
147
  * It goes through the props rather than being read off the api because `useGraphOverlays` is
134
- * declared *after* this hook — it needs `getGraph` and `getResident` from it. A host bridges the
135
- * two with one ref, which is the smallest honest answer to a cycle that is genuinely mutual:
136
- * overlays need the graph, and the graph's repaint owes the overlays a nudge.
148
+ * declared *after* this hook — it takes what this hook returns. A host bridges the two with one
149
+ * ref, which is the smallest honest answer to a cycle that is genuinely mutual: the overlays need
150
+ * the graph, and the graph's repaint owes the overlays a nudge.
137
151
  */
138
152
  schedule?: () => void;
139
153
  }
@@ -156,6 +170,20 @@ export interface GraphApi {
156
170
  * attaches it itself, and nothing works until something does.
157
171
  */
158
172
  hostRef: RefObject<HTMLDivElement | null>;
173
+ /**
174
+ * The renderer and the current map, read from inside a callback that was created once.
175
+ *
176
+ * **These two nearly left with `GraphOverlayOptions`, and the census is why they stayed.** The
177
+ * reason they were on the api was that `useGraphOverlays` asked for them as a pair, so every host
178
+ * copied them out of this object into that hook's argument — which is a re-statement, and that
179
+ * hook takes the api now. What kept them is a different set of readers: `useGraphSelection` takes
180
+ * both (its `commit` is a policy only a product can write, so it cannot be folded into the api the
181
+ * way the overlays were), and a real consumer outside this repository reads `getGraph` and
182
+ * `getResident` off `useGraphContext()` to paint its own mask over the buffers. Both are reads
183
+ * from inside a callback registered once, which is exactly the case `slice` and `resident` cannot
184
+ * serve — a value would re-render every consumer on every camera move to hand back a reference
185
+ * they only dereference when something is clicked.
186
+ */
159
187
  getGraph: () => Graph | null;
160
188
  getResident: () => Resident;
161
189
  /** The answer currently drawn, or `null` before the first one. */
@@ -1 +1 @@
1
- {"version":3,"file":"use-graph.d.ts","sourceRoot":"","sources":["../src/use-graph.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"use-graph.d.ts","sourceRoot":"","sources":["../src/use-graph.ts"],"names":[],"mappings":"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"}
package/dist/use-graph.js CHANGED
@@ -1,101 +1,102 @@
1
1
  "use client";
2
- import { useMemo as P, useRef as s, useCallback as R } from "react";
3
- import { DEFAULT_LOOK as j } from "./graph-looks.js";
4
- import { isColour as q } from "./graph-model.js";
5
- import { residentOf as z } from "./resident.js";
6
- import { useQueryLoop as H } from "./use-query-loop.js";
7
- import { useRenderer as I } from "./use-renderer.js";
8
- import { useGraphLook as J } from "./use-graph-look.js";
9
- const S = z(null);
10
- function nr(C) {
2
+ import { useMemo as l, useRef as c, useCallback as b } from "react";
3
+ import { resolveLook as I } from "./graph-looks.js";
4
+ import { isColour as J } from "./graph-model.js";
5
+ import { residentOf as K } from "./resident.js";
6
+ import { resolveSim as U } from "./graph-sim.js";
7
+ import { useQueryLoop as V } from "./use-query-loop.js";
8
+ import { useRenderer as W } from "./use-renderer.js";
9
+ import { useGraphLook as X } from "./use-graph-look.js";
10
+ const _ = K(null);
11
+ function ir(x) {
11
12
  const {
12
- clusters: D,
13
- debounce: E,
14
- events: a,
13
+ clusters: B,
14
+ debounce: D,
15
+ events: p,
15
16
  fill: i,
16
- limit: L,
17
- look: b = j,
18
- onFailure: p,
19
- pinned: x,
20
- r: B,
21
- report: G,
17
+ limit: E,
18
+ look: d,
19
+ onFailure: k,
20
+ pinned: G,
21
+ r: L,
22
+ report: y,
22
23
  reportProgress: T,
23
- schedule: y,
24
- sim: F,
25
- simulate: Z = !1,
26
- source: w,
27
- stroke: d,
28
- symbol: l
29
- } = C, A = q(i) ? l : i, K = P(() => ({ fill: i, stroke: d, symbol: l }), [i, d, l]), u = s(null), f = s(null), c = s(S), { explore: M, pending: N, refresh: m, resident: k, slice: v, sliced: Q, total: U } = H({
30
- debounce: E,
31
- fill: A,
24
+ schedule: Z,
25
+ sim: v,
26
+ simulate: w = !1,
27
+ source: F,
28
+ stroke: h,
29
+ symbol: m
30
+ } = x, M = l(() => I(d), [d]), N = l(() => U(v), [v]), Q = J(i) ? m : i, S = l(() => ({ fill: i, stroke: h, symbol: m }), [i, h, m]), u = c(null), f = c(null), s = c(_), { explore: Y, pending: j, refresh: a, resident: g, slice: P, sliced: q, total: z } = V({
31
+ debounce: D,
32
+ fill: Q,
32
33
  graphRef: f,
33
34
  hostRef: u,
34
- limit: L,
35
- onError: p,
36
- pinned: x,
37
- r: B,
38
- source: w
35
+ limit: E,
36
+ onError: k,
37
+ pinned: G,
38
+ r: L,
39
+ source: F
39
40
  });
40
- c.current = k;
41
- const g = R(() => f.current, []), Y = R(() => c.current, []), t = s(a);
42
- t.current = a;
43
- const h = s(m);
44
- h.current = m;
45
- const _ = P(
41
+ s.current = g;
42
+ const O = b(() => f.current, []), A = b(() => s.current, []), t = c(p);
43
+ t.current = p;
44
+ const R = c(a);
45
+ R.current = a;
46
+ const H = l(
46
47
  () => ({
47
48
  onBackgroundClick: () => {
48
- var r, e;
49
- return (e = (r = t.current) == null ? void 0 : r.onBackgroundClick) == null ? void 0 : e.call(r);
49
+ var r, o;
50
+ return (o = (r = t.current) == null ? void 0 : r.onBackgroundClick) == null ? void 0 : o.call(r);
50
51
  },
51
52
  onDragEnd: (r) => {
52
- var o, n;
53
- const e = c.current.at(r);
54
- e !== void 0 && ((n = (o = t.current) == null ? void 0 : o.onDragEnd) == null || n.call(o, e));
53
+ var e, n;
54
+ const o = s.current.at(r);
55
+ o !== void 0 && ((n = (e = t.current) == null ? void 0 : e.onDragEnd) == null || n.call(e, o));
55
56
  },
56
- onPointClick: (r, e) => {
57
- var n, O;
58
- const o = c.current.at(e);
59
- o !== void 0 && ((O = (n = t.current) == null ? void 0 : n.onPointClick) == null || O.call(n, o, r, e));
57
+ onPointClick: (r, o) => {
58
+ var n, C;
59
+ const e = s.current.at(o);
60
+ e !== void 0 && ((C = (n = t.current) == null ? void 0 : n.onPointClick) == null || C.call(n, e, r, o));
60
61
  },
61
62
  onPointerOut: () => {
62
- var r, e;
63
- return (e = (r = t.current) == null ? void 0 : r.onPointerOut) == null ? void 0 : e.call(r);
63
+ var r, o;
64
+ return (o = (r = t.current) == null ? void 0 : r.onPointerOut) == null ? void 0 : o.call(r);
64
65
  },
65
66
  onPointerOver: (r) => {
66
- var o, n;
67
- const e = c.current.at(r);
68
- e !== void 0 && ((n = (o = t.current) == null ? void 0 : o.onPointerOver) == null || n.call(o, e, r));
67
+ var e, n;
68
+ const o = s.current.at(r);
69
+ o !== void 0 && ((n = (e = t.current) == null ? void 0 : e.onPointerOver) == null || n.call(e, o, r));
69
70
  },
70
71
  onTick: () => {
71
- var r, e;
72
- return (e = (r = t.current) == null ? void 0 : r.onTick) == null ? void 0 : e.call(r);
72
+ var r, o;
73
+ return (o = (r = t.current) == null ? void 0 : r.onTick) == null ? void 0 : o.call(r);
73
74
  },
74
75
  // The camera moved, so the graph is re-asked. A host that forgot this line got a canvas that
75
76
  // drew its first answer and never asked again — which is what `refresh` being the host's
76
77
  // responsibility used to cost.
77
78
  onZoom: () => {
78
- var r, e;
79
- h.current(), (e = (r = t.current) == null ? void 0 : r.onZoom) == null || e.call(r);
79
+ var r, o;
80
+ R.current(), (o = (r = t.current) == null ? void 0 : r.onZoom) == null || o.call(r);
80
81
  }
81
82
  }),
82
83
  // Built once: every reference inside is a ref this hook owns, so there is nothing to depend on.
83
84
  // It used to close over a ref object a host could substitute, which is the prop this file deletes.
84
85
  []
85
86
  );
86
- return I({
87
- clusters: D,
88
- events: _,
87
+ return W({
88
+ clusters: B,
89
+ events: H,
89
90
  graphRef: f,
90
91
  hostRef: u,
91
- onFailure: p,
92
- report: G,
92
+ onFailure: k,
93
+ report: y,
93
94
  reportProgress: T,
94
- sim: F,
95
- simulate: Z
96
- }), J({ channels: K, getGraph: g, hostRef: u, look: b, schedule: y, slice: v }), { explore: M, getGraph: g, getResident: Y, hostRef: u, pending: N, refresh: m, resident: k, slice: v, sliced: Q, total: U };
95
+ sim: N,
96
+ simulate: w
97
+ }), X({ channels: S, getGraph: O, hostRef: u, look: M, schedule: Z, slice: P }), { explore: Y, getGraph: O, getResident: A, hostRef: u, pending: j, refresh: a, resident: g, slice: P, sliced: q, total: z };
97
98
  }
98
99
  export {
99
- nr as useGraph
100
+ ir as useGraph
100
101
  };
101
102
  //# sourceMappingURL=use-graph.js.map