@neo4j-nvl/base 1.2.0-e691fdb4 → 1.2.0-ec100981

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 (52) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +1 -1
  3. package/dist/base.mjs +1 -1
  4. package/dist/types/index.d.ts +5 -4
  5. package/dist/types/layouts/animatedlayout/AnimatedLayout.d.ts +2 -2
  6. package/dist/types/layouts/d3forcelayout/constants.d.ts +0 -1
  7. package/dist/types/layouts/d3forcelayout/d3ForceLayout.d.ts +2 -2
  8. package/dist/types/layouts/forcedirectedlayout/physlayout/PhysLayout.d.ts +83 -13
  9. package/dist/types/layouts/forcedirectedlayout/physlayout/clustering/ClusterByMerger.d.ts +60 -0
  10. package/dist/types/layouts/forcedirectedlayout/physlayout/clustering/SolarMerger.d.ts +55 -0
  11. package/dist/types/layouts/forcedirectedlayout/physlayout/clustering/clustering-utils.d.ts +112 -0
  12. package/dist/types/layouts/forcedirectedlayout/physlayout/clustering/clustering-utils.test.d.ts +1 -0
  13. package/dist/types/layouts/forcedirectedlayout/physlayout/{solarmerger → clustering}/types.d.ts +5 -0
  14. package/dist/types/layouts/forcedirectedlayout/physlayout/shaders/multilevel-fragment.d.ts +1 -1
  15. package/dist/types/layouts/forcedirectedlayout/physlayout/shaders/multilevel-repulsive-fragment.d.ts +1 -1
  16. package/dist/types/layouts/freeLayout/FreeLayout.d.ts +2 -4
  17. package/dist/types/layouts/gridLayout/GridLayout.d.ts +2 -6
  18. package/dist/types/modules/NvlController.d.ts +30 -2
  19. package/dist/types/modules/dataset.d.ts +3 -1
  20. package/dist/types/modules/state/types.d.ts +34 -15
  21. package/dist/types/renderers/domrenderer/canvasrenderer/arrowDrawing.d.ts +2 -16
  22. package/dist/types/renderers/domrenderer/shared/ImageCache.d.ts +1 -0
  23. package/dist/types/renderers/domrenderer/shared/arrows/arrows.d.ts +4 -6
  24. package/dist/types/renderers/webglrenderer/Controller.d.ts +26 -6
  25. package/dist/types/renderers/webglrenderer/IconLayerRegistry.d.ts +14 -0
  26. package/dist/types/renderers/webglrenderer/IconLayerRegistry.test.d.ts +1 -0
  27. package/dist/types/renderers/webglrenderer/Renderer.d.ts +43 -2
  28. package/dist/types/renderers/webglrenderer/node-animation-fragment.d.ts +1 -1
  29. package/dist/types/renderers/webglrenderer/node-animation-vertex.d.ts +1 -1
  30. package/dist/types/renderers/webglrenderer/node-fragment-point.d.ts +1 -1
  31. package/dist/types/renderers/webglrenderer/node-vertex-point.d.ts +1 -1
  32. package/dist/types/renderers/webglrenderer/rel-glsl-utils.d.ts +1 -0
  33. package/dist/types/renderers/webglrenderer/rel-vertex-arrow.d.ts +2 -0
  34. package/dist/types/renderers/webglrenderer/rel-vertex-line.d.ts +1 -1
  35. package/dist/types/renderers/webglrenderer/viewport-box-fragment.d.ts +1 -1
  36. package/dist/types/renderers/webglrenderer/viewport-box-vertex.d.ts +1 -1
  37. package/dist/types/types/graph-element.d.ts +9 -1
  38. package/dist/types/types/nvl-window-functions.d.ts +2 -2
  39. package/dist/types/utils/constants.d.ts +5 -2
  40. package/dist/types/utils/geometry.d.ts +16 -0
  41. package/dist/types/utils/layoutSeeding.d.ts +7 -0
  42. package/dist/types/utils/layoutSeeding.test.d.ts +1 -0
  43. package/dist/types/utils/webWorkerUtils.d.ts +1 -1
  44. package/dist/types/utils/zoomFunctions.d.ts +9 -2
  45. package/package.json +5 -5
  46. package/dist/types/layouts/forcedirectedlayout/ForceCytoLayout.d.ts +0 -22
  47. package/dist/types/layouts/forcedirectedlayout/cosebilkentlayout/CoseBilkentLayout.d.ts +0 -33
  48. package/dist/types/layouts/forcedirectedlayout/physlayout/solarmerger/SolarMerger.d.ts +0 -21
  49. package/dist/types/layouts/forcedirectedlayout/physlayout/solarmerger/solar-placer.d.ts +0 -2
  50. /package/dist/types/layouts/forcedirectedlayout/physlayout/{solarmerger/SolarMerger.bench.d.ts → clustering/ClusterByMerger.test.d.ts} +0 -0
  51. /package/dist/types/layouts/forcedirectedlayout/physlayout/{solarmerger/SolarMerger.test.d.ts → clustering/SolarMerger.bench.d.ts} +0 -0
  52. /package/dist/types/layouts/forcedirectedlayout/physlayout/{solarmerger/solar-placer.test.d.ts → clustering/SolarMerger.test.d.ts} +0 -0
