@neo4j-nvl/base 1.2.1 → 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 (74) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/dist/base.mjs +1 -1
  3. package/dist/types/index.d.ts +21 -16
  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 +3 -3
  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 +129 -27
  25. package/dist/types/modules/state/utils.d.ts +2 -2
  26. package/dist/types/renderers/ClusterOverlay.d.ts +73 -0
  27. package/dist/types/renderers/ClusterOverlay.test.d.ts +1 -0
  28. package/dist/types/renderers/domrenderer/shared/ImageCache.d.ts +1 -0
  29. package/dist/types/renderers/domrenderer/shared/types.d.ts +1 -2
  30. package/dist/types/renderers/webglrenderer/Controller.d.ts +27 -6
  31. package/dist/types/renderers/webglrenderer/IconLayerRegistry.d.ts +14 -0
  32. package/dist/types/renderers/webglrenderer/IconLayerRegistry.test.d.ts +1 -0
  33. package/dist/types/renderers/webglrenderer/Renderer.d.ts +43 -3
  34. package/dist/types/renderers/webglrenderer/node-animation-fragment.d.ts +1 -1
  35. package/dist/types/renderers/webglrenderer/node-animation-vertex.d.ts +1 -1
  36. package/dist/types/renderers/webglrenderer/node-fragment-point.d.ts +1 -1
  37. package/dist/types/renderers/webglrenderer/node-vertex-point.d.ts +1 -1
  38. package/dist/types/renderers/webglrenderer/rel-fragment-line.d.ts +1 -1
  39. package/dist/types/renderers/webglrenderer/rel-glsl-utils.d.ts +1 -0
  40. package/dist/types/renderers/webglrenderer/rel-vertex-arrow.d.ts +2 -0
  41. package/dist/types/renderers/webglrenderer/rel-vertex-line.d.ts +1 -1
  42. package/dist/types/renderers/webglrenderer/viewport-box-fragment.d.ts +1 -1
  43. package/dist/types/renderers/webglrenderer/viewport-box-vertex.d.ts +1 -1
  44. package/dist/types/types/graph-element.d.ts +9 -1
  45. package/dist/types/types/nvl-window-functions.d.ts +2 -2
  46. package/dist/types/utils/canvasManagement.d.ts +1 -1
  47. package/dist/types/utils/clusterGeometry.d.ts +14 -0
  48. package/dist/types/utils/clusterGeometry.test.d.ts +1 -0
  49. package/dist/types/utils/clusterHitRegions.d.ts +42 -0
  50. package/dist/types/utils/clusterHitRegions.test.d.ts +1 -0
  51. package/dist/types/utils/constants.d.ts +6 -0
  52. package/dist/types/utils/geometry.d.ts +44 -1
  53. package/dist/types/utils/hittest.d.ts +30 -4
  54. package/dist/types/utils/jsDriverResultTransformer.d.ts +11 -9
  55. package/dist/types/utils/layoutSeeding.d.ts +7 -0
  56. package/dist/types/utils/layoutSeeding.test.d.ts +1 -0
  57. package/dist/types/utils/webWorkerUtils.d.ts +1 -1
  58. package/dist/types/utils/zoomFunctions.d.ts +9 -2
  59. package/package.json +3 -3
  60. package/dist/types/layouts/forcedirectedlayout/ForceCytoLayout.d.ts +0 -22
  61. package/dist/types/layouts/forcedirectedlayout/cosebilkentlayout/CoseBilkentLayout.d.ts +0 -33
  62. package/dist/types/layouts/forcedirectedlayout/physlayout/PhysLayout.d.ts +0 -174
  63. package/dist/types/layouts/forcedirectedlayout/physlayout/shaders/multilevel-fragment.d.ts +0 -2
  64. package/dist/types/layouts/forcedirectedlayout/physlayout/shaders/multilevel-repulsive-fragment.d.ts +0 -2
  65. package/dist/types/layouts/forcedirectedlayout/physlayout/shaders/multilevel-update-fragment.d.ts +0 -2
  66. package/dist/types/layouts/forcedirectedlayout/physlayout/shaders/sim-vertex.d.ts +0 -2
  67. package/dist/types/layouts/forcedirectedlayout/physlayout/shaders/workaround-fragment.d.ts +0 -2
  68. package/dist/types/layouts/forcedirectedlayout/physlayout/solarmerger/SolarMerger.d.ts +0 -21
  69. package/dist/types/layouts/forcedirectedlayout/physlayout/solarmerger/solar-placer.d.ts +0 -2
  70. /package/dist/types/layouts/forcedirectedlayout/{physlayout/PhysLayout.test.d.ts → PhysLayout.test.d.ts} +0 -0
  71. /package/dist/types/layouts/forcedirectedlayout/{physlayout/solarmerger/SolarMerger.bench.d.ts → clustering/ClusterByMerger.test.d.ts} +0 -0
  72. /package/dist/types/layouts/forcedirectedlayout/{physlayout/solarmerger/SolarMerger.test.d.ts → clustering/SolarMerger.bench.d.ts} +0 -0
  73. /package/dist/types/layouts/forcedirectedlayout/{physlayout/solarmerger/solar-placer.test.d.ts → clustering/SolarMerger.test.d.ts} +0 -0
  74. /package/dist/types/layouts/forcedirectedlayout/{physlayout/glwrapper.d.ts → glwrapper.d.ts} +0 -0
