@cocorof/graphier 0.1.0 → 1.1.1

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
@@ -1703,27 +1703,31 @@ function applyNodeHighlight(nodesMesh, nodes, selectedNodeId, highlightSet, them
1703
1703
  } else if (node.id === selectedNodeId) {
1704
1704
  tmpColor.set(brightColor);
1705
1705
  brightTmp.setRGB(1, 1, 1);
1706
- tmpColor.lerp(brightTmp, 0.5);
1706
+ tmpColor.lerp(brightTmp, 0.65);
1707
+ tmpColor.multiplyScalar(1.4);
1708
+ tmpColor.r = Math.min(tmpColor.r, 2);
1709
+ tmpColor.g = Math.min(tmpColor.g, 2);
1710
+ tmpColor.b = Math.min(tmpColor.b, 2);
1707
1711
  } else if (highlightSet.has(node.id)) {
1708
1712
  const hop = highlightSet.get(node.id);
1709
1713
  if (hop === 1) {
1710
- tmpColor.set(brightColor);
1714
+ tmpColor.set(brightColor).multiplyScalar(1.2);
1711
1715
  } else if (hop === 2) {
1712
1716
  tmpColor.set(baseColor);
1713
1717
  brightTmp.set(brightColor);
1714
- tmpColor.lerp(brightTmp, 0.4);
1718
+ tmpColor.lerp(brightTmp, 0.5);
1715
1719
  } else {
1716
- tmpColor.set(baseColor).multiplyScalar(0.7);
1720
+ tmpColor.set(baseColor).multiplyScalar(0.6);
1717
1721
  }
1718
1722
  } else {
1719
- tmpColor.set(baseColor).multiplyScalar(0.12);
1723
+ tmpColor.set(baseColor).multiplyScalar(0.04);
1720
1724
  }
1721
1725
  nodesMesh.setColorAt(i, tmpColor);
1722
1726
  }
1723
1727
  if (nodesMesh.instanceColor) nodesMesh.instanceColor.needsUpdate = true;
1724
1728
  const mat = nodesMesh.material;
1725
1729
  if ((_a = mat.uniforms) == null ? void 0 : _a.uGlowIntensity) {
1726
- mat.uniforms.uGlowIntensity.value = highlightSet ? 1.15 : 1;
1730
+ mat.uniforms.uGlowIntensity.value = highlightSet ? 1.5 : 1;
1727
1731
  }
1728
1732
  }
1729
1733
  function applyEdgeHighlight(edgesMesh, links, edgeNodeIndices, edgeLinkIndices, highlightSet, edgeOpacity, nodeCount, theme) {
@@ -1751,7 +1755,7 @@ function applyEdgeHighlight(edgesMesh, links, edgeNodeIndices, edgeLinkIndices,
1751
1755
  tmpColor.multiplyScalar(1.2);
1752
1756
  }
1753
1757
  } else {
1754
- tmpColor.setRGB(0.03, 0.03, 0.06);
1758
+ tmpColor.setRGB(0.01, 0.01, 0.02);
1755
1759
  }
1756
1760
  colorArr[i * 6 + 0] = tmpColor.r;
1757
1761
  colorArr[i * 6 + 1] = tmpColor.g;
@@ -1838,6 +1842,9 @@ function createLabelSystem(maxLabels) {
1838
1842
  }
1839
1843
  function defaultLabelText$1(node) {
1840
1844
  const label = node.label ?? node.id ?? "";
1845
+ if (node.type === "repo" && label.includes("/")) {
1846
+ return label.split("/").pop();
1847
+ }
1841
1848
  return label.length > 30 ? label.substring(0, 27) + "…" : label;
1842
1849
  }
1843
1850
  function getOrCreateTexture(cache, nodeId, text, color) {
@@ -1876,7 +1883,7 @@ function getOrCreateTexture(cache, nodeId, text, color) {
1876
1883
  }
1877
1884
  return entry;
1878
1885
  }
