@fusefactory/fuse-three-forcegraph 1.0.2 → 1.0.4
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 +24 -9
- package/dist/index.mjs +138 -56
- package/package.json +13 -7
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
|
|
@@ -802,6 +803,10 @@ declare class LinksRenderer {
|
|
|
802
803
|
* Highlight links connected to a specific node
|
|
803
804
|
*/
|
|
804
805
|
highlightConnectedLinks(nodeId: string, step?: number): void;
|
|
806
|
+
/**
|
|
807
|
+
* Highlight links connected to any of the given nodes
|
|
808
|
+
*/
|
|
809
|
+
highlightConnectedToNodes(nodeIds: Set<string>, step?: number): void;
|
|
805
810
|
/**
|
|
806
811
|
* Clear all highlights (return to defaultAlpha)
|
|
807
812
|
*/
|
|
@@ -1013,20 +1018,15 @@ declare class TooltipManager {
|
|
|
1013
1018
|
* Hide all tooltips
|
|
1014
1019
|
*/
|
|
1015
1020
|
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
1021
|
showMainTooltip(content: string, x: number, y: number): void;
|
|
1026
1022
|
hideMainTooltip(): void;
|
|
1027
1023
|
showChainTooltip(nodeId: string, content: string, x: number, y: number): void;
|
|
1028
1024
|
hideChainTooltips(): void;
|
|
1029
1025
|
updateMainTooltipPos(x: number, y: number): void;
|
|
1026
|
+
/**
|
|
1027
|
+
* Set main tooltip visibility (doesn't affect open state or content)
|
|
1028
|
+
*/
|
|
1029
|
+
setMainTooltipVisibility(visible: boolean): void;
|
|
1030
1030
|
dispose(): void;
|
|
1031
1031
|
}
|
|
1032
1032
|
//#endregion
|
|
@@ -1225,6 +1225,7 @@ declare class Engine {
|
|
|
1225
1225
|
private isRunning;
|
|
1226
1226
|
private boundResizeHandler;
|
|
1227
1227
|
private groupOrder?;
|
|
1228
|
+
private smoothedTooltipPos;
|
|
1228
1229
|
constructor(canvas: HTMLCanvasElement, options?: EngineOptions);
|
|
1229
1230
|
/**
|
|
1230
1231
|
* Handle window resize event
|
|
@@ -1239,6 +1240,17 @@ declare class Engine {
|
|
|
1239
1240
|
* This allows changing visibility/behavior without resetting the simulation
|
|
1240
1241
|
*/
|
|
1241
1242
|
updateNodeStates(callback: (node: GraphNode) => NodeState): void;
|
|
1243
|
+
/**
|
|
1244
|
+
* Set target positions for elastic force (tree layout, etc.)
|
|
1245
|
+
* Writes positions into the "original positions" GPU buffer that ElasticPass reads from,
|
|
1246
|
+
* without touching current positions or node states.
|
|
1247
|
+
* Must be called AFTER applyPreset() since updateNodeStates() overwrites original positions.
|
|
1248
|
+
*/
|
|
1249
|
+
setTargetPositions(targets: Map<string, {
|
|
1250
|
+
x: number;
|
|
1251
|
+
y: number;
|
|
1252
|
+
z: number;
|
|
1253
|
+
}>): void;
|
|
1242
1254
|
/**
|
|
1243
1255
|
* Reheat the simulation
|
|
1244
1256
|
*/
|
|
@@ -1289,13 +1301,16 @@ declare class Engine {
|
|
|
1289
1301
|
getNodePosition(nodeId: string): THREE.Vector3 | null;
|
|
1290
1302
|
/**
|
|
1291
1303
|
* Get node screen position from world position
|
|
1304
|
+
* Returns position and visibility info (whether node is on screen and in front of camera)
|
|
1292
1305
|
*/
|
|
1293
1306
|
getNodeScreenPosition(nodeId: string): {
|
|
1294
1307
|
x: number;
|
|
1295
1308
|
y: number;
|
|
1309
|
+
visible: boolean;
|
|
1296
1310
|
} | null;
|
|
1297
1311
|
/**
|
|
1298
1312
|
* Update sticky tooltip position to follow node during camera movement
|
|
1313
|
+
* Uses lerp smoothing to filter out micro-jitter from force simulation
|
|
1299
1314
|
*/
|
|
1300
1315
|
private updateStickyTooltipPosition;
|
|
1301
1316
|
/**
|
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
|
-
|
|
471
|
-
|
|
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
|
|
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();
|
|
@@ -699,7 +686,7 @@ var InteractionManager = class {
|
|
|
699
686
|
const linkRenderer = this.graphScene.getLinkRenderer();
|
|
700
687
|
const nodeRenderer = this.graphScene.getNodeRenderer();
|
|
701
688
|
if (this.searchHighlightIds.length > 0) {
|
|
702
|
-
linkRenderer?.
|
|
689
|
+
linkRenderer?.highlightConnectedToNodes(new Set(this.searchHighlightIds), .05);
|
|
703
690
|
nodeRenderer?.highlight(this.searchHighlightIds);
|
|
704
691
|
} else {
|
|
705
692
|
linkRenderer?.clearHighlights(.05);
|
|
@@ -741,7 +728,7 @@ var InteractionManager = class {
|
|
|
741
728
|
const linkRenderer = this.graphScene.getLinkRenderer();
|
|
742
729
|
const nodeRenderer = this.graphScene.getNodeRenderer();
|
|
743
730
|
if (this.searchHighlightIds.length > 0) {
|
|
744
|
-
linkRenderer?.
|
|
731
|
+
linkRenderer?.highlightConnectedToNodes(new Set(this.searchHighlightIds), .05);
|
|
745
732
|
nodeRenderer?.highlight(this.searchHighlightIds);
|
|
746
733
|
} else {
|
|
747
734
|
linkRenderer?.clearHighlights(.05);
|
|
@@ -789,7 +776,7 @@ var InteractionManager = class {
|
|
|
789
776
|
nodeRenderer?.highlight(connectedIds);
|
|
790
777
|
}
|
|
791
778
|
} else if (this.searchHighlightIds.length > 0) {
|
|
792
|
-
linkRenderer?.
|
|
779
|
+
linkRenderer?.highlightConnectedToNodes(new Set(this.searchHighlightIds), .05);
|
|
793
780
|
nodeRenderer?.highlight(this.searchHighlightIds);
|
|
794
781
|
} else {
|
|
795
782
|
linkRenderer?.clearHighlights(.05);
|
|
@@ -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: .
|
|
1440
|
+
noiseStrength: .1,
|
|
1454
1441
|
defaultAlpha: .02,
|
|
1455
1442
|
highlightAlpha: .8,
|
|
1456
1443
|
dimmedAlpha: .02,
|
|
@@ -1545,6 +1532,26 @@ var LinksRenderer = class {
|
|
|
1545
1532
|
this.linkOpacity.setTargets(targets);
|
|
1546
1533
|
}
|
|
1547
1534
|
/**
|
|
1535
|
+
* Highlight links connected to any of the given nodes
|
|
1536
|
+
*/
|
|
1537
|
+
highlightConnectedToNodes(nodeIds, step = .01) {
|
|
1538
|
+
if (!this.lines || !this.linkOpacity) return;
|
|
1539
|
+
const linkCount = this.lines.geometry instanceof THREE.InstancedBufferGeometry ? this.lines.geometry.instanceCount : 0;
|
|
1540
|
+
if (linkCount === 0 || linkCount === void 0) return;
|
|
1541
|
+
const targets = new Array(linkCount).fill(this.params.dimmedAlpha);
|
|
1542
|
+
let hasConnections = false;
|
|
1543
|
+
for (const [linkId, instanceIndex] of this.linkIndexMap.entries()) {
|
|
1544
|
+
const [sourceId, targetId] = linkId.split("-");
|
|
1545
|
+
if (nodeIds.has(sourceId) || nodeIds.has(targetId)) {
|
|
1546
|
+
targets[instanceIndex] = this.params.defaultAlpha;
|
|
1547
|
+
hasConnections = true;
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
if (!hasConnections) return;
|
|
1551
|
+
this.linkOpacity.setStep(step);
|
|
1552
|
+
this.linkOpacity.setTargets(targets);
|
|
1553
|
+
}
|
|
1554
|
+
/**
|
|
1548
1555
|
* Clear all highlights (return to defaultAlpha)
|
|
1549
1556
|
*/
|
|
1550
1557
|
clearHighlights(step = .1) {
|
|
@@ -1636,7 +1643,13 @@ var LinksRenderer = class {
|
|
|
1636
1643
|
/**
|
|
1637
1644
|
* Create link geometry with interpolated segments
|
|
1638
1645
|
*/
|
|
1639
|
-
createLinkGeometry(links, nodes, nodeTextureSize
|
|
1646
|
+
createLinkGeometry(links, nodes, nodeTextureSize, groupOrder = [
|
|
1647
|
+
"root",
|
|
1648
|
+
"series",
|
|
1649
|
+
"artwork",
|
|
1650
|
+
"exhibition",
|
|
1651
|
+
"media"
|
|
1652
|
+
]) {
|
|
1640
1653
|
const linkIndexMap = /* @__PURE__ */ new Map();
|
|
1641
1654
|
const nodeMap = /* @__PURE__ */ new Map();
|
|
1642
1655
|
nodes.forEach((node) => nodeMap.set(node.id, node));
|
|
@@ -1664,6 +1677,11 @@ var LinksRenderer = class {
|
|
|
1664
1677
|
const instanceColorA = new Float32Array(links.length * 3);
|
|
1665
1678
|
const instanceColorB = new Float32Array(links.length * 3);
|
|
1666
1679
|
const instanceAlphaIndex = new Float32Array(links.length);
|
|
1680
|
+
const instanceRank = new Float32Array(links.length);
|
|
1681
|
+
const categoryRankMap = /* @__PURE__ */ new Map();
|
|
1682
|
+
groupOrder.forEach((g, i) => categoryRankMap.set(g, i));
|
|
1683
|
+
let nextRank = groupOrder.length;
|
|
1684
|
+
const maxRank = Math.max(groupOrder.length - 1, 1);
|
|
1667
1685
|
links.forEach((link, i) => {
|
|
1668
1686
|
const sourceId = link.source && typeof link.source === "object" ? link.source.id : link.source;
|
|
1669
1687
|
const targetId = link.target && typeof link.target === "object" ? link.target.id : link.target;
|
|
@@ -1698,6 +1716,9 @@ var LinksRenderer = class {
|
|
|
1698
1716
|
instanceColorB[i * 3 + 1] = tgtColor.g;
|
|
1699
1717
|
instanceColorB[i * 3 + 2] = tgtColor.b;
|
|
1700
1718
|
instanceAlphaIndex[i] = i;
|
|
1719
|
+
const targetCategory = targetNode?.category ?? "";
|
|
1720
|
+
if (targetCategory && !categoryRankMap.has(targetCategory)) categoryRankMap.set(targetCategory, nextRank++);
|
|
1721
|
+
instanceRank[i] = 1 - .75 * ((categoryRankMap.get(targetCategory) ?? 0) / maxRank);
|
|
1701
1722
|
const linkId = `${sourceId}-${targetId}`;
|
|
1702
1723
|
linkIndexMap.set(linkId, i);
|
|
1703
1724
|
});
|
|
@@ -1706,6 +1727,7 @@ var LinksRenderer = class {
|
|
|
1706
1727
|
geometry.setAttribute("instanceColorA", new THREE.InstancedBufferAttribute(instanceColorA, 3));
|
|
1707
1728
|
geometry.setAttribute("instanceColorB", new THREE.InstancedBufferAttribute(instanceColorB, 3));
|
|
1708
1729
|
geometry.setAttribute("instanceAlphaIndex", new THREE.InstancedBufferAttribute(instanceAlphaIndex, 1));
|
|
1730
|
+
geometry.setAttribute("instanceRank", new THREE.InstancedBufferAttribute(instanceRank, 1));
|
|
1709
1731
|
return {
|
|
1710
1732
|
geometry,
|
|
1711
1733
|
linkIndexMap
|
|
@@ -1780,10 +1802,8 @@ var LinksRenderer = class {
|
|
|
1780
1802
|
const category = nodeCategories[tIdx] || "";
|
|
1781
1803
|
if (category && !categoryRankMap.has(category)) categoryRankMap.set(category, nextRank++);
|
|
1782
1804
|
const rank = categoryRankMap.get(category) ?? 0;
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
linkPropertiesData[di] = strength;
|
|
1786
|
-
linkPropertiesData[di + 1] = distance;
|
|
1805
|
+
1 / (rank + 1);
|
|
1806
|
+
.3 ** rank;
|
|
1787
1807
|
}
|
|
1788
1808
|
nodeCursor[tIdx] = offset + 1;
|
|
1789
1809
|
}
|
|
@@ -2235,7 +2255,8 @@ var GraphScene = class {
|
|
|
2235
2255
|
canvas,
|
|
2236
2256
|
antialias: true,
|
|
2237
2257
|
alpha: true,
|
|
2238
|
-
depth: true
|
|
2258
|
+
depth: true,
|
|
2259
|
+
logarithmicDepthBuffer: true
|
|
2239
2260
|
});
|
|
2240
2261
|
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
|
2241
2262
|
this.renderer.setSize(window.innerWidth, window.innerHeight);
|
|
@@ -3137,7 +3158,7 @@ var ForceSimulation = class {
|
|
|
3137
3158
|
}
|
|
3138
3159
|
updateDrag(targetWorldPos) {
|
|
3139
3160
|
this.dragPass.updateDrag(targetWorldPos);
|
|
3140
|
-
this.reheat(.
|
|
3161
|
+
this.reheat(.1);
|
|
3141
3162
|
}
|
|
3142
3163
|
endDrag() {
|
|
3143
3164
|
this.dragPass.endDrag();
|
|
@@ -3850,6 +3871,7 @@ var Engine = class {
|
|
|
3850
3871
|
this.animationFrameId = null;
|
|
3851
3872
|
this.isRunning = false;
|
|
3852
3873
|
this.boundResizeHandler = null;
|
|
3874
|
+
this.smoothedTooltipPos = null;
|
|
3853
3875
|
this.animate = () => {
|
|
3854
3876
|
if (!this.isRunning) return;
|
|
3855
3877
|
const deltaTime = this.clock.update();
|
|
@@ -3940,6 +3962,33 @@ var Engine = class {
|
|
|
3940
3962
|
this.simulationBuffers.setOriginalPositions(newOriginalPositions);
|
|
3941
3963
|
}
|
|
3942
3964
|
/**
|
|
3965
|
+
* Set target positions for elastic force (tree layout, etc.)
|
|
3966
|
+
* Writes positions into the "original positions" GPU buffer that ElasticPass reads from,
|
|
3967
|
+
* without touching current positions or node states.
|
|
3968
|
+
* Must be called AFTER applyPreset() since updateNodeStates() overwrites original positions.
|
|
3969
|
+
*/
|
|
3970
|
+
setTargetPositions(targets) {
|
|
3971
|
+
if (!this.simulationBuffers.isReady()) return;
|
|
3972
|
+
const nodes = this.graphStore.getNodes();
|
|
3973
|
+
const currentPositions = this.simulationBuffers.readPositions();
|
|
3974
|
+
const newOriginalPositions = new Float32Array(nodes.length * 4);
|
|
3975
|
+
nodes.forEach((node, i) => {
|
|
3976
|
+
const target = targets.get(node.id);
|
|
3977
|
+
const state = currentPositions[i * 4 + 3];
|
|
3978
|
+
if (target) {
|
|
3979
|
+
newOriginalPositions[i * 4] = target.x;
|
|
3980
|
+
newOriginalPositions[i * 4 + 1] = target.y;
|
|
3981
|
+
newOriginalPositions[i * 4 + 2] = target.z;
|
|
3982
|
+
} else {
|
|
3983
|
+
newOriginalPositions[i * 4] = currentPositions[i * 4];
|
|
3984
|
+
newOriginalPositions[i * 4 + 1] = currentPositions[i * 4 + 1];
|
|
3985
|
+
newOriginalPositions[i * 4 + 2] = currentPositions[i * 4 + 2];
|
|
3986
|
+
}
|
|
3987
|
+
newOriginalPositions[i * 4 + 3] = state;
|
|
3988
|
+
});
|
|
3989
|
+
this.simulationBuffers.setOriginalPositions(newOriginalPositions);
|
|
3990
|
+
}
|
|
3991
|
+
/**
|
|
3943
3992
|
* Reheat the simulation
|
|
3944
3993
|
*/
|
|
3945
3994
|
reheat(alpha = 1) {
|
|
@@ -3992,13 +4041,20 @@ var Engine = class {
|
|
|
3992
4041
|
this.forceSimulation.reheat();
|
|
3993
4042
|
}
|
|
3994
4043
|
}
|
|
4044
|
+
if (this.forceSimulation.config.enableRadial) {
|
|
4045
|
+
const rootCategories = preset.radial?.rootCategories ?? ["root"];
|
|
4046
|
+
this.computeRadialDepths(rootCategories);
|
|
4047
|
+
}
|
|
3995
4048
|
if (preset.links) {
|
|
3996
4049
|
const linkRenderer = this.graphScene.getLinkRenderer();
|
|
3997
|
-
if (linkRenderer) linkRenderer.setOptions({
|
|
4050
|
+
if (linkRenderer) linkRenderer.setOptions({
|
|
4051
|
+
alpha: { default: preset.links.opacity },
|
|
4052
|
+
noiseStrength: preset.links.noiseStrength
|
|
4053
|
+
});
|
|
3998
4054
|
}
|
|
3999
4055
|
}
|
|
4000
4056
|
/**
|
|
4001
|
-
*
|
|
4057
|
+
*
|
|
4002
4058
|
* Start render loop
|
|
4003
4059
|
*/
|
|
4004
4060
|
start() {
|
|
@@ -4041,6 +4097,7 @@ var Engine = class {
|
|
|
4041
4097
|
highlightNodes(nodeIds) {
|
|
4042
4098
|
this.interactionManager.searchHighlightIds = nodeIds;
|
|
4043
4099
|
this.graphScene.getNodeRenderer()?.highlight(nodeIds);
|
|
4100
|
+
this.graphScene.getLinkRenderer()?.highlightConnectedToNodes(new Set(nodeIds));
|
|
4044
4101
|
}
|
|
4045
4102
|
/**
|
|
4046
4103
|
* Clear highlights
|
|
@@ -4048,6 +4105,7 @@ var Engine = class {
|
|
|
4048
4105
|
clearHighlights() {
|
|
4049
4106
|
this.interactionManager.searchHighlightIds = [];
|
|
4050
4107
|
this.graphScene.getNodeRenderer()?.clearHighlights();
|
|
4108
|
+
this.graphScene.getLinkRenderer()?.clearHighlights();
|
|
4051
4109
|
}
|
|
4052
4110
|
/**
|
|
4053
4111
|
* Get node position in 3D space
|
|
@@ -4061,6 +4119,7 @@ var Engine = class {
|
|
|
4061
4119
|
}
|
|
4062
4120
|
/**
|
|
4063
4121
|
* Get node screen position from world position
|
|
4122
|
+
* Returns position and visibility info (whether node is on screen and in front of camera)
|
|
4064
4123
|
*/
|
|
4065
4124
|
getNodeScreenPosition(nodeId) {
|
|
4066
4125
|
const pos = this.getNodePosition(nodeId);
|
|
@@ -4069,18 +4128,40 @@ var Engine = class {
|
|
|
4069
4128
|
const size = new THREE.Vector2();
|
|
4070
4129
|
this.graphScene.renderer.getSize(size);
|
|
4071
4130
|
const rect = this.graphScene.renderer.domElement.getBoundingClientRect();
|
|
4131
|
+
const x = (screenPos.x + 1) / 2 * size.x + rect.left;
|
|
4132
|
+
const y = -(screenPos.y - 1) / 2 * size.y + rect.top;
|
|
4133
|
+
const behindCamera = screenPos.z > 1;
|
|
4134
|
+
const outsideViewport = x < rect.left || x > rect.right || y < rect.top || y > rect.bottom;
|
|
4072
4135
|
return {
|
|
4073
|
-
x
|
|
4074
|
-
y
|
|
4136
|
+
x,
|
|
4137
|
+
y,
|
|
4138
|
+
visible: !behindCamera && !outsideViewport
|
|
4075
4139
|
};
|
|
4076
4140
|
}
|
|
4077
4141
|
/**
|
|
4078
4142
|
* Update sticky tooltip position to follow node during camera movement
|
|
4143
|
+
* Uses lerp smoothing to filter out micro-jitter from force simulation
|
|
4079
4144
|
*/
|
|
4080
4145
|
updateStickyTooltipPosition() {
|
|
4081
|
-
if (!this.interactionManager.isTooltipSticky || !this.interactionManager.stickyNodeId)
|
|
4146
|
+
if (!this.interactionManager.isTooltipSticky || !this.interactionManager.stickyNodeId) {
|
|
4147
|
+
this.smoothedTooltipPos = null;
|
|
4148
|
+
return;
|
|
4149
|
+
}
|
|
4082
4150
|
const screenPos = this.getNodeScreenPosition(this.interactionManager.stickyNodeId);
|
|
4083
|
-
if (screenPos)
|
|
4151
|
+
if (!screenPos) return;
|
|
4152
|
+
this.interactionManager.tooltipManager.setMainTooltipVisibility(screenPos.visible);
|
|
4153
|
+
if (!screenPos.visible) return;
|
|
4154
|
+
const targetX = screenPos.x;
|
|
4155
|
+
const targetY = screenPos.y;
|
|
4156
|
+
if (!this.smoothedTooltipPos || this.smoothedTooltipPos.nodeId !== this.interactionManager.stickyNodeId) this.smoothedTooltipPos = {
|
|
4157
|
+
x: targetX,
|
|
4158
|
+
y: targetY,
|
|
4159
|
+
nodeId: this.interactionManager.stickyNodeId
|
|
4160
|
+
};
|
|
4161
|
+
const lerpFactor = .35;
|
|
4162
|
+
this.smoothedTooltipPos.x += (targetX - this.smoothedTooltipPos.x) * lerpFactor;
|
|
4163
|
+
this.smoothedTooltipPos.y += (targetY - this.smoothedTooltipPos.y) * lerpFactor;
|
|
4164
|
+
this.interactionManager.tooltipManager.updateMainTooltipPos(this.smoothedTooltipPos.x, this.smoothedTooltipPos.y);
|
|
4084
4165
|
}
|
|
4085
4166
|
/**
|
|
4086
4167
|
* Programmatically select a node (highlight + tooltip)
|
|
@@ -4095,6 +4176,7 @@ var Engine = class {
|
|
|
4095
4176
|
const connectedIds = this.graphStore.getConnectedNodeIds(node.id);
|
|
4096
4177
|
connectedIds.push(node.id);
|
|
4097
4178
|
nodeRenderer?.highlight(connectedIds);
|
|
4179
|
+
this.smoothedTooltipPos = null;
|
|
4098
4180
|
const screenPos = this.getNodeScreenPosition(nodeId);
|
|
4099
4181
|
if (screenPos) this.interactionManager.tooltipManager.showFull(node, screenPos.x, screenPos.y);
|
|
4100
4182
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fusefactory/fuse-three-forcegraph",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
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
|
-
|
|
37
|
-
"build": "tsdown",
|
|
38
|
-
"watch": "tsdown --watch"
|
|
39
|
-
}
|
|
40
|
-
}
|
|
45
|
+
"homepage": "https://github.com/fusefactory/fuse-three-forcegraph#readme"
|
|
46
|
+
}
|