@kanzo-tech/graph 0.1.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.
- package/README.md +94 -0
- package/dist/adaptive.d.ts +27 -0
- package/dist/adaptive.d.ts.map +1 -0
- package/dist/adaptive.js +25 -0
- package/dist/adaptive.js.map +1 -0
- package/dist/bounded.d.ts +314 -0
- package/dist/bounded.d.ts.map +1 -0
- package/dist/bounded.js +39 -0
- package/dist/bounded.js.map +1 -0
- package/dist/cluster-ring.d.ts +25 -0
- package/dist/cluster-ring.d.ts.map +1 -0
- package/dist/cluster-ring.js +16 -0
- package/dist/cluster-ring.js.map +1 -0
- package/dist/css-color.d.ts +11 -0
- package/dist/css-color.d.ts.map +1 -0
- package/dist/css-color.js +23 -0
- package/dist/css-color.js.map +1 -0
- package/dist/duck-source.d.ts +182 -0
- package/dist/duck-source.d.ts.map +1 -0
- package/dist/duck-source.js +413 -0
- package/dist/duck-source.js.map +1 -0
- package/dist/graph-canvas.d.ts +50 -0
- package/dist/graph-canvas.d.ts.map +1 -0
- package/dist/graph-canvas.js +35 -0
- package/dist/graph-canvas.js.map +1 -0
- package/dist/graph-looks.d.ts +146 -0
- package/dist/graph-looks.d.ts.map +1 -0
- package/dist/graph-looks.js +44 -0
- package/dist/graph-looks.js.map +1 -0
- package/dist/graph-model.d.ts +130 -0
- package/dist/graph-model.d.ts.map +1 -0
- package/dist/graph-model.js +99 -0
- package/dist/graph-model.js.map +1 -0
- package/dist/graph-sim.d.ts +40 -0
- package/dist/graph-sim.d.ts.map +1 -0
- package/dist/graph-sim.js +20 -0
- package/dist/graph-sim.js.map +1 -0
- package/dist/index.d.ts +72 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +53 -0
- package/dist/index.js.map +1 -0
- package/dist/memory-source.d.ts +32 -0
- package/dist/memory-source.d.ts.map +1 -0
- package/dist/memory-source.js +134 -0
- package/dist/memory-source.js.map +1 -0
- package/dist/obligations.d.ts +62 -0
- package/dist/obligations.d.ts.map +1 -0
- package/dist/resident.d.ts +79 -0
- package/dist/resident.d.ts.map +1 -0
- package/dist/resident.js +44 -0
- package/dist/resident.js.map +1 -0
- package/dist/section.d.ts +82 -0
- package/dist/section.d.ts.map +1 -0
- package/dist/section.js +142 -0
- package/dist/section.js.map +1 -0
- package/dist/slice-client.d.ts +78 -0
- package/dist/slice-client.d.ts.map +1 -0
- package/dist/slice-client.js +98 -0
- package/dist/slice-client.js.map +1 -0
- package/dist/types.d.ts +57 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/use-graph-look.d.ts +28 -0
- package/dist/use-graph-look.d.ts.map +1 -0
- package/dist/use-graph-look.js +28 -0
- package/dist/use-graph-look.js.map +1 -0
- package/dist/use-graph-overlays.d.ts +53 -0
- package/dist/use-graph-overlays.d.ts.map +1 -0
- package/dist/use-graph-overlays.js +96 -0
- package/dist/use-graph-overlays.js.map +1 -0
- package/dist/use-graph-selection.d.ts +59 -0
- package/dist/use-graph-selection.d.ts.map +1 -0
- package/dist/use-graph-selection.js +101 -0
- package/dist/use-graph-selection.js.map +1 -0
- package/dist/use-graph.d.ts +177 -0
- package/dist/use-graph.d.ts.map +1 -0
- package/dist/use-graph.js +101 -0
- package/dist/use-graph.js.map +1 -0
- package/dist/use-query-loop.d.ts +84 -0
- package/dist/use-query-loop.d.ts.map +1 -0
- package/dist/use-query-loop.js +151 -0
- package/dist/use-query-loop.js.map +1 -0
- package/dist/use-renderer.d.ts +86 -0
- package/dist/use-renderer.d.ts.map +1 -0
- package/dist/use-renderer.js +185 -0
- package/dist/use-renderer.js.map +1 -0
- package/dist/when-ready.d.ts +35 -0
- package/dist/when-ready.d.ts.map +1 -0
- package/dist/when-ready.js +17 -0
- package/dist/when-ready.js.map +1 -0
- package/package.json +69 -0
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { VertexId } from './resident';
|
|
2
|
+
/**
|
|
3
|
+
* The selection tools, and the gesture that reaches them without a mode.
|
|
4
|
+
*
|
|
5
|
+
* `null` is the reading posture: drag pans, drag on a node pins it. Picking a tool swaps the drag
|
|
6
|
+
* for a selection gesture — and holding Shift borrows the marquee for one drag without picking
|
|
7
|
+
* anything, which is how most selections actually get made.
|
|
8
|
+
*/
|
|
9
|
+
export type Tool = "rect" | "lasso" | null;
|
|
10
|
+
/** `settled` converged on its own; `paused` is waiting for you. */
|
|
11
|
+
export type Motion = "running" | "settled" | "paused";
|
|
12
|
+
/**
|
|
13
|
+
* Where a selection came from. Open-ended on purpose: a host with its own panels adds its own
|
|
14
|
+
* sources, and the canvas only ever uses this to label the chip in the corner.
|
|
15
|
+
*/
|
|
16
|
+
export type SelectionSource = "marquee" | "lasso" | "node" | "order" | "ask";
|
|
17
|
+
/**
|
|
18
|
+
* The selection — one value, published once.
|
|
19
|
+
*
|
|
20
|
+
* The rule this encodes: a panel does not touch the crossfilter. It hands a selection to the
|
|
21
|
+
* canvas, the canvas publishes it as a single clause, and the corner shows it. Before that there
|
|
22
|
+
* were four ways to say "look at these nodes" and therefore four half-answers to "what is selected
|
|
23
|
+
* right now". Replacing rather than intersecting is the deliberate half — one live selection is
|
|
24
|
+
* legible, and a reader can see the whole of it in one place.
|
|
25
|
+
*
|
|
26
|
+
* A *search* is not a selection and keeps its own clause: filtering narrows the corpus, selecting
|
|
27
|
+
* points at part of it.
|
|
28
|
+
*/
|
|
29
|
+
export interface Selection {
|
|
30
|
+
/**
|
|
31
|
+
* Identities, never buffer indices.
|
|
32
|
+
*
|
|
33
|
+
* A selection is the one thing on this canvas guaranteed to outlive the answer that made it: a
|
|
34
|
+
* reader selects, pans, and expects to come back to it. An index would have been reused by then,
|
|
35
|
+
* and by a different vertex.
|
|
36
|
+
*/
|
|
37
|
+
vertices: VertexId[];
|
|
38
|
+
source: SelectionSource;
|
|
39
|
+
/** What the corner calls it. */
|
|
40
|
+
label: string;
|
|
41
|
+
}
|
|
42
|
+
/** What the canvas can be told to do. Registered by the canvas, called by the panels. */
|
|
43
|
+
export interface GraphCommands {
|
|
44
|
+
zoomBy(factor: number): void;
|
|
45
|
+
fit(): void;
|
|
46
|
+
pause(): void;
|
|
47
|
+
resume(): void;
|
|
48
|
+
restart(): void;
|
|
49
|
+
/** Let go of every pinned node, so the simulation gets the whole layout back. */
|
|
50
|
+
unpin(): void;
|
|
51
|
+
/** Centre and select one vertex. */
|
|
52
|
+
reveal(vertex: VertexId): void;
|
|
53
|
+
/** Frame whatever the canvas currently has selected. */
|
|
54
|
+
frameSelection(): void;
|
|
55
|
+
clear(): void;
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C;;;;;;GAMG;AACH,MAAM,MAAM,IAAI,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,CAAC;AAE3C,mEAAmE;AACnE,MAAM,MAAM,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,QAAQ,CAAC;AAEtD;;;GAGG;AACH,MAAM,MAAM,eAAe,GAAG,SAAS,GAAG,OAAO,GAAG,MAAM,GAAG,OAAO,GAAG,KAAK,CAAC;AAE7E;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,SAAS;IACxB;;;;;;OAMG;IACH,QAAQ,EAAE,QAAQ,EAAE,CAAC;IACrB,MAAM,EAAE,eAAe,CAAC;IACxB,gCAAgC;IAChC,KAAK,EAAE,MAAM,CAAC;CACf;AAED,yFAAyF;AACzF,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,GAAG,IAAI,IAAI,CAAC;IACZ,KAAK,IAAI,IAAI,CAAC;IACd,MAAM,IAAI,IAAI,CAAC;IACf,OAAO,IAAI,IAAI,CAAC;IAChB,iFAAiF;IACjF,KAAK,IAAI,IAAI,CAAC;IACd,oCAAoC;IACpC,MAAM,CAAC,MAAM,EAAE,QAAQ,GAAG,IAAI,CAAC;IAC/B,wDAAwD;IACxD,cAAc,IAAI,IAAI,CAAC;IACvB,KAAK,IAAI,IAAI,CAAC;CACf"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Graph } from '@cosmos.gl/graph';
|
|
2
|
+
import { Slice } from './bounded';
|
|
3
|
+
import { Channels } from './graph-model';
|
|
4
|
+
import { Look } from './graph-looks';
|
|
5
|
+
export declare function useGraphLook(options: {
|
|
6
|
+
getGraph: () => Graph | null;
|
|
7
|
+
/** The element the tokens are resolved against — inside the canvas' own tree. */
|
|
8
|
+
hostRef: React.RefObject<HTMLElement | null>;
|
|
9
|
+
/** The answer currently drawn. Every slice is a fresh set of points, so every slice repaints. */
|
|
10
|
+
slice: Slice | null;
|
|
11
|
+
look: Look;
|
|
12
|
+
/**
|
|
13
|
+
* What each channel is bound to.
|
|
14
|
+
*
|
|
15
|
+
* Only the bindings the *buffers* read arrive here — `fill` when it is a constant, `symbol`,
|
|
16
|
+
* `stroke`. `fill` as a column name and `r` name columns a query has to fetch, so those reach the
|
|
17
|
+
* source instead. One vocabulary at the call site, two destinations underneath.
|
|
18
|
+
*/
|
|
19
|
+
channels?: Channels;
|
|
20
|
+
/**
|
|
21
|
+
* Ask the overlays to reposition: point sizes changed, so the labels sit differently.
|
|
22
|
+
*
|
|
23
|
+
* Optional, because it is only owed to `useGraphOverlays` — a host drawing no labels has no
|
|
24
|
+
* overlays to reposition, and was writing a no-op to say so.
|
|
25
|
+
*/
|
|
26
|
+
schedule?: () => void;
|
|
27
|
+
}): void;
|
|
28
|
+
//# sourceMappingURL=use-graph-look.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-graph-look.d.ts","sourceRoot":"","sources":["../src/use-graph-look.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAI9C,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,WAAW,CAAC;AACvC,OAAO,EAAuB,KAAK,QAAQ,EAAE,MAAM,eAAe,CAAC;AACnE,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AA+B1C,wBAAgB,YAAY,CAAC,OAAO,EAAE;IACpC,QAAQ,EAAE,MAAM,KAAK,GAAG,IAAI,CAAC;IAC7B,iFAAiF;IACjF,OAAO,EAAE,KAAK,CAAC,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAC7C,iGAAiG;IACjG,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,IAAI,EAAE,IAAI,CAAC;IACX;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;CACvB,GAAG,IAAI,CAmCP"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useEffect as u } from "react";
|
|
3
|
+
import { useThemeTick as C } from "@kanzo-tech/ui";
|
|
4
|
+
import { buffers as P, appearance as T } from "./graph-model.js";
|
|
5
|
+
import { whenReady as l } from "./when-ready.js";
|
|
6
|
+
const z = () => {
|
|
7
|
+
};
|
|
8
|
+
function b(m) {
|
|
9
|
+
const { channels: h, getGraph: s, hostRef: r, look: n, schedule: p = z, slice: o } = m, f = C();
|
|
10
|
+
u(() => {
|
|
11
|
+
const t = s(), e = r.current;
|
|
12
|
+
if (!o || !t || !e) return;
|
|
13
|
+
const { colors: c, linkColors: a, shapes: k, sizes: g } = P(o, n, e, h);
|
|
14
|
+
return l(t, (i) => {
|
|
15
|
+
i.setPointColors(c), i.setPointSizes(g), i.setPointShapes(k), i.setLinkColors(a);
|
|
16
|
+
});
|
|
17
|
+
}, [h, s, r, n, o, f]), u(() => {
|
|
18
|
+
const t = s(), e = r.current;
|
|
19
|
+
if (!(!o || !t || !e))
|
|
20
|
+
return l(t, (c) => {
|
|
21
|
+
c.setConfigPartial(T(n, e)), c.render(), p();
|
|
22
|
+
});
|
|
23
|
+
}, [s, r, n, p, o, f]);
|
|
24
|
+
}
|
|
25
|
+
export {
|
|
26
|
+
b as useGraphLook
|
|
27
|
+
};
|
|
28
|
+
//# sourceMappingURL=use-graph-look.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-graph-look.js","sources":["../src/use-graph-look.ts"],"sourcesContent":["\"use client\";\n\nimport { useEffect } from \"react\";\nimport type { Graph } from \"@cosmos.gl/graph\";\n// The root barrel, not `/analytics`: `useThemeTick` moved there with the barrel review. Anything\n// that paints from tokens needs it, which is more surfaces than the chart half.\nimport { useThemeTick } from \"@kanzo-tech/ui\";\nimport type { Slice } from \"./bounded\";\nimport { appearance, buffers, type Channels } from \"./graph-model\";\nimport type { Look } from \"./graph-looks\";\nimport { whenReady } from \"./when-ready\";\n\n/**\n * Putting a look on the canvas, and keeping it there when the theme flips.\n *\n * Four buffer uploads and one `setConfig` — no query, no restart, no rebuilt graph. That is the\n * whole reason a look is data rather than a variant of the component: changing one costs the GPU a\n * few arrays and costs the database nothing.\n *\n * Two effects, not one, because those are two different costs. The buffers depend on the data, the\n * look and the theme; the config depends on the same three and is far cheaper. Fused, every tick of\n * the Edge opacity and Node size sliders rebuilt all four arrays and re-uploaded them: 41,440 bytes\n * on this corpus — 31,440 of buffer plus the 10,000-byte padded float texture cosmos.gl expands the\n * sizes into — and six forced style recalcs, because `buffers` resolves every token off the live\n * DOM. All of it to move two scalars the GPU reads from a uniform.\n *\n * Those two sliders are gone — see `lookFrom` — but the split stays, and it stays for what is left:\n * `renderLinks` and `linkOpacity` are uniforms, so turning the edge layer off uploads nothing.\n *\n * The theme half is the part that is easy to forget. A look names its colours as `var(--chart-1)`,\n * and those tokens change under the reader — the `.dark` flip, and a tenant's palette document\n * swapped underneath — without React having any reason to re-render. `useThemeTick` watches\n * `<html>` for exactly that.\n *\n * It is the library's, not a local copy. The copy this replaced filtered attributes down to\n * `class`, `style` and `data-theme`, so any axis added after it was written stopped repainting the\n * graph — a filter is a list of the axes that existed the day someone typed it.\n */\nconst noop = () => {};\n\nexport function useGraphLook(options: {\n getGraph: () => Graph | null;\n /** The element the tokens are resolved against — inside the canvas' own tree. */\n hostRef: React.RefObject<HTMLElement | null>;\n /** The answer currently drawn. Every slice is a fresh set of points, so every slice repaints. */\n slice: Slice | null;\n look: Look;\n /**\n * What each channel is bound to.\n *\n * Only the bindings the *buffers* read arrive here — `fill` when it is a constant, `symbol`,\n * `stroke`. `fill` as a column name and `r` name columns a query has to fetch, so those reach the\n * source instead. One vocabulary at the call site, two destinations underneath.\n */\n channels?: Channels;\n /**\n * Ask the overlays to reposition: point sizes changed, so the labels sit differently.\n *\n * Optional, because it is only owed to `useGraphOverlays` — a host drawing no labels has no\n * overlays to reposition, and was writing a no-op to say so.\n */\n schedule?: () => void;\n}): void {\n const { channels, getGraph, hostRef, look, schedule = noop, slice } = options;\n const themeTick = useThemeTick();\n\n useEffect(() => {\n const graph = getGraph();\n const host = hostRef.current;\n if (!slice || !graph || !host) return;\n const { colors, linkColors, shapes, sizes } = buffers(slice, look, host, channels);\n // **This is the quiet half of the readiness race and the one that cost the most to find.**\n // These four raise a dirty flag and return, so a call before the device exists is not an error\n // anywhere — it is simply a picture that never gets uploaded. Geometry can arrive correctly,\n // the badge can report a full slice, and every point is still drawn at no size in no colour.\n return whenReady(graph, (ready) => {\n ready.setPointColors(colors);\n ready.setPointSizes(sizes);\n ready.setPointShapes(shapes);\n ready.setLinkColors(linkColors);\n });\n }, [channels, getGraph, hostRef, look, slice, themeTick]);\n\n // The paint, once, for both. These deps are a superset of the ones above, so whenever the buffers\n // are rebuilt this runs in the same commit and right after — and `setPointColors` and friends only\n // raise a dirty flag, which `render()` is what discharges. Painting in both would mean two\n // `graph.update()` passes (a full re-derivation, adjacency lists included) for one change.\n useEffect(() => {\n const graph = getGraph();\n const host = hostRef.current;\n if (!slice || !graph || !host) return;\n return whenReady(graph, (ready) => {\n ready.setConfigPartial(appearance(look, host));\n ready.render();\n schedule();\n });\n }, [getGraph, hostRef, look, schedule, slice, themeTick]);\n}\n"],"names":[],"mappings":";;;;;AAsCA;AAAoB;AAEb;AAuBL;AAGA;AACE;AAEA;AACA;AAKA;AACE;AAG8B;AAC/B;AAQD;AAEA;AACA;AACE;AAEA;AACD;AAEL;;;;"}
|
|
@@ -0,0 +1,53 @@
|
|
|
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;
|
|
25
|
+
export interface GraphOverlays {
|
|
26
|
+
/** The box the overlays are positioned within — the canvas' own bounds. */
|
|
27
|
+
hostRef: React.RefObject<HTMLDivElement | null>;
|
|
28
|
+
gridRef: React.RefObject<HTMLDivElement | null>;
|
|
29
|
+
cardRef: React.RefObject<HTMLDivElement | null>;
|
|
30
|
+
/** A `ref` callback for a given vertex's label. */
|
|
31
|
+
labelRef: (vertex: VertexId) => (element: HTMLElement | null) => void;
|
|
32
|
+
/** The labelled vertices, in the order the declutter pass should place them. */
|
|
33
|
+
setLabelOrder: (vertices: VertexId[]) => void;
|
|
34
|
+
setHovered: (vertex: VertexId | null) => void;
|
|
35
|
+
/**
|
|
36
|
+
* Re-register the tracked points with cosmos.gl.
|
|
37
|
+
*
|
|
38
|
+
* Call it after the graph exists and whenever the set of overlaid nodes changes. It has to be
|
|
39
|
+
* driven from outside because effects run in declaration order, so this hook's own effects cannot
|
|
40
|
+
* see a graph that a later hook is about to construct — and that construction is exactly what
|
|
41
|
+
* clears the registration.
|
|
42
|
+
*/
|
|
43
|
+
track: () => void;
|
|
44
|
+
/** Ask for a repaint. Coalesced — many calls in a frame cost one. */
|
|
45
|
+
schedule: () => void;
|
|
46
|
+
}
|
|
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;
|
|
53
|
+
//# sourceMappingURL=use-graph-overlays.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,96 @@
|
|
|
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));
|
|
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(() => {
|
|
7
|
+
const t = l();
|
|
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(() => {
|
|
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
|
+
}, B = [], g = S.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";
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const [c, h] = t.spaceToScreenPosition(a);
|
|
29
|
+
let s = L.current.get(n);
|
|
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) {
|
|
33
|
+
e.style.opacity = "0";
|
|
34
|
+
continue;
|
|
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";
|
|
37
|
+
}
|
|
38
|
+
const k = z.current;
|
|
39
|
+
if (k) {
|
|
40
|
+
const n = t.getZoomLevel();
|
|
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`;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const y = O.current, D = E.current;
|
|
47
|
+
if (y && D !== null && g) {
|
|
48
|
+
const n = u(D);
|
|
49
|
+
if (n) {
|
|
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;
|
|
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";
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}, [l, f]);
|
|
60
|
+
H(() => {
|
|
61
|
+
const t = T.current;
|
|
62
|
+
if (!t) return;
|
|
63
|
+
const o = new ResizeObserver(([i]) => {
|
|
64
|
+
const u = i == null ? void 0 : i.contentRect;
|
|
65
|
+
u && (S.current = { width: u.width, height: u.height });
|
|
66
|
+
});
|
|
67
|
+
return o.observe(t), () => o.disconnect();
|
|
68
|
+
}, []);
|
|
69
|
+
const W = x(() => {
|
|
70
|
+
p.current || (p.current = requestAnimationFrame(() => {
|
|
71
|
+
p.current = 0, _();
|
|
72
|
+
}));
|
|
73
|
+
}, [_]);
|
|
74
|
+
H(
|
|
75
|
+
() => () => {
|
|
76
|
+
p.current && cancelAnimationFrame(p.current), p.current = 0;
|
|
77
|
+
},
|
|
78
|
+
[]
|
|
79
|
+
);
|
|
80
|
+
const q = x(
|
|
81
|
+
(t) => (o) => {
|
|
82
|
+
o ? A.current.set(t, o) : A.current.delete(t);
|
|
83
|
+
},
|
|
84
|
+
[]
|
|
85
|
+
), Z = x((t) => {
|
|
86
|
+
m.current = t, L.current.clear();
|
|
87
|
+
}, []), j = x((t) => {
|
|
88
|
+
E.current = t, $.current = null;
|
|
89
|
+
}, []);
|
|
90
|
+
return { hostRef: T, gridRef: z, cardRef: O, labelRef: q, setLabelOrder: Z, setHovered: j, track: F, schedule: W };
|
|
91
|
+
}
|
|
92
|
+
export {
|
|
93
|
+
Q as GRID,
|
|
94
|
+
tt as useGraphOverlays
|
|
95
|
+
};
|
|
96
|
+
//# sourceMappingURL=use-graph-overlays.js.map
|
|
@@ -0,0 +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;;;;;"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { default as React } from 'react';
|
|
2
|
+
import { Graph } from '@cosmos.gl/graph';
|
|
3
|
+
import { Resident, VertexId } from './resident';
|
|
4
|
+
import { Selection, SelectionSource, Tool } from './types';
|
|
5
|
+
/**
|
|
6
|
+
* Drawing a selection on the canvas: the marquee, the lasso, and the keys that modify them.
|
|
7
|
+
*
|
|
8
|
+
* The whole gesture lives here — what is being drawn, how many nodes it currently holds, and what
|
|
9
|
+
* happens when the pointer comes up. The component gets handlers to spread onto an overlay and the
|
|
10
|
+
* state to draw it with; it does not need to know that Shift borrows the marquee or that a hit test
|
|
11
|
+
* costs a GPU readback.
|
|
12
|
+
*
|
|
13
|
+
* Modifiers are read at *release*, not at press, because that is when the reader has decided:
|
|
14
|
+
* `Alt` removes what was drawn from the selection, `⌘`/`Ctrl` adds it, and neither replaces.
|
|
15
|
+
*/
|
|
16
|
+
export type Point = [number, number];
|
|
17
|
+
/** A selection gesture in flight. The two tools differ only in what they accumulate. */
|
|
18
|
+
export type Drag = {
|
|
19
|
+
tool: "rect";
|
|
20
|
+
from: Point;
|
|
21
|
+
to: Point;
|
|
22
|
+
} | {
|
|
23
|
+
tool: "lasso";
|
|
24
|
+
path: Point[];
|
|
25
|
+
};
|
|
26
|
+
/** The running count rides just off the cursor, at the edge the gesture is growing from. */
|
|
27
|
+
export declare function cursorChip(drag: Drag | null): React.CSSProperties;
|
|
28
|
+
export interface GraphSelectionGesture {
|
|
29
|
+
/** The gesture being drawn, for the overlay to render. */
|
|
30
|
+
drag: Drag | null;
|
|
31
|
+
/** How many nodes it holds right now, so the number lands before the mouse does. */
|
|
32
|
+
preview: number | null;
|
|
33
|
+
/** The tool actually in force: the chosen one, or the marquee Shift is lending. */
|
|
34
|
+
active: Tool;
|
|
35
|
+
handlers: {
|
|
36
|
+
onPointerDown: (event: React.PointerEvent) => void;
|
|
37
|
+
onPointerMove: (event: React.PointerEvent) => void;
|
|
38
|
+
onPointerUp: (event: React.PointerEvent) => void;
|
|
39
|
+
onPointerCancel: () => void;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export interface GraphSelectionOptions {
|
|
43
|
+
getGraph: () => Graph | null;
|
|
44
|
+
/**
|
|
45
|
+
* Who is drawn right now — the only thing that can turn a hit-test index into an identity.
|
|
46
|
+
*
|
|
47
|
+
* A gesture selects positions on screen, and a position is a buffer index, which the next
|
|
48
|
+
* residency reuses for a different vertex. So the gesture resolves to identities here and now,
|
|
49
|
+
* while the answer that produced them is still the one on screen.
|
|
50
|
+
*/
|
|
51
|
+
getResident: () => Resident;
|
|
52
|
+
/** The live selection, for the modifiers to add to or subtract from. */
|
|
53
|
+
getSelection: () => Selection | null;
|
|
54
|
+
commit: (vertices: Set<VertexId> | null, source: SelectionSource, label: string) => void;
|
|
55
|
+
tool: Tool;
|
|
56
|
+
setTool: (tool: Tool) => void;
|
|
57
|
+
}
|
|
58
|
+
export declare function useGraphSelection(options: GraphSelectionOptions): GraphSelectionGesture;
|
|
59
|
+
//# sourceMappingURL=use-graph-selection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-graph-selection.d.ts","sourceRoot":"","sources":["../src/use-graph-selection.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACrD,OAAO,KAAK,EAAE,SAAS,EAAE,eAAe,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAGhE;;;;;;;;;;GAUG;AACH,MAAM,MAAM,KAAK,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAErC,wFAAwF;AACxF,MAAM,MAAM,IAAI,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,KAAK,CAAC;IAAC,EAAE,EAAE,KAAK,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,KAAK,EAAE,CAAA;CAAE,CAAC;AAS/F,4FAA4F;AAC5F,wBAAgB,UAAU,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,GAAG,KAAK,CAAC,aAAa,CAIjE;AAED,MAAM,WAAW,qBAAqB;IACpC,0DAA0D;IAC1D,IAAI,EAAE,IAAI,GAAG,IAAI,CAAC;IAClB,oFAAoF;IACpF,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,mFAAmF;IACnF,MAAM,EAAE,IAAI,CAAC;IACb,QAAQ,EAAE;QACR,aAAa,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,YAAY,KAAK,IAAI,CAAC;QACnD,aAAa,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,YAAY,KAAK,IAAI,CAAC;QACnD,WAAW,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,YAAY,KAAK,IAAI,CAAC;QACjD,eAAe,EAAE,MAAM,IAAI,CAAC;KAC7B,CAAC;CACH;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,MAAM,KAAK,GAAG,IAAI,CAAC;IAC7B;;;;;;OAMG;IACH,WAAW,EAAE,MAAM,QAAQ,CAAC;IAC5B,wEAAwE;IACxE,YAAY,EAAE,MAAM,SAAS,GAAG,IAAI,CAAC;IACrC,MAAM,EAAE,CAAC,QAAQ,EAAE,GAAG,CAAC,QAAQ,CAAC,GAAG,IAAI,EAAE,MAAM,EAAE,eAAe,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACzF,IAAI,EAAE,IAAI,CAAC;IACX,OAAO,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,IAAI,CAAC;CAC/B;AAOD,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,qBAAqB,GAAG,qBAAqB,CA6IvF"}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useState as m, useRef as C, useCallback as P, useEffect as I } from "react";
|
|
3
|
+
import { isReady as K } from "./when-ready.js";
|
|
4
|
+
const x = 3, _ = 4, q = 60;
|
|
5
|
+
function N(l) {
|
|
6
|
+
if (!l) return { display: "none" };
|
|
7
|
+
const [i = 0, a = 0] = l.tool === "rect" ? l.to : l.path[l.path.length - 1] ?? [];
|
|
8
|
+
return { left: i + 14, top: a + 14 };
|
|
9
|
+
}
|
|
10
|
+
const z = {
|
|
11
|
+
rect: { source: "marquee", label: "Marquee" },
|
|
12
|
+
lasso: { source: "lasso", label: "Lasso" }
|
|
13
|
+
};
|
|
14
|
+
function U(l) {
|
|
15
|
+
const { commit: i, getGraph: a, getResident: k, getSelection: v, setTool: y, tool: h } = l, [s, c] = m(null), [L, f] = m(null), [R, p] = m(!1), b = C(0), d = P(
|
|
16
|
+
(t) => {
|
|
17
|
+
const e = a();
|
|
18
|
+
if (!e) return [];
|
|
19
|
+
if (!K(e)) return [];
|
|
20
|
+
if (t.tool === "rect") {
|
|
21
|
+
const [[o, n], [r, u]] = [t.from, t.to];
|
|
22
|
+
return Math.abs(r - o) < x || Math.abs(u - n) < x ? [] : Array.from(
|
|
23
|
+
e.findPointsInRect([
|
|
24
|
+
[Math.min(o, r), Math.min(n, u)],
|
|
25
|
+
[Math.max(o, r), Math.max(n, u)]
|
|
26
|
+
])
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
return t.path.length < 3 ? [] : Array.from(e.findPointsInPolygon(t.path));
|
|
30
|
+
},
|
|
31
|
+
[a]
|
|
32
|
+
), E = P(
|
|
33
|
+
(t) => {
|
|
34
|
+
const e = performance.now();
|
|
35
|
+
e - b.current < q || (b.current = e, f(d(t).length));
|
|
36
|
+
},
|
|
37
|
+
[d]
|
|
38
|
+
);
|
|
39
|
+
I(() => {
|
|
40
|
+
const t = (n) => {
|
|
41
|
+
n.key === "Shift" && p(!0), n.key === "Escape" && (c((r) => (r || (h ? y(null) : i(null, "node", "")), null)), f(null));
|
|
42
|
+
}, e = (n) => {
|
|
43
|
+
n.key === "Shift" && p(!1);
|
|
44
|
+
}, o = () => p(!1);
|
|
45
|
+
return window.addEventListener("keydown", t), window.addEventListener("keyup", e), window.addEventListener("blur", o), () => {
|
|
46
|
+
window.removeEventListener("keydown", t), window.removeEventListener("keyup", e), window.removeEventListener("blur", o);
|
|
47
|
+
};
|
|
48
|
+
}, [i, y, h]);
|
|
49
|
+
const M = (t) => {
|
|
50
|
+
const e = t.currentTarget.getBoundingClientRect();
|
|
51
|
+
return [t.clientX - e.left, t.clientY - e.top];
|
|
52
|
+
}, T = (t, e) => {
|
|
53
|
+
var g;
|
|
54
|
+
if (c(null), f(null), !t) return;
|
|
55
|
+
const o = new Set(k().verticesAt(d(t)));
|
|
56
|
+
if (o.size === 0 && !e.altKey) return;
|
|
57
|
+
const { label: n, source: r } = z[t.tool], u = ((g = v()) == null ? void 0 : g.vertices) ?? [];
|
|
58
|
+
if (e.altKey) {
|
|
59
|
+
const w = new Set(u);
|
|
60
|
+
for (const A of o) w.delete(A);
|
|
61
|
+
i(w.size > 0 ? w : null, r, n);
|
|
62
|
+
} else e.metaKey || e.ctrlKey ? i(/* @__PURE__ */ new Set([...u, ...o]), r, n) : i(o, r, n);
|
|
63
|
+
}, S = h ?? (R ? "rect" : null);
|
|
64
|
+
return {
|
|
65
|
+
drag: s,
|
|
66
|
+
preview: L,
|
|
67
|
+
active: S,
|
|
68
|
+
handlers: {
|
|
69
|
+
onPointerDown: (t) => {
|
|
70
|
+
if (t.button !== 0) return;
|
|
71
|
+
t.currentTarget.setPointerCapture(t.pointerId);
|
|
72
|
+
const e = M(t);
|
|
73
|
+
c(
|
|
74
|
+
S === "rect" ? { tool: "rect", from: e, to: e } : { tool: "lasso", path: [e] }
|
|
75
|
+
);
|
|
76
|
+
},
|
|
77
|
+
onPointerMove: (t) => {
|
|
78
|
+
if (!s) return;
|
|
79
|
+
const e = M(t);
|
|
80
|
+
if (s.tool === "rect") {
|
|
81
|
+
const r = { ...s, to: e };
|
|
82
|
+
c(r), E(r);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const o = s.path[s.path.length - 1];
|
|
86
|
+
if (Math.hypot(e[0] - o[0], e[1] - o[1]) < _) return;
|
|
87
|
+
const n = { tool: "lasso", path: [...s.path, e] };
|
|
88
|
+
c(n), E(n);
|
|
89
|
+
},
|
|
90
|
+
onPointerUp: (t) => T(s, t),
|
|
91
|
+
onPointerCancel: () => {
|
|
92
|
+
c(null), f(null);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
export {
|
|
98
|
+
N as cursorChip,
|
|
99
|
+
U as useGraphSelection
|
|
100
|
+
};
|
|
101
|
+
//# sourceMappingURL=use-graph-selection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-graph-selection.js","sources":["../src/use-graph-selection.ts"],"sourcesContent":["\"use client\";\n\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport type React from \"react\";\nimport type { Graph } from \"@cosmos.gl/graph\";\nimport type { Resident, VertexId } from \"./resident\";\nimport type { Selection, SelectionSource, Tool } from \"./types\";\nimport { isReady } from \"./when-ready\";\n\n/**\n * Drawing a selection on the canvas: the marquee, the lasso, and the keys that modify them.\n *\n * The whole gesture lives here — what is being drawn, how many nodes it currently holds, and what\n * happens when the pointer comes up. The component gets handlers to spread onto an overlay and the\n * state to draw it with; it does not need to know that Shift borrows the marquee or that a hit test\n * costs a GPU readback.\n *\n * Modifiers are read at *release*, not at press, because that is when the reader has decided:\n * `Alt` removes what was drawn from the selection, `⌘`/`Ctrl` adds it, and neither replaces.\n */\nexport type Point = [number, number];\n\n/** A selection gesture in flight. The two tools differ only in what they accumulate. */\nexport type Drag = { tool: \"rect\"; from: Point; to: Point } | { tool: \"lasso\"; path: Point[] };\n\n/** Below this the hit test would be answering about a click, not a box. */\nconst MIN_RECT = 3;\n/** A raw pointer stream makes a polygon of near-duplicate vertices; the hit test walks every one. */\nconst PATH_STEP = 4;\n/** Slower than the hand, faster than the eye — and one GPU readback per gesture frame is plenty. */\nconst MEASURE_MS = 60;\n\n/** The running count rides just off the cursor, at the edge the gesture is growing from. */\nexport function cursorChip(drag: Drag | null): React.CSSProperties {\n if (!drag) return { display: \"none\" };\n const [x = 0, y = 0] = drag.tool === \"rect\" ? drag.to : (drag.path[drag.path.length - 1] ?? []);\n return { left: x + 14, top: y + 14 };\n}\n\nexport interface GraphSelectionGesture {\n /** The gesture being drawn, for the overlay to render. */\n drag: Drag | null;\n /** How many nodes it holds right now, so the number lands before the mouse does. */\n preview: number | null;\n /** The tool actually in force: the chosen one, or the marquee Shift is lending. */\n active: Tool;\n handlers: {\n onPointerDown: (event: React.PointerEvent) => void;\n onPointerMove: (event: React.PointerEvent) => void;\n onPointerUp: (event: React.PointerEvent) => void;\n onPointerCancel: () => void;\n };\n}\n\nexport interface GraphSelectionOptions {\n getGraph: () => Graph | null;\n /**\n * Who is drawn right now — the only thing that can turn a hit-test index into an identity.\n *\n * A gesture selects positions on screen, and a position is a buffer index, which the next\n * residency reuses for a different vertex. So the gesture resolves to identities here and now,\n * while the answer that produced them is still the one on screen.\n */\n getResident: () => Resident;\n /** The live selection, for the modifiers to add to or subtract from. */\n getSelection: () => Selection | null;\n commit: (vertices: Set<VertexId> | null, source: SelectionSource, label: string) => void;\n tool: Tool;\n setTool: (tool: Tool) => void;\n}\n\nconst NAME: Record<\"rect\" | \"lasso\", { source: SelectionSource; label: string }> = {\n rect: { source: \"marquee\", label: \"Marquee\" },\n lasso: { source: \"lasso\", label: \"Lasso\" },\n};\n\nexport function useGraphSelection(options: GraphSelectionOptions): GraphSelectionGesture {\n const { commit, getGraph, getResident, getSelection, setTool, tool } = options;\n const [drag, setDrag] = useState<Drag | null>(null);\n const [preview, setPreview] = useState<number | null>(null);\n const [shift, setShift] = useState(false);\n const measured = useRef(0);\n\n const hitTest = useCallback(\n (shape: Drag): number[] => {\n const graph = getGraph();\n if (!graph) return [];\n // **Upstream's own words, on these two and on nothing else in its `.d.ts`:** *this method is\n // synchronous and must only be called when the graph is ready*. There is nothing to await in\n // a pointer handler, so the answer for a device that is not there is the honest one — a\n // gesture over a graph that has not drawn selects nothing, which is what it looks like.\n if (!isReady(graph)) return [];\n if (shape.tool === \"rect\") {\n const [[ax, ay], [bx, by]] = [shape.from, shape.to];\n if (Math.abs(bx - ax) < MIN_RECT || Math.abs(by - ay) < MIN_RECT) return [];\n return Array.from(\n graph.findPointsInRect([\n [Math.min(ax, bx), Math.min(ay, by)],\n [Math.max(ax, bx), Math.max(ay, by)],\n ]),\n );\n }\n if (shape.path.length < 3) return [];\n return Array.from(graph.findPointsInPolygon(shape.path));\n },\n [getGraph],\n );\n\n // Both hit tests are a GPU pass followed by a synchronous `readPixels`, so running one per\n // pointer event would stall the frame the gesture is being drawn into.\n const measure = useCallback(\n (shape: Drag) => {\n const now = performance.now();\n if (now - measured.current < MEASURE_MS) return;\n measured.current = now;\n setPreview(hitTest(shape).length);\n },\n [hitTest],\n );\n\n // Escape is the way out of everything, in layers: it abandons a drag in progress, then drops the\n // tool, then clears what is held. And a window that loses focus never delivers the Shift keyup,\n // which would leave the canvas unable to pan.\n useEffect(() => {\n const down = (event: KeyboardEvent) => {\n if (event.key === \"Shift\") setShift(true);\n if (event.key !== \"Escape\") return;\n setDrag((current) => {\n if (current) return null;\n if (tool) setTool(null);\n else commit(null, \"node\", \"\");\n return null;\n });\n setPreview(null);\n };\n const up = (event: KeyboardEvent) => {\n if (event.key === \"Shift\") setShift(false);\n };\n const blur = () => setShift(false);\n window.addEventListener(\"keydown\", down);\n window.addEventListener(\"keyup\", up);\n window.addEventListener(\"blur\", blur);\n return () => {\n window.removeEventListener(\"keydown\", down);\n window.removeEventListener(\"keyup\", up);\n window.removeEventListener(\"blur\", blur);\n };\n }, [commit, setTool, tool]);\n\n /** Screen coordinates inside the canvas — the space every cosmos.gl hit test speaks. */\n const at = (event: React.PointerEvent): Point => {\n const box = event.currentTarget.getBoundingClientRect();\n return [event.clientX - box.left, event.clientY - box.top];\n };\n\n const finish = (shape: Drag | null, event: React.PointerEvent) => {\n setDrag(null);\n setPreview(null);\n if (!shape) return;\n // The one line the whole gesture turns on: the hit test answers in buffer indices, and they stop\n // meaning anything the moment the resident set changes.\n const vertices = new Set(getResident().verticesAt(hitTest(shape)));\n // A gesture that caught nothing and asked for nothing is a misfire, not a request to clear —\n // clearing is what the corner's own button and Escape are for.\n if (vertices.size === 0 && !event.altKey) return;\n const { label, source } = NAME[shape.tool];\n const held = getSelection()?.vertices ?? [];\n if (event.altKey) {\n const next = new Set(held);\n for (const vertex of vertices) next.delete(vertex);\n commit(next.size > 0 ? next : null, source, label);\n } else if (event.metaKey || event.ctrlKey) {\n commit(new Set([...held, ...vertices]), source, label);\n } else {\n commit(vertices, source, label);\n }\n };\n\n const active: Tool = tool ?? (shift ? \"rect\" : null);\n\n return {\n drag,\n preview,\n active,\n handlers: {\n onPointerDown: (event) => {\n if (event.button !== 0) return;\n event.currentTarget.setPointerCapture(event.pointerId);\n const start = at(event);\n setDrag(\n active === \"rect\"\n ? { tool: \"rect\", from: start, to: start }\n : { tool: \"lasso\", path: [start] },\n );\n },\n onPointerMove: (event) => {\n if (!drag) return;\n const next = at(event);\n if (drag.tool === \"rect\") {\n const shape: Drag = { ...drag, to: next };\n setDrag(shape);\n measure(shape);\n return;\n }\n const last = drag.path[drag.path.length - 1] as Point;\n if (Math.hypot(next[0] - last[0], next[1] - last[1]) < PATH_STEP) return;\n const shape: Drag = { tool: \"lasso\", path: [...drag.path, next] };\n setDrag(shape);\n measure(shape);\n },\n onPointerUp: (event) => finish(drag, event),\n onPointerCancel: () => {\n setDrag(null);\n setPreview(null);\n },\n },\n };\n}\n"],"names":[],"mappings":";;;AA0BA;AAOO;AACL;AACA;AACA;AACF;AAkCA;AAAmF;AAC/C;AAEpC;AAEO;AACL;AAMgB;AAEZ;AACA;AAKA;AACA;AACE;AACA;AACa;AACY;AACc;AACA;AACpC;AAAA;AAGL;AACuD;AACzD;AACS;AAKK;AAEZ;AACA;AAEgC;AAClC;AACQ;AAMV;AACE;AAEE;AAOe;AAGf;AAAyC;AAG3C;AAIE;AAEuC;AACzC;AAIF;AACE;AACA;AAAyD;;AAMzD;AAGA;AAGA;AACA;AAEA;AACE;AACA;AACA;AAAiD;AAInB;AAMlC;AAAO;AACL;AACA;AACA;AACU;AAEN;AACA;AACA;AACA;AAAA;AAGmC;AAAE;AAEvC;AAEE;AACA;AACA;AACE;AACA;AAEA;AAAA;AAEF;AACA;AACA;AACA;AACa;AACf;AAC0C;AAExC;AACe;AACjB;AAAA;AAGN;;;;;"}
|