@cocorof/graphier 1.1.1 → 1.3.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.d.ts CHANGED
@@ -102,6 +102,13 @@ export declare interface LayoutConfig {
102
102
  velocityDecay?: number;
103
103
  /** Convergence threshold — simulation stops when alpha drops below this (default: 0.005) */
104
104
  settledThreshold?: number;
105
+ /**
106
+ * Spread multiplier — scales charge strength and link distance to control
107
+ * how far apart nodes spread. 1.0 = default, 2.0 = twice as spread out.
108
+ * Automatically scaled by node count when set to "auto".
109
+ * (default: "auto")
110
+ */
111
+ spreadFactor?: "auto" | number;
105
112
  }
106
113
 
107
114
  /** Layout settled callback */
@@ -192,6 +199,8 @@ export declare interface NetworkGraph3DRef {
192
199
  getCamera(): THREE.PerspectiveCamera | null;
193
200
  /** Capture a screenshot as a PNG data URL string (synchronous, matches original) */
194
201
  captureScreenshot(): string | null;
202
+ /** Re-run force layout from current positions (useful after changing spreadFactor) */
203
+ reheatLayout(): void;
195
204
  }
196
205
 
197
206
  export declare function NodeDetailPanel(props: NodeDetailPanelProps): JSX_2.Element;
@@ -286,6 +295,11 @@ export declare interface StyleConfig {
286
295
  maxLabels?: number;
287
296
  /** Edge line width multiplier (default: 1.0). Note: WebGL limits linewidth to 1 on most platforms. */
288
297
  edgeWidthScale?: number;
298
+ /**
299
+ * Fly camera thrust speed multiplier (default: 1.0).
300
+ * 0.5 = half speed, 2.0 = double speed.
301
+ */
302
+ flySpeed?: number;
289
303
  }
290
304
 
