@fusefactory/fuse-three-forcegraph 1.0.2 → 1.0.3

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.
package/dist/index.d.mts CHANGED
@@ -220,6 +220,7 @@ interface GraphPreset {
220
220
  };
221
221
  links?: {
222
222
  opacity?: number;
223
+ noiseStrength?: number;
223
224
  };
224
225
  }
225
226
  //#endregion
@@ -1013,20 +1014,15 @@ declare class TooltipManager {
1013
1014
  * Hide all tooltips
1014
1015
  */
1015
1016
  hideAll(): void;
1016
- /**
1017
- * Generate preview content (subset of fields)
1018
- */
1019
- private generatePreviewContent;
1020
- /**
1021
- * Generate full tooltip content (all fields)
1022
- */
1023
- private generateFullContent;
1024
- private escapeHtml;
1025
1017
  showMainTooltip(content: string, x: number, y: number): void;
1026
1018
  hideMainTooltip(): void;
1027
1019
  showChainTooltip(nodeId: string, content: string, x: number, y: number): void;
1028
1020
  hideChainTooltips(): void;
1029
1021
  updateMainTooltipPos(x: number, y: number): void;
1022
+ /**
1023
+ * Set main tooltip visibility (doesn't affect open state or content)
1024
+ */
1025
+ setMainTooltipVisibility(visible: boolean): void;
1030
1026
  dispose(): void;
1031
1027
  }
1032
1028
  //#endregion
