@loradb/lora-graph-canvas 0.10.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 (64) hide show
  1. package/LICENSE +87 -0
  2. package/README.md +191 -0
  3. package/THIRD_PARTY.md +40 -0
  4. package/dist/LoraGraphCanvas.d.ts +9 -0
  5. package/dist/LoraGraphCanvas.stories.d.ts +23 -0
  6. package/dist/engines/3d-force-graph/index.d.ts +1 -0
  7. package/dist/engines/3d-force-graph/kapsule.d.ts +12 -0
  8. package/dist/engines/createEngineUnified.d.ts +13 -0
  9. package/dist/engines/propBindings.d.ts +29 -0
  10. package/dist/engines/rafAnim.d.ts +13 -0
  11. package/dist/engines/types.d.ts +86 -0
  12. package/dist/hooks/useAccessorOverrides.d.ts +62 -0
  13. package/dist/hooks/useAutoIndexNeighbors.d.ts +6 -0
  14. package/dist/hooks/useClickToleranceShim.d.ts +46 -0
  15. package/dist/hooks/useGraphClipboard.d.ts +47 -0
  16. package/dist/hooks/useGraphData.d.ts +42 -0
  17. package/dist/hooks/useGraphEngine.d.ts +23 -0
  18. package/dist/hooks/useGraphForces.d.ts +12 -0
  19. package/dist/hooks/useGraphKeybindings.d.ts +27 -0
  20. package/dist/hooks/useGraphSelection.d.ts +14 -0
  21. package/dist/hooks/useHoverState.d.ts +67 -0
  22. package/dist/hooks/useImperativeGraphHandle.d.ts +22 -0
  23. package/dist/hooks/useLabelRenderer.d.ts +47 -0
  24. package/dist/hooks/useLinkLabelRenderer.d.ts +56 -0
  25. package/dist/hooks/useMarqueeAndCursor.d.ts +59 -0
  26. package/dist/hooks/usePerfTierDefaults.d.ts +13 -0
  27. package/dist/hooks/useResizeObserver.d.ts +7 -0
  28. package/dist/hooks/useShiftHeld.d.ts +5 -0
  29. package/dist/index.cjs +685 -0
  30. package/dist/index.cjs.map +1 -0
  31. package/dist/index.d.ts +5 -0
  32. package/dist/index.js +11309 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/internal/accessor-fn.d.ts +2 -0
  35. package/dist/internal/bezier.d.ts +16 -0
  36. package/dist/internal/canvas-color-tracker.d.ts +13 -0
  37. package/dist/internal/debounce.d.ts +5 -0
  38. package/dist/internal/float-tooltip.d.ts +14 -0
  39. package/dist/internal/kapsule-link.d.ts +24 -0
  40. package/dist/internal/kapsule.d.ts +43 -0
  41. package/dist/internal/throttle.d.ts +6 -0
  42. package/dist/internal/tween.d.ts +31 -0
  43. package/dist/style.css +1 -0
  44. package/dist/theme/presets.d.ts +8 -0
  45. package/dist/tools/ContextMenu.d.ts +17 -0
  46. package/dist/tools/GraphToolbar.d.ts +18 -0
  47. package/dist/tools/GroupLegend.d.ts +11 -0
  48. package/dist/tools/HoverTooltip.d.ts +15 -0
  49. package/dist/tools/MarqueeOverlay.d.ts +13 -0
  50. package/dist/tools/ModeToggle.d.ts +10 -0
  51. package/dist/tools/OptionsMenu.d.ts +28 -0
  52. package/dist/tools/SelectionPanel.d.ts +18 -0
  53. package/dist/tools/icons.d.ts +23 -0
  54. package/dist/tools/tools.d.ts +37 -0
  55. package/dist/types.d.ts +375 -0
  56. package/dist/utils/accessor.d.ts +36 -0
  57. package/dist/utils/download.d.ts +4 -0
  58. package/dist/utils/geometry.d.ts +8 -0
  59. package/dist/utils/grid.d.ts +9 -0
  60. package/dist/utils/ids.d.ts +3 -0
  61. package/dist/utils/perfTier.d.ts +29 -0
  62. package/dist/utils/spriteLabel.d.ts +61 -0
  63. package/dist/utils/themeStyle.d.ts +5 -0
  64. package/package.json +105 -0