1879
- function updateLabels(state, nodes, positions, scales, camera, theme, showLabels, labelScale, labelThreshold, maxLabels, labelFormatter) {
1886
+ function updateLabels(state, nodes, positions, scales, camera, theme, showLabels, labelScale, labelThreshold, maxLabels, labelFormatter, highlightSet) {
1880
1887
  if (!positions || nodes.length === 0 || !showLabels) {
1881
1888
  for (const sp of state.sprites) sp.visible = false;
1882
1889
  return;
@@ -1890,6 +1897,7 @@ function updateLabels(state, nodes, positions, scales, camera, theme, showLabels
1890
1897
  const maxDistSq = maxDist * maxDist;
1891
1898
  const candidates = [];
1892
1899
  for (let i = 0; i < nc; i++) {
1900
+ if (highlightSet && !highlightSet.has(nodes[i].id)) continue;
1893
1901
  const dx = positions[i * 3] - camPos.x;
1894
1902
  const dy = positions[i * 3 + 1] - camPos.y;
1895
1903
  const dz = positions[i * 3 + 2] - camPos.z;
@@ -1935,58 +1943,48 @@ function disposeLabelSystem(state) {
1935
1943
  }
1936
1944
  state.textureCache.clear();
1937
1945
  }
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;
1946
+ const ORBIT_ACCEL = 12e-4;
1947
+ const ORBIT_MAX_SPEED = 0.06;
1948
+ const ORBIT_DAMPING = 0.88;
1949
+ const ORBIT_ZOOM_FACTOR = 0.015;
1950
+ const ORBIT_DEAD_ZONE = 1e-4;
1951
+ const FLY_ACCEL = 6e-3;
1952
+ const FLY_FRICTION = 0.92;
1953
+ const FLY_MAX_THRUST = 0.18;
1954
+ const FLY_TURN_RATE = 8e-3;
1955
+ const FLY_DEAD_ZONE = 2e-4;
1943
1956
  const _offset = new THREE.Vector3();
1944
1957
  const _right = new THREE.Vector3();
1945
1958
  const _q = new THREE.Quaternion();
1946
- function setupKeyboardControls(camera, controls, container) {
1947
- const keys = {};
1959
+ const _dir = new THREE.Vector3();
1960
+ function clamp(v, min, max) {
1961
+ return v < min ? min : v > max ? max : v;
1962
+ }
1963
+ function createOrbitUpdate(camera, controls, keys) {
1948
1964
  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;
1965
+ return function update() {
1966
+ if (keys["z"] || keys["Z"]) vel.zoom -= ORBIT_ACCEL;
1967
+ if (keys["x"] || keys["X"]) vel.zoom += ORBIT_ACCEL;
1968
+ if (keys["ArrowLeft"]) vel.azimuth -= ORBIT_ACCEL;
1969
+ if (keys["ArrowRight"]) vel.azimuth += ORBIT_ACCEL;
1970
+ if (keys["ArrowUp"]) vel.polar -= ORBIT_ACCEL;
1971
+ if (keys["ArrowDown"]) vel.polar += ORBIT_ACCEL;
1972
+ vel.zoom = clamp(vel.zoom, -ORBIT_MAX_SPEED, ORBIT_MAX_SPEED);
1973
+ vel.azimuth = clamp(vel.azimuth, -ORBIT_MAX_SPEED, ORBIT_MAX_SPEED);
1974
+ vel.polar = clamp(vel.polar, -ORBIT_MAX_SPEED, ORBIT_MAX_SPEED);
1975
+ vel.zoom *= ORBIT_DAMPING;
1976
+ vel.azimuth *= ORBIT_DAMPING;
1977
+ vel.polar *= ORBIT_DAMPING;
1978
+ if (Math.abs(vel.zoom) < ORBIT_DEAD_ZONE) vel.zoom = 0;
1979
+ if (Math.abs(vel.azimuth) < ORBIT_DEAD_ZONE) vel.azimuth = 0;
1980
+ if (Math.abs(vel.polar) < ORBIT_DEAD_ZONE) vel.polar = 0;
1983
1981
  const hasMotion = vel.zoom !== 0 || vel.azimuth !== 0 || vel.polar !== 0;
1984
1982
  if (!hasMotion) return false;
1985
1983
  const target = controls.target;
1986
1984
  _offset.copy(camera.position).sub(target);
1987
1985
  let radius = _offset.length();
1988
1986
  if (vel.zoom !== 0) {
1989
- radius *= 1 + vel.zoom * ZOOM_FACTOR * radius * 0.01;
1987
+ radius *= 1 + vel.zoom * ORBIT_ZOOM_FACTOR * radius * 0.01;
1990
1988
  radius = Math.max(controls.minDistance, Math.min(controls.maxDistance, radius));
1991
1989
  }
1992
1990
  if (vel.azimuth !== 0) {
@@ -2007,16 +2005,72 @@ function setupKeyboardControls(camera, controls, container) {
2007
2005
  camera.lookAt(target);
2008
2006
  controls.target.copy(target);
2009
2007
  return true;
2008
+ };
2009
+ }
2010
+ function createFlyUpdate(camera, controls, keys) {
2011
+ const vel = { thrust: 0, yaw: 0, pitch: 0 };
2012
+ return function update() {
2013
+ if (keys["z"] || keys["Z"]) vel.thrust += FLY_ACCEL;
2014
+ if (keys["x"] || keys["X"]) vel.thrust -= FLY_ACCEL;
2015
+ if (keys["a"] || keys["A"] || keys["ArrowLeft"]) vel.yaw += FLY_TURN_RATE;
2016
+ if (keys["d"] || keys["D"] || keys["ArrowRight"]) vel.yaw -= FLY_TURN_RATE;
2017
+ if (keys["w"] || keys["W"] || keys["ArrowUp"]) vel.pitch += FLY_TURN_RATE;
2018
+ if (keys["s"] || keys["S"] || keys["ArrowDown"]) vel.pitch -= FLY_TURN_RATE;
2019
+ const lookDist = camera.position.distanceTo(controls.target);
2020
+ const distScale = Math.max(0.3, lookDist * 3e-3);
2021
+ vel.thrust = clamp(vel.thrust, -FLY_MAX_THRUST * distScale, FLY_MAX_THRUST * distScale);
2022
+ vel.yaw = clamp(vel.yaw, -FLY_TURN_RATE * 2, FLY_TURN_RATE * 2);
2023
+ vel.pitch = clamp(vel.pitch, -FLY_TURN_RATE * 2, FLY_TURN_RATE * 2);
2024
+ vel.thrust *= FLY_FRICTION;
2025
+ vel.yaw *= FLY_FRICTION;
2026
+ vel.pitch *= FLY_FRICTION;
2027
+ if (Math.abs(vel.thrust) < FLY_DEAD_ZONE) vel.thrust = 0;
2028
+ if (Math.abs(vel.yaw) < FLY_DEAD_ZONE) vel.yaw = 0;
2029
+ if (Math.abs(vel.pitch) < FLY_DEAD_ZONE) vel.pitch = 0;
2030
+ const hasMotion = vel.thrust !== 0 || vel.yaw !== 0 || vel.pitch !== 0;
2031
+ if (!hasMotion) return false;
2032
+ if (vel.yaw !== 0) {
2033
+ _q.setFromAxisAngle(camera.up, vel.yaw);
2034
+ camera.quaternion.premultiply(_q);
2035
+ }
2036
+ if (vel.pitch !== 0) {
2037
+ _right.setFromMatrixColumn(camera.matrixWorld, 0).normalize();
2038
+ _q.setFromAxisAngle(_right, vel.pitch);
2039
+ camera.quaternion.premultiply(_q);
2040
+ }
2041
+ if (vel.thrust !== 0) {
2042
+ camera.getWorldDirection(_dir);
2043
+ _dir.multiplyScalar(vel.thrust * distScale * 100);
2044
+ camera.position.add(_dir);
2045
+ controls.target.add(_dir);
2046
+ }
2047
+ return true;
2048
+ };
2049
+ }
2050
+ function setupKeyboardControls(camera, controls, container, mode = "fly") {
2051
+ const keys = {};
2052
+ function onKeyDown2(e) {
2053
+ var _a;
2054
+ const tag = (_a = e.target) == null ? void 0 : _a.tagName;
2055
+ if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return;
2056
+ keys[e.key] = true;
2057
+ }
2058
+ function onKeyUp(e) {
2059
+ keys[e.key] = false;
2060
+ }
2061
+ function onBlur() {
2062
+ for (const k of Object.keys(keys)) keys[k] = false;
2010
2063
  }
2064
+ container.addEventListener("keydown", onKeyDown2);
2065
+ container.addEventListener("keyup", onKeyUp);
2066
+ container.addEventListener("blur", onBlur);
2067
+ const updateFn = mode === "fly" ? createFlyUpdate(camera, controls, keys) : createOrbitUpdate(camera, controls, keys);
2011
2068
  function cleanup() {
2012
2069
  container.removeEventListener("keydown", onKeyDown2);
2013
2070
  container.removeEventListener("keyup", onKeyUp);
2014
2071
  container.removeEventListener("blur", onBlur);
2015
2072
  }
2016
- return { update, cleanup };
2017
- }
2018
- function clamp(v, min, max) {
2019
- return v < min ? min : v > max ? max : v;
2073
+ return { update: updateFn, cleanup };
2020
2074
  }
2021
2075
  function createScene(container, backgroundColor, style, rendererConfig) {
2022
2076
  const scene = new THREE.Scene();
@@ -2053,7 +2107,8 @@ function createScene(container, backgroundColor, style, rendererConfig) {
2053
2107
  scene.add(selectionRing);
2054
2108
  const labels = createLabelSystem(style.maxLabels);
2055
2109
  scene.add(labels.group);
2056
- const keyboard = setupKeyboardControls(camera, controls, container);
2110
+ const cameraMode = (rendererConfig == null ? void 0 : rendererConfig.cameraMode) ?? "fly";
2111
+ const keyboard = setupKeyboardControls(camera, controls, container, cameraMode);
2057
2112
  return {
2058
2113
  scene,
2059
2114
  camera,
@@ -2392,21 +2447,136 @@ function zoomToFitPositions(camera, controls, positions, duration, padding) {
2392
2447
  duration
2393
2448
  );
2394
2449
  }
2395
- function setupInteraction(canvas, camera, controls, getNodesMesh, getNodes, callbacks, container) {
2450
+ const EDGE_HIT_THRESHOLD = 5;
2451
+ function setupInteraction(canvas, camera, controls, getNodesMesh, getNodes, callbacks, container, getEdgeData) {
2396
2452
  const raycaster = new THREE.Raycaster();
2397
2453
  const mouse = new THREE.Vector2();
2398
2454
  let mouseDownPos = null;
2399
2455
  let lastClickTime = 0;
2400
2456
  let lastClickNodeId = null;
2401
2457
  let singleClickTimer = null;
2458
+ let dragNode = null;
2459
+ let isDragging = false;
2460
+ const dragPlane = new THREE.Plane();
2461
+ const dragIntersect = new THREE.Vector3();
2402
2462
  function onPointerDown2(e) {
2403
2463
  mouseDownPos = { x: e.clientX, y: e.clientY };
2404
2464
  if (container && document.activeElement !== container) {
2405
2465
  container.focus({ preventScroll: true });
2406
2466
  }
2467
+ if (callbacks.onNodeDrag) {
2468
+ const nodesMesh = getNodesMesh();
2469
+ if (nodesMesh) {
2470
+ const rect = canvas.getBoundingClientRect();
2471
+ mouse.x = (e.clientX - rect.left) / rect.width * 2 - 1;
2472
+ mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
2473
+ raycaster.setFromCamera(mouse, camera);
2474
+ const hits = raycaster.intersectObject(nodesMesh);
2475
+ if (hits.length > 0 && hits[0].instanceId != null) {
2476
+ const nodes = getNodes();
2477
+ dragNode = nodes[hits[0].instanceId] ?? null;
2478
+ hits[0].instanceId;
2479
+ }
2480
+ }
2481
+ }
2482
+ }
2483
+ function onPointerMove2(e) {
2484
+ var _a, _b, _c;
2485
+ if (dragNode && mouseDownPos && callbacks.onNodeDrag) {
2486
+ const dx = Math.abs(e.clientX - mouseDownPos.x);
2487
+ const dy = Math.abs(e.clientY - mouseDownPos.y);
2488
+ if (dx + dy > 5) {
2489
+ isDragging = true;
2490
+ controls.enabled = false;
2491
+ const rect2 = canvas.getBoundingClientRect();
2492
+ mouse.x = (e.clientX - rect2.left) / rect2.width * 2 - 1;
2493
+ mouse.y = -((e.clientY - rect2.top) / rect2.height) * 2 + 1;
2494
+ const nodePos = new THREE.Vector3(
2495
+ dragNode.x ?? 0,
2496
+ dragNode.y ?? 0,
2497
+ dragNode.z ?? 0
2498
+ );
2499
+ dragPlane.setFromNormalAndCoplanarPoint(
2500
+ camera.getWorldDirection(new THREE.Vector3()).negate(),
2501
+ nodePos
2502
+ );
2503
+ raycaster.setFromCamera(mouse, camera);
2504
+ if (raycaster.ray.intersectPlane(dragPlane, dragIntersect)) {
2505
+ callbacks.onNodeDrag(dragNode, {
2506
+ x: dragIntersect.x,
2507
+ y: dragIntersect.y,
2508
+ z: dragIntersect.z
2509
+ });
2510
+ }
2511
+ return;
2512
+ }
2513
+ }
2514
+ if (!callbacks.onNodeHover && !callbacks.onLinkHover) return;
2515
+ const nodesMesh = getNodesMesh();
2516
+ if (!nodesMesh) return;
2517
+ const rect = canvas.getBoundingClientRect();
2518
+ mouse.x = (e.clientX - rect.left) / rect.width * 2 - 1;
2519
+ mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
2520
+ raycaster.setFromCamera(mouse, camera);
2521
+ const hits = raycaster.intersectObject(nodesMesh);
2522
+ if (hits.length > 0 && hits[0].instanceId != null) {
2523
+ const nodes = getNodes();
2524
+ const node = nodes[hits[0].instanceId];
2525
+ if (node && node.id !== lastHoveredId) {
2526
+ lastHoveredId = node.id;
2527
+ lastHoveredLinkIdx = -1;
2528
+ canvas.style.cursor = "pointer";
2529
+ (_a = callbacks.onNodeHover) == null ? void 0 : _a.call(callbacks, node);
2530
+ (_b = callbacks.onLinkHover) == null ? void 0 : _b.call(callbacks, null);
2531
+ }
2532
+ } else {
2533
+ if (lastHoveredId !== null) {
2534
+ lastHoveredId = null;
2535
+ canvas.style.cursor = "default";
2536
+ (_c = callbacks.onNodeHover) == null ? void 0 : _c.call(callbacks, null);
2537
+ }
2538
+ if (callbacks.onLinkHover && getEdgeData) {
2539
+ const edgeData = getEdgeData();
2540
+ if (edgeData == null ? void 0 : edgeData.positions) {
2541
+ const hitLink = findNearestEdge(
2542
+ e.clientX,
2543
+ e.clientY,
2544
+ rect,
2545
+ camera,
2546
+ edgeData
2547
+ );
2548
+ if (hitLink) {
2549
+ if (lastHoveredLinkIdx !== hitLink.index) {
2550
+ lastHoveredLinkIdx = hitLink.index;
2551
+ canvas.style.cursor = "pointer";
2552
+ callbacks.onLinkHover(hitLink.link);
2553
+ }
2554
+ } else if (lastHoveredLinkIdx !== -1) {
2555
+ lastHoveredLinkIdx = -1;
2556
+ canvas.style.cursor = "default";
2557
+ callbacks.onLinkHover(null);
2558
+ }
2559
+ }
2560
+ }
2561
+ }
2407
2562
  }
2408
2563
  function onPointerUp2(e) {
2409
2564
  var _a;
2565
+ if (isDragging && dragNode && callbacks.onNodeDragEnd) {
2566
+ callbacks.onNodeDragEnd(dragNode, {
2567
+ x: dragNode.x ?? 0,
2568
+ y: dragNode.y ?? 0,
2569
+ z: dragNode.z ?? 0
2570
+ });
2571
+ }
2572
+ if (isDragging) {
2573
+ isDragging = false;
2574
+ dragNode = null;
2575
+ controls.enabled = true;
2576
+ mouseDownPos = null;
2577
+ return;
2578
+ }
2579
+ dragNode = null;
2410
2580
  if (!mouseDownPos) return;
2411
2581
  const dx = Math.abs(e.clientX - mouseDownPos.x);
2412
2582
  const dy = Math.abs(e.clientY - mouseDownPos.y);
@@ -2454,6 +2624,22 @@ function setupInteraction(canvas, camera, controls, getNodesMesh, getNodes, call
2454
2624
  callbacks.onNodeClick(node);
2455
2625
  }, 250);
2456
2626
  } else {
2627
+ if (callbacks.onLinkClick && getEdgeData) {
2628
+ const edgeData = getEdgeData();
2629
+ if (edgeData == null ? void 0 : edgeData.positions) {
2630
+ const hitLink = findNearestEdge(
2631
+ e.clientX,
2632
+ e.clientY,
2633
+ rect,
2634
+ camera,
2635
+ edgeData
2636
+ );
2637
+ if (hitLink) {
2638
+ callbacks.onLinkClick(hitLink.link);
2639
+ return;
2640
+ }
2641
+ }
2642
+ }
2457
2643
  if (singleClickTimer) clearTimeout(singleClickTimer);
2458
2644
  singleClickTimer = null;
2459
2645
  lastClickTime = 0;
@@ -2461,13 +2647,9 @@ function setupInteraction(canvas, camera, controls, getNodesMesh, getNodes, call
2461
2647
  callbacks.onNodeClick(null);
2462
2648
  }
2463
2649
  }
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;
2650
+ function onContextMenu2(e) {
2651
+ if (!callbacks.onContextMenu) return;
2652
+ e.preventDefault();
2471
2653
  const nodesMesh = getNodesMesh();
2472
2654
  if (!nodesMesh) return;
2473
2655
  const rect = canvas.getBoundingClientRect();
@@ -2478,20 +2660,21 @@ function setupInteraction(canvas, camera, controls, getNodesMesh, getNodes, call
2478
2660
  if (hits.length > 0 && hits[0].instanceId != null) {
2479
2661
  const nodes = getNodes();
2480
2662
  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);
2663
+ if (node) {
2664
+ callbacks.onContextMenu(node, { x: e.clientX, y: e.clientY });
2485
2665
  }
2486
- } else if (lastHoveredId !== null) {
2487
- lastHoveredId = null;
2488
- canvas.style.cursor = "default";
2489
- callbacks.onNodeHover(null);
2490
2666
  }
2491
2667
  }
2668
+ const keyTarget = container ?? window;
2669
+ function onKeyDown2(e) {
2670
+ if (e.key === "Escape") callbacks.onNodeClick(null);
2671
+ }
2672
+ let lastHoveredId = null;
2673
+ let lastHoveredLinkIdx = -1;
2492
2674
  canvas.addEventListener("pointerdown", onPointerDown2);
2493
2675
  canvas.addEventListener("pointerup", onPointerUp2);
2494
2676
  canvas.addEventListener("pointermove", onPointerMove2);
2677
+ canvas.addEventListener("contextmenu", onContextMenu2);
2495
2678
  keyTarget.addEventListener("keydown", onKeyDown2);
2496
2679
  return {
2497
2680
  cleanup: () => {
@@ -2499,10 +2682,48 @@ function setupInteraction(canvas, camera, controls, getNodesMesh, getNodes, call
2499
2682
  canvas.removeEventListener("pointerdown", onPointerDown2);
2500
2683
  canvas.removeEventListener("pointerup", onPointerUp2);
2501
2684
  canvas.removeEventListener("pointermove", onPointerMove2);
2685
+ canvas.removeEventListener("contextmenu", onContextMenu2);
2502
2686
  keyTarget.removeEventListener("keydown", onKeyDown2);
2503
2687
  }
2504
2688
  };
2505
2689
  }
2690
+ function findNearestEdge(clientX, clientY, rect, camera, edgeData) {
2691
+ const { links, edgeNodeIndices, edgeLinkIndices, positions } = edgeData;
2692
+ const screenX = clientX - rect.left;
2693
+ const screenY = clientY - rect.top;
2694
+ const w = rect.width;
2695
+ const h = rect.height;
2696
+ let bestDist = EDGE_HIT_THRESHOLD;
2697
+ let bestIdx = -1;
2698
+ const tmpVec = new THREE.Vector3();
2699
+ for (let i = 0; i < edgeNodeIndices.length; i++) {
2700
+ const [si, ti] = edgeNodeIndices[i];
2701
+ tmpVec.set(positions[si * 3], positions[si * 3 + 1], positions[si * 3 + 2]);
2702
+ tmpVec.project(camera);
2703
+ const sx1 = (tmpVec.x * 0.5 + 0.5) * w;
2704
+ const sy1 = (-tmpVec.y * 0.5 + 0.5) * h;
2705
+ tmpVec.set(positions[ti * 3], positions[ti * 3 + 1], positions[ti * 3 + 2]);
2706
+ tmpVec.project(camera);
2707
+ const sx2 = (tmpVec.x * 0.5 + 0.5) * w;
2708
+ const sy2 = (-tmpVec.y * 0.5 + 0.5) * h;
2709
+ const dist = pointToSegmentDist(screenX, screenY, sx1, sy1, sx2, sy2);
2710
+ if (dist < bestDist) {
2711
+ bestDist = dist;
2712
+ bestIdx = i;
2713
+ }
2714
+ }
2715
+ if (bestIdx === -1) return null;
2716
+ return { link: links[edgeLinkIndices[bestIdx]], index: bestIdx };
2717
+ }
2718
+ function pointToSegmentDist(px, py, ax, ay, bx, by) {
2719
+ const dx = bx - ax;
2720
+ const dy = by - ay;
2721
+ const lenSq = dx * dx + dy * dy;
2722
+ if (lenSq < 1e-4) return Math.hypot(px - ax, py - ay);
2723
+ let t = ((px - ax) * dx + (py - ay) * dy) / lenSq;
2724
+ t = Math.max(0, Math.min(1, t));
2725
+ return Math.hypot(px - (ax + t * dx), py - (ay + t * dy));
2726
+ }
2506
2727
  function disposeObject(obj) {
2507
2728
  if (!obj) return;
2508
2729
  const mesh = obj;
@@ -2524,6 +2745,13 @@ function NetworkGraph3DInner(props, ref) {
2524
2745
  onNodeClick,
2525
2746
  onNodeDoubleClick,
2526
2747
  onNodeHover,
2748
+ onContextMenu: onContextMenu2,
2749
+ onLinkClick,
2750
+ onLinkHover,
2751
+ onNodeDrag,
2752
+ onNodeDragEnd,
2753
+ onLayoutSettled,
2754
+ onLayoutTick,
2527
2755
  selectedNodeId = null,
2528
2756
  highlightHops = 3,
2529
2757
  theme: themeProp,
@@ -2556,13 +2784,28 @@ function NetworkGraph3DInner(props, ref) {
2556
2784
  const onNodeClickRef = useRef(onNodeClick);
2557
2785
  const onNodeDoubleClickRef = useRef(onNodeDoubleClick);
2558
2786
  const onNodeHoverRef = useRef(onNodeHover);
2787
+ const onContextMenuRef = useRef(onContextMenu2);
2788
+ const onLinkClickRef = useRef(onLinkClick);
2789
+ const onLinkHoverRef = useRef(onLinkHover);
2790
+ const onNodeDragRef = useRef(onNodeDrag);
2791
+ const onNodeDragEndRef = useRef(onNodeDragEnd);
2792
+ const onLayoutSettledRef = useRef(onLayoutSettled);
2793
+ const onLayoutTickRef = useRef(onLayoutTick);
2559
2794
  const styleRef = useRef(resolvedStyle);
2560
2795
  const themeRef = useRef(resolvedTheme);
2561
2796
  const selectedNodeIdRef = useRef(selectedNodeId);
2562
2797
  const labelFormatterRef = useRef(labelFormatter);
2798
+ const highlightSetRef = useRef(null);
2563
2799
  onNodeClickRef.current = onNodeClick;
2564
2800
  onNodeDoubleClickRef.current = onNodeDoubleClick;
2565
2801
  onNodeHoverRef.current = onNodeHover;
2802
+ onContextMenuRef.current = onContextMenu2;
2803
+ onLinkClickRef.current = onLinkClick;
2804
+ onLinkHoverRef.current = onLinkHover;
2805
+ onNodeDragRef.current = onNodeDrag;
2806
+ onNodeDragEndRef.current = onNodeDragEnd;
2807
+ onLayoutSettledRef.current = onLayoutSettled;
2808
+ onLayoutTickRef.current = onLayoutTick;
2566
2809
  styleRef.current = resolvedStyle;
2567
2810
  themeRef.current = resolvedTheme;
2568
2811
  selectedNodeIdRef.current = selectedNodeId;
@@ -2628,7 +2871,8 @@ function NetworkGraph3DInner(props, ref) {
2628
2871
  styleRef.current.labelScale,
2629
2872
  styleRef.current.labelThreshold,
2630
2873
  styleRef.current.maxLabels,
2631
- labelFormatterRef.current
2874
+ labelFormatterRef.current,
2875
+ highlightSetRef.current
2632
2876
  );
2633
2877
  });
2634
2878
  const resizeObserver = setupResize(container, state);
@@ -2653,9 +2897,52 @@ function NetworkGraph3DInner(props, ref) {
2653
2897
  onNodeHover: (node) => {
2654
2898
  var _a;
2655
2899
  return (_a = onNodeHoverRef.current) == null ? void 0 : _a.call(onNodeHoverRef, node);
2900
+ },
2901
+ onContextMenu: (node, pos) => {
2902
+ var _a;
2903
+ return (_a = onContextMenuRef.current) == null ? void 0 : _a.call(onContextMenuRef, node, pos);
2904
+ },
2905
+ onLinkClick: (link) => {
2906
+ var _a;
2907
+ return (_a = onLinkClickRef.current) == null ? void 0 : _a.call(onLinkClickRef, link);
2908
+ },
2909
+ onLinkHover: (link) => {
2910
+ var _a;
2911
+ return (_a = onLinkHoverRef.current) == null ? void 0 : _a.call(onLinkHoverRef, link);
2912
+ },
2913
+ onNodeDrag: (node, pos) => {
2914
+ var _a;
2915
+ (_a = onNodeDragRef.current) == null ? void 0 : _a.call(onNodeDragRef, node, pos);
2916
+ const d = dataRef.current;
2917
+ const idx = d.nodeIdToIndex.get(node.id);
2918
+ if (idx !== void 0 && d.positions) {
2919
+ d.positions[idx * 3] = pos.x;
2920
+ d.positions[idx * 3 + 1] = pos.y;
2921
+ d.positions[idx * 3 + 2] = pos.z;
2922
+ node.x = pos.x;
2923
+ node.y = pos.y;
2924
+ node.z = pos.z;
2925
+ const gObj = graphObjRef.current;
2926
+ if (gObj && d.scales) {
2927
+ updateNodePositions(gObj.nodesMesh, d.positions, d.scales, d.nodes.length);
2928
+ }
2929
+ }
2930
+ },
2931
+ onNodeDragEnd: (node, pos) => {
2932
+ var _a;
2933
+ return (_a = onNodeDragEndRef.current) == null ? void 0 : _a.call(onNodeDragEndRef, node, pos);
2656
2934
  }
2657
2935
  },
2658
- container
2936
+ container,
2937
+ () => {
2938
+ const d = dataRef.current;
2939
+ return {
2940
+ links: d.links,
2941
+ edgeNodeIndices: d.edgeNodeIndices,
2942
+ edgeLinkIndices: d.edgeLinkIndices,
2943
+ positions: d.positions
2944
+ };
2945
+ }
2659
2946
  );
2660
2947
  return () => {
2661
2948
  cancelAnim();
@@ -2795,6 +3082,7 @@ function NetworkGraph3DInner(props, ref) {
2795
3082
  console.error("Graphier: Layout worker error:", err);
2796
3083
  };
2797
3084
  worker.onmessage = (e) => {
3085
+ var _a, _b;
2798
3086
  const msg = e.data;
2799
3087
  if (msg.type === "positions") {
2800
3088
  const positions = new Float32Array(msg.positions);
@@ -2822,9 +3110,11 @@ function NetworkGraph3DInner(props, ref) {
2822
3110
  );
2823
3111
  }
2824
3112
  }
3113
+ (_a = onLayoutTickRef.current) == null ? void 0 : _a.call(onLayoutTickRef, msg.alpha);
2825
3114
  }
2826
3115
  if (msg.type === "settled") {
2827
3116
  dataRef.current.settled = true;
3117
+ (_b = onLayoutSettledRef.current) == null ? void 0 : _b.call(onLayoutSettledRef);
2828
3118
  }
2829
3119
  };
2830
3120
  const layoutParams = resolveLayoutParams(nc, layoutProp);
@@ -2869,6 +3159,7 @@ function NetworkGraph3DInner(props, ref) {
2869
3159
  const d = dataRef.current;
2870
3160
  const theme = themeRef.current;
2871
3161
  const style = styleRef.current;
3162
+ highlightSetRef.current = highlightSet;
2872
3163
  applyNodeHighlight(
2873
3164
  gObj.nodesMesh,
2874
3165
  d.nodes,
@@ -2886,6 +3177,18 @@ function NetworkGraph3DInner(props, ref) {
2886
3177
  d.nodes.length,
2887
3178
  theme
2888
3179
  );
3180
+ if (sceneState.bloomPass) {
3181
+ const style2 = styleRef.current;
3182
+ if (highlightSet) {
3183
+ sceneState.bloomPass.strength = style2.bloomStrength * 1.8;
3184
+ sceneState.bloomPass.radius = style2.bloomRadius * 1.4;
3185
+ sceneState.bloomPass.threshold = 0.25;
3186
+ } else {
3187
+ sceneState.bloomPass.strength = style2.bloomStrength;
3188
+ sceneState.bloomPass.radius = style2.bloomRadius;
3189
+ sceneState.bloomPass.threshold = style2.bloomThreshold;
3190
+ }
3191
+ }
2889
3192
  const selectedNode = selectedNodeId ? d.nodes.find((n) => n.id === selectedNodeId) : null;
2890
3193
  updateSelectionRing(
2891
3194
  sceneState.selectionRing,
@@ -3033,13 +3336,11 @@ function NetworkGraph3DInner(props, ref) {
3033
3336
  var _a;
3034
3337
  return ((_a = sceneRef.current) == null ? void 0 : _a.camera) ?? null;
3035
3338
  },
3036
- async screenshot() {
3339
+ captureScreenshot() {
3037
3340
  const s = sceneRef.current;
3038
3341
  if (!s) return null;
3039
3342
  s.renderer.render(s.scene, s.camera);
3040
- return new Promise((resolve) => {
3041
- s.renderer.domElement.toBlob(resolve, "image/png");
3042
- });
3343
+ return s.renderer.domElement.toDataURL("image/png");
3043
3344
  }
3044
3345
  }));
3045
3346
  return /* @__PURE__ */ jsx(