@cocorof/graphier 1.4.0 → 1.4.2

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/README.md CHANGED
@@ -39,12 +39,14 @@ const ref = useRef<NetworkGraph3DRef>(null);
39
39
  <GraphMinimap graphRef={ref} width={200} height={140} />
40
40
  ```
41
41
 
42
- - `layout.dimensions: 2` — simulation runs in 2D (z locked to 0); camera locks to pan/zoom (drag pans, wheel/pinch zooms, rotation off).
42
+ - `layout.dimensions: 2` — flat Obsidian-style plane: simulation runs in 2D (z locked to 0). Left-drag pans, right-drag tilts/orbits the world, wheel/pinch zooms, arrows/WASD pan and z/x zoom (v1.4.1).
43
+ - `enableNodeDrag={false}` — left-drag always reaches the camera even over nodes; essential for dense graphs (v1.4.1).
43
44
  - `layout.clusterBy: "type" | "group"` + `clusterStrength` — pulls same-key nodes toward a shared centroid so categories form visible clusters.
44
45
  - `linkVisibility={(link) => bool}` — reheat-free per-edge filter (e.g. hide a link type).
45
46
  - `visibleNodeIds` — client-side filter; hidden nodes/edges/labels vanish via per-instance scale + collapsed segments. Positions are preserved: toggling filters never reheats the simulation.
46
47
  - `hoverHighlight` / `hoverHighlightHops` — Obsidian-style neighborhood emphasis on hover (selection wins while active).
47
48
  - `theme="paper"` — light background preset; edge blending switches to normal automatically (additive lines vanish on white) and highlight/dim directions invert.
49
+ - `renderer.navigation` — remap pointer/keyboard navigation per consumer, e.g. `{ leftButton: "pan", rightButton: "rotate", keyboard: "pan" }` for pan-first 3D graphs (v1.4.2).
48
50
  - `GraphMinimap` — 2D-canvas overview (no second WebGL context): draws all visible nodes + the camera viewport rectangle, click/drag to pan. Powered by `ref.getGraphSnapshot()` / `ref.getViewportRect()` / `ref.panTo(x, y)`.
49
51
 
50
52
  ## Install
package/dist/index.cjs CHANGED
@@ -2049,6 +2049,45 @@ const _right = new THREE__namespace.Vector3();
2049
2049
  const _q = new THREE__namespace.Quaternion();
2050
2050
  const _dir = new THREE__namespace.Vector3();
2051
2051
  const _worldUp = new THREE__namespace.Vector3(0, 1, 0);
2052
+ const PAN_SPEED = 0.02;
2053
+ const PAN_ZOOM_FACTOR = 0.02;
2054
+ function createPanUpdate(camera, controls, keys) {
2055
+ const _panRight = new THREE__namespace.Vector3();
2056
+ const _panUp = new THREE__namespace.Vector3();
2057
+ const _panOffset = new THREE__namespace.Vector3();
2058
+ return function update() {
2059
+ let dx = 0;
2060
+ let dy = 0;
2061
+ if (keys["ArrowLeft"] || keys["a"] || keys["A"]) dx -= 1;
2062
+ if (keys["ArrowRight"] || keys["d"] || keys["D"]) dx += 1;
2063
+ if (keys["ArrowUp"] || keys["w"] || keys["W"]) dy += 1;
2064
+ if (keys["ArrowDown"] || keys["s"] || keys["S"]) dy -= 1;
2065
+ const zoomIn = keys["z"] || keys["Z"] || keys["+"] || keys["="];
2066
+ const zoomOut = keys["x"] || keys["X"] || keys["-"] || keys["_"];
2067
+ if (!dx && !dy && !zoomIn && !zoomOut) return false;
2068
+ const target = controls.target;
2069
+ const dist = camera.position.distanceTo(target);
2070
+ if (dx || dy) {
2071
+ const step = Math.max(dist, 50) * PAN_SPEED;
2072
+ _panRight.setFromMatrixColumn(camera.matrix, 0);
2073
+ _panUp.setFromMatrixColumn(camera.matrix, 1);
2074
+ _panOffset.set(0, 0, 0).addScaledVector(_panRight, dx * step).addScaledVector(_panUp, dy * step);
2075
+ camera.position.add(_panOffset);
2076
+ target.add(_panOffset);
2077
+ }
2078
+ if (zoomIn || zoomOut) {
2079
+ const factor = zoomIn ? -PAN_ZOOM_FACTOR : PAN_ZOOM_FACTOR;
2080
+ _panOffset.copy(camera.position).sub(target);
2081
+ const newLen = Math.max(
2082
+ controls.minDistance,
2083
+ Math.min(controls.maxDistance, _panOffset.length() * (1 + factor))
2084
+ );
2085
+ _panOffset.setLength(newLen);
2086
+ camera.position.copy(target).add(_panOffset);
2087
+ }
2088
+ return true;
2089
+ };
2090
+ }
2052
2091
  function createOrbitUpdate(camera, controls, keys) {
2053
2092
  return function update() {
2054
2093
  let zoom = 0;
@@ -2148,8 +2187,13 @@ function setupKeyboardControls(camera, controls, container, mode = "fly", flySpe
2148
2187
  container.addEventListener("keyup", onKeyUp);
2149
2188
  container.addEventListener("blur", onBlur);
2150
2189
  let enabled = true;
2151
- const innerUpdate = mode === "fly" ? createFlyUpdate(camera, controls, keys, state) : createOrbitUpdate(camera, controls, keys);
2152
- const updateFn = () => enabled ? innerUpdate() : false;
2190
+ let currentMode = mode;
2191
+ const updaters = {
2192
+ fly: createFlyUpdate(camera, controls, keys, state),
2193
+ orbit: createOrbitUpdate(camera, controls, keys),
2194
+ pan: createPanUpdate(camera, controls, keys)
2195
+ };
2196
+ const updateFn = () => enabled ? updaters[currentMode]() : false;
2153
2197
  function cleanup() {
2154
2198
  container.removeEventListener("keydown", onKeyDown2);
2155
2199
  container.removeEventListener("keyup", onKeyUp);
@@ -2162,7 +2206,11 @@ function setupKeyboardControls(camera, controls, container, mode = "fly", flySpe
2162
2206
  enabled = on;
2163
2207
  if (!on) for (const k of Object.keys(keys)) keys[k] = false;
2164
2208
  }
2165
- return { update: updateFn, cleanup, setFlySpeed, setEnabled };
2209
+ function setMode(m2) {
2210
+ currentMode = m2;
2211
+ for (const k of Object.keys(keys)) keys[k] = false;
2212
+ }
2213
+ return { update: updateFn, cleanup, setFlySpeed, setEnabled, setMode };
2166
2214
  }
2167
2215
  function createScene(container, backgroundColor, style, rendererConfig) {
2168
2216
  const scene = new THREE__namespace.Scene();
@@ -2202,7 +2250,10 @@ function createScene(container, backgroundColor, style, rendererConfig) {
2202
2250
  const cameraMode = (rendererConfig == null ? void 0 : rendererConfig.cameraMode) ?? "fly";
2203
2251
  const keyboard = setupKeyboardControls(camera, controls, container, cameraMode, style.flySpeed ?? 1);
2204
2252
  controls.enableZoom = false;
2205
- const flags = { is2D: false };
2253
+ const flags = {
2254
+ is2D: false,
2255
+ keyboardMode3D: cameraMode
2256
+ };
2206
2257
  const _wheelDir = new THREE__namespace.Vector3();
2207
2258
  const onWheel = (e) => {
2208
2259
  if (flags.is2D) return;
@@ -2233,33 +2284,38 @@ function createScene(container, backgroundColor, style, rendererConfig) {
2233
2284
  flags
2234
2285
  };
2235
2286
  }
2236
- function applyCameraMode2D(state, is2D) {
2287
+ const MOUSE_ACTION = {
2288
+ rotate: THREE__namespace.MOUSE.ROTATE,
2289
+ pan: THREE__namespace.MOUSE.PAN
2290
+ };
2291
+ function applyNavigationMode(state, is2D, nav) {
2237
2292
  const { controls, camera, keyboard } = state;
2238
2293
  state.flags.is2D = is2D;
2239
- controls.enableRotate = !is2D;
2294
+ const left = (nav == null ? void 0 : nav.leftButton) ?? (is2D ? "pan" : "rotate");
2295
+ const right = (nav == null ? void 0 : nav.rightButton) ?? (is2D ? "rotate" : "pan");
2296
+ controls.mouseButtons = {
2297
+ LEFT: MOUSE_ACTION[left],
2298
+ MIDDLE: THREE__namespace.MOUSE.DOLLY,
2299
+ RIGHT: MOUSE_ACTION[right]
2300
+ };
2301
+ controls.enableRotate = left === "rotate" || right === "rotate";
2302
+ controls.touches = left === "pan" ? { ONE: THREE__namespace.TOUCH.PAN, TWO: THREE__namespace.TOUCH.DOLLY_PAN } : { ONE: THREE__namespace.TOUCH.ROTATE, TWO: THREE__namespace.TOUCH.DOLLY_PAN };
2240
2303
  controls.enableZoom = is2D;
2241
2304
  controls.screenSpacePanning = is2D;
2242
- keyboard.setEnabled(!is2D);
2305
+ const kb = (nav == null ? void 0 : nav.keyboard) ?? (is2D ? "pan" : state.flags.keyboardMode3D);
2306
+ if (kb === "off") {
2307
+ keyboard.setEnabled(false);
2308
+ } else {
2309
+ keyboard.setEnabled(true);
2310
+ keyboard.setMode(kb);
2311
+ }
2243
2312
  if (is2D) {
2244
- controls.mouseButtons = {
2245
- LEFT: THREE__namespace.MOUSE.PAN,
2246
- MIDDLE: THREE__namespace.MOUSE.DOLLY,
2247
- RIGHT: THREE__namespace.MOUSE.PAN
2248
- };
2249
- controls.touches = { ONE: THREE__namespace.TOUCH.PAN, TWO: THREE__namespace.TOUCH.DOLLY_PAN };
2250
2313
  const t = controls.target;
2251
2314
  const dist = Math.max(camera.position.distanceTo(t), 50);
2252
2315
  t.z = 0;
2253
2316
  camera.up.set(0, 1, 0);
2254
2317
  camera.position.set(t.x, t.y, dist);
2255
2318
  camera.lookAt(t);
2256
- } else {
2257
- controls.mouseButtons = {
2258
- LEFT: THREE__namespace.MOUSE.ROTATE,
2259
- MIDDLE: THREE__namespace.MOUSE.DOLLY,
2260
- RIGHT: THREE__namespace.MOUSE.PAN
2261
- };
2262
- controls.touches = { ONE: THREE__namespace.TOUCH.ROTATE, TWO: THREE__namespace.TOUCH.DOLLY_PAN };
2263
2319
  }
2264
2320
  controls.update();
2265
2321
  }
@@ -2622,11 +2678,12 @@ function setupInteraction(canvas, camera, controls, getNodesMesh, getNodes, call
2622
2678
  const dragPlane = new THREE__namespace.Plane();
2623
2679
  const dragIntersect = new THREE__namespace.Vector3();
2624
2680
  function onPointerDown2(e) {
2681
+ var _a;
2625
2682
  mouseDownPos = { x: e.clientX, y: e.clientY };
2626
2683
  if (container && document.activeElement !== container) {
2627
2684
  container.focus({ preventScroll: true });
2628
2685
  }
2629
- if (callbacks.onNodeDrag) {
2686
+ if (callbacks.onNodeDrag && (((_a = options == null ? void 0 : options.getNodeDragEnabled) == null ? void 0 : _a.call(options)) ?? true)) {
2630
2687
  const nodesMesh = getNodesMesh();
2631
2688
  if (nodesMesh) {
2632
2689
  const rect = canvas.getBoundingClientRect();
@@ -2938,6 +2995,7 @@ function NetworkGraph3DInner(props, ref) {
2938
2995
  nodeValueAccessor,
2939
2996
  visibleNodeIds,
2940
2997
  linkVisibility,
2998
+ enableNodeDrag = true,
2941
2999
  clickToFocus = true,
2942
3000
  hoverHighlight = false,
2943
3001
  hoverHighlightHops = 1
@@ -2978,6 +3036,7 @@ function NetworkGraph3DInner(props, ref) {
2978
3036
  const labelFormatterRef = react.useRef(labelFormatter);
2979
3037
  const highlightSetRef = react.useRef(null);
2980
3038
  const clickToFocusRef = react.useRef(clickToFocus);
3039
+ const enableNodeDragRef = react.useRef(enableNodeDrag);
2981
3040
  const hoverHighlightRef = react.useRef(hoverHighlight);
2982
3041
  const hoverHighlightHopsRef = react.useRef(hoverHighlightHops);
2983
3042
  const adjacencyMapRef = react.useRef(/* @__PURE__ */ new Map());
@@ -2999,6 +3058,7 @@ function NetworkGraph3DInner(props, ref) {
2999
3058
  selectedNodeIdRef.current = selectedNodeId;
3000
3059
  labelFormatterRef.current = labelFormatter;
3001
3060
  clickToFocusRef.current = clickToFocus;
3061
+ enableNodeDragRef.current = enableNodeDrag;
3002
3062
  hoverHighlightRef.current = hoverHighlight;
3003
3063
  hoverHighlightHopsRef.current = hoverHighlightHops;
3004
3064
  const is2D = (layoutProp == null ? void 0 : layoutProp.dimensions) === 2;
@@ -3181,7 +3241,8 @@ function NetworkGraph3DInner(props, ref) {
3181
3241
  },
3182
3242
  {
3183
3243
  getClickToFocus: () => clickToFocusRef.current,
3184
- getIs2D: () => is2DRef.current
3244
+ getIs2D: () => is2DRef.current,
3245
+ getNodeDragEnabled: () => enableNodeDragRef.current
3185
3246
  }
3186
3247
  );
3187
3248
  return () => {
@@ -3461,10 +3522,11 @@ function NetworkGraph3DInner(props, ref) {
3461
3522
  );
3462
3523
  }
3463
3524
  }, [visibleSet, linkVisibility, mergedData]);
3525
+ const navKey = JSON.stringify((rendererProp == null ? void 0 : rendererProp.navigation) ?? null);
3464
3526
  react.useEffect(() => {
3465
3527
  const s = sceneRef.current;
3466
- if (s) applyCameraMode2D(s, is2D);
3467
- }, [is2D]);
3528
+ if (s) applyNavigationMode(s, is2D, rendererProp == null ? void 0 : rendererProp.navigation);
3529
+ }, [is2D, navKey]);
3468
3530
  react.useEffect(() => {
3469
3531
  const gObj = graphObjRef.current;
3470
3532
  const sceneState = sceneRef.current;
@@ -3716,11 +3778,12 @@ function NetworkGraph3DInner(props, ref) {
3716
3778
  const s = sceneRef.current;
3717
3779
  if (!s) return null;
3718
3780
  const cam = s.camera;
3719
- const dist = Math.abs(cam.position.z);
3781
+ const t = s.controls.target;
3782
+ const dist = Math.max(cam.position.distanceTo(t), 1);
3720
3783
  const halfH = Math.tan(cam.fov * Math.PI / 360) * dist;
3721
3784
  return {
3722
- cx: cam.position.x,
3723
- cy: cam.position.y,
3785
+ cx: t.x,
3786
+ cy: t.y,
3724
3787
  halfW: halfH * cam.aspect,
3725
3788
  halfH
3726
3789
  };