@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 +20 -9
- package/dist/index.mjs +113 -53
- 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
|
|
@@ -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
|
-
|
|
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();
|
|
@@ -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,
|
|
@@ -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
|
-
|
|
1784
|
-
|
|
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(.
|
|
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({
|
|
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
|
|
4074
|
-
y
|
|
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)
|
|
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)
|
|
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.
|
|
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
|
-
|
|
37
|
-
"build": "tsdown",
|
|
38
|
-
"watch": "tsdown --watch"
|
|
39
|
-
}
|
|
40
|
-
}
|
|
45
|
+
"homepage": "https://github.com/fusefactory/fuse-three-forcegraph#readme"
|
|
46
|
+
}
|