@omiron33/omi-neuron-web 0.2.25 → 0.2.26

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.
@@ -339,6 +339,19 @@ interface NeuronWebProps {
339
339
  onNodeDrag?: (node: NeuronNode, position: [number, number, number]) => void;
340
340
  /** Called when a node drag operation completes. */
341
341
  onNodeDragEnd?: (node: NeuronNode, position: [number, number, number]) => void;
342
+ /**
343
+ * Enable keyboard controls for camera movement:
344
+ * - WASD: Pan camera
345
+ * - Q/E: Zoom in/out
346
+ * This frees the mouse for node interaction.
347
+ */
348
+ keyboardControls?: boolean | {
349
+ enabled: boolean;
350
+ /** Pan speed multiplier (default: 1) */
351
+ panSpeed?: number;
352
+ /** Zoom speed multiplier (default: 1) */
353
+ zoomSpeed?: number;
354
+ };
342
355
  /** When set, plays a story beat by id using the built-in study path player. */
343
356
  activeStoryBeatId?: string | null;
344
357
  /** Optional override for story beat step duration (ms). */
@@ -424,7 +437,7 @@ interface NeuronWebExplorerProps {
424
437
  renderLoadingState?: () => React__default.ReactNode;
425
438
  }
426
439
 
427
- declare function NeuronWeb({ graphData, className, style, fullHeight, isFullScreen, isLoading, error, selectedNode, focusNodeSlug, onFocusConsumed, visibleNodeSlugs, renderEmptyState, renderLoadingState, ariaLabel, theme, domainColors, layout, cameraFit, cardsMode, clickCard, clickZoom, studyPathRequest, onStudyPathComplete, renderNodeHover, renderNodeDetail, hoverCard, hoverCardSlots, onNodeHover, onNodeClick, onNodeDoubleClick, onNodeFocused, onEdgeClick, onBackgroundClick, onCameraChange, draggable, onNodeDrag, onNodeDragEnd, performanceMode, density, rendering, activeStoryBeatId, storyBeatStepDurationMs, onStoryBeatComplete, }: NeuronWebProps): React__default.ReactElement;
440
+ declare function NeuronWeb({ graphData, className, style, fullHeight, isFullScreen, isLoading, error, selectedNode, focusNodeSlug, onFocusConsumed, visibleNodeSlugs, renderEmptyState, renderLoadingState, ariaLabel, theme, domainColors, layout, cameraFit, cardsMode, clickCard, clickZoom, studyPathRequest, onStudyPathComplete, renderNodeHover, renderNodeDetail, hoverCard, hoverCardSlots, onNodeHover, onNodeClick, onNodeDoubleClick, onNodeFocused, onEdgeClick, onBackgroundClick, onCameraChange, draggable, onNodeDrag, onNodeDragEnd, keyboardControls, performanceMode, density, rendering, activeStoryBeatId, storyBeatStepDurationMs, onStoryBeatComplete, }: NeuronWebProps): React__default.ReactElement;
428
441
 
429
442
  declare function NeuronWebExplorer(props: NeuronWebExplorerProps): React__default.ReactElement;
430
443
 
@@ -339,6 +339,19 @@ interface NeuronWebProps {
339
339
  onNodeDrag?: (node: NeuronNode, position: [number, number, number]) => void;
340
340
  /** Called when a node drag operation completes. */
341
341
  onNodeDragEnd?: (node: NeuronNode, position: [number, number, number]) => void;
342
+ /**
343
+ * Enable keyboard controls for camera movement:
344
+ * - WASD: Pan camera
345
+ * - Q/E: Zoom in/out
346
+ * This frees the mouse for node interaction.
347
+ */
348
+ keyboardControls?: boolean | {
349
+ enabled: boolean;
350
+ /** Pan speed multiplier (default: 1) */
351
+ panSpeed?: number;
352
+ /** Zoom speed multiplier (default: 1) */
353
+ zoomSpeed?: number;
354
+ };
342
355
  /** When set, plays a story beat by id using the built-in study path player. */
343
356
  activeStoryBeatId?: string | null;
344
357
  /** Optional override for story beat step duration (ms). */
@@ -424,7 +437,7 @@ interface NeuronWebExplorerProps {
424
437
  renderLoadingState?: () => React__default.ReactNode;
425
438
  }
426
439
 
427
- declare function NeuronWeb({ graphData, className, style, fullHeight, isFullScreen, isLoading, error, selectedNode, focusNodeSlug, onFocusConsumed, visibleNodeSlugs, renderEmptyState, renderLoadingState, ariaLabel, theme, domainColors, layout, cameraFit, cardsMode, clickCard, clickZoom, studyPathRequest, onStudyPathComplete, renderNodeHover, renderNodeDetail, hoverCard, hoverCardSlots, onNodeHover, onNodeClick, onNodeDoubleClick, onNodeFocused, onEdgeClick, onBackgroundClick, onCameraChange, draggable, onNodeDrag, onNodeDragEnd, performanceMode, density, rendering, activeStoryBeatId, storyBeatStepDurationMs, onStoryBeatComplete, }: NeuronWebProps): React__default.ReactElement;
440
+ declare function NeuronWeb({ graphData, className, style, fullHeight, isFullScreen, isLoading, error, selectedNode, focusNodeSlug, onFocusConsumed, visibleNodeSlugs, renderEmptyState, renderLoadingState, ariaLabel, theme, domainColors, layout, cameraFit, cardsMode, clickCard, clickZoom, studyPathRequest, onStudyPathComplete, renderNodeHover, renderNodeDetail, hoverCard, hoverCardSlots, onNodeHover, onNodeClick, onNodeDoubleClick, onNodeFocused, onEdgeClick, onBackgroundClick, onCameraChange, draggable, onNodeDrag, onNodeDragEnd, keyboardControls, performanceMode, density, rendering, activeStoryBeatId, storyBeatStepDurationMs, onStoryBeatComplete, }: NeuronWebProps): React__default.ReactElement;
428
441
 
429
442
  declare function NeuronWebExplorer(props: NeuronWebExplorerProps): React__default.ReactElement;
430
443
 
@@ -2585,6 +2585,12 @@ var InteractionManager = class {
2585
2585
  };
2586
2586
  onBackgroundClick = () => {
2587
2587
  };
2588
+ /** Called when pointer-down occurs on a node (before drag threshold). Use to disable controls. */
2589
+ onNodePointerDown = () => {
2590
+ };
2591
+ /** Called when pointer-up occurs without a drag. Use to re-enable controls. */
2592
+ onNodePointerUp = () => {
2593
+ };
2588
2594
  onNodeDragStart = () => {
2589
2595
  };
2590
2596
  onNodeDrag = () => {
@@ -2635,6 +2641,7 @@ var InteractionManager = class {
2635
2641
  this.pointerDownPosition.copy(this.pointer);
2636
2642
  const node = this.getIntersectedNode(this.pointer);
2637
2643
  if (node) {
2644
+ this.onNodePointerDown(node);
2638
2645
  const nodeObject = this.nodeObjects.find(
2639
2646
  (obj) => obj.userData?.nodeId === node.id
2640
2647
  );
@@ -2676,7 +2683,10 @@ var InteractionManager = class {
2676
2683
  this.dragNode = null;
2677
2684
  return;
2678
2685
  }
2679
- this.dragNode = null;
2686
+ if (this.dragNode) {
2687
+ this.onNodePointerUp();
2688
+ this.dragNode = null;
2689
+ }
2680
2690
  if (!this.config.enableClick) return;
2681
2691
  const node = this.getIntersectedNode(this.pointer);
2682
2692
  if (node) {
@@ -2939,6 +2949,7 @@ function NeuronWeb({
2939
2949
  draggable,
2940
2950
  onNodeDrag,
2941
2951
  onNodeDragEnd,
2952
+ keyboardControls,
2942
2953
  performanceMode,
2943
2954
  density,
2944
2955
  rendering,
@@ -3378,6 +3389,75 @@ function NeuronWeb({
3378
3389
  constrainToPlane: draggable.constrainToPlane ?? "xy"
3379
3390
  };
3380
3391
  }, [draggable]);
3392
+ const keyboardControlsConfig = useMemo(() => {
3393
+ if (!keyboardControls) return { enabled: false, panSpeed: 1, zoomSpeed: 1 };
3394
+ if (keyboardControls === true) {
3395
+ return { enabled: true, panSpeed: 1, zoomSpeed: 1 };
3396
+ }
3397
+ return {
3398
+ enabled: keyboardControls.enabled,
3399
+ panSpeed: keyboardControls.panSpeed ?? 1,
3400
+ zoomSpeed: keyboardControls.zoomSpeed ?? 1
3401
+ };
3402
+ }, [keyboardControls]);
3403
+ const pressedKeysRef = useRef(/* @__PURE__ */ new Set());
3404
+ useEffect(() => {
3405
+ if (!keyboardControlsConfig.enabled || !sceneManager) return;
3406
+ const basePanSpeed = 0.15 * keyboardControlsConfig.panSpeed;
3407
+ const baseZoomSpeed = 0.5 * keyboardControlsConfig.zoomSpeed;
3408
+ const handleKeyDown2 = (e) => {
3409
+ const key = e.key.toLowerCase();
3410
+ if (["w", "a", "s", "d", "q", "e"].includes(key)) {
3411
+ pressedKeysRef.current.add(key);
3412
+ e.preventDefault();
3413
+ }
3414
+ };
3415
+ const handleKeyUp = (e) => {
3416
+ const key = e.key.toLowerCase();
3417
+ pressedKeysRef.current.delete(key);
3418
+ };
3419
+ let animationFrameId;
3420
+ const animate = () => {
3421
+ const keys = pressedKeysRef.current;
3422
+ if (keys.size > 0) {
3423
+ const camera = sceneManager.camera;
3424
+ const controls = sceneManager.controls;
3425
+ const right = new THREE.Vector3();
3426
+ const up = new THREE.Vector3();
3427
+ camera.getWorldDirection(up);
3428
+ right.crossVectors(camera.up, up).normalize();
3429
+ up.copy(camera.up);
3430
+ let dx = 0, dy = 0, dz = 0;
3431
+ if (keys.has("a")) dx -= basePanSpeed;
3432
+ if (keys.has("d")) dx += basePanSpeed;
3433
+ if (keys.has("w")) dy += basePanSpeed;
3434
+ if (keys.has("s")) dy -= basePanSpeed;
3435
+ if (keys.has("q")) dz += baseZoomSpeed;
3436
+ if (keys.has("e")) dz -= baseZoomSpeed;
3437
+ const panOffset = right.multiplyScalar(dx).add(up.multiplyScalar(dy));
3438
+ camera.position.add(panOffset);
3439
+ controls.target.add(panOffset);
3440
+ if (dz !== 0) {
3441
+ const direction = new THREE.Vector3();
3442
+ camera.getWorldDirection(direction);
3443
+ const distance = camera.position.distanceTo(controls.target);
3444
+ const newDistance = Math.max(controls.minDistance, Math.min(controls.maxDistance, distance - dz));
3445
+ camera.position.copy(controls.target).add(direction.multiplyScalar(-newDistance));
3446
+ }
3447
+ controls.update();
3448
+ }
3449
+ animationFrameId = requestAnimationFrame(animate);
3450
+ };
3451
+ window.addEventListener("keydown", handleKeyDown2);
3452
+ window.addEventListener("keyup", handleKeyUp);
3453
+ animationFrameId = requestAnimationFrame(animate);
3454
+ return () => {
3455
+ window.removeEventListener("keydown", handleKeyDown2);
3456
+ window.removeEventListener("keyup", handleKeyUp);
3457
+ cancelAnimationFrame(animationFrameId);
3458
+ pressedKeysRef.current.clear();
3459
+ };
3460
+ }, [keyboardControlsConfig, sceneManager]);
3381
3461
  const interactionManager = useMemo(() => {
3382
3462
  if (!sceneManager) return null;
3383
3463
  return new InteractionManager(sceneManager.scene, sceneManager.camera, sceneManager.renderer, {
@@ -4178,11 +4258,18 @@ function NeuronWeb({
4178
4258
  }
4179
4259
  };
4180
4260
  if (dragConfig.enabled) {
4181
- interactionManager.onNodeDragStart = () => {
4261
+ interactionManager.onNodePointerDown = () => {
4182
4262
  if (sceneManager) {
4183
4263
  sceneManager.controls.enabled = false;
4184
4264
  }
4185
4265
  };
4266
+ interactionManager.onNodePointerUp = () => {
4267
+ if (sceneManager) {
4268
+ sceneManager.controls.enabled = true;
4269
+ }
4270
+ };
4271
+ interactionManager.onNodeDragStart = () => {
4272
+ };
4186
4273
  interactionManager.onNodeDrag = (node, position) => {
4187
4274
  nodeRenderer.updateNodePosition(node.id, position);
4188
4275
  if (onNodeDrag) {
@@ -4772,5 +4859,5 @@ function dedupePreserveOrder(values) {
4772
4859
  }
4773
4860
 
4774
4861
  export { DEFAULT_RENDERING_OPTIONS, DEFAULT_STATUS_COLORS, DEFAULT_THEME, NeuronContext, NeuronWeb, NeuronWebExplorer, SceneManager, ThemeEngine, applyFuzzyLayout, applyTreeLayout, createStoryBeat, createStudyPathFromBeat, createStudyPathFromNodeIds, getAutoPerformanceMode, normalizeStoryBeat, useNeuronContext, useNeuronGraph, validateStoryBeat };
4775
- //# sourceMappingURL=chunk-XQ7UTBAR.js.map
4776
- //# sourceMappingURL=chunk-XQ7UTBAR.js.map
4862
+ //# sourceMappingURL=chunk-QKCKZYLH.js.map
4863
+ //# sourceMappingURL=chunk-QKCKZYLH.js.map