@@ -1225,6 +1221,7 @@ declare class Engine {
1225
1221
  private isRunning;
1226
1222
  private boundResizeHandler;
1227
1223
  private groupOrder?;
1224
+ private smoothedTooltipPos;
1228
1225
  constructor(canvas: HTMLCanvasElement, options?: EngineOptions);
1229
1226
  /**
1230
1227
  * Handle window resize event
@@ -1239,6 +1236,17 @@ declare class Engine {
1239
1236
  * This allows changing visibility/behavior without resetting the simulation
1240
1237
  */
1241
1238
  updateNodeStates(callback: (node: GraphNode) => NodeState): void;
1239
+ /**
1240
+ * Set target positions for elastic force (tree layout, etc.)
1241
+ * Writes positions into the "original positions" GPU buffer that ElasticPass reads from,
1242
+ * without touching current positions or node states.
1243
+ * Must be called AFTER applyPreset() since updateNodeStates() overwrites original positions.
1244
+ */
1245
+ setTargetPositions(targets: Map<string, {
1246
+ x: number;
1247
+ y: number;
1248
+ z: number;
1249
+ }>): void;
1242
1250
  /**
1243
1251
  * Reheat the simulation
1244
1252
  */
@@ -1289,13 +1297,16 @@ declare class Engine {
1289
1297
  getNodePosition(nodeId: string): THREE.Vector3 | null;
1290
1298
  /**
1291
1299
  * Get node screen position from world position
1300
+ * Returns position and visibility info (whether node is on screen and in front of camera)
1292
1301
  */
1293
1302
  getNodeScreenPosition(nodeId: string): {
1294
1303
  x: number;
1295
1304
  y: number;
1305
+ visible: boolean;
1296
1306
  } | null;
1297
1307
  /**
1298
1308
  * Update sticky tooltip position to follow node during camera movement
1309
+ * Uses lerp smoothing to filter out micro-jitter from force simulation
1299
1310
  */
1300
1311
  private updateStickyTooltipPosition;
1301
1312
  /**
package/dist/index.mjs CHANGED
@@ -449,6 +449,18 @@ var Tooltip = class {
449
449
  if (this.positionCallback) this.positionCallback(x, y);
450
450
  }
451
451
  }
452
+ /**
453
+ * Show tooltip (visibility only, doesn't affect open state)
454
+ */
455
+ show() {
456
+ if (this.isVisible) this.element.style.visibility = "visible";
457
+ }
458
+ /**
459
+ * Hide tooltip (visibility only, doesn't close/cleanup)
460
+ */
461
+ hide() {
462
+ this.element.style.visibility = "hidden";
463
+ }
452
464
  getElement() {
453
465
  return this.element;
454
466
  }
@@ -466,12 +478,9 @@ var TooltipManager = class {
466
478
  this.canvas = canvas;
467
479
  this.chainTooltips = /* @__PURE__ */ new Map();
468
480
  this.closeButton = null;
469
- this.config = {
470
- useDefaultStyles: true,
471
- ...config
472
- };
473
- this.mainTooltip = new Tooltip("main", container, this.config.useDefaultStyles);
474
- this.previewTooltip = new Tooltip("preview", container, this.config.useDefaultStyles);
481
+ this.config = { ...config };
482
+ this.mainTooltip = new Tooltip("main", container);
483
+ this.previewTooltip = new Tooltip("preview", container);
475
484
  }
476
485
  /**
477
486
  * Show grab cursor when hovering over a node
@@ -594,45 +603,16 @@ var TooltipManager = class {
594
603
  this.hideFull();
595
604
  this.hideChainTooltips();
596
605
  }
597
- /**
598
- * Generate preview content (subset of fields)
599
- */
600
- generatePreviewContent(node) {
601
- return `
602
- <div class="tooltip-preview" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;">
603
- <h3 style="margin: 0 0 8px 0; font-size: 16px; font-weight: 600;">${this.escapeHtml(node.title || node.id)}</h3>
604
- <p style="margin: 0; font-size: 13px; opacity: 0.8;">${this.escapeHtml(node.group || "unknown")}</p>
605
- </div>
606
- `;
607
- }
608
- /**
609
- * Generate full tooltip content (all fields)
610
- */
611
- generateFullContent(node) {
612
- return `
613
- <div class="tooltip-full" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; padding-right: 24px;">
614
- <h2 style="margin: 0 0 12px 0; font-size: 18px; font-weight: 600;">${this.escapeHtml(node.title || node.id)}</h2>
615
- ${node.thumbnailUrl ? `<img src="${this.escapeHtml(node.thumbnailUrl)}" alt="thumbnail" style="max-width: 100%; height: auto; border-radius: 4px; margin: 0 0 12px 0;" />` : ""}
616
- <p style="margin: 0 0 8px 0; font-size: 13px;"><strong>Group:</strong> ${this.escapeHtml(node.group || "unknown")}</p>
617
- ${node.description ? `<p style="margin: 0 0 8px 0; font-size: 13px; line-height: 1.5;">${this.escapeHtml(node.description)}</p>` : ""}
618
- <p style="margin: 0; font-size: 12px; opacity: 0.6;"><strong>ID:</strong> ${this.escapeHtml(node.id)}</p>
619
- </div>
620
- `;
621
- }
622
- escapeHtml(text) {
623
- const div = document.createElement("div");
624
- div.textContent = text;
625
- return div.innerHTML;
626
- }
627
606
  showMainTooltip(content, x, y) {
628
607
  this.mainTooltip.open(content);
629
608
  this.mainTooltip.updatePos(x, y);
609
+ console.log("Showing main tooltip");
630
610
  }
631
611
  hideMainTooltip() {
632
612
  this.mainTooltip.close();
633
613
  }
634
614
  showChainTooltip(nodeId, content, x, y) {
635
- if (!this.chainTooltips.has(nodeId)) this.chainTooltips.set(nodeId, new Tooltip("label", this.container, this.config.useDefaultStyles));
615
+ if (!this.chainTooltips.has(nodeId)) this.chainTooltips.set(nodeId, new Tooltip("label", this.container));
636
616
  const tooltip = this.chainTooltips.get(nodeId);
637
617
  tooltip.open(content);
638
618
  tooltip.updatePos(x, y);
@@ -643,6 +623,13 @@ var TooltipManager = class {
643
623
  updateMainTooltipPos(x, y) {
644
624
  this.mainTooltip.updatePos(x, y);
645
625
  }
626
+ /**
627
+ * Set main tooltip visibility (doesn't affect open state or content)
628
+ */
629
+ setMainTooltipVisibility(visible) {
630
+ if (visible) this.mainTooltip.show();
631
+ else this.mainTooltip.hide();
632
+ }
646
633
  dispose() {
647
634
  this.removeCloseButton();
648
635
  this.mainTooltip.destroy();
@@ -995,11 +982,11 @@ var CameraController = class {
995
982
 
996
983
  //#endregion
997
984
  //#region assets/glsl/lines/lines.frag
998
- var lines_default$1 = "precision mediump float;\n\nvarying vec3 vColor;\r\nflat varying float vAlphaIndex;\n\nuniform sampler2D uAlphaTexture;\r\nuniform vec2 uAlphaTextureSize; \n\nvec2 alphaUvFromIndex(float idx) {\r\n float width = uAlphaTextureSize.x;\r\n float texX = mod(idx, width);\r\n float texY = floor(idx / width);\r\n return (vec2(texX + 0.5, texY + 0.5) / uAlphaTextureSize);\r\n}\n\nvoid main() {\r\n vec2 uv = alphaUvFromIndex(vAlphaIndex);\r\n float a = texture2D(uAlphaTexture, uv).r;\n\n gl_FragColor = vec4(vColor, a);\r\n #include <colorspace_fragment>\r\n}";
985
+ var lines_default$1 = "precision mediump float;\n\nvarying vec3 vColor;\r\nflat varying float vAlphaIndex;\r\nflat varying float vRank;\n\nuniform sampler2D uAlphaTexture;\r\nuniform vec2 uAlphaTextureSize; \n\nvec2 alphaUvFromIndex(float idx) {\r\n float width = uAlphaTextureSize.x;\r\n float texX = mod(idx, width);\r\n float texY = floor(idx / width);\r\n return (vec2(texX + 0.5, texY + 0.5) / uAlphaTextureSize);\r\n}\n\nvoid main() {\r\n vec2 uv = alphaUvFromIndex(vAlphaIndex);\r\n float a = texture2D(uAlphaTexture, uv).r;\n\n gl_FragColor = vec4(vColor, a * vRank);\r\n #include <colorspace_fragment>\r\n}";
999
986
 
1000
987
  //#endregion
1001
988
  //#region assets/glsl/lines/lines.vert
1002
- var lines_default = "attribute float t;\n\nattribute vec2 instanceLinkA;\r\nattribute vec2 instanceLinkB;\r\nattribute vec3 instanceColorA;\r\nattribute vec3 instanceColorB;\r\nattribute float instanceAlphaIndex;\n\nuniform sampler2D uPositionsTexture;\r\nuniform vec2 uPositionsTextureSize;\r\nuniform float uNoiseStrength;\r\nuniform float uTime;\n\nvarying vec3 vColor;\r\nflat varying float vAlphaIndex;\n\nvec3 organicNoise(vec3 p, float seed) {\r\n float frequency = 1.5;\r\n float branchiness = 5.;\r\n \r\n \n vec3 n1 = sin(p * frequency + seed) * 0.5;\r\n vec3 n2 = sin(p * frequency * 2.3 + seed * 1.5) * 0.25 * (1.0 + branchiness);\r\n vec3 n3 = sin(p * frequency * 4.7 + seed * 2.3) * 0.125 * (1.0 + branchiness);\r\n \r\n return n1 + n2 + n3;\r\n}\n\nfloat smoothCurve(float t) {\r\n return t * t * (3.0 - 2.0 * t);\r\n}\n\nvoid main() {\r\n \n vColor = mix(instanceColorA, instanceColorB, t);\r\n vAlphaIndex = instanceAlphaIndex;\n\n \n vec2 uvA = (instanceLinkA + vec2(0.5)) / uPositionsTextureSize;\r\n vec2 uvB = (instanceLinkB + vec2(0.5)) / uPositionsTextureSize;\n\n vec4 posA = texture2D(uPositionsTexture, uvA);\r\n vec4 posB = texture2D(uPositionsTexture, uvB);\r\n if (posA.w < 0.5 || posB.w < 0.5) {\r\n \n gl_Position = vec4(2.0, 2.0, 2.0, 1.0); \r\n return;\r\n }\r\n vec3 linear = mix(posA.xyz, posB.xyz, t);\r\n \r\n \n vec3 dir = normalize(posB.xyz - posA.xyz);\r\n float dist = length(posB.xyz - posA.xyz);\r\n vec3 up = abs(dir.y) < 0.99 ? vec3(0.0, 1.0, 0.0) : vec3(1.0, 0.0, 0.0);\r\n vec3 perp = normalize(cross(dir, up));\r\n vec3 upVec = normalize(cross(perp, dir));\n\n \r\n \n float sinPhase = instanceAlphaIndex * 0.1 + uTime * 0.3;\r\n float sinWave = sin(t * 3.14159265 * 2.0 + sinPhase);\r\n \r\n \n float tSmooth = smoothCurve(t);\r\n float curve = sin(t * 3.14159265) * tSmooth;\r\n \r\n \n float curvature = 0.1;\r\n curve = pow(curve, 1.0 - curvature * 0.5);\r\n \r\n float intensity = curve * uNoiseStrength;\n\n vec3 pos = linear;\n\n if (intensity > 0.001) {\r\n \n vec3 samplePos = linear * 0.5 + vec3(instanceAlphaIndex, 0.0, instanceAlphaIndex * 0.7);\r\n vec3 noise = organicNoise(samplePos, instanceAlphaIndex);\r\n \r\n \n float sinModulation = 0.5 + 0.5 * sinWave;\r\n pos += perp * noise.x * intensity * sinModulation;\r\n pos += upVec * noise.y * intensity * 0.7;\r\n pos += dir * noise.z * intensity * 0.3;\r\n }\n\n vec4 mvPosition = modelViewMatrix * vec4(pos, 1.0);\r\n gl_Position = projectionMatrix * mvPosition;\r\n}";
989
+ var lines_default = "attribute float t;\n\nattribute vec2 instanceLinkA;\r\nattribute vec2 instanceLinkB;\r\nattribute vec3 instanceColorA;\r\nattribute vec3 instanceColorB;\r\nattribute float instanceAlphaIndex;\r\nattribute float instanceRank;\n\nuniform sampler2D uPositionsTexture;\r\nuniform vec2 uPositionsTextureSize;\r\nuniform float uNoiseStrength;\r\nuniform float uTime;\n\nvarying vec3 vColor;\r\nflat varying float vAlphaIndex;\r\nflat varying float vRank;\n\nvec3 organicNoise(vec3 p, float seed) {\r\n float frequency = 1.5;\r\n float branchiness = 5.;\r\n \r\n \n vec3 n1 = sin(p * frequency + seed) * 0.5;\r\n vec3 n2 = sin(p * frequency * 2.3 + seed * 1.5) * 0.25 * (1.0 + branchiness);\r\n vec3 n3 = sin(p * frequency * 4.7 + seed * 2.3) * 0.125 * (1.0 + branchiness);\r\n \r\n return n1 + n2 + n3;\r\n}\n\nfloat smoothCurve(float t) {\r\n return t * t * (3.0 - 2.0 * t);\r\n}\n\nvoid main() {\r\n \n vColor = mix(instanceColorA, instanceColorB, t);\r\n vAlphaIndex = instanceAlphaIndex;\r\n vRank = instanceRank;\n\n \n vec2 uvA = (instanceLinkA + vec2(0.5)) / uPositionsTextureSize;\r\n vec2 uvB = (instanceLinkB + vec2(0.5)) / uPositionsTextureSize;\n\n vec4 posA = texture2D(uPositionsTexture, uvA);\r\n vec4 posB = texture2D(uPositionsTexture, uvB);\r\n if (posA.w < 0.5 || posB.w < 0.5) {\r\n \n gl_Position = vec4(2.0, 2.0, 2.0, 1.0); \r\n return;\r\n }\r\n vec3 linear = mix(posA.xyz, posB.xyz, t);\r\n \r\n \n vec3 dir = normalize(posB.xyz - posA.xyz);\r\n float dist = length(posB.xyz - posA.xyz);\r\n vec3 up = abs(dir.y) < 0.99 ? vec3(0.0, 1.0, 0.0) : vec3(1.0, 0.0, 0.0);\r\n vec3 perp = normalize(cross(dir, up));\r\n vec3 upVec = normalize(cross(perp, dir));\n\n \r\n \n float sinPhase = instanceAlphaIndex * 0.1 + uTime * 0.3;\r\n float sinWave = sin(t * 3.14159265 * 2.0 + sinPhase);\r\n \r\n \n float tSmooth = smoothCurve(t);\r\n float curve = sin(t * 3.14159265) * tSmooth;\r\n \r\n \n float curvature = 0.1;\r\n curve = pow(curve, 1.0 - curvature * 0.5);\r\n \r\n float intensity = curve * uNoiseStrength;\n\n vec3 pos = linear;\n\n if (intensity > 0.001) {\r\n \n vec3 samplePos = linear * 0.5 + vec3(instanceAlphaIndex, 0.0, instanceAlphaIndex * 0.7);\r\n vec3 noise = organicNoise(samplePos, instanceAlphaIndex);\r\n \r\n \n float sinModulation = 0.5 + 0.5 * sinWave;\r\n pos += perp * noise.x * intensity * sinModulation;\r\n pos += upVec * noise.y * intensity * 0.7;\r\n pos += dir * noise.z * intensity * 0.3;\r\n }\n\n vec4 mvPosition = modelViewMatrix * vec4(pos, 1.0);\r\n gl_Position = projectionMatrix * mvPosition;\r\n}";
1003
990
 
1004
991
  //#endregion
1005
992
  //#region core/StyleRegistry.ts
@@ -1450,7 +1437,7 @@ var LinksRenderer = class {
1450
1437
  this.linkIndexMap = /* @__PURE__ */ new Map();
1451
1438
  this.interpolationSteps = 24;
1452
1439
  this.params = {
1453
- noiseStrength: .2,
1440
+ noiseStrength: .1,
1454
1441
  defaultAlpha: .02,
1455
1442
  highlightAlpha: .8,
1456
1443
  dimmedAlpha: .02,
@@ -1636,7 +1623,13 @@ var LinksRenderer = class {
1636
1623
  /**
1637
1624
  * Create link geometry with interpolated segments
1638
1625
  */
1639
- createLinkGeometry(links, nodes, nodeTextureSize) {
1626
+ createLinkGeometry(links, nodes, nodeTextureSize, groupOrder = [
1627
+ "root",
1628
+ "series",
1629
+ "artwork",
1630
+ "exhibition",
1631
+ "media"
1632
+ ]) {
1640
1633
  const linkIndexMap = /* @__PURE__ */ new Map();
1641
1634
  const nodeMap = /* @__PURE__ */ new Map();
1642
1635
  nodes.forEach((node) => nodeMap.set(node.id, node));
@@ -1664,6 +1657,11 @@ var LinksRenderer = class {
1664
1657
  const instanceColorA = new Float32Array(links.length * 3);
1665
1658
  const instanceColorB = new Float32Array(links.length * 3);
1666
1659
  const instanceAlphaIndex = new Float32Array(links.length);
1660
+ const instanceRank = new Float32Array(links.length);
1661
+ const categoryRankMap = /* @__PURE__ */ new Map();
1662
+ groupOrder.forEach((g, i) => categoryRankMap.set(g, i));
1663
+ let nextRank = groupOrder.length;
1664
+ const maxRank = Math.max(groupOrder.length - 1, 1);
1667
1665
  links.forEach((link, i) => {
1668
1666
  const sourceId = link.source && typeof link.source === "object" ? link.source.id : link.source;
1669
1667
  const targetId = link.target && typeof link.target === "object" ? link.target.id : link.target;
@@ -1698,6 +1696,9 @@ var LinksRenderer = class {
1698
1696
  instanceColorB[i * 3 + 1] = tgtColor.g;
1699
1697
  instanceColorB[i * 3 + 2] = tgtColor.b;
1700
1698
  instanceAlphaIndex[i] = i;
1699
+ const targetCategory = targetNode?.category ?? "";
1700
+ if (targetCategory && !categoryRankMap.has(targetCategory)) categoryRankMap.set(targetCategory, nextRank++);
1701
+ instanceRank[i] = 1 - .75 * ((categoryRankMap.get(targetCategory) ?? 0) / maxRank);
1701
1702
  const linkId = `${sourceId}-${targetId}`;
1702
1703
  linkIndexMap.set(linkId, i);
1703
1704
  });
@@ -1706,6 +1707,7 @@ var LinksRenderer = class {
1706
1707
  geometry.setAttribute("instanceColorA", new THREE.InstancedBufferAttribute(instanceColorA, 3));
1707
1708
  geometry.setAttribute("instanceColorB", new THREE.InstancedBufferAttribute(instanceColorB, 3));
1708
1709
  geometry.setAttribute("instanceAlphaIndex", new THREE.InstancedBufferAttribute(instanceAlphaIndex, 1));
1710
+ geometry.setAttribute("instanceRank", new THREE.InstancedBufferAttribute(instanceRank, 1));
1709
1711
  return {
1710
1712
  geometry,
1711
1713
  linkIndexMap
@@ -1780,10 +1782,8 @@ var LinksRenderer = class {
1780
1782
  const category = nodeCategories[tIdx] || "";
1781
1783
  if (category && !categoryRankMap.has(category)) categoryRankMap.set(category, nextRank++);
1782
1784
  const rank = categoryRankMap.get(category) ?? 0;
1783
- const strength = 1 / (rank + 1);
1784
- const distance = .3 ** rank;
1785
- linkPropertiesData[di] = strength;
1786
- linkPropertiesData[di + 1] = distance;
1785
+ 1 / (rank + 1);
1786
+ .3 ** rank;
1787
1787
  }
1788
1788
  nodeCursor[tIdx] = offset + 1;
1789
1789
  }
@@ -2235,7 +2235,8 @@ var GraphScene = class {
2235
2235
  canvas,
2236
2236
  antialias: true,
2237
2237
  alpha: true,
2238
- depth: true
2238
+ depth: true,
2239
+ logarithmicDepthBuffer: true
2239
2240
  });
2240
2241
  this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
2241
2242
  this.renderer.setSize(window.innerWidth, window.innerHeight);
@@ -3137,7 +3138,7 @@ var ForceSimulation = class {
3137
3138
  }
3138
3139
  updateDrag(targetWorldPos) {
3139
3140
  this.dragPass.updateDrag(targetWorldPos);
3140
- this.reheat(.01);
3141
+ this.reheat(.1);
3141
3142
  }
3142
3143
  endDrag() {
3143
3144
  this.dragPass.endDrag();
@@ -3850,6 +3851,7 @@ var Engine = class {
3850
3851
  this.animationFrameId = null;
3851
3852
  this.isRunning = false;
3852
3853
  this.boundResizeHandler = null;
3854
+ this.smoothedTooltipPos = null;
3853
3855
  this.animate = () => {
3854
3856
  if (!this.isRunning) return;
3855
3857
  const deltaTime = this.clock.update();
@@ -3940,6 +3942,33 @@ var Engine = class {
3940
3942
  this.simulationBuffers.setOriginalPositions(newOriginalPositions);
3941
3943
  }
3942
3944
  /**
3945
+ * Set target positions for elastic force (tree layout, etc.)
3946
+ * Writes positions into the "original positions" GPU buffer that ElasticPass reads from,
3947
+ * without touching current positions or node states.
3948
+ * Must be called AFTER applyPreset() since updateNodeStates() overwrites original positions.
3949
+ */
3950
+ setTargetPositions(targets) {
3951
+ if (!this.simulationBuffers.isReady()) return;
3952
+ const nodes = this.graphStore.getNodes();
3953
+ const currentPositions = this.simulationBuffers.readPositions();
3954
+ const newOriginalPositions = new Float32Array(nodes.length * 4);
3955
+ nodes.forEach((node, i) => {
3956
+ const target = targets.get(node.id);
3957
+ const state = currentPositions[i * 4 + 3];
3958
+ if (target) {
3959
+ newOriginalPositions[i * 4] = target.x;
3960
+ newOriginalPositions[i * 4 + 1] = target.y;
3961
+ newOriginalPositions[i * 4 + 2] = target.z;
3962
+ } else {
3963
+ newOriginalPositions[i * 4] = currentPositions[i * 4];
3964
+ newOriginalPositions[i * 4 + 1] = currentPositions[i * 4 + 1];
3965
+ newOriginalPositions[i * 4 + 2] = currentPositions[i * 4 + 2];
3966
+ }
3967
+ newOriginalPositions[i * 4 + 3] = state;
3968
+ });
3969
+ this.simulationBuffers.setOriginalPositions(newOriginalPositions);
3970
+ }
3971
+ /**
3943
3972
  * Reheat the simulation
3944
3973
  */
3945
3974
  reheat(alpha = 1) {
@@ -3992,13 +4021,20 @@ var Engine = class {
3992
4021
  this.forceSimulation.reheat();
3993
4022
  }
3994
4023
  }
4024
+ if (this.forceSimulation.config.enableRadial) {
4025
+ const rootCategories = preset.radial?.rootCategories ?? ["root"];
4026
+ this.computeRadialDepths(rootCategories);
4027
+ }
3995
4028
  if (preset.links) {
3996
4029
  const linkRenderer = this.graphScene.getLinkRenderer();
3997
- if (linkRenderer) linkRenderer.setOptions({ alpha: { default: preset.links.opacity } });
4030
+ if (linkRenderer) linkRenderer.setOptions({
4031
+ alpha: { default: preset.links.opacity },
4032
+ noiseStrength: preset.links.noiseStrength
4033
+ });
3998
4034
  }
3999
4035
  }
4000
4036
  /**
4001
- *
4037
+ *
4002
4038
  * Start render loop
4003
4039
  */
4004
4040
  start() {
@@ -4061,6 +4097,7 @@ var Engine = class {
4061
4097
  }
4062
4098
  /**
4063
4099
  * Get node screen position from world position
4100
+ * Returns position and visibility info (whether node is on screen and in front of camera)
4064
4101
  */
4065
4102
  getNodeScreenPosition(nodeId) {
4066
4103
  const pos = this.getNodePosition(nodeId);
@@ -4069,18 +4106,40 @@ var Engine = class {
4069
4106
  const size = new THREE.Vector2();
4070
4107
  this.graphScene.renderer.getSize(size);
4071
4108
  const rect = this.graphScene.renderer.domElement.getBoundingClientRect();
4109
+ const x = (screenPos.x + 1) / 2 * size.x + rect.left;
4110
+ const y = -(screenPos.y - 1) / 2 * size.y + rect.top;
4111
+ const behindCamera = screenPos.z > 1;
4112
+ const outsideViewport = x < rect.left || x > rect.right || y < rect.top || y > rect.bottom;
4072
4113
  return {
4073
- x: (screenPos.x + 1) / 2 * size.x + rect.left,
4074
- y: -(screenPos.y - 1) / 2 * size.y + rect.top
4114
+ x,
4115
+ y,
4116
+ visible: !behindCamera && !outsideViewport
4075
4117
  };
4076
4118
  }
4077
4119
  /**
4078
4120
  * Update sticky tooltip position to follow node during camera movement
4121
+ * Uses lerp smoothing to filter out micro-jitter from force simulation
4079
4122
  */
4080
4123
  updateStickyTooltipPosition() {
4081
- if (!this.interactionManager.isTooltipSticky || !this.interactionManager.stickyNodeId) return;
4124
+ if (!this.interactionManager.isTooltipSticky || !this.interactionManager.stickyNodeId) {
4125
+ this.smoothedTooltipPos = null;
4126
+ return;
4127
+ }
4082
4128
  const screenPos = this.getNodeScreenPosition(this.interactionManager.stickyNodeId);
4083
- if (screenPos) this.interactionManager.tooltipManager.updateMainTooltipPos(screenPos.x, screenPos.y);
4129
+ if (!screenPos) return;
4130
+ this.interactionManager.tooltipManager.setMainTooltipVisibility(screenPos.visible);
4131
+ if (!screenPos.visible) return;
4132
+ const targetX = screenPos.x;
4133
+ const targetY = screenPos.y;
4134
+ if (!this.smoothedTooltipPos || this.smoothedTooltipPos.nodeId !== this.interactionManager.stickyNodeId) this.smoothedTooltipPos = {
4135
+ x: targetX,
4136
+ y: targetY,
4137
+ nodeId: this.interactionManager.stickyNodeId
4138
+ };
4139
+ const lerpFactor = .35;
4140
+ this.smoothedTooltipPos.x += (targetX - this.smoothedTooltipPos.x) * lerpFactor;
4141
+ this.smoothedTooltipPos.y += (targetY - this.smoothedTooltipPos.y) * lerpFactor;
4142
+ this.interactionManager.tooltipManager.updateMainTooltipPos(this.smoothedTooltipPos.x, this.smoothedTooltipPos.y);
4084
4143
  }
4085
4144
  /**
4086
4145
  * Programmatically select a node (highlight + tooltip)
@@ -4095,6 +4154,7 @@ var Engine = class {
4095
4154
  const connectedIds = this.graphStore.getConnectedNodeIds(node.id);
4096
4155
  connectedIds.push(node.id);
4097
4156
  nodeRenderer?.highlight(connectedIds);
4157
+ this.smoothedTooltipPos = null;
4098
4158
  const screenPos = this.getNodeScreenPosition(nodeId);
4099
4159
  if (screenPos) this.interactionManager.tooltipManager.showFull(node, screenPos.x, screenPos.y);
4100
4160
  }
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@fusefactory/fuse-three-forcegraph",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
+ "packageManager": "pnpm@10.6.4",
4
5
  "description": "A high-performance GPU-accelerated force-directed graph visualization library built with Three.js. Features a modular pass-based architecture for flexible and extensible force simulations.",
5
6
  "author": "Matteo Amerena",
6
7
  "license": "ISC",
@@ -14,6 +15,10 @@
14
15
  "files": [
15
16
  "dist"
16
17
  ],
18
+ "scripts": {
19
+ "build": "tsdown",
20
+ "watch": "tsdown --watch"
21
+ },
17
22
  "dependencies": {
18
23
  "@rnbo/js": "^1.4.2",
19
24
  "camera-controls": "^3.1.1",
@@ -25,6 +30,11 @@
25
30
  "typescript": "^5.9.3",
26
31
  "vite-plugin-glsl": "^1.5.5"
27
32
  },
33
+ "pnpm": {
34
+ "onlyBuiltDependencies": [
35
+ "esbuild"
36
+ ]
37
+ },
28
38
  "repository": {
29
39
  "type": "git",
30
40
  "url": "git+https://github.com/fusefactory/fuse-three-forcegraph.git"
@@ -32,9 +42,5 @@
32
42
  "bugs": {
33
43
  "url": "https://github.com/fusefactory/fuse-three-forcegraph/issues"
34
44
  },
35
- "homepage": "https://github.com/fusefactory/fuse-three-forcegraph#readme",
36
- "scripts": {
37
- "build": "tsdown",
38
- "watch": "tsdown --watch"
39
- }
40
- }
45
+ "homepage": "https://github.com/fusefactory/fuse-three-forcegraph#readme"
46
+ }