@neo4j-nvl/base 1.2.2 → 2.0.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 (73) hide show
  1. package/CHANGELOG.md +31 -1
  2. package/dist/base.mjs +1 -1
  3. package/dist/types/index.d.ts +21 -20
  4. package/dist/types/layouts/animatedlayout/AnimatedLayout.d.ts +2 -2
  5. package/dist/types/layouts/d3forcelayout/constants.d.ts +0 -1
  6. package/dist/types/layouts/d3forcelayout/d3ForceLayout.d.ts +3 -3
  7. package/dist/types/layouts/forcedirectedlayout/PhysLayout.d.ts +275 -0
  8. package/dist/types/layouts/forcedirectedlayout/clustering/ClusterByMerger.d.ts +62 -0
  9. package/dist/types/layouts/forcedirectedlayout/clustering/SolarMerger.d.ts +56 -0
  10. package/dist/types/layouts/forcedirectedlayout/clustering/clustering-utils.d.ts +143 -0
  11. package/dist/types/layouts/forcedirectedlayout/clustering/clustering-utils.test.d.ts +1 -0
  12. package/dist/types/layouts/forcedirectedlayout/{physlayout/solarmerger → clustering}/types.d.ts +11 -1
  13. package/dist/types/layouts/forcedirectedlayout/shaders/multilevel-fragment.d.ts +2 -0
  14. package/dist/types/layouts/forcedirectedlayout/shaders/multilevel-repulsive-fragment.d.ts +2 -0
  15. package/dist/types/layouts/forcedirectedlayout/shaders/multilevel-update-fragment.d.ts +2 -0
  16. package/dist/types/layouts/forcedirectedlayout/shaders/sim-vertex.d.ts +2 -0
  17. package/dist/types/layouts/freeLayout/FreeLayout.d.ts +2 -4
  18. package/dist/types/layouts/gridLayout/GridLayout.d.ts +2 -6
  19. package/dist/types/layouts/hierarchicallayout/HierarchicalLayout.d.ts +5 -0
  20. package/dist/types/modules/ExternalCallbackHandler.d.ts +4 -10
  21. package/dist/types/modules/NvlController.d.ts +85 -5
  22. package/dist/types/modules/Shader.d.ts +1 -1
  23. package/dist/types/modules/dataset.d.ts +4 -2
  24. package/dist/types/modules/state/types.d.ts +118 -25
  25. package/dist/types/renderers/ClusterOverlay.d.ts +73 -0
  26. package/dist/types/renderers/ClusterOverlay.test.d.ts +1 -0
  27. package/dist/types/renderers/domrenderer/shared/ImageCache.d.ts +1 -0
  28. package/dist/types/renderers/domrenderer/shared/types.d.ts +1 -2
  29. package/dist/types/renderers/webglrenderer/Controller.d.ts +27 -6
  30. package/dist/types/renderers/webglrenderer/IconLayerRegistry.d.ts +14 -0
  31. package/dist/types/renderers/webglrenderer/IconLayerRegistry.test.d.ts +1 -0
  32. package/dist/types/renderers/webglrenderer/Renderer.d.ts +43 -3
  33. package/dist/types/renderers/webglrenderer/node-animation-fragment.d.ts +1 -1
  34. package/dist/types/renderers/webglrenderer/node-animation-vertex.d.ts +1 -1
  35. package/dist/types/renderers/webglrenderer/node-fragment-point.d.ts +1 -1
  36. package/dist/types/renderers/webglrenderer/node-vertex-point.d.ts +1 -1
  37. package/dist/types/renderers/webglrenderer/rel-fragment-line.d.ts +1 -1
  38. package/dist/types/renderers/webglrenderer/rel-glsl-utils.d.ts +1 -0
  39. package/dist/types/renderers/webglrenderer/rel-vertex-arrow.d.ts +2 -0
  40. package/dist/types/renderers/webglrenderer/rel-vertex-line.d.ts +1 -1
  41. package/dist/types/renderers/webglrenderer/viewport-box-fragment.d.ts +1 -1
  42. package/dist/types/renderers/webglrenderer/viewport-box-vertex.d.ts +1 -1
  43. package/dist/types/types/graph-element.d.ts +9 -1
  44. package/dist/types/types/nvl-window-functions.d.ts +2 -2
  45. package/dist/types/utils/canvasManagement.d.ts +1 -1
  46. package/dist/types/utils/clusterGeometry.d.ts +14 -0
  47. package/dist/types/utils/clusterGeometry.test.d.ts +1 -0
  48. package/dist/types/utils/clusterHitRegions.d.ts +42 -0
  49. package/dist/types/utils/clusterHitRegions.test.d.ts +1 -0
  50. package/dist/types/utils/constants.d.ts +6 -0
  51. package/dist/types/utils/geometry.d.ts +44 -1
  52. package/dist/types/utils/hittest.d.ts +30 -4
  53. package/dist/types/utils/jsDriverResultTransformer.d.ts +11 -9
  54. package/dist/types/utils/layoutSeeding.d.ts +7 -0
  55. package/dist/types/utils/layoutSeeding.test.d.ts +1 -0
  56. package/dist/types/utils/webWorkerUtils.d.ts +1 -1
  57. package/dist/types/utils/zoomFunctions.d.ts +9 -2
  58. package/package.json +3 -3
  59. package/dist/types/layouts/forcedirectedlayout/ForceCytoLayout.d.ts +0 -22
  60. package/dist/types/layouts/forcedirectedlayout/cosebilkentlayout/CoseBilkentLayout.d.ts +0 -33
  61. package/dist/types/layouts/forcedirectedlayout/physlayout/PhysLayout.d.ts +0 -174
  62. package/dist/types/layouts/forcedirectedlayout/physlayout/shaders/multilevel-fragment.d.ts +0 -2
  63. package/dist/types/layouts/forcedirectedlayout/physlayout/shaders/multilevel-repulsive-fragment.d.ts +0 -2
  64. package/dist/types/layouts/forcedirectedlayout/physlayout/shaders/multilevel-update-fragment.d.ts +0 -2
  65. package/dist/types/layouts/forcedirectedlayout/physlayout/shaders/sim-vertex.d.ts +0 -2
  66. package/dist/types/layouts/forcedirectedlayout/physlayout/shaders/workaround-fragment.d.ts +0 -2
  67. package/dist/types/layouts/forcedirectedlayout/physlayout/solarmerger/SolarMerger.d.ts +0 -21
  68. package/dist/types/layouts/forcedirectedlayout/physlayout/solarmerger/solar-placer.d.ts +0 -2
  69. /package/dist/types/layouts/forcedirectedlayout/{physlayout/PhysLayout.test.d.ts → PhysLayout.test.d.ts} +0 -0
  70. /package/dist/types/layouts/forcedirectedlayout/{physlayout/solarmerger/SolarMerger.bench.d.ts → clustering/ClusterByMerger.test.d.ts} +0 -0
  71. /package/dist/types/layouts/forcedirectedlayout/{physlayout/solarmerger/SolarMerger.test.d.ts → clustering/SolarMerger.bench.d.ts} +0 -0
  72. /package/dist/types/layouts/forcedirectedlayout/{physlayout/solarmerger/solar-placer.test.d.ts → clustering/SolarMerger.test.d.ts} +0 -0
  73. /package/dist/types/layouts/forcedirectedlayout/{physlayout/glwrapper.d.ts → glwrapper.d.ts} +0 -0
@@ -34,29 +34,98 @@ export type DisabledItemStyles = {
34
34
  fontColor: string;
35
35
  };