@@ -1,4 +1,4 @@
1
- import type { Node } from '../../../../types/graph-element';
1
+ import type { Node } from '../../../types/graph-element';
2
2
  export interface SolarNode extends Omit<Node, 'id' | 'size'> {
3
3
  id: number;
4
4
  weight?: number;
@@ -13,6 +13,7 @@ export interface SolarNode extends Omit<Node, 'id' | 'size'> {
13
13
  y: number;
14
14
  };
15
15
  size?: number | (() => number);
16
+ synthetic?: boolean;
16
17
  }
17
18
  export interface SolarRelationship {
18
19
  id: string;
@@ -23,6 +24,11 @@ export interface CoarsenedGraph {
23
24
  nodes: SolarNode[];
24
25
  relationships: number[][];
25
26
  idToRel: Record<string, SolarRelationship>;
27
+ /**
28
+ * Contains the "real" {@link relationships} plus any synthetic edges purely for layout purposes.
29
+ * Undefined when there are no synthetic edges (e.g. SolarMerger).
30
+ */
31
+ springRelationships?: number[][];
26
32
  }
27
33
  export interface Sun extends SolarNode {
28
34
  planets: Planet[];
@@ -30,6 +36,8 @@ export interface Sun extends SolarNode {
30
36
  children: () => Planet[];
31
37
  previousIndex: number;
32
38
  }
39
+ /** Sun fields known before {@link attachPlanetsToSun} fills planets/size/children/weight. */
40
+ export type PartialSun = Omit<Sun, 'planets' | 'size' | 'children' | 'previousIndex' | 'weight'>;
33
41
  export interface Planet extends SolarNode {
34
42
  parent: Sun;
35
43
  sunId: number;
@@ -37,6 +45,8 @@ export interface Planet extends SolarNode {
37
45
  size: () => number;
38
46
  children: () => Moon[];
39
47
  }
48
+ /** Planet fields known before {@link attachPlanetsToSun} sets parent. */
49
+ export type PartialPlanet = Omit<Planet, 'parent'>;
40
50
  export interface Moon extends SolarNode {
41
51
  parent: Planet;
42
52
  sunId: number;
@@ -0,0 +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;\nuniform float u_maxNodeSize;\nuniform float u_crossClusterRelationshipMultiplier;\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) * u_collisionMultiplier;\n\n // u_crossClusterRelationshipMultiplier only damps edges that cross a cluster boundary\n if (u_crossClusterRelationshipMultiplier < 1.0) {\n float otherClusterStartIndex = getTextureData(u_clusterData, curSpring, 256.0).y;\n F *= otherClusterStartIndex == myClusterStartIndex ? 1.0 : u_crossClusterRelationshipMultiplier;\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) * u_collisionMultiplier;\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) * u_collisionMultiplier;\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 // Prime the second ping-pong texture with the initial state without calculating forces\n // that would otherwise be discarded at the end of this first iteration.\n if (u_curIteration == 0.0) {\n fragColor = vec4(myPosition.xy, myPosition.zw);\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 }\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 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}";
2
+ export default _default;
@@ -0,0 +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 getAdjustedClusterWeight(float clusterWeight) {\n return (clusterWeight * clusterWeight) / (clusterWeight + 1.0);\n}\n\n// Repulsion area from peer mass (weight). .z size is only for mid-level loop bounds.\nfloat getPeerFootprintArea(float peerWeight) {\n return pow(sqrt(peerWeight) * 4.0 + u_baseLength * 2.0, 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 bool isTopLevel = u_isTopLevel == 1.0;\n\n float clusterIndex = clusterData.x;\n float clusterStartIndex = clusterData.y;\n float clusterSize = clusterData.z;\n float singleNodeRepulsionScale = isTopLevel && clusterSize == 1.0 ? 0.5 : 1.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 float loopEnd = isTopLevel ? u_numNodes : (clusterStartIndex + clusterSize);\n\n // Repulsion & Collision Detection\n for (float i = 0.0; i < 256.0 * 256.0; i++) {\n if (i >= loopEnd || i >= u_numNodes) {\n break;\n }\n if ((!isTopLevel && 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 area = getPeerFootprintArea(otherClusterData.w);\n float F = (area * fScale) / (dist * dist);\n\n if (isTopLevel) {\n repulsionForceScale = 0.2;\n }\n\n acceleration += ((delta / sqrt(dist)) * F) * repulsionForceScale * otherClusterWeight * singleNodeRepulsionScale;\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
+ export default _default;
@@ -0,0 +1,2 @@
1
+ declare const _default: "#version 300 es\nprecision mediump float;\n\nuniform sampler2D u_physData;\nuniform sampler2D u_updateData;\nuniform float u_numNodesNew;\n\nout vec4 fragColor;\n\nconst float INDEX_FLAG = 999999.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\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_numNodesNew) {\n discard;\n }\n\n float firstValue = getTextureData(u_updateData, index * 2.0, 256.0).r;\n float secondValue = getTextureData(u_updateData, index * 2.0 + 1.0, 256.0).r;\n\n if (secondValue == INDEX_FLAG) {\n vec4 myPosition = getTextureData(u_physData, firstValue, 256.0);\n fragColor = myPosition;\n } else {\n fragColor = vec4(firstValue, secondValue, 0.0, 0.0);\n }\n}\n";
2
+ export default _default;
@@ -0,0 +1,2 @@
1
+ declare const _default: "#version 300 es\nuniform mat4 u_projection;\n\nin vec2 a_position;\n//in float a_index;\n\n//out highp float index;\n\nvoid main() {\n// index = a_index;\n gl_Position = u_projection * vec4(a_position, 0.0, 1.0);\n}";
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;
@@ -7,6 +7,8 @@ type HierarchicalLayoutConfig = {
7
7
  export declare class HierarchicalLayout extends AnimatedLayout {
8
8
  private direction;
9
9
  private packing;
10
+ private clusterBy?;
11
+ private lastClusterByAssignments;
10
12
  private stateDisposers;
11
13
  private oldComputing;
12
14
  private computing;
@@ -14,9 +16,12 @@ export declare class HierarchicalLayout extends AnimatedLayout {
14
16
  private worker;
15
17
  private directionChanged;
16
18
  private packingChanged;
19
+ private clusterByChanged;
17
20
  private workersDisabled;
18
21
  constructor(config: HierarchicalLayoutConfig);
19
22
  setOptions(options: LayoutOptions): void;
23
+ private applyClusterByOption;
24
+ private buildClusterAssignments;
20
25
  update(refreshPositions?: boolean): void;
21
26
  getShouldUpdate(): boolean;
22
27
  getComputing(): boolean;
@@ -1,4 +1,4 @@
1
- import type { Node } from '../types/graph-element';
1
+ import type { NodeWithClientPosition } from '../types/graph-element';
2
2
  /**
3
3
  * An object specifying the required callback functions for certain actions
4
4
  */
@@ -6,7 +6,7 @@ export interface ExternalCallbacks {
6
6
  /** Triggered when a layout is done moving. */
7
7
  onLayoutDone?: () => void;
8
8
  /** Triggered on each step of a layout. */
9
- onLayoutStep?: (p: Node[]) => void;
9
+ onLayoutStep?: (p: NodeWithClientPosition[]) => void;
10
10
  /** Triggered when NVL throws an error after initialization. */
11
11
  onError?: (error: Error) => void;
12
12
  /** Triggered when NVL is initialized. */
@@ -32,7 +32,7 @@ export default class ExternalCallbackHandler {
32
32
  onInitialization(): void;
33
33
  onZoomTransitionDone(): void;
34
34
  onLayoutDone(): void;
35
- onLayoutStep(p: Node[]): void;
35
+ onLayoutStep(p: NodeWithClientPosition[]): void;
36
36
  onLayoutComputing(isComputing: boolean): void;
37
37
  onError(error: Error): void;
38
38
  onWebGLContextLost(webGLContextEvent: WebGLContextEvent): 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';
@@ -31,10 +31,13 @@ export default class NvlController {
31
31
  private readonly forceLayout;
32
32
  private readonly canvasRenderer;
33
33
  private readonly svgRenderer;
34
+ private clusterOverlay;
34
35
  private readonly glCanvas;
35
36
  private canvasRect;
36
37
  private readonly glMinimapCanvas;
37
38
  private readonly c2dCanvas;
39
+ private clusterOverlayCanvas;
40
+ private readonly canvasParent;
38
41
  private readonly svg;
39
42
  private isInRenderSwitchAnimation;
40
43
  private previousRenderer;
@@ -46,25 +49,96 @@ export default class NvlController {
46
49
  private readonly stateDisposers;
47
50
  private readonly zoomTransitionHandler;
48
51
  private currentLayout;
52
+ private lastTimestamp;
53
+ private accumulator;
49
54
  private readonly layoutTimeLimit;
55
+ private readonly smoothFrameBudgetMs;
50
56
  private pixelRatio;
51
57
  private readonly removeResizeListener;
52
58
  private readonly removeMinimapResizeListener;
53
59
  private pendingZoom;
54
60
  private pendingPan;
55
61
  private pendingViewportOverride;
56
- private layoutRunner;
57
62
  private animationRequestId;
58
63
  private layoutDoneCallback;
59
64
  private layoutComputingCallback;
60
65
  private currentLayoutType;
61
66
  private descriptionElement;
67
+ /** True when a minimap container was provided at construction. */
68
+ private readonly hasMinimap;
69
+ private lastMinimapPositions;
70
+ private lastMinimapCacheVersion;
62
71
  constructor(state: NvlState, frame: HTMLElement, options: NvlOptions);
63
72
  private onWebGLContextLost;
73
+ private fpsToFrameBudgetMs;
74
+ private getLayoutNeedsToUpdate;
75
+ private isWithinBudget;
76
+ /**
77
+ * Add elapsed frame time to the layout-step accumulator, capped at `layoutTimeLimit`.
78
+ * Prevents a slow frame (e.g. blocked GPU readback) from inflating catch-up debt and
79
+ * scheduling ever more work in a feedback loop; the layout falls behind gracefully instead.
80
+ */
81
+ private accumulateLayoutElapsedTime;
82
+ /**
83
+ * Whether the GPU force-directed layout is active.
84
+ * @returns `true` when PhysLayout is the current layout.
85
+ */
86
+ private usesGpuSimulation;
87
+ /**
88
+ * Whether the GPU-texture fast path can be taken this frame.
89
+ * Requires the WebGL renderer, GPU layout active, and no CPU-position consumers
90
+ * (layout-step callback, HTML overlays). A provided minimap no longer blocks this path;
91
+ * the minimap and cluster overlay are updated from the lagged async position cache instead.
92
+ * @returns `true` when rendering directly from the GPU position texture is safe.
93
+ */
94
+ private canRenderFromGpuTexture;
95
+ /**
96
+ * Uploads lagged PhysLayout cache positions to the minimap when the cache version changed.
97
+ * @returns `true` when positions were refreshed this call.
98
+ */
99
+ private refreshMinimapFromCacheIfStale;
100
+ /**
101
+ * Fits, renders, and caches minimap positions (and PhysLayout cache version when applicable).
102
+ */
103
+ private syncMinimap;
104
+ /** Whether either cluster decoration is enabled in the current layout options. */
105
+ private hasClusterDecorations;
106
+ /**
107
+ * Creates the decoration canvas and its overlay.
108
+ * @returns The overlay to draw with.
109
+ */
110
+ private createClusterOverlay;
111
+ /**
112
+ * Resolves the overlay to draw into, allocating it on first use and tearing it down again
113
+ * while cluster decorations are off.
114
+ * @returns The overlay to draw with, or null when decorations are disabled.
115
+ */
116
+ private ensureClusterOverlay;
117
+ /** Tears the overlay and its canvas back down once decorations are switched off. */
118
+ private destroyClusterOverlay;
119
+ /**
120
+ * Draws (or clears) cluster decorations on the Canvas2D overlay from explicit positions.
121
+ * @param positions - CPU node positions for this frame, or null to clear.
122
+ */
123
+ private renderClusterOverlay;
124
+ /**
125
+ * Draws cluster decorations from PhysLayout's lagged async PBO cache without a sync dump.
126
+ * @param forceLayout - The PhysLayout instance to render the cluster overlay from.
127
+ */
128
+ private renderClusterOverlayFromCache;
129
+ /** Syncs idToPosition from the GPU layout on demand; no-op for CPU layouts. */
130
+ syncPositionsFromLayout(): void;
131
+ /**
132
+ * Fitted {@link ClusteringOptions.clusterBy} hull regions for the current layout, or [].
133
+ * @returns Cluster hit regions for picking.
134
+ */
135
+ getClusterByHitRegions(): import("../utils/clusterHitRegions").ClusterHitRegion[];
64
136
  private updateMinimapZoom;
65
137
  private startMainLoop;
138
+ private renderNodeHtmlOverlays;
139
+ private renderRelationshipHtmlOverlays;
66
140
  updateHtmlOverlay(): void;
67
- dumpNodes(): Node[];
141
+ dumpNodes(): NodeWithClientPosition[];
68
142
  dumpRelationships(): Relationship[];
69
143
  /**
70
144
  * Map a relative Canvas space to the DOM position
@@ -93,7 +167,7 @@ export default class NvlController {
93
167
  };
94
168
  registerDoneCallback(cb: NvlController['layoutDoneCallback']): void;
95
169
  registerComputingCallback(cb: NvlController['layoutComputingCallback']): void;
96
- getPosition(nodeId: string): Node;
170
+ getPosition(nodeId: string): Node & Point;
97
171
  getScale(): number;
98
172
  getPan(): {
99
173
  x: number;
@@ -113,10 +187,16 @@ export default class NvlController {
113
187
  getCanvasNodesAt(point: Point, hitNodeMarginWidth?: number): import("..").HitTargetNode[];
114
188
  private getLayout;
115
189
  setLayout(layoutType: Layout): void;
116
- setLayoutOptions(options: LayoutOptions): void;
190
+ setLayoutOptions(options?: LayoutOptions): void;
191
+ /**
192
+ * Syncs positions if needed and redraws (or clears) the cluster overlay from current state.
193
+ */
194
+ private refreshClusterOverlay;
117
195
  private getDataUrlForCanvas;
118
196
  private initiateFileDownload;
119
197
  private updateLayoutAndPositions;
198
+ private getAndUpdatePositions;
199
+ private getLayoutPositions;
120
200
  saveToFile(options: SaveToFileOptions): void;
121
201
  private createSvgString;
122
202
  saveToSvg(options?: SaveToFileOptions): Promise<void>;
@@ -4,7 +4,7 @@ export default class Shader {
4
4
  private curTexture;
5
5
  private attributeInfo;
6
6
  private uniformInfo;
7
- constructor(gl: WebGLRenderingContext, vsSource: string, fsSource: string, defines?: {});
7
+ constructor(gl: WebGL2RenderingContext, vsSource: string, fsSource: string, defines?: {});
8
8
  setUniform(name: string, value: any): void;
9
9
  setAttributePointer(name: string, size: number, start: number, stride: number): void;
10
10
  setAttributePointerFloat(name: string, size: number, start: number, stride: number): void;
@@ -1,5 +1,5 @@
1
1
  import type { Node, Relationship } from '../types/graph-element';
2
- import type { Point } from '../utils/geometry';
2
+ import { type Point } from '../utils/geometry';
3
3
  /** A graph stored with mobx. */
4
4
  export interface DataSet<T> {
5
5
  items: T[];
@@ -8,6 +8,7 @@ export interface DataSet<T> {
8
8
  idToPosition: Record<string, Node & Point>;
9
9
  idToHtmlOverlay?: Record<string, HTMLElement>;
10
10
  version: number;
11
+ maxDimension: number;
11
12
  addChannel: (this: DataSet<T>, channelName: string) => void;
12
13
  removeChannel: (this: DataSet<T>, channelName: string) => void;
13
14
  clearChannel: (this: DataSet<T>, channelName: string) => void;
@@ -29,7 +30,8 @@ export interface Channel<T> {
29
30
  updates: Record<string, T>;
30
31
  removes: Record<string, T>;
31
32
  }
32
- export declare const getHtmlProperty: (item: Partial<Node | Relationship>) => HTMLElement | undefined;
33
+ export declare const getHtmlKey: (item: Node | Relationship) => "html" | "captionHtml";
34
+ export declare const getHtmlProperty: (item: Node | Relationship) => HTMLElement | undefined;
33
35
  /**
34
36
  * Creates an empty dataset in which a graph be stored and updated using mobx actions.
35
37
  * @returns A new and empty {@link DataSet}.
@@ -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
  /**
@@ -226,15 +307,36 @@ export interface NvlOptions {
226
307
  disabledItemColor?: string;
227
308
  /** The color to use for the labels of the disabled nodes and relationships */
228
309
  disabledItemFontColor?: string;
229
- /** The color to use for the viewport box in the minimap */
310
+ /**
311
+ * Stroke color for overlay UI lines (e.g. minimap viewport box).
312
+ * Use a dark color on light backgrounds and a light color on dark backgrounds.
313
+ * @defaultValue `#1A1B1D`
314
+ */
315
+ overlayLineColor?: string;
316
+ /**
317
+ * The color to use for the viewport box in the minimap.
318
+ * @deprecated Use {@link overlayLineColor}.
319
+ */
230
320
  minimapViewportBoxColor?: string;
231
321
  };
232
322
  /**
233
- * @internal
234
- * @hidden
235
- * 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.
236
338
  */
237
- layoutTimeLimit?: number;
339
+ maxFps?: number;
238
340
  /**
239
341
  * Disables tracking of library usage
240
342
  * @defaultValue false
@@ -320,6 +422,6 @@ export interface NvlState {
320
422
  disabledItemStyles: DisabledItemStyles;
321
423
  defaultNodeColor: string;
322
424
  defaultRelationshipColor: string;
323
- minimapViewportBoxColor: string;
425
+ overlayLineColor: string;
324
426
  }
325
427
  export {};
@@ -6,7 +6,7 @@ import type { NvlOptions } from './types';
6
6
  * If both caption and captions are provided, captions takes precedence.
7
7
  */
8
8
  export declare const processCaptions: (item: Partial<Node | Relationship>) => StyledCaption[];
9
- export declare const processColorStyles: (styling: NvlOptions["styling"]) => {
9
+ export declare const processColorStyles: (styling?: NvlOptions["styling"]) => {
10
10
  nodeBorderStyles: {
11
11
  default: {
12
12
  rings?: {
@@ -42,5 +42,5 @@ export declare const processColorStyles: (styling: NvlOptions["styling"]) => {
42
42
  };
43
43
  defaultNodeColor: string;
44
44
  defaultRelationshipColor: string;
45
- minimapViewportBoxColor: string;
45
+ overlayLineColor: string;
46
46
  };
@@ -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 {};