mirror of
https://github.com/markmanx/isoflow.git
synced 2025-01-31 23:22:31 +00:00
feat: adds utility methods on the window for debugging
This commit is contained in:
parent
b2df9db905
commit
38c4278e16
19 changed files with 244 additions and 58 deletions
|
@ -14,53 +14,41 @@ import {
|
|||
import { useSceneStore } from 'src/stores/useSceneStore';
|
||||
import { GlobalStyles } from 'src/styles/GlobalStyles';
|
||||
import { Renderer } from 'src/components/Renderer/Renderer';
|
||||
import { sceneInputtoScene, sceneToSceneInput } from 'src/utils';
|
||||
import { sceneToSceneInput } from 'src/utils';
|
||||
import { LabelContainer } from 'src/components/Node/LabelContainer';
|
||||
import { useWindowUtils } from 'src/hooks/useWindowUtils';
|
||||
import { ItemControlsManager } from './components/ItemControls/ItemControlsManager';
|
||||
import { useUiStateStore } from './stores/useUiStateStore';
|
||||
|
||||
interface Props {
|
||||
initialScene: SceneInput;
|
||||
initialScene: SceneInput & {
|
||||
zoom?: number;
|
||||
hideToolbar?: boolean;
|
||||
};
|
||||
onSceneUpdated?: (scene: SceneInput, prevScene: SceneInput) => void;
|
||||
width?: number | string;
|
||||
height?: number | string;
|
||||
}
|
||||
|
||||
const InnerApp = React.memo(
|
||||
({ height, width }: Pick<Props, 'height' | 'width'>) => {
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<GlobalStyles />
|
||||
<Box
|
||||
sx={{
|
||||
width: width ?? '100%',
|
||||
height,
|
||||
position: 'relative',
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
>
|
||||
<Renderer />
|
||||
<ItemControlsManager />
|
||||
<ToolMenu />
|
||||
</Box>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const Isoflow = ({
|
||||
initialScene,
|
||||
width,
|
||||
height = 500,
|
||||
onSceneUpdated
|
||||
}: Props) => {
|
||||
useWindowUtils();
|
||||
const sceneActions = useSceneStore((state) => {
|
||||
return state.actions;
|
||||
});
|
||||
const uiActions = useUiStateStore((state) => {
|
||||
return state.actions;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const convertedInput = sceneInputtoScene(initialScene);
|
||||
sceneActions.set(convertedInput);
|
||||
}, [initialScene, sceneActions]);
|
||||
uiActions.setZoom(initialScene.zoom ?? 1);
|
||||
uiActions.setToolbarVisibility(initialScene.hideToolbar ?? false);
|
||||
sceneActions.setScene(initialScene);
|
||||
}, [initialScene, sceneActions, uiActions]);
|
||||
|
||||
useSceneStore.subscribe((scene, prevScene) => {
|
||||
if (!onSceneUpdated) return;
|
||||
|
@ -68,7 +56,23 @@ const Isoflow = ({
|
|||
onSceneUpdated(sceneToSceneInput(scene), sceneToSceneInput(prevScene));
|
||||
});
|
||||
|
||||
return <InnerApp height={height} width={width} />;
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<GlobalStyles />
|
||||
<Box
|
||||
sx={{
|
||||
width: width ?? '100%',
|
||||
height,
|
||||
position: 'relative',
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
>
|
||||
<Renderer />
|
||||
<ItemControlsManager />
|
||||
{!initialScene.hideToolbar && <ToolMenu />}
|
||||
</Box>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const useIsoflow = () => {
|
||||
|
@ -81,6 +85,8 @@ const useIsoflow = () => {
|
|||
};
|
||||
};
|
||||
|
||||
export default Isoflow;
|
||||
|
||||
export {
|
||||
Scene,
|
||||
SceneInput,
|
||||
|
@ -91,5 +97,3 @@ export {
|
|||
useIsoflow,
|
||||
LabelContainer
|
||||
};
|
||||
|
||||
export default Isoflow;
|
||||
|
|
6
src/components/DebugUtils/DebugUtils.tsx
Normal file
6
src/components/DebugUtils/DebugUtils.tsx
Normal file
|
@ -0,0 +1,6 @@
|
|||
import React from 'react';
|
||||
import { SizeIndicator } from './SizeIndicator';
|
||||
|
||||
export const DebugUtils = () => {
|
||||
return <SizeIndicator />;
|
||||
};
|
23
src/components/DebugUtils/SizeIndicator.tsx
Normal file
23
src/components/DebugUtils/SizeIndicator.tsx
Normal file
|
@ -0,0 +1,23 @@
|
|||
import React, { useMemo } from 'react';
|
||||
import { Box } from '@mui/material';
|
||||
import { useDiagramUtils } from 'src/hooks/useDiagramUtils';
|
||||
|
||||
export const SizeIndicator = () => {
|
||||
const { getDiagramBoundingBox } = useDiagramUtils();
|
||||
const diagramBoundingBox = useMemo(() => {
|
||||
return getDiagramBoundingBox();
|
||||
}, [getDiagramBoundingBox]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
width: diagramBoundingBox.width,
|
||||
height: diagramBoundingBox.height,
|
||||
left: diagramBoundingBox.x,
|
||||
top: diagramBoundingBox.y,
|
||||
border: '5px solid red'
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useCallback } from 'react';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { Box } from '@mui/material';
|
||||
import { Node as NodeI } from 'src/types';
|
||||
import { useUiStateStore } from 'src/stores/useUiStateStore';
|
||||
|
@ -9,8 +9,10 @@ import { Cursor } from 'src/components/Cursor/Cursor';
|
|||
import { Node } from 'src/components/Node/Node';
|
||||
import { Group } from 'src/components/Group/Group';
|
||||
import { Connector } from 'src/components/Connector/Connector';
|
||||
import { DebugUtils } from 'src/components/DebugUtils/DebugUtils';
|
||||
|
||||
export const Renderer = () => {
|
||||
const [isDebugModeOn] = useState(false);
|
||||
const scene = useSceneStore(({ nodes, connectors, groups }) => {
|
||||
return { nodes, connectors, groups };
|
||||
});
|
||||
|
@ -98,6 +100,11 @@ export const Renderer = () => {
|
|||
/>
|
||||
);
|
||||
})}
|
||||
{isDebugModeOn && (
|
||||
<Box sx={{ position: 'absolute' }}>
|
||||
<DebugUtils />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -4,13 +4,15 @@ import {
|
|||
PanTool as PanToolIcon,
|
||||
ZoomIn as ZoomInIcon,
|
||||
ZoomOut as ZoomOutIcon,
|
||||
NearMe as NearMeIcon
|
||||
NearMe as NearMeIcon,
|
||||
CenterFocusStrong as CenterFocusStrongIcon
|
||||
} from '@mui/icons-material';
|
||||
import {
|
||||
useUiStateStore,
|
||||
MIN_ZOOM,
|
||||
MAX_ZOOM
|
||||
} from 'src/stores/useUiStateStore';
|
||||
import { useDiagramUtils } from 'src/hooks/useDiagramUtils';
|
||||
import { IconButton } from '../IconButton/IconButton';
|
||||
|
||||
export const ToolMenu = () => {
|
||||
|
@ -24,6 +26,7 @@ export const ToolMenu = () => {
|
|||
const uiStateStoreActions = useUiStateStore((state) => {
|
||||
return state.actions;
|
||||
});
|
||||
const { fitDiagramToScreen } = useDiagramUtils();
|
||||
|
||||
return (
|
||||
<Card
|
||||
|
@ -74,6 +77,12 @@ export const ToolMenu = () => {
|
|||
size={theme.customVars.toolMenu.height}
|
||||
disabled={zoom === MIN_ZOOM}
|
||||
/>
|
||||
<IconButton
|
||||
name="Center"
|
||||
Icon={<CenterFocusStrongIcon />}
|
||||
onClick={fitDiagramToScreen}
|
||||
size={theme.customVars.toolMenu.height}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -88,7 +88,7 @@ export const CustomNode = () => {
|
|||
iconId: 'server',
|
||||
position: {
|
||||
x: 0,
|
||||
y: 3
|
||||
y: -3
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
@ -4,8 +4,8 @@ import { BasicEditor } from './BasicEditor/BasicEditor';
|
|||
import { CustomNode } from './CustomNode/CustomNode';
|
||||
|
||||
const examples = [
|
||||
{ name: 'Basic Editor', component: BasicEditor },
|
||||
{ name: 'Live Diagrams', component: CustomNode }
|
||||
{ name: 'Live Diagrams', component: CustomNode },
|
||||
{ name: 'Basic Editor', component: BasicEditor }
|
||||
];
|
||||
|
||||
export const Examples = () => {
|
||||
|
|
13
src/global.d.ts
vendored
13
src/global.d.ts
vendored
|
@ -1,4 +1,11 @@
|
|||
declare module '*.svg' {
|
||||
const content: React.FunctionComponent<React.SVGAttributes<SVGElement>>;
|
||||
export default content;
|
||||
import { Size, Coords, SceneInput } from 'src/types';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
Isoflow: {
|
||||
getDiagramBoundingBox: () => Size & Coords;
|
||||
fitDiagramToScreen: () => void;
|
||||
setScene: (scene: SceneInput) => void;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
87
src/hooks/useDiagramUtils.ts
Normal file
87
src/hooks/useDiagramUtils.ts
Normal file
|
@ -0,0 +1,87 @@
|
|||
import { useCallback } from 'react';
|
||||
import { useSceneStore } from 'src/stores/useSceneStore';
|
||||
import { useUiStateStore } from 'src/stores/useUiStateStore';
|
||||
import { Size, Coords } from 'src/types';
|
||||
import {
|
||||
getBoundingBox,
|
||||
getBoundingBoxSize,
|
||||
sortByPosition,
|
||||
getTilePosition
|
||||
} from 'src/utils';
|
||||
|
||||
const BOUNDING_BOX_PADDING = 4;
|
||||
|
||||
export const useDiagramUtils = () => {
|
||||
const zoom = useUiStateStore((state) => {
|
||||
return state.zoom;
|
||||
});
|
||||
const scroll = useUiStateStore((state) => {
|
||||
return state.scroll;
|
||||
});
|
||||
const scene = useSceneStore(({ nodes, groups, connectors, icons }) => {
|
||||
return {
|
||||
nodes,
|
||||
groups,
|
||||
connectors,
|
||||
icons
|
||||
};
|
||||
});
|
||||
const uiStateActions = useUiStateStore((state) => {
|
||||
return state.actions;
|
||||
});
|
||||
|
||||
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) => {
|
||||
return node.position;
|
||||
});
|
||||
|
||||
const corners = getBoundingBox(nodePositions, {
|
||||
x: BOUNDING_BOX_PADDING,
|
||||
y: BOUNDING_BOX_PADDING
|
||||
});
|
||||
const cornerPositions = corners.map((corner) => {
|
||||
return getTilePosition({
|
||||
scroll,
|
||||
tile: corner,
|
||||
zoom
|
||||
});
|
||||
});
|
||||
const sortedCorners = sortByPosition(cornerPositions);
|
||||
const topLeft = { x: sortedCorners.lowX, y: sortedCorners.lowY };
|
||||
const size = getBoundingBoxSize(cornerPositions);
|
||||
|
||||
return {
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
x: topLeft.x,
|
||||
y: topLeft.y
|
||||
};
|
||||
}, [scene, scroll, zoom]);
|
||||
|
||||
const fitDiagramToScreen = useCallback(() => {
|
||||
const boundingBox = getDiagramBoundingBox();
|
||||
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]);
|
||||
|
||||
return {
|
||||
getDiagramBoundingBox,
|
||||
fitDiagramToScreen
|
||||
};
|
||||
};
|
26
src/hooks/useWindowUtils.ts
Normal file
26
src/hooks/useWindowUtils.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
import { useEffect } from 'react';
|
||||
import { useSceneStore } from 'src/stores/useSceneStore';
|
||||
import { useDiagramUtils } from 'src/hooks/useDiagramUtils';
|
||||
|
||||
export const useWindowUtils = () => {
|
||||
const scene = useSceneStore(({ nodes, groups, connectors, icons }) => {
|
||||
return {
|
||||
nodes,
|
||||
groups,
|
||||
connectors,
|
||||
icons
|
||||
};
|
||||
});
|
||||
const sceneActions = useSceneStore(({ actions }) => {
|
||||
return actions;
|
||||
});
|
||||
const { fitDiagramToScreen, getDiagramBoundingBox } = useDiagramUtils();
|
||||
|
||||
useEffect(() => {
|
||||
window.Isoflow = {
|
||||
getDiagramBoundingBox,
|
||||
fitDiagramToScreen,
|
||||
setScene: sceneActions.setScene
|
||||
};
|
||||
}, [getDiagramBoundingBox, fitDiagramToScreen, scene, sceneActions]);
|
||||
};
|
|
@ -41,9 +41,6 @@ export const useInteractionManager = () => {
|
|||
const scene = useSceneStore(({ nodes, connectors, groups, icons }) => {
|
||||
return { nodes, connectors, groups, icons };
|
||||
});
|
||||
const sceneActions = useSceneStore((state) => {
|
||||
return state.actions;
|
||||
});
|
||||
|
||||
const onMouseEvent = useCallback(
|
||||
(e: MouseEvent) => {
|
||||
|
@ -110,7 +107,6 @@ export const useInteractionManager = () => {
|
|||
uiStateActions.setMode(newState.mode);
|
||||
uiStateActions.setContextMenu(newState.contextMenu);
|
||||
uiStateActions.setSidebar(newState.itemControls);
|
||||
sceneActions.setItems(newState.scene);
|
||||
},
|
||||
[
|
||||
mode,
|
||||
|
@ -120,7 +116,6 @@ export const useInteractionManager = () => {
|
|||
scroll,
|
||||
itemControls,
|
||||
uiStateActions,
|
||||
sceneActions,
|
||||
scene,
|
||||
contextMenu,
|
||||
zoom
|
||||
|
|
4
src/module.d.ts
vendored
Normal file
4
src/module.d.ts
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
declare module '*.svg' {
|
||||
const content: React.FunctionComponent<React.SVGAttributes<SVGElement>>;
|
||||
export default content;
|
||||
}
|
|
@ -4,6 +4,8 @@ import { v4 as uuid } from 'uuid';
|
|||
import { produce } from 'immer';
|
||||
import { NODE_DEFAULTS } from 'src/config';
|
||||
import { Scene, SceneActions, Node, SceneItemTypeEnum } from 'src/types';
|
||||
import { sceneInput } from 'src/validation/scene';
|
||||
import { sceneInputtoScene } from 'src/utils';
|
||||
|
||||
export type UseSceneStore = Scene & {
|
||||
actions: SceneActions;
|
||||
|
@ -17,11 +19,12 @@ export const useSceneStore = create<UseSceneStore>((set, get) => {
|
|||
groups: [],
|
||||
icons: [],
|
||||
actions: {
|
||||
set: (scene) => {
|
||||
set(scene);
|
||||
},
|
||||
setItems: (items) => {
|
||||
set({ nodes: items.nodes });
|
||||
setScene: (scene) => {
|
||||
sceneInput.parse(scene);
|
||||
|
||||
const newScene = sceneInputtoScene(scene);
|
||||
|
||||
set(newScene);
|
||||
},
|
||||
updateNode: (id, updates) => {
|
||||
const { nodes } = get();
|
||||
|
|
|
@ -13,6 +13,7 @@ export type UseUiStateStore = UiState & {
|
|||
|
||||
export const useUiStateStore = create<UseUiStateStore>((set, get) => {
|
||||
return {
|
||||
hideToolbar: false,
|
||||
mode: {
|
||||
type: 'CURSOR',
|
||||
showCursor: true,
|
||||
|
@ -44,6 +45,12 @@ export const useUiStateStore = create<UseUiStateStore>((set, get) => {
|
|||
const targetZoom = clamp(zoom - ZOOM_INCREMENT, MIN_ZOOM, MAX_ZOOM);
|
||||
set({ zoom: roundToOneDecimalPlace(targetZoom) });
|
||||
},
|
||||
setToolbarVisibility: (visible) => {
|
||||
set({ hideToolbar: visible });
|
||||
},
|
||||
setZoom: (zoom) => {
|
||||
set({ zoom });
|
||||
},
|
||||
setScroll: ({ position, offset }) => {
|
||||
set({ scroll: { position, offset: offset ?? get().scroll.offset } });
|
||||
},
|
||||
|
|
|
@ -3,5 +3,13 @@ import { GlobalStyles as MUIGlobalStyles } from '@mui/material';
|
|||
import 'react-quill/dist/quill.snow.css';
|
||||
|
||||
export const GlobalStyles = () => {
|
||||
return <MUIGlobalStyles styles={{}} />;
|
||||
return (
|
||||
<MUIGlobalStyles
|
||||
styles={{
|
||||
div: {
|
||||
boxSizing: 'border-box'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Coords } from './common';
|
||||
import { IconInput } from './inputs';
|
||||
import { IconInput, SceneInput } from './inputs';
|
||||
|
||||
export enum TileOriginEnum {
|
||||
CENTER = 'CENTER',
|
||||
|
@ -54,12 +54,7 @@ export type Scene = {
|
|||
};
|
||||
|
||||
export interface SceneActions {
|
||||
set: (scene: Scene) => void;
|
||||
setItems: (elements: {
|
||||
nodes: Node[];
|
||||
connectors: Connector[];
|
||||
groups: Group[];
|
||||
}) => void;
|
||||
setScene: (scene: SceneInput) => void;
|
||||
updateNode: (id: string, updates: Partial<Node>) => void;
|
||||
createNode: (position: Coords) => void;
|
||||
}
|
||||
|
|
|
@ -80,6 +80,7 @@ export interface Scroll {
|
|||
}
|
||||
|
||||
export interface UiState {
|
||||
hideToolbar: boolean;
|
||||
mode: Mode;
|
||||
itemControls: ItemControls;
|
||||
contextMenu: ContextMenu;
|
||||
|
@ -90,8 +91,10 @@ export interface UiState {
|
|||
|
||||
export interface UiStateActions {
|
||||
setMode: (mode: Mode) => void;
|
||||
setToolbarVisibility: (visible: boolean) => void;
|
||||
incrementZoom: () => void;
|
||||
decrementZoom: () => void;
|
||||
setZoom: (zoom: number) => void;
|
||||
setScroll: (scroll: Scroll) => void;
|
||||
setSidebar: (itemControls: ItemControls) => void;
|
||||
setContextMenu: (contextMenu: ContextMenu) => void;
|
||||
|
|
|
@ -10,7 +10,6 @@ import {
|
|||
Group
|
||||
} from 'src/types';
|
||||
import { NODE_DEFAULTS, DEFAULT_COLOR } from 'src/config';
|
||||
import { customVars } from 'src/styles/theme';
|
||||
|
||||
export const nodeInputToNode = (nodeInput: NodeInput): Node => {
|
||||
return {
|
||||
|
|
|
@ -14,7 +14,10 @@ module.exports = {
|
|||
static: {
|
||||
directory: path.join(__dirname, "build"),
|
||||
},
|
||||
allowedHosts: ['.csb.app'],
|
||||
allowedHosts: [
|
||||
'.csb.app', // So Codesandbox.io can run the dev server
|
||||
'.ngrok-free.app'
|
||||
],
|
||||
port: 3000,
|
||||
},
|
||||
module: {
|
||||
|
|
Loading…
Reference in a new issue