291
305
  export declare interface SubgraphOptions {
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("#", "");
@@ -32,8 +34,8 @@ function hexToRgb(hex) {
32
34
  return [(n >> 16) / 255, (n >> 8 & 255) / 255, (n & 255) / 255];
33
35
  }
34
36
  function rgbToHex(r, g, b) {
35
- const clamp2 = (v) => Math.round(Math.max(0, Math.min(1, v)) * 255);
36
- const hex = clamp2(r) << 16 | clamp2(g) << 8 | clamp2(b);
37
+ const clamp = (v) => Math.round(Math.max(0, Math.min(1, v)) * 255);
38
+ const hex = clamp(r) << 16 | clamp(g) << 8 | clamp(b);
37
39
  return "#" + hex.toString(16).padStart(6, "0");
38
40
  }
39
41
  function brighten(hex, factor) {
@@ -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,
@@ -1943,60 +1954,45 @@ function disposeLabelSystem(state) {
1943
1954
  }
1944
1955
  state.textureCache.clear();
1945
1956
  }
1946
- const ORBIT_ACCEL = 12e-4;
1947
- const ORBIT_MAX_SPEED = 0.06;
1948
- const ORBIT_DAMPING = 0.88;
1957
+ const ORBIT_ROTATE_SPEED = 0.02;
1949
1958
  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;
1959
+ const FLY_THRUST_SPEED = 0.08;
1954
1960
  const FLY_TURN_RATE = 8e-3;
1955
- const FLY_DEAD_ZONE = 2e-4;
1956
1961
  const _offset = new THREE.Vector3();
1957
1962
  const _right = new THREE.Vector3();
1958
1963
  const _q = new THREE.Quaternion();
1959
1964
  const _dir = new THREE.Vector3();
1960
- function clamp(v, min, max) {
1961
- return v < min ? min : v > max ? max : v;
1962
- }
1965
+ const _worldUp = new THREE.Vector3(0, 1, 0);
1963
1966
  function createOrbitUpdate(camera, controls, keys) {
1964
- const vel = { zoom: 0, azimuth: 0, polar: 0 };
1965
1967
  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;
1981
- const hasMotion = vel.zoom !== 0 || vel.azimuth !== 0 || vel.polar !== 0;
1968
+ let zoom = 0;
1969
+ let azimuth = 0;
1970
+ let polar = 0;
1971
+ if (keys["z"] || keys["Z"]) zoom -= ORBIT_ROTATE_SPEED;
1972
+ if (keys["x"] || keys["X"]) zoom += ORBIT_ROTATE_SPEED;
1973
+ if (keys["ArrowLeft"]) azimuth -= ORBIT_ROTATE_SPEED;
1974
+ if (keys["ArrowRight"]) azimuth += ORBIT_ROTATE_SPEED;
1975
+ if (keys["ArrowUp"]) polar -= ORBIT_ROTATE_SPEED;
1976
+ if (keys["ArrowDown"]) polar += ORBIT_ROTATE_SPEED;
1977
+ const hasMotion = zoom !== 0 || azimuth !== 0 || polar !== 0;
1982
1978
  if (!hasMotion) return false;
1983
1979
  const target = controls.target;
1984
1980
  _offset.copy(camera.position).sub(target);
1985
1981
  let radius = _offset.length();
1986
- if (vel.zoom !== 0) {
1987
- radius *= 1 + vel.zoom * ORBIT_ZOOM_FACTOR * radius * 0.01;
1982
+ if (zoom !== 0) {
1983
+ radius *= 1 + zoom * ORBIT_ZOOM_FACTOR * radius * 0.01;
1988
1984
  radius = Math.max(controls.minDistance, Math.min(controls.maxDistance, radius));
1989
1985
  }
1990
- if (vel.azimuth !== 0) {
1991
- _q.setFromAxisAngle(camera.up, -vel.azimuth);
1986
+ if (azimuth !== 0) {
1987
+ _q.setFromAxisAngle(camera.up, -azimuth);
1992
1988
  _offset.applyQuaternion(_q);
1993
1989
  }
1994
- if (vel.polar !== 0) {
1990
+ if (polar !== 0) {
1995
1991
  _right.copy(_offset).normalize().cross(camera.up).normalize();
1996
1992
  if (_right.lengthSq() < 1e-3) {
1997
1993
  _right.setFromMatrixColumn(camera.matrixWorld, 0);
1998
1994
  }
1999
- _q.setFromAxisAngle(_right, vel.polar);
1995
+ _q.setFromAxisAngle(_right, polar);
2000
1996
  _offset.applyQuaternion(_q);
2001
1997
  camera.up.applyQuaternion(_q).normalize();
2002
1998
  }
@@ -2007,48 +2003,47 @@ function createOrbitUpdate(camera, controls, keys) {
2007
2003
  return true;
2008
2004
  };
2009
2005
  }
2010
- function createFlyUpdate(camera, controls, keys) {
2011
- const vel = { thrust: 0, yaw: 0, pitch: 0 };
2006
+ function createFlyUpdate(camera, controls, keys, state) {
2012
2007
  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;
2008
+ const speedMul = state.flySpeed;
2009
+ let thrust = 0;
2010
+ let yaw = 0;
2011
+ let pitch = 0;
2012
+ if (keys["z"] || keys["Z"]) thrust += FLY_THRUST_SPEED * speedMul;
2013
+ if (keys["x"] || keys["X"]) thrust -= FLY_THRUST_SPEED * speedMul;
2014
+ if (keys["a"] || keys["A"] || keys["ArrowLeft"]) yaw += FLY_TURN_RATE;
2015
+ if (keys["d"] || keys["D"] || keys["ArrowRight"]) yaw -= FLY_TURN_RATE;
2016
+ if (keys["w"] || keys["W"] || keys["ArrowUp"]) pitch += FLY_TURN_RATE;
2017
+ if (keys["s"] || keys["S"] || keys["ArrowDown"]) pitch -= FLY_TURN_RATE;
2018
+ const hasMotion = thrust !== 0 || yaw !== 0 || pitch !== 0;
2031
2019
  if (!hasMotion) return false;
2032
- if (vel.yaw !== 0) {
2033
- _q.setFromAxisAngle(camera.up, vel.yaw);
2020
+ if (yaw !== 0) {
2021
+ const upDot = camera.up.dot(_worldUp);
2022
+ const yawSign = upDot < 0 ? -1 : 1;
2023
+ _q.setFromAxisAngle(_worldUp, yaw * yawSign);
2034
2024
  camera.quaternion.premultiply(_q);
2025
+ camera.up.applyQuaternion(_q).normalize();
2035
2026
  }
2036
- if (vel.pitch !== 0) {
2027
+ if (pitch !== 0) {
2037
2028
  _right.setFromMatrixColumn(camera.matrixWorld, 0).normalize();
2038
- _q.setFromAxisAngle(_right, vel.pitch);
2029
+ _q.setFromAxisAngle(_right, pitch);
2039
2030
  camera.quaternion.premultiply(_q);
2031
+ camera.up.applyQuaternion(_q).normalize();
2040
2032
  }
2041
- if (vel.thrust !== 0) {
2033
+ if (thrust !== 0) {
2034
+ const lookDist = camera.position.distanceTo(controls.target);
2035
+ const distScale = Math.max(0.3, lookDist * 3e-3);
2042
2036
  camera.getWorldDirection(_dir);
2043
- _dir.multiplyScalar(vel.thrust * distScale * 100);
2037
+ _dir.multiplyScalar(thrust * distScale * 100);
2044
2038
  camera.position.add(_dir);
2045
2039
  controls.target.add(_dir);
2046
2040
  }
2047
2041
  return true;
2048
2042
  };
2049
2043
  }
2050
- function setupKeyboardControls(camera, controls, container, mode = "fly") {
2044
+ function setupKeyboardControls(camera, controls, container, mode = "fly", flySpeed = 1) {
2051
2045
  const keys = {};
2046
+ const state = { flySpeed };
2052
2047
  function onKeyDown2(e) {
2053
2048
  var _a;
2054
2049
  const tag = (_a = e.target) == null ? void 0 : _a.tagName;
@@ -2064,13 +2059,16 @@ function setupKeyboardControls(camera, controls, container, mode = "fly") {
2064
2059
  container.addEventListener("keydown", onKeyDown2);
2065
2060
  container.addEventListener("keyup", onKeyUp);
2066
2061
  container.addEventListener("blur", onBlur);
2067
- const updateFn = mode === "fly" ? createFlyUpdate(camera, controls, keys) : createOrbitUpdate(camera, controls, keys);
2062
+ const updateFn = mode === "fly" ? createFlyUpdate(camera, controls, keys, state) : createOrbitUpdate(camera, controls, keys);
2068
2063
  function cleanup() {
2069
2064
  container.removeEventListener("keydown", onKeyDown2);
2070
2065
  container.removeEventListener("keyup", onKeyUp);
2071
2066
  container.removeEventListener("blur", onBlur);
2072
2067
  }
2073
- return { update: updateFn, cleanup };
2068
+ function setFlySpeed(speed) {
2069
+ state.flySpeed = speed;
2070
+ }
2071
+ return { update: updateFn, cleanup, setFlySpeed };
2074
2072
  }
2075
2073
  function createScene(container, backgroundColor, style, rendererConfig) {
2076
2074
  const scene = new THREE.Scene();
@@ -2108,7 +2106,22 @@ function createScene(container, backgroundColor, style, rendererConfig) {
2108
2106
  const labels = createLabelSystem(style.maxLabels);
2109
2107
  scene.add(labels.group);
2110
2108
  const cameraMode = (rendererConfig == null ? void 0 : rendererConfig.cameraMode) ?? "fly";
2111
- const keyboard = setupKeyboardControls(camera, controls, container, cameraMode);
2109
+ const keyboard = setupKeyboardControls(camera, controls, container, cameraMode, style.flySpeed ?? 1);
2110
+ controls.enableZoom = false;
2111
+ const _wheelDir = new THREE.Vector3();
2112
+ const onWheel = (e) => {
2113
+ e.preventDefault();
2114
+ let delta = -e.deltaY;
2115
+ if (e.deltaMode === 1) delta *= 40;
2116
+ if (e.deltaMode === 2) delta *= 800;
2117
+ camera.getWorldDirection(_wheelDir);
2118
+ const dist = camera.position.distanceTo(controls.target);
2119
+ const moveAmount = delta * 2e-3 * Math.max(dist * 0.1, 1);
2120
+ camera.position.addScaledVector(_wheelDir, moveAmount);
2121
+ controls.target.addScaledVector(_wheelDir, moveAmount);
2122
+ };
2123
+ canvas.addEventListener("wheel", onWheel, { passive: false });
2124
+ const scrollCleanup = () => canvas.removeEventListener("wheel", onWheel);
2112
2125
  return {
2113
2126
  scene,
2114
2127
  camera,
@@ -2119,7 +2132,8 @@ function createScene(container, backgroundColor, style, rendererConfig) {
2119
2132
  labels,
2120
2133
  composer: null,
2121
2134
  bloomPass: null,
2122
- keyboard
2135
+ keyboard,
2136
+ scrollCleanup
2123
2137
  };
2124
2138
  }
2125
2139
  function initBloom(state, nodeCount, style) {
@@ -2146,9 +2160,21 @@ function initBloom(state, nodeCount, style) {
2146
2160
  }
2147
2161
  function startAnimationLoop(state, onTick) {
2148
2162
  let animFrame;
2163
+ let wasKeyboardActive = false;
2164
+ const _syncDir = new THREE.Vector3();
2149
2165
  function animate() {
2150
2166
  animFrame = requestAnimationFrame(animate);
2151
2167
  const keyboardActive = state.keyboard.update();
2168
+ if (wasKeyboardActive && !keyboardActive) {
2169
+ const dist = state.camera.position.distanceTo(
2170
+ state.controls.target
2171
+ );
2172
+ state.camera.getWorldDirection(_syncDir);
2173
+ state.controls.target.copy(state.camera.position).add(_syncDir.multiplyScalar(dist));
2174
+ state.camera.up.set(0, 1, 0);
2175
+ state.camera.lookAt(state.controls.target);
2176
+ }
2177
+ wasKeyboardActive = keyboardActive;
2152
2178
  if (!keyboardActive) state.controls.update();
2153
2179
  if (state.selectionRing.visible) {
2154
2180
  state.selectionRing.quaternion.copy(state.camera.quaternion);
@@ -2949,6 +2975,7 @@ function NetworkGraph3DInner(props, ref) {
2949
2975
  resizeObserver.disconnect();
2950
2976
  interaction.cleanup();
2951
2977
  state.keyboard.cleanup();
2978
+ state.scrollCleanup();
2952
2979
  state.controls.dispose();
2953
2980
  disposeObject(state.stars);
2954
2981
  disposeObject(state.selectionRing);
@@ -3264,6 +3291,11 @@ function NetworkGraph3DInner(props, ref) {
3264
3291
  }, 30);
3265
3292
  return () => clearInterval(id);
3266
3293
  }, [resolvedStyle.autoOrbit]);
3294
+ useEffect(() => {
3295
+ const s = sceneRef.current;
3296
+ if (!s) return;
3297
+ s.keyboard.setFlySpeed(resolvedStyle.flySpeed);
3298
+ }, [resolvedStyle.flySpeed]);
3267
3299
  useImperativeHandle(ref, () => ({
3268
3300
  cameraPosition(pos, lookAt, duration = 1e3) {
3269
3301
  const s = sceneRef.current;
@@ -3341,6 +3373,16 @@ function NetworkGraph3DInner(props, ref) {
3341
3373
  if (!s) return null;
3342
3374
  s.renderer.render(s.scene, s.camera);
3343
3375
  return s.renderer.domElement.toDataURL("image/png");
3376
+ },
3377
+ reheatLayout() {
3378
+ const gObj = graphObjRef.current;
3379
+ if (gObj == null ? void 0 : gObj.worker) {
3380
+ gObj.worker.postMessage({ type: "stop" });
3381
+ gObj.worker.terminate();
3382
+ gObj.worker = null;
3383
+ }
3384
+ dataRef.current.settled = false;
3385
+ setAppendedData((prev) => prev ? { ...prev } : { nodes: [], links: [] });
3344
3386
  }
3345
3387
  }));
3346
3388
  return /* @__PURE__ */ jsx(