@omiron33/omi-neuron-web 0.2.24 → 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.
- package/dist/{NeuronWebExplorer-XMtk_7z-.d.ts → NeuronWebExplorer-BBTb828y.d.ts} +33 -1
- package/dist/{NeuronWebExplorer-OYEmP22R.d.cts → NeuronWebExplorer-C7WGF5kT.d.cts} +33 -1
- package/dist/{chunk-FROPRJSB.js → chunk-QKCKZYLH.js} +261 -30
- package/dist/chunk-QKCKZYLH.js.map +1 -0
- package/dist/{chunk-GSVCFN4Y.cjs → chunk-RCLZFQVX.cjs} +261 -30
- package/dist/chunk-RCLZFQVX.cjs.map +1 -0
- package/dist/index.cjs +17 -17
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/visualization/index.cjs +16 -16
- package/dist/visualization/index.d.cts +2 -2
- package/dist/visualization/index.d.ts +2 -2
- package/dist/visualization/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-FROPRJSB.js.map +0 -1
- package/dist/chunk-GSVCFN4Y.cjs.map +0 -1
|
@@ -109,6 +109,12 @@ interface TreeLayoutOptions {
|
|
|
109
109
|
rootNodeId?: string;
|
|
110
110
|
/** Direction the tree grows: 'down'/'up' for vertical, 'left'/'right' for horizontal (default: 'down') */
|
|
111
111
|
direction?: 'down' | 'up' | 'left' | 'right';
|
|
112
|
+
/**
|
|
113
|
+
* When true, reverses edge interpretation: edge.from becomes child and edge.to becomes parent.
|
|
114
|
+
* Use this if your edges point from child to parent (e.g., "derives from" relationships).
|
|
115
|
+
* Default: false (edges point from parent to child)
|
|
116
|
+
*/
|
|
117
|
+
reverseEdgeDirection?: boolean;
|
|
112
118
|
}
|
|
113
119
|
interface NeuronLayoutOptions {
|
|
114
120
|
mode?: NeuronLayoutMode;
|
|
@@ -320,6 +326,32 @@ interface NeuronWebProps {
|
|
|
320
326
|
onEdgeClick?: (edge: NeuronEdge) => void;
|
|
321
327
|
onBackgroundClick?: () => void;
|
|
322
328
|
onCameraChange?: (position: [number, number, number]) => void;
|
|
329
|
+
/**
|
|
330
|
+
* Enable node dragging for manual arrangement.
|
|
331
|
+
* - `true` or `{ enabled: true }`: Enable dragging, constrain to XY plane in tree mode
|
|
332
|
+
* - `{ enabled: true, constrainToPlane: 'xy' | 'xz' | 'yz' }`: Constrain to specific plane
|
|
333
|
+
*/
|
|
334
|
+
draggable?: boolean | {
|
|
335
|
+
enabled: boolean;
|
|
336
|
+
constrainToPlane?: 'xy' | 'xz' | 'yz';
|
|
337
|
+
};
|
|
338
|
+
/** Called continuously while a node is being dragged. */
|
|
339
|
+
onNodeDrag?: (node: NeuronNode, position: [number, number, number]) => void;
|
|
340
|
+
/** Called when a node drag operation completes. */
|
|
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
|
+
};
|
|
323
355
|
/** When set, plays a story beat by id using the built-in study path player. */
|
|
324
356
|
activeStoryBeatId?: string | null;
|
|
325
357
|
/** Optional override for story beat step duration (ms). */
|
|
@@ -405,7 +437,7 @@ interface NeuronWebExplorerProps {
|
|
|
405
437
|
renderLoadingState?: () => React__default.ReactNode;
|
|
406
438
|
}
|
|
407
439
|
|
|
408
|
-
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, 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;
|
|
409
441
|
|
|
410
442
|
declare function NeuronWebExplorer(props: NeuronWebExplorerProps): React__default.ReactElement;
|
|
411
443
|
|
|
@@ -109,6 +109,12 @@ interface TreeLayoutOptions {
|
|
|
109
109
|
rootNodeId?: string;
|
|
110
110
|
/** Direction the tree grows: 'down'/'up' for vertical, 'left'/'right' for horizontal (default: 'down') */
|
|
111
111
|
direction?: 'down' | 'up' | 'left' | 'right';
|
|
112
|
+
/**
|
|
113
|
+
* When true, reverses edge interpretation: edge.from becomes child and edge.to becomes parent.
|
|
114
|
+
* Use this if your edges point from child to parent (e.g., "derives from" relationships).
|
|
115
|
+
* Default: false (edges point from parent to child)
|
|
116
|
+
*/
|
|
117
|
+
reverseEdgeDirection?: boolean;
|
|
112
118
|
}
|
|
113
119
|
interface NeuronLayoutOptions {
|
|
114
120
|
mode?: NeuronLayoutMode;
|
|
@@ -320,6 +326,32 @@ interface NeuronWebProps {
|
|
|
320
326
|
onEdgeClick?: (edge: NeuronEdge) => void;
|
|
321
327
|
onBackgroundClick?: () => void;
|
|
322
328
|
onCameraChange?: (position: [number, number, number]) => void;
|
|
329
|
+
/**
|
|
330
|
+
* Enable node dragging for manual arrangement.
|
|
331
|
+
* - `true` or `{ enabled: true }`: Enable dragging, constrain to XY plane in tree mode
|
|
332
|
+
* - `{ enabled: true, constrainToPlane: 'xy' | 'xz' | 'yz' }`: Constrain to specific plane
|
|
333
|
+
*/
|
|
334
|
+
draggable?: boolean | {
|
|
335
|
+
enabled: boolean;
|
|
336
|
+
constrainToPlane?: 'xy' | 'xz' | 'yz';
|
|
337
|
+
};
|
|
338
|
+
/** Called continuously while a node is being dragged. */
|
|
339
|
+
onNodeDrag?: (node: NeuronNode, position: [number, number, number]) => void;
|
|
340
|
+
/** Called when a node drag operation completes. */
|
|
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
|
+
};
|
|
323
355
|
/** When set, plays a story beat by id using the built-in study path player. */
|
|
324
356
|
activeStoryBeatId?: string | null;
|
|
325
357
|
/** Optional override for story beat step duration (ms). */
|
|
@@ -405,7 +437,7 @@ interface NeuronWebExplorerProps {
|
|
|
405
437
|
renderLoadingState?: () => React__default.ReactNode;
|
|
406
438
|
}
|
|
407
439
|
|
|
408
|
-
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, 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;
|
|
409
441
|
|
|
410
442
|
declare function NeuronWebExplorer(props: NeuronWebExplorerProps): React__default.ReactElement;
|
|
411
443
|
|
|
@@ -1080,6 +1080,14 @@ var NodeRenderer = class {
|
|
|
1080
1080
|
const state = this.nodeStates.get(nodeId);
|
|
1081
1081
|
return state ? state.object.position.clone() : null;
|
|
1082
1082
|
}
|
|
1083
|
+
/** Updates a node's position during drag operations */
|
|
1084
|
+
updateNodePosition(nodeId, position) {
|
|
1085
|
+
const state = this.nodeStates.get(nodeId);
|
|
1086
|
+
if (!state) return;
|
|
1087
|
+
state.basePosition.copy(position);
|
|
1088
|
+
state.object.position.copy(position);
|
|
1089
|
+
state.positionTween = null;
|
|
1090
|
+
}
|
|
1083
1091
|
getNodeObject(nodeId) {
|
|
1084
1092
|
const state = this.nodeStates.get(nodeId);
|
|
1085
1093
|
return state?.object ?? null;
|
|
@@ -2266,6 +2274,7 @@ function applyTreeLayout(nodes, edges, options = {}) {
|
|
|
2266
2274
|
const verticalSpacing = treeOptions.verticalSpacing ?? 4;
|
|
2267
2275
|
const direction = treeOptions.direction ?? "down";
|
|
2268
2276
|
const rootNodeId = treeOptions.rootNodeId;
|
|
2277
|
+
const reverseEdgeDirection = treeOptions.reverseEdgeDirection ?? false;
|
|
2269
2278
|
const nodeById = /* @__PURE__ */ new Map();
|
|
2270
2279
|
const nodeBySlug = /* @__PURE__ */ new Map();
|
|
2271
2280
|
for (const node of nodes) {
|
|
@@ -2280,8 +2289,8 @@ function applyTreeLayout(nodes, edges, options = {}) {
|
|
|
2280
2289
|
const fromNode = nodeBySlug.get(edge.from) ?? nodeById.get(edge.from);
|
|
2281
2290
|
const toNode = nodeBySlug.get(edge.to) ?? nodeById.get(edge.to);
|
|
2282
2291
|
if (!fromNode || !toNode) continue;
|
|
2283
|
-
const parentId = fromNode.id;
|
|
2284
|
-
const childId = toNode.id;
|
|
2292
|
+
const parentId = reverseEdgeDirection ? toNode.id : fromNode.id;
|
|
2293
|
+
const childId = reverseEdgeDirection ? fromNode.id : toNode.id;
|
|
2285
2294
|
if (!parentMap.has(childId)) {
|
|
2286
2295
|
parentMap.set(childId, parentId);
|
|
2287
2296
|
const siblings = childrenMap.get(parentId) ?? [];
|
|
@@ -2557,6 +2566,15 @@ var InteractionManager = class {
|
|
|
2557
2566
|
lastHoverId = null;
|
|
2558
2567
|
lastClickTime = 0;
|
|
2559
2568
|
lastClickId = null;
|
|
2569
|
+
// Drag state
|
|
2570
|
+
isDragging = false;
|
|
2571
|
+
dragNode = null;
|
|
2572
|
+
dragPlane = new THREE.Plane();
|
|
2573
|
+
dragOffset = new THREE.Vector3();
|
|
2574
|
+
dragIntersection = new THREE.Vector3();
|
|
2575
|
+
pointerDownPosition = new THREE.Vector2();
|
|
2576
|
+
dragThreshold = 5;
|
|
2577
|
+
// pixels
|
|
2560
2578
|
onNodeHover = () => {
|
|
2561
2579
|
};
|
|
2562
2580
|
onNodeClick = () => {
|
|
@@ -2567,9 +2585,45 @@ var InteractionManager = class {
|
|
|
2567
2585
|
};
|
|
2568
2586
|
onBackgroundClick = () => {
|
|
2569
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
|
+
};
|
|
2594
|
+
onNodeDragStart = () => {
|
|
2595
|
+
};
|
|
2596
|
+
onNodeDrag = () => {
|
|
2597
|
+
};
|
|
2598
|
+
onNodeDragEnd = () => {
|
|
2599
|
+
};
|
|
2570
2600
|
onPointerMove(event) {
|
|
2571
|
-
if (!this.config.enableHover) return;
|
|
2572
2601
|
this.updatePointer(event);
|
|
2602
|
+
if (this.config.enableDrag && this.dragNode) {
|
|
2603
|
+
if (!this.isDragging) {
|
|
2604
|
+
const dx = (this.pointer.x - this.pointerDownPosition.x) * this.renderer.domElement.clientWidth;
|
|
2605
|
+
const dy = (this.pointer.y - this.pointerDownPosition.y) * this.renderer.domElement.clientHeight;
|
|
2606
|
+
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
2607
|
+
if (distance > this.dragThreshold) {
|
|
2608
|
+
this.isDragging = true;
|
|
2609
|
+
const nodeObject = this.nodeObjects.find(
|
|
2610
|
+
(obj) => obj.userData?.nodeId === this.dragNode.id
|
|
2611
|
+
);
|
|
2612
|
+
if (nodeObject) {
|
|
2613
|
+
this.onNodeDragStart(this.dragNode, nodeObject.position.clone());
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
if (this.isDragging) {
|
|
2618
|
+
this.raycaster.setFromCamera(this.pointer, this.camera);
|
|
2619
|
+
if (this.raycaster.ray.intersectPlane(this.dragPlane, this.dragIntersection)) {
|
|
2620
|
+
const newPosition = this.dragIntersection.clone().add(this.dragOffset);
|
|
2621
|
+
this.onNodeDrag(this.dragNode, newPosition);
|
|
2622
|
+
}
|
|
2623
|
+
return;
|
|
2624
|
+
}
|
|
2625
|
+
}
|
|
2626
|
+
if (!this.config.enableHover) return;
|
|
2573
2627
|
const node = this.getIntersectedNode(this.pointer);
|
|
2574
2628
|
if (this.hoverTimeout) {
|
|
2575
2629
|
window.clearTimeout(this.hoverTimeout);
|
|
@@ -2581,11 +2635,59 @@ var InteractionManager = class {
|
|
|
2581
2635
|
}
|
|
2582
2636
|
}, this.config.hoverDelay);
|
|
2583
2637
|
}
|
|
2584
|
-
onPointerDown() {
|
|
2638
|
+
onPointerDown(event) {
|
|
2639
|
+
if (!this.config.enableDrag) return;
|
|
2640
|
+
this.updatePointer(event);
|
|
2641
|
+
this.pointerDownPosition.copy(this.pointer);
|
|
2642
|
+
const node = this.getIntersectedNode(this.pointer);
|
|
2643
|
+
if (node) {
|
|
2644
|
+
this.onNodePointerDown(node);
|
|
2645
|
+
const nodeObject = this.nodeObjects.find(
|
|
2646
|
+
(obj) => obj.userData?.nodeId === node.id
|
|
2647
|
+
);
|
|
2648
|
+
if (nodeObject) {
|
|
2649
|
+
const normal = this.getPlaneNormal();
|
|
2650
|
+
this.dragPlane.setFromNormalAndCoplanarPoint(normal, nodeObject.position);
|
|
2651
|
+
this.raycaster.setFromCamera(this.pointer, this.camera);
|
|
2652
|
+
if (this.raycaster.ray.intersectPlane(this.dragPlane, this.dragIntersection)) {
|
|
2653
|
+
this.dragOffset.copy(nodeObject.position).sub(this.dragIntersection);
|
|
2654
|
+
this.dragNode = node;
|
|
2655
|
+
}
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
2658
|
+
}
|
|
2659
|
+
getPlaneNormal() {
|
|
2660
|
+
switch (this.config.dragConstrainPlane) {
|
|
2661
|
+
case "xz":
|
|
2662
|
+
return new THREE.Vector3(0, 1, 0);
|
|
2663
|
+
case "yz":
|
|
2664
|
+
return new THREE.Vector3(1, 0, 0);
|
|
2665
|
+
case "xy":
|
|
2666
|
+
default:
|
|
2667
|
+
return new THREE.Vector3(0, 0, 1);
|
|
2668
|
+
}
|
|
2669
|
+
}
|
|
2670
|
+
/** Returns true if currently dragging a node */
|
|
2671
|
+
get dragging() {
|
|
2672
|
+
return this.isDragging;
|
|
2585
2673
|
}
|
|
2586
2674
|
onPointerUp(event) {
|
|
2587
|
-
if (!this.config.enableClick) return;
|
|
2588
2675
|
this.updatePointer(event);
|
|
2676
|
+
if (this.isDragging && this.dragNode) {
|
|
2677
|
+
this.raycaster.setFromCamera(this.pointer, this.camera);
|
|
2678
|
+
if (this.raycaster.ray.intersectPlane(this.dragPlane, this.dragIntersection)) {
|
|
2679
|
+
const finalPosition = this.dragIntersection.clone().add(this.dragOffset);
|
|
2680
|
+
this.onNodeDragEnd(this.dragNode, finalPosition);
|
|
2681
|
+
}
|
|
2682
|
+
this.isDragging = false;
|
|
2683
|
+
this.dragNode = null;
|
|
2684
|
+
return;
|
|
2685
|
+
}
|
|
2686
|
+
if (this.dragNode) {
|
|
2687
|
+
this.onNodePointerUp();
|
|
2688
|
+
this.dragNode = null;
|
|
2689
|
+
}
|
|
2690
|
+
if (!this.config.enableClick) return;
|
|
2589
2691
|
const node = this.getIntersectedNode(this.pointer);
|
|
2590
2692
|
if (node) {
|
|
2591
2693
|
const now = performance.now();
|
|
@@ -2662,8 +2764,6 @@ var InteractionManager = class {
|
|
|
2662
2764
|
this.pointer.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
|
|
2663
2765
|
}
|
|
2664
2766
|
};
|
|
2665
|
-
|
|
2666
|
-
// src/visualization/animations/animation-controller.ts
|
|
2667
2767
|
var AnimationController = class {
|
|
2668
2768
|
constructor(camera, controls, config) {
|
|
2669
2769
|
this.camera = camera;
|
|
@@ -2672,8 +2772,13 @@ var AnimationController = class {
|
|
|
2672
2772
|
}
|
|
2673
2773
|
focusTween = null;
|
|
2674
2774
|
focusOnNode(position, callback) {
|
|
2675
|
-
const direction = this.camera.position.clone().sub(this.controls.target).normalize();
|
|
2676
2775
|
const distance = this.camera.position.distanceTo(this.controls.target);
|
|
2776
|
+
let direction;
|
|
2777
|
+
if (this.config.constrainTo2D) {
|
|
2778
|
+
direction = new THREE.Vector3(0, 0, 1);
|
|
2779
|
+
} else {
|
|
2780
|
+
direction = this.camera.position.clone().sub(this.controls.target).normalize();
|
|
2781
|
+
}
|
|
2677
2782
|
const targetPosition = position.clone().add(direction.multiplyScalar(distance));
|
|
2678
2783
|
this.focusOnPosition(targetPosition, position, callback);
|
|
2679
2784
|
}
|
|
@@ -2841,6 +2946,10 @@ function NeuronWeb({
|
|
|
2841
2946
|
onEdgeClick,
|
|
2842
2947
|
onBackgroundClick,
|
|
2843
2948
|
onCameraChange,
|
|
2949
|
+
draggable,
|
|
2950
|
+
onNodeDrag,
|
|
2951
|
+
onNodeDragEnd,
|
|
2952
|
+
keyboardControls,
|
|
2844
2953
|
performanceMode,
|
|
2845
2954
|
density,
|
|
2846
2955
|
rendering,
|
|
@@ -3270,24 +3379,106 @@ function NeuronWeb({
|
|
|
3270
3379
|
};
|
|
3271
3380
|
}, [clusterRenderer]);
|
|
3272
3381
|
const doubleClickEnabled = false;
|
|
3382
|
+
const dragConfig = useMemo(() => {
|
|
3383
|
+
if (!draggable) return { enabled: false, constrainToPlane: "xy" };
|
|
3384
|
+
if (draggable === true) {
|
|
3385
|
+
return { enabled: true, constrainToPlane: "xy" };
|
|
3386
|
+
}
|
|
3387
|
+
return {
|
|
3388
|
+
enabled: draggable.enabled,
|
|
3389
|
+
constrainToPlane: draggable.constrainToPlane ?? "xy"
|
|
3390
|
+
};
|
|
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]);
|
|
3273
3461
|
const interactionManager = useMemo(() => {
|
|
3274
3462
|
if (!sceneManager) return null;
|
|
3275
3463
|
return new InteractionManager(sceneManager.scene, sceneManager.camera, sceneManager.renderer, {
|
|
3276
3464
|
enableHover: true,
|
|
3277
3465
|
enableClick: true,
|
|
3278
3466
|
enableDoubleClick: doubleClickEnabled,
|
|
3467
|
+
enableDrag: dragConfig.enabled,
|
|
3279
3468
|
hoverDelay: Math.max(40, resolvedTheme.animation.hoverCardFadeDuration * 0.6),
|
|
3280
|
-
doubleClickDelay: 280
|
|
3469
|
+
doubleClickDelay: 280,
|
|
3470
|
+
dragConstrainPlane: dragConfig.constrainToPlane
|
|
3281
3471
|
});
|
|
3282
|
-
}, [sceneManager, resolvedTheme.animation.hoverCardFadeDuration, doubleClickEnabled]);
|
|
3472
|
+
}, [sceneManager, resolvedTheme.animation.hoverCardFadeDuration, doubleClickEnabled, dragConfig]);
|
|
3283
3473
|
const animationController = useMemo(() => {
|
|
3284
3474
|
if (!sceneManager) return null;
|
|
3285
3475
|
return new AnimationController(sceneManager.camera, sceneManager.controls, {
|
|
3286
3476
|
focusDuration: resolvedAnimationConfig.focusDurationMs,
|
|
3287
3477
|
transitionDuration: resolvedAnimationConfig.transitionDurationMs,
|
|
3288
|
-
easing: resolvedAnimationConfig.easing
|
|
3478
|
+
easing: resolvedAnimationConfig.easing,
|
|
3479
|
+
constrainTo2D: isTreeLayout
|
|
3289
3480
|
});
|
|
3290
|
-
}, [sceneManager, resolvedAnimationConfig]);
|
|
3481
|
+
}, [sceneManager, resolvedAnimationConfig, isTreeLayout]);
|
|
3291
3482
|
const rippleEnabled = resolvedAnimationConfig.enableSelectionRipple && resolvedPerformanceMode !== "fallback";
|
|
3292
3483
|
const selectionRipple = useMemo(() => {
|
|
3293
3484
|
if (!sceneManager || !rippleEnabled) return null;
|
|
@@ -3504,6 +3695,16 @@ function NeuronWeb({
|
|
|
3504
3695
|
const focusOnNodePosition = useCallback(
|
|
3505
3696
|
(nodePosition, callback) => {
|
|
3506
3697
|
if (!animationController && !sceneManager) return;
|
|
3698
|
+
const getCameraDirection = () => {
|
|
3699
|
+
if (isTreeLayout) {
|
|
3700
|
+
return new THREE.Vector3(0, 0, 1);
|
|
3701
|
+
}
|
|
3702
|
+
const direction = sceneManager.camera.position.clone().sub(sceneManager.controls.target);
|
|
3703
|
+
if (direction.lengthSq() < 1e-4) {
|
|
3704
|
+
direction.set(0, 0, 1);
|
|
3705
|
+
}
|
|
3706
|
+
return direction.normalize();
|
|
3707
|
+
};
|
|
3507
3708
|
if (Array.isArray(clickZoomOffset) && clickZoomOffset.length === 3) {
|
|
3508
3709
|
const offset = new THREE.Vector3(...clickZoomOffset);
|
|
3509
3710
|
const targetPosition = nodePosition.clone().add(offset);
|
|
@@ -3524,11 +3725,7 @@ function NeuronWeb({
|
|
|
3524
3725
|
}
|
|
3525
3726
|
return;
|
|
3526
3727
|
}
|
|
3527
|
-
const direction =
|
|
3528
|
-
if (direction.lengthSq() < 1e-4) {
|
|
3529
|
-
direction.set(0, 0, 1);
|
|
3530
|
-
}
|
|
3531
|
-
direction.normalize();
|
|
3728
|
+
const direction = getCameraDirection();
|
|
3532
3729
|
const targetPosition = nodePosition.clone().add(direction.multiplyScalar(clickZoomDistance));
|
|
3533
3730
|
if (cameraTweenEnabled && animationController) {
|
|
3534
3731
|
animationController.focusOnPosition(targetPosition, nodePosition, callback);
|
|
@@ -3543,11 +3740,7 @@ function NeuronWeb({
|
|
|
3543
3740
|
if (cameraTweenEnabled && animationController) {
|
|
3544
3741
|
animationController.focusOnNode(nodePosition, callback);
|
|
3545
3742
|
} else if (sceneManager) {
|
|
3546
|
-
const direction =
|
|
3547
|
-
if (direction.lengthSq() < 1e-4) {
|
|
3548
|
-
direction.set(0, 0, 1);
|
|
3549
|
-
}
|
|
3550
|
-
direction.normalize();
|
|
3743
|
+
const direction = getCameraDirection();
|
|
3551
3744
|
const distance = sceneManager.camera.position.distanceTo(sceneManager.controls.target);
|
|
3552
3745
|
const targetPosition = nodePosition.clone().add(direction.multiplyScalar(distance));
|
|
3553
3746
|
sceneManager.camera.position.copy(targetPosition);
|
|
@@ -3556,7 +3749,7 @@ function NeuronWeb({
|
|
|
3556
3749
|
if (callback) callback();
|
|
3557
3750
|
}
|
|
3558
3751
|
},
|
|
3559
|
-
[animationController, clickZoomOffset, clickZoomDistance, sceneManager, cameraTweenEnabled]
|
|
3752
|
+
[animationController, clickZoomOffset, clickZoomDistance, sceneManager, cameraTweenEnabled, isTreeLayout]
|
|
3560
3753
|
);
|
|
3561
3754
|
const handleKeyDown = useCallback(
|
|
3562
3755
|
(event) => {
|
|
@@ -3783,11 +3976,16 @@ function NeuronWeb({
|
|
|
3783
3976
|
const hFov = 2 * Math.atan(Math.tan(vFov / 2) * aspect);
|
|
3784
3977
|
const targetFov = Math.min(vFov, hFov);
|
|
3785
3978
|
const distance = radius * (1 + padding) / Math.tan(targetFov * viewportFraction / 2);
|
|
3786
|
-
|
|
3787
|
-
if (
|
|
3788
|
-
direction.
|
|
3979
|
+
let direction;
|
|
3980
|
+
if (isTreeLayout) {
|
|
3981
|
+
direction = new THREE.Vector3(0, 0, 1);
|
|
3982
|
+
} else {
|
|
3983
|
+
direction = camera.position.clone().sub(sceneManager.controls.target);
|
|
3984
|
+
if (direction.lengthSq() < 1e-4) {
|
|
3985
|
+
direction.set(0, 0, 1);
|
|
3986
|
+
}
|
|
3987
|
+
direction.normalize();
|
|
3789
3988
|
}
|
|
3790
|
-
direction.normalize();
|
|
3791
3989
|
const targetPosition = sphere.center.clone().add(direction.multiplyScalar(distance));
|
|
3792
3990
|
if (cameraTweenEnabled) {
|
|
3793
3991
|
animationController.focusOnPosition(targetPosition, sphere.center.clone());
|
|
@@ -3804,7 +4002,8 @@ function NeuronWeb({
|
|
|
3804
4002
|
displayNodes,
|
|
3805
4003
|
fitSignature,
|
|
3806
4004
|
cameraFitSuspended,
|
|
3807
|
-
cameraTweenEnabled
|
|
4005
|
+
cameraTweenEnabled,
|
|
4006
|
+
isTreeLayout
|
|
3808
4007
|
]);
|
|
3809
4008
|
useEffect(() => {
|
|
3810
4009
|
if (!sceneManager) return;
|
|
@@ -4058,6 +4257,34 @@ function NeuronWeb({
|
|
|
4058
4257
|
onEdgeClick(edge);
|
|
4059
4258
|
}
|
|
4060
4259
|
};
|
|
4260
|
+
if (dragConfig.enabled) {
|
|
4261
|
+
interactionManager.onNodePointerDown = () => {
|
|
4262
|
+
if (sceneManager) {
|
|
4263
|
+
sceneManager.controls.enabled = false;
|
|
4264
|
+
}
|
|
4265
|
+
};
|
|
4266
|
+
interactionManager.onNodePointerUp = () => {
|
|
4267
|
+
if (sceneManager) {
|
|
4268
|
+
sceneManager.controls.enabled = true;
|
|
4269
|
+
}
|
|
4270
|
+
};
|
|
4271
|
+
interactionManager.onNodeDragStart = () => {
|
|
4272
|
+
};
|
|
4273
|
+
interactionManager.onNodeDrag = (node, position) => {
|
|
4274
|
+
nodeRenderer.updateNodePosition(node.id, position);
|
|
4275
|
+
if (onNodeDrag) {
|
|
4276
|
+
onNodeDrag(node, [position.x, position.y, position.z]);
|
|
4277
|
+
}
|
|
4278
|
+
};
|
|
4279
|
+
interactionManager.onNodeDragEnd = (node, position) => {
|
|
4280
|
+
if (sceneManager) {
|
|
4281
|
+
sceneManager.controls.enabled = true;
|
|
4282
|
+
}
|
|
4283
|
+
if (onNodeDragEnd) {
|
|
4284
|
+
onNodeDragEnd(node, [position.x, position.y, position.z]);
|
|
4285
|
+
}
|
|
4286
|
+
};
|
|
4287
|
+
}
|
|
4061
4288
|
}, [
|
|
4062
4289
|
interactionManager,
|
|
4063
4290
|
nodeRenderer,
|
|
@@ -4076,6 +4303,10 @@ function NeuronWeb({
|
|
|
4076
4303
|
onBackgroundClick,
|
|
4077
4304
|
applyFocusEdges,
|
|
4078
4305
|
selectionRipple,
|
|
4306
|
+
dragConfig,
|
|
4307
|
+
sceneManager,
|
|
4308
|
+
onNodeDrag,
|
|
4309
|
+
onNodeDragEnd,
|
|
4079
4310
|
selectionControlled,
|
|
4080
4311
|
doubleClickEnabled
|
|
4081
4312
|
]);
|
|
@@ -4628,5 +4859,5 @@ function dedupePreserveOrder(values) {
|
|
|
4628
4859
|
}
|
|
4629
4860
|
|
|
4630
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 };
|
|
4631
|
-
//# sourceMappingURL=chunk-
|
|
4632
|
-
//# sourceMappingURL=chunk-
|
|
4862
|
+
//# sourceMappingURL=chunk-QKCKZYLH.js.map
|
|
4863
|
+
//# sourceMappingURL=chunk-QKCKZYLH.js.map
|