@cocorof/graphier 0.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -16,7 +16,8 @@ const DEFAULT_STYLE = {
16
16
  labelThreshold: 0.8,
17
17
  showLabels: true,
18
18
  maxLabels: 150,
19
- edgeWidthScale: 1
19
+ edgeWidthScale: 1,
20
+ flySpeed: 1
20
21
  };
21
22
  const DEFAULT_LAYOUT = {
22
23
  type: "force-3d",
@@ -24,7 +25,8 @@ const DEFAULT_LAYOUT = {
24
25
  linkDistance: "auto",
25
26
  alphaDecay: "auto",
26
27
  velocityDecay: 0.4,
27
- settledThreshold: 5e-3
28
+ settledThreshold: 5e-3,
29
+ spreadFactor: "auto"
28
30
  };
29
31
  function hexToRgb(hex) {
30
32
  const c2 = hex.replace("#", "");
@@ -170,17 +172,26 @@ function resolveTheme(theme) {
170
172
  }
171
173
  return { nodeColor, nodeColorBright, linkColor, backgroundColor };
172
174
  }
175
+ function autoSpreadFactor(n) {
176
+ if (n <= 500) return 1;
177
+ if (n <= 2e3) return 1 + (n - 500) / 1500 * 0.3;
178
+ if (n <= 1e4) return 1.3 + (n - 2e3) / 8e3 * 0.5;
179
+ return Math.min(2.5, 1.8 + (n - 1e4) / 4e4 * 0.7);
180
+ }
173
181
  function resolveLayoutParams(nodeCount, config) {
174
182
  const n = nodeCount;
175
- const charge = (config == null ? void 0 : config.charge) !== "auto" && (config == null ? void 0 : config.charge) != null ? config.charge : n > 5e4 ? -80 : n > 1e4 ? -120 : -200;
176
- const distanceMax = n > 5e4 ? 1500 : n > 1e4 ? 2e3 : 3e3;
183
+ const spread = (config == null ? void 0 : config.spreadFactor) !== "auto" && (config == null ? void 0 : config.spreadFactor) != null ? config.spreadFactor : autoSpreadFactor(n);
184
+ const baseCharge = (config == null ? void 0 : config.charge) !== "auto" && (config == null ? void 0 : config.charge) != null ? config.charge : n > 5e4 ? -80 : n > 1e4 ? -120 : -200;
185
+ const charge = baseCharge * spread;
186
+ const distanceMax = (n > 5e4 ? 1500 : n > 1e4 ? 2e3 : 3e3) * spread;
177
187
  const theta = n > 2e4 ? 1.5 : n > 5e3 ? 1.2 : 0.9;
178
- const linkDistance = (config == null ? void 0 : config.linkDistance) !== "auto" && (config == null ? void 0 : config.linkDistance) != null ? config.linkDistance : n > 5e4 ? 120 : n > 1e4 ? 180 : 250;
188
+ const baseLinkDistance = (config == null ? void 0 : config.linkDistance) !== "auto" && (config == null ? void 0 : config.linkDistance) != null ? config.linkDistance : n > 5e4 ? 120 : n > 1e4 ? 180 : 250;
189
+ const linkDistance = baseLinkDistance * spread;
179
190
  const alphaDecay = (config == null ? void 0 : config.alphaDecay) !== "auto" && (config == null ? void 0 : config.alphaDecay) != null ? config.alphaDecay : n > 5e4 ? 0.04 : n > 1e4 ? 0.03 : 0.02;
180
191
  const velocityDecay = (config == null ? void 0 : config.velocityDecay) ?? 0.4;
181
192
  const settledThreshold = (config == null ? void 0 : config.settledThreshold) ?? 5e-3;
182
193
  const postEvery = n > 5e4 ? 5 : n > 1e4 ? 3 : 2;
183
- const initialRadius2 = 500 + Math.min(n, 1e4) * 0.1;
194
+ const initialRadius2 = (500 + Math.min(n, 1e4) * 0.1) * spread;
184
195
  return {
185
196
  charge,
186
197
  distanceMax,
@@ -1703,27 +1714,31 @@ function applyNodeHighlight(nodesMesh, nodes, selectedNodeId, highlightSet, them
1703
1714
  } else if (node.id === selectedNodeId) {
1704
1715
  tmpColor.set(brightColor);
1705
1716
  brightTmp.setRGB(1, 1, 1);
1706
- tmpColor.lerp(brightTmp, 0.5);
1717
+ tmpColor.lerp(brightTmp, 0.65);
1718
+ tmpColor.multiplyScalar(1.4);
1719
+ tmpColor.r = Math.min(tmpColor.r, 2);
1720
+ tmpColor.g = Math.min(tmpColor.g, 2);
1721
+ tmpColor.b = Math.min(tmpColor.b, 2);
1707
1722
  } else if (highlightSet.has(node.id)) {
1708
1723
  const hop = highlightSet.get(node.id);
1709
1724
  if (hop === 1) {
1710
- tmpColor.set(brightColor);
1725
+ tmpColor.set(brightColor).multiplyScalar(1.2);
1711
1726
  } else if (hop === 2) {
1712
1727
  tmpColor.set(baseColor);
1713
1728
  brightTmp.set(brightColor);
1714
- tmpColor.lerp(brightTmp, 0.4);
1729
+ tmpColor.lerp(brightTmp, 0.5);
1715
1730
  } else {
1716
- tmpColor.set(baseColor).multiplyScalar(0.7);
1731
+ tmpColor.set(baseColor).multiplyScalar(0.6);
1717
1732
  }
1718
1733
  } else {
1719
- tmpColor.set(baseColor).multiplyScalar(0.12);
1734
+ tmpColor.set(baseColor).multiplyScalar(0.04);
1720
1735
  }
1721
1736
  nodesMesh.setColorAt(i, tmpColor);
1722
1737
  }
1723
1738
  if (nodesMesh.instanceColor) nodesMesh.instanceColor.needsUpdate = true;
1724
1739
  const mat = nodesMesh.material;
1725
1740
  if ((_a = mat.uniforms) == null ? void 0 : _a.uGlowIntensity) {
1726
- mat.uniforms.uGlowIntensity.value = highlightSet ? 1.15 : 1;
1741
+ mat.uniforms.uGlowIntensity.value = highlightSet ? 1.5 : 1;
1727
1742
  }
1728
1743
  }
1729
1744
  function applyEdgeHighlight(edgesMesh, links, edgeNodeIndices, edgeLinkIndices, highlightSet, edgeOpacity, nodeCount, theme) {
@@ -1751,7 +1766,7 @@ function applyEdgeHighlight(edgesMesh, links, edgeNodeIndices, edgeLinkIndices,
1751
1766
  tmpColor.multiplyScalar(1.2);
1752
1767
  }
1753
1768
  } else {
1754
- tmpColor.setRGB(0.03, 0.03, 0.06);
1769
+ tmpColor.setRGB(0.01, 0.01, 0.02);
1755
1770
  }
1756
1771
  colorArr[i * 6 + 0] = tmpColor.r;
1757
1772
  colorArr[i * 6 + 1] = tmpColor.g;
@@ -1838,6 +1853,9 @@ function createLabelSystem(maxLabels) {
1838
1853
  }
1839
1854
  function defaultLabelText$1(node) {
1840
1855
  const label = node.label ?? node.id ?? "";
1856
+ if (node.type === "repo" && label.includes("/")) {
1857
+ return label.split("/").pop();
1858
+ }
1841
1859
  return label.length > 30 ? label.substring(0, 27) + "…" : label;
1842
1860
  }
1843
1861
  function getOrCreateTexture(cache, nodeId, text, color) {
@@ -1876,7 +1894,7 @@ function getOrCreateTexture(cache, nodeId, text, color) {
1876
1894
  }
1877
1895
  return entry;
1878
1896
  }
1879
- function updateLabels(state, nodes, positions, scales, camera, theme, showLabels, labelScale, labelThreshold, maxLabels, labelFormatter) {
1897
+ function updateLabels(state, nodes, positions, scales, camera, theme, showLabels, labelScale, labelThreshold, maxLabels, labelFormatter, highlightSet) {
1880
1898
  if (!positions || nodes.length === 0 || !showLabels) {
1881
1899
  for (const sp of state.sprites) sp.visible = false;
1882
1900
  return;
@@ -1890,6 +1908,7 @@ function updateLabels(state, nodes, positions, scales, camera, theme, showLabels
1890
1908
  const maxDistSq = maxDist * maxDist;
1891
1909
  const candidates = [];
1892
1910
  for (let i = 0; i < nc; i++) {
1911
+ if (highlightSet && !highlightSet.has(nodes[i].id)) continue;
1893
1912
  const dx = positions[i * 3] - camPos.x;
1894
1913
  const dy = positions[i * 3 + 1] - camPos.y;
1895
1914
  const dz = positions[i * 3 + 2] - camPos.z;
@@ -1935,58 +1954,49 @@ function disposeLabelSystem(state) {
1935
1954
  }
1936
1955
  state.textureCache.clear();
1937
1956
  }
1938
- const ACCEL = 12e-4;
1939
- const MAX_SPEED = 0.06;
1940
- const DAMPING = 0.88;
1941
- const ZOOM_FACTOR = 0.015;
1942
- const DEAD_ZONE = 1e-4;
1957
+ const ORBIT_ACCEL = 12e-4;
1958
+ const ORBIT_MAX_SPEED = 0.06;
1959
+ const ORBIT_DAMPING = 0.88;
1960
+ const ORBIT_ZOOM_FACTOR = 0.015;
1961
+ const ORBIT_DEAD_ZONE = 1e-4;
1962
+ const FLY_ACCEL = 6e-3;
1963
+ const FLY_FRICTION = 0.92;
1964
+ const FLY_MAX_THRUST = 0.18;
1965
+ const FLY_TURN_RATE = 8e-3;
1966
+ const FLY_DEAD_ZONE = 2e-4;
1943
1967
  const _offset = new THREE.Vector3();
1944
1968
  const _right = new THREE.Vector3();
1945
1969
  const _q = new THREE.Quaternion();
1946
- function setupKeyboardControls(camera, controls, container) {
1947
- const keys = {};
1970
+ const _dir = new THREE.Vector3();
1971
+ const _worldUp = new THREE.Vector3(0, 1, 0);
1972
+ function clamp(v, min, max) {
1973
+ return v < min ? min : v > max ? max : v;
1974
+ }
1975
+ function createOrbitUpdate(camera, controls, keys) {
1948
1976
  const vel = { zoom: 0, azimuth: 0, polar: 0 };
1949
- function onKeyDown2(e) {
1950
- var _a;
1951
- const tag = (_a = e.target) == null ? void 0 : _a.tagName;
1952
- if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return;
1953
- keys[e.key] = true;
1954
- }
1955
- function onKeyUp(e) {
1956
- keys[e.key] = false;
1957
- }
1958
- function onBlur() {
1959
- for (const k of Object.keys(keys)) keys[k] = false;
1960
- vel.zoom = 0;
1961
- vel.azimuth = 0;
1962
- vel.polar = 0;
1963
- }
1964
- container.addEventListener("keydown", onKeyDown2);
1965
- container.addEventListener("keyup", onKeyUp);
1966
- container.addEventListener("blur", onBlur);
1967
- function update() {
1968
- if (keys["z"] || keys["Z"]) vel.zoom -= ACCEL;
1969
- if (keys["x"] || keys["X"]) vel.zoom += ACCEL;
1970
- if (keys["ArrowLeft"]) vel.azimuth -= ACCEL;
1971
- if (keys["ArrowRight"]) vel.azimuth += ACCEL;
1972
- if (keys["ArrowUp"]) vel.polar -= ACCEL;
1973
- if (keys["ArrowDown"]) vel.polar += ACCEL;
1974
- vel.zoom = clamp(vel.zoom, -MAX_SPEED, MAX_SPEED);
1975
- vel.azimuth = clamp(vel.azimuth, -MAX_SPEED, MAX_SPEED);
1976
- vel.polar = clamp(vel.polar, -MAX_SPEED, MAX_SPEED);
1977
- vel.zoom *= DAMPING;
1978
- vel.azimuth *= DAMPING;
1979
- vel.polar *= DAMPING;
1980
- if (Math.abs(vel.zoom) < DEAD_ZONE) vel.zoom = 0;
1981
- if (Math.abs(vel.azimuth) < DEAD_ZONE) vel.azimuth = 0;
1982
- if (Math.abs(vel.polar) < DEAD_ZONE) vel.polar = 0;
1977
+ return function update() {
1978
+ if (keys["z"] || keys["Z"]) vel.zoom -= ORBIT_ACCEL;
1979
+ if (keys["x"] || keys["X"]) vel.zoom += ORBIT_ACCEL;
1980
+ if (keys["ArrowLeft"]) vel.azimuth -= ORBIT_ACCEL;
1981
+ if (keys["ArrowRight"]) vel.azimuth += ORBIT_ACCEL;
1982
+ if (keys["ArrowUp"]) vel.polar -= ORBIT_ACCEL;
1983
+ if (keys["ArrowDown"]) vel.polar += ORBIT_ACCEL;
1984
+ vel.zoom = clamp(vel.zoom, -ORBIT_MAX_SPEED, ORBIT_MAX_SPEED);
1985
+ vel.azimuth = clamp(vel.azimuth, -ORBIT_MAX_SPEED, ORBIT_MAX_SPEED);
1986
+ vel.polar = clamp(vel.polar, -ORBIT_MAX_SPEED, ORBIT_MAX_SPEED);
1987
+ vel.zoom *= ORBIT_DAMPING;
1988
+ vel.azimuth *= ORBIT_DAMPING;
1989
+ vel.polar *= ORBIT_DAMPING;
1990
+ if (Math.abs(vel.zoom) < ORBIT_DEAD_ZONE) vel.zoom = 0;
1991
+ if (Math.abs(vel.azimuth) < ORBIT_DEAD_ZONE) vel.azimuth = 0;
1992
+ if (Math.abs(vel.polar) < ORBIT_DEAD_ZONE) vel.polar = 0;
1983
1993
  const hasMotion = vel.zoom !== 0 || vel.azimuth !== 0 || vel.polar !== 0;
1984
1994
  if (!hasMotion) return false;
1985
1995
  const target = controls.target;
1986
1996
  _offset.copy(camera.position).sub(target);
1987
1997
  let radius = _offset.length();
1988
1998
  if (vel.zoom !== 0) {
1989
- radius *= 1 + vel.zoom * ZOOM_FACTOR * radius * 0.01;
1999
+ radius *= 1 + vel.zoom * ORBIT_ZOOM_FACTOR * radius * 0.01;
1990
2000
  radius = Math.max(controls.minDistance, Math.min(controls.maxDistance, radius));
1991
2001
  }
1992
2002
  if (vel.azimuth !== 0) {
@@ -2007,16 +2017,81 @@ function setupKeyboardControls(camera, controls, container) {
2007
2017
  camera.lookAt(target);
2008
2018
  controls.target.copy(target);
2009
2019
  return true;
2020
+ };
2021
+ }
2022
+ function createFlyUpdate(camera, controls, keys, state) {
2023
+ const vel = { thrust: 0, yaw: 0, pitch: 0 };
2024
+ return function update() {
2025
+ const speedMul = state.flySpeed;
2026
+ if (keys["z"] || keys["Z"]) vel.thrust += FLY_ACCEL * speedMul;
2027
+ if (keys["x"] || keys["X"]) vel.thrust -= FLY_ACCEL * speedMul;
2028
+ if (keys["a"] || keys["A"] || keys["ArrowLeft"]) vel.yaw += FLY_TURN_RATE;
2029
+ if (keys["d"] || keys["D"] || keys["ArrowRight"]) vel.yaw -= FLY_TURN_RATE;
2030
+ if (keys["w"] || keys["W"] || keys["ArrowUp"]) vel.pitch += FLY_TURN_RATE;
2031
+ if (keys["s"] || keys["S"] || keys["ArrowDown"]) vel.pitch -= FLY_TURN_RATE;
2032
+ const lookDist = camera.position.distanceTo(controls.target);
2033
+ const distScale = Math.max(0.3, lookDist * 3e-3);
2034
+ vel.thrust = clamp(vel.thrust, -FLY_MAX_THRUST * distScale * speedMul, FLY_MAX_THRUST * distScale * speedMul);
2035
+ vel.yaw = clamp(vel.yaw, -FLY_TURN_RATE * 2, FLY_TURN_RATE * 2);
2036
+ vel.pitch = clamp(vel.pitch, -FLY_TURN_RATE * 2, FLY_TURN_RATE * 2);
2037
+ vel.thrust *= FLY_FRICTION;
2038
+ vel.yaw *= FLY_FRICTION;
2039
+ vel.pitch *= FLY_FRICTION;
2040
+ if (Math.abs(vel.thrust) < FLY_DEAD_ZONE) vel.thrust = 0;
2041
+ if (Math.abs(vel.yaw) < FLY_DEAD_ZONE) vel.yaw = 0;
2042
+ if (Math.abs(vel.pitch) < FLY_DEAD_ZONE) vel.pitch = 0;
2043
+ const hasMotion = vel.thrust !== 0 || vel.yaw !== 0 || vel.pitch !== 0;
2044
+ if (!hasMotion) return false;
2045
+ if (vel.yaw !== 0) {
2046
+ const upDot = camera.up.dot(_worldUp);
2047
+ const yawSign = upDot < 0 ? -1 : 1;
2048
+ _q.setFromAxisAngle(_worldUp, vel.yaw * yawSign);
2049
+ camera.quaternion.premultiply(_q);
2050
+ camera.up.applyQuaternion(_q).normalize();
2051
+ }
2052
+ if (vel.pitch !== 0) {
2053
+ _right.setFromMatrixColumn(camera.matrixWorld, 0).normalize();
2054
+ _q.setFromAxisAngle(_right, vel.pitch);
2055
+ camera.quaternion.premultiply(_q);
2056
+ camera.up.applyQuaternion(_q).normalize();
2057
+ }
2058
+ if (vel.thrust !== 0) {
2059
+ camera.getWorldDirection(_dir);
2060
+ _dir.multiplyScalar(vel.thrust * distScale * 100);
2061
+ camera.position.add(_dir);
2062
+ controls.target.add(_dir);
2063
+ }
2064
+ return true;
2065
+ };
2066
+ }
2067
+ function setupKeyboardControls(camera, controls, container, mode = "fly", flySpeed = 1) {
2068
+ const keys = {};
2069
+ const state = { flySpeed };
2070
+ function onKeyDown2(e) {
2071
+ var _a;
2072
+ const tag = (_a = e.target) == null ? void 0 : _a.tagName;
2073
+ if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return;
2074
+ keys[e.key] = true;
2010
2075
  }
2076
+ function onKeyUp(e) {
2077
+ keys[e.key] = false;
2078
+ }
2079
+ function onBlur() {
2080
+ for (const k of Object.keys(keys)) keys[k] = false;
2081
+ }
2082
+ container.addEventListener("keydown", onKeyDown2);
2083
+ container.addEventListener("keyup", onKeyUp);
2084
+ container.addEventListener("blur", onBlur);
2085
+ const updateFn = mode === "fly" ? createFlyUpdate(camera, controls, keys, state) : createOrbitUpdate(camera, controls, keys);
2011
2086
  function cleanup() {
2012
2087
  container.removeEventListener("keydown", onKeyDown2);
2013
2088
  container.removeEventListener("keyup", onKeyUp);
2014
2089
  container.removeEventListener("blur", onBlur);
2015
2090
  }
2016
- return { update, cleanup };
2017
- }
2018
- function clamp(v, min, max) {
2019
- return v < min ? min : v > max ? max : v;
2091
+ function setFlySpeed(speed) {
2092
+ state.flySpeed = speed;
2093
+ }
2094
+ return { update: updateFn, cleanup, setFlySpeed };
2020
2095
  }
2021
2096
  function createScene(container, backgroundColor, style, rendererConfig) {
2022
2097
  const scene = new THREE.Scene();
@@ -2053,7 +2128,8 @@ function createScene(container, backgroundColor, style, rendererConfig) {
2053
2128
  scene.add(selectionRing);
2054
2129
  const labels = createLabelSystem(style.maxLabels);
2055
2130
  scene.add(labels.group);
2056
- const keyboard = setupKeyboardControls(camera, controls, container);
2131
+ const cameraMode = (rendererConfig == null ? void 0 : rendererConfig.cameraMode) ?? "fly";
2132
+ const keyboard = setupKeyboardControls(camera, controls, container, cameraMode, style.flySpeed ?? 1);
2057
2133
  return {
2058
2134
  scene,
2059
2135
  camera,
@@ -2392,21 +2468,136 @@ function zoomToFitPositions(camera, controls, positions, duration, padding) {
2392
2468
  duration
2393
2469
  );
2394
2470
  }
2395
- function setupInteraction(canvas, camera, controls, getNodesMesh, getNodes, callbacks, container) {
2471
+ const EDGE_HIT_THRESHOLD = 5;
2472
+ function setupInteraction(canvas, camera, controls, getNodesMesh, getNodes, callbacks, container, getEdgeData) {
2396
2473
  const raycaster = new THREE.Raycaster();
2397
2474
  const mouse = new THREE.Vector2();
2398
2475
  let mouseDownPos = null;
2399
2476
  let lastClickTime = 0;
2400
2477
  let lastClickNodeId = null;
2401
2478
  let singleClickTimer = null;
2479
+ let dragNode = null;
2480
+ let isDragging = false;
2481
+ const dragPlane = new THREE.Plane();
2482
+ const dragIntersect = new THREE.Vector3();
2402
2483
  function onPointerDown2(e) {
2403
2484
  mouseDownPos = { x: e.clientX, y: e.clientY };
2404
2485
  if (container && document.activeElement !== container) {
2405
2486
  container.focus({ preventScroll: true });
2406
2487
  }
2488
+ if (callbacks.onNodeDrag) {
2489
+ const nodesMesh = getNodesMesh();
2490
+ if (nodesMesh) {
2491
+ const rect = canvas.getBoundingClientRect();
2492
+ mouse.x = (e.clientX - rect.left) / rect.width * 2 - 1;
2493
+ mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
2494
+ raycaster.setFromCamera(mouse, camera);
2495
+ const hits = raycaster.intersectObject(nodesMesh);
2496
+ if (hits.length > 0 && hits[0].instanceId != null) {
2497
+ const nodes = getNodes();
2498
+ dragNode = nodes[hits[0].instanceId] ?? null;
2499
+ hits[0].instanceId;
2500
+ }
2501
+ }
2502
+ }
2503
+ }
2504
+ function onPointerMove2(e) {
2505
+ var _a, _b, _c;
2506
+ if (dragNode && mouseDownPos && callbacks.onNodeDrag) {
2507
+ const dx = Math.abs(e.clientX - mouseDownPos.x);
2508
+ const dy = Math.abs(e.clientY - mouseDownPos.y);
2509
+ if (dx + dy > 5) {
2510
+ isDragging = true;
2511
+ controls.enabled = false;
2512
+ const rect2 = canvas.getBoundingClientRect();
2513
+ mouse.x = (e.clientX - rect2.left) / rect2.width * 2 - 1;
2514
+ mouse.y = -((e.clientY - rect2.top) / rect2.height) * 2 + 1;
2515
+ const nodePos = new THREE.Vector3(
2516
+ dragNode.x ?? 0,
2517
+ dragNode.y ?? 0,
2518
+ dragNode.z ?? 0
2519
+ );
2520
+ dragPlane.setFromNormalAndCoplanarPoint(
2521
+ camera.getWorldDirection(new THREE.Vector3()).negate(),
2522
+ nodePos
2523
+ );
2524
+ raycaster.setFromCamera(mouse, camera);
2525
+ if (raycaster.ray.intersectPlane(dragPlane, dragIntersect)) {
2526
+ callbacks.onNodeDrag(dragNode, {
2527
+ x: dragIntersect.x,
2528
+ y: dragIntersect.y,
2529
+ z: dragIntersect.z
2530
+ });
2531
+ }
2532
+ return;
2533
+ }
2534
+ }
2535
+ if (!callbacks.onNodeHover && !callbacks.onLinkHover) return;
2536
+ const nodesMesh = getNodesMesh();
2537
+ if (!nodesMesh) return;
2538
+ const rect = canvas.getBoundingClientRect();
2539
+ mouse.x = (e.clientX - rect.left) / rect.width * 2 - 1;
2540
+ mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
2541
+ raycaster.setFromCamera(mouse, camera);
2542
+ const hits = raycaster.intersectObject(nodesMesh);
2543
+ if (hits.length > 0 && hits[0].instanceId != null) {
2544
+ const nodes = getNodes();
2545
+ const node = nodes[hits[0].instanceId];
2546
+ if (node && node.id !== lastHoveredId) {
2547
+ lastHoveredId = node.id;
2548
+ lastHoveredLinkIdx = -1;
2549
+ canvas.style.cursor = "pointer";
2550
+ (_a = callbacks.onNodeHover) == null ? void 0 : _a.call(callbacks, node);
2551
+ (_b = callbacks.onLinkHover) == null ? void 0 : _b.call(callbacks, null);
2552
+ }
2553
+ } else {
2554
+ if (lastHoveredId !== null) {
2555
+ lastHoveredId = null;
2556
+ canvas.style.cursor = "default";
2557
+ (_c = callbacks.onNodeHover) == null ? void 0 : _c.call(callbacks, null);
2558
+ }
2559
+ if (callbacks.onLinkHover && getEdgeData) {
2560
+ const edgeData = getEdgeData();
2561
+ if (edgeData == null ? void 0 : edgeData.positions) {
2562
+ const hitLink = findNearestEdge(
2563
+ e.clientX,
2564
+ e.clientY,
2565
+ rect,
2566
+ camera,
2567
+ edgeData
2568
+ );
2569
+ if (hitLink) {
2570
+ if (lastHoveredLinkIdx !== hitLink.index) {
2571
+ lastHoveredLinkIdx = hitLink.index;
2572
+ canvas.style.cursor = "pointer";
2573
+ callbacks.onLinkHover(hitLink.link);
2574
+ }
2575
+ } else if (lastHoveredLinkIdx !== -1) {
2576
+ lastHoveredLinkIdx = -1;
2577
+ canvas.style.cursor = "default";
2578
+ callbacks.onLinkHover(null);
2579
+ }
2580
+ }
2581
+ }
2582
+ }
2407
2583
  }
2408
2584
  function onPointerUp2(e) {
2409
2585
  var _a;
2586
+ if (isDragging && dragNode && callbacks.onNodeDragEnd) {
2587
+ callbacks.onNodeDragEnd(dragNode, {
2588
+ x: dragNode.x ?? 0,
2589
+ y: dragNode.y ?? 0,
2590
+ z: dragNode.z ?? 0
2591
+ });
2592
+ }
2593
+ if (isDragging) {
2594
+ isDragging = false;
2595
+ dragNode = null;
2596
+ controls.enabled = true;
2597
+ mouseDownPos = null;
2598
+ return;
2599
+ }
2600
+ dragNode = null;
2410
2601
  if (!mouseDownPos) return;
2411
2602
  const dx = Math.abs(e.clientX - mouseDownPos.x);
2412
2603
  const dy = Math.abs(e.clientY - mouseDownPos.y);
@@ -2454,6 +2645,22 @@ function setupInteraction(canvas, camera, controls, getNodesMesh, getNodes, call
2454
2645
  callbacks.onNodeClick(node);
2455
2646
  }, 250);
2456
2647
  } else {
2648
+ if (callbacks.onLinkClick && getEdgeData) {
2649
+ const edgeData = getEdgeData();
2650
+ if (edgeData == null ? void 0 : edgeData.positions) {
2651
+ const hitLink = findNearestEdge(
2652
+ e.clientX,
2653
+ e.clientY,
2654
+ rect,
2655
+ camera,
2656
+ edgeData
2657
+ );
2658
+ if (hitLink) {
2659
+ callbacks.onLinkClick(hitLink.link);
2660
+ return;
2661
+ }
2662
+ }
2663
+ }
2457
2664
  if (singleClickTimer) clearTimeout(singleClickTimer);
2458
2665
  singleClickTimer = null;
2459
2666
  lastClickTime = 0;
@@ -2461,13 +2668,9 @@ function setupInteraction(canvas, camera, controls, getNodesMesh, getNodes, call
2461
2668
  callbacks.onNodeClick(null);
2462
2669
  }
2463
2670
  }
2464
- const keyTarget = container ?? window;
2465
- function onKeyDown2(e) {
2466
- if (e.key === "Escape") callbacks.onNodeClick(null);
2467
- }
2468
- let lastHoveredId = null;
2469
- function onPointerMove2(e) {
2470
- if (!callbacks.onNodeHover) return;
2671
+ function onContextMenu2(e) {
2672
+ if (!callbacks.onContextMenu) return;
2673
+ e.preventDefault();
2471
2674
  const nodesMesh = getNodesMesh();
2472
2675
  if (!nodesMesh) return;
2473
2676
  const rect = canvas.getBoundingClientRect();
@@ -2478,20 +2681,21 @@ function setupInteraction(canvas, camera, controls, getNodesMesh, getNodes, call
2478
2681
  if (hits.length > 0 && hits[0].instanceId != null) {
2479
2682
  const nodes = getNodes();
2480
2683
  const node = nodes[hits[0].instanceId];
2481
- if (node && node.id !== lastHoveredId) {
2482
- lastHoveredId = node.id;
2483
- canvas.style.cursor = "pointer";
2484
- callbacks.onNodeHover(node);
2684
+ if (node) {
2685
+ callbacks.onContextMenu(node, { x: e.clientX, y: e.clientY });
2485
2686
  }
2486
- } else if (lastHoveredId !== null) {
2487
- lastHoveredId = null;
2488
- canvas.style.cursor = "default";
2489
- callbacks.onNodeHover(null);
2490
2687
  }
2491
2688
  }
2689
+ const keyTarget = container ?? window;
2690
+ function onKeyDown2(e) {
2691
+ if (e.key === "Escape") callbacks.onNodeClick(null);
2692
+ }
2693
+ let lastHoveredId = null;
2694
+ let lastHoveredLinkIdx = -1;
2492
2695
  canvas.addEventListener("pointerdown", onPointerDown2);
2493
2696
  canvas.addEventListener("pointerup", onPointerUp2);
2494
2697
  canvas.addEventListener("pointermove", onPointerMove2);
2698
+ canvas.addEventListener("contextmenu", onContextMenu2);
2495
2699
  keyTarget.addEventListener("keydown", onKeyDown2);
2496
2700
  return {
2497
2701
  cleanup: () => {
@@ -2499,10 +2703,48 @@ function setupInteraction(canvas, camera, controls, getNodesMesh, getNodes, call
2499
2703
  canvas.removeEventListener("pointerdown", onPointerDown2);
2500
2704
  canvas.removeEventListener("pointerup", onPointerUp2);
2501
2705
  canvas.removeEventListener("pointermove", onPointerMove2);
2706
+ canvas.removeEventListener("contextmenu", onContextMenu2);
2502
2707
  keyTarget.removeEventListener("keydown", onKeyDown2);
2503
2708
  }
2504
2709
  };
2505
2710
  }
2711
+ function findNearestEdge(clientX, clientY, rect, camera, edgeData) {
2712
+ const { links, edgeNodeIndices, edgeLinkIndices, positions } = edgeData;
2713
+ const screenX = clientX - rect.left;
2714
+ const screenY = clientY - rect.top;
2715
+ const w = rect.width;
2716
+ const h = rect.height;
2717
+ let bestDist = EDGE_HIT_THRESHOLD;
2718
+ let bestIdx = -1;
2719
+ const tmpVec = new THREE.Vector3();
2720
+ for (let i = 0; i < edgeNodeIndices.length; i++) {
2721
+ const [si, ti] = edgeNodeIndices[i];
2722
+ tmpVec.set(positions[si * 3], positions[si * 3 + 1], positions[si * 3 + 2]);
2723
+ tmpVec.project(camera);
2724
+ const sx1 = (tmpVec.x * 0.5 + 0.5) * w;
2725
+ const sy1 = (-tmpVec.y * 0.5 + 0.5) * h;
2726
+ tmpVec.set(positions[ti * 3], positions[ti * 3 + 1], positions[ti * 3 + 2]);
2727
+ tmpVec.project(camera);
2728
+ const sx2 = (tmpVec.x * 0.5 + 0.5) * w;
2729
+ const sy2 = (-tmpVec.y * 0.5 + 0.5) * h;
2730
+ const dist = pointToSegmentDist(screenX, screenY, sx1, sy1, sx2, sy2);
2731
+ if (dist < bestDist) {
2732
+ bestDist = dist;
2733
+ bestIdx = i;
2734
+ }
2735
+ }
2736
+ if (bestIdx === -1) return null;
2737
+ return { link: links[edgeLinkIndices[bestIdx]], index: bestIdx };
2738
+ }
2739
+ function pointToSegmentDist(px, py, ax, ay, bx, by) {
2740
+ const dx = bx - ax;
2741
+ const dy = by - ay;
2742
+ const lenSq = dx * dx + dy * dy;
2743
+ if (lenSq < 1e-4) return Math.hypot(px - ax, py - ay);
2744
+ let t = ((px - ax) * dx + (py - ay) * dy) / lenSq;
2745
+ t = Math.max(0, Math.min(1, t));
2746
+ return Math.hypot(px - (ax + t * dx), py - (ay + t * dy));
2747
+ }
2506
2748
  function disposeObject(obj) {
2507
2749
  if (!obj) return;
2508
2750
  const mesh = obj;
@@ -2524,6 +2766,13 @@ function NetworkGraph3DInner(props, ref) {
2524
2766
  onNodeClick,
2525
2767
  onNodeDoubleClick,
2526
2768
  onNodeHover,
2769
+ onContextMenu: onContextMenu2,
2770
+ onLinkClick,
2771
+ onLinkHover,
2772
+ onNodeDrag,
2773
+ onNodeDragEnd,
2774
+ onLayoutSettled,
2775
+ onLayoutTick,
2527
2776
  selectedNodeId = null,
2528
2777
  highlightHops = 3,
2529
2778
  theme: themeProp,
@@ -2556,13 +2805,28 @@ function NetworkGraph3DInner(props, ref) {
2556
2805
  const onNodeClickRef = useRef(onNodeClick);
2557
2806
  const onNodeDoubleClickRef = useRef(onNodeDoubleClick);
2558
2807
  const onNodeHoverRef = useRef(onNodeHover);
2808
+ const onContextMenuRef = useRef(onContextMenu2);
2809
+ const onLinkClickRef = useRef(onLinkClick);
2810
+ const onLinkHoverRef = useRef(onLinkHover);
2811
+ const onNodeDragRef = useRef(onNodeDrag);
2812
+ const onNodeDragEndRef = useRef(onNodeDragEnd);
2813
+ const onLayoutSettledRef = useRef(onLayoutSettled);
2814
+ const onLayoutTickRef = useRef(onLayoutTick);
2559
2815
  const styleRef = useRef(resolvedStyle);
2560
2816
  const themeRef = useRef(resolvedTheme);
2561
2817
  const selectedNodeIdRef = useRef(selectedNodeId);
2562
2818
  const labelFormatterRef = useRef(labelFormatter);
2819
+ const highlightSetRef = useRef(null);
2563
2820
  onNodeClickRef.current = onNodeClick;
2564
2821
  onNodeDoubleClickRef.current = onNodeDoubleClick;
2565
2822
  onNodeHoverRef.current = onNodeHover;
2823
+ onContextMenuRef.current = onContextMenu2;
2824
+ onLinkClickRef.current = onLinkClick;
2825
+ onLinkHoverRef.current = onLinkHover;
2826
+ onNodeDragRef.current = onNodeDrag;
2827
+ onNodeDragEndRef.current = onNodeDragEnd;
2828
+ onLayoutSettledRef.current = onLayoutSettled;
2829
+ onLayoutTickRef.current = onLayoutTick;
2566
2830
  styleRef.current = resolvedStyle;
2567
2831
  themeRef.current = resolvedTheme;
2568
2832
  selectedNodeIdRef.current = selectedNodeId;
@@ -2628,7 +2892,8 @@ function NetworkGraph3DInner(props, ref) {
2628
2892
  styleRef.current.labelScale,
2629
2893
  styleRef.current.labelThreshold,
2630
2894
  styleRef.current.maxLabels,
2631
- labelFormatterRef.current
2895
+ labelFormatterRef.current,
2896
+ highlightSetRef.current
2632
2897
  );
2633
2898
  });
2634
2899
  const resizeObserver = setupResize(container, state);
@@ -2653,9 +2918,52 @@ function NetworkGraph3DInner(props, ref) {
2653
2918
  onNodeHover: (node) => {
2654
2919
  var _a;
2655
2920
  return (_a = onNodeHoverRef.current) == null ? void 0 : _a.call(onNodeHoverRef, node);
2921
+ },
2922
+ onContextMenu: (node, pos) => {
2923
+ var _a;
2924
+ return (_a = onContextMenuRef.current) == null ? void 0 : _a.call(onContextMenuRef, node, pos);
2925
+ },
2926
+ onLinkClick: (link) => {
2927
+ var _a;
2928
+ return (_a = onLinkClickRef.current) == null ? void 0 : _a.call(onLinkClickRef, link);
2929
+ },
2930
+ onLinkHover: (link) => {
2931
+ var _a;
2932
+ return (_a = onLinkHoverRef.current) == null ? void 0 : _a.call(onLinkHoverRef, link);
2933
+ },
2934
+ onNodeDrag: (node, pos) => {
2935
+ var _a;
2936
+ (_a = onNodeDragRef.current) == null ? void 0 : _a.call(onNodeDragRef, node, pos);
2937
+ const d = dataRef.current;
2938
+ const idx = d.nodeIdToIndex.get(node.id);
2939
+ if (idx !== void 0 && d.positions) {
2940
+ d.positions[idx * 3] = pos.x;
2941
+ d.positions[idx * 3 + 1] = pos.y;
2942
+ d.positions[idx * 3 + 2] = pos.z;
2943
+ node.x = pos.x;
2944
+ node.y = pos.y;
2945
+ node.z = pos.z;
2946
+ const gObj = graphObjRef.current;
2947
+ if (gObj && d.scales) {
2948
+ updateNodePositions(gObj.nodesMesh, d.positions, d.scales, d.nodes.length);
2949
+ }
2950
+ }
2951
+ },
2952
+ onNodeDragEnd: (node, pos) => {
2953
+ var _a;
2954
+ return (_a = onNodeDragEndRef.current) == null ? void 0 : _a.call(onNodeDragEndRef, node, pos);
2656
2955
  }
2657
2956
  },
2658
- container
2957
+ container,
2958
+ () => {
2959
+ const d = dataRef.current;
2960
+ return {
2961
+ links: d.links,
2962
+ edgeNodeIndices: d.edgeNodeIndices,
2963
+ edgeLinkIndices: d.edgeLinkIndices,
2964
+ positions: d.positions
2965
+ };
2966
+ }
2659
2967
  );
2660
2968
  return () => {
2661
2969
  cancelAnim();
@@ -2795,6 +3103,7 @@ function NetworkGraph3DInner(props, ref) {
2795
3103
  console.error("Graphier: Layout worker error:", err);
2796
3104
  };
2797
3105
  worker.onmessage = (e) => {
3106
+ var _a, _b;
2798
3107
  const msg = e.data;
2799
3108
  if (msg.type === "positions") {
2800
3109
  const positions = new Float32Array(msg.positions);
@@ -2822,9 +3131,11 @@ function NetworkGraph3DInner(props, ref) {
2822
3131
  );
2823
3132
  }
2824
3133
  }
3134
+ (_a = onLayoutTickRef.current) == null ? void 0 : _a.call(onLayoutTickRef, msg.alpha);
2825
3135
  }
2826
3136
  if (msg.type === "settled") {
2827
3137
  dataRef.current.settled = true;
3138
+ (_b = onLayoutSettledRef.current) == null ? void 0 : _b.call(onLayoutSettledRef);
2828
3139
  }
2829
3140
  };
2830
3141
  const layoutParams = resolveLayoutParams(nc, layoutProp);
@@ -2869,6 +3180,7 @@ function NetworkGraph3DInner(props, ref) {
2869
3180
  const d = dataRef.current;
2870
3181
  const theme = themeRef.current;
2871
3182
  const style = styleRef.current;
3183
+ highlightSetRef.current = highlightSet;
2872
3184
  applyNodeHighlight(
2873
3185
  gObj.nodesMesh,
2874
3186
  d.nodes,
@@ -2886,6 +3198,18 @@ function NetworkGraph3DInner(props, ref) {
2886
3198
  d.nodes.length,
2887
3199
  theme
2888
3200
  );
3201
+ if (sceneState.bloomPass) {
3202
+ const style2 = styleRef.current;
3203
+ if (highlightSet) {
3204
+ sceneState.bloomPass.strength = style2.bloomStrength * 1.8;
3205
+ sceneState.bloomPass.radius = style2.bloomRadius * 1.4;
3206
+ sceneState.bloomPass.threshold = 0.25;
3207
+ } else {
3208
+ sceneState.bloomPass.strength = style2.bloomStrength;
3209
+ sceneState.bloomPass.radius = style2.bloomRadius;
3210
+ sceneState.bloomPass.threshold = style2.bloomThreshold;
3211
+ }
3212
+ }
2889
3213
  const selectedNode = selectedNodeId ? d.nodes.find((n) => n.id === selectedNodeId) : null;
2890
3214
  updateSelectionRing(
2891
3215
  sceneState.selectionRing,
@@ -2961,6 +3285,11 @@ function NetworkGraph3DInner(props, ref) {
2961
3285
  }, 30);
2962
3286
  return () => clearInterval(id);
2963
3287
  }, [resolvedStyle.autoOrbit]);
3288
+ useEffect(() => {
3289
+ const s = sceneRef.current;
3290
+ if (!s) return;
3291
+ s.keyboard.setFlySpeed(resolvedStyle.flySpeed);
3292
+ }, [resolvedStyle.flySpeed]);
2964
3293
  useImperativeHandle(ref, () => ({
2965
3294
  cameraPosition(pos, lookAt, duration = 1e3) {
2966
3295
  const s = sceneRef.current;
@@ -3033,13 +3362,21 @@ function NetworkGraph3DInner(props, ref) {
3033
3362
  var _a;
3034
3363
  return ((_a = sceneRef.current) == null ? void 0 : _a.camera) ?? null;
3035
3364
  },
3036
- async screenshot() {
3365
+ captureScreenshot() {
3037
3366
  const s = sceneRef.current;
3038
3367
  if (!s) return null;
3039
3368
  s.renderer.render(s.scene, s.camera);
3040
- return new Promise((resolve) => {
3041
- s.renderer.domElement.toBlob(resolve, "image/png");
3042
- });
3369
+ return s.renderer.domElement.toDataURL("image/png");
3370
+ },
3371
+ reheatLayout() {
3372
+ const gObj = graphObjRef.current;
3373
+ if (gObj == null ? void 0 : gObj.worker) {
3374
+ gObj.worker.postMessage({ type: "stop" });
3375
+ gObj.worker.terminate();
3376
+ gObj.worker = null;
3377
+ }
3378
+ dataRef.current.settled = false;
3379
+ setAppendedData((prev) => prev ? { ...prev } : { nodes: [], links: [] });
3043
3380
  }
3044
3381
  }));
3045
3382
  return /* @__PURE__ */ jsx(