36
36
  /**
37
- * The options for the force directed layout
37
+ * Identifier returned by {@link ClusterByFn} to group nodes into physical clusters.
38
+ * Nodes sharing the same string or number are clustered together.
38
39
  */
39
- export interface ForceDirectedOptions {
40
+ export type ClusterId = string | number;
41
+ /**
42
+ * A physical cluster produced by {@link ClusteringOptions.clusterBy}.
43
+ * Not a graph element — used for hit-testing and interaction callbacks.
44
+ */
45
+ export type Cluster = {
46
+ /** Semantic cluster id returned by {@link ClusterByFn}. */
47
+ id: ClusterId;
48
+ /** Real member node ids belonging to this cluster (at least two). */
49
+ nodeIds: string[];
50
+ };
51
+ /**
52
+ * Function that maps a node id to a {@link ClusterId} for force-directed clustering.
53
+ * Returns `undefined` for nodes that should remain independent.
54
+ */
55
+ export type ClusterByFn = (nodeId: string) => ClusterId | undefined;
56
+ /**
57
+ * Options that control physical clustering and cluster decorations for the force-directed layout.
58
+ */
59
+ export interface ClusteringOptions {
60
+ /**
61
+ * Optional function to group nodes into physical clusters based on a custom identifier.
62
+ * Nodes returning the same string or number will be grouped together in the layout.
63
+ * Nodes returning undefined will be treated as independent.
64
+ *
65
+ * @note Ensure this function has a stable reference to avoid evaluation on every render.
66
+ */
67
+ clusterBy?: ClusterByFn;
40
68
  /**
41
- * Enables a workaround for Intel GPU compatibility issues in WebGL shader compilation.
42
- * When enabled, this flag modifies how physics simulation shaders are compiled to avoid
43
- * rendering problems on Intel graphics hardware.
69
+ * Draws a convex-hull outline around each cluster.
44
70
  *
45
- * @remarks Requires a restart of NVL as shaders need to be recompiled.
71
+ * @note Outline color is the same as the nodes in the cluster.
72
+ * If the nodes in the cluster have different colors, the outline uses
73
+ * {@link NvlOptions.styling.overlayLineColor}.
74
+ * @defaultValue false
75
+ */
76
+ drawOutline?: boolean;
77
+ /**
78
+ * Fills a soft glowing halo around each cluster.
46
79
  *
47
- * @deprecated This option will be removed in a future release, where WebGL will be upgraded to WebGL2,
48
- * which resolves the compatibility issues with Intel GPUs.
80
+ * @note Halo color is the same as the nodes in the cluster.
81
+ * If the nodes in the cluster have different colors, the halo won't be shown.
82
+ * @defaultValue false
49
83
  */
50
- intelWorkaround?: boolean;
84
+ drawHalo?: boolean;
51
85
  /**
52
- * @deprecated This option will be removed in a future release. For small datasets, use the d3Force layout instead.
53
- * Whether to enable automatic switching to CoseBilkent layout for small graphs.
54
- * When enabled, small graphs will automatically use the CoseBilkent layout algorithm
55
- * which can provide better initial positioning for smaller datasets.
56
- * @defaultValue true
86
+ * Spring force multiplier for relationships that cross a {@link ClusteringOptions.clusterBy}
87
+ * cluster boundary.
88
+ * Capped between `0` and `1`.
89
+ * `0` disables cross-cluster spring pull. `1` is full strength. Intra-cluster edges are always
90
+ * full strength. Only applies when {@link ClusteringOptions.clusterBy} is set.
91
+ * @defaultValue 0
92
+ * @experimental
93
+ * @note Can cause unstable layouts when set to close to `1` in highly connected graphs.
94
+ */
95
+ crossClusterRelationshipMultiplier?: number;
96
+ }
97
+ /**
98
+ * The options for the force directed layout
99
+ */
100
+ export interface ForceDirectedOptions {
101
+ /**
102
+ * How initial node positions are seeded before the physics simulation runs.
103
+ * @defaultValue 'box'
104
+ */
105
+ seedingMethod?: 'box' | 'circle';
106
+ /**
107
+ * Physical clustering and cluster decoration options.
57
108
  */
58
- enableCytoscape?: boolean;
109
+ clustering?: ClusteringOptions;
59
110
  }
111
+ /**
112
+ * Returns the clustering options from force-directed layout options, if present.
113
+ * @param layoutOptions - The layout options to read from.
114
+ * @returns The clustering options, or undefined when absent / not force-directed.
115
+ */
116
+ export declare const getClusteringOptions: (layoutOptions?: LayoutOptions) => ClusteringOptions | undefined;
117
+ /**
118
+ * Whether {@link ClusteringOptions.drawOutline} is enabled on the given layout options.
119
+ * @param layoutOptions - The layout options to check.
120
+ * @returns Whether {@link ClusteringOptions.drawOutline} is enabled on the given layout options.
121
+ */
122
+ export declare const isDrawClusterOutlineEnabled: (layoutOptions?: LayoutOptions) => boolean;
123
+ /**
124
+ * Whether {@link ClusteringOptions.drawHalo} is enabled on the given layout options.
125
+ * @param layoutOptions - The layout options to check.
126
+ * @returns Whether {@link ClusteringOptions.drawHalo} is enabled on the given layout options.
127
+ */
128
+ export declare const isDrawClusterHaloEnabled: (layoutOptions?: LayoutOptions) => boolean;
60
129
  /**
61
130
  * The options for the hierarchical layout
62
131
  */
@@ -65,6 +134,16 @@ export interface HierarchicalOptions {
65
134
  direction?: 'up' | 'down' | 'left' | 'right';
66
135
  /** The packing method to be used */
67
136
  packing?: 'bin' | 'stack';
137
+ /**
138
+ * Optional function to group nodes into semantic clusters based on a custom identifier.
139
+ * Nodes returning the same string or number will be grouped together in the layout via
140
+ * dagre compound subgraphs. Nodes returning undefined will be treated as independent.
141
+ *
142
+ * @note Ensure this function has a stable reference to avoid evaluation on every render.
143
+ *
144
+ * @experimental
145
+ */
146
+ clusterBy?: ClusterByFn;
68
147
  }
69
148
  export declare const isHierarchicalLayoutOptions: (options: LayoutOptions) => options is HierarchicalOptions;
70
149
  /**
@@ -102,11 +181,12 @@ export declare const d3ForceLayoutType = "d3Force";
102
181
  */
103
182
  export declare const CircularLayoutType = "circular";
104
183
  /**
105
- * A WebGL renderer that uses the GPU for rendering.
184
+ * A WebGL2 renderer that uses the GPU for rendering.
106
185
  *
107
186
  * @remarks
108
- * This renderer has better performance than the canvas renderer,
109
- * but does not support captions and arrowheads.
187
+ * Upgraded to WebGL2 in NVL 2.0.
188
+ * This renderer has better performance than the canvas renderer.
189
+ * Supports rendering node icons and relationship arrowheads, but does not support captions.
110
190
  */
111
191
  export declare const WebGLRendererType = "webgl";
112
192
  /**
@@ -114,7 +194,7 @@ export declare const WebGLRendererType = "webgl";
114
194
  *
115
195
  * @remarks
116
196
  * This renderer has lower performance than the WebGL renderer,
117
- * but supports captions and arrowheads.
197
+ * but supports captions.
118
198
  */
119
199
  export declare const CanvasRendererType = "canvas";
120
200
  /**
@@ -183,8 +263,9 @@ export interface NvlOptions {
183
263
  /**
184
264
  * What renderer to use
185
265
  * @defaultValue 'canvas'
186
- * WebGL renderer uses GPU and has better performance.
187
- * Captions and arrowheads are only displayed when using the canvas or svg renderer.
266
+ * WebGL renderer uses GPU (WebGL2) and has better performance.
267
+ * Supports node icons and relationship arrowheads.
268
+ * Captions are only displayed when using the canvas or svg renderer.
188
269
  */
189
270
  renderer?: Renderer;
190
271
  /**
@@ -239,11 +320,23 @@ export interface NvlOptions {
239
320
  minimapViewportBoxColor?: string;
240
321
  };
241
322
  /**
242
- * @internal
243
- * @hidden
244
- * Defines a time limit for how long layout iterations may run
323
+ * Target minimum presentation fps while layout is active, at the cost of stabilization speed when set higher.
324
+ * Budgets layout steps only. Actual fps may undershoot when rendering is expensive.
325
+ *
326
+ * @defaultValue 3
327
+ *
328
+ * @note Will be capped between 1 and 120.
329
+ */
330
+ minFps?: number;
331
+ /**
332
+ * Target maximum presentation fps while layout is active; extra time in the tick goes to layout stabilization.
333
+ * Budgets layout steps only. Actual fps may undershoot when rendering is expensive.
334
+ *
335
+ * @defaultValue 30
336
+ *
337
+ * @note Will be capped between 1 and 120.
245
338
  */
246
- layoutTimeLimit?: number;
339
+ maxFps?: number;
247
340
  /**
248
341
  * Disables tracking of library usage
249
342
  * @defaultValue false
@@ -0,0 +1,73 @@
1
+ import { type CoarsenedGraph } from '../layouts/forcedirectedlayout/clustering/types';
2
+ import { type NvlState } from '../modules/state/types';
3
+ import type { Node } from '../types/graph-element';
4
+ import { type Point } from '../utils/geometry';
5
+ type ClusterPositions = Readonly<Record<string, Point>> | (() => (Node & Point)[]);
6
+ /**
7
+ * Canvas2D overlay for cluster decorations.
8
+ */
9
+ export default class ClusterOverlay {
10
+ private readonly canvas;
11
+ private readonly context;
12
+ private readonly state;
13
+ private readonly stateDisposers;
14
+ private readonly clusterColors;
15
+ private clusterColorsDirty;
16
+ private cachedFirstSubGraphLevel;
17
+ private cachedIdToPosition;
18
+ private cachedPositionListVersion;
19
+ private readonly clusterGeometry;
20
+ private geometryPositionsVersion;
21
+ constructor(canvas: HTMLCanvasElement, context: CanvasRenderingContext2D, state: NvlState);
22
+ /** Clears the overlay. */
23
+ clear(): void;
24
+ /**
25
+ * Draws cluster decorations for a given multi-member first-level sun system and positions.
26
+ * @param subGraphs - Layout coarsening hierarchy (`subGraphs[1]` = first-level suns).
27
+ * @param positions - Node id to position map, or a getter for a lagged position list.
28
+ * @param cacheVersion - PhysLayout positions-cache version.
29
+ */
30
+ render(subGraphs: CoarsenedGraph[], positions: ClusterPositions, cacheVersion: number): void;
31
+ destroy(): void;
32
+ private resetClusterColorCache;
33
+ private resetGeometryCache;
34
+ private syncGeometryCacheVersion;
35
+ private resetPositionCache;
36
+ /**
37
+ * Returns an id to position map. Records are used as-is. Getters are converted once per `cacheVersion`.
38
+ * @param positions - Node id to position map, or a getter for a lagged position list.
39
+ * @param cacheVersion - PhysLayout positions-cache version; ignored for maps.
40
+ * @returns An id to position map.
41
+ */
42
+ private resolveIdToPosition;
43
+ /**
44
+ * Rebuilds {@link clusterColors} for every multi-member first-level sun.
45
+ * @param firstSubGraphLevel - `subGraphs[1]` from the current layout coarsening hierarchy.
46
+ */
47
+ private rebuildClusterColors;
48
+ /**
49
+ * Returns fitted world-space hull for a cluster, reusing the last fit while the
50
+ * positions cache version is unchanged (e.g. pan/zoom-only frames while layout is stable).
51
+ * @param sunId - Original id of the sun the hull belongs to.
52
+ * @param collectMembers - Builds the member disks to fit; only called on a cache miss.
53
+ * @returns The fitted hull, or null when the cluster cannot be fitted.
54
+ */
55
+ private getCachedHull;
56
+ private fillHalo;
57
+ private strokeOutline;
58
+ /**
59
+ * Traces a closed rounded path along the given points.
60
+ * @param points - The points to trace the path along.
61
+ */
62
+ private traceSmoothClosedPath;
63
+ private applyZoomAndPan;
64
+ /**
65
+ * Returns the shared member color when color is consistent; otherwise null.
66
+ * @param nodeIds - The IDs of the nodes in the cluster.
67
+ * @param idToItem - The map of node IDs to items.
68
+ * @param defaultNodeColor - The default node color.
69
+ * @returns The shared member color when color is consistent; otherwise null.
70
+ */
71
+ private resolveUnanimousClusterColor;
72
+ }
73
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -4,6 +4,7 @@ declare class ImageCache {
4
4
  private readonly onImageLoadedCb?;
5
5
  constructor(onImageLoaded?: () => void);
6
6
  getImage(src: string, inverted?: boolean): HTMLCanvasElement;
7
+ isDrawn(src: string, inverted?: boolean): boolean;
7
8
  private getOrCreateEntry;
8
9
  private invertCanvas;
9
10
  private loadImage;
@@ -11,8 +11,7 @@ export type TextSegment = {
11
11
  */
12
12
  export type StyledCaption = {
13
13
  /**
14
- * The font styles as an array of style keywords.
15
- * @example `['bold', 'italic']`
14
+ * The font styles as an array of style keywords, e.g. `['bold', 'italic']`.
16
15
  */
17
16
  styles?: string[];
18
17
  /** The text value for the caption. */
@@ -10,8 +10,10 @@ export interface ControllerInterface {
10
10
  stateDisposers: NvlState['autorun'][];
11
11
  state: NvlState;
12
12
  needsToRun: () => boolean;
13
- renderMainScene: (positionArray: Node[]) => void;
14
- renderMinimap: (positionArray: Node[]) => void;
13
+ renderMainScene: (positionArray: Node[], positionsChanged?: boolean) => void;
14
+ renderMainSceneFromTexture: (positionTexture: WebGLTexture) => void;
15
+ setPositionTexelIndices: (map: Readonly<Record<string, number>> | null) => void;
16
+ renderMinimap: (positionArray: Node[], positionsChanged?: boolean) => void;
15
17
  checkForUpdates: (nodes: DataSet<Node>, rels: DataSet<Relationship>) => void;
16
18
  onResize: () => void;
17
19
  updateMainViewport: (zoom: number, panX: number, panY: number) => void;
@@ -34,8 +36,25 @@ export default class Controller implements ControllerInterface {
34
36
  state?: NvlState;
35
37
  });
36
38
  needsToRun(): boolean;
37
- renderMainScene(positionArray: Node[]): void;
38
- renderMinimap(positionArray: Node[]): void;
39
+ renderMainScene(positionArray: Node[], positionsChanged?: boolean): void;
40
+ /**
41
+ * Renders the main scene from a GPU position texture instead of a
42
+ * CPU position array, avoiding the readback + upload round trip.
43
+ * @param positionTexture - Live GPU node-position texture to render from.
44
+ */
45
+ renderMainSceneFromTexture(positionTexture: WebGLTexture): void;
46
+ /**
47
+ * Sets the node-id -> texel-index map for the main scene renderer so it
48
+ * samples a shared position texture in the layout's order. Pass null to use
49
+ * insertion order.
50
+ * @param map - The node-id-to-texel-index map, or null.
51
+ *
52
+ * @note The minimap keeps insertion-order sampling and uploads lagged CPU
53
+ * positions (async PhysLayout cache) while the main scene may sample the
54
+ * shared GPU texture.
55
+ */
56
+ setPositionTexelIndices(map: Readonly<Record<string, number>> | null): void;
57
+ renderMinimap(positionArray: Node[], positionsChanged?: boolean): void;
39
58
  checkForUpdates(nodes: DataSet<Node>, rels: DataSet<Relationship>): void;
40
59
  onResize(): void;
41
60
  updateMainViewport(zoom: number, panX: number, panY: number): void;
@@ -52,8 +71,10 @@ export declare class DummyController implements ControllerInterface {
52
71
  minimapMouseDown: boolean;
53
72
  stateDisposers: NvlState['autorun'][];
54
73
  state: NvlState;
55
- renderMainScene(positionArray: Node[]): void;
56
- renderMinimap(positionArray: Node[]): void;
74
+ renderMainScene(positionArray: Node[], positionsChanged?: boolean): void;
75
+ renderMainSceneFromTexture(positionTexture: WebGLTexture): void;
76
+ setPositionTexelIndices(map: Readonly<Record<string, number>> | null): void;
77
+ renderMinimap(positionArray: Node[], positionsChanged?: boolean): void;
57
78
  checkForUpdates(nodes: DataSet<Node>, rels: DataSet<Relationship>): void;
58
79
  onResize(): void;
59
80
  updateMainViewport(zoom: number, panX: number, panY: number): void;
@@ -0,0 +1,14 @@
1
+ export declare const MaxIconLayers = 256;
2
+ /**
3
+ * Maps icon URLs to stable layer indices for a WebGL `sampler2DArray`.
4
+ * Delegates image loading and rasterization to {@link ImageCache}.
5
+ */
6
+ export declare class IconLayerRegistry {
7
+ private readonly imageCache;
8
+ private readonly layerMap;
9
+ constructor(onImageLoaded?: () => void);
10
+ getLayer(url: string, inverted: boolean): number | null;
11
+ hasLoadingLayers(): boolean;
12
+ layerCount(): number;
13
+ getAvailableCanvases(): Map<number, HTMLCanvasElement>;
14
+ }
@@ -2,11 +2,17 @@ import { mat4 } from 'gl-matrix';
2
2
  import type { DataSet } from '../../modules/dataset';
3
3
  import type { NvlState } from '../../modules/state/types';
4
4
  import type { Node, Relationship } from '../../types/graph-element';
5
+ export type RendererOptions = {
6
+ enableIcons?: boolean;
7
+ };
5
8
  export default class Renderer {
6
9
  private readonly nodeShader;
7
10
  private readonly nodeAnimShader;
8
11
  private readonly relShader;
12
+ private readonly arrowShader;
9
13
  private relDataBuffer;
14
+ private arrowDataBuffer;
15
+ private arrowData;
10
16
  private readonly viewportBoxShader;
11
17
  private readonly defaultRelColor;
12
18
  private readonly defaultNodeColor;
@@ -14,39 +20,73 @@ export default class Renderer {
14
20
  private readonly disableNodeColor;
15
21
  private readonly gl;
16
22
  activeNodes: Record<string, boolean>;
23
+ numActiveNodes: number;
17
24
  readonly canvas: HTMLCanvasElement;
18
25
  projection: mat4;
19
26
  private idToIndex;
27
+ /** Node-id → texel-index map for GPU-shared rendering; null falls back to insertion order. */
28
+ private texelIndexForId;
29
+ /** texelIndexForId value when vertex buffers were last built; used to detect changes in the texel-index mapping. */
30
+ private texelMapAtBuild;
31
+ private lastNodes;
32
+ private lastBundledRels;
33
+ private idToSize;
20
34
  private posBuffer;
35
+ private positionsDirty;
21
36
  private numRels;
22
37
  private numNodes;
23
38
  private nodeDataByte;
24
39
  private relIdToIndex;
25
40
  private relData;
26
41
  private viewportBoxBuffer;
42
+ private readonly viewportBoxVertices;
27
43
  private relBuffer;
44
+ private arrowBuffer;
28
45
  private nodeBuffer;
29
46
  private posTexture;
47
+ private iconTexture;
30
48
  private nodeVao;
31
49
  private relVao;
50
+ private arrowVao;
32
51
  private nodeAnimVao;
33
- private vaoExt;
34
- constructor(gl: WebGLRenderingContext, nodes: DataSet<Node>, rels: DataSet<Relationship>, state: NvlState);
52
+ private readonly iconLayers;
53
+ private uploadedIconLayers;
54
+ private nodeColors;
55
+ private nodeDisabled;
56
+ constructor(gl: WebGL2RenderingContext, nodes: DataSet<Node>, rels: DataSet<Relationship>, state: NvlState, options?: RendererOptions);
35
57
  private setShaderUniforms;
36
58
  setData(data: {
37
59
  nodes: Node[];
38
60
  rels: Relationship[];
39
61
  }): void;
40
- render(positionArray: Node[]): void;
62
+ /** Sets the texel-index map for GPU-shared rendering, or null for insertion order. */
63
+ setPositionTexelIndices(map: Readonly<Record<string, number>> | null): void;
64
+ /** Texel index for a node id; falls back to insertion index, or 0 if the id is unknown. */
65
+ private getTexelIndex;
66
+ /**
67
+ * Rebuilds vertex buffers when the texel-index mapping has changed since the last build.
68
+ */
69
+ private rebuildBuffersIfTexelMappingChanged;
70
+ render(positionArray: Node[], positionsChanged?: boolean): void;
71
+ /** Renders using a GPU position texture directly as `u_positions` */
72
+ renderFromGpuTexture(positionTexture: WebGLTexture): void;
73
+ private renderRelationships;
41
74
  renderViewbox(): void;
42
75
  updateNodes(updates: Node[]): void;
43
76
  updateRelationships(relItems: Relationship[]): void;
44
77
  private createPositionTexture;
78
+ private createIconTexture;
79
+ private uploadPendingIconLayers;
80
+ hasPendingIcons(): boolean;
45
81
  updateViewportBox(zoom: number, panX: number, panY: number, viewportWidth: number, viewportHeight: number): void;
46
82
  updateViewport(zoom: number, panX: number, panY: number): void;
47
83
  private setupViewportRendering;
84
+ private resolveNodeColor;
48
85
  private setupNodeRendering;
49
86
  private setupRelationshipRendering;
87
+ private setupArrowRendering;
88
+ private getRelBufferAttributes;
89
+ private updateEndpointSizesForNode;
50
90
  private renderAnimations;
51
91
  destroy(): void;
52
92
  }
@@ -1,2 +1,2 @@
1
- declare const _default: "uniform mediump float u_zoom;\nuniform lowp float u_animPos;\nuniform mediump float u_glAdjust;\n\nvarying lowp vec4 color;\nvarying lowp float varNodeSize;\nmediump float circleSize = 0.50;\n\nvoid main(void) {\n\n lowp float dist = distance(gl_PointCoord, vec2(0.5));\n\n if (dist > 0.51) {\n discard;\n }\n else {\n lowp float alpha = 1.0 - smoothstep(circleSize - (1.0 / (varNodeSize * 2.0 * u_zoom * u_glAdjust)), circleSize, dist);\n lowp float pulseAlpha = 0.9 - 0.9 * u_animPos;\n gl_FragColor = vec4(color.xyz, 1.0) * alpha * pulseAlpha;\n }\n}\n";
1
+ declare const _default: "#version 300 es\nprecision mediump float;\n\nuniform mediump float u_zoom;\nuniform lowp float u_animPos;\nuniform mediump float u_glAdjust;\n\nin lowp vec4 color;\nin mediump float varNodeSize;\n\nout vec4 fragColor;\n\nconst float pulseCircleEdge = 0.50;\n\nvoid main(void) {\n float dist = distance(gl_PointCoord, vec2(0.5));\n\n if (dist > 0.51) {\n discard;\n } else {\n float alpha = 1.0 - smoothstep(pulseCircleEdge - (1.0 / (varNodeSize * 2.0 * u_zoom * u_glAdjust)), pulseCircleEdge, dist);\n float pulseAlpha = 0.9 - 0.9 * u_animPos;\n fragColor = vec4(color.xyz, 1.0) * alpha * pulseAlpha;\n }\n}\n";
2
2
  export default _default;
@@ -1,2 +1,2 @@
1
- declare const _default: "uniform mat4 u_projection;\nuniform sampler2D u_positions;\nuniform lowp float u_animPos;\nuniform mediump float u_zoom;\nuniform mediump float u_glAdjust;\n\nattribute float a_size;\nattribute lowp vec2 a_index;\nattribute lowp vec4 a_color;\nattribute lowp float a_active;\n\nvarying lowp float varNodeSize;\nvarying lowp vec4 color;\n\nvoid main(void) {\n vec4 data = texture2D(u_positions, a_index);\n\n gl_Position = u_projection * vec4(data.xy, 0.0, 1.0);\n\n float nodeSize = a_size * u_zoom * u_glAdjust;\n nodeSize = nodeSize * (0.4 / 0.5); // Remove extra space added for shadow and selection ring\n\n gl_PointSize = a_active > 0.5\n ? nodeSize + 0.88 * nodeSize * u_animPos\n : 0.0;\n\n color = a_color;\n varNodeSize = a_size;\n}";
1
+ declare const _default: "#version 300 es\nuniform mat4 u_projection;\nuniform sampler2D u_positions;\nuniform lowp float u_animPos;\nuniform mediump float u_zoom;\nuniform mediump float u_glAdjust;\n\nin float a_size;\nin highp vec2 a_index;\nin lowp vec4 a_color;\nin lowp float a_active;\n\nout mediump float varNodeSize;\nout lowp vec4 color;\n\nvoid main(void) {\n vec4 data = texture(u_positions, a_index);\n\n gl_Position = u_projection * vec4(data.xy, 0.0, 1.0);\n\n float nodeSize = a_size * u_zoom * u_glAdjust;\n nodeSize = nodeSize * 0.8;\n\n gl_PointSize = a_active > 0.5\n ? nodeSize + 0.88 * nodeSize * u_animPos\n : 0.0;\n\n color = a_color;\n varNodeSize = a_size;\n}";
2
2
  export default _default;
@@ -1,2 +1,2 @@
1
- declare const _default: "uniform mediump float u_zoom;\nuniform mediump float u_glAdjust;\n\nvarying lowp vec4 color;\nvarying lowp float drawDefaultBorder;\nvarying lowp vec3 nodeBorderColor;\nvarying lowp vec3 selectedBorderColor;\nvarying lowp vec3 selectedInnerBorderColor;\nvarying lowp vec3 shadowColor;\nvarying lowp float selected;\nvarying lowp float hovered;\nvarying lowp float varNodeSize;\n\nmediump float circleSize = 0.40;\nmediump float defaultBoderSize = 0.42;\nmediump float innerSelectedRingSize = 0.43;\nmediump float outerSelectedRingSize = 0.47;\nmediump float shadowSize = 0.51;\n\nlowp float getShadowAlpha(lowp float size, lowp float dist) {\n return (1.0 - smoothstep(size, shadowSize, dist)) * 0.25;\n}\n\nlowp float getCircleAlpha(lowp float ringSize, lowp float nodeSize, lowp float dist) {\n return color.w - smoothstep(ringSize - nodeSize, ringSize, dist);\n}\n\nvoid main() {\n lowp float dist = distance(gl_PointCoord, vec2(0.5));\n\n if (dist > 0.51) {\n discard;\n }\n else {\n lowp float nodeSize = 1.0 / varNodeSize;\n lowp float nodeAlpha = getCircleAlpha(circleSize, nodeSize, dist);\n lowp float borderAlpha = getCircleAlpha(defaultBoderSize, nodeSize, dist);\n lowp float ringAlpha = getCircleAlpha(innerSelectedRingSize, nodeSize, dist);\n lowp vec3 finalColor = color.xyz;\n lowp float finalAlpha = nodeAlpha;\n\n if (selected > 0.0) {\n lowp float shadowAlpha = getShadowAlpha(outerSelectedRingSize, dist);\n lowp float ringAlpha2 = getCircleAlpha(outerSelectedRingSize, nodeSize, dist);\n\n if (drawDefaultBorder > 0.0) {\n finalColor = mix(nodeBorderColor, finalColor, nodeAlpha);\n finalColor = mix(selectedInnerBorderColor, finalColor, borderAlpha);\n finalColor = mix(selectedBorderColor, finalColor, ringAlpha);\n finalColor = mix(shadowColor, finalColor, ringAlpha2);\n finalAlpha = max(shadowAlpha, max(nodeAlpha, max(borderAlpha, ringAlpha2)));\n } else {\n finalColor = mix(selectedInnerBorderColor, finalColor, nodeAlpha);\n finalColor = mix(selectedBorderColor, finalColor, ringAlpha);\n finalColor = mix(shadowColor, finalColor, ringAlpha2);\n finalAlpha = max(shadowAlpha, max(nodeAlpha, max(ringAlpha, ringAlpha2)));\n }\n } else if (hovered > 0.0) {\n if (drawDefaultBorder > 0.0) {\n lowp float shadowAlpha = getShadowAlpha(defaultBoderSize, dist);\n \n finalColor = mix(nodeBorderColor, finalColor, nodeAlpha);\n finalColor = mix(shadowColor, finalColor, borderAlpha);\n finalAlpha = max(shadowAlpha, max(nodeAlpha, ringAlpha));\n } else {\n lowp float shadowAlpha = getShadowAlpha(circleSize, dist);\n\n finalColor = mix(shadowColor, finalColor, nodeAlpha);\n finalAlpha = max(shadowAlpha, nodeAlpha);\n }\n } else {\n if (drawDefaultBorder > 0.0) {\n finalColor = mix(nodeBorderColor, finalColor, nodeAlpha);\n finalAlpha = borderAlpha;\n }\n }\n gl_FragColor = vec4(finalColor, 1.0) * finalAlpha;\n }\n}\n";
1
+ declare const _default: "#version 300 es\nprecision mediump float;\n\nuniform lowp vec3 u_nodeBorderColor;\nuniform lowp vec3 u_selectedBorderColor;\nuniform lowp vec3 u_selectedInnerBorderColor;\nuniform lowp vec3 u_shadowColor;\nuniform lowp float u_drawDefaultBorder;\nuniform lowp sampler2DArray u_icons;\nuniform mediump float u_minIconScreenPx;\n\nin lowp vec4 color;\nin lowp float selected;\nin lowp float hovered;\nin mediump float varNodeSize;\nin mediump float iconLayer;\nin lowp float hasIcon;\n\nout vec4 fragColor;\n\nconst float circleSize = 0.40;\nconst float defaultBorderSize = 0.42;\nconst float innerSelectedRingSize = 0.43;\nconst float outerSelectedRingSize = 0.47;\nconst float shadowSize = 0.51;\n\nfloat getShadowAlpha(float size, float dist) {\n return (1.0 - smoothstep(size, shadowSize, dist)) * 0.25;\n}\n\nfloat getCircleAlpha(float ringSize, float nodeSize, float dist) {\n return color.w - smoothstep(ringSize - nodeSize, ringSize, dist);\n}\n\nvoid main() {\n float dist = distance(gl_PointCoord, vec2(0.5));\n\n if (dist > 0.51) {\n discard;\n } else {\n float nodeSize = 1.0 / varNodeSize;\n float nodeAlpha = getCircleAlpha(circleSize, nodeSize, dist);\n float borderAlpha = getCircleAlpha(defaultBorderSize, nodeSize, dist);\n float ringAlpha = getCircleAlpha(innerSelectedRingSize, nodeSize, dist);\n vec3 finalColor = color.xyz;\n float finalAlpha = nodeAlpha;\n\n if (selected > 0.0) {\n float shadowAlpha = getShadowAlpha(outerSelectedRingSize, dist);\n float ringAlpha2 = getCircleAlpha(outerSelectedRingSize, nodeSize, dist);\n\n if (u_drawDefaultBorder > 0.0) {\n finalColor = mix(u_nodeBorderColor, finalColor, nodeAlpha);\n finalColor = mix(u_selectedInnerBorderColor, finalColor, borderAlpha);\n finalColor = mix(u_selectedBorderColor, finalColor, ringAlpha);\n finalColor = mix(u_shadowColor, finalColor, ringAlpha2);\n finalAlpha = max(shadowAlpha, max(nodeAlpha, max(borderAlpha, ringAlpha2)));\n } else {\n finalColor = mix(u_selectedInnerBorderColor, finalColor, nodeAlpha);\n finalColor = mix(u_selectedBorderColor, finalColor, ringAlpha);\n finalColor = mix(u_shadowColor, finalColor, ringAlpha2);\n finalAlpha = max(shadowAlpha, max(nodeAlpha, max(ringAlpha, ringAlpha2)));\n }\n } else if (hovered > 0.0) {\n if (u_drawDefaultBorder > 0.0) {\n float shadowAlpha = getShadowAlpha(defaultBorderSize, dist);\n finalColor = mix(u_nodeBorderColor, finalColor, nodeAlpha);\n finalColor = mix(u_shadowColor, finalColor, borderAlpha);\n finalAlpha = max(shadowAlpha, max(nodeAlpha, ringAlpha));\n } else {\n float shadowAlpha = getShadowAlpha(circleSize, dist);\n finalColor = mix(u_shadowColor, finalColor, nodeAlpha);\n finalAlpha = max(shadowAlpha, nodeAlpha);\n }\n } else {\n if (u_drawDefaultBorder > 0.0) {\n finalColor = mix(u_nodeBorderColor, finalColor, nodeAlpha);\n finalAlpha = borderAlpha;\n }\n }\n\n bool iconLargeEnough = varNodeSize * circleSize >= u_minIconScreenPx;\n if (hasIcon > 0.5 && iconLargeEnough) {\n vec2 uv = (gl_PointCoord - vec2(0.5)) / circleSize + 0.5;\n\n float inX = step(0.0, uv.x) - step(1.0, uv.x);\n float inY = step(0.0, uv.y) - step(1.0, uv.y);\n float isInside = inX * inY;\n\n vec4 ic = texture(u_icons, vec3(uv, iconLayer));\n finalColor = mix(finalColor, ic.rgb, ic.a * isInside);\n }\n\n fragColor = vec4(finalColor, 1.0) * finalAlpha;\n }\n}\n";
2
2
  export default _default;
@@ -1,2 +1,2 @@
1
- declare const _default: "uniform mat4 u_projection;\nuniform sampler2D u_positions;\nuniform mediump float u_zoom;\nuniform mediump float u_glAdjust;\nuniform lowp vec3 u_nodeBorderColor;\nuniform lowp vec3 u_selectedBorderColor;\nuniform lowp vec3 u_selectedInnerBorderColor;\nuniform lowp vec3 u_shadowColor;\nuniform lowp float u_drawDefaultBorder;\n\nattribute float a_size;\nattribute lowp vec2 a_index;\nattribute lowp vec4 a_color;\nattribute lowp float a_selected;\nattribute lowp float a_hovered;\n\nvarying lowp float window;\nvarying lowp vec4 color;\nvarying lowp float selected;\nvarying lowp float hovered;\nvarying lowp float varNodeSize;\nvarying lowp vec3 nodeBorderColor;\nvarying lowp vec3 selectedBorderColor;\nvarying lowp vec3 selectedInnerBorderColor;\nvarying lowp vec3 shadowColor;\nvarying lowp float drawDefaultBorder;\n\nlowp float MinNodeSize = 5.0;\n\nvoid main(void) {\n vec4 data = texture2D(u_positions, a_index);\n gl_Position = u_projection * vec4(data.xy, 0.0, 1.0);\n\n lowp float nodeSize = a_size * u_zoom * u_glAdjust;\n if (nodeSize < MinNodeSize) {\n gl_PointSize = MinNodeSize;\n varNodeSize = MinNodeSize;\n } else {\n gl_PointSize = nodeSize;\n varNodeSize = nodeSize;\n }\n\n color = a_color;\n selected = a_selected;\n hovered = a_hovered;\n nodeBorderColor = u_nodeBorderColor;\n selectedBorderColor = u_selectedBorderColor;\n selectedInnerBorderColor = u_selectedInnerBorderColor;\n shadowColor = u_shadowColor;\n drawDefaultBorder = u_drawDefaultBorder;\n}";
1
+ declare const _default: "#version 300 es\nuniform mat4 u_projection;\nuniform sampler2D u_positions;\nuniform mediump float u_zoom;\nuniform mediump float u_glAdjust;\n\nin float a_size;\nin highp vec2 a_index;\nin lowp vec4 a_color;\nin lowp float a_selected;\nin lowp float a_hovered;\nin lowp float a_iconLayer;\nin lowp float a_hasIcon;\n\nout lowp vec4 color;\nout lowp float selected;\nout lowp float hovered;\nout mediump float varNodeSize;\nout mediump float iconLayer;\nout lowp float hasIcon;\n\nconst float MinNodeSize = 5.0;\n\nvoid main(void) {\n vec4 data = texture(u_positions, a_index);\n gl_Position = u_projection * vec4(data.xy, 0.0, 1.0);\n\n float nodeSize = max(a_size * u_zoom * u_glAdjust, MinNodeSize);\n gl_PointSize = nodeSize;\n varNodeSize = nodeSize;\n\n color = a_color;\n selected = a_selected;\n hovered = a_hovered;\n iconLayer = a_iconLayer * 255.0;\n hasIcon = a_hasIcon;\n}";
2
2
  export default _default;
@@ -1,2 +1,2 @@
1
- declare const _default: "varying lowp vec4 color;\n\nvoid main(void) {\n gl_FragColor = vec4(color.xyz, 1.0) * color.w;\n}\n";
1
+ declare const _default: "#version 300 es\nprecision lowp float;\n\nin lowp vec4 color;\n\nout vec4 fragColor;\n\nvoid main(void) {\n fragColor = vec4(color.xyz, 1.0) * color.w;\n}\n";
2
2
  export default _default;
@@ -0,0 +1 @@
1
+ export declare const arrowShaderUtilsGLSL = "\nconst float CircleSize = 0.40;\nconst float HeadHalfWidthScale = 4.0;\nconst float HeadLengthScale = 2.5;\nconst float MaxHeadHalfWidthRatio = 0.6;\nconst float MaxHeadLengthRatio = 1.0;\n\n// Self-referencing relationships aren't supported in WebGL yet.\nbool relationshipIsSelfLoop(vec2 fromTexel, vec2 toTexel) {\n return all(equal(fromTexel, toTexel));\n}\n\nvec2 perpendicular(vec2 dir) {\n return vec2(-dir.y, dir.x);\n}\n\nfloat nodeRadiusFromSize(float size) {\n return size * u_glAdjust * CircleSize;\n}\n\nfloat relationshipHalfWidth(float width) {\n return width * u_glAdjust * 0.25;\n}\n\nfloat naturalArrowHeadHalfWidth(float lineHalfWidth) {\n return lineHalfWidth * HeadHalfWidthScale + u_glAdjust;\n}\n\nfloat naturalArrowHeadLength(float headHalfWidth) {\n return headHalfWidth * HeadLengthScale;\n}\n\nfloat clampArrowHeadHalfWidth(float lineHalfWidth, float nodeRadius) {\n float natural = naturalArrowHeadHalfWidth(lineHalfWidth);\n float minHalfWidth = lineHalfWidth;\n float maxHalfWidth = max(minHalfWidth, nodeRadius * MaxHeadHalfWidthRatio);\n return clamp(natural, minHalfWidth, maxHalfWidth);\n}\n\nfloat clampArrowHeadLength(float headHalfWidth, float nodeRadius) {\n float natural = naturalArrowHeadLength(headHalfWidth);\n float maxLength = nodeRadius * MaxHeadLengthRatio;\n return min(natural, maxLength);\n}\n\nfloat arrowHeadClearance(float lineHalfWidth, float nodeRadius) {\n float headHalfWidth = clampArrowHeadHalfWidth(lineHalfWidth, nodeRadius);\n return clampArrowHeadLength(headHalfWidth, nodeRadius);\n}\n";
@@ -0,0 +1,2 @@
1
+ declare const _default: "#version 300 es\nuniform mat4 u_projection;\nuniform sampler2D u_positions;\nuniform mediump float u_glAdjust;\n\nin vec2 a_from;\nin vec2 a_to;\nin lowp vec4 a_color;\nin float a_width;\nin float a_toSize;\n\nout lowp vec4 color;\n\n\nconst float CircleSize = 0.40;\nconst float HeadHalfWidthScale = 4.0;\nconst float HeadLengthScale = 2.5;\nconst float MaxHeadHalfWidthRatio = 0.6;\nconst float MaxHeadLengthRatio = 1.0;\n\n// Self-referencing relationships aren't supported in WebGL yet.\nbool relationshipIsSelfLoop(vec2 fromTexel, vec2 toTexel) {\n return all(equal(fromTexel, toTexel));\n}\n\nvec2 perpendicular(vec2 dir) {\n return vec2(-dir.y, dir.x);\n}\n\nfloat nodeRadiusFromSize(float size) {\n return size * u_glAdjust * CircleSize;\n}\n\nfloat relationshipHalfWidth(float width) {\n return width * u_glAdjust * 0.25;\n}\n\nfloat naturalArrowHeadHalfWidth(float lineHalfWidth) {\n return lineHalfWidth * HeadHalfWidthScale + u_glAdjust;\n}\n\nfloat naturalArrowHeadLength(float headHalfWidth) {\n return headHalfWidth * HeadLengthScale;\n}\n\nfloat clampArrowHeadHalfWidth(float lineHalfWidth, float nodeRadius) {\n float natural = naturalArrowHeadHalfWidth(lineHalfWidth);\n float minHalfWidth = lineHalfWidth;\n float maxHalfWidth = max(minHalfWidth, nodeRadius * MaxHeadHalfWidthRatio);\n return clamp(natural, minHalfWidth, maxHalfWidth);\n}\n\nfloat clampArrowHeadLength(float headHalfWidth, float nodeRadius) {\n float natural = naturalArrowHeadLength(headHalfWidth);\n float maxLength = nodeRadius * MaxHeadLengthRatio;\n return min(natural, maxLength);\n}\n\nfloat arrowHeadClearance(float lineHalfWidth, float nodeRadius) {\n float headHalfWidth = clampArrowHeadHalfWidth(lineHalfWidth, nodeRadius);\n return clampArrowHeadLength(headHalfWidth, nodeRadius);\n}\n\n\nvoid main(void) {\n vec4 from = texture(u_positions, a_from);\n vec4 to = texture(u_positions, a_to);\n\n if (relationshipIsSelfLoop(a_from, a_to)) {\n color = a_color;\n gl_Position = u_projection * vec4(from.xy, 0.0, 1.0);\n return;\n }\n\n vec2 delta = to.xy - from.xy;\n vec2 dir = normalize(delta);\n vec2 perp = perpendicular(dir);\n\n float toRadius = nodeRadiusFromSize(a_toSize);\n float lineHalfWidth = relationshipHalfWidth(a_width);\n float headHalfWidth = clampArrowHeadHalfWidth(lineHalfWidth, toRadius);\n float headLength = clampArrowHeadLength(headHalfWidth, toRadius);\n\n vec2 tip = to.xy - dir * toRadius;\n vec2 baseCenter = tip - dir * headLength;\n\n int corner = gl_VertexID % 3;\n float isTip = float(corner == 0);\n float perpSign = float(corner == 1) - float(corner == 2);\n vec2 basePos = mix(baseCenter, tip, isTip);\n vec2 vertexPosition = basePos + perp * (headHalfWidth * perpSign);\n\n color = a_color;\n gl_Position = u_projection * vec4(vertexPosition, 0.0, 1.0);\n}\n";
2
+ export default _default;
@@ -1,2 +1,2 @@
1
- declare const _default: "uniform mat4 u_projection;\nuniform sampler2D u_positions;\nuniform mediump float u_glAdjust;\n\nattribute vec2 a_from;\nattribute vec2 a_to;\nattribute lowp vec4 a_color;\nattribute lowp float a_triside;\nattribute float a_width;\n\nvarying lowp vec4 color;\n\nvoid main(void) {\n vec4 from = texture2D(u_positions, a_from);\n vec4 to = texture2D(u_positions, a_to);\n vec2 direction = normalize(to.xy - from.xy);\n direction = vec2(-direction.y, direction.x);\n\n float width = a_width * u_glAdjust * 0.5;\n\n if (a_triside == 0.0) {\n from.y += (width / 2.0) * direction.y;\n from.x += (width / 2.0) * direction.x;\n } else {\n from.y -= (width / 2.0) * direction.y;\n from.x -= (width / 2.0) * direction.x;\n }\n\n color = a_color;\n gl_Position = u_projection * vec4(from.xy, 0.0, 1.0);\n}\n";
1
+ declare const _default: "#version 300 es\nuniform mat4 u_projection;\nuniform sampler2D u_positions;\nuniform mediump float u_glAdjust;\n\nin vec2 a_from;\nin vec2 a_to;\nin lowp vec4 a_color;\nin lowp float a_triside;\nin lowp float a_isTarget;\nin float a_endpointSize;\nin float a_otherEndpointSize;\nin float a_width;\n\nout lowp vec4 color;\n\n\nconst float CircleSize = 0.40;\nconst float HeadHalfWidthScale = 4.0;\nconst float HeadLengthScale = 2.5;\nconst float MaxHeadHalfWidthRatio = 0.6;\nconst float MaxHeadLengthRatio = 1.0;\n\n// Self-referencing relationships aren't supported in WebGL yet.\nbool relationshipIsSelfLoop(vec2 fromTexel, vec2 toTexel) {\n return all(equal(fromTexel, toTexel));\n}\n\nvec2 perpendicular(vec2 dir) {\n return vec2(-dir.y, dir.x);\n}\n\nfloat nodeRadiusFromSize(float size) {\n return size * u_glAdjust * CircleSize;\n}\n\nfloat relationshipHalfWidth(float width) {\n return width * u_glAdjust * 0.25;\n}\n\nfloat naturalArrowHeadHalfWidth(float lineHalfWidth) {\n return lineHalfWidth * HeadHalfWidthScale + u_glAdjust;\n}\n\nfloat naturalArrowHeadLength(float headHalfWidth) {\n return headHalfWidth * HeadLengthScale;\n}\n\nfloat clampArrowHeadHalfWidth(float lineHalfWidth, float nodeRadius) {\n float natural = naturalArrowHeadHalfWidth(lineHalfWidth);\n float minHalfWidth = lineHalfWidth;\n float maxHalfWidth = max(minHalfWidth, nodeRadius * MaxHeadHalfWidthRatio);\n return clamp(natural, minHalfWidth, maxHalfWidth);\n}\n\nfloat clampArrowHeadLength(float headHalfWidth, float nodeRadius) {\n float natural = naturalArrowHeadLength(headHalfWidth);\n float maxLength = nodeRadius * MaxHeadLengthRatio;\n return min(natural, maxLength);\n}\n\nfloat arrowHeadClearance(float lineHalfWidth, float nodeRadius) {\n float headHalfWidth = clampArrowHeadHalfWidth(lineHalfWidth, nodeRadius);\n return clampArrowHeadLength(headHalfWidth, nodeRadius);\n}\n\n\nvoid main(void) {\n vec4 from = texture(u_positions, a_from);\n vec4 to = texture(u_positions, a_to);\n\n if (relationshipIsSelfLoop(a_from, a_to)) {\n color = a_color;\n gl_Position = u_projection * vec4(from.xy, 0.0, 1.0);\n return;\n }\n\n vec2 delta = from.xy - to.xy;\n float halfWidth = relationshipHalfWidth(a_width);\n float ownRadius = nodeRadiusFromSize(a_endpointSize);\n float otherRadius = nodeRadiusFromSize(a_otherEndpointSize);\n float toRadius = mix(otherRadius, ownRadius, a_isTarget);\n float arrowClearance = arrowHeadClearance(halfWidth, toRadius);\n float endpointClearance = ownRadius + a_isTarget * arrowClearance;\n\n vec2 along = normalize(delta);\n from.xy -= along * endpointClearance;\n\n float sideSign = 1.0 - 2.0 * a_triside;\n from.xy += sideSign * halfWidth * perpendicular(along);\n\n color = a_color;\n gl_Position = u_projection * vec4(from.xy, 0.0, 1.0);\n}\n";
2
2
  export default _default;
@@ -1,2 +1,2 @@
1
- declare const _default: "\nvarying lowp vec4 minimapViewportBoxColor;\n\nvoid main(void) {\n gl_FragColor = minimapViewportBoxColor;\n}\n";
1
+ declare const _default: "#version 300 es\nprecision lowp float;\n\nuniform lowp vec4 u_minimapViewportBoxColor;\n\nout vec4 fragColor;\n\nvoid main(void) {\n fragColor = u_minimapViewportBoxColor;\n}\n";
2
2
  export default _default;
@@ -1,2 +1,2 @@
1
- declare const _default: "\nattribute vec2 coordinates;\nuniform mat4 u_projection;\nuniform lowp vec4 u_minimapViewportBoxColor;\n\nvarying lowp vec4 minimapViewportBoxColor;\n\nvoid main(void) {\n gl_Position = u_projection * vec4(coordinates, 0.0, 1.0);\n minimapViewportBoxColor = u_minimapViewportBoxColor;\n}\n";
1
+ declare const _default: "#version 300 es\nin vec2 coordinates;\nuniform mat4 u_projection;\n\nvoid main(void) {\n gl_Position = u_projection * vec4(coordinates, 0.0, 1.0);\n}\n";
2
2
  export default _default;
@@ -1,4 +1,5 @@
1
1
  import type { StyledCaption } from '../renderers/domrenderer/shared/types';
2
+ import type { Point } from '../utils/geometry';
2
3
  /**
3
4
  * The base type for graph elements.
4
5
  * @private
@@ -97,6 +98,13 @@ interface Node extends GraphElement {
97
98
  */
98
99
  html?: HTMLElement;
99
100
  }
101
+ /**
102
+ * A node returned by {@link NVL.getNodes} with layout coordinates and screen position.
103
+ * `x` and `y` are canvas/layout space; `clientPosition` is relative to the canvas element.
104
+ */
105
+ type NodeWithClientPosition = Node & Point & {
106
+ clientPosition: Point;
107
+ };
100
108
  /**
101
109
  * A node with only a subset of its properties.
102
110
  */
@@ -132,4 +140,4 @@ type PartialRelationship = Partial<Relationship> & {
132
140
  /** The id of the relationship. */
133
141
  id: string;
134
142
  };
135
- export type { Node, Relationship, PartialNode, PartialRelationship };
143
+ export type { Node, NodeWithClientPosition, Relationship, PartialNode, PartialRelationship };
@@ -1,6 +1,6 @@
1
- import type { Node, Relationship } from './graph-element';
1
+ import type { Node, NodeWithClientPosition, Relationship } from './graph-element';
2
2
  export interface NvlWindow {
3
- __Nvl_dumpNodes: (instanceId?: string) => Node[];
3
+ __Nvl_dumpNodes: (instanceId?: string) => NodeWithClientPosition[];
4
4
  __Nvl_dumpRelationships: (instanceId?: string) => Relationship[];
5
5
  __Nvl_getNodesOnScreen: (instanceId?: string) => {
6
6
  nodes: Node[];
@@ -13,7 +13,7 @@ export declare const createCanvasForScreenshot: (fullWidth: number, fullHeight:
13
13
  export declare const createCanvasForRenderer: (parent?: HTMLElement, onContextLostCallback?: (e: WebGLContextEvent) => void) => HTMLCanvasElement;
14
14
  export declare const removeCanvas: (canvas: HTMLCanvasElement) => void;
15
15
  export declare const getCanvas2DContext: (canvas: HTMLCanvasElement) => CanvasRenderingContext2D;
16
- export declare const getWebGLContext: (canvas: HTMLCanvasElement) => WebGLRenderingContext;
16
+ export declare const getWebGLContext: (canvas: HTMLCanvasElement) => WebGL2RenderingContext | null;
17
17
  export declare const removeWebGLContext: (webGLCtx: WebGLRenderingContext) => void;
18
18
  export declare const checkWebGLAvailability: (canvas?: HTMLCanvasElement) => boolean;
19
19
  /**
@@ -0,0 +1,14 @@
1
+ import { type Point } from './geometry';
2
+ /** Extra world-space padding around each member disk before taking the cluster hull. */
3
+ export declare const ClusterHullPadding = 14;
4
+ /** Circumference samples per member disk before taking the convex hull. */
5
+ export declare const DiskSampleCount = 8;
6
+ export type ClusterMemberDisk = Point & {
7
+ radius: number;
8
+ };
9
+ /**
10
+ * Fits a convex hull around the given disks by sampling each member circumference.
11
+ * @param members - The cluster member disks to fit the hull around.
12
+ * @returns The fitted hull, or null if fewer than two members have a usable position/radius.
13
+ */
14
+ export declare const fitClusterHull: (members: ClusterMemberDisk[]) => Point[] | null;
@@ -0,0 +1 @@
1
+ export {};