feat: allows project to be centered

This commit is contained in:
Mark Mankarious 2023-08-13 12:54:37 +01:00
parent 9cfef256e2
commit c638bf015c
12 changed files with 179 additions and 34 deletions

View file

@ -30,6 +30,7 @@ interface Props {
onSceneUpdated?: (scene: SceneInput) => void;
width?: number | string;
height?: number | string;
debugMode?: boolean;
}
const App = ({
@ -37,7 +38,8 @@ const App = ({
width,
height = 500,
interactionsEnabled: interactionsEnabledProp = true,
onSceneUpdated
onSceneUpdated,
debugMode = false
}: Props) => {
const [isReady, setIsReady] = useState(false);
useWindowUtils();
@ -73,6 +75,10 @@ const App = ({
onSceneUpdated(sceneInput);
}, [scene, onSceneUpdated, isReady]);
useEffect(() => {
uiActions.setDebugMode(debugMode);
}, [debugMode, uiActions]);
return (
<>
<GlobalStyles />

View file

@ -1,6 +1,38 @@
import React from 'react';
import { Box, Typography, useTheme } from '@mui/material';
import { useUiStateStore } from 'src/stores/uiStateStore';
import { SizeIndicator } from './SizeIndicator';
export const DebugUtils = () => {
return <SizeIndicator />;
const {
customVars: { appPadding }
} = useTheme();
const { scroll, mouse } = useUiStateStore(({ scroll, mouse }) => {
return { scroll, mouse };
});
return (
<>
<SizeIndicator />
<Box
sx={{
position: 'absolute',
left: appPadding.x,
top: appPadding.y,
bgcolor: 'common.white',
p: 2,
borderRadius: 1,
border: 1,
minWidth: 200
}}
>
<Typography variant="body2">
Scroll: {scroll.position.x}, {scroll.position.y}
</Typography>
<Typography variant="body2">
Mouse: {mouse.position.tile.x}, {mouse.position.tile.y}
</Typography>
</Box>
</>
);
};

View file

@ -3,10 +3,10 @@ import { Box } from '@mui/material';
import { useDiagramUtils } from 'src/hooks/useDiagramUtils';
export const SizeIndicator = () => {
const { getDiagramBoundingBox } = useDiagramUtils();
const { getUnprojectedBounds } = useDiagramUtils();
const diagramBoundingBox = useMemo(() => {
return getDiagramBoundingBox();
}, [getDiagramBoundingBox]);
return getUnprojectedBounds();
}, [getUnprojectedBounds]);
return (
<Box

View file

@ -1,4 +1,4 @@
import React, { useCallback, useState, useEffect, useRef } from 'react';
import React, { useCallback, useEffect, useRef } from 'react';
import { Box } from '@mui/material';
import { Node as NodeI } from 'src/types';
import { useUiStateStore } from 'src/stores/uiStateStore';
@ -14,8 +14,10 @@ import { useResizeObserver } from 'src/hooks/useResizeObserver';
import { SceneLayer } from 'src/components/SceneLayer/SceneLayer';
export const Renderer = () => {
const [isDebugModeOn] = useState(false);
const containerRef = useRef<HTMLDivElement>();
const debugMode = useUiStateStore((state) => {
return state.debugMode;
});
const nodes = useSceneStore((state) => {
return state.nodes;
});
@ -135,7 +137,7 @@ export const Renderer = () => {
);
})}
</SceneLayer>
{isDebugModeOn && (
{debugMode && (
<SceneLayer>
<DebugUtils />
</SceneLayer>

View file

@ -0,0 +1,51 @@
import React from 'react';
import Isoflow from 'src/Isoflow';
import { icons } from '../icons';
export const DebugTools = () => {
return (
<Isoflow
initialScene={{
icons,
connectors: [
{
id: 'connector1',
from: 'database',
to: 'server',
label: 'connection'
}
],
groups: [
{
id: 'group1',
nodeIds: ['server', 'database']
}
],
nodes: [
{
id: 'server',
label: 'Requests per minute',
labelHeight: 40,
iconId: 'server',
position: {
x: 0,
y: 0
}
},
{
id: 'database',
label: 'Transactions',
labelHeight: 40,
iconId: 'server',
position: {
x: 5,
y: 3
}
}
]
}}
debugMode
height="100%"
/>
);
};

View file

@ -3,8 +3,10 @@ import { Box, Select, MenuItem, useTheme } from '@mui/material';
import { BasicEditor } from './BasicEditor/BasicEditor';
import { CustomNode } from './CustomNode/CustomNode';
import { Callbacks } from './Callbacks/Callbacks';
import { DebugTools } from './DebugTools/DebugTools';
const examples = [
{ name: 'Debug tools', component: DebugTools },
{ name: 'Live Diagrams', component: CustomNode },
{ name: 'Callbacks', component: Callbacks },
{ name: 'Basic Editor', component: BasicEditor }

2
src/global.d.ts vendored
View file

@ -3,7 +3,7 @@ import { Size, Coords, SceneInput } from 'src/types';
declare global {
interface Window {
Isoflow: {
getDiagramBoundingBox: () => Size & Coords;
getUnprojectedBounds: () => Size & Coords;
fitDiagramToScreen: () => void;
setScene: (scene: SceneInput) => void;
};

View file

@ -1,11 +1,12 @@
import { useCallback } from 'react';
import { useSceneStore } from 'src/stores/sceneStore';
import { useUiStateStore } from 'src/stores/uiStateStore';
import { Size, Coords } from 'src/types';
import { Size, Coords, Node, TileOriginEnum } from 'src/types';
import { getBoundingBox, getBoundingBoxSize, sortByPosition } from 'src/utils';
import { useGetTilePosition } from 'src/hooks/useGetTilePosition';
import { useScroll } from 'src/hooks/useScroll';
const BOUNDING_BOX_PADDING = 4;
const BOUNDING_BOX_PADDING = 0;
export const useDiagramUtils = () => {
const scene = useSceneStore(({ nodes, groups, connectors, icons }) => {
@ -16,15 +17,17 @@ export const useDiagramUtils = () => {
icons
};
});
const { scrollToTile } = useScroll();
const uiStateActions = useUiStateStore((state) => {
return state.actions;
});
const scroll = useUiStateStore((state) => {
return state.scroll;
});
const { getTilePosition } = useGetTilePosition();
const getDiagramBoundingBox = useCallback((): Size & Coords => {
if (scene.nodes.length === 0) return { width: 0, height: 0, x: 0, y: 0 };
const nodePositions = scene.nodes.map((node) => {
const getProjectBounds = useCallback((nodes: Node[]): Coords[] => {
const nodePositions = nodes.map((node) => {
return node.position;
});
@ -32,7 +35,14 @@ export const useDiagramUtils = () => {
x: BOUNDING_BOX_PADDING,
y: BOUNDING_BOX_PADDING
});
const cornerPositions = corners.map((corner) => {
return corners;
}, []);
const getUnprojectedBounds = useCallback((): Size & Coords => {
const projectBounds = getProjectBounds(scene.nodes);
const cornerPositions = projectBounds.map((corner) => {
return getTilePosition({
tile: corner
});
@ -47,30 +57,26 @@ export const useDiagramUtils = () => {
x: topLeft.x,
y: topLeft.y
};
}, [scene, getTilePosition]);
}, [scene, getTilePosition, getProjectBounds]);
const fitDiagramToScreen = useCallback(() => {
const boundingBox = getDiagramBoundingBox();
const boundingBox = getProjectBounds(scene.nodes);
const sortedCornerPositions = sortByPosition(boundingBox);
const boundingBoxSize = getBoundingBoxSize(boundingBox);
// const newZoom = Math.min(
// window.innerWidth / boundingBox.width,
// window.innerHeight / boundingBox.height
// );
uiStateActions.setScroll({
offset: {
x: 0,
y: 0
},
position: {
x: -(boundingBox.x + boundingBox.width / 2) + window.innerWidth / 2,
y: -(boundingBox.y + boundingBox.height / 2) + window.innerHeight / 2
}
});
// uiStateActions.setZoom(newZoom);
}, [getDiagramBoundingBox, uiStateActions]);
const centralTile: Coords = {
x: sortedCornerPositions.lowX + Math.floor(boundingBoxSize.width / 2),
y: sortedCornerPositions.lowY + Math.floor(boundingBoxSize.height / 2)
};
scrollToTile(centralTile);
}, [getProjectBounds, scene, scrollToTile]);
return {
getDiagramBoundingBox,
getUnprojectedBounds,
fitDiagramToScreen
};
};

40
src/hooks/useScroll.ts Normal file
View file

@ -0,0 +1,40 @@
import { useCallback } from 'react';
import { Coords, TileOriginEnum } from 'src/types';
import { useGetTilePosition } from 'src/hooks/useGetTilePosition';
import { useUiStateStore } from 'src/stores/uiStateStore';
export const useScroll = () => {
const { getTilePosition } = useGetTilePosition();
const scroll = useUiStateStore((state) => {
return state.scroll;
});
const uiStateActions = useUiStateStore((state) => {
return state.actions;
});
const rendererSize = useUiStateStore((state) => {
return state.rendererSize;
});
const scrollToTile = useCallback(
(tile: Coords, origin?: TileOriginEnum) => {
const tilePosition = getTilePosition({ tile, origin });
const scrollTo: Coords = {
x: scroll.position.x - tilePosition.x + rendererSize.width / 2,
y: scroll.position.y - tilePosition.y + rendererSize.height / 2
};
uiStateActions.setScroll({
offset: {
x: 0,
y: 0
},
position: scrollTo
});
},
[getTilePosition, scroll.position, uiStateActions, rendererSize]
);
return {
scrollToTile
};
};

View file

@ -14,13 +14,13 @@ export const useWindowUtils = () => {
const sceneActions = useSceneStore(({ actions }) => {
return actions;
});
const { fitDiagramToScreen, getDiagramBoundingBox } = useDiagramUtils();
const { fitDiagramToScreen, getUnprojectedBounds } = useDiagramUtils();
useEffect(() => {
window.Isoflow = {
getDiagramBoundingBox,
getUnprojectedBounds,
fitDiagramToScreen,
setScene: sceneActions.setScene
};
}, [getDiagramBoundingBox, fitDiagramToScreen, scene, sceneActions]);
}, [getUnprojectedBounds, fitDiagramToScreen, scene, sceneActions]);
};

View file

@ -29,6 +29,7 @@ const initialState = () => {
position: { x: 0, y: 0 },
offset: { x: 0, y: 0 }
},
debugMode: false,
zoom: 1,
rendererSize: { width: 0, height: 0 },
actions: {
@ -71,6 +72,9 @@ const initialState = () => {
mode: { type: 'CURSOR', showCursor: true, mousedown: null }
});
}
},
setDebugMode: (debugMode) => {
set({ debugMode });
}
}
};

View file

@ -98,6 +98,7 @@ export interface UiState {
scroll: Scroll;
mouse: Mouse;
rendererSize: Size;
debugMode: boolean;
}
export interface UiStateActions {
@ -111,4 +112,5 @@ export interface UiStateActions {
setMouse: (mouse: Mouse) => void;
setRendererSize: (rendererSize: Size) => void;
setInteractionsEnabled: (enabled: boolean) => void;
setDebugMode: (enabled: boolean) => void;
}