feat: adds utility methods on the window for debugging

This commit is contained in:
Mark Mankarious 2023-08-06 21:20:31 +01:00
parent b2df9db905
commit 38c4278e16
19 changed files with 244 additions and 58 deletions

View file

@ -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;

View file

@ -0,0 +1,6 @@
import React from 'react';
import { SizeIndicator } from './SizeIndicator';
export const DebugUtils = () => {
return <SizeIndicator />;
};

View 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'
}}
/>
);
};

View file

@ -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>
);
};

View file

@ -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>
);
};

View file

@ -88,7 +88,7 @@ export const CustomNode = () => {
iconId: 'server',
position: {
x: 0,
y: 3
y: -3
}
}
]

View file

@ -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
View file

@ -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;
};
}
}

View 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
};
};

View 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]);
};

View file

@ -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
View file

@ -0,0 +1,4 @@
declare module '*.svg' {
const content: React.FunctionComponent<React.SVGAttributes<SVGElement>>;
export default content;
}

View file

@ -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();

View file

@ -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 } });
},

View file

@ -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'
}
}}
/>
);
};

View file

@ -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;
}

View file

@ -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;

View file

@ -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 {

View file

@ -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: {