diff --git a/docs/Centering.mdx b/docs/Centering.mdx index ee67ae0b..56cbcd6a 100644 --- a/docs/Centering.mdx +++ b/docs/Centering.mdx @@ -4,7 +4,7 @@ import { GraphCanvas } from '../src'; # Centering -Reagraph supports the ability to dynamically center nodes using the `centerGraph` method from the `GraphCanvasRef`. This method allows you to programmatically center the camera view around specific nodes or the entire graph. +Reagraph supports the ability to dynamically center nodes using the `centerGraph` and `fitNodesInView` methods from the `GraphCanvasRef`. These methods allows you to programmatically center the camera view around specific nodes or the entire graph. ### Usage First, you need to get a reference to the `GraphRef`: @@ -17,20 +17,30 @@ return ( ) ``` -Then, you can use the `centerGraph` method to center all nodes within view of the camera: +Then, you can use the `fitNodesInView` method to center all nodes within view of the camera: ```js -graphRef.current?.centerGraph(); +graphRef.current?.fitNodesInView(); ``` -If you want to center the view around specific nodes, you can pass an array of node ids to the `centerGraph` method: +If you want to fit the view around specific nodes, you can pass an array of node ids to the `fitNodesInView` method: ```jsx +graphRef.current?.fitNodesInView(['node1', 'node2']); +``` + +If you want to center the camera on a given set of nodes without adjusting the zoom, you can use the `centerGraph` method: + +```jsx +// Center the camera position based on all nodes in the graph +graphRef.current?.centerGraph(); + +// Center the camera position based on specific nodes graphRef.current?.centerGraph(['node1', 'node2']); ``` ### Examples -In this example, clicking the "Center Graph" button will center the camera around all the nodes in the graph: +In this example, clicking the "Fit View" button will fit the view around all the nodes in the graph: ```jsx import React, { useRef } from 'react'; import { GraphCanvas } from 'reagraph'; @@ -38,21 +48,21 @@ import { GraphCanvas } from 'reagraph'; const MyComponent = () => { const graphRef = useRef(null); - const centerGraph = () => { - graphRef.current?.centerGraph(); + const fitView = () => { + graphRef.current?.fitNodesInView(); }; return (
- +
); }; ``` -Here's a more advanced example that centers the camera on the whole graph whenever new nodes are added: +Here's a more advanced example that fits the view on the whole graph whenever new nodes are added: ```jsx import React, { useRef, useEffect } from 'react'; import { GraphCanvas } from 'reagraph'; @@ -61,7 +71,7 @@ const MyComponent = ({ nodes }) => { const graphRef = useRef(null); useEffect(() => { - graphRef.current?.centerGraph(); + graphRef.current?.fitNodesInView(); }, [nodes]); return ; diff --git a/docs/demos/Basic.story.tsx b/docs/demos/Basic.story.tsx index 004c1e88..b2aec0cb 100644 --- a/docs/demos/Basic.story.tsx +++ b/docs/demos/Basic.story.tsx @@ -105,7 +105,7 @@ export const LiveUpdates = () => { const [edges, setEdges] = useState(simpleEdges); useEffect(() => { - ref.current?.centerGraph(); + ref.current?.fitNodesInView(); }, [nodes]); return ( diff --git a/docs/demos/Controls.story.tsx b/docs/demos/Controls.story.tsx index 08dc93d8..21af49c6 100644 --- a/docs/demos/Controls.story.tsx +++ b/docs/demos/Controls.story.tsx @@ -15,6 +15,7 @@ export const All = () => {
+
diff --git a/src/CameraControls/useCenterGraph.ts b/src/CameraControls/useCenterGraph.ts index 5a8328c9..2b42df29 100644 --- a/src/CameraControls/useCenterGraph.ts +++ b/src/CameraControls/useCenterGraph.ts @@ -16,6 +16,11 @@ export interface CenterNodesParams { centerOnlyIfNodesNotInView?: boolean; } +export interface FitNodesParams { + animated?: boolean; + fitOnlyIfNodesNotInView?: boolean; +} + export interface CenterGraphInput { /** * Whether the animate the transition or not. @@ -54,10 +59,10 @@ export interface CenterGraphOutput { /** * Centers the graph on a specific node or list of nodes. * - * @param ids - An array of node IDs to center the graph on. If this parameter is omitted, + * @param nodeIds - An array of node IDs to center the graph on. If this parameter is omitted, * the graph will be centered on all nodes. * - * @param centerOnlyIfNodesNotInView - A boolean flag that determines whether the graph should + * @param opts.centerOnlyIfNodesNotInView - A boolean flag that determines whether the graph should * only be centered if the nodes specified by `ids` are not currently in view. If this * parameter is `true`, the graph will only be re-centered if one or more of the nodes * specified by `ids` are not currently in view. If this parameter is @@ -66,6 +71,21 @@ export interface CenterGraphOutput { */ centerNodesById: (nodeIds: string[], opts?: CenterNodesParams) => void; + /** + * Fit all the given nodes into view of the camera. + * + * @param nodeIds - An array of node IDs to fit the view on. If this parameter is omitted, + * the view will fit to all nodes. + * + * @param opts.fitOnlyIfNodesNotInView - A boolean flag that determines whether the view should + * only be fit if the nodes specified by `ids` are not currently in view. If this + * parameter is `true`, the view will only be fit if one or more of the nodes + * specified by `ids` are not currently visible in the viewport. If this parameter is + * `false` or omitted, the view will be fit regardless of whether the nodes + * are currently in view. + */ + fitNodesInViewById: (nodeIds: string[], opts?: FitNodesParams) => void; + /** * Whether the graph is centered or not. */ @@ -99,8 +119,34 @@ export const useCenterGraph = ({ nodes?.some(node => !isNodeInView(camera, node.position))) ) { // Centers the graph based on the central most node - const { minX, maxX, minY, maxY, minZ, maxZ, x, y, z } = - getLayoutCenter(nodes); + const { x, y, z } = getLayoutCenter(nodes); + + await controls.setTarget(x, y, z, animated); + + if (!isCentered) { + setIsCentered(true); + } + + invalidate(); + } + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [invalidate, controls, nodes] + ); + + const fitNodesInView = useCallback( + async ( + nodes, + opts: FitNodesParams = { animated: true, fitOnlyIfNodesNotInView: false } + ) => { + const { fitOnlyIfNodesNotInView } = opts; + + if ( + !fitOnlyIfNodesNotInView || + (fitOnlyIfNodesNotInView && + nodes?.some(node => !isNodeInView(camera, node.position))) + ) { + const { minX, maxX, minY, maxY, minZ, maxZ } = getLayoutCenter(nodes); if (!layoutType.includes('3d')) { // fitToBox will auto rotate to the closest axis including the z axis, @@ -115,12 +161,14 @@ export const useCenterGraph = ({ void controls?.rotate(horizontalRotation, verticalRotation, true); } + void controls?.zoomTo(1, opts?.animated); + await controls?.fitToBox( new Box3( new Vector3(minX, minY, minZ), new Vector3(maxX, maxY, maxZ) ), - animated, + opts?.animated, { cover: false, paddingLeft: PADDING, @@ -129,21 +177,13 @@ export const useCenterGraph = ({ paddingTop: PADDING } ); - await controls.setTarget(x, y, z, animated); - - if (!isCentered) { - setIsCentered(true); - } - - invalidate(); } }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [invalidate, controls, nodes] + [camera, controls, layoutType] ); - const centerNodesById = useCallback( - (nodeIds: string[], opts: CenterNodesParams) => { + const getNodesById = useCallback( + (nodeIds: string[]) => { let mappedNodes: InternalGraphNode[] | null = null; if (nodeIds?.length) { @@ -162,12 +202,30 @@ export const useCenterGraph = ({ }, []); } + return mappedNodes; + }, + [nodes] + ); + + const centerNodesById = useCallback( + (nodeIds: string[], opts: CenterNodesParams) => { + const mappedNodes = getNodesById(nodeIds); + centerNodes(mappedNodes || nodes, { animated, centerOnlyIfNodesNotInView: opts?.centerOnlyIfNodesNotInView }); }, - [animated, centerNodes, nodes] + [animated, centerNodes, getNodesById, nodes] + ); + + const fitNodesInViewById = useCallback( + async (nodeIds: string[], opts: FitNodesParams) => { + const mappedNodes = getNodesById(nodeIds); + + await fitNodesInView(mappedNodes || nodes, { animated, ...opts }); + }, + [animated, fitNodesInView, getNodesById, nodes] ); useLayoutEffect(() => { @@ -177,13 +235,14 @@ export const useCenterGraph = ({ if (!mounted.current) { // Center the graph once nodes are loaded on mount await centerNodes(nodes, { animated: false }); + await fitNodesInView(nodes, { animated: false }); mounted.current = true; } } } load(); - }, [controls, centerNodes, nodes, animated, camera]); + }, [controls, centerNodes, nodes, animated, camera, fitNodesInView]); useHotkeys([ { @@ -195,5 +254,5 @@ export const useCenterGraph = ({ } ]); - return { centerNodes, centerNodesById, isCentered }; + return { centerNodes, centerNodesById, fitNodesInViewById, isCentered }; }; diff --git a/src/GraphCanvas.tsx b/src/GraphCanvas.tsx index afd5614f..e452d2c9 100644 --- a/src/GraphCanvas.tsx +++ b/src/GraphCanvas.tsx @@ -134,6 +134,8 @@ export const GraphCanvas: FC }> = useImperativeHandle(ref, () => ({ centerGraph: (nodeIds, opts) => rendererRef.current?.centerGraph(nodeIds, opts), + fitNodesInView: (nodeIds, opts) => + rendererRef.current?.fitNodesInView(nodeIds, opts), zoomIn: () => controlsRef.current?.zoomIn(), zoomOut: () => controlsRef.current?.zoomOut(), panLeft: () => controlsRef.current?.panLeft(), diff --git a/src/GraphScene.tsx b/src/GraphScene.tsx index 845ec118..9051bd6c 100644 --- a/src/GraphScene.tsx +++ b/src/GraphScene.tsx @@ -30,7 +30,11 @@ import { Edges, Node } from './symbols'; -import { CenterNodesParams, useCenterGraph } from './CameraControls'; +import { + CenterNodesParams, + FitNodesParams, + useCenterGraph +} from './CameraControls'; import { LabelVisibilityType } from './utils'; import { useStore } from './store'; import Graph from 'graphology'; @@ -266,7 +270,7 @@ export interface GraphSceneRef { * @param nodeIds - An array of node IDs to center the graph on. If this parameter is omitted, * the graph will be centered on all nodes. * - * @param centerOnlyIfNodesNotInView - A boolean flag that determines whether the graph should + * @param opts.centerOnlyIfNodesNotInView - A boolean flag that determines whether the graph should * only be centered if the nodes specified by `ids` are not currently in view. If this * parameter is `true`, the graph will only be re-centered if one or more of the nodes * specified by `ids` are not currently in view. If this parameter is @@ -275,6 +279,21 @@ export interface GraphSceneRef { */ centerGraph: (nodeIds?: string[], opts?: CenterNodesParams) => void; + /** + * Fit all the given nodes into view of the camera. + * + * @param nodeIds - An array of node IDs to fit the view on. If this parameter is omitted, + * the view will fit to all nodes. + * + * @param opts.fitOnlyIfNodesNotInView - A boolean flag that determines whether the view should + * only be fit if the nodes specified by `ids` are not currently in view. If this + * parameter is `true`, the view will only be fit if one or more of the nodes + * specified by `ids` are not currently visible in the viewport. If this parameter is + * `false` or omitted, the view will be fit regardless of whether the nodes + * are currently in view. + */ + fitNodesInView: (nodeIds?: string[], opts?: FitNodesParams) => void; + /** * Calls render scene on the graph. this is useful when you want to manually render the graph * for things like screenshots. @@ -338,21 +357,23 @@ export const GraphScene: FC }> = const clusters = useStore(state => [...state.clusters.values()]); // Center the graph on the nodes - const { centerNodesById, isCentered } = useCenterGraph({ - animated, - disabled, - layoutType - }); + const { centerNodesById, fitNodesInViewById, isCentered } = + useCenterGraph({ + animated, + disabled, + layoutType + }); // Let's expose some helper methods useImperativeHandle( ref, () => ({ centerGraph: centerNodesById, + fitNodesInView: fitNodesInViewById, graph, renderScene: () => gl.render(scene, camera) }), - [centerNodesById, graph, gl, scene, camera] + [centerNodesById, fitNodesInViewById, graph, gl, scene, camera] ); const nodeComponents = useMemo( diff --git a/src/selection/useSelection.ts b/src/selection/useSelection.ts index bb66de67..d87313d0 100644 --- a/src/selection/useSelection.ts +++ b/src/selection/useSelection.ts @@ -267,8 +267,8 @@ export const useSelection = ({ pathSelectionType ); - ref.current?.centerGraph([data.id, ...adjacents], { - centerOnlyIfNodesNotInView: true + ref.current.fitNodesInView([data.id, ...adjacents], { + fitOnlyIfNodesNotInView: true }); } }, @@ -352,9 +352,7 @@ export const useSelection = ({ throw new Error('No ref found for the graph canvas.'); } - ref.current?.centerGraph([], { - centerOnlyIfNodesNotInView: true - }); + ref.current.fitNodesInView([], { fitOnlyIfNodesNotInView: true }); } } },