@@ -3,7 +3,7 @@ import type { CircularOptions, ForceDirectedOptions, HierarchicalOptions, Render
3
3
  import { CanvasRendererType, CircularLayoutType, ForceDirectedLayoutType, FreeLayoutType, GridLayoutType, HierarchicalLayoutType, SvgRendererType, WebGLRendererType, d3ForceLayoutType } from './modules/state/types';
4
4
  import { drawCircleBand } from './renderers/domrenderer/canvasrenderer/nodeDrawing';
5
5
  import type { StyledCaption } from './renderers/domrenderer/shared/types';
6
- import type { Node, PartialNode, PartialRelationship, Relationship } from './types/graph-element';
6
+ import type { Node, NodeWithClientPosition, PartialNode, PartialRelationship, Relationship } from './types/graph-element';
7
7
  import { CompatibilityError } from './utils/errors';
8
8
  import type { Point } from './utils/geometry';
9
9
  import type { HitTargetNode, HitTargetRelationship, HitTargets } from './utils/hittest';
@@ -139,9 +139,10 @@ declare class NVL {
139
139
  removeRelationshipsWithIds(relationshipIds: string[]): void;
140
140
  /**
141
141
  * Returns all nodes that is currently stored in the visualisation.
142
- * @returns The array of {@link Node nodes}.
142
+ * Each node includes layout coordinates (`x`, `y`) and a `clientPosition` in screen space.
143
+ * @returns The array of {@link NodeWithClientPosition nodes}.
143
144
  */
144
- getNodes(): Node[];
145
+ getNodes(): NodeWithClientPosition[];
145
146
  /**
146
147
  * Returns the relationships that are currently in the visualization.
147
148
  * @returns An array of the {@link Relationship relationships} in the visualization.
@@ -415,5 +416,5 @@ declare const colorMapperFunctions: {
415
416
  */
416
417
  type Renderer = Exclude<InternalRenderer, typeof SvgRendererType>;
417
418
  export default NVL;
418
- export type { NvlOptions, Renderer, Node, Relationship, PartialNode, PartialRelationship, Layout, LayoutOptions, ForceDirectedOptions, HierarchicalOptions, CircularOptions, ExternalCallbacks, HitTargets, HitTargetNode, HitTargetRelationship, Point, NvlMouseEvent, ZoomOptions, StyledCaption, WebGLRendererType, CanvasRendererType };
419
+ export type { NvlOptions, Renderer, Node, NodeWithClientPosition, Relationship, PartialNode, PartialRelationship, Layout, LayoutOptions, ForceDirectedOptions, HierarchicalOptions, CircularOptions, ExternalCallbacks, HitTargets, HitTargetNode, HitTargetRelationship, Point, NvlMouseEvent, ZoomOptions, StyledCaption, WebGLRendererType, CanvasRendererType };
419
420
  export { NVL, colorMapperFunctions, CompatibilityError, ForceDirectedLayoutType, HierarchicalLayoutType, GridLayoutType, FreeLayoutType, d3ForceLayoutType, CircularLayoutType, drawCircleBand, nvlResultTransformer, getZoomTargetForNodePositions };
@@ -1,5 +1,5 @@
1
1
  import type { NvlState } from '../../modules/state/types';
2
- import type { Node } from '../../types/graph-element';
2
+ import type { Node, PartialNode } from '../../types/graph-element';
3
3
  export default class AnimatedLayout {
4
4
  positions: Record<string, Node>;
5
5
  oldPositions: Record<string, Node>;
@@ -20,6 +20,6 @@ export default class AnimatedLayout {
20
20
  updateOldPositionFromState(): void;
21
21
  startAnimation(): void;
22
22
  update(): void;
23
- updateNodes(positionList: Node[]): void;
23
+ updateNodes(positionList: PartialNode[]): void;
24
24
  getNodePositions(nodeList: Node[]): Node[];
25
25
  }
@@ -12,7 +12,6 @@ export declare const BARNES_HUT_THETA = 1.2;
12
12
  export declare const FORCE_LINK_DISTANCE: (relationship: SimulationLinkDatum<D3Node>) => number;
13
13
  export declare const FORCE_LINK_STRENGTH: (link: SimulationLinkDatum<SimulationNodeDatum>, count: number[]) => number;
14
14
  export declare const FORCE_COLLIDE_RADIUS: (node: D3Node) => number;
15
- export declare const LAYOUT_RADIUS: (numberOfNodes: number) => number;
16
15
  export declare const FORCE_CHARGE: () => number;
17
16
  export declare const FORCE_CHARGE_START: () => number;
18
17
  export declare const FORCE_CENTER_X = 0.03;
@@ -1,6 +1,6 @@
1
1
  import type { Simulation, SimulationLinkDatum, SimulationNodeDatum } from 'd3-force';
2
2
  import type { NvlState } from '../../modules/state/types';
3
- import type { Node, Relationship } from '../../types/graph-element';
3
+ import type { Node, PartialNode, Relationship } from '../../types/graph-element';
4
4
  import type { Point } from '../../utils/geometry';
5
5
  import type { D3Node } from './types';
6
6
  export declare class D3ForceLayout {
@@ -20,7 +20,7 @@ export declare class D3ForceLayout {
20
20
  state: NvlState;
21
21
  });
22
22
  setOptions(options: any): void;
23
- updateNodes(positionList: D3Node[]): void;
23
+ updateNodes(positionList: PartialNode[]): void;
24
24
  update(refreshPositions?: boolean): void;
25
25
  layout(_nodes: Node[], _rels: Relationship[], firstTimeAddingNodes: boolean): void;
26
26
  getNodePositions(nodeList: D3Node[]): any[];
@@ -1,6 +1,14 @@
1
1
  import type { ForceDirectedOptions, NvlState } from '../../../modules/state/types';
2
- import type { Node, Relationship } from '../../../types/graph-element';
3
- import type { Point } from '../../../utils/geometry';
2
+ import type { Node, PartialNode, Relationship } from '../../../types/graph-element';
3
+ import { type Point } from '../../../utils/geometry';
4
+ import type { CoarsenedGraph } from './clustering/types';
5
+ export declare const ConvergenceCheckInterval = 20;
6
+ /**
7
+ * Number of consecutive convergence checks that must report a below-threshold
8
+ * graph speed before the layout is considered settled. This debounces a single
9
+ * lucky-low reading so a still-moving graph isn't stopped prematurely.
10
+ */
11
+ export declare const StableChecksToConverge = 3;
4
12
  export declare class PhysLayout {
5
13
  private physVbo;
6
14
  private physSmallVbo;
@@ -10,14 +18,13 @@ export declare class PhysLayout {
10
18
  private averageNodeSize;
11
19
  private shouldUpdate;
12
20
  private iterationCount;
13
- private lastSpeedValues;
14
- private rollingAvgGraphSpeed;
15
21
  private nodeVariation;
16
22
  private nodeCenterPoint;
17
23
  private peakIterationMultiplier;
18
24
  private stateDisposers;
19
25
  private state;
20
26
  private dpr;
27
+ private maxGlNodeSize;
21
28
  private simulationStopVelocitySquared;
22
29
  private gravity;
23
30
  private force;
@@ -25,10 +32,11 @@ export declare class PhysLayout {
25
32
  private nodeIndexToId;
26
33
  private flatRelationshipKeys;
27
34
  private numNodes;
28
- private solarMerger;
29
35
  private subGraphs;
30
- private nodeSortMap;
31
36
  private firstUpdate;
37
+ private precomputeMs;
38
+ private precomputeStarted;
39
+ private precomputing;
32
40
  private curPhysData;
33
41
  private apprxRepForceShader;
34
42
  private levelsClusterTexture;
@@ -42,11 +50,18 @@ export declare class PhysLayout {
42
50
  private updateData;
43
51
  private updateShader;
44
52
  private physPositions;
53
+ private physDirty;
54
+ private convergencePbo;
55
+ private convergenceFence;
56
+ private graphIsStableCount;
45
57
  private springTexture;
46
58
  private sizeTexture;
47
59
  private offsetTexture;
48
60
  private pinTexture;
49
61
  private addedNodes;
62
+ private seedingMethod;
63
+ private clusterBy?;
64
+ private lastClusterByAssignments;
50
65
  private updateTexture;
51
66
  private physVao;
52
67
  private physSmallVao;
@@ -61,6 +76,8 @@ export declare class PhysLayout {
61
76
  * @param initial - Whether this is the first setting of options.
62
77
  */
63
78
  setOptions(options?: ForceDirectedOptions, initial?: boolean): void;
79
+ private applyClusterByOption;
80
+ private coarsenGraph;
64
81
  /**
65
82
  * Sets graph data to be used
66
83
  * @param {{ nodes: Node[]; rels: Relationship[] }} data the graph object
@@ -69,7 +86,18 @@ export declare class PhysLayout {
69
86
  setData(data: {
70
87
  nodes: Node[];
71
88
  rels: Relationship[];
72
- }): import("./solarmerger/types").CoarsenedGraph;
89
+ }): CoarsenedGraph;
90
+ /**
91
+ * Synchronously runs physics simulation for the configured time budget before
92
+ * the layout is first rendered.
93
+ * Sets `getComputing()` to `true` while running and clears
94
+ * it when precompute finishes (including early exit when the layout halts).
95
+ */
96
+ private runPrecompute;
97
+ private isPrecomputeEnabled;
98
+ private hasPendingPrecompute;
99
+ private hasInteractingNodes;
100
+ private shouldRunPrecompute;
73
101
  /**
74
102
  * Updates the node's positions for the next step in the physics layout iterations if the layout needs to update
75
103
  * @param refreshPositions whether the entire layout should be reheated
@@ -80,15 +108,58 @@ export declare class PhysLayout {
80
108
  * Stops the layout
81
109
  */
82
110
  terminateUpdate(): void;
111
+ /**
112
+ * Synchronously reads the latest node positions/velocities from the GPU into
113
+ * this.physPositions.
114
+ */
115
+ private readbackPositions;
116
+ /**
117
+ * Drives the convergence-readback state machine for the current iteration:
118
+ * consumes a pending fence if one is ready, otherwise starts a new readback
119
+ * when a convergence check is due.
120
+ * @returns Whether fresh velocity data is available.
121
+ */
122
+ private tryConsumeConvergenceReadback;
123
+ /**
124
+ * The GPU texture holding the latest node positions (xy) and velocities (zw).
125
+ * @returns The RGBA32F position texture.
126
+ *
127
+ * @note Texel layout matches the WebGL renderer's `u_positions` sampler, so it
128
+ * can be bound directly to webgl renderer.
129
+ */
130
+ getPositionTexture(): WebGLTexture;
131
+ /**
132
+ * Maps node id to its texel index in the position texture.
133
+ * @returns The node-id-to-texel-index map.
134
+ *
135
+ * @note This is the coarsening / solar-hierarchy order (re-derived on add/remove), which
136
+ * differs from the renderer's insertion order. Consumers rendering from {@link getPositionTexture}
137
+ * must build their sampling indices from this.
138
+ */
139
+ getNodeTexelIndices(): Readonly<Record<Node['id'], number>>;
140
+ private shouldCheckConvergence;
141
+ /** Allocates the pixel-pack buffer used for the async convergence readback. */
142
+ private setupConvergencePbo;
143
+ /**
144
+ * Kicks off an async copy of the position texture into the pixel-pack buffer
145
+ * and fences it. Only issued when no readback is already in flight.
146
+ */
147
+ private issueConvergenceReadback;
148
+ /**
149
+ * Polls the in-flight fence without blocking; if the readback is ready,
150
+ * copies it into physPositions and clears the fence.
151
+ * @returns Whether fresh velocity data was consumed this call.
152
+ */
153
+ private consumeConvergenceReadback;
154
+ /** Drops any in-flight readback fence (on stop, reset, or destroy). */
155
+ private cancelConvergenceReadback;
83
156
  /**
84
157
  * Returns whether the current layout needs to be updated in the next iteration step.
85
158
  * @returns Whether the layout should update in the following step.
86
159
  */
87
160
  getShouldUpdate(): boolean;
88
161
  /**
89
- * Returns whether the current layout is computing.
90
- * Always returns false because the force-directed layout has no computing step
91
- * @returns Whether the layout is computing.
162
+ * @returns Whether the layout is running the precompute step.
92
163
  */
93
164
  getComputing(): boolean;
94
165
  /**
@@ -109,7 +180,7 @@ export declare class PhysLayout {
109
180
  * Updates the existing nodes of a graph with a given list of updates
110
181
  * @param nodeList - The list of updates.
111
182
  */
112
- updateNodes(nodeList: Node[]): void;
183
+ updateNodes(nodeList: PartialNode[]): void;
113
184
  /**
114
185
  * Adds and removes nodes from the graph based on given updates
115
186
  * @param fullData - The entire graph object.
@@ -128,7 +199,7 @@ export declare class PhysLayout {
128
199
  }, relationshipChanges: {
129
200
  adds: Record<string, Relationship>;
130
201
  removes: Record<string, Relationship>;
131
- }): import("./solarmerger/types").CoarsenedGraph;
202
+ }): CoarsenedGraph;
132
203
  /**
133
204
  * Destroys the class and all buffers and textures.
134
205
  */
@@ -148,7 +219,6 @@ export declare class PhysLayout {
148
219
  private checkForUpdates;
149
220
  private getNodePosition;
150
221
  private getMaxSpeedSquared;
151
- private getMedianSpeedSquared;
152
222
  private getPhysData;
153
223
  private newTexture;
154
224
  private newFramebuffer;
@@ -0,0 +1,60 @@
1
+ import type { Node, Relationship } from '../../../../types/graph-element';
2
+ import type { CoarsenedGraph } from './types';
3
+ type ClusterByFn = (nodeId: string) => string | number | undefined;
4
+ /**
5
+ * Scales down spring forces for edges that cross a clusterBy cluster boundary, so cluster
6
+ * separation isn't fought by relationships that happen to link different clusters. Edges within
7
+ * the same cluster are unaffected (always full strength) — relationships still get to shape how a
8
+ * cluster's own members arrange themselves. Non-clusterBy (SolarMerger) layouts always get full
9
+ * spring strength on every edge.
10
+ */
11
+ export declare const CLUSTER_BY_RELATIONSHIP_FORCE_MULTIPLIER = 0.005;
12
+ /**
13
+ * Builds a two-level Sun/Planet hierarchy from semantic cluster identifiers.
14
+ * Nodes sharing the same cluster id become planets orbiting a cluster sun; nodes
15
+ * without a cluster id remain independent suns.
16
+ */
17
+ export default class ClusterByMerger {
18
+ subGraphs: CoarsenedGraph[];
19
+ sunMap: Record<string, string>;
20
+ relIdMap: string[][];
21
+ clusterAssignments: Map<string, string | number | undefined>;
22
+ private nodeSortMap;
23
+ /**
24
+ * Coarsens the input graph into a property-based solar hierarchy.
25
+ * @param inputGraph - The nodes and relationships to coarsen.
26
+ * @param nodeIdToIndex - Maps node ids to contiguous array indices.
27
+ * @param clusterBy - Returns a cluster id for each node, or undefined for independent nodes.
28
+ */
29
+ constructor(inputGraph: {
30
+ nodes: Node[];
31
+ rels: Relationship[];
32
+ }, nodeIdToIndex: Record<string, number>, clusterBy: ClusterByFn);
33
+ /**
34
+ * Clones the finest adjacency and adds an undirected "star" edge from each cluster member to its
35
+ * Sun, skipping any pair already connected by a real edge.
36
+ * Layout-only: fed to the spring solver, never written back into the real
37
+ * adjacency and never rendered.
38
+ * @param suns - The cluster suns, each with its planets, post-sort.
39
+ * @param finestRelationships - The finest-level real adjacency (sorted index space).
40
+ * @returns A cloned adjacency augmented with intra-cluster star edges.
41
+ */
42
+ private buildClusterSpringRelationships;
43
+ /**
44
+ * Groups node indices into buckets by cluster id, preserving first-seen order.
45
+ * Nodes without a cluster id each get their own single-node bucket.
46
+ * @param nodes - Input graph nodes indexed by position.
47
+ * @param clusterBy - Returns a cluster id for each node, or undefined for independent nodes.
48
+ * @returns Arrays of node indices, one bucket per cluster or independent node.
49
+ */
50
+ private groupNodesByCluster;
51
+ /**
52
+ * Converts cluster buckets into a Sun/Planet hierarchy for the first coarsening level.
53
+ * The first node in each bucket becomes the sun; remaining nodes become its planets.
54
+ * @param buckets - Node index groups produced by {@link groupNodesByCluster}.
55
+ * @param nodes - Input graph nodes indexed by position.
56
+ * @returns Cluster suns and a map from each node index to its sun index.
57
+ */
58
+ private buildPropertyHierarchy;
59
+ }
60
+ export {};
@@ -0,0 +1,55 @@
1
+ import type { Node, Relationship } from '../../../../types/graph-element';
2
+ import type { CoarsenedGraph, SolarRelationship } from './types';
3
+ /**
4
+ * Coarsens a graph into a hierarchy of "solar systems", following the sun/planet/moon partitioning scheme from
5
+ * Hachul and Jünger, "Drawing Large Graphs with a Potential-Field-Based Multilevel Algorithm".
6
+ *
7
+ * @see {@link https://kups.ub.uni-koeln.de/1409/ Drawing Large Graphs with a Potential-Field-Based Multilevel Algorithm}
8
+ */
9
+ export default class SolarMerger {
10
+ graph: {
11
+ nodes: Node[];
12
+ relationships: number[][];
13
+ idToRel: Record<string, SolarRelationship>;
14
+ };
15
+ subGraphs: CoarsenedGraph[];
16
+ sunMap: Record<string, string>;
17
+ relIdMap: string[][];
18
+ nodeSortMap?: Record<number, number>;
19
+ constructor(inputGraph: {
20
+ nodes: Node[];
21
+ rels: Relationship[];
22
+ }, nodeIdToIndex: Record<string, number>);
23
+ /**
24
+ * Repeatedly coarsens the graph, one solar-system level at a time, until it has at most `targetSize` nodes or
25
+ * cannot be coarsened further. Each level's result is recorded in {@link SolarMerger.subGraphs}.
26
+ *
27
+ * @param targetSize - The node count to coarsen down to.
28
+ * @returns The coarsest graph reached.
29
+ */
30
+ coarsenTo(targetSize: number): CoarsenedGraph;
31
+ /**
32
+ * Repeatedly coarsens the graph by exactly `levelToCoarsenBy` solar-system levels, regardless of the resulting
33
+ * node count. Each level's result is recorded in {@link SolarMerger.subGraphs}.
34
+ *
35
+ * @param levelToCoarsenBy - The number of levels to coarsen by.
36
+ * @returns The coarsest graph reached.
37
+ */
38
+ coarsenBy(levelToCoarsenBy: number): CoarsenedGraph;
39
+ /**
40
+ * Partitions `graph` into one level of solar systems and collapses each into a single node, producing the next
41
+ * coarser graph.
42
+ *
43
+ * Follows the three-step solar-system construction from Hachul and Jünger, "Drawing Large Graphs with a
44
+ * Potential-Field-Based Multilevel Algorithm" (create suns, label their neighbors as planets, assign
45
+ * remaining nodes as moons of their nearest planet).
46
+ *
47
+ * @param graph - The graph to coarsen by one level.
48
+ * @param firstIteration - Whether `graph` is the original, uncoarsened input graph.
49
+ * @returns The collapsed graph (`output`), the input graph with nodes reordered by solar system (`sortedInput`),
50
+ * and a map from the input graph's node ids to their position in `sortedInput` (`nodeSortMap`).
51
+ * @see {@link https://kups.ub.uni-koeln.de/1409/ Drawing Large Graphs with a Potential-Field-Based Multilevel Algorithm}
52
+ *
53
+ */
54
+ private coarsen;
55
+ }
@@ -0,0 +1,112 @@
1
+ import type { Node, Relationship } from '../../../../types/graph-element';
2
+ import type { CoarsenedGraph, Planet, SolarNode, SolarRelationship, Sun } from './types';
3
+ /**
4
+ * How many times a node's degree must exceed the graph's average degree before it is treated as a hub, see
5
+ * {@link isHub}.
6
+ *
7
+ * @remarks
8
+ * Mi et al., "Interactive Graph Layout of a Million Nodes", Sec. 4.3, proposes capping cluster *size* to avoid
9
+ * the outlier clusters this multiplier targets. `HubDegreeMultiplier` targets this problem by checking a node's
10
+ * own degree at claim time, before moon-matching runs.
11
+ * @see {@link https://www.mdpi.com/2227-9709/3/4/23 Interactive Graph Layout of a Million Nodes} Sec. 4.3
12
+ */
13
+ export declare const HubDegreeMultiplier = 3;
14
+ /**
15
+ * @param relationships - The graph's adjacency list, indexed by node id.
16
+ * @returns The degree a node must exceed to be treated as a hub by {@link isHub}.
17
+ */
18
+ export declare const getHubDegreeThreshold: (relationships: number[][]) => number;
19
+ /**
20
+ * Whether a node's degree is enough of an outlier that it must become its own sun rather than being absorbed as
21
+ * a planet/moon of another sun.
22
+ *
23
+ * @param index - The node id to check.
24
+ * @param relationships - The graph's adjacency list, indexed by node id.
25
+ * @param hubDegreeThreshold - The threshold from {@link getHubDegreeThreshold}.
26
+ * @see {@link HubDegreeMultiplier}
27
+ */
28
+ export declare const isHub: (index: number, relationships: number[][], hubDegreeThreshold: number) => boolean;
29
+ /**
30
+ * Builds adjacency lists and relationship id maps from the input graph.
31
+ * @param inputGraph - The nodes and relationships to convert.
32
+ * @param nodeIdToIndex - Maps node ids to contiguous array indices.
33
+ * @returns Parsed adjacency data for coarsening.
34
+ */
35
+ export declare const buildAdjacencyGraph: (inputGraph: {
36
+ nodes: Node[];
37
+ rels: Relationship[];
38
+ }, nodeIdToIndex: Record<string, number>) => {
39
+ nodes: Node[];
40
+ relationships: number[][];
41
+ relIdMap: string[][];
42
+ idToRel: Record<string, SolarRelationship>;
43
+ };
44
+ /**
45
+ * Wraps input nodes as {@link SolarNode}s indexed by their original position.
46
+ * @param nodes - The input graph nodes.
47
+ * @returns {SolarNode[]} Solar nodes with positional ids and original ids preserved.
48
+ */
49
+ export declare const createSolarNodesFromInput: (nodes: Node[]) => SolarNode[];
50
+ /**
51
+ * Creates a Planet with an unsettled parent link (caller must attach via {@link attachPlanetsToSun}).
52
+ */
53
+ export declare const createPlanet: (params: {
54
+ id: number;
55
+ sunId: number;
56
+ weight: number;
57
+ originalId: string;
58
+ finestIndex?: number;
59
+ size?: () => number;
60
+ }) => Planet;
61
+ /**
62
+ * Finalises a sun's planets list, weight/size/children closures, and back-links parent on each planet.
63
+ * @param sun - The sun to attach planets to.
64
+ * @param planets - The planets to attach to the sun.
65
+ * @param centreWeight - The weight of the sun itself, added to the sum of its planets' weights.
66
+ * @param centreSize - The size of the sun itself, added to the sum of its planets' sizes.
67
+ * @returns The sun with planets attached and weight/size/children closures set.
68
+ */
69
+ export declare const attachPlanetsToSun: (sun: Sun, planets: Planet[], centreWeight: number, centreSize: number) => Sun;
70
+ /**
71
+ * Rolls node-level adjacency up to sun-level adjacency between clusters.
72
+ * When `weakLinkSunIds` is provided, edges where either endpoint is a Sun are skipped.
73
+ * @param relationships - Node adjacency lists indexed by node position.
74
+ * @param nodeIndexToSunIndex - Maps each node index to its sun index.
75
+ * @param sunCount - Number of suns in the coarse graph.
76
+ * @param weakLinkSunIds - Optional set of node indices that are Suns. When set, Sun-leaf and Sun-Sun edges are ignored.
77
+ * @returns {number[][]} Sun-level adjacency lists without duplicate edges.
78
+ */
79
+ export declare const rollupInterSunRelationships: (relationships: number[][], nodeIndexToSunIndex: Record<number, number>, sunCount: number, weakLinkSunIds?: Set<number>) => number[][];
80
+ /**
81
+ * Remaps adjacency lists to a sorted node order.
82
+ * @param relationships - Node adjacency lists in the pre-sort index order.
83
+ * @param nodeSortMap - Maps old node indices to sorted indices.
84
+ * @param relIdMap - Relationship ids aligned with adjacency lists.
85
+ * @param remapRelIds - Whether relationship ids should be remapped as well.
86
+ * @returns {{ relationships: number[][]; relIdMap: string[][] }} Remapped adjacency data.
87
+ */
88
+ export declare const remapAdjacencyWithSortMap: (relationships: number[][], nodeSortMap: Record<number, number>, relIdMap: string[][], remapRelIds?: boolean) => {
89
+ relationships: number[][];
90
+ relIdMap: string[][];
91
+ };
92
+ export type SortHierarchyOptions = {
93
+ suns: Sun[];
94
+ solarNodes: SolarNode[];
95
+ sourceNodes?: (Node | SolarNode)[];
96
+ relationships: number[][];
97
+ relIdMap: string[][];
98
+ idToRel: Record<string, SolarRelationship>;
99
+ firstIteration?: boolean;
100
+ includeMoons?: boolean;
101
+ };
102
+ /**
103
+ * Sorts suns, planets, and optionally moons into shader layout order and builds the finest subgraph.
104
+ * @param options - Hierarchy members and adjacency data to sort.
105
+ * @returns The finest subgraph, sort map, sun membership map, and remapped relationship ids.
106
+ */
107
+ export declare const sortHierarchyForShaderLayout: ({ suns, solarNodes, sourceNodes, relationships, relIdMap, idToRel, firstIteration, includeMoons }: SortHierarchyOptions) => {
108
+ sortedInput: CoarsenedGraph;
109
+ nodeSortMap: Record<number, number>;
110
+ sunMap: Record<string, string>;
111
+ relIdMap: string[][];
112
+ };
@@ -23,6 +23,11 @@ export interface CoarsenedGraph {
23
23
  nodes: SolarNode[];
24
24
  relationships: number[][];
25
25
  idToRel: Record<string, SolarRelationship>;
26
+ /**
27
+ * Contains the "real" {@link relationships} plus any synthetic edges purely for layout purposes.
28
+ * Undefined when there are no synthetic edges (e.g. SolarMerger).
29
+ */
30
+ springRelationships?: number[][];
26
31
  }
27
32
  export interface Sun extends SolarNode {
28
33
  planets: Planet[];
@@ -1,2 +1,2 @@
1
- declare const _default: "#version 300 es\nprecision mediump float;\n\nuniform sampler2D u_physData;\nuniform sampler2D u_connections;\nuniform sampler2D u_connectionOffsets;\nuniform sampler2D u_pinnedNodes;\nuniform sampler2D u_sizeTexture;\nuniform float u_baseLength;\nuniform float u_curIteration;\nuniform float u_iterationMultiplier;\nuniform vec2 u_gravityCenter;\nuniform float u_numNodes;\nuniform float u_gravity;\n\nuniform sampler2D u_clusterData;\nuniform sampler2D u_prevForce;\nuniform float u_collisionMultiplier;\n\nout vec4 fragColor;\n\nfloat DAMP = 0.6;\nfloat COOL = 0.99998;\nfloat TIMESTEP = 1.0 / 30.0;\nfloat VELOCITYDECAY = 0.6;\nfloat accLimitLow = 500000.0;\nfloat accLimitHigh = 750000.0;\nfloat accLimitPosHigh = 10000000.0;\n\nconst float denseNodeThreshold = 1000.0;\nconst float MIN_DISTANCE = 0.00000001;\nconst float MAX_DISTANCE = 1000000000.0;\nconst float MAX_ACCELERATION = 50000.0;\n\nvec4 getTextureData(sampler2D tex, float index, float base) {\n float x = mod(index, base);\n float y = (index - x) / base;\n return texture(tex, vec2(x + 0.5, y + 0.5) / base);\n}\n\nbool isNan(float val) {\n return (val < 0.0 || 0.0 < val || val == 0.0) ? false : true;\n}\n\nconst float BIG_NUMBER = 999999999999999999.0;\nbool isInf(float val) {\n return val > BIG_NUMBER || val < -BIG_NUMBER;\n}\n\nvec2 getDelta(vec2 v1, vec2 v2) {\n vec2 delta = v1 - v2;\n float dist = length(delta);\n if (dist < MIN_DISTANCE || isNan(dist)) {\n return vec2(MIN_DISTANCE, MIN_DISTANCE);\n }\n if (dist > MAX_DISTANCE) {\n vec2 normDelta = delta / dist;\n return normDelta * MAX_DISTANCE;\n }\n return delta;\n}\n\nvec4 getOtherNodePosition(float i) {\n return getTextureData(u_physData, i, 256.0);\n}\n\nfloat getCombinedNodeSize(float i, float nodeSize) {\n float otherNodeSize = getTextureData(u_sizeTexture, i, 256.0).r;\n return nodeSize + otherNodeSize;\n}\n\nvec2 getSpringForce(float curConnection, float numConnections, float springFScale, vec4 myPosition, float i) {\n float curSpring = getTextureData(u_connections, curConnection + i, 4096.0).r;\n\n vec4 otherPosition = getTextureData(u_physData, curSpring, 256.0);\n float otherDataPosition = getTextureData(u_connectionOffsets, curSpring, 256.0).r;\n float otherNumConnections = getTextureData(u_connections, otherDataPosition, 4096.0).r;\n float avgDegree = max((otherNumConnections + numConnections * 3.0) / 4.0, 4.0);\n\n vec2 delta = getDelta(myPosition.xy, otherPosition.xy);\n float dist = length(delta);\n\n float F = (dist * dist * springFScale) / (u_baseLength * avgDegree);\n if (u_collisionMultiplier > 0.0) {\n F *= u_collisionMultiplier;\n }\n return (-delta / dist) * F;\n}\n\nvec2 getCollisionForce(float combinedNodeSize, float dist, float fScale, vec2 delta) {\n float collisionForce = (combinedNodeSize - dist) / (dist);\n if (u_collisionMultiplier > 0.0) {\n collisionForce *= u_collisionMultiplier;\n }\n return (delta * collisionForce * fScale * combinedNodeSize) / (combinedNodeSize + 1.0);\n}\n\nvec2 getRepulsionForce(float dist, float fScale, vec2 delta) {\n float F = (u_baseLength * u_baseLength * fScale) / (dist);\n return (delta / dist) * F * 0.05;\n}\n\nvoid main(void) {\n float textureSide = 256.0; //#TEXTURE_SIDE#;\n float index = (gl_FragCoord.x - 0.5) + (gl_FragCoord.y - 0.5) * textureSide;\n\n if (index >= u_numNodes) {\n discard;\n }\n\n vec4 clusterData = getTextureData(u_clusterData, index, 256.0);\n\n float clusterIndex = clusterData.x;\n float clusterStartIndex = clusterData.y;\n float clusterSize = clusterData.z;\n float clusterWeight = clusterData.w;\n\n vec4 myPosition = getTextureData(u_physData, index, 256.0);\n vec4 previousForce = getTextureData(u_prevForce, clusterIndex, 256.0);\n\n vec2 acceleration = previousForce.xy;\n\n float isPinned = getTextureData(u_pinnedNodes, index, 256.0).r;\n float nodeSize = getTextureData(u_sizeTexture, index, 256.0).r;\n\n if (isPinned > 0.5) {\n fragColor = vec4(myPosition.xy, 0.0, 0.0);\n return;\n }\n\n float curConnection = getTextureData(u_connectionOffsets, index, 256.0).r;\n float numConnections = getTextureData(u_connections, curConnection, 4096.0).r;\n\n float fScale = 1.0 + sqrt(u_iterationMultiplier);\n float springFScale = fScale;\n\n if (numConnections > denseNodeThreshold) {\n springFScale = sqrt(fScale);\n }\n\n float numOfRels = 0.0;\n\n // Springs\n for (float i = 1.0; i <= 256.0 * 256.0; i++) {\n if (numOfRels >= numConnections) {\n break;\n }\n acceleration += getSpringForce(curConnection, numConnections, springFScale, myPosition, i);\n numOfRels += 1.0;\n }\n\n // Repulsion && collision detection\n if (u_collisionMultiplier > 0.0) {\n float number_of_collisions = 0.0;\n for (float i = 0.0; i < 256.0 * 256.0; i++) {\n if (i >= u_numNodes) {\n break;\n }\n\n if (i == index) {\n continue;\n }\n\n vec4 otherPosition = getOtherNodePosition(i);\n vec2 delta = getDelta(myPosition.xy, otherPosition.xy);\n float dist = length(delta);\n float combinedNodeSize = getCombinedNodeSize(i, nodeSize) * 2.0;\n\n if (dist < combinedNodeSize && number_of_collisions < 40.0) {\n number_of_collisions++;\n acceleration += getCollisionForce(combinedNodeSize, dist, fScale, delta);\n }\n\n if (i >= clusterStartIndex && i < clusterStartIndex + clusterSize) {\n acceleration += getRepulsionForce(dist, fScale, delta);\n }\n }\n } else {\n for (float i = 0.0; i < 256.0 * 256.0; i++) {\n if (i >= clusterStartIndex + clusterSize || i >= u_numNodes) {\n break;\n }\n\n if (i < clusterStartIndex || i == index) {\n continue;\n }\n\n vec4 otherPosition = getOtherNodePosition(i);\n vec2 delta = getDelta(myPosition.xy, otherPosition.xy);\n float dist = length(delta);\n float combinedNodeSize = getCombinedNodeSize(i, nodeSize);\n\n if (dist < combinedNodeSize) {\n acceleration += getCollisionForce(combinedNodeSize, dist, fScale, delta);\n }\n\n acceleration += getRepulsionForce(dist, fScale, delta);\n }\n }\n\n // Gravity\n vec2 delta = getDelta(u_gravityCenter, myPosition.xy);\n float dist = length(delta);\n\n vec2 grav = (delta / dist) * u_gravity * fScale * (dist / 1000.0);\n acceleration += grav;\n\n float accMagnitude = length(acceleration);\n acceleration *= min(MAX_ACCELERATION, accMagnitude) / accMagnitude;\n\n float iterationFrictionThreshold = 1000.0;\n\n if (u_curIteration > iterationFrictionThreshold) {\n float friction = 1.0 + pow((u_curIteration - iterationFrictionThreshold), 2.0) / 100.0;\n acceleration *= 1.0 / friction;\n }\n\n if (u_curIteration == 0.0) {\n fragColor = vec4(myPosition.xy, myPosition.zw);\n } else {\n float TEMP = max(0.02, pow(COOL, u_curIteration));\n vec2 prevVelocity = myPosition.zw;\n vec2 currentPos = myPosition.xy;\n\n vec2 newPos = currentPos + TEMP * ( DAMP * (prevVelocity) + acceleration * TIMESTEP * TIMESTEP);\n vec2 newVelocity = newPos - currentPos;\n fragColor = vec4(newPos, newVelocity);\n }\n}";
1
+ declare const _default: "#version 300 es\nprecision mediump float;\n\nuniform sampler2D u_physData;\nuniform sampler2D u_connections;\nuniform sampler2D u_connectionOffsets;\nuniform sampler2D u_pinnedNodes;\nuniform sampler2D u_sizeTexture;\nuniform float u_baseLength;\nuniform float u_curIteration;\nuniform float u_iterationMultiplier;\nuniform vec2 u_gravityCenter;\nuniform float u_numNodes;\nuniform float u_gravity;\n\nuniform sampler2D u_clusterData;\nuniform sampler2D u_prevForce;\nuniform float u_collisionMultiplier;\nuniform float u_maxNodeSize;\nuniform float u_springForceMultiplier;\n\nout vec4 fragColor;\n\nfloat DAMP = 0.6;\nfloat COOL = 0.99998;\nfloat TIMESTEP = 1.0 / 30.0;\nfloat VELOCITYDECAY = 0.6;\nfloat accLimitLow = 500000.0;\nfloat accLimitHigh = 750000.0;\nfloat accLimitPosHigh = 10000000.0;\n\nconst float denseNodeThreshold = 1000.0;\nconst float MIN_DISTANCE = 0.00000001;\nconst float MAX_DISTANCE = 1000000000.0;\nconst float MAX_ACCELERATION = 50000.0;\n\nvec4 getTextureData(sampler2D tex, float index, float base) {\n float x = mod(index, base);\n float y = (index - x) / base;\n return texture(tex, vec2(x + 0.5, y + 0.5) / base);\n}\n\nbool isNan(float val) {\n return (val < 0.0 || 0.0 < val || val == 0.0) ? false : true;\n}\n\nconst float BIG_NUMBER = 999999999999999999.0;\nbool isInf(float val) {\n return val > BIG_NUMBER || val < -BIG_NUMBER;\n}\n\nvec2 getDelta(vec2 v1, vec2 v2) {\n vec2 delta = v1 - v2;\n float dist = length(delta);\n if (dist < MIN_DISTANCE || isNan(dist)) {\n return vec2(MIN_DISTANCE, MIN_DISTANCE);\n }\n if (dist > MAX_DISTANCE) {\n vec2 normDelta = delta / dist;\n return normDelta * MAX_DISTANCE;\n }\n return delta;\n}\n\nvec4 getOtherNodePosition(float i) {\n return getTextureData(u_physData, i, 256.0);\n}\n\nfloat getCombinedNodeSize(float i, float nodeSize) {\n float otherNodeSize = getTextureData(u_sizeTexture, i, 256.0).r;\n return nodeSize + otherNodeSize;\n}\n\nvec2 getSpringForce(float curConnection, float numConnections, float springFScale, vec4 myPosition, float i, float myClusterStartIndex) {\n float curSpring = getTextureData(u_connections, curConnection + i, 4096.0).r;\n\n vec4 otherPosition = getTextureData(u_physData, curSpring, 256.0);\n float otherDataPosition = getTextureData(u_connectionOffsets, curSpring, 256.0).r;\n float otherNumConnections = getTextureData(u_connections, otherDataPosition, 4096.0).r;\n float avgDegree = max((otherNumConnections + numConnections * 3.0) / 4.0, 4.0);\n\n vec2 delta = getDelta(myPosition.xy, otherPosition.xy);\n float dist = length(delta);\n\n float F = (dist * dist * springFScale) / (u_baseLength * avgDegree);\n if (u_collisionMultiplier > 0.0) {\n F *= u_collisionMultiplier;\n }\n\n // u_springForceMultiplier only damps edges that cross a cluster boundary\n if (u_springForceMultiplier < 1.0) {\n float otherClusterStartIndex = getTextureData(u_clusterData, curSpring, 256.0).y;\n F *= otherClusterStartIndex == myClusterStartIndex ? 1.0 : u_springForceMultiplier;\n }\n\n return (-delta / dist) * F;\n}\n\nvec2 getCollisionForce(float combinedNodeSize, float dist, float fScale, vec2 delta) {\n float collisionForce = (combinedNodeSize - dist) / (dist);\n if (u_collisionMultiplier > 0.0) {\n collisionForce *= u_collisionMultiplier;\n }\n return (delta * collisionForce * fScale * combinedNodeSize) / (combinedNodeSize + 1.0);\n}\n\nvec2 getRepulsionForce(float dist, float fScale, vec2 delta) {\n float F = (u_baseLength * u_baseLength * fScale) / (dist);\n return (delta / dist) * F * 0.05;\n}\n\nvoid main(void) {\n float textureSide = 256.0; //#TEXTURE_SIDE#;\n float index = (gl_FragCoord.x - 0.5) + (gl_FragCoord.y - 0.5) * textureSide;\n\n if (index >= u_numNodes) {\n discard;\n }\n\n vec4 clusterData = getTextureData(u_clusterData, index, 256.0);\n\n float clusterIndex = clusterData.x;\n float clusterStartIndex = clusterData.y;\n float clusterSize = clusterData.z;\n\n vec4 myPosition = getTextureData(u_physData, index, 256.0);\n vec4 previousForce = getTextureData(u_prevForce, clusterIndex, 256.0);\n\n vec2 acceleration = previousForce.xy;\n\n float isPinned = getTextureData(u_pinnedNodes, index, 256.0).r;\n float nodeSize = getTextureData(u_sizeTexture, index, 256.0).r;\n\n if (isPinned > 0.5) {\n fragColor = vec4(myPosition.xy, 0.0, 0.0);\n return;\n }\n\n float curConnection = getTextureData(u_connectionOffsets, index, 256.0).r;\n float numConnections = getTextureData(u_connections, curConnection, 4096.0).r;\n\n float fScale = 1.0 + sqrt(u_iterationMultiplier);\n float springFScale = fScale;\n\n if (numConnections > denseNodeThreshold) {\n springFScale = sqrt(fScale);\n }\n\n float numOfRels = 0.0;\n\n // Springs\n for (float i = 1.0; i <= 256.0 * 256.0; i++) {\n if (numOfRels >= numConnections) {\n break;\n }\n acceleration += getSpringForce(curConnection, numConnections, springFScale, myPosition, i, clusterStartIndex);\n numOfRels += 1.0;\n }\n\n // Repulsion && collision detection\n if (u_collisionMultiplier > 0.0) {\n float number_of_collisions = 0.0;\n for (float i = 0.0; i < 256.0 * 256.0; i++) {\n if (i >= u_numNodes) {\n break;\n }\n\n if (i == index) {\n continue;\n }\n\n vec4 otherPosition = getOtherNodePosition(i);\n vec2 delta = getDelta(myPosition.xy, otherPosition.xy);\n float dist = length(delta);\n\n if (number_of_collisions < 40.0 && dist < (nodeSize + u_maxNodeSize) * 2.0) {\n float combinedNodeSize = getCombinedNodeSize(i, nodeSize) * 2.0;\n if (dist < combinedNodeSize) {\n number_of_collisions++;\n acceleration += getCollisionForce(combinedNodeSize, dist, fScale, delta);\n }\n }\n\n if (i >= clusterStartIndex && i < clusterStartIndex + clusterSize) {\n acceleration += getRepulsionForce(dist, fScale, delta);\n }\n }\n } else {\n for (float i = 0.0; i < 256.0 * 256.0; i++) {\n if (i >= clusterStartIndex + clusterSize || i >= u_numNodes) {\n break;\n }\n\n if (i < clusterStartIndex || i == index) {\n continue;\n }\n\n vec4 otherPosition = getOtherNodePosition(i);\n vec2 delta = getDelta(myPosition.xy, otherPosition.xy);\n float dist = length(delta);\n float combinedNodeSize = getCombinedNodeSize(i, nodeSize);\n\n if (dist < combinedNodeSize) {\n acceleration += getCollisionForce(combinedNodeSize, dist, fScale, delta);\n }\n\n acceleration += getRepulsionForce(dist, fScale, delta);\n }\n }\n\n // Gravity\n vec2 delta = getDelta(u_gravityCenter, myPosition.xy);\n float dist = length(delta);\n\n vec2 grav = (delta / dist) * u_gravity * fScale * (dist / 1000.0);\n acceleration += grav;\n\n float accMagnitude = length(acceleration);\n acceleration *= min(MAX_ACCELERATION, accMagnitude) / accMagnitude;\n\n float iterationFrictionThreshold = 1000.0;\n\n if (u_curIteration > iterationFrictionThreshold) {\n float friction = 1.0 + pow((u_curIteration - iterationFrictionThreshold), 2.0) / 100.0;\n acceleration *= 1.0 / friction;\n }\n\n if (u_curIteration == 0.0) {\n fragColor = vec4(myPosition.xy, myPosition.zw);\n } else {\n float TEMP = max(0.02, pow(COOL, u_curIteration));\n vec2 prevVelocity = myPosition.zw;\n vec2 currentPos = myPosition.xy;\n\n vec2 newPos = currentPos + TEMP * ( DAMP * (prevVelocity) + acceleration * TIMESTEP * TIMESTEP);\n vec2 newVelocity = newPos - currentPos;\n fragColor = vec4(newPos, newVelocity);\n }\n}";
2
2
  export default _default;
@@ -1,2 +1,2 @@
1
- declare const _default: "#version 300 es\nprecision mediump float;\nuniform sampler2D u_physData;\nuniform sampler2D u_clusterData;\nuniform sampler2D u_finestIndexes;\nuniform sampler2D u_prevForce;\n\nuniform float u_baseLength;\nuniform float u_numNodes;\nuniform float u_iterationMultiplier;\nuniform float u_isTopLevel;\n\nout vec4 fragColor;\n\nfloat PI = 3.1415926535897932384626433832795;\nfloat TIMESTEP = 1.0 / 30.0;\nfloat VELOCITYDECAY = 1.0;\n\nvec4 getTextureData(sampler2D tex, float index, float base) {\n float x = mod(index, base);\n float y = (index - x) / base;\n return texture(tex, vec2(x + 0.5, y + 0.5) / base);\n}\n\nfloat getSquaredLogClusterWeight(float value) {\n return pow(log(value), 2.0);\n}\n\nvoid main(void) {\n float index = (gl_FragCoord.x - 0.5) + (gl_FragCoord.y - 0.5) * 256.0;\n\n if (index >= u_numNodes) {\n discard;\n }\n\n vec4 clusterData = getTextureData(u_clusterData, index, 256.0);\n\n float clusterIndex = clusterData.x;\n float clusterStartIndex = clusterData.y;\n float clusterSize = clusterData.z;\n float clusterArea = pow(clusterSize + u_baseLength * 2.0, 2.0);\n\n float finestIndex = getTextureData(u_finestIndexes, index, 256.0).r;\n vec4 myPosition = getTextureData(u_physData, finestIndex, 256.0);\n vec4 previousForce = getTextureData(u_prevForce, clusterIndex, 256.0);\n float fScale = 1.0 + sqrt(u_iterationMultiplier);\n\n vec2 acceleration = previousForce.xy;\n\n // Repulsion & Collision Detection\n for (float i = 0.0; i < 256.0 * 256.0; i++) {\n if (i >= clusterStartIndex + clusterSize || i >= u_numNodes) {\n break;\n }\n if (i < clusterStartIndex || i == index) {\n continue;\n }\n\n vec4 otherClusterData = getTextureData(u_clusterData, i, 256.0);\n float otherClusterWeight = getSquaredLogClusterWeight(otherClusterData.w);\n\n float otherFinestIndex = getTextureData(u_finestIndexes, i, 256.0).r;\n vec4 otherPosition = getTextureData(u_physData, otherFinestIndex, 256.0);\n\n vec2 delta = myPosition.xy - otherPosition.xy;\n float dist = max(length(delta), 0.0000001);\n float maxDist = 25.0;\n float repulsionForceScale = 0.1;\n\n float F = (clusterArea * fScale) / (dist * dist);\n\n if (u_isTopLevel == 1.0) {\n repulsionForceScale = 0.2;\n }\n\n acceleration += ((delta / sqrt(dist)) * F) * repulsionForceScale * otherClusterWeight;\n\n if (dist < maxDist) {\n float collide = (maxDist - dist) / (dist * dist);\n acceleration += delta * collide * fScale * maxDist / (maxDist + 1.0);\n }\n }\n\n fragColor = vec4(acceleration, vec2(finestIndex, 0));\n}";
1
+ declare const _default: "#version 300 es\nprecision mediump float;\nuniform sampler2D u_physData;\nuniform sampler2D u_clusterData;\nuniform sampler2D u_finestIndexes;\nuniform sampler2D u_prevForce;\n\nuniform float u_baseLength;\nuniform float u_numNodes;\nuniform float u_iterationMultiplier;\nuniform float u_isTopLevel;\n\nout vec4 fragColor;\n\nfloat PI = 3.1415926535897932384626433832795;\nfloat TIMESTEP = 1.0 / 30.0;\nfloat VELOCITYDECAY = 1.0;\n\nvec4 getTextureData(sampler2D tex, float index, float base) {\n float x = mod(index, base);\n float y = (index - x) / base;\n return texture(tex, vec2(x + 0.5, y + 0.5) / base);\n}\n\nfloat getAdjustedClusterWeight(float clusterWeight) {\n return (clusterWeight * clusterWeight) / (clusterWeight + 1.0);\n}\n\nvoid main(void) {\n float index = (gl_FragCoord.x - 0.5) + (gl_FragCoord.y - 0.5) * 256.0;\n\n if (index >= u_numNodes) {\n discard;\n }\n\n vec4 clusterData = getTextureData(u_clusterData, index, 256.0);\n\n float clusterIndex = clusterData.x;\n float clusterStartIndex = clusterData.y;\n float clusterSize = clusterData.z;\n float clusterArea = pow(clusterSize + u_baseLength * 2.0, 2.0);\n\n float finestIndex = getTextureData(u_finestIndexes, index, 256.0).r;\n vec4 myPosition = getTextureData(u_physData, finestIndex, 256.0);\n vec4 previousForce = getTextureData(u_prevForce, clusterIndex, 256.0);\n float fScale = 1.0 + sqrt(u_iterationMultiplier);\n\n vec2 acceleration = previousForce.xy;\n\n // Repulsion & Collision Detection\n for (float i = 0.0; i < 256.0 * 256.0; i++) {\n if (i >= clusterStartIndex + clusterSize || i >= u_numNodes) {\n break;\n }\n if (i < clusterStartIndex || i == index) {\n continue;\n }\n\n vec4 otherClusterData = getTextureData(u_clusterData, i, 256.0);\n float otherClusterWeight = getAdjustedClusterWeight(otherClusterData.w);\n\n float otherFinestIndex = getTextureData(u_finestIndexes, i, 256.0).r;\n vec4 otherPosition = getTextureData(u_physData, otherFinestIndex, 256.0);\n\n vec2 delta = myPosition.xy - otherPosition.xy;\n float dist = max(length(delta), 0.0000001);\n float maxDist = 25.0;\n float repulsionForceScale = 0.1;\n\n float F = (clusterArea * fScale) / (dist * dist);\n\n if (u_isTopLevel == 1.0) {\n repulsionForceScale = 0.2;\n }\n\n acceleration += ((delta / sqrt(dist)) * F) * repulsionForceScale * otherClusterWeight;\n\n if (dist < maxDist) {\n float collide = (maxDist - dist) / (dist * dist);\n acceleration += delta * collide * fScale * maxDist / (maxDist + 1.0);\n }\n }\n\n fragColor = vec4(acceleration, vec2(finestIndex, 0));\n}";
2
2
  export default _default;
@@ -1,5 +1,5 @@
1
1
  import type { NvlState } from '../../modules/state/types';
2
- import type { Node } from '../../types/graph-element';
2
+ import type { Node, PartialNode } from '../../types/graph-element';
3
3
  import type { Point } from '../../utils/geometry';
4
4
  export declare class FreeLayout {
5
5
  private state;
@@ -10,9 +10,7 @@ export declare class FreeLayout {
10
10
  state: NvlState;
11
11
  });
12
12
  setOptions(): any;
13
- updateNodes(positionList: (Point & {
14
- id: string;
15
- })[]): void;
13
+ updateNodes(positionList: PartialNode[]): void;
16
14
  update(refreshPositions?: boolean): void;
17
15
  layout(nodes: Node[], nodeIds: Record<string, Node>, idToPosition: Record<string, Node>): void;
18
16
  setNodePositions(p: Record<string, Point>): void;
@@ -1,5 +1,5 @@
1
1
  import type { NvlState } from '../../modules/state/types';
2
- import type { Node } from '../../types/graph-element';
2
+ import type { Node, PartialNode } from '../../types/graph-element';
3
3
  import type { Point } from '../../utils/geometry';
4
4
  export declare class GridLayout {
5
5
  state: NvlState;
@@ -10,11 +10,7 @@ export declare class GridLayout {
10
10
  state: NvlState;
11
11
  });
12
12
  setOptions(): void;
13
- updateNodes(positionList: {
14
- id: string;
15
- x: number;
16
- y: number;
17
- }[]): void;
13
+ updateNodes(positionList: PartialNode[]): void;
18
14
  update(refreshPositions?: boolean): void;
19
15
  layout(mobxNodes: any): void;
20
16
  setNodePositions(idToPosition: Record<string, Node & Point>): void;
@@ -1,4 +1,4 @@
1
- import { type Node, type PartialNode, type PartialRelationship, type Relationship } from '../types/graph-element';
1
+ import { type Node, type NodeWithClientPosition, type PartialNode, type PartialRelationship, type Relationship } from '../types/graph-element';
2
2
  import '../types/nvl-window-functions';
3
3
  import type { Point } from '../utils/geometry';
4
4
  import type { ExternalCallbacks } from './ExternalCallbackHandler';
@@ -49,6 +49,7 @@ export default class NvlController {
49
49
  private lastTimestamp;
50
50
  private accumulator;
51
51
  private readonly layoutTimeLimit;
52
+ private readonly smoothFrameBudgetMs;
52
53
  private pixelRatio;
53
54
  private readonly removeResizeListener;
54
55
  private readonly removeMinimapResizeListener;
@@ -60,14 +61,39 @@ export default class NvlController {
60
61
  private layoutComputingCallback;
61
62
  private currentLayoutType;
62
63
  private descriptionElement;
64
+ /** True when a minimap container was provided; disables the GPU-texture fast path (separate GL context). */
65
+ private readonly hasMinimap;
63
66
  constructor(state: NvlState, frame: HTMLElement, options: NvlOptions);
64
67
  private onWebGLContextLost;
68
+ private fpsToFrameBudgetMs;
65
69
  private getLayoutNeedsToUpdate;
66
70
  private isWithinBudget;
71
+ /**
72
+ * Add elapsed frame time to the layout-step accumulator, capped at `layoutTimeLimit`.
73
+ * Prevents a slow frame (e.g. blocked GPU readback) from inflating catch-up debt and
74
+ * scheduling ever more work in a feedback loop; the layout falls behind gracefully instead.
75
+ */
76
+ private accumulateLayoutElapsedTime;
77
+ /**
78
+ * Whether the GPU force-directed layout is active.
79
+ * @returns `true` when PhysLayout is the current layout.
80
+ */
81
+ private usesGpuSimulation;
82
+ /**
83
+ * Whether the GPU-texture fast path can be taken this frame.
84
+ * Requires the WebGL renderer, GPU layout active, no minimap, and no CPU-position consumers
85
+ * (layout-step callback, HTML overlays).
86
+ * @returns `true` when rendering directly from the GPU position texture is safe.
87
+ */
88
+ private canRenderFromGpuTexture;
89
+ /** Syncs idToPosition from the GPU layout on demand; no-op for CPU layouts. */
90
+ syncPositionsFromLayout(): void;
67
91
  private updateMinimapZoom;
68
92
  private startMainLoop;
93
+ private renderNodeHtmlOverlays;
94
+ private renderRelationshipHtmlOverlays;
69
95
  updateHtmlOverlay(): void;
70
- dumpNodes(): Node[];
96
+ dumpNodes(): NodeWithClientPosition[];
71
97
  dumpRelationships(): Relationship[];
72
98
  /**
73
99
  * Map a relative Canvas space to the DOM position
@@ -120,6 +146,8 @@ export default class NvlController {
120
146
  private getDataUrlForCanvas;
121
147
  private initiateFileDownload;
122
148
  private updateLayoutAndPositions;
149
+ private getAndUpdatePositions;
150
+ private getLayoutPositions;
123
151
  saveToFile(options: SaveToFileOptions): void;
124
152
  private createSvgString;
125
153
  saveToSvg(options?: SaveToFileOptions): Promise<void>;