@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.cjs CHANGED
@@ -34,7 +34,8 @@ const DEFAULT_STYLE = {
34
34
  labelThreshold: 0.8,
35
35
  showLabels: true,
36
36
  maxLabels: 150,
37
- edgeWidthScale: 1
37
+ edgeWidthScale: 1,
38
+ flySpeed: 1
38
39
  };
39
40
  const DEFAULT_LAYOUT = {
40
41
  type: "force-3d",
@@ -42,7 +43,8 @@ const DEFAULT_LAYOUT = {
42
43
  linkDistance: "auto",
43
44
  alphaDecay: "auto",
44
45
  velocityDecay: 0.4,
45
- settledThreshold: 5e-3
46
+ settledThreshold: 5e-3,
47
+ spreadFactor: "auto"
46
48
  };
47
49
  function hexToRgb(hex) {
48
50
  const c2 = hex.replace("#", "");
@@ -188,17 +190,26 @@ function resolveTheme(theme) {
188
190
  }
189
191
  return { nodeColor, nodeColorBright, linkColor, backgroundColor };
190
192
  }
193
+ function autoSpreadFactor(n) {
194
+ if (n <= 500) return 1;
195
+ if (n <= 2e3) return 1 + (n - 500) / 1500 * 0.3;
196
+ if (n <= 1e4) return 1.3 + (n - 2e3) / 8e3 * 0.5;
197
+ return Math.min(2.5, 1.8 + (n - 1e4) / 4e4 * 0.7);
198
+ }
191
199
  function resolveLayoutParams(nodeCount, config) {
192
200
  const n = nodeCount;
193
- 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;
194
- const distanceMax = n > 5e4 ? 1500 : n > 1e4 ? 2e3 : 3e3;
201
+ const spread = (config == null ? void 0 : config.spreadFactor) !== "auto" && (config == null ? void 0 : config.spreadFactor) != null ? config.spreadFactor : autoSpreadFactor(n);
202
+ 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;
203
+ const charge = baseCharge * spread;
204
+ const distanceMax = (n > 5e4 ? 1500 : n > 1e4 ? 2e3 : 3e3) * spread;
195
205
  const theta = n > 2e4 ? 1.5 : n > 5e3 ? 1.2 : 0.9;
196
- 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;
206
+ 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;
207
+ const linkDistance = baseLinkDistance * spread;
197
208
  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;
198
209
  const velocityDecay = (config == null ? void 0 : config.velocityDecay) ?? 0.4;
199
210
  const settledThreshold = (config == null ? void 0 : config.settledThreshold) ?? 5e-3;
200
211
  const postEvery = n > 5e4 ? 5 : n > 1e4 ? 3 : 2;
201
- const initialRadius2 = 500 + Math.min(n, 1e4) * 0.1;
212
+ const initialRadius2 = (500 + Math.min(n, 1e4) * 0.1) * spread;
202
213
  return {
203
214
  charge,
204
215
  distanceMax,
@@ -1721,27 +1732,31 @@ function applyNodeHighlight(nodesMesh, nodes, selectedNodeId, highlightSet, them
1721
1732
  } else if (node.id === selectedNodeId) {
1722
1733
  tmpColor.set(brightColor);
1723
1734
  brightTmp.setRGB(1, 1, 1);
1724
- tmpColor.lerp(brightTmp, 0.5);
1735
+ tmpColor.lerp(brightTmp, 0.65);
1736
+ tmpColor.multiplyScalar(1.4);
1737
+ tmpColor.r = Math.min(tmpColor.r, 2);
1738
+ tmpColor.g = Math.min(tmpColor.g, 2);
1739
+ tmpColor.b = Math.min(tmpColor.b, 2);
1725
1740
  } else if (highlightSet.has(node.id)) {
1726
1741
  const hop = highlightSet.get(node.id);
1727
1742
  if (hop === 1) {
1728
- tmpColor.set(brightColor);
1743
+ tmpColor.set(brightColor).multiplyScalar(1.2);
1729
1744
  } else if (hop === 2) {
1730
1745
  tmpColor.set(baseColor);
1731
1746
  brightTmp.set(brightColor);
1732
- tmpColor.lerp(brightTmp, 0.4);
1747
+ tmpColor.lerp(brightTmp, 0.5);
1733
1748
  } else {
1734
- tmpColor.set(baseColor).multiplyScalar(0.7);
1749
+ tmpColor.set(baseColor).multiplyScalar(0.6);
1735
1750
  }
1736
1751
  } else {
1737
- tmpColor.set(baseColor).multiplyScalar(0.12);
1752
+ tmpColor.set(baseColor).multiplyScalar(0.04);
1738
1753
  }
1739
1754
  nodesMesh.setColorAt(i, tmpColor);
1740
1755
  }
1741
1756
  if (nodesMesh.instanceColor) nodesMesh.instanceColor.needsUpdate = true;
1742
1757
  const mat = nodesMesh.material;
1743
1758
  if ((_a = mat.uniforms) == null ? void 0 : _a.uGlowIntensity) {
1744
- mat.uniforms.uGlowIntensity.value = highlightSet ? 1.15 : 1;
1759
+ mat.uniforms.uGlowIntensity.value = highlightSet ? 1.5 : 1;
1745
1760
  }
1746
1761
  }
1747
1762
  function applyEdgeHighlight(edgesMesh, links, edgeNodeIndices, edgeLinkIndices, highlightSet, edgeOpacity, nodeCount, theme) {
@@ -1769,7 +1784,7 @@ function applyEdgeHighlight(edgesMesh, links, edgeNodeIndices, edgeLinkIndices,
1769
1784
  tmpColor.multiplyScalar(1.2);
1770
1785
  }
1771
1786
  } else {
1772
- tmpColor.setRGB(0.03, 0.03, 0.06);
1787
+ tmpColor.setRGB(0.01, 0.01, 0.02);
1773
1788
  }
1774
1789
  colorArr[i * 6 + 0] = tmpColor.r;
1775
1790
  colorArr[i * 6 + 1] = tmpColor.g;
@@ -1856,6 +1871,9 @@ function createLabelSystem(maxLabels) {
1856
1871
  }
1857
1872
  function defaultLabelText$1(node) {
1858
1873
  const label = node.label ?? node.id ?? "";
1874
+ if (node.type === "repo" && label.includes("/")) {
1875
+ return label.split("/").pop();
1876
+ }
1859
1877
  return label.length > 30 ? label.substring(0, 27) + "…" : label;
1860
1878
  }
1861
1879
  function getOrCreateTexture(cache, nodeId, text, color) {
@@ -1894,7 +1912,7 @@ function getOrCreateTexture(cache, nodeId, text, color) {
1894
1912
  }
1895
1913
  return entry;
1896
1914
  }
1897
- function updateLabels(state, nodes, positions, scales, camera, theme, showLabels, labelScale, labelThreshold, maxLabels, labelFormatter) {
1915
+ function updateLabels(state, nodes, positions, scales, camera, theme, showLabels, labelScale, labelThreshold, maxLabels, labelFormatter, highlightSet) {
1898
1916
  if (!positions || nodes.length === 0 || !showLabels) {
1899
1917
  for (const sp of state.sprites) sp.visible = false;
1900
1918
  return;
@@ -1908,6 +1926,7 @@ function updateLabels(state, nodes, positions, scales, camera, theme, showLabels
1908
1926
  const maxDistSq = maxDist * maxDist;
1909
1927
  const candidates = [];
1910
1928
  for (let i = 0; i < nc; i++) {
1929
+ if (highlightSet && !highlightSet.has(nodes[i].id)) continue;
1911
1930
  const dx = positions[i * 3] - camPos.x;
1912
1931
  const dy = positions[i * 3 + 1] - camPos.y;
1913
1932
  const dz = positions[i * 3 + 2] - camPos.z;
@@ -1953,58 +1972,49 @@ function disposeLabelSystem(state) {
1953
1972
  }
1954
1973
  state.textureCache.clear();
1955
1974
  }
1956
- const ACCEL = 12e-4;
1957
- const MAX_SPEED = 0.06;
1958
- const DAMPING = 0.88;
1959
- const ZOOM_FACTOR = 0.015;
1960
- const DEAD_ZONE = 1e-4;
1975
+ const ORBIT_ACCEL = 12e-4;
1976
+ const ORBIT_MAX_SPEED = 0.06;
1977
+ const ORBIT_DAMPING = 0.88;
1978
+ const ORBIT_ZOOM_FACTOR = 0.015;
1979
+ const ORBIT_DEAD_ZONE = 1e-4;
1980
+ const FLY_ACCEL = 6e-3;
1981
+ const FLY_FRICTION = 0.92;
1982
+ const FLY_MAX_THRUST = 0.18;
1983
+ const FLY_TURN_RATE = 8e-3;
1984
+ const FLY_DEAD_ZONE = 2e-4;
1961
1985
  const _offset = new THREE__namespace.Vector3();
1962
1986
  const _right = new THREE__namespace.Vector3();
1963
1987
  const _q = new THREE__namespace.Quaternion();
1964
- function setupKeyboardControls(camera, controls, container) {
1965
- const keys = {};
1988
+ const _dir = new THREE__namespace.Vector3();
1989
+ const _worldUp = new THREE__namespace.Vector3(0, 1, 0);
1990
+ function clamp(v, min, max) {
1991
+ return v < min ? min : v > max ? max : v;
1992
+ }
1993
+ function createOrbitUpdate(camera, controls, keys) {
1966
1994
  const vel = { zoom: 0, azimuth: 0, polar: 0 };
1967
- function onKeyDown2(e) {
1968
- var _a;
1969
- const tag = (_a = e.target) == null ? void 0 : _a.tagName;
1970
- if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return;
1971
- keys[e.key] = true;
1972
- }
1973
- function onKeyUp(e) {
1974
- keys[e.key] = false;
1975
- }
1976
- function onBlur() {
1977
- for (const k of Object.keys(keys)) keys[k] = false;
1978
- vel.zoom = 0;
1979
- vel.azimuth = 0;
1980
- vel.polar = 0;
1981
- }
1982
- container.addEventListener("keydown", onKeyDown2);
1983
- container.addEventListener("keyup", onKeyUp);
1984
- container.addEventListener("blur", onBlur);
1985
- function update() {
1986
- if (keys["z"] || keys["Z"]) vel.zoom -= ACCEL;
1987
- if (keys["x"] || keys["X"]) vel.zoom += ACCEL;
1988
- if (keys["ArrowLeft"]) vel.azimuth -= ACCEL;
1989
- if (keys["ArrowRight"]) vel.azimuth += ACCEL;
1990
- if (keys["ArrowUp"]) vel.polar -= ACCEL;
1991
- if (keys["ArrowDown"]) vel.polar += ACCEL;
1992
- vel.zoom = clamp(vel.zoom, -MAX_SPEED, MAX_SPEED);
1993
- vel.azimuth = clamp(vel.azimuth, -MAX_SPEED, MAX_SPEED);
1994
- vel.polar = clamp(vel.polar, -MAX_SPEED, MAX_SPEED);
1995
- vel.zoom *= DAMPING;
1996
- vel.azimuth *= DAMPING;
1997
- vel.polar *= DAMPING;
1998
- if (Math.abs(vel.zoom) < DEAD_ZONE) vel.zoom = 0;
1999
- if (Math.abs(vel.azimuth) < DEAD_ZONE) vel.azimuth = 0;
2000
- if (Math.abs(vel.polar) < DEAD_ZONE) vel.polar = 0;
1995
+ return function update() {
1996
+ if (keys["z"] || keys["Z"]) vel.zoom -= ORBIT_ACCEL;
1997
+ if (keys["x"] || keys["X"]) vel.zoom += ORBIT_ACCEL;
1998
+ if (keys["ArrowLeft"]) vel.azimuth -= ORBIT_ACCEL;
1999
+ if (keys["ArrowRight"]) vel.azimuth += ORBIT_ACCEL;
2000
+ if (keys["ArrowUp"]) vel.polar -= ORBIT_ACCEL;
2001
+ if (keys["ArrowDown"]) vel.polar += ORBIT_ACCEL;
2002
+ vel.zoom = clamp(vel.zoom, -ORBIT_MAX_SPEED, ORBIT_MAX_SPEED);
2003
+ vel.azimuth = clamp(vel.azimuth, -ORBIT_MAX_SPEED, ORBIT_MAX_SPEED);
2004
+ vel.polar = clamp(vel.polar, -ORBIT_MAX_SPEED, ORBIT_MAX_SPEED);
2005
+ vel.zoom *= ORBIT_DAMPING;
2006
+ vel.azimuth *= ORBIT_DAMPING;
2007
+ vel.polar *= ORBIT_DAMPING;
2008
+ if (Math.abs(vel.zoom) < ORBIT_DEAD_ZONE) vel.zoom = 0;
2009
+ if (Math.abs(vel.azimuth) < ORBIT_DEAD_ZONE) vel.azimuth = 0;
2010
+ if (Math.abs(vel.polar) < ORBIT_DEAD_ZONE) vel.polar = 0;
2001
2011
  const hasMotion = vel.zoom !== 0 || vel.azimuth !== 0 || vel.polar !== 0;
2002
2012
  if (!hasMotion) return false;
2003
2013
  const target = controls.target;
2004
2014
  _offset.copy(camera.position).sub(target);
2005
2015
  let radius = _offset.length();
2006
2016
  if (vel.zoom !== 0) {
2007
- radius *= 1 + vel.zoom * ZOOM_FACTOR * radius * 0.01;
2017
+ radius *= 1 + vel.zoom * ORBIT_ZOOM_FACTOR * radius * 0.01;
2008
2018
  radius = Math.max(controls.minDistance, Math.min(controls.maxDistance, radius));
2009
2019
  }
2010
2020
  if (vel.azimuth !== 0) {
@@ -2025,16 +2035,81 @@ function setupKeyboardControls(camera, controls, container) {
2025
2035
  camera.lookAt(target);
2026
2036
  controls.target.copy(target);
2027
2037
  return true;
2038
+ };
2039
+ }
2040
+ function createFlyUpdate(camera, controls, keys, state) {
2041
+ const vel = { thrust: 0, yaw: 0, pitch: 0 };
2042
+ return function update() {
2043
+ const speedMul = state.flySpeed;
2044
+ if (keys["z"] || keys["Z"]) vel.thrust += FLY_ACCEL * speedMul;
2045
+ if (keys["x"] || keys["X"]) vel.thrust -= FLY_ACCEL * speedMul;
2046
+ if (keys["a"] || keys["A"] || keys["ArrowLeft"]) vel.yaw += FLY_TURN_RATE;
2047
+ if (keys["d"] || keys["D"] || keys["ArrowRight"]) vel.yaw -= FLY_TURN_RATE;
2048
+ if (keys["w"] || keys["W"] || keys["ArrowUp"]) vel.pitch += FLY_TURN_RATE;
2049
+ if (keys["s"] || keys["S"] || keys["ArrowDown"]) vel.pitch -= FLY_TURN_RATE;
2050
+ const lookDist = camera.position.distanceTo(controls.target);
2051
+ const distScale = Math.max(0.3, lookDist * 3e-3);
2052
+ vel.thrust = clamp(vel.thrust, -FLY_MAX_THRUST * distScale * speedMul, FLY_MAX_THRUST * distScale * speedMul);
2053
+ vel.yaw = clamp(vel.yaw, -FLY_TURN_RATE * 2, FLY_TURN_RATE * 2);
2054
+ vel.pitch = clamp(vel.pitch, -FLY_TURN_RATE * 2, FLY_TURN_RATE * 2);
2055
+ vel.thrust *= FLY_FRICTION;
2056
+ vel.yaw *= FLY_FRICTION;
2057
+ vel.pitch *= FLY_FRICTION;
2058
+ if (Math.abs(vel.thrust) < FLY_DEAD_ZONE) vel.thrust = 0;
2059
+ if (Math.abs(vel.yaw) < FLY_DEAD_ZONE) vel.yaw = 0;
2060
+ if (Math.abs(vel.pitch) < FLY_DEAD_ZONE) vel.pitch = 0;
2061
+ const hasMotion = vel.thrust !== 0 || vel.yaw !== 0 || vel.pitch !== 0;
2062
+ if (!hasMotion) return false;
2063
+ if (vel.yaw !== 0) {
2064
+ const upDot = camera.up.dot(_worldUp);
2065
+ const yawSign = upDot < 0 ? -1 : 1;
2066
+ _q.setFromAxisAngle(_worldUp, vel.yaw * yawSign);
2067
+ camera.quaternion.premultiply(_q);
2068
+ camera.up.applyQuaternion(_q).normalize();
2069
+ }
2070
+ if (vel.pitch !== 0) {
2071
+ _right.setFromMatrixColumn(camera.matrixWorld, 0).normalize();
2072
+ _q.setFromAxisAngle(_right, vel.pitch);
2073
+ camera.quaternion.premultiply(_q);
2074
+ camera.up.applyQuaternion(_q).normalize();
2075
+ }
2076
+ if (vel.thrust !== 0) {
2077
+ camera.getWorldDirection(_dir);
2078
+ _dir.multiplyScalar(vel.thrust * distScale * 100);
2079
+ camera.position.add(_dir);
2080
+ controls.target.add(_dir);
2081
+ }
2082
+ return true;
2083
+ };
2084
+ }
2085
+ function setupKeyboardControls(camera, controls, container, mode = "fly", flySpeed = 1) {
2086
+ const keys = {};
2087
+ const state = { flySpeed };
2088
+ function onKeyDown2(e) {
2089
+ var _a;
2090
+ const tag = (_a = e.target) == null ? void 0 : _a.tagName;
2091
+ if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return;
2092
+ keys[e.key] = true;
2028
2093
  }
2094
+ function onKeyUp(e) {
2095
+ keys[e.key] = false;
2096
+ }
2097
+ function onBlur() {
2098
+ for (const k of Object.keys(keys)) keys[k] = false;
2099
+ }
2100
+ container.addEventListener("keydown", onKeyDown2);
2101
+ container.addEventListener("keyup", onKeyUp);
2102
+ container.addEventListener("blur", onBlur);
2103
+ const updateFn = mode === "fly" ? createFlyUpdate(camera, controls, keys, state) : createOrbitUpdate(camera, controls, keys);
2029
2104
  function cleanup() {
2030
2105
  container.removeEventListener("keydown", onKeyDown2);
2031
2106
  container.removeEventListener("keyup", onKeyUp);
2032
2107
  container.removeEventListener("blur", onBlur);
2033
2108
  }
2034
- return { update, cleanup };
2035
- }
2036
- function clamp(v, min, max) {
2037
- return v < min ? min : v > max ? max : v;
2109
+ function setFlySpeed(speed) {
2110
+ state.flySpeed = speed;
2111
+ }
2112
+ return { update: updateFn, cleanup, setFlySpeed };
2038
2113
  }
2039
2114
  function createScene(container, backgroundColor, style, rendererConfig) {
2040
2115
  const scene = new THREE__namespace.Scene();
@@ -2071,7 +2146,8 @@ function createScene(container, backgroundColor, style, rendererConfig) {
2071
2146
  scene.add(selectionRing);
2072
2147
  const labels = createLabelSystem(style.maxLabels);
2073
2148
  scene.add(labels.group);
2074
- const keyboard = setupKeyboardControls(camera, controls, container);
2149
+ const cameraMode = (rendererConfig == null ? void 0 : rendererConfig.cameraMode) ?? "fly";
2150
+ const keyboard = setupKeyboardControls(camera, controls, container, cameraMode, style.flySpeed ?? 1);
2075
2151
  return {
2076
2152
  scene,
2077
2153
  camera,
@@ -2410,21 +2486,136 @@ function zoomToFitPositions(camera, controls, positions, duration, padding) {
2410
2486
  duration
2411
2487
  );
2412
2488
  }
2413
- function setupInteraction(canvas, camera, controls, getNodesMesh, getNodes, callbacks, container) {
2489
+ const EDGE_HIT_THRESHOLD = 5;
2490
+ function setupInteraction(canvas, camera, controls, getNodesMesh, getNodes, callbacks, container, getEdgeData) {
2414
2491
  const raycaster = new THREE__namespace.Raycaster();
2415
2492
  const mouse = new THREE__namespace.Vector2();
2416
2493
  let mouseDownPos = null;
2417
2494
  let lastClickTime = 0;
2418
2495
  let lastClickNodeId = null;
2419
2496
  let singleClickTimer = null;
2497
+ let dragNode = null;
2498
+ let isDragging = false;
2499
+ const dragPlane = new THREE__namespace.Plane();
2500
+ const dragIntersect = new THREE__namespace.Vector3();
2420
2501
  function onPointerDown2(e) {
2421
2502
  mouseDownPos = { x: e.clientX, y: e.clientY };
2422
2503
  if (container && document.activeElement !== container) {
2423
2504
  container.focus({ preventScroll: true });
2424
2505
  }
2506
+ if (callbacks.onNodeDrag) {
2507
+ const nodesMesh = getNodesMesh();
2508
+ if (nodesMesh) {
2509
+ const rect = canvas.getBoundingClientRect();
2510
+ mouse.x = (e.clientX - rect.left) / rect.width * 2 - 1;
2511
+ mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
2512
+ raycaster.setFromCamera(mouse, camera);
2513
+ const hits = raycaster.intersectObject(nodesMesh);
2514
+ if (hits.length > 0 && hits[0].instanceId != null) {
2515
+ const nodes = getNodes();
2516
+ dragNode = nodes[hits[0].instanceId] ?? null;
2517
+ hits[0].instanceId;
2518
+ }
2519
+ }
2520
+ }
2521
+ }
2522
+ function onPointerMove2(e) {
2523
+ var _a, _b, _c;
2524
+ if (dragNode && mouseDownPos && callbacks.onNodeDrag) {
2525
+ const dx = Math.abs(e.clientX - mouseDownPos.x);
2526
+ const dy = Math.abs(e.clientY - mouseDownPos.y);
2527
+ if (dx + dy > 5) {
2528
+ isDragging = true;
2529
+ controls.enabled = false;
2530
+ const rect2 = canvas.getBoundingClientRect();
2531
+ mouse.x = (e.clientX - rect2.left) / rect2.width * 2 - 1;
2532
+ mouse.y = -((e.clientY - rect2.top) / rect2.height) * 2 + 1;
2533
+ const nodePos = new THREE__namespace.Vector3(
2534
+ dragNode.x ?? 0,
2535
+ dragNode.y ?? 0,
2536
+ dragNode.z ?? 0
2537
+ );
2538
+ dragPlane.setFromNormalAndCoplanarPoint(
2539
+ camera.getWorldDirection(new THREE__namespace.Vector3()).negate(),
2540
+ nodePos
2541
+ );
2542
+ raycaster.setFromCamera(mouse, camera);
2543
+ if (raycaster.ray.intersectPlane(dragPlane, dragIntersect)) {
2544
+ callbacks.onNodeDrag(dragNode, {
2545
+ x: dragIntersect.x,
2546
+ y: dragIntersect.y,
2547
+ z: dragIntersect.z
2548
+ });
2549
+ }
2550
+ return;
2551
+ }
2552
+ }
2553
+ if (!callbacks.onNodeHover && !callbacks.onLinkHover) return;
2554
+ const nodesMesh = getNodesMesh();
2555
+ if (!nodesMesh) return;
2556
+ const rect = canvas.getBoundingClientRect();
2557
+ mouse.x = (e.clientX - rect.left) / rect.width * 2 - 1;
2558
+ mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
2559
+ raycaster.setFromCamera(mouse, camera);
2560
+ const hits = raycaster.intersectObject(nodesMesh);
2561
+ if (hits.length > 0 && hits[0].instanceId != null) {
2562
+ const nodes = getNodes();
2563
+ const node = nodes[hits[0].instanceId];
2564
+ if (node && node.id !== lastHoveredId) {
2565
+ lastHoveredId = node.id;
2566
+ lastHoveredLinkIdx = -1;
2567
+ canvas.style.cursor = "pointer";
2568
+ (_a = callbacks.onNodeHover) == null ? void 0 : _a.call(callbacks, node);
2569
+ (_b = callbacks.onLinkHover) == null ? void 0 : _b.call(callbacks, null);
2570
+ }
2571
+ } else {
2572
+ if (lastHoveredId !== null) {
2573
+ lastHoveredId = null;
2574
+ canvas.style.cursor = "default";
2575
+ (_c = callbacks.onNodeHover) == null ? void 0 : _c.call(callbacks, null);
2576
+ }
2577
+ if (callbacks.onLinkHover && getEdgeData) {
2578
+ const edgeData = getEdgeData();
2579
+ if (edgeData == null ? void 0 : edgeData.positions) {
2580
+ const hitLink = findNearestEdge(
2581
+ e.clientX,
2582
+ e.clientY,
2583
+ rect,
2584
+ camera,
2585
+ edgeData
2586
+ );
2587
+ if (hitLink) {
2588
+ if (lastHoveredLinkIdx !== hitLink.index) {
2589
+ lastHoveredLinkIdx = hitLink.index;
2590
+ canvas.style.cursor = "pointer";
2591
+ callbacks.onLinkHover(hitLink.link);
2592
+ }
2593
+ } else if (lastHoveredLinkIdx !== -1) {
2594
+ lastHoveredLinkIdx = -1;
2595
+ canvas.style.cursor = "default";
2596
+ callbacks.onLinkHover(null);
2597
+ }
2598
+ }
2599
+ }
2600
+ }
2425
2601
  }
2426
2602
  function onPointerUp2(e) {
2427
2603
  var _a;
2604
+ if (isDragging && dragNode && callbacks.onNodeDragEnd) {
2605
+ callbacks.onNodeDragEnd(dragNode, {
2606
+ x: dragNode.x ?? 0,
2607
+ y: dragNode.y ?? 0,
2608
+ z: dragNode.z ?? 0
2609
+ });
2610
+ }
2611
+ if (isDragging) {
2612
+ isDragging = false;
2613
+ dragNode = null;
2614
+ controls.enabled = true;
2615
+ mouseDownPos = null;
2616
+ return;
2617
+ }
2618
+ dragNode = null;
2428
2619
  if (!mouseDownPos) return;
2429
2620
  const dx = Math.abs(e.clientX - mouseDownPos.x);
2430
2621
  const dy = Math.abs(e.clientY - mouseDownPos.y);
@@ -2472,6 +2663,22 @@ function setupInteraction(canvas, camera, controls, getNodesMesh, getNodes, call
2472
2663
  callbacks.onNodeClick(node);
2473
2664
  }, 250);
2474
2665
  } else {
2666
+ if (callbacks.onLinkClick && getEdgeData) {
2667
+ const edgeData = getEdgeData();
2668
+ if (edgeData == null ? void 0 : edgeData.positions) {
2669
+ const hitLink = findNearestEdge(
2670
+ e.clientX,
2671
+ e.clientY,
2672
+ rect,
2673
+ camera,
2674
+ edgeData
2675
+ );
2676
+ if (hitLink) {
2677
+ callbacks.onLinkClick(hitLink.link);
2678
+ return;
2679
+ }
2680
+ }
2681
+ }
2475
2682
  if (singleClickTimer) clearTimeout(singleClickTimer);
2476
2683
  singleClickTimer = null;
2477
2684
  lastClickTime = 0;
@@ -2479,13 +2686,9 @@ function setupInteraction(canvas, camera, controls, getNodesMesh, getNodes, call
2479
2686
  callbacks.onNodeClick(null);
2480
2687
  }
2481
2688
  }
2482
- const keyTarget = container ?? window;
2483
- function onKeyDown2(e) {
2484
- if (e.key === "Escape") callbacks.onNodeClick(null);
2485
- }
2486
- let lastHoveredId = null;
2487
- function onPointerMove2(e) {
2488
- if (!callbacks.onNodeHover) return;
2689
+ function onContextMenu2(e) {
2690
+ if (!callbacks.onContextMenu) return;
2691
+ e.preventDefault();
2489
2692
  const nodesMesh = getNodesMesh();
2490
2693
  if (!nodesMesh) return;
2491
2694
  const rect = canvas.getBoundingClientRect();
@@ -2496,20 +2699,21 @@ function setupInteraction(canvas, camera, controls, getNodesMesh, getNodes, call
2496
2699
  if (hits.length > 0 && hits[0].instanceId != null) {
2497
2700
  const nodes = getNodes();
2498
2701
  const node = nodes[hits[0].instanceId];
2499
- if (node && node.id !== lastHoveredId) {
2500
- lastHoveredId = node.id;
2501
- canvas.style.cursor = "pointer";
2502
- callbacks.onNodeHover(node);
2702
+ if (node) {
2703
+ callbacks.onContextMenu(node, { x: e.clientX, y: e.clientY });
2503
2704
  }
2504
- } else if (lastHoveredId !== null) {
2505
- lastHoveredId = null;
2506
- canvas.style.cursor = "default";
2507
- callbacks.onNodeHover(null);
2508
2705
  }
2509
2706
  }
2707
+ const keyTarget = container ?? window;
2708
+ function onKeyDown2(e) {
2709
+ if (e.key === "Escape") callbacks.onNodeClick(null);
2710
+ }
2711
+ let lastHoveredId = null;
2712
+ let lastHoveredLinkIdx = -1;
2510
2713
  canvas.addEventListener("pointerdown", onPointerDown2);
2511
2714
  canvas.addEventListener("pointerup", onPointerUp2);
2512
2715
  canvas.addEventListener("pointermove", onPointerMove2);
2716
+ canvas.addEventListener("contextmenu", onContextMenu2);
2513
2717
  keyTarget.addEventListener("keydown", onKeyDown2);
2514
2718
  return {
2515
2719
  cleanup: () => {
@@ -2517,10 +2721,48 @@ function setupInteraction(canvas, camera, controls, getNodesMesh, getNodes, call
2517
2721
  canvas.removeEventListener("pointerdown", onPointerDown2);
2518
2722
  canvas.removeEventListener("pointerup", onPointerUp2);
2519
2723
  canvas.removeEventListener("pointermove", onPointerMove2);
2724
+ canvas.removeEventListener("contextmenu", onContextMenu2);
2520
2725
  keyTarget.removeEventListener("keydown", onKeyDown2);
2521
2726
  }
2522
2727
  };
2523
2728
  }
2729
+ function findNearestEdge(clientX, clientY, rect, camera, edgeData) {
2730
+ const { links, edgeNodeIndices, edgeLinkIndices, positions } = edgeData;
2731
+ const screenX = clientX - rect.left;
2732
+ const screenY = clientY - rect.top;
2733
+ const w = rect.width;
2734
+ const h = rect.height;
2735
+ let bestDist = EDGE_HIT_THRESHOLD;
2736
+ let bestIdx = -1;
2737
+ const tmpVec = new THREE__namespace.Vector3();
2738
+ for (let i = 0; i < edgeNodeIndices.length; i++) {
2739
+ const [si, ti] = edgeNodeIndices[i];
2740
+ tmpVec.set(positions[si * 3], positions[si * 3 + 1], positions[si * 3 + 2]);
2741
+ tmpVec.project(camera);
2742
+ const sx1 = (tmpVec.x * 0.5 + 0.5) * w;
2743
+ const sy1 = (-tmpVec.y * 0.5 + 0.5) * h;
2744
+ tmpVec.set(positions[ti * 3], positions[ti * 3 + 1], positions[ti * 3 + 2]);
2745
+ tmpVec.project(camera);
2746
+ const sx2 = (tmpVec.x * 0.5 + 0.5) * w;
2747
+ const sy2 = (-tmpVec.y * 0.5 + 0.5) * h;
2748
+ const dist = pointToSegmentDist(screenX, screenY, sx1, sy1, sx2, sy2);
2749
+ if (dist < bestDist) {
2750
+ bestDist = dist;
2751
+ bestIdx = i;
2752
+ }
2753
+ }
2754
+ if (bestIdx === -1) return null;
2755
+ return { link: links[edgeLinkIndices[bestIdx]], index: bestIdx };
2756
+ }
2757
+ function pointToSegmentDist(px, py, ax, ay, bx, by) {
2758
+ const dx = bx - ax;
2759
+ const dy = by - ay;
2760
+ const lenSq = dx * dx + dy * dy;
2761
+ if (lenSq < 1e-4) return Math.hypot(px - ax, py - ay);
2762
+ let t = ((px - ax) * dx + (py - ay) * dy) / lenSq;
2763
+ t = Math.max(0, Math.min(1, t));
2764
+ return Math.hypot(px - (ax + t * dx), py - (ay + t * dy));
2765
+ }
2524
2766
  function disposeObject(obj) {
2525
2767
  if (!obj) return;
2526
2768
  const mesh = obj;
@@ -2542,6 +2784,13 @@ function NetworkGraph3DInner(props, ref) {
2542
2784
  onNodeClick,
2543
2785
  onNodeDoubleClick,
2544
2786
  onNodeHover,
2787
+ onContextMenu: onContextMenu2,
2788
+ onLinkClick,
2789
+ onLinkHover,
2790
+ onNodeDrag,
2791
+ onNodeDragEnd,
2792
+ onLayoutSettled,
2793
+ onLayoutTick,
2545
2794
  selectedNodeId = null,
2546
2795
  highlightHops = 3,
2547
2796
  theme: themeProp,
@@ -2574,13 +2823,28 @@ function NetworkGraph3DInner(props, ref) {
2574
2823
  const onNodeClickRef = react.useRef(onNodeClick);
2575
2824
  const onNodeDoubleClickRef = react.useRef(onNodeDoubleClick);
2576
2825
  const onNodeHoverRef = react.useRef(onNodeHover);
2826
+ const onContextMenuRef = react.useRef(onContextMenu2);
2827
+ const onLinkClickRef = react.useRef(onLinkClick);
2828
+ const onLinkHoverRef = react.useRef(onLinkHover);
2829
+ const onNodeDragRef = react.useRef(onNodeDrag);
2830
+ const onNodeDragEndRef = react.useRef(onNodeDragEnd);
2831
+ const onLayoutSettledRef = react.useRef(onLayoutSettled);
2832
+ const onLayoutTickRef = react.useRef(onLayoutTick);
2577
2833
  const styleRef = react.useRef(resolvedStyle);
2578
2834
  const themeRef = react.useRef(resolvedTheme);
2579
2835
  const selectedNodeIdRef = react.useRef(selectedNodeId);
2580
2836
  const labelFormatterRef = react.useRef(labelFormatter);
2837
+ const highlightSetRef = react.useRef(null);
2581
2838
  onNodeClickRef.current = onNodeClick;
2582
2839
  onNodeDoubleClickRef.current = onNodeDoubleClick;
2583
2840
  onNodeHoverRef.current = onNodeHover;
2841
+ onContextMenuRef.current = onContextMenu2;
2842
+ onLinkClickRef.current = onLinkClick;
2843
+ onLinkHoverRef.current = onLinkHover;
2844
+ onNodeDragRef.current = onNodeDrag;
2845
+ onNodeDragEndRef.current = onNodeDragEnd;
2846
+ onLayoutSettledRef.current = onLayoutSettled;
2847
+ onLayoutTickRef.current = onLayoutTick;
2584
2848
  styleRef.current = resolvedStyle;
2585
2849
  themeRef.current = resolvedTheme;
2586
2850
  selectedNodeIdRef.current = selectedNodeId;
@@ -2646,7 +2910,8 @@ function NetworkGraph3DInner(props, ref) {
2646
2910
  styleRef.current.labelScale,
2647
2911
  styleRef.current.labelThreshold,
2648
2912
  styleRef.current.maxLabels,
2649
- labelFormatterRef.current
2913
+ labelFormatterRef.current,
2914
+ highlightSetRef.current
2650
2915
  );
2651
2916
  });
2652
2917
  const resizeObserver = setupResize(container, state);
@@ -2671,9 +2936,52 @@ function NetworkGraph3DInner(props, ref) {
2671
2936
  onNodeHover: (node) => {
2672
2937
  var _a;
2673
2938
  return (_a = onNodeHoverRef.current) == null ? void 0 : _a.call(onNodeHoverRef, node);
2939
+ },
2940
+ onContextMenu: (node, pos) => {
2941
+ var _a;
2942
+ return (_a = onContextMenuRef.current) == null ? void 0 : _a.call(onContextMenuRef, node, pos);
2943
+ },
2944
+ onLinkClick: (link) => {
2945
+ var _a;
2946
+ return (_a = onLinkClickRef.current) == null ? void 0 : _a.call(onLinkClickRef, link);
2947
+ },
2948
+ onLinkHover: (link) => {
2949
+ var _a;
2950
+ return (_a = onLinkHoverRef.current) == null ? void 0 : _a.call(onLinkHoverRef, link);
2951
+ },
2952
+ onNodeDrag: (node, pos) => {
2953
+ var _a;
2954
+ (_a = onNodeDragRef.current) == null ? void 0 : _a.call(onNodeDragRef, node, pos);
2955
+ const d = dataRef.current;
2956
+ const idx = d.nodeIdToIndex.get(node.id);
2957
+ if (idx !== void 0 && d.positions) {
2958
+ d.positions[idx * 3] = pos.x;
2959
+ d.positions[idx * 3 + 1] = pos.y;
2960
+ d.positions[idx * 3 + 2] = pos.z;
2961
+ node.x = pos.x;
2962
+ node.y = pos.y;
2963
+ node.z = pos.z;
2964
+ const gObj = graphObjRef.current;
2965
+ if (gObj && d.scales) {
2966
+ updateNodePositions(gObj.nodesMesh, d.positions, d.scales, d.nodes.length);
2967
+ }
2968
+ }
2969
+ },
2970
+ onNodeDragEnd: (node, pos) => {
2971
+ var _a;
2972
+ return (_a = onNodeDragEndRef.current) == null ? void 0 : _a.call(onNodeDragEndRef, node, pos);
2674
2973
  }
2675
2974
  },
2676
- container
2975
+ container,
2976
+ () => {
2977
+ const d = dataRef.current;
2978
+ return {
2979
+ links: d.links,
2980
+ edgeNodeIndices: d.edgeNodeIndices,
2981
+ edgeLinkIndices: d.edgeLinkIndices,
2982
+ positions: d.positions
2983
+ };
2984
+ }
2677
2985
  );
2678
2986
  return () => {
2679
2987
  cancelAnim();
@@ -2813,6 +3121,7 @@ function NetworkGraph3DInner(props, ref) {
2813
3121
  console.error("Graphier: Layout worker error:", err);
2814
3122
  };
2815
3123
  worker.onmessage = (e) => {
3124
+ var _a, _b;
2816
3125
  const msg = e.data;
2817
3126
  if (msg.type === "positions") {
2818
3127
  const positions = new Float32Array(msg.positions);
@@ -2840,9 +3149,11 @@ function NetworkGraph3DInner(props, ref) {
2840
3149
  );
2841
3150
  }
2842
3151
  }
3152
+ (_a = onLayoutTickRef.current) == null ? void 0 : _a.call(onLayoutTickRef, msg.alpha);
2843
3153
  }
2844
3154
  if (msg.type === "settled") {
2845
3155
  dataRef.current.settled = true;
3156
+ (_b = onLayoutSettledRef.current) == null ? void 0 : _b.call(onLayoutSettledRef);
2846
3157
  }
2847
3158
  };
2848
3159
  const layoutParams = resolveLayoutParams(nc, layoutProp);
@@ -2887,6 +3198,7 @@ function NetworkGraph3DInner(props, ref) {
2887
3198
  const d = dataRef.current;
2888
3199
  const theme = themeRef.current;
2889
3200
  const style = styleRef.current;
3201
+ highlightSetRef.current = highlightSet;
2890
3202
  applyNodeHighlight(
2891
3203
  gObj.nodesMesh,
2892
3204
  d.nodes,
@@ -2904,6 +3216,18 @@ function NetworkGraph3DInner(props, ref) {
2904
3216
  d.nodes.length,
2905
3217
  theme
2906
3218
  );
3219
+ if (sceneState.bloomPass) {
3220
+ const style2 = styleRef.current;
3221
+ if (highlightSet) {
3222
+ sceneState.bloomPass.strength = style2.bloomStrength * 1.8;
3223
+ sceneState.bloomPass.radius = style2.bloomRadius * 1.4;
3224
+ sceneState.bloomPass.threshold = 0.25;
3225
+ } else {
3226
+ sceneState.bloomPass.strength = style2.bloomStrength;
3227
+ sceneState.bloomPass.radius = style2.bloomRadius;
3228
+ sceneState.bloomPass.threshold = style2.bloomThreshold;
3229
+ }
3230
+ }
2907
3231
  const selectedNode = selectedNodeId ? d.nodes.find((n) => n.id === selectedNodeId) : null;
2908
3232
  updateSelectionRing(
2909
3233
  sceneState.selectionRing,
@@ -2979,6 +3303,11 @@ function NetworkGraph3DInner(props, ref) {
2979
3303
  }, 30);
2980
3304
  return () => clearInterval(id);
2981
3305
  }, [resolvedStyle.autoOrbit]);
3306
+ react.useEffect(() => {
3307
+ const s = sceneRef.current;
3308
+ if (!s) return;
3309
+ s.keyboard.setFlySpeed(resolvedStyle.flySpeed);
3310
+ }, [resolvedStyle.flySpeed]);
2982
3311
  react.useImperativeHandle(ref, () => ({
2983
3312
  cameraPosition(pos, lookAt, duration = 1e3) {
2984
3313
  const s = sceneRef.current;
@@ -3051,13 +3380,21 @@ function NetworkGraph3DInner(props, ref) {
3051
3380
  var _a;
3052
3381
  return ((_a = sceneRef.current) == null ? void 0 : _a.camera) ?? null;
3053
3382
  },
3054
- async screenshot() {
3383
+ captureScreenshot() {
3055
3384
  const s = sceneRef.current;
3056
3385
  if (!s) return null;
3057
3386
  s.renderer.render(s.scene, s.camera);
3058
- return new Promise((resolve) => {
3059
- s.renderer.domElement.toBlob(resolve, "image/png");
3060
- });
3387
+ return s.renderer.domElement.toDataURL("image/png");
3388
+ },
3389
+ reheatLayout() {
3390
+ const gObj = graphObjRef.current;
3391
+ if (gObj == null ? void 0 : gObj.worker) {
3392
+ gObj.worker.postMessage({ type: "stop" });
3393
+ gObj.worker.terminate();
3394
+ gObj.worker = null;
3395
+ }
3396
+ dataRef.current.settled = false;
3397
+ setAppendedData((prev) => prev ? { ...prev } : { nodes: [], links: [] });
3061
3398
  }
3062
3399
  }));
3063
3400
  return /* @__PURE__ */ jsxRuntime.jsx(