reagraph
Version:
WebGL Node-based Graph for React
1,666 lines • 164 kB
JavaScript
(function() {
"use strict";
try {
if (typeof document != "undefined") {
var elementStyle = document.createElement("style");
elementStyle.appendChild(document.createTextNode("._canvas_670zp_1 {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n}\n._container_155l7_1 {\n transform-origin: bottom right;\n overflow: hidden;\n position: absolute;\n border: solid 1px var(--radial-menu-border);\n}\n\n ._container_155l7_1._disabled_155l7_7 {\n opacity: 0.6;\n }\n\n ._container_155l7_1._disabled_155l7_7 ._contentContainer_155l7_10 {\n cursor: not-allowed;\n }\n\n ._container_155l7_1:not(._disabled_155l7_7) ._contentContainer_155l7_10 {\n cursor: pointer;\n }\n\n ._container_155l7_1:not(._disabled_155l7_7) ._contentContainer_155l7_10:hover {\n color: var(--radial-menu-active-color);\n background: var(--radial-menu-active-background);\n }\n\n ._container_155l7_1 ._contentContainer_155l7_10 {\n width: 200%;\n height: 200%;\n transform-origin: 50% 50%;\n border-radius: 50%;\n outline: none;\n transition: background 150ms ease-in-out;\n color: var(--radial-menu-color);\n }\n\n ._container_155l7_1 ._contentContainer_155l7_10 ._contentInner_155l7_35 {\n position: absolute;\n width: 100%;\n text-align: center;\n }\n\n ._container_155l7_1 ._contentContainer_155l7_10 ._contentInner_155l7_35 ._content_155l7_10 {\n display: inline-block;\n }\n\n ._container_155l7_1 svg {\n margin: 0 auto;\n fill: var(--radial-menu-active-color);\n height: 25px;\n width: 25px;\n display: block;\n }\n._container_x9hyx_1 {\n border-radius: 50%;\n z-index: 9;\n position: relative;\n height: 175px;\n width: 175px;\n border: solid 5px var(--radial-menu-border);\n overflow: hidden;\n background: var(--radial-menu-background);\n}\n\n ._container_x9hyx_1:before {\n content: ' ';\n background: var(--radial-menu-border);\n border-radius: 50%;\n height: 25px;\n width: 25px;\n position: absolute;\n z-index: 9;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n }"));
document.head.appendChild(elementStyle);
}
} catch (e) {
console.error("vite-plugin-css-injected-by-js", e);
}
})();
import { jsxs, jsx, Fragment } from "react/jsx-runtime";
import React, { createContext, useContext, useCallback, useRef, useEffect, useMemo, useState, useLayoutEffect, forwardRef, useImperativeHandle, Fragment as Fragment$1, Suspense } from "react";
import { useThree, useFrame, extend, Canvas } from "@react-three/fiber";
import { forceRadial as forceRadial$1, forceSimulation, forceX, forceY, forceCollide, forceManyBody, forceLink, forceCenter, forceZ } from "d3-force-3d";
import { treemap, hierarchy, stratify, tree } from "d3-hierarchy";
import circular from "graphology-layout/circular.js";
import noverlapLayout from "graphology-layout-noverlap";
import forceAtlas2Layout from "graphology-layout-forceatlas2";
import random from "graphology-layout/random.js";
import pagerank from "graphology-metrics/centrality/pagerank.js";
import { degreeCentrality } from "graphology-metrics/centrality/degree.js";
import { scaleLinear } from "d3-scale";
import { create, useStore as useStore$1 } from "zustand";
import { useShallow } from "zustand/shallow";
import { Vector3, QuadraticBezierCurve3, LineCurve3, Color, Plane, Vector2, DoubleSide, ShaderMaterial, TubeGeometry, Euler, BoxGeometry, CylinderGeometry, Quaternion, BufferAttribute, Mesh, TextureLoader, LinearFilter, Box3, MathUtils, Raycaster, Sphere as Sphere$1, Spherical, Matrix4, Vector4, MOUSE, Scene } from "three";
import { bidirectional } from "graphology-shortest-path";
import Graph from "graphology";
import { useSpring, a } from "@react-spring/three";
import { Billboard, RoundedBox, Text, useCursor, Html, Svg as Svg$1 } from "@react-three/drei";
import ellipsize from "ellipsize";
import { useGesture } from "@use-gesture/react";
import { mergeBufferGeometries, SelectionBox } from "three-stdlib";
import ThreeCameraControls from "camera-controls";
import * as holdEvent from "hold-event";
import classNames from "classnames";
function tick(layout) {
return new Promise((resolve, _reject) => {
let stable;
function run() {
if (!stable) {
stable = layout.step();
run();
} else {
resolve(stable);
}
}
run();
});
}
function buildNodeEdges(graph) {
const nodes = [];
const edges = [];
graph.forEachNode((id, n) => {
nodes.push({
...n,
id,
// This is for the clustering
radius: n.size || 1
});
});
graph.forEachEdge((id, l) => {
edges.push({ ...l, id });
});
return { nodes, edges };
}
function concentricLayout({
graph,
radius = 40,
drags,
getNodePosition,
concentricSpacing = 100
}) {
const { nodes, edges } = buildNodeEdges(graph);
const layout = {};
const getNodesInLevel = (level) => {
const circumference = 2 * Math.PI * (radius + level * concentricSpacing);
const minNodeSpacing = 40;
return Math.floor(circumference / minNodeSpacing);
};
const fixedLevelMap = /* @__PURE__ */ new Map();
const dynamicNodes = [];
for (const node of nodes) {
const data = graph.getNodeAttribute(node.id, "data");
const level = data?.level;
if (typeof level === "number" && level >= 0) {
if (!fixedLevelMap.has(level)) {
fixedLevelMap.set(level, []);
}
fixedLevelMap.get(level).push(node.id);
} else {
dynamicNodes.push({ id: node.id, metric: graph.degree(node.id) });
}
}
dynamicNodes.sort((a2, b) => b.metric - a2.metric);
for (const [level, nodeIds] of fixedLevelMap.entries()) {
const count = nodeIds.length;
const r = radius + level * concentricSpacing;
for (let i2 = 0; i2 < count; i2++) {
const angle = 2 * Math.PI * i2 / count;
layout[nodeIds[i2]] = {
x: r * Math.cos(angle),
y: r * Math.sin(angle)
};
}
}
const occupiedLevels = new Set(fixedLevelMap.keys());
let dynamicLevel = 0;
let i = 0;
while (i < dynamicNodes.length) {
while (occupiedLevels.has(dynamicLevel)) {
dynamicLevel++;
}
const nodesInLevel = getNodesInLevel(dynamicLevel);
const r = radius + dynamicLevel * concentricSpacing;
for (let j = 0; j < nodesInLevel && i < dynamicNodes.length; j++) {
const angle = 2 * Math.PI * j / nodesInLevel;
layout[dynamicNodes[i].id] = {
x: r * Math.cos(angle),
y: r * Math.sin(angle)
};
i++;
}
dynamicLevel++;
}
return {
step() {
return true;
},
getNodePosition(id) {
if (getNodePosition) {
const pos = getNodePosition(id, { graph, drags, nodes, edges });
if (pos) return pos;
}
if (drags?.[id]?.position) {
return drags[id].position;
}
return layout[id];
}
};
}
function traverseGraph(nodes, nodeStack = []) {
const currentDepth = nodeStack.length;
for (const node of nodes) {
const idx = nodeStack.indexOf(node);
if (idx > -1) {
const loop = [...nodeStack.slice(idx), node].map((d) => d.data.id);
throw new Error(
`Invalid Graph: Circular node path detected: ${loop.join(" -> ")}.`
);
}
if (currentDepth > node.depth) {
node.depth = currentDepth;
traverseGraph(node.out, [...nodeStack, node]);
}
}
}
function getNodeDepth(nodes, links) {
let invalid = false;
const graph = nodes.reduce(
(acc, cur) => ({
...acc,
[cur.id]: {
data: cur,
out: [],
depth: -1,
ins: []
}
}),
{}
);
try {
for (const link of links) {
const from = link.source;
const to = link.target;
if (!graph.hasOwnProperty(from)) {
throw new Error(`Missing source Node ${from}`);
}
if (!graph.hasOwnProperty(to)) {
throw new Error(`Missing target Node ${to}`);
}
const sourceNode = graph[from];
const targetNode = graph[to];
targetNode.ins.push(sourceNode);
sourceNode.out.push(targetNode);
}
traverseGraph(Object.values(graph));
} catch (e) {
invalid = true;
}
const allDepths = Object.keys(graph).map((id) => graph[id].depth);
const maxDepth = Math.max(...allDepths);
return {
invalid,
depths: graph,
maxDepth: maxDepth || 1
};
}
const RADIALS = ["radialin", "radialout"];
function forceRadial({
nodes,
edges,
mode = "lr",
nodeLevelRatio = 2
}) {
const { depths, maxDepth, invalid } = getNodeDepth(nodes, edges);
if (invalid) {
return null;
}
const modeDistance = RADIALS.includes(mode) ? 1 : 5;
const dagLevelDistance = nodes.length / maxDepth * nodeLevelRatio * modeDistance;
if (mode) {
const getFFn = (fix, invert) => (node) => !fix ? void 0 : (depths[node.id].depth - maxDepth / 2) * dagLevelDistance * (invert ? -1 : 1);
const fxFn = getFFn(["lr", "rl"].includes(mode), mode === "rl");
const fyFn = getFFn(["td", "bu"].includes(mode), mode === "td");
const fzFn = getFFn(["zin", "zout"].includes(mode), mode === "zout");
nodes.forEach((node) => {
node.fx = fxFn(node);
node.fy = fyFn(node);
node.fz = fzFn(node);
});
}
return RADIALS.includes(mode) ? forceRadial$1((node) => {
const nodeDepth = depths[node.id];
const depth = mode === "radialin" ? maxDepth - nodeDepth.depth : nodeDepth.depth;
return depth * dagLevelDistance;
}).strength(1) : null;
}
function forceInABox() {
const constant = (_) => () => _;
const index = (d) => d.index;
let id = index;
let nodes = [];
let links = [];
let clusters;
let tree2;
let size = [100, 100];
let forceNodeSize = constant(1);
let forceCharge = constant(-1);
let forceLinkDistance = constant(100);
let forceLinkStrength = constant(0.1);
let foci = {};
let linkStrengthIntraCluster = 0.1;
let linkStrengthInterCluster = 1e-3;
let templateNodes = [];
let offset = [0, 0];
let templateForce;
let groupBy = (d) => d.cluster;
let template = "treemap";
let enableGrouping = true;
let strength = 0.1;
function force(alpha) {
if (!enableGrouping) {
return force;
}
if (template === "force") {
templateForce.tick();
getFocisFromTemplate();
}
for (let i = 0, n = nodes.length, node, k = alpha * strength; i < n; ++i) {
node = nodes[i];
node.vx += (foci[groupBy(node)].x - node.x) * k;
node.vy += (foci[groupBy(node)].y - node.y) * k;
}
}
function initialize() {
if (!nodes) {
return;
}
if (template === "treemap") {
initializeWithTreemap();
} else {
initializeWithForce();
}
}
force.initialize = function(_) {
nodes = _;
initialize();
};
function getLinkKey(l) {
let sourceID = groupBy(l.source), targetID = groupBy(l.target);
return sourceID <= targetID ? sourceID + "~" + targetID : targetID + "~" + sourceID;
}
function computeClustersNodeCounts(nodes2) {
let clustersCounts = /* @__PURE__ */ new Map(), tmpCount = {};
nodes2.forEach(function(d) {
if (!clustersCounts.has(groupBy(d))) {
clustersCounts.set(groupBy(d), { count: 0, sumforceNodeSize: 0 });
}
});
nodes2.forEach(function(d) {
tmpCount = clustersCounts.get(groupBy(d));
tmpCount.count = tmpCount.count + 1;
tmpCount.sumforceNodeSize = tmpCount.sumforceNodeSize + // @ts-ignore
Math.PI * (forceNodeSize(d) * forceNodeSize(d)) * 1.3;
clustersCounts.set(groupBy(d), tmpCount);
});
return clustersCounts;
}
function computeClustersLinkCounts(links2) {
let dClusterLinks = /* @__PURE__ */ new Map(), clusterLinks = [];
links2.forEach(function(l) {
let key = getLinkKey(l), count;
if (dClusterLinks.has(key)) {
count = dClusterLinks.get(key);
} else {
count = 0;
}
count += 1;
dClusterLinks.set(key, count);
});
dClusterLinks.forEach(function(value, key) {
let source, target;
source = key.split("~")[0];
target = key.split("~")[1];
if (source !== void 0 && target !== void 0) {
clusterLinks.push({
source,
target,
count: value
});
}
});
return clusterLinks;
}
function getGroupsGraph() {
let gnodes = [];
let glinks = [];
let dNodes = /* @__PURE__ */ new Map();
let c;
let i;
let cc;
let clustersCounts;
let clustersLinks;
clustersCounts = computeClustersNodeCounts(nodes);
clustersLinks = computeClustersLinkCounts(links);
for (c of clustersCounts.keys()) {
cc = clustersCounts.get(c);
gnodes.push({
id: c,
size: cc.count,
r: Math.sqrt(cc.sumforceNodeSize / Math.PI)
});
dNodes.set(c, i);
}
clustersLinks.forEach(function(l) {
let source = dNodes.get(l.source), target = dNodes.get(l.target);
if (source !== void 0 && target !== void 0) {
glinks.push({
source,
target,
count: l.count
});
}
});
return { nodes: gnodes, links: glinks };
}
function getGroupsTree() {
let children = [];
let c;
let cc;
let clustersCounts;
clustersCounts = computeClustersNodeCounts(force.nodes());
for (c of clustersCounts.keys()) {
cc = clustersCounts.get(c);
children.push({ id: c, size: cc.count });
}
return { id: "clustersTree", children };
}
function getFocisFromTemplate() {
foci.none = { x: 0, y: 0 };
templateNodes.forEach(function(d) {
if (template === "treemap") {
foci[d.data.id] = {
x: d.x0 + (d.x1 - d.x0) / 2 - offset[0],
y: d.y0 + (d.y1 - d.y0) / 2 - offset[1]
};
} else {
foci[d.id] = {
x: d.x - offset[0],
y: d.y - offset[1]
};
}
});
return foci;
}
function initializeWithTreemap() {
let sim = treemap().size(force.size());
tree2 = hierarchy(getGroupsTree()).sum((d) => d.radius).sort(function(a2, b) {
return b.height - a2.height || b.value - a2.value;
});
templateNodes = sim(tree2).leaves();
getFocisFromTemplate();
}
function checkLinksAsObjects() {
let linkCount = 0;
if (nodes.length === 0) return;
links.forEach(function(link) {
let source, target;
if (!nodes) {
return;
}
source = link.source;
target = link.target;
if (typeof link.source !== "object") {
source = nodes.find((n) => n.id === link.source);
}
if (typeof link.target !== "object") {
target = nodes.find((n) => n.id === link.target);
}
if (source === void 0 || target === void 0) {
throw Error(
"Error setting links, couldnt find nodes for a link (see it on the console)"
);
}
link.source = source;
link.target = target;
link.index = linkCount++;
});
}
function initializeWithForce() {
let net;
if (!nodes || !nodes.length) {
return;
}
checkLinksAsObjects();
net = getGroupsGraph();
if (clusters.size > 0) {
net.nodes.forEach((n) => {
n.fx = clusters.get(n.id)?.position?.x;
n.fy = clusters.get(n.id)?.position?.y;
});
}
templateForce = forceSimulation(net.nodes).force("x", forceX(size[0] / 2).strength(0.1)).force("y", forceY(size[1] / 2).strength(0.1)).force("collide", forceCollide((d) => d.r).iterations(4)).force("charge", forceManyBody().strength(forceCharge)).force(
"links",
forceLink(net.nodes.length ? net.links : []).distance(forceLinkDistance).strength(forceLinkStrength)
);
templateNodes = templateForce.nodes();
getFocisFromTemplate();
}
force.template = function(x) {
if (!arguments.length) {
return template;
}
template = x;
initialize();
return force;
};
force.groupBy = function(x) {
if (!arguments.length) {
return groupBy;
}
if (typeof x === "string") {
groupBy = function(d) {
return d[x];
};
return force;
}
groupBy = x;
return force;
};
force.enableGrouping = function(x) {
if (!arguments.length) {
return enableGrouping;
}
enableGrouping = x;
return force;
};
force.strength = function(x) {
if (!arguments.length) {
return strength;
}
strength = x;
return force;
};
force.getLinkStrength = function(e) {
if (enableGrouping) {
if (groupBy(e.source) === groupBy(e.target)) {
if (typeof linkStrengthIntraCluster === "function") {
return linkStrengthIntraCluster(e);
} else {
return linkStrengthIntraCluster;
}
} else {
if (typeof linkStrengthInterCluster === "function") {
return linkStrengthInterCluster(e);
} else {
return linkStrengthInterCluster;
}
}
} else {
if (typeof linkStrengthIntraCluster === "function") {
return linkStrengthIntraCluster(e);
} else {
return linkStrengthIntraCluster;
}
}
};
force.id = function(_) {
return arguments.length ? (id = _, force) : id;
};
force.size = function(_) {
return arguments.length ? (size = _, force) : size;
};
force.linkStrengthInterCluster = function(_) {
return arguments.length ? (linkStrengthInterCluster = _, force) : linkStrengthInterCluster;
};
force.linkStrengthIntraCluster = function(_) {
return arguments.length ? (linkStrengthIntraCluster = _, force) : linkStrengthIntraCluster;
};
force.nodes = function(_) {
return arguments.length ? (nodes = _, force) : nodes;
};
force.links = function(_) {
if (!arguments.length) {
return links;
}
if (_ === null) {
links = [];
} else {
links = _;
}
initialize();
return force;
};
force.template = function(x) {
if (!arguments.length) {
return template;
}
template = x;
initialize();
return force;
};
force.forceNodeSize = function(_) {
return arguments.length ? (forceNodeSize = typeof _ === "function" ? _ : constant(+_), initialize(), force) : forceNodeSize;
};
force.nodeSize = force.forceNodeSize;
force.forceCharge = function(_) {
return arguments.length ? (forceCharge = typeof _ === "function" ? _ : constant(+_), initialize(), force) : forceCharge;
};
force.forceLinkDistance = function(_) {
return arguments.length ? (forceLinkDistance = typeof _ === "function" ? _ : constant(+_), initialize(), force) : forceLinkDistance;
};
force.forceLinkStrength = function(_) {
return arguments.length ? (forceLinkStrength = typeof _ === "function" ? _ : constant(+_), initialize(), force) : forceLinkStrength;
};
force.offset = function(_) {
return arguments.length ? (offset = typeof _ === "function" ? _ : constant(+_), force) : offset;
};
force.getFocis = getFocisFromTemplate;
force.setClusters = function(value) {
clusters = value;
return force;
};
return force;
}
function forceDirected({
graph,
nodeLevelRatio = 2,
mode = null,
dimensions = 2,
nodeStrength = -250,
linkDistance = 50,
clusterStrength = 0.5,
linkStrengthInterCluster = 0.01,
linkStrengthIntraCluster = 0.5,
forceLinkDistance = 100,
forceLinkStrength = 0.1,
clusterType = "force",
forceCharge = -700,
getNodePosition,
drags,
clusters,
clusterAttribute,
forceLayout
}) {
const { nodes, edges } = buildNodeEdges(graph);
const is2d = dimensions === 2;
const nodeStrengthAdjustment = is2d && edges.length > 25 ? nodeStrength * 2 : nodeStrength;
let forceX$1;
let forceY$1;
if (forceLayout === "forceDirected2d") {
forceX$1 = forceX();
forceY$1 = forceY();
} else {
forceX$1 = forceX(600).strength(0.05);
forceY$1 = forceY(600).strength(0.05);
}
const sim = forceSimulation().force("center", forceCenter(0, 0)).force("link", forceLink()).force("charge", forceManyBody().strength(nodeStrengthAdjustment)).force("x", forceX$1).force("y", forceY$1).force("z", forceZ()).force(
"collide",
forceCollide((d) => d.radius + 10)
).force(
"dagRadial",
forceRadial({
nodes,
edges,
mode,
nodeLevelRatio
})
).stop();
let groupingForce;
if (clusterAttribute) {
let forceChargeAdjustment = forceCharge;
if (nodes?.length) {
const adjustmentFactor = Math.ceil(nodes.length / 200);
forceChargeAdjustment = forceCharge * adjustmentFactor;
}
groupingForce = forceInABox().setClusters(clusters).strength(clusterStrength).template(clusterType).groupBy((d) => d.data[clusterAttribute]).links(edges).size([100, 100]).linkStrengthInterCluster(linkStrengthInterCluster).linkStrengthIntraCluster(linkStrengthIntraCluster).forceLinkDistance(forceLinkDistance).forceLinkStrength(forceLinkStrength).forceCharge(forceChargeAdjustment).forceNodeSize((d) => d.radius);
}
let layout = sim.numDimensions(dimensions).nodes(nodes);
if (groupingForce) {
layout = layout.force("group", groupingForce);
}
if (linkDistance) {
let linkForce = layout.force("link");
if (linkForce) {
linkForce.id((d) => d.id).links(edges).distance(linkDistance);
if (groupingForce) {
linkForce = linkForce.strength(groupingForce?.getLinkStrength ?? 0.1);
}
}
}
const nodeMap = new Map(nodes.map((n) => [n.id, n]));
return {
step() {
while (sim.alpha() > 0.01) {
sim.tick();
}
return true;
},
getNodePosition(id) {
if (getNodePosition) {
const pos = getNodePosition(id, { graph, drags, nodes, edges });
if (pos) {
return pos;
}
}
if (drags?.[id]?.position) {
return drags?.[id]?.position;
}
return nodeMap.get(id);
}
};
}
function circular2d({
graph,
radius,
drags,
getNodePosition
}) {
const layout = circular(graph, {
scale: radius
});
const { nodes, edges } = buildNodeEdges(graph);
return {
step() {
return true;
},
getNodePosition(id) {
if (getNodePosition) {
const pos = getNodePosition(id, { graph, drags, nodes, edges });
if (pos) {
return pos;
}
}
if (drags?.[id]?.position) {
return drags?.[id]?.position;
}
return layout?.[id];
}
};
}
const DIRECTION_MAP = {
td: {
x: "x",
y: "y",
factor: -1
},
lr: {
x: "y",
y: "x",
factor: 1
}
};
function hierarchical({
graph,
drags,
mode = "td",
nodeSeparation = 1,
nodeSize = [50, 50],
getNodePosition
}) {
const { nodes, edges } = buildNodeEdges(graph);
const { depths } = getNodeDepth(nodes, edges);
const rootNodes = Object.keys(depths).map((d) => depths[d]);
const root = stratify().id((d) => d.data.id).parentId((d) => d.ins?.[0]?.data?.id)(rootNodes);
const treeRoot = tree().separation(() => nodeSeparation).nodeSize(nodeSize)(hierarchy(root));
const treeNodes = treeRoot.descendants();
const path = DIRECTION_MAP[mode];
const mappedNodes = new Map(
nodes.map((n) => {
const { x, y } = treeNodes.find((t) => t.data.id === n.id);
return [
n.id,
{
...n,
[path.x]: x * path.factor,
[path.y]: y * path.factor,
z: 0
}
];
})
);
return {
step() {
return true;
},
getNodePosition(id) {
if (getNodePosition) {
const pos = getNodePosition(id, { graph, drags, nodes, edges });
if (pos) {
return pos;
}
}
if (drags?.[id]?.position) {
return drags?.[id]?.position;
}
return mappedNodes.get(id);
}
};
}
function nooverlap({
graph,
margin,
drags,
getNodePosition,
ratio,
gridSize,
maxIterations
}) {
const { nodes, edges } = buildNodeEdges(graph);
const layout = noverlapLayout(graph, {
maxIterations,
inputReducer: (_key, attr) => ({
...attr,
// Have to specify defaults for the engine
x: attr.x || 0,
y: attr.y || 0
}),
settings: {
ratio,
margin,
gridSize
}
});
return {
step() {
return true;
},
getNodePosition(id) {
if (getNodePosition) {
const pos = getNodePosition(id, { graph, drags, nodes, edges });
if (pos) {
return pos;
}
}
if (drags?.[id]?.position) {
return drags?.[id]?.position;
}
return layout?.[id];
}
};
}
function forceAtlas2({
graph,
drags,
iterations,
...rest
}) {
random.assign(graph);
const layout = forceAtlas2Layout(graph, {
iterations,
settings: rest
});
return {
step() {
return true;
},
getNodePosition(id) {
return drags?.[id]?.position || layout?.[id];
}
};
}
function custom({ graph, drags, getNodePosition }) {
const { nodes, edges } = buildNodeEdges(graph);
return {
step() {
return true;
},
getNodePosition(id) {
return getNodePosition(id, { graph, drags, nodes, edges });
}
};
}
const FORCE_LAYOUTS = [
"forceDirected2d",
"treeTd2d",
"treeLr2d",
"radialOut2d",
"treeTd3d",
"treeLr3d",
"radialOut3d",
"forceDirected3d"
];
function layoutProvider({
type,
...rest
}) {
if (FORCE_LAYOUTS.includes(type)) {
const { nodeStrength, linkDistance, nodeLevelRatio } = rest;
if (type === "forceDirected2d") {
return forceDirected({
...rest,
dimensions: 2,
nodeLevelRatio: nodeLevelRatio || 2,
nodeStrength: nodeStrength || -250,
linkDistance,
forceLayout: type
});
} else if (type === "treeTd2d") {
return forceDirected({
...rest,
mode: "td",
dimensions: 2,
nodeLevelRatio: nodeLevelRatio || 5,
nodeStrength: nodeStrength || -250,
linkDistance: linkDistance || 50,
forceLayout: type
});
} else if (type === "treeLr2d") {
return forceDirected({
...rest,
mode: "lr",
dimensions: 2,
nodeLevelRatio: nodeLevelRatio || 5,
nodeStrength: nodeStrength || -250,
linkDistance: linkDistance || 50,
forceLayout: type
});
} else if (type === "radialOut2d") {
return forceDirected({
...rest,
mode: "radialout",
dimensions: 2,
nodeLevelRatio: nodeLevelRatio || 5,
nodeStrength: nodeStrength || -500,
linkDistance: linkDistance || 100,
forceLayout: type
});
} else if (type === "treeTd3d") {
return forceDirected({
...rest,
mode: "td",
dimensions: 3,
nodeLevelRatio: nodeLevelRatio || 2,
nodeStrength: nodeStrength || -500,
linkDistance: linkDistance || 50
});
} else if (type === "treeLr3d") {
return forceDirected({
...rest,
mode: "lr",
dimensions: 3,
nodeLevelRatio: nodeLevelRatio || 2,
nodeStrength: nodeStrength || -500,
linkDistance: linkDistance || 50,
forceLayout: type
});
} else if (type === "radialOut3d") {
return forceDirected({
...rest,
mode: "radialout",
dimensions: 3,
nodeLevelRatio: nodeLevelRatio || 2,
nodeStrength: nodeStrength || -500,
linkDistance: linkDistance || 100,
forceLayout: type
});
} else if (type === "forceDirected3d") {
return forceDirected({
...rest,
dimensions: 3,
nodeLevelRatio: nodeLevelRatio || 2,
nodeStrength: nodeStrength || -250,
linkDistance,
forceLayout: type
});
}
} else if (type === "circular2d") {
const { radius } = rest;
return circular2d({
...rest,
radius: radius || 300
});
} else if (type === "concentric2d") {
return concentricLayout(rest);
} else if (type === "hierarchicalTd") {
return hierarchical({ ...rest, mode: "td" });
} else if (type === "hierarchicalLr") {
return hierarchical({ ...rest, mode: "lr" });
} else if (type === "nooverlap") {
const { graph, maxIterations, ratio, margin, gridSize, ...settings } = rest;
return nooverlap({
graph,
margin: margin || 10,
maxIterations: maxIterations || 50,
ratio: ratio || 10,
gridSize: gridSize || 20,
...settings
});
} else if (type === "forceatlas2") {
const { graph, iterations, gravity, scalingRatio, ...settings } = rest;
return forceAtlas2({
type: "forceatlas2",
graph,
...settings,
scalingRatio: scalingRatio || 100,
gravity: gravity || 10,
iterations: iterations || 50
});
} else if (type === "custom") {
return custom({
...rest
});
}
throw new Error(`Layout ${type} not found.`);
}
function recommendLayout(nodes, edges) {
const { invalid } = getNodeDepth(nodes, edges);
const nodeCount = nodes.length;
if (!invalid) {
if (nodeCount > 100) {
return "radialOut2d";
} else {
return "treeTd2d";
}
}
return "forceDirected2d";
}
function calcLabelVisibility({
nodePosition,
labelType,
camera
}) {
return (shape, size) => {
const isAlwaysVisible = labelType === "all" || labelType === "nodes" && shape === "node" || labelType === "edges" && shape === "edge";
if (!isAlwaysVisible && camera && nodePosition && camera?.position?.z / camera?.zoom - nodePosition?.z > 6e3) {
return false;
}
if (isAlwaysVisible) {
return true;
} else if (labelType === "auto" && shape === "node") {
if (size > 7) {
return true;
} else if (camera && nodePosition && camera.position.z / camera.zoom - nodePosition.z < 3e3) {
return true;
}
}
return false;
};
}
function getLabelOffsetByType(offset, position) {
switch (position) {
case "above":
return offset;
case "below":
return -offset;
case "inline":
case "natural":
default:
return 0;
}
}
const isServerRender = typeof window === "undefined";
function pageRankSizing({
graph
}) {
const ranks = pagerank(graph);
return {
ranks,
getSizeForNode: (nodeID) => ranks[nodeID] * 80
};
}
function centralitySizing({
graph
}) {
const ranks = degreeCentrality(graph);
return {
ranks,
getSizeForNode: (nodeID) => ranks[nodeID] * 20
};
}
function attributeSizing({
graph,
attribute,
defaultSize
}) {
const map = /* @__PURE__ */ new Map();
if (attribute) {
graph.forEachNode((id, node) => {
const size = node.data?.[attribute];
if (isNaN(size)) {
console.warn(`Attribute ${size} is not a number for node ${node.id}`);
}
map.set(id, size || 0);
});
} else {
console.warn("Attribute sizing configured but no attribute provided");
}
return {
getSizeForNode: (nodeId) => {
if (!attribute || !map) {
return defaultSize;
}
return map.get(nodeId);
}
};
}
const providers = {
pagerank: pageRankSizing,
centrality: centralitySizing,
attribute: attributeSizing,
none: ({ defaultSize }) => ({
getSizeForNode: (_id) => defaultSize
})
};
function nodeSizeProvider({ type, ...rest }) {
const provider = providers[type]?.(rest);
if (!provider && type !== "default") {
throw new Error(`Unknown sizing strategy: ${type}`);
}
const { graph, minSize, maxSize } = rest;
const sizes = /* @__PURE__ */ new Map();
let min;
let max;
graph.forEachNode((id, node) => {
let size;
if (type === "default") {
size = node.size || rest.defaultSize;
} else {
size = provider.getSizeForNode(id);
}
if (min === void 0 || size < min) {
min = size;
}
if (max === void 0 || size > max) {
max = size;
}
sizes.set(id, size);
});
if (type !== "none") {
const scale = scaleLinear().domain([min, max]).rangeRound([minSize, maxSize]);
for (const [nodeId, size] of sizes) {
sizes.set(nodeId, scale(size));
}
}
return sizes;
}
function buildGraph(graph, nodes, edges) {
graph.clear();
const addedNodes = /* @__PURE__ */ new Set();
for (const node of nodes) {
try {
if (!addedNodes.has(node.id)) {
graph.addNode(node.id, node);
addedNodes.add(node.id);
}
} catch (e) {
console.error(`[Graph] Error adding node '${node.id}`, e);
}
}
for (const edge of edges) {
if (!addedNodes.has(edge.source) || !addedNodes.has(edge.target)) {
continue;
}
try {
graph.addEdge(edge.source, edge.target, edge);
} catch (e) {
console.error(
`[Graph] Error adding edge '${edge.source} -> ${edge.target}`,
e
);
}
}
return graph;
}
function transformGraph({
graph,
layout,
sizingType,
labelType,
sizingAttribute,
defaultNodeSize,
minNodeSize,
maxNodeSize,
clusterAttribute
}) {
const nodes = [];
const edges = [];
const map = /* @__PURE__ */ new Map();
const sizes = nodeSizeProvider({
graph,
type: sizingType,
attribute: sizingAttribute,
minSize: minNodeSize,
maxSize: maxNodeSize,
defaultSize: defaultNodeSize
});
graph.nodes().length;
const checkVisibility = calcLabelVisibility({ labelType });
graph.forEachNode((id, node) => {
const position = layout.getNodePosition(id);
const { data, fill, icon, label, size, ...rest } = node;
const nodeSize = sizes.get(node.id);
const labelVisible = checkVisibility("node", nodeSize);
const nodeLinks = graph.inboundNeighbors(node.id) || [];
const parents = nodeLinks.map((n2) => graph.getNodeAttributes(n2));
const n = {
...node,
size: nodeSize,
labelVisible,
label,
icon,
fill,
cluster: clusterAttribute ? data[clusterAttribute] : void 0,
parents,
data: {
...rest,
...data ?? {}
},
position: {
...position,
x: position.x || 0,
y: position.y || 0,
z: position.z || 1
}
};
map.set(node.id, n);
nodes.push(n);
});
graph.forEachEdge((_id, link) => {
const from = map.get(link.source);
const to = map.get(link.target);
if (from && to) {
const { data, id, label, size, ...rest } = link;
const labelVisible = checkVisibility("edge", size);
edges.push({
...link,
id,
label,
labelVisible,
size,
data: {
...rest,
id,
...data || {}
}
});
}
});
return {
nodes,
edges
};
}
const animationConfig = {
mass: 10,
tension: 1e3,
friction: 300,
// Decreasing precision to improve performance from 0.00001
precision: 0.1
};
function getArrowVectors(placement, curve, arrowLength) {
const curveLength = curve.getLength();
const absSize = placement === "end" ? curveLength : curveLength / 2;
const offset = placement === "end" ? arrowLength / 2 : 0;
const u = (absSize - offset) / curveLength;
const position = curve.getPointAt(u);
const rotation = curve.getTangentAt(u);
return [position, rotation];
}
function getArrowSize(size) {
return [size + 6, 2 + size / 1.5];
}
const MULTI_EDGE_OFFSET_FACTOR = 0.7;
function getMidPoint(from, to, offset = 0) {
const fromVector = new Vector3(from.x, from.y || 0, from.z || 0);
const toVector = new Vector3(to.x, to.y || 0, to.z || 0);
const midVector = new Vector3().addVectors(fromVector, toVector).divideScalar(2);
return midVector.setLength(midVector.length() + offset);
}
function getCurvePoints(from, to, offset = -1) {
const fromVector = from.clone();
const toVector = to.clone();
const v = new Vector3().subVectors(toVector, fromVector);
const vlen = v.length();
const vn = v.clone().normalize();
const vv = new Vector3().subVectors(toVector, fromVector).divideScalar(2);
const k = Math.abs(vn.x) % 1;
const b = new Vector3(-vn.y, vn.x - k * vn.z, k * vn.y).normalize();
const vm = new Vector3().add(fromVector).add(vv).add(b.multiplyScalar(vlen / 4).multiplyScalar(offset));
return [from, vm, to];
}
function getCurve(from, fromOffset, to, toOffset, curved, curveOffset) {
const offsetFrom = getPointBetween(from, to, fromOffset);
const offsetTo = getPointBetween(to, from, toOffset);
return curved ? new QuadraticBezierCurve3(
...getCurvePoints(offsetFrom, offsetTo, curveOffset)
) : new LineCurve3(offsetFrom, offsetTo);
}
function getVector(node) {
return new Vector3(node.position.x, node.position.y, node.position.z || 0);
}
function getPointBetween(from, to, offset) {
const distance = from.distanceTo(to);
return from.clone().add(
to.clone().sub(from).multiplyScalar(offset / distance)
);
}
function updateNodePosition(node, offset) {
return {
...node,
position: {
...node.position,
x: node.position.x + offset.x,
y: node.position.y + offset.y,
z: node.position.z + offset.z
}
};
}
function calculateEdgeCurveOffset({ edge, edges, curved }) {
let updatedCurved = curved;
let curveOffset;
const parallelEdges = edges.filter((e) => e.target === edge.target && e.source === edge.source).map((e) => e.id);
if (parallelEdges.length > 1) {
updatedCurved = true;
const edgeIndex = parallelEdges.indexOf(edge.id);
const offsetMultiplier = edgeIndex === 0 ? 1 : 1 + edgeIndex * 0.8;
const side = edgeIndex % 2 === 0 ? 1 : -1;
const magnitude = MULTI_EDGE_OFFSET_FACTOR * offsetMultiplier;
curveOffset = side * magnitude;
}
if (edge.data?.isAggregated && edges.length > 1) {
const edgeIndex = parallelEdges.indexOf(edge.id);
return {
curved: true,
curveOffset: edgeIndex === 0 ? MULTI_EDGE_OFFSET_FACTOR : -MULTI_EDGE_OFFSET_FACTOR
};
}
return { curved: updatedCurved, curveOffset };
}
function calculateSubLabelOffset(fromPosition, toPosition, subLabelPlacement) {
const dx = toPosition.x - fromPosition.x;
const dy = toPosition.y - fromPosition.y;
const angle = Math.atan2(dy, dx);
const perpAngle = subLabelPlacement === "above" ? dx >= 0 ? angle + Math.PI / 2 : angle - Math.PI / 2 : dx >= 0 ? angle - Math.PI / 2 : angle + Math.PI / 2;
const offsetDistance = 7;
const offsetX = Math.cos(perpAngle) * offsetDistance;
const offsetY = Math.sin(perpAngle) * offsetDistance;
return { x: offsetX, y: offsetY, z: 0 };
}
function getLayoutCenter(nodes) {
let minX = Number.POSITIVE_INFINITY;
let maxX = Number.NEGATIVE_INFINITY;
let minY = Number.POSITIVE_INFINITY;
let maxY = Number.NEGATIVE_INFINITY;
let minZ = Number.POSITIVE_INFINITY;
let maxZ = Number.NEGATIVE_INFINITY;
for (let node of nodes) {
minX = Math.min(minX, node.position.x);
maxX = Math.max(maxX, node.position.x);
minY = Math.min(minY, node.position.y);
maxY = Math.max(maxY, node.position.y);
minZ = Math.min(minZ, node.position.z);
maxZ = Math.max(maxZ, node.position.z);
}
return {
height: maxY - minY,
width: maxX - minX,
minX,
maxX,
minY,
maxY,
minZ,
maxZ,
x: (maxX + minX) / 2,
y: (maxY + minY) / 2,
z: (maxZ + minZ) / 2
};
}
function buildClusterGroups(nodes, clusterAttribute) {
if (!clusterAttribute) {
return /* @__PURE__ */ new Map();
}
return nodes.reduce((entryMap, e) => {
const val = e.data[clusterAttribute];
if (val) {
entryMap.set(val, [...entryMap.get(val) || [], e]);
}
return entryMap;
}, /* @__PURE__ */ new Map());
}
function calculateClusters({
nodes,
clusterAttribute
}) {
const result = /* @__PURE__ */ new Map();
if (clusterAttribute) {
const groups = buildClusterGroups(nodes, clusterAttribute);
for (const [key, nodes2] of groups) {
const position = getLayoutCenter(nodes2);
result.set(key, {
label: key,
nodes: nodes2,
position
});
}
}
return result;
}
function findPath(graph, source, target) {
return bidirectional(graph, source, target);
}
const isNotEditableElement = (element) => {
return element.tagName !== "INPUT" && element.tagName !== "SELECT" && element.tagName !== "TEXTAREA" && !element.isContentEditable;
};
const createStore = ({
actives = [],
selections = [],
collapsedNodeIds = [],
theme
}) => create((set) => ({
theme: {
...theme,
edge: {
...theme?.edge,
label: {
...theme?.edge?.label,
fontSize: theme?.edge?.label?.fontSize ?? 6
}
}
},
edges: [],
nodes: [],
collapsedNodeIds,
clusters: /* @__PURE__ */ new Map(),
panning: false,
draggingIds: [],
actives,
edgeContextMenus: /* @__PURE__ */ new Set(),
edgeMeshes: [],
selections,
hoveredNodeId: null,
drags: {},
graph: new Graph({ multi: true }),
setTheme: (theme2) => set((state) => ({ ...state, theme: theme2 })),
setClusters: (clusters) => set((state) => ({ ...state, clusters })),
setEdgeContextMenus: (edgeContextMenus) => set((state) => ({
...state,
edgeContextMenus
})),
setEdgeMeshes: (edgeMeshes) => set((state) => ({ ...state, edgeMeshes })),
setPanning: (panning) => set((state) => ({ ...state, panning })),
setDrags: (drags) => set((state) => ({ ...state, drags })),
addDraggingId: (id) => set((state) => ({ ...state, draggingIds: [...state.draggingIds, id] })),
removeDraggingId: (id) => set((state) => ({
...state,
draggingIds: state.draggingIds.filter((drag) => drag !== id)
})),
setActives: (actives2) => set((state) => ({ ...state, actives: actives2 })),
setSelections: (selections2) => set((state) => ({ ...state, selections: selections2 })),
setHoveredNodeId: (hoveredNodeId) => set((state) => ({ ...state, hoveredNodeId })),
setNodes: (nodes) => set((state) => ({
...state,
nodes,
centerPosition: getLayoutCenter(nodes)
})),
setEdges: (edges) => set((state) => ({ ...state, edges })),
setNodePosition: (id, position) => set((state) => {
const node = state.nodes.find((n) => n.id === id);
const originalVector = getVector(node);
const newVector = new Vector3(position.x, position.y, position.z);
const offset = newVector.sub(originalVector);
const nodes = [...state.nodes];
if (state.selections?.includes(id)) {
state.selections?.forEach((id2) => {
const node2 = state.nodes.find((n) => n.id === id2);
if (node2) {
const nodeIndex = state.nodes.indexOf(node2);
nodes[nodeIndex] = updateNodePosition(node2, offset);
}
});
} else {
const nodeIndex = state.nodes.indexOf(node);
nodes[nodeIndex] = updateNodePosition(node, offset);
}
return {
...state,
drags: {
...state.drags,
[id]: node
},
nodes
};
}),
setCollapsedNodeIds: (nodeIds = []) => set((state) => ({ ...state, collapsedNodeIds: nodeIds })),
// Update the position of a cluster with nodes inside it
setClusterPosition: (id, position) => set((state) => {
const clusters = new Map(state.clusters);
const cluster = clusters.get(id);
if (cluster) {
const oldPos = cluster.position;
const offset = new Vector3(
position.x - oldPos.x,
position.y - oldPos.y,
position.z - (oldPos.z ?? 0)
);
const nodes = [...state.nodes];
const drags = { ...state.drags };
nodes.forEach((node, index) => {
if (node.cluster === id) {
nodes[index] = {
...node,
position: {
...node.position,
x: node.position.x + offset.x,
y: node.position.y + offset.y,
z: node.position.z + (offset.z ?? 0)
}
};
drags[node.id] = node;
}
});
const clusterNodes = nodes.filter(
(node) => node.cluster === id
);
const newClusterPosition = getLayoutCenter(clusterNodes);
clusters.set(id, {
...cluster,
position: newClusterPosition
});
return {
...state,
drags: {
...drags,
[id]: cluster
},
clusters,
nodes
};
}
return state;
})
}));
const defaultStore = createStore({});
const StoreContext = isServerRender ? null : createContext(defaultStore);
const Provider = ({ children, store = defaultStore }) => {
if (isServerRender) {
return children;
}
return React.createElement(StoreContext.Provider, { value: store }, children);
};
const useStore = (selector) => {
const store = useContext(StoreContext);
return useStore$1(store, useShallow(selector));
};
function getHiddenChildren({
nodeId,
nodes,
edges,
currentHiddenNodes,
currentHiddenEdges
}) {
const hiddenNodes = [];
const hiddenEdges = [];
const curHiddenNodeIds = currentHiddenNodes.map((n) => n.id);
const curHiddenEdgeIds = currentHiddenEdges.map((e) => e.id);
const outboundEdges = edges.filter((l) => l.source === nodeId);
const outboundEdgeNodeIds = outboundEdges.map((l) => l.target);
hiddenEdges.push(...outboundEdges);
for (const outboundEdgeNodeId of outboundEdgeNodeIds) {
const incomingEdges = edges.filter(
(l) => l.target === outboundEdgeNodeId && l.source !== nodeId
);
let hideNode = false;
if (incomingEdges.length === 0) {
hideNode = true;
} else if (incomingEdges.length > 0 && !curHiddenNodeIds.includes(outboundEdgeNodeId)) {
const inboundNodeLinkIds = incomingEdges.map((l) => l.id);
if (inboundNodeLinkIds.every((i) => curHiddenEdgeIds.includes(i))) {
hideNode = true;
}
}
if (hideNode) {
const node = nodes.find((n) => n.id === outboundEdgeNodeId);
if (node) {
hiddenNodes.push(node);
}
const nested = getHiddenChildren({
nodeId: outboundEdgeNodeId,
nodes,
edges,
currentHiddenEdges: hiddenEdges,
currentHiddenNodes: hiddenNodes
});
hiddenEdges.push(...nested.hiddenEdges);
hiddenNodes.push(...nested.hiddenNodes);
}
}
const uniqueEdges = Object.values(
hiddenEdges.reduce(
(acc, next) => ({
...acc,
[next.id]: next
}),
{}
)
);
const uniqueNodes = Object.values(
hiddenNodes.reduce(
(acc, next) => ({
...acc,
[next.id]: next
}),
{}
)
);
return {
hiddenEdges: uniqueEdges,
hiddenNodes: uniqueNodes
};
}
const getVisibleEntities = ({
collapsedIds,
nodes,
edges
}) => {
const curHiddenNodes = [];
const curHiddenEdges = [];
for (const collapsedId of collapsedIds) {
const { hiddenEdges, hiddenNodes } = getHiddenChildren({
nodeId: collapsedId,
nodes,
edges,
currentHiddenEdges: curHiddenEdges,
currentHiddenNodes: curHiddenNodes
});
curHiddenNodes.push(...hiddenNodes);
curHiddenEdges.push(...hiddenEdges);
}
const hiddenNodeIds = curHiddenNodes.map((n) => n.id);
const hiddenEdgeIds = curHiddenEdges.map((e) => e.id);
const visibleNodes = nodes.filter((n) => !hiddenNodeIds.includes(n.id));
const visibleEdges = edges.filter((e) => !hiddenEdgeIds.includes(e.id));
return {
visibleNodes,
visibleEdges
};
};
const getExpandPath = ({
nodeId,
edges,
visibleEdgeIds
}) => {
const parentIds = [];
const inboundEdges = edges.filter((l) => l.target === nodeId);
const inboundEdgeIds = inboundEdges.map((e) => e.id);
const hasVisibleInboundEdge = inboundEdgeIds.some(
(id) => visibleEdgeIds.includes(id)
);
if (hasVisibleInboundEdge) {
return parentIds;
}
const inboundEdgeNodeIds = inboundEdges.map((l) => l.source);
let addedParent = false;
for (const inboundNodeId of inboundEdgeNodeIds) {
if (!addedParent) {
parentIds.push(
...[
inboundNodeId,
...getExpandPath({ nodeId: inboundNodeId, edges, visibleEdgeIds })
]
);
addedParent = true;
}
}
return parentIds;
};
const useCollapse = ({
collapsedNodeIds = [],
nodes = [],
edges = []
}) => {
const getIsCollapsed = useCallback(
(nodeId) => {
const { visibleNodes } = getVisibleEntities({
nodes,
edges,
collapsedIds: collapsedNodeIds
});
const visibleNodeIds = visibleNodes.map((n) => n.id);
return !visibleNodeIds.includes(nodeId);
},
[collapsedNodeIds, edges, nodes]
);
const getExpandPathIds = useCallback(
(nodeId) => {
const { visibleEdges } = getVisibleEntities({
nodes,
edges,
collapsedIds: collapsedNodeIds
});
const visibleEdgeIds = visibleEdges.map((e) => e.id);
return getExpandPath({ nodeId, edges, visibleEdgeIds });
},
[collapsedNodeIds, edges, nodes]
);
return {
getIsCollapsed,
getExpandPathIds
};
};
const useGraph = ({
layoutType,
sizingType,
labelType,
sizingAttribute,
clusterAttribute,
selections,
nodes,
edges,
actives,
collapsedNodeIds,
defaultNodeSize,
maxNodeSize,
minNodeSize,
layoutOverrides,
constrainDragging
}) => {
const graph = useStore((state) => state.graph);
const clusters = useStore((state) => state.clusters);
const storedNodes = useStore((state) => state.nodes);
const setClusters = useStore((state) => state.setClusters);
const stateCollapsedNodeIds = useStore((state) => state.collapsedNodeIds);
const setEdges = useStore((state) => state.setEdges);
const stateNodes = useStore((state) => state.nodes);
const setNodes = useStore((state) => state.setNodes);
const setSelections = useStore((state) => state.setSelections);
const setActives = useStore((state) => state.setActives);
const drags = useStore((state) => state.drags);
const setDrags = useStore((state) => state.setDrags);
const setCollapsedNodeIds = useStore((state) => state.setCollapsedNodeIds);
const layoutMounted = useRef(false);
const layout = useRef(null);
const camera = useThree((state) => s