package/LICENSE ADDED
@@ -0,0 +1,87 @@
1
+ Business Source License 1.1
2
+
3
+ Parameters
4
+
5
+ Licensor: LoraDB, Inc.
6
+ Licensed Work: LoraDB
7
+ Change Date: 2029-04-19
8
+ Change License: Apache License, Version 2.0
9
+
10
+ Additional Use Grant: You may use, copy, modify, create derivative works of,
11
+ distribute, and make production use of the Licensed Work
12
+ for internal business purposes and non-production
13
+ purposes, provided that you do not use the Licensed Work
14
+ to offer, operate, or make available a database-as-a-
15
+ service, hosted API, managed database platform, or any
16
+ substantially similar hosted service for third parties.
17
+
18
+ The Additional Use Grant does not permit using the
19
+ Licensed Work to provide a commercial or non-commercial
20
+ hosted service that allows third parties to access LoraDB
21
+ functionality, including as part of a competing managed
22
+ database platform, backend-as-a-service, application
23
+ platform, developer platform, or resale offering.
24
+
25
+ Business Source License
26
+
27
+ Terms
28
+
29
+ The Licensor hereby grants you the right to copy, modify, create derivative
30
+ works, redistribute, and make non-production use of the Licensed Work. The
31
+ Licensor may make an Additional Use Grant, above, permitting limited production
32
+ use.
33
+
34
+ Effective on the Change Date, or the fourth anniversary of the first publicly
35
+ available distribution of a specific version of the Licensed Work under this
36
+ License, whichever comes first, the Licensor hereby grants you rights under
37
+ the terms of the Change License, and the rights granted in the paragraph
38
+ above terminate.
39
+
40
+ If your use of the Licensed Work does not comply with the requirements
41
+ currently in effect as described in this License, you must purchase a
42
+ commercial license from the Licensor, its affiliated entities, or authorized
43
+ resellers, or you must refrain from using the Licensed Work.
44
+
45
+ All copies of the original and modified Licensed Work, and derivative works
46
+ of the Licensed Work, are subject to this License. This License applies
47
+ separately for each version of the Licensed Work and the Change Date may vary
48
+ for each version of the Licensed Work released by Licensor.
49
+
50
+ You must conspicuously display this License on each original or modified copy
51
+ of the Licensed Work. If you receive the Licensed Work in original or modified
52
+ form from a third party, the terms and conditions set forth in this License
53
+ apply to your use of that work.
54
+
55
+ Any use of the Licensed Work in violation of this License will automatically
56
+ terminate your rights under this License for the current and all other
57
+ versions of the Licensed Work.
58
+
59
+ This License does not grant you any right in any trademark or logo of Licensor
60
+ or its affiliates.
61
+
62
+ TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON
63
+ AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS,
64
+ EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF MERCHANTABILITY,
65
+ FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND TITLE.
66
+
67
+ MariaDB hereby grants you permission to use this License's text to license
68
+ your works, and to refer to it using the trademark "Business Source License",
69
+ as long as you comply with the Covenants of Licensor below.
70
+
71
+ Covenants of Licensor
72
+
73
+ In consideration of the right to use this License's text and the "Business
74
+ Source License" name and trademark, Licensor covenants to MariaDB, and to all
75
+ other recipients of the licensed work to be provided by Licensor:
76
+
77
+ 1. To specify as the Change License the GPL Version 2.0 or any later version,
78
+ or a license that is compatible with GPL Version 2.0 or a later version,
79
+ where "compatible" means that software provided under the Change License can
80
+ be included in a program with software provided under GPL Version 2.0 or a
81
+ later version. Licensor may specify additional Change Licenses without
82
+ limitation.
83
+ 2. To either: (a) specify an additional grant of rights to use that does not
84
+ impose any additional restriction on the right granted in this License, as
85
+ the Additional Use Grant; or (b) insert the text "None".
86
+ 3. To specify a Change Date.
87
+ 4. Not to modify this License in any other way.
package/README.md ADDED
@@ -0,0 +1,191 @@
1
+ # @loradb/lora-graph-canvas
2
+
3
+ React graph canvas for LoraDB. One component, `<LoraGraphCanvas />`, that
4
+ wraps `force-graph` (2D HTML5 canvas) and `3d-force-graph` (WebGL via
5
+ Three.js). Switch between 2D and 3D at runtime without re-mounting your
6
+ data, and use the built-in tool palette to build, edit, select, and
7
+ delete nodes interactively.
8
+
9
+ ```tsx
10
+ import { LoraGraphCanvas } from "@loradb/lora-graph-canvas";
11
+ import "@loradb/lora-graph-canvas/styles.css";
12
+
13
+ <LoraGraphCanvas
14
+ defaultData={{
15
+ nodes: [{ id: "a" }, { id: "b" }],
16
+ links: [{ source: "a", target: "b" }],
17
+ }}
18
+ nodeLabel="id"
19
+ nodeAutoColorBy="group"
20
+ />;
21
+ ```
22
+
23
+ ## Install
24
+
25
+ ```sh
26
+ yarn add @loradb/lora-graph-canvas three
27
+ # or
28
+ npm install @loradb/lora-graph-canvas three
29
+ ```
30
+
31
+ `three` is a required peer dependency — the package itself does not bundle
32
+ Three.js, so 2D-only consumers can dedupe with the rest of the app's
33
+ Three usage, and 3D consumers can pin a specific version.
34
+
35
+ ## Built-in tools
36
+
37
+ The toolbar ships with twelve tools, controllable individually:
38
+
39
+ | Tool | Shortcut | What it does |
40
+ | ------------ | -------- | -------------------------------------------------- |
41
+ | Select | `V` | Click / shift-click nodes to select them. |
42
+ | Pan | `H` | Pan-only cursor. |
43
+ | Add node | `N` | Click on the canvas to drop a node. |
44
+ | Add link | `L` | Click two nodes to connect them. |
45
+ | Delete | `⌫` / `⌦` | Delete the current selection (cascades links). |
46
+ | Fit | `F` | `zoomToFit` to the current graph bbox. |
47
+ | Zoom in/out | `+` `-` | Step the zoom level. |
48
+ | Pause/Resume | — | Stop / start the d3 simulation. |
49
+ | Screenshot | — | Download a PNG of the canvas. |
50
+ | Toggle 2D/3D | `3` | Swap engines, preserving data and selection. |
51
+
52
+ ```tsx
53
+ // Pick a subset…
54
+ <LoraGraphCanvas tools={["select", "add-node", "delete", "toggle-mode"]} />
55
+
56
+ // …or hide the whole bar and drive everything from the ref:
57
+ <LoraGraphCanvas tools={false} ref={ref} />
58
+ ```
59
+
60
+ ## Ref handle
61
+
62
+ ```tsx
63
+ import { useRef } from "react";
64
+ import {
65
+ LoraGraphCanvas,
66
+ type LoraGraphCanvasHandle,
67
+ } from "@loradb/lora-graph-canvas";
68
+
69
+ const ref = useRef<LoraGraphCanvasHandle>(null);
70
+
71
+ ref.current?.addNode({ id: "x", label: "hello" });
72
+ ref.current?.addLink({ source: "x", target: "y" });
73
+ ref.current?.removeNode("x"); // also removes attached links
74
+ ref.current?.fit(400, 40);
75
+ ref.current?.setMode("3d");
76
+ const blob = await ref.current?.screenshot();
77
+ ```
78
+
79
+ Available methods: data (`getData`, `setData`, `addNode`, `addNodes`,
80
+ `updateNode`, `removeNode`, `removeNodes`, `addLink`, `addLinks`,
81
+ `removeLink`, `clear`); selection (`getSelection`, `setSelection`,
82
+ `selectAll`, `clearSelection`); view (`getMode`, `setMode`, `fit`,
83
+ `centerAt`, `zoom`, `zoomIn`, `zoomOut`); engine (`pause`, `resume`,
84
+ `reheat`, `screenshot`); escape hatches (`engine2D`, `engine3D`).
85
+
86
+ ## Controlled vs uncontrolled
87
+
88
+ ```tsx
89
+ // Uncontrolled — internal state owns the graph, host gets a notification:
90
+ <LoraGraphCanvas defaultData={initialData} onDataChange={save} />
91
+
92
+ // Controlled — host owns the graph:
93
+ <LoraGraphCanvas data={dataFromState} onDataChange={setDataState} />
94
+ ```
95
+
96
+ The same dichotomy applies to `mode` (`defaultMode` vs `mode`).
97
+
98
+ ## Theming
99
+
100
+ The chrome (toolbar, context menu, tooltips) is driven by `--lgc-*` CSS
101
+ variables. Override the ones you want either through the `theme` prop or
102
+ by attaching CSS to the `.lora-graph-canvas` container. The engine's own
103
+ canvas reads `backgroundColor` from a regular prop, independent of the
104
+ theme.
105
+
106
+ ```tsx
107
+ import { LoraGraphCanvas, darkTheme } from "@loradb/lora-graph-canvas";
108
+
109
+ <LoraGraphCanvas
110
+ backgroundColor="#0e1014"
111
+ theme={{ ...darkTheme, accent: "#ff6699" }}
112
+ />;
113
+ ```
114
+
115
+ Two presets are exported: `lightTheme` and `darkTheme`.
116
+
117
+ ## Performance knobs
118
+
119
+ For large graphs, cap `cooldownTicks` (default ∞) and increase
120
+ `warmupTicks` to spend more time computing layout off-screen before the
121
+ first paint:
122
+
123
+ ```tsx
124
+ <LoraGraphCanvas cooldownTicks={50} warmupTicks={20} />
125
+ ```
126
+
127
+ For graphs above ~10k nodes, switch the layout engine in 3D mode:
128
+
129
+ ```tsx
130
+ <LoraGraphCanvas mode="3d" forceEngine="ngraph" />
131
+ ```
132
+
133
+ ## Opt-in UX flags
134
+
135
+ Each is a single boolean prop:
136
+
137
+ | Prop | What it does |
138
+ | --- | --- |
139
+ | `focusOnClick` | Click a node → animated camera focus; click again to restore. |
140
+ | `highlightNeighborsOnHover` | Hover a node → it + its neighbours light up in the accent color. Pair with `autoIndexNeighbors` so the component builds the neighbour index for you, or stash `_neighbors` / `_links` yourself. |
141
+ | `autoIndexNeighbors` | After every data change, build `_neighbors` and `_links` arrays on each node — required by the highlight flag if you don't provide them. |
142
+ | `collideNodes` | Inject `d3-force-3d`'s `forceCollide` so circles don't overlap. Pass a number to override the radius. |
143
+ | `showGrid` | Faint background grid that adapts to the zoom level. 2D only. Pass `{ spacing, color }` to customise. |
144
+ | `showLegend` | Bottom-left widget enumerating `nodeAutoColorBy` groups with a colour swatch; click a group to toggle its visibility (drives `nodeVisibility` automatically). |
145
+ | `enableRename` | (Default true.) Double-click a node to rename it inline. Set false to suppress. |
146
+ | `enableClipboard` | (Default true.) ⌘C / ⌘X / ⌘V keybindings + matching ref methods. |
147
+
148
+ ```tsx
149
+ <LoraGraphCanvas
150
+ data={graph}
151
+ nodeAutoColorBy="group"
152
+ focusOnClick
153
+ highlightNeighborsOnHover
154
+ autoIndexNeighbors
155
+ collideNodes
156
+ showGrid
157
+ showLegend
158
+ />
159
+ ```
160
+
161
+ ## Animated particles for events
162
+
163
+ The kapsule emits an animated particle along a link via `emitParticle`.
164
+ Wire it through the ref to visualise events (data flow, message sent,
165
+ etc.):
166
+
167
+ ```tsx
168
+ const ref = useRef<LoraGraphCanvasHandle>(null);
169
+ useEffect(() => {
170
+ socket.on("message", (msg) => {
171
+ const link = graph.links.find((l) => l.id === msg.linkId);
172
+ if (link) ref.current?.emitParticle(link);
173
+ });
174
+ }, []);
175
+ ```
176
+
177
+ ## Custom forces
178
+
179
+ Inject d3-force primitives through `d3Force(name, fn)`:
180
+
181
+ ```tsx
182
+ import { forceRadial } from "d3-force-3d";
183
+
184
+ ref.current?.d3Force("radial", forceRadial(200));
185
+ ref.current?.reheat();
186
+ ```
187
+
188
+ ## License
189
+
190
+ BUSL-1.1. Third-party attributions for `force-graph` and `3d-force-graph`
191
+ (MIT, © Vasco Asturiano) live in `THIRD_PARTY.md`.
package/THIRD_PARTY.md ADDED
@@ -0,0 +1,40 @@
1
+ # Third-party notices
2
+
3
+ This package depends on the following open-source projects. Their license
4
+ texts are reproduced below in full.
5
+
6
+ ## force-graph
7
+
8
+ - Copyright (c) Vasco Asturiano
9
+ - License: MIT
10
+ - Source: https://github.com/vasturiano/force-graph
11
+
12
+ ```
13
+ MIT License
14
+
15
+ Permission is hereby granted, free of charge, to any person obtaining a
16
+ copy of this software and associated documentation files (the "Software"),
17
+ to deal in the Software without restriction, including without limitation
18
+ the rights to use, copy, modify, merge, publish, distribute, sublicense,
19
+ and/or sell copies of the Software, and to permit persons to whom the
20
+ Software is furnished to do so, subject to the following conditions:
21
+
22
+ The above copyright notice and this permission notice shall be included
23
+ in all copies or substantial portions of the Software.
24
+
25
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
26
+ OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
27
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
28
+ THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
29
+ OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
30
+ ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
31
+ OTHER DEALINGS IN THE SOFTWARE.
32
+ ```
33
+
34
+ ## 3d-force-graph
35
+
36
+ - Copyright (c) Vasco Asturiano
37
+ - License: MIT
38
+ - Source: https://github.com/vasturiano/3d-force-graph
39
+
40
+ The MIT license text above applies identically.
@@ -0,0 +1,9 @@
1
+ import { LinkObject, LoraGraphCanvasHandle, LoraGraphCanvasProps, NodeObject } from './types';
2
+ declare function LoraGraphCanvasInner<N extends NodeObject = NodeObject, L extends LinkObject = LinkObject>(props: LoraGraphCanvasProps<N, L>, ref: React.Ref<LoraGraphCanvasHandle<N, L>>): import("react/jsx-runtime").JSX.Element;
3
+ /** React component exporting both a default-instantiated and a generic
4
+ * signature. Consumers can pass `<LoraGraphCanvas<MyNode, MyLink> ... />`
5
+ * to constrain the node / link shape. */
6
+ export declare const LoraGraphCanvas: <N extends NodeObject = NodeObject, L extends LinkObject = LinkObject>(props: LoraGraphCanvasProps<N, L> & {
7
+ ref?: React.Ref<LoraGraphCanvasHandle<N, L>>;
8
+ }) => ReturnType<typeof LoraGraphCanvasInner>;
9
+ export {};
@@ -0,0 +1,23 @@
1
+ import { Meta, StoryObj } from '@storybook/react';
2
+ import { LoraGraphCanvas } from '.';
3
+ declare const meta: Meta<typeof LoraGraphCanvas>;
4
+ export default meta;
5
+ type Story = StoryObj<typeof LoraGraphCanvas>;
6
+ export declare const Basic: Story;
7
+ export declare const ThreeDimensional: Story;
8
+ export declare const ModeToggle: Story;
9
+ export declare const BuildAGraphStory: Story;
10
+ export declare const HeadlessStory: Story;
11
+ export declare const LargeGraphStory: Story;
12
+ export declare const StressGraphStory: Story;
13
+ export declare const DagStoryStory: Story;
14
+ export declare const Theming: Story;
15
+ export declare const HighlightNeighbors: Story;
16
+ export declare const ClickToFocus: Story;
17
+ export declare const Grid: Story;
18
+ export declare const Collide: Story;
19
+ export declare const Legend: Story;
20
+ export declare const Beeswarm: Story;
21
+ export declare const EmitParticle: Story;
22
+ export declare const RandomGraphStory: Story;
23
+ export declare const LabeledStory: Story;
@@ -0,0 +1 @@
1
+ export { default } from './kapsule';
@@ -0,0 +1,12 @@
1
+ import { WebGLRendererParameters } from 'three';
2
+ import { KapsuleClassCtor } from '../../internal/kapsule';
3
+ export interface ForceGraph3DConfigOptions {
4
+ controlType?: "trackball" | "orbit" | "fly";
5
+ rendererConfig?: WebGLRendererParameters;
6
+ /** Additional renderers (e.g. CSS2DRenderer for HTML overlays).
7
+ * Typed loosely — three.js no longer exports a base `Renderer`
8
+ * interface in newer revisions. */
9
+ extraRenderers?: unknown[];
10
+ }
11
+ declare const ForceGraph3D: KapsuleClassCtor;
12
+ export default ForceGraph3D;
@@ -0,0 +1,13 @@
1
+ import { CreateEngineOptions, GraphEngine } from './types';
2
+ import { GraphMode, LinkObject, LoraGraphCanvasProps, NodeObject } from '../types';
3
+ export interface UnifiedEngine<N extends NodeObject = NodeObject, L extends LinkObject = LinkObject> extends GraphEngine<N, L> {
4
+ /** Switch presentation mode in place. Animates the camera + the
5
+ * per-node z constraints; does not destroy the engine. */
6
+ setMode(mode: GraphMode, durationMs?: number): void;
7
+ }
8
+ export declare function createEngineUnified<N extends NodeObject, L extends LinkObject>(mount: HTMLElement, opts: CreateEngineOptions<N, L> & {
9
+ initialMode: GraphMode;
10
+ }, handlerRef: {
11
+ current: LoraGraphCanvasProps<N, L>;
12
+ }): UnifiedEngine<N, L>;
13
+ export type EngineUnified = ReturnType<typeof createEngineUnified>;
@@ -0,0 +1,29 @@
1
+ import { LinkObject, LoraGraphCanvasProps, NodeObject } from '../types';
2
+ /** A kapsule prop the engine exposes as a chainable setter. */
3
+ export interface PropBinding<P> {
4
+ /** Name of the prop on `LoraGraphCanvasProps` we read from. */
5
+ key: keyof LoraGraphCanvasProps<NodeObject, LinkObject>;
6
+ /** Name of the chainable setter on the engine instance. */
7
+ setter: string;
8
+ /** Whether this binding is supported in the current engine mode. */
9
+ supports: (mode: "2d" | "3d") => boolean;
10
+ /** Optional transformer applied before passing the value to the setter. */
11
+ transform?: (value: unknown, props: P) => unknown;
12
+ }
13
+ /** Bindings shared between 2D and 3D engines. Order doesn't matter; the
14
+ * adapter just walks the list each render. */
15
+ export declare const SHARED_BINDINGS: PropBinding<unknown>[];
16
+ /** Walk the bindings, calling each engine setter for props whose value
17
+ * changed (using Object.is). The engine instance is treated as a loose
18
+ * record of chainable setters so this works for both kapsule types. */
19
+ export declare function applyDiffedProps<N extends NodeObject, L extends LinkObject>(engine: Record<string, (value: unknown) => unknown>, props: LoraGraphCanvasProps<N, L>, prev: LoraGraphCanvasProps<N, L>, mode: "2d" | "3d"): void;
20
+ /** Event-handler bindings. These are wired once at engine construction;
21
+ * the React layer forwards through a stable trampoline so latest props
22
+ * always win without re-binding. */
23
+ export declare const EVENT_BINDINGS: readonly ["onNodeClick", "onNodeRightClick", "onNodeHover", "onNodeDrag", "onNodeDragEnd", "onLinkClick", "onLinkRightClick", "onLinkHover", "onBackgroundClick", "onBackgroundRightClick", "onEngineTick", "onEngineStop"];
24
+ /** 2D-only event bindings (zoom + render-frame callbacks). The 3D
25
+ * kapsule doesn't expose these — they're driven by Three.js controls
26
+ * there. We wire them in the same trampoline way for consistency. */
27
+ export declare const EVENT_BINDINGS_2D: readonly ["onZoom", "onZoomEnd", "onRenderFramePre", "onRenderFramePost"];
28
+ export type EventName = (typeof EVENT_BINDINGS)[number];
29
+ export type EventName2D = (typeof EVENT_BINDINGS_2D)[number];
@@ -0,0 +1,13 @@
1
+ /** A tiny RAF-driven tween we use instead of the kapsule's built-in
2
+ * tween group. The kapsule's tweens live inside a private state
3
+ * object — there's no public API for cancelling them once started.
4
+ * Doing the animation ourselves lets us stop it the instant the
5
+ * user interacts with the canvas.
6
+ *
7
+ * Each `runAnim` returns a `cancel()` function; calling it freezes
8
+ * the animation at the current frame (no further `step` invocations).
9
+ * Returns null when running in an environment without
10
+ * `requestAnimationFrame` (jsdom in unit tests). */
11
+ export declare function runAnim(durationMs: number, step: (t: number) => void, onDone?: () => void): () => void;
12
+ export declare function easeOutQuad(t: number): number;
13
+ export declare function lerp(a: number, b: number, t: number): number;
@@ -0,0 +1,86 @@
1
+ import { GraphData, GraphMode, LinkObject, NodeObject, LoraGraphCanvasProps } from '../types';
2
+ /** Camera state snapshot. The shape differs per mode so we can
3
+ * faithfully restore in both 2D (pan + zoom) and 3D (position +
4
+ * lookAt). */
5
+ export type CameraState = {
6
+ mode: "2d";
7
+ x: number;
8
+ y: number;
9
+ k: number;
10
+ } | {
11
+ mode: "3d";
12
+ x: number;
13
+ y: number;
14
+ z: number;
15
+ lookAt: {
16
+ x: number;
17
+ y: number;
18
+ z: number;
19
+ };
20
+ };
21
+ /** Subset of the kapsule API both 2D and 3D engines implement. Each
22
+ * adapter normalises naming so the React layer never has to special-case
23
+ * the dimension. */
24
+ export interface GraphEngine<N extends NodeObject = NodeObject, L extends LinkObject = LinkObject> {
25
+ mode: GraphMode;
26
+ setGraphData(data: GraphData<N, L>): void;
27
+ getGraphData(): GraphData<N, L>;
28
+ fit(durationMs?: number, padding?: number): void;
29
+ centerAt(x: number, y: number, z?: number, durationMs?: number): void;
30
+ zoom(scale: number, durationMs?: number): void;
31
+ getZoom(): number;
32
+ screen2Graph(x: number, y: number, distance?: number): {
33
+ x: number;
34
+ y: number;
35
+ z?: number;
36
+ };
37
+ graph2Screen(x: number, y: number, z?: number): {
38
+ x: number;
39
+ y: number;
40
+ z?: number;
41
+ };
42
+ getGraphBbox(): {
43
+ x: [number, number];
44
+ y: [number, number];
45
+ z?: [number, number];
46
+ };
47
+ pause(): void;
48
+ resume(): void;
49
+ reheat(): void;
50
+ resize(width: number, height: number): void;
51
+ /** Get / set / clear a d3-force by name. Pass `null` to remove. */
52
+ d3Force(name: string, fn?: unknown | null): unknown;
53
+ /** Emit a one-off animated particle along the given link. */
54
+ emitParticle(link: L): void;
55
+ /** Snap any in-flight camera tween (centerAt / zoom / cameraPosition)
56
+ * to its current state. Used to interrupt a focus animation when
57
+ * the user starts a new interaction. No-op when nothing is
58
+ * animating. */
59
+ stopAnimation(): void;
60
+ /** Animate the camera so the given target point is centered, while
61
+ * preserving the current viewing angle. In 3D the camera flies
62
+ * along its current vector from the target so the user's orbit
63
+ * is kept; in 2D it's a `centerAt` + `zoom`. */
64
+ focusOn(target: {
65
+ x: number;
66
+ y: number;
67
+ z?: number;
68
+ }, opts?: {
69
+ distance?: number;
70
+ zoom?: number;
71
+ durationMs?: number;
72
+ }): void;
73
+ /** Snapshot the current camera so it can be restored later. */
74
+ getCameraState(): CameraState;
75
+ /** Restore a snapshot produced by `getCameraState`. */
76
+ setCameraState(state: CameraState, durationMs?: number): void;
77
+ applyProps(props: LoraGraphCanvasProps<N, L>, prev: LoraGraphCanvasProps<N, L>): void;
78
+ getCanvasElement(): HTMLCanvasElement | null;
79
+ destroy(): void;
80
+ }
81
+ export interface CreateEngineOptions<N extends NodeObject = NodeObject, L extends LinkObject = LinkObject> {
82
+ initialProps: LoraGraphCanvasProps<N, L>;
83
+ initialData: GraphData<N, L>;
84
+ width: number;
85
+ height: number;
86
+ }
@@ -0,0 +1,62 @@
1
+ import { Accessor, GraphMode, LinkObject, NodeObject } from '../types';
2
+ export type NodePointerAreaPaint<N extends NodeObject> = (node: N, color: string, ctx: CanvasRenderingContext2D, globalScale: number) => void;
3
+ export interface UseAccessorOverridesParams<N extends NodeObject, L extends LinkObject> {
4
+ mode: GraphMode;
5
+ accentColor: string;
6
+ nodeColor?: Accessor<string, N>;
7
+ linkColor?: Accessor<string, L>;
8
+ linkWidth?: Accessor<number, L>;
9
+ nodeVal?: Accessor<number, N>;
10
+ nodeRelSize?: number;
11
+ nodePointerAreaPaint?: NodePointerAreaPaint<N>;
12
+ nodeAutoColorBy?: Accessor<string | null, N>;
13
+ nodeVisibility?: Accessor<boolean, N>;
14
+ selectedNodeSet: ReadonlySet<string | number>;
15
+ selectedLinkSet: ReadonlySet<string | number>;
16
+ highlightNeighborsOnHover: boolean;
17
+ highlightedNodeIds: ReadonlySet<string | number>;
18
+ highlightedLinkIds: ReadonlySet<string | number>;
19
+ hoverNodeId: string | number | null;
20
+ /** Direct-link hover. Distinct from `highlightedLinkIds`, which
21
+ * only fires when a node is hovered and its neighbour links light
22
+ * up. When the user hovers a link itself, only `hoverLinkId` is
23
+ * set — both signals need to flow into the size/colour wrappers
24
+ * so the link bumps up on direct hover, not just node-neighbour
25
+ * hover. */
26
+ hoverLinkId: string | number | null;
27
+ hiddenGroups: ReadonlySet<string>;
28
+ }
29
+ export interface AccessorOverrides<N extends NodeObject, L extends LinkObject> {
30
+ nodeColor: Accessor<string, N> | undefined;
31
+ linkColor: Accessor<string, L> | undefined;
32
+ linkWidth: Accessor<number, L> | undefined;
33
+ nodeVal: Accessor<number, N> | undefined;
34
+ nodePointerAreaPaint: NodePointerAreaPaint<N> | undefined;
35
+ nodeVisibility: ((node: N) => boolean) | undefined;
36
+ }
37
+ /** Wrap the host's color / width / value / visibility accessors so the
38
+ * current selection + hover-highlight + legend-filter overlay on top
39
+ * without losing the underlying styling.
40
+ *
41
+ * Each wrapper is conditional in two ways:
42
+ *
43
+ * 1. When there's nothing to overlay (no selection, no hover, no
44
+ * legend filter) we return the host's accessor *untouched*. The
45
+ * kapsule never enters our closure on hot paths in that case.
46
+ *
47
+ * 2. When an overlay is active, the wrapper identity flips on every
48
+ * selection/hover state change. This is REQUIRED — three-forcegraph's
49
+ * link/node digest (see `three-forcegraph.mjs:1185`) only re-evaluates
50
+ * accessors when one of its tracked props changes identity. Keeping
51
+ * the wrapper "stable" through selection changes — as an earlier
52
+ * iteration of this hook did — caused the kapsule to cache the colour
53
+ * and width of whichever link the user first clicked and never update
54
+ * them on subsequent clicks, despite the React state being correct.
55
+ *
56
+ * The closure body still reads the latest state from a ref, but the
57
+ * memo deps include every overlay signal so the identity flips with
58
+ * each tick of the state machine. The kapsule's own internal digest
59
+ * is smart enough to only rebuild geometries/materials whose value
60
+ * actually changed (cheap on small per-click selection deltas), so
61
+ * the cost of identity churn here is bounded. */
62
+ export declare function useAccessorOverrides<N extends NodeObject, L extends LinkObject>(params: UseAccessorOverridesParams<N, L>): AccessorOverrides<N, L>;
@@ -0,0 +1,6 @@
1
+ import { GraphData, LinkObject, NodeObject } from '../types';
2
+ /** When enabled, walk the graph once on every data change and stash
3
+ * `_neighbors` (Node[]) and `_links` (Link[]) arrays on each node. The
4
+ * hover-highlight code reads from these refs directly so the per-frame
5
+ * work stays O(1). */
6
+ export declare function useAutoIndexNeighbors<N extends NodeObject, L extends LinkObject>(enabled: boolean, data: GraphData<N, L>): void;
@@ -0,0 +1,46 @@
1
+ export interface ClickToleranceShimOptions {
2
+ /** Distance in CSS pixels the cursor may travel from the press
3
+ * position before the gesture is committed as a drag. */
4
+ tolerancePx?: number;
5
+ /** Milliseconds after `pointerdown` during which every move is
6
+ * suppressed regardless of distance, so press-induced jitter never
7
+ * promotes a click into a drag. */
8
+ pressGraceMs?: number;
9
+ }
10
+ /** Click-vs-drag tolerance shim.
11
+ *
12
+ * The upstream kapsule (force-graph + 3d-force-graph) flips
13
+ * `isPointerDragging` to true on the *first* pointermove after
14
+ * pointerdown for any mouse event — even a 1px jitter — which then
15
+ * suppresses the click / right-click handler on pointerup. That
16
+ * makes background-click deselect, node selection, and right-click
17
+ * context menu all feel fragile.
18
+ *
19
+ * We add a movement dead-zone by intercepting pointer **and** mouse
20
+ * move events at the window level in the capture phase. Two
21
+ * independent thresholds combine to keep clicks from accidentally
22
+ * becoming drags:
23
+ *
24
+ * 1. **Press-grace window** — for the first `pressGraceMs` after
25
+ * `pointerdown`, every move is swallowed regardless of distance.
26
+ * This covers the involuntary jitter spike that fires right
27
+ * after the button physically engages, which on a HiDPI trackpad
28
+ * can easily exceed 6–8px in a single event.
29
+ * 2. **Distance dead-zone** — past the grace window, moves within
30
+ * `tolerancePx` of the press position are still swallowed. Once
31
+ * the cursor crosses that radius the gesture commits to "drag"
32
+ * and we stop intercepting, so pan / orbit / drag-node remain
33
+ * snappy for the rest of the gesture.
34
+ *
35
+ * Suppressing moves stops them propagating to:
36
+ * - the kapsule's container-level `pointermove` listener (so its
37
+ * `isPointerDragging` flag stays false and the click handler
38
+ * still fires on `pointerup`);
39
+ * - d3-zoom's `mousemove.zoom` listener on `window` (so a slight
40
+ * pan doesn't shift a node under the cursor between pointerdown
41
+ * and pointerup — that was the bug where clicking empty space to
42
+ * deselect would silently toggle whichever node slid under the
43
+ * cursor mid-gesture);
44
+ * - Three.js OrbitControls in 3D (same idea — no camera nudge
45
+ * during a click). */
46
+ export declare function useClickToleranceShim(mount: HTMLElement | null, options?: number | ClickToleranceShimOptions): void;