@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
@@ -0,0 +1,375 @@
1
+ import { CSSProperties } from 'react';
2
+ /** Graph node. `id` is required; the rest are optional. The engine writes
3
+ * back `x/y/z` (and `vx/vy/vz`) every tick, and the host can pin a node
4
+ * by setting `fx/fy/fz`. */
5
+ export interface NodeObject {
6
+ id: string | number;
7
+ x?: number;
8
+ y?: number;
9
+ z?: number;
10
+ vx?: number;
11
+ vy?: number;
12
+ vz?: number;
13
+ fx?: number;
14
+ fy?: number;
15
+ fz?: number;
16
+ label?: string;
17
+ color?: string;
18
+ group?: string | number;
19
+ val?: number;
20
+ [key: string]: unknown;
21
+ }
22
+ /** Graph link. `source` and `target` are required. */
23
+ export interface LinkObject {
24
+ id?: string | number;
25
+ source: string | number | NodeObject;
26
+ target: string | number | NodeObject;
27
+ label?: string;
28
+ color?: string;
29
+ width?: number;
30
+ curvature?: number;
31
+ [key: string]: unknown;
32
+ }
33
+ export interface GraphData<N extends NodeObject = NodeObject, L extends LinkObject = LinkObject> {
34
+ nodes: N[];
35
+ links: L[];
36
+ }
37
+ export type GraphMode = "2d" | "3d";
38
+ /** Built-in tool identifiers. */
39
+ export type ToolId = "select" | "pan" | "add-node" | "add-link" | "delete" | "duplicate" | "select-all" | "fit" | "zoom-in" | "zoom-out" | "pause" | "resume" | "screenshot" | "export-json" | "import-json" | "toggle-mode";
40
+ /** Accepts a literal value, a property accessor string, or a function. */
41
+ export type Accessor<T, In> = T | string | ((obj: In) => T);
42
+ export type DagMode = "td" | "bu" | "lr" | "rl" | "radialout" | "radialin" | null;
43
+ /** CSS-variable theme. Each key maps to one of the `--lgc-*` variables.
44
+ * Only set what you want to override. */
45
+ export interface LoraGraphTheme {
46
+ background?: string;
47
+ foreground?: string;
48
+ border?: string;
49
+ accent?: string;
50
+ toolbarBackground?: string;
51
+ toolbarForeground?: string;
52
+ toolbarBorder?: string;
53
+ toolActiveBackground?: string;
54
+ toolHoverBackground?: string;
55
+ tooltipBackground?: string;
56
+ tooltipForeground?: string;
57
+ menuBackground?: string;
58
+ menuForeground?: string;
59
+ menuHoverBackground?: string;
60
+ fontFamily?: string;
61
+ fontSize?: string;
62
+ }
63
+ export interface ToolbarConfig {
64
+ include?: ToolId[];
65
+ exclude?: ToolId[];
66
+ position?: "top" | "top-right" | "top-left" | "bottom";
67
+ }
68
+ export interface LoraGraphCanvasProps<N extends NodeObject = NodeObject, L extends LinkObject = LinkObject> {
69
+ data?: GraphData<N, L>;
70
+ defaultData?: GraphData<N, L>;
71
+ onDataChange?: (next: GraphData<N, L>) => void;
72
+ mode?: GraphMode;
73
+ defaultMode?: GraphMode;
74
+ onModeChange?: (mode: GraphMode) => void;
75
+ width?: number;
76
+ height?: number;
77
+ className?: string;
78
+ style?: CSSProperties;
79
+ backgroundColor?: string;
80
+ theme?: Partial<LoraGraphTheme>;
81
+ nodeId?: string;
82
+ linkSource?: string;
83
+ linkTarget?: string;
84
+ nodeColor?: Accessor<string, N>;
85
+ nodeLabel?: Accessor<string | HTMLElement, N>;
86
+ nodeVal?: Accessor<number, N>;
87
+ nodeAutoColorBy?: Accessor<string | null, N>;
88
+ nodeRelSize?: number;
89
+ nodeVisibility?: Accessor<boolean, N>;
90
+ nodeOpacity?: number;
91
+ nodeResolution?: number;
92
+ linkColor?: Accessor<string, L>;
93
+ linkLabel?: Accessor<string | HTMLElement, L>;
94
+ linkWidth?: Accessor<number, L>;
95
+ linkCurvature?: Accessor<number, L>;
96
+ linkLineDash?: Accessor<number[] | null, L>;
97
+ linkAutoColorBy?: Accessor<string | null, L>;
98
+ linkVisibility?: Accessor<boolean, L>;
99
+ linkOpacity?: number;
100
+ linkResolution?: number;
101
+ linkCurveRotation?: Accessor<number, L>;
102
+ linkDirectionalArrowLength?: Accessor<number, L>;
103
+ linkDirectionalArrowColor?: Accessor<string, L>;
104
+ linkDirectionalArrowRelPos?: Accessor<number, L>;
105
+ linkDirectionalArrowResolution?: number;
106
+ linkDirectionalParticles?: Accessor<number, L>;
107
+ linkDirectionalParticleSpeed?: Accessor<number, L>;
108
+ linkDirectionalParticleWidth?: Accessor<number, L>;
109
+ linkDirectionalParticleOffset?: Accessor<number, L>;
110
+ linkDirectionalParticleColor?: Accessor<string, L>;
111
+ linkDirectionalParticleResolution?: number;
112
+ cooldownTicks?: number;
113
+ cooldownTime?: number;
114
+ warmupTicks?: number;
115
+ d3AlphaDecay?: number;
116
+ d3VelocityDecay?: number;
117
+ d3AlphaMin?: number;
118
+ d3AlphaTarget?: number;
119
+ dagMode?: DagMode;
120
+ dagLevelDistance?: number;
121
+ dagNodeFilter?: (node: N) => boolean;
122
+ onDagError?: (loopNodeIds: Array<string | number>) => void;
123
+ /** 3D-only: drop layout to 1, 2, or 3 dimensions (flattens z when 2). */
124
+ numDimensions?: 1 | 2 | 3;
125
+ /** 3D-only: switch the layout engine. ngraph is ~3× faster for >10k nodes. */
126
+ forceEngine?: "d3" | "ngraph";
127
+ /** 3D-only: ngraph physics tuning, passed straight through. */
128
+ ngraphPhysics?: object;
129
+ /** 3D-only: throttle the raycaster used for hover / click hit
130
+ * testing. Defaults to 0 (every frame). Bump to 50-200ms on
131
+ * huge graphs to skip raycasts on most frames — the perf tier
132
+ * picks a sensible value automatically. */
133
+ pointerRaycasterThrottleMs?: number;
134
+ /** Add a node-collision force so circles don't overlap. `true` uses
135
+ * `nodeRelSize` as the radius; pass a number to override. */
136
+ collideNodes?: boolean | number;
137
+ tools?: boolean | ToolId[] | ToolbarConfig;
138
+ showContextMenu?: boolean;
139
+ showLegend?: boolean;
140
+ selection?: "none" | "single" | "multi";
141
+ /** Allow ⌘C / ⌘X / ⌘V (and the matching ref methods). */
142
+ enableClipboard?: boolean;
143
+ /** Render the floating tooltip (the cursor-anchored label resolved
144
+ * from `nodeLabel` / `linkLabel`) on hover. Defaults to `false` —
145
+ * the hover state still drives neighbour highlighting and on-canvas
146
+ * labels, just without the mouse-attached pill. */
147
+ enableTooltip?: boolean;
148
+ /** Play an animated "zoom in to the bounds" intro on first mount.
149
+ * The camera starts pulled back proportional to node count (so a
150
+ * sparse 10-node graph reveals from ~×3 out and a 10k stress graph
151
+ * from ~×6 out) and tweens into the fitted view. Defaults to
152
+ * `true`. Pass `false` to suppress the intro and snap to the
153
+ * kapsule's own initial fit instead. */
154
+ introZoom?: boolean;
155
+ enableNodeDrag?: boolean;
156
+ enableZoom?: boolean;
157
+ enablePan?: boolean;
158
+ enablePointerInteraction?: boolean;
159
+ enableNavigationControls?: boolean;
160
+ linkHoverPrecision?: number;
161
+ minZoom?: number;
162
+ maxZoom?: number;
163
+ showPointerCursor?: Accessor<boolean, N | L>;
164
+ showNavInfo?: boolean;
165
+ autoPauseRedraw?: boolean;
166
+ /** When true (default false), clicking a node animates the camera
167
+ * toward it; clicking the same node again restores the prior view. */
168
+ focusOnClick?: boolean;
169
+ /** When true (default false), hovering a node highlights it and its
170
+ * neighbours via the accent color. Requires cross-linked neighbour
171
+ * refs in the node data, or set `autoIndexNeighbors`. */
172
+ highlightNeighborsOnHover?: boolean;
173
+ /** Auto-build `node._neighbors` and `node._links` arrays after every
174
+ * data change so `highlightNeighborsOnHover` has something to walk. */
175
+ autoIndexNeighbors?: boolean;
176
+ /** Background helper: draw a faint grid behind the canvas. Useful in
177
+ * a playground. 2D only. */
178
+ showGrid?: boolean | {
179
+ spacing?: number;
180
+ color?: string;
181
+ };
182
+ /** Render each node's label directly on the 2D canvas (under the
183
+ * node). When this is on, the default node colour is automatically
184
+ * faded so the label text reads clearly on top of overlapping
185
+ * nodes. 2D only — in 3D, use `nodeLabel` for the hover tooltip. */
186
+ showLabels?: boolean;
187
+ /** When `true` (default), releasing a dragged node pins it at its
188
+ * new position by writing `fx`/`fy`/`fz`. Dragging a node that's
189
+ * part of the current selection moves all selected nodes together
190
+ * and pins them as a group on release. Set to `false` to keep the
191
+ * default simulation-pull behaviour. */
192
+ fixOnDrop?: boolean;
193
+ /** Auto-tune renderer / simulation settings based on graph size so
194
+ * the canvas stays responsive into the 50k-100k-node range.
195
+ *
196
+ * - `"auto"` (default): pick a tier from the live node + link count.
197
+ * - `"off"`: never inject perf defaults — only the host's props
198
+ * drive the kapsule.
199
+ * - `"default" | "large" | "xlarge" | "huge"`: force a specific
200
+ * tier regardless of size (useful for benchmarking).
201
+ *
202
+ * Each tier overrides things like `cooldownTicks`, `d3AlphaDecay`,
203
+ * 3D `nodeResolution`/`linkResolution`, and the 3D layout engine.
204
+ * Any prop the host sets explicitly always wins. */
205
+ performanceProfile?: "auto" | "off" | "default" | "large" | "xlarge" | "huge";
206
+ /** Switch the simulation into a beeswarm layout: nodes spread along
207
+ * one axis driven by a value accessor, with a weak orthogonal pull
208
+ * and collision so they don't overlap. Pass `true` for an
209
+ * id-hashed spread, or an object to control the value source and
210
+ * axis. Disables the default `center` + `charge` forces while
211
+ * active. */
212
+ beeswarm?: boolean | {
213
+ /** Target axis (default `"x"`). */
214
+ axis?: "x" | "y";
215
+ /** Position along the chosen axis — `(node) => number` or a
216
+ * property key whose value is numeric. Defaults to a stable
217
+ * hash of the node id. */
218
+ value?: string | ((n: N) => number);
219
+ /** Strength of the orthogonal pull-to-zero force. Default 0.2. */
220
+ strength?: number;
221
+ };
222
+ onNodeClick?: (node: N, event: MouseEvent) => void;
223
+ onNodeRightClick?: (node: N, event: MouseEvent) => void;
224
+ onNodeHover?: (node: N | null, previousNode: N | null) => void;
225
+ onNodeDoubleClick?: (node: N, event: MouseEvent) => void;
226
+ onNodeDrag?: (node: N, translate: {
227
+ x: number;
228
+ y: number;
229
+ z?: number;
230
+ }) => void;
231
+ onNodeDragEnd?: (node: N, translate: {
232
+ x: number;
233
+ y: number;
234
+ z?: number;
235
+ }) => void;
236
+ onLinkClick?: (link: L, event: MouseEvent) => void;
237
+ onLinkRightClick?: (link: L, event: MouseEvent) => void;
238
+ onLinkHover?: (link: L | null, previousLink: L | null) => void;
239
+ onBackgroundClick?: (event: MouseEvent) => void;
240
+ onBackgroundRightClick?: (event: MouseEvent) => void;
241
+ onSelectionChange?: (selectedIds: Array<string | number>) => void;
242
+ onEngineTick?: () => void;
243
+ onEngineStop?: () => void;
244
+ onZoom?: (transform: {
245
+ k: number;
246
+ x: number;
247
+ y: number;
248
+ }) => void;
249
+ onZoomEnd?: (transform: {
250
+ k: number;
251
+ x: number;
252
+ y: number;
253
+ }) => void;
254
+ onRenderFramePre?: (ctx: CanvasRenderingContext2D, globalScale: number) => void;
255
+ onRenderFramePost?: (ctx: CanvasRenderingContext2D, globalScale: number) => void;
256
+ /** Fires when the user copies nodes (⌘C) — receives the snapshot. */
257
+ onCopy?: (nodes: N[]) => void;
258
+ /** Fires when the user cuts nodes (⌘X) — receives the snapshot
259
+ * *before* the originals are removed from the graph. */
260
+ onCut?: (nodes: N[]) => void;
261
+ /** Fires after a paste places new nodes in the graph. */
262
+ onPaste?: (nodes: N[]) => void;
263
+ nodeCanvasObject?: (n: N, ctx: CanvasRenderingContext2D, globalScale: number) => void;
264
+ nodeCanvasObjectMode?: "replace" | "before" | "after" | ((n: N) => "replace" | "before" | "after" | undefined);
265
+ nodePointerAreaPaint?: (n: N, color: string, ctx: CanvasRenderingContext2D, globalScale: number) => void;
266
+ linkCanvasObject?: (l: L, ctx: CanvasRenderingContext2D, globalScale: number) => void;
267
+ linkCanvasObjectMode?: "replace" | "before" | "after" | ((l: L) => "replace" | "before" | "after" | undefined);
268
+ linkPointerAreaPaint?: (l: L, color: string, ctx: CanvasRenderingContext2D, globalScale: number) => void;
269
+ nodeThreeObject?: (n: N) => unknown;
270
+ nodeThreeObjectExtend?: Accessor<boolean, N>;
271
+ linkThreeObject?: (l: L) => unknown;
272
+ linkThreeObjectExtend?: Accessor<boolean, L>;
273
+ linkMaterial?: Accessor<unknown, L>;
274
+ nodePositionUpdate?: ((obj: unknown, coords: {
275
+ x: number;
276
+ y: number;
277
+ z: number;
278
+ }, node: N) => void | boolean | null) | null;
279
+ linkPositionUpdate?: ((obj: unknown, coords: {
280
+ start: {
281
+ x: number;
282
+ y: number;
283
+ z: number;
284
+ };
285
+ end: {
286
+ x: number;
287
+ y: number;
288
+ z: number;
289
+ };
290
+ }, link: L) => void | boolean | null) | null;
291
+ /** 3D init-only — passed once on engine construction. */
292
+ controlType?: "trackball" | "orbit" | "fly";
293
+ rendererConfig?: Record<string, unknown>;
294
+ extraRenderers?: unknown[];
295
+ }
296
+ export interface LoraGraphCanvasHandle<N extends NodeObject = NodeObject, L extends LinkObject = LinkObject> {
297
+ getData(): GraphData<N, L>;
298
+ setData(next: GraphData<N, L>): void;
299
+ addNode(node?: Partial<N> & {
300
+ id?: string | number;
301
+ }, opts?: {
302
+ at?: {
303
+ x: number;
304
+ y: number;
305
+ z?: number;
306
+ };
307
+ }): N;
308
+ addNodes(nodes: Array<Partial<N> & {
309
+ id?: string | number;
310
+ }>): N[];
311
+ updateNode(id: string | number, patch: Partial<N>): void;
312
+ removeNode(id: string | number): void;
313
+ removeNodes(ids: Array<string | number>): void;
314
+ addLink(link: {
315
+ source: string | number;
316
+ target: string | number;
317
+ id?: string | number;
318
+ } & Partial<L>): L;
319
+ addLinks(links: Array<{
320
+ source: string | number;
321
+ target: string | number;
322
+ } & Partial<L>>): L[];
323
+ removeLink(predicate: (l: L) => boolean): void;
324
+ clear(): void;
325
+ getSelection(): Array<string | number>;
326
+ setSelection(ids: Array<string | number>): void;
327
+ selectAll(): void;
328
+ clearSelection(): void;
329
+ getMode(): GraphMode;
330
+ setMode(mode: GraphMode): void;
331
+ fit(durationMs?: number, padding?: number): void;
332
+ centerAt(x: number, y: number, z?: number, durationMs?: number): void;
333
+ zoom(scale: number, durationMs?: number): void;
334
+ zoomIn(step?: number): void;
335
+ zoomOut(step?: number): void;
336
+ copy(): N[];
337
+ cut(): N[];
338
+ paste(opts?: {
339
+ at?: {
340
+ x: number;
341
+ y: number;
342
+ z?: number;
343
+ };
344
+ }): N[];
345
+ duplicate(): N[];
346
+ /** Create a fresh node and link each currently selected node to it.
347
+ * No-op when the selection is empty. Returns the new node, or null
348
+ * if nothing was created. */
349
+ addConnectedNode(opts?: {
350
+ at?: {
351
+ x: number;
352
+ y: number;
353
+ z?: number;
354
+ };
355
+ label?: string;
356
+ }): N | null;
357
+ togglePin(id: string | number): void;
358
+ exportJSON(): string;
359
+ importJSON(json: string): void;
360
+ downloadJSON(filename?: string): void;
361
+ pause(): void;
362
+ resume(): void;
363
+ reheat(): void;
364
+ /** Get / set / clear a d3-force by name. Pass `null` to remove. */
365
+ d3Force(name: string): unknown;
366
+ d3Force(name: string, fn: unknown | null): void;
367
+ /** Emit a one-off particle along a link (visual ping for events). */
368
+ emitParticle(link: L): void;
369
+ /** Halt any in-flight camera animation (e.g. a focus tween) and
370
+ * freeze the camera at its current state. */
371
+ stopAnimation(): void;
372
+ screenshot(): Promise<Blob | null>;
373
+ engine2D(): unknown | null;
374
+ engine3D(): unknown | null;
375
+ }
@@ -0,0 +1,36 @@
1
+ /** Resolve an accessor against an object. Mirrors the kapsule's
2
+ * `accessor-fn` semantics: a function is invoked; a string is used as
3
+ * a property name; anything else (including undefined) is returned as
4
+ * is. */
5
+ export declare function readAccessor<T, In>(accessor: T | string | ((obj: In) => T) | undefined, obj: In): T | undefined;
6
+ /** Resolve a node's display caption. Precedence:
7
+ * 1. Host accessor (`nodeLabel`) when it returns a non-empty value.
8
+ * Strings pass through; HTMLElements yield their textContent;
9
+ * other primitives (e.g. numeric `nodeLabel="id"`) are stringified.
10
+ * 2. The node's own `label` field, if it's a non-empty string.
11
+ * 3. The node id, stringified.
12
+ * Returns "" when nothing resolves — callers should treat that as
13
+ * "skip drawing". */
14
+ export declare function resolveNodeLabelText<N extends {
15
+ id?: string | number;
16
+ label?: unknown;
17
+ }>(accessor: string | HTMLElement | ((n: N) => string | HTMLElement) | undefined, node: N): string;
18
+ /** Resolve a link's display caption. Same precedence as
19
+ * `resolveNodeLabelText`, with the id-based fallback rendering as
20
+ * `source → target` using the resolved endpoint ids. Returns "" when
21
+ * no caption can be formed. */
22
+ export declare function resolveLinkLabelText<L extends {
23
+ label?: unknown;
24
+ source: unknown;
25
+ target: unknown;
26
+ }>(accessor: string | HTMLElement | ((l: L) => string | HTMLElement) | undefined, link: L): string;
27
+ /** Adjust a CSS color string toward the given alpha. Best-effort —
28
+ * passes through unrecognised inputs untouched. Used by the
29
+ * neighbour-highlight code to dim non-hovered neighbours. */
30
+ export declare function adjustAlpha(color: string, alpha: number): string;
31
+ /** Shared sentinel for the "no hover-highlight" state. Reusing one
32
+ * instance lets React's useState bail out when transitioning empty →
33
+ * empty (e.g. mouseleave → mouseleave), avoiding a no-op re-render plus
34
+ * downstream engineProps re-memo and kapsule re-bind. Treated as
35
+ * read-only — all consumers only call `.has()` on it. */
36
+ export declare const EMPTY_ID_SET: Set<string | number>;
@@ -0,0 +1,4 @@
1
+ /** Trigger a browser download for a given Blob. */
2
+ export declare function downloadBlob(blob: Blob, filename: string): void;
3
+ /** Snapshot the supplied canvas to a PNG and trigger a download. */
4
+ export declare function downloadScreenshot(canvas: HTMLCanvasElement | null | undefined): void;
@@ -0,0 +1,8 @@
1
+ export interface Point2 {
2
+ x: number;
3
+ y: number;
4
+ }
5
+ export declare function distance2(a: Point2, b: Point2): number;
6
+ /** Default snap distances for "drag node onto node → link" gesture. */
7
+ export declare const SNAP_IN = 15;
8
+ export declare const SNAP_OUT = 40;
@@ -0,0 +1,9 @@
1
+ export interface GridOptions {
2
+ spacing?: number;
3
+ color?: string;
4
+ }
5
+ /** Draws a faint, infinite grid behind the graph. Hooked up via the
6
+ * engine's `onRenderFramePre` callback so the grid stays under the
7
+ * nodes / links. The grid adapts to the current zoom level: at high
8
+ * zoom we draw a tighter sub-grid, at low zoom a coarser one. */
9
+ export declare function drawBackgroundGrid(ctx: CanvasRenderingContext2D, globalScale: number, opts?: GridOptions): void;
@@ -0,0 +1,3 @@
1
+ export declare function createId(prefix?: string): string;
2
+ /** Reset all counters. Intended only for tests so they observe stable IDs. */
3
+ export declare function __resetIdCounters(): void;
@@ -0,0 +1,29 @@
1
+ import { GraphMode, LinkObject, LoraGraphCanvasProps, NodeObject } from '../types';
2
+ /** Performance tier — picked from the live node + link count and used
3
+ * to inject sensible defaults on top of the user's props. Higher
4
+ * tiers trade visual quality for frame rate so the renderer stays
5
+ * responsive on large graphs. */
6
+ export type PerfTier = "default" | "large" | "xlarge" | "huge";
7
+ export interface PickPerfTierInput {
8
+ nodeCount: number;
9
+ linkCount: number;
10
+ }
11
+ /** Map graph size to a tier. Links count for half a node since they're
12
+ * cheaper than nodes to render (one canvas line vs a circle + label),
13
+ * but they still drive force ticks. Thresholds were picked empirically
14
+ * to keep 60fps on a 2019 MacBook Pro:
15
+ * – default : < 2k (no tuning, full quality)
16
+ * – large : 2k … 10k (mild tweaks, still pretty)
17
+ * – xlarge : 10k … 50k (aggressive: ngraph layout in 3D, no
18
+ * particles, faster cooldown)
19
+ * – huge : 50k+ (all bets off — 100k target)
20
+ */
21
+ export declare function pickPerfTier({ nodeCount, linkCount, }: PickPerfTierInput): PerfTier;
22
+ /** Tier-specific prop defaults. Returned as a plain object so it can
23
+ * be spread *before* the user's props in the engine prop bag — the
24
+ * host's explicit values always win, so this only fills holes.
25
+ *
26
+ * We deliberately only set the props that move the needle on the
27
+ * bottleneck for that tier (drawing or simulation), and only when the
28
+ * user hasn't already opted in to a more conservative value. */
29
+ export declare function perfTierDefaults<N extends NodeObject = NodeObject, L extends LinkObject = LinkObject>(tier: PerfTier, mode: GraphMode): Partial<LoraGraphCanvasProps<N, L>>;
@@ -0,0 +1,61 @@
1
+ import { Sprite, Texture } from 'three';
2
+ export interface SpriteLabelOpts {
3
+ text: string;
4
+ /** Font size used when rasterising into the canvas texture. Larger
5
+ * values → sharper text when the camera is close. Defaults to 32. */
6
+ fontSize?: number;
7
+ fontFamily?: string;
8
+ color?: string;
9
+ /** CSS color string for the background pill. Pass an empty string
10
+ * for no background. Defaults to a semi-opaque dark pill. */
11
+ backgroundColor?: string;
12
+ /** Optional foreground for the alternate "selected" rasterisation.
13
+ * When supplied alongside `selectedBackgroundColor`, a second
14
+ * texture is baked at construction and stored on
15
+ * `sprite.userData.selectedTexture`. Swap `material.map` between
16
+ * that and `sprite.userData.normalTexture` to toggle styling
17
+ * without re-rasterising on each selection change. */
18
+ selectedColor?: string;
19
+ /** Optional pill background for the alternate "selected"
20
+ * rasterisation. See `selectedColor`. */
21
+ selectedBackgroundColor?: string;
22
+ /** Padding in canvas pixels around the rasterised text. */
23
+ padding?: number;
24
+ /** World-space height of the resulting sprite. Width derives from
25
+ * the rasterised aspect ratio. Defaults to 4. Ignored when
26
+ * `pixelHeight` is set. */
27
+ worldHeight?: number;
28
+ /** When provided, the sprite auto-scales each frame so its on-screen
29
+ * height stays at roughly this many CSS pixels regardless of camera
30
+ * distance — the constant-screen-size behaviour 2D labels get for
31
+ * free via `fontSize / globalScale`. Overrides `worldHeight`.
32
+ * Requires a PerspectiveCamera (the kapsule's default). */
33
+ pixelHeight?: number;
34
+ }
35
+ /** Extra fields stashed on `sprite.userData` for labels built with
36
+ * alternate "selected" styling. Consumers can flip
37
+ * `material.map = userData.selectedTexture` to switch styles without
38
+ * rebuilding the sprite. */
39
+ export interface SpriteLabelUserData {
40
+ normalTexture?: Texture;
41
+ selectedTexture?: Texture;
42
+ }
43
+ /** Release every GPU resource (both textures + the material) owned by
44
+ * a label sprite. Call when removing the sprite from the scene for
45
+ * good — without this the CanvasTextures stay resident in GPU memory
46
+ * forever, which on a heavily-edited 10k+-node graph accumulates
47
+ * into hundreds of MB of orphaned textures. */
48
+ export declare function disposeLabelSprite(sprite: Sprite): void;
49
+ /** Build a billboarded text sprite for use as a 3D label.
50
+ *
51
+ * Rasterises the text once into a `CanvasTexture` and wraps it in
52
+ * a `Sprite`. The sprite always faces the camera (that's the
53
+ * `Sprite` semantic) and respects depth so it occludes nodes
54
+ * behind it correctly while remaining flat.
55
+ *
56
+ * Allocates a fresh canvas + texture + material per call. Hosts
57
+ * rendering 10k+ labels should dispose them when the corresponding
58
+ * node / link is removed — we don't keep a global cache because the
59
+ * text content varies per item, and pooling by text+style alone
60
+ * would let stale sprites pin GPU memory after data updates. */
61
+ export declare function createTextSprite(opts: SpriteLabelOpts): Sprite;
@@ -0,0 +1,5 @@
1
+ import { CSSProperties } from 'react';
2
+ import { LoraGraphTheme } from '../types';
3
+ /** Translate a partial `LoraGraphTheme` into a style object that sets
4
+ * the matching CSS custom properties on the host element. */
5
+ export declare function themeToStyle(theme?: Partial<LoraGraphTheme>): CSSProperties;
package/package.json ADDED
@@ -0,0 +1,105 @@
1
+ {
2
+ "name": "@loradb/lora-graph-canvas",
3
+ "version": "0.10.0",
4
+ "description": "React graph canvas for LoraDB. Unified 2D/3D force-directed component with a built-in tool palette for adding, connecting, selecting, and removing nodes.",
5
+ "license": "BUSL-1.1",
6
+ "author": "LoraDB, Inc.",
7
+ "homepage": "https://github.com/lora-db/lora/tree/main/packages/lora-graph-canvas#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/lora-db/lora.git",
11
+ "directory": "packages/lora-graph-canvas"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/lora-db/lora/issues"
15
+ },
16
+ "keywords": [
17
+ "lora",
18
+ "loradb",
19
+ "graph",
20
+ "canvas",
21
+ "react",
22
+ "force-directed",
23
+ "2d",
24
+ "3d",
25
+ "three"
26
+ ],
27
+ "type": "module",
28
+ "main": "dist/index.cjs",
29
+ "module": "dist/index.js",
30
+ "types": "dist/index.d.ts",
31
+ "exports": {
32
+ ".": {
33
+ "types": "./dist/index.d.ts",
34
+ "import": "./dist/index.js",
35
+ "require": "./dist/index.cjs"
36
+ },
37
+ "./styles.css": "./dist/style.css"
38
+ },
39
+ "files": [
40
+ "dist/",
41
+ "README.md",
42
+ "LICENSE",
43
+ "THIRD_PARTY.md"
44
+ ],
45
+ "publishConfig": {
46
+ "access": "public",
47
+ "registry": "https://registry.npmjs.org/"
48
+ },
49
+ "engines": {
50
+ "node": ">=20"
51
+ },
52
+ "scripts": {
53
+ "build": "vite build",
54
+ "dev": "vite",
55
+ "typecheck": "tsc -p tsconfig.json --noEmit",
56
+ "test": "vitest run",
57
+ "test:watch": "vitest",
58
+ "lint": "eslint src test",
59
+ "format": "prettier --write src test",
60
+ "format:check": "prettier --check src test",
61
+ "storybook": "storybook dev -p 6007",
62
+ "build:storybook": "storybook build -o storybook-static"
63
+ },
64
+ "peerDependencies": {
65
+ "react": "^18.0.0 || ^19.0.0",
66
+ "react-dom": "^18.0.0 || ^19.0.0",
67
+ "three": ">=0.150.0"
68
+ },
69
+ "dependencies": {
70
+ "d3-array": "^3.2.0",
71
+ "d3-drag": "^3.0.0",
72
+ "d3-force-3d": "^3.0.0",
73
+ "d3-scale": "^4.0.0",
74
+ "d3-scale-chromatic": "^3.0.0",
75
+ "d3-selection": "^3.0.0",
76
+ "d3-zoom": "^3.0.0",
77
+ "three-forcegraph": "^1.43.0",
78
+ "three-render-objects": "^1.41.0"
79
+ },
80
+ "devDependencies": {
81
+ "@storybook/addon-essentials": "^8.3.0",
82
+ "@storybook/addon-interactions": "^8.3.0",
83
+ "@storybook/addon-links": "^8.3.0",
84
+ "@storybook/blocks": "^8.3.0",
85
+ "@storybook/react": "^8.3.0",
86
+ "@storybook/react-vite": "^8.3.0",
87
+ "@storybook/test": "^8.3.0",
88
+ "@testing-library/dom": "^10.4.0",
89
+ "@testing-library/react": "^16.0.0",
90
+ "@testing-library/user-event": "^14.5.0",
91
+ "@types/react": "^18.2.0",
92
+ "@types/react-dom": "^18.2.0",
93
+ "@types/three": "^0.182.0",
94
+ "@vitejs/plugin-react": "^4.3.0",
95
+ "jsdom": "^25.0.0",
96
+ "react": "^18.2.0",
97
+ "react-dom": "^18.2.0",
98
+ "storybook": "^8.3.0",
99
+ "three": "^0.182.0",
100
+ "typescript": "^5.6.0",
101
+ "vite": "^5.4.0",
102
+ "vite-plugin-dts": "^4.2.0",
103
+ "vitest": "^2.1.0"
104
+ }
105
+ }