mirror of
https://github.com/stan-smith/FossFLOW.git
synced 2025-12-24 06:58:48 -05:00
feat: refactors schema to accomodate model
This commit is contained in:
@@ -41,7 +41,7 @@
|
||||
- [x] Nodes, connectors & groups
|
||||
- [x] Import / export diagrams to local storage (in JSON format)
|
||||
- [x] Icon support for AWS, GCP, Azure, K8S & generic network hardware (e.g. server, database)
|
||||
- [x] onSceneUpdated callback
|
||||
- [x] onModelUpdated callback
|
||||
- [x] Documentation
|
||||
- [x] Pipeline for publishing to NPM
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"index": "Props",
|
||||
"initialScene": "initialScene"
|
||||
"InitialData": "InitialData"
|
||||
}
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
| Name | Type | Description | Default |
|
||||
| --- | --- | --- | --- |
|
||||
| `initialScene` | [`object`](/docs/api/initialScene) | The initial scene that Isoflow should render. If `undefined`, isoflow loads a blank scene. | `undefined` |
|
||||
| `InitialData` | [`object`](/docs/api/InitialData) | The initial Model that Isoflow should render. If `undefined`, isoflow loads a blank Model. | `undefined` |
|
||||
| `width` | `number` \| `string` | Width of the Isoflow renderer as a CSS value. | `100%` |
|
||||
| `height` | `number` \| `string` | Height of the Isoflow renderer as a CSS value. | `100%` |
|
||||
| `onSceneUpdate` | `function` | A callback that is triggered whenever an item is added, updated or removed from the scene. The callback is called with the updated scene as the first argument. | `undefined` |
|
||||
| `onModelUpdate` | `function` | A callback that is triggered whenever an item is added, updated or removed from the Model. The callback is called with the updated Model as the first argument. | `undefined` |
|
||||
| `enableDebugTools` | `boolean` | Enables extra tools for debugging purposes. | `false` |
|
||||
| `editorMode` | `"EXPLORABLE_READONLY"` \| `"NON_INTERACTIVE"` \| `"EDITABLE"` | Enables / disables editor features. | `"EDITABLE"` |
|
||||
| `mainMenuOptions` | `("ACTION.OPEN" \| "EXPORT.JSON" \| "EXPORT.PNG" \| "ACTION.CLEAR_CANVAS" \| "LINK.GITHUB" \| "LINK.DISCORD" \| "VERSION")[]` | Shows / hides options in the main menu. If `[]` is passed, the menu is hidden. | All enabled |
|
||||
@@ -1,6 +1,6 @@
|
||||
# `initialScene`
|
||||
# `InitialData`
|
||||
|
||||
The `initialScene` object contains the following properties:
|
||||
The `InitialData` object contains the following properties:
|
||||
|
||||
| Name | Type | Default |
|
||||
| --- | --- | --- |
|
||||
@@ -120,11 +120,11 @@ ref:
|
||||
}
|
||||
```
|
||||
|
||||
## `initialScene` example
|
||||
A demo scene is hosted here on [CodeSandbox](https://codesandbox.io/p/sandbox/github/markmanx/isoflow/tree/main).
|
||||
## `InitialData` example
|
||||
A demo Model is hosted here on [CodeSandbox](https://codesandbox.io/p/sandbox/github/markmanx/isoflow/tree/main).
|
||||
|
||||
## Validation
|
||||
`initialScene` is validated before Isoflow renders the scene, and an error is thrown if the data is invalid.
|
||||
`InitialData` is validated before Isoflow renders the Model, and an error is thrown if the data is invalid.
|
||||
|
||||
Examples of common errors are as follows:
|
||||
- A `ConnectorAnchor` references a `Node` that does not exist.
|
||||
|
||||
@@ -45,7 +45,7 @@ const icons = flattenCollections([
|
||||
|
||||
const App = () => {
|
||||
return (
|
||||
<Isoflow initialScene={{ icons }} />
|
||||
<Isoflow InitialData={{ icons }} />
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
130
src/Isoflow.tsx
130
src/Isoflow.tsx
@@ -4,44 +4,47 @@ import { ThemeProvider } from '@mui/material/styles';
|
||||
import { Box } from '@mui/material';
|
||||
import { theme } from 'src/styles/theme';
|
||||
import {
|
||||
SceneInput,
|
||||
IconInput,
|
||||
NodeInput,
|
||||
ConnectorInput,
|
||||
RectangleInput,
|
||||
Model,
|
||||
ModelItem,
|
||||
Icon,
|
||||
Connector,
|
||||
Rectangle,
|
||||
IsoflowProps,
|
||||
InitialScene
|
||||
InitialData
|
||||
} from 'src/types';
|
||||
import { sceneToSceneInput, setWindowCursor, CoordsUtils } from 'src/utils';
|
||||
import { useSceneStore, SceneProvider } from 'src/stores/sceneStore';
|
||||
import { setWindowCursor, generateId } from 'src/utils';
|
||||
import { modelSchema } from 'src/validation/model';
|
||||
import { useModelStore, ModelProvider } from 'src/stores/modelStore';
|
||||
import { SceneProvider } from 'src/stores/sceneStore';
|
||||
import { GlobalStyles } from 'src/styles/GlobalStyles';
|
||||
import { Renderer } from 'src/components/Renderer/Renderer';
|
||||
import { useWindowUtils } from 'src/hooks/useWindowUtils';
|
||||
import { sceneInput as sceneValidationSchema } from 'src/validation/scene';
|
||||
import { UiOverlay } from 'src/components/UiOverlay/UiOverlay';
|
||||
import { UiStateProvider, useUiStateStore } from 'src/stores/uiStateStore';
|
||||
import { INITIAL_SCENE, MAIN_MENU_OPTIONS } from 'src/config';
|
||||
import {
|
||||
INITIAL_DATA,
|
||||
MAIN_MENU_OPTIONS,
|
||||
INITIAL_UI_STATE,
|
||||
VIEW_DEFAULTS
|
||||
} from 'src/config';
|
||||
import { createView } from 'src/stores/reducers';
|
||||
import { useIconCategories } from './hooks/useIconCategories';
|
||||
|
||||
const App = ({
|
||||
initialScene,
|
||||
initialData,
|
||||
mainMenuOptions = MAIN_MENU_OPTIONS,
|
||||
width = '100%',
|
||||
height = '100%',
|
||||
onSceneUpdated,
|
||||
onModelUpdated,
|
||||
enableDebugTools = false,
|
||||
editorMode = 'EDITABLE'
|
||||
}: IsoflowProps) => {
|
||||
useWindowUtils();
|
||||
const prevInitialScene = useRef<SceneInput>(INITIAL_SCENE);
|
||||
const prevInitialData = useRef<Model>(INITIAL_DATA);
|
||||
const [isReady, setIsReady] = useState(false);
|
||||
const scene = useSceneStore(
|
||||
({ title, nodes, connectors, textBoxes, rectangles, icons }) => {
|
||||
return { title, nodes, connectors, textBoxes, rectangles, icons };
|
||||
},
|
||||
shallow
|
||||
);
|
||||
const sceneActions = useSceneStore((state) => {
|
||||
const model = useModelStore((state) => {
|
||||
return state;
|
||||
}, shallow);
|
||||
const modelActions = useModelStore((state) => {
|
||||
return state.actions;
|
||||
});
|
||||
const uiActions = useUiStateStore((state) => {
|
||||
@@ -50,18 +53,24 @@ const App = ({
|
||||
const { setIconCategoriesState } = useIconCategories();
|
||||
|
||||
useEffect(() => {
|
||||
uiActions.setZoom(initialScene?.zoom ?? 1);
|
||||
uiActions.setScroll({
|
||||
position: initialScene?.scroll ?? CoordsUtils.zero(),
|
||||
offset: CoordsUtils.zero()
|
||||
});
|
||||
if (initialData?.zoom) {
|
||||
uiActions.setZoom(initialData.zoom);
|
||||
}
|
||||
|
||||
if (initialData?.scroll) {
|
||||
uiActions.setScroll({
|
||||
position: initialData.scroll,
|
||||
offset: INITIAL_UI_STATE.scroll.offset
|
||||
});
|
||||
}
|
||||
|
||||
uiActions.setEditorMode(editorMode);
|
||||
uiActions.setMainMenuOptions(mainMenuOptions);
|
||||
}, [
|
||||
initialScene?.zoom,
|
||||
initialScene?.scroll,
|
||||
initialData?.zoom,
|
||||
initialData?.scroll,
|
||||
editorMode,
|
||||
sceneActions,
|
||||
modelActions,
|
||||
uiActions,
|
||||
mainMenuOptions
|
||||
]);
|
||||
@@ -73,31 +82,42 @@ const App = ({
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!initialScene || prevInitialScene.current === initialScene) return;
|
||||
if (!initialData || prevInitialData.current === initialData) return;
|
||||
setIsReady(false);
|
||||
|
||||
const fullInitialScene = { ...INITIAL_SCENE, ...initialScene };
|
||||
let fullInitialData = { ...INITIAL_DATA, ...initialData };
|
||||
|
||||
prevInitialScene.current = fullInitialScene;
|
||||
sceneActions.setScene(fullInitialScene);
|
||||
if (fullInitialData.views.length === 0) {
|
||||
const newView = {
|
||||
...VIEW_DEFAULTS,
|
||||
id: generateId()
|
||||
};
|
||||
|
||||
fullInitialData = createView(newView, fullInitialData);
|
||||
}
|
||||
|
||||
prevInitialData.current = fullInitialData;
|
||||
modelActions.set(fullInitialData);
|
||||
uiActions.setView(fullInitialData.views[0].id);
|
||||
setIsReady(true);
|
||||
}, [initialScene, sceneActions, uiActions]);
|
||||
}, [initialData, modelActions, uiActions]);
|
||||
|
||||
useEffect(() => {
|
||||
setIconCategoriesState();
|
||||
}, [scene.icons, setIconCategoriesState]);
|
||||
}, [model.icons, setIconCategoriesState]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isReady || !onSceneUpdated) return;
|
||||
if (!isReady || !onModelUpdated) return;
|
||||
|
||||
const sceneInput = sceneToSceneInput(scene);
|
||||
onSceneUpdated(sceneInput);
|
||||
}, [scene, onSceneUpdated, isReady]);
|
||||
onModelUpdated(model);
|
||||
}, [model, onModelUpdated, isReady]);
|
||||
|
||||
useEffect(() => {
|
||||
uiActions.setenableDebugTools(enableDebugTools);
|
||||
}, [enableDebugTools, uiActions]);
|
||||
|
||||
if (!isReady) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<GlobalStyles />
|
||||
@@ -120,11 +140,13 @@ const App = ({
|
||||
export const Isoflow = (props: IsoflowProps) => {
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<SceneProvider>
|
||||
<UiStateProvider>
|
||||
<App {...props} />
|
||||
</UiStateProvider>
|
||||
</SceneProvider>
|
||||
<ModelProvider>
|
||||
<SceneProvider>
|
||||
<UiStateProvider>
|
||||
<App {...props} />
|
||||
</UiStateProvider>
|
||||
</SceneProvider>
|
||||
</ModelProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
@@ -134,7 +156,7 @@ const useIsoflow = () => {
|
||||
return state.rendererEl;
|
||||
});
|
||||
|
||||
const sceneActions = useSceneStore((state) => {
|
||||
const ModelActions = useModelStore((state) => {
|
||||
return state.actions;
|
||||
});
|
||||
|
||||
@@ -143,7 +165,7 @@ const useIsoflow = () => {
|
||||
});
|
||||
|
||||
return {
|
||||
scene: sceneActions,
|
||||
Model: ModelActions,
|
||||
uiState: uiStateActions,
|
||||
rendererEl
|
||||
};
|
||||
@@ -151,17 +173,17 @@ const useIsoflow = () => {
|
||||
|
||||
export {
|
||||
useIsoflow,
|
||||
InitialScene,
|
||||
SceneInput,
|
||||
IconInput,
|
||||
NodeInput,
|
||||
RectangleInput,
|
||||
ConnectorInput,
|
||||
sceneValidationSchema,
|
||||
InitialData,
|
||||
Model,
|
||||
Icon,
|
||||
ModelItem,
|
||||
Rectangle,
|
||||
Connector,
|
||||
modelSchema,
|
||||
IsoflowProps
|
||||
};
|
||||
|
||||
export const version = PACKAGE_VERSION;
|
||||
export const initialScene = INITIAL_SCENE;
|
||||
export const initialData = INITIAL_DATA;
|
||||
|
||||
export default Isoflow;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
import { Box } from '@mui/material';
|
||||
import { useUiStateStore } from 'src/stores/uiStateStore';
|
||||
import { useSceneStore } from 'src/stores/sceneStore';
|
||||
import { useResizeObserver } from 'src/hooks/useResizeObserver';
|
||||
import { useScene } from 'src/hooks/useScene';
|
||||
import { LineItem } from './LineItem';
|
||||
|
||||
export const DebugUtils = () => {
|
||||
@@ -11,9 +11,7 @@ export const DebugUtils = () => {
|
||||
return { scroll, mouse, zoom, mode, rendererEl };
|
||||
}
|
||||
);
|
||||
const scene = useSceneStore((state) => {
|
||||
return state;
|
||||
});
|
||||
const scene = useScene();
|
||||
const { size: rendererSize } = useResizeObserver(uiState.rendererEl);
|
||||
|
||||
return (
|
||||
@@ -55,7 +53,10 @@ export const DebugUtils = () => {
|
||||
title="Size"
|
||||
value={`${rendererSize.width}, ${rendererSize.height}`}
|
||||
/>
|
||||
<LineItem title="Scene info" value={`${scene.nodes.length} nodes`} />
|
||||
<LineItem
|
||||
title="Scene info"
|
||||
value={`${scene.items.length} items in scene`}
|
||||
/>
|
||||
<LineItem title="Mode" value={uiState.mode.type} />
|
||||
<LineItem title="Mode data" value={JSON.stringify(uiState.mode)} />
|
||||
</Box>
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Box } from '@mui/material';
|
||||
import { Coords, TileOriginEnum, IconInput } from 'src/types';
|
||||
import { Coords } from 'src/types';
|
||||
import { getTilePosition } from 'src/utils';
|
||||
import { useIcon } from 'src/hooks/useIcon';
|
||||
|
||||
interface Props {
|
||||
icon: IconInput;
|
||||
iconId: string;
|
||||
tile: Coords;
|
||||
}
|
||||
|
||||
export const DragAndDrop = ({ icon, tile }: Props) => {
|
||||
const { iconComponent } = useIcon(icon.id);
|
||||
export const DragAndDrop = ({ iconId, tile }: Props) => {
|
||||
const { iconComponent } = useIcon(iconId);
|
||||
|
||||
const tilePosition = useMemo(() => {
|
||||
return getTilePosition({ tile, origin: TileOriginEnum.BOTTOM });
|
||||
return getTilePosition({ tile, origin: 'BOTTOM' });
|
||||
}, [tile]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -8,9 +8,8 @@ import {
|
||||
Stack,
|
||||
Alert
|
||||
} from '@mui/material';
|
||||
import { useSceneStore } from 'src/stores/sceneStore';
|
||||
import { useModelStore } from 'src/stores/modelStore';
|
||||
import {
|
||||
sceneToSceneInput,
|
||||
exportAsImage,
|
||||
downloadFile as downloadFileUtil,
|
||||
getTileScrollPosition,
|
||||
@@ -34,16 +33,18 @@ export const ExportImageDialog = ({ onClose }: Props) => {
|
||||
const uiStateActions = useUiStateStore((state) => {
|
||||
return state.actions;
|
||||
});
|
||||
const scene = useSceneStore((state) => {
|
||||
return {
|
||||
title: state.title,
|
||||
nodes: state.nodes,
|
||||
connectors: state.connectors,
|
||||
textBoxes: state.textBoxes,
|
||||
rectangles: state.rectangles,
|
||||
icons: state.icons
|
||||
};
|
||||
});
|
||||
const model = useModelStore(
|
||||
({ views, description, version, title, items, icons }) => {
|
||||
return {
|
||||
views,
|
||||
description,
|
||||
version,
|
||||
title,
|
||||
items,
|
||||
icons
|
||||
};
|
||||
}
|
||||
);
|
||||
const unprojectedBounds = useMemo(() => {
|
||||
return getUnprojectedBounds();
|
||||
}, [getUnprojectedBounds]);
|
||||
@@ -116,9 +117,9 @@ export const ExportImageDialog = ({ onClose }: Props) => {
|
||||
>
|
||||
<Isoflow
|
||||
editorMode="NON_INTERACTIVE"
|
||||
onSceneUpdated={exportImage}
|
||||
initialScene={{
|
||||
...sceneToSceneInput(scene),
|
||||
onModelUpdated={exportImage}
|
||||
initialData={{
|
||||
...model,
|
||||
zoom: previewParams.zoom,
|
||||
scroll: getTileScrollPosition(previewParams.scrollTarget)
|
||||
}}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { Connector, ConnectorStyleEnum } from 'src/types';
|
||||
import { connectorStyleOptions } from 'src/validation/sceneItems';
|
||||
import React from 'react';
|
||||
import { Connector, connectorStyleOptions } from 'src/types';
|
||||
import {
|
||||
useTheme,
|
||||
Box,
|
||||
@@ -9,10 +8,10 @@ import {
|
||||
MenuItem,
|
||||
TextField
|
||||
} from '@mui/material';
|
||||
import { useSceneStore } from 'src/stores/sceneStore';
|
||||
import { useConnector } from 'src/hooks/useConnector';
|
||||
import { ColorSelector } from 'src/components/ColorSelector/ColorSelector';
|
||||
import { useUiStateStore } from 'src/stores/uiStateStore';
|
||||
import { useScene } from 'src/hooks/useScene';
|
||||
import { ControlsContainer } from '../components/ControlsContainer';
|
||||
import { Section } from '../components/Section';
|
||||
import { DeleteButton } from '../components/DeleteButton';
|
||||
@@ -23,34 +22,22 @@ interface Props {
|
||||
|
||||
export const ConnectorControls = ({ id }: Props) => {
|
||||
const theme = useTheme();
|
||||
const sceneActions = useSceneStore((state) => {
|
||||
return state.actions;
|
||||
});
|
||||
const uiStateActions = useUiStateStore((state) => {
|
||||
return state.actions;
|
||||
});
|
||||
const connector = useConnector(id);
|
||||
|
||||
const onConnectorUpdated = useCallback(
|
||||
(updates: Partial<Connector>) => {
|
||||
sceneActions.updateConnector(id, updates);
|
||||
},
|
||||
[sceneActions, id]
|
||||
);
|
||||
|
||||
const onConnectorDeleted = useCallback(() => {
|
||||
uiStateActions.setItemControls(null);
|
||||
sceneActions.deleteConnector(id);
|
||||
}, [sceneActions, id, uiStateActions]);
|
||||
const { updateConnector, deleteConnector } = useScene();
|
||||
|
||||
return (
|
||||
<ControlsContainer>
|
||||
<Section>
|
||||
<TextField
|
||||
label="Label"
|
||||
value={connector.label}
|
||||
value={connector.description}
|
||||
onChange={(e) => {
|
||||
return onConnectorUpdated({ label: e.target.value as string });
|
||||
updateConnector(connector.id, {
|
||||
description: e.target.value as string
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Section>
|
||||
@@ -58,7 +45,7 @@ export const ConnectorControls = ({ id }: Props) => {
|
||||
<ColorSelector
|
||||
colors={Object.values(theme.customVars.customPalette)}
|
||||
onChange={(color) => {
|
||||
return onConnectorUpdated({ color });
|
||||
return updateConnector(connector.id, { color });
|
||||
}}
|
||||
activeColor={connector.color}
|
||||
/>
|
||||
@@ -71,7 +58,7 @@ export const ConnectorControls = ({ id }: Props) => {
|
||||
max={30}
|
||||
value={connector.width}
|
||||
onChange={(e, newWidth) => {
|
||||
onConnectorUpdated({ width: newWidth as number });
|
||||
updateConnector(connector.id, { width: newWidth as number });
|
||||
}}
|
||||
/>
|
||||
</Section>
|
||||
@@ -79,8 +66,8 @@ export const ConnectorControls = ({ id }: Props) => {
|
||||
<Select
|
||||
value={connector.style}
|
||||
onChange={(e) => {
|
||||
return onConnectorUpdated({
|
||||
style: e.target.value as ConnectorStyleEnum
|
||||
updateConnector(connector.id, {
|
||||
style: e.target.value as Connector['style']
|
||||
});
|
||||
}}
|
||||
>
|
||||
@@ -91,7 +78,12 @@ export const ConnectorControls = ({ id }: Props) => {
|
||||
</Section>
|
||||
<Section>
|
||||
<Box>
|
||||
<DeleteButton onClick={onConnectorDeleted} />
|
||||
<DeleteButton
|
||||
onClick={() => {
|
||||
uiStateActions.setItemControls(null);
|
||||
deleteConnector(connector.id);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Section>
|
||||
</ControlsContainer>
|
||||
|
||||
@@ -21,12 +21,12 @@ export const IconSelectionControls = () => {
|
||||
|
||||
const onMouseDown = useCallback(
|
||||
(icon: Icon) => {
|
||||
if (mode.type !== 'PLACE_ELEMENT') return;
|
||||
if (mode.type !== 'PLACE_ICON') return;
|
||||
|
||||
uiStateActions.setMode({
|
||||
type: 'PLACE_ELEMENT',
|
||||
type: 'PLACE_ICON',
|
||||
showCursor: true,
|
||||
icon
|
||||
id: icon.id
|
||||
});
|
||||
},
|
||||
[mode, uiStateActions]
|
||||
|
||||
@@ -14,7 +14,7 @@ export const ItemControlsManager = () => {
|
||||
|
||||
const Controls = useMemo(() => {
|
||||
switch (itemControls?.type) {
|
||||
case 'NODE':
|
||||
case 'ITEM':
|
||||
return <NodeControls key={itemControls.id} id={itemControls.id} />;
|
||||
case 'CONNECTOR':
|
||||
return <ConnectorControls key={itemControls.id} id={itemControls.id} />;
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import React, { useState, useCallback, useMemo } from 'react';
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { Box, Stack, Button } from '@mui/material';
|
||||
import {
|
||||
ChevronRight as ChevronRightIcon,
|
||||
ChevronLeft as ChevronLeftIcon
|
||||
} from '@mui/icons-material';
|
||||
import { Node } from 'src/types';
|
||||
import { useSceneStore } from 'src/stores/sceneStore';
|
||||
import { useNode } from 'src/hooks/useNode';
|
||||
import { useUiStateStore } from 'src/stores/uiStateStore';
|
||||
import { useIconCategories } from 'src/hooks/useIconCategories';
|
||||
import { getItemById } from 'src/utils';
|
||||
import { useIcon } from 'src/hooks/useIcon';
|
||||
import { useScene } from 'src/hooks/useScene';
|
||||
import { useViewItem } from 'src/hooks/useViewItem';
|
||||
import { useUiStateStore } from 'src/stores/uiStateStore';
|
||||
import { useModelItem } from 'src/hooks/useModelItem';
|
||||
import { ControlsContainer } from '../components/ControlsContainer';
|
||||
import { Icons } from '../IconSelectionControls/Icons';
|
||||
import { NodeSettings } from './NodeSettings/NodeSettings';
|
||||
@@ -19,44 +19,25 @@ interface Props {
|
||||
id: string;
|
||||
}
|
||||
|
||||
const ModeEnum = {
|
||||
Settings: 'Settings',
|
||||
ChangeIcon: 'ChangeIcon'
|
||||
const ModeOptions = {
|
||||
SETTINGS: 'SETTINGS',
|
||||
CHANGE_ICON: 'CHANGE_ICON'
|
||||
} as const;
|
||||
|
||||
type Mode = keyof typeof ModeOptions;
|
||||
|
||||
export const NodeControls = ({ id }: Props) => {
|
||||
const [mode, setMode] = useState<keyof typeof ModeEnum>('Settings');
|
||||
const { iconCategories } = useIconCategories();
|
||||
const icons = useSceneStore((state) => {
|
||||
return state.icons;
|
||||
});
|
||||
const sceneActions = useSceneStore((state) => {
|
||||
return state.actions;
|
||||
});
|
||||
const [mode, setMode] = useState<Mode>('SETTINGS');
|
||||
const { updateModelItem, updateViewItem, deleteViewItem } = useScene();
|
||||
const uiStateActions = useUiStateStore((state) => {
|
||||
return state.actions;
|
||||
});
|
||||
const node = useNode(id);
|
||||
const viewItem = useViewItem(id);
|
||||
const modelItem = useModelItem(id);
|
||||
const { iconCategories } = useIconCategories();
|
||||
const { icon } = useIcon(modelItem.icon);
|
||||
|
||||
const iconUrl = useMemo(() => {
|
||||
const { item: icon } = getItemById(icons, node.icon);
|
||||
|
||||
return icon.url;
|
||||
}, [node.icon, icons]);
|
||||
|
||||
const onNodeUpdated = useCallback(
|
||||
(updates: Partial<Node>) => {
|
||||
sceneActions.updateNode(id, updates);
|
||||
},
|
||||
[sceneActions, id]
|
||||
);
|
||||
|
||||
const onNodeDeleted = useCallback(() => {
|
||||
uiStateActions.setItemControls(null);
|
||||
sceneActions.deleteNode(id);
|
||||
}, [sceneActions, id, uiStateActions]);
|
||||
|
||||
const onSwitchMode = useCallback((newMode: keyof typeof ModeEnum) => {
|
||||
const onSwitchMode = useCallback((newMode: Mode) => {
|
||||
setMode(newMode);
|
||||
}, []);
|
||||
|
||||
@@ -76,23 +57,27 @@ export const NodeControls = ({ id }: Props) => {
|
||||
alignItems="flex-end"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Box component="img" src={iconUrl} sx={{ width: 70, height: 70 }} />
|
||||
{mode === 'Settings' && (
|
||||
<Box
|
||||
component="img"
|
||||
src={icon.url}
|
||||
sx={{ width: 70, height: 70 }}
|
||||
/>
|
||||
{mode === 'SETTINGS' && (
|
||||
<Button
|
||||
endIcon={<ChevronRightIcon />}
|
||||
onClick={() => {
|
||||
onSwitchMode('ChangeIcon');
|
||||
onSwitchMode('CHANGE_ICON');
|
||||
}}
|
||||
variant="text"
|
||||
>
|
||||
Update icon
|
||||
</Button>
|
||||
)}
|
||||
{mode === 'ChangeIcon' && (
|
||||
{mode === 'CHANGE_ICON' && (
|
||||
<Button
|
||||
startIcon={<ChevronLeftIcon />}
|
||||
onClick={() => {
|
||||
onSwitchMode('Settings');
|
||||
onSwitchMode('SETTINGS');
|
||||
}}
|
||||
variant="text"
|
||||
>
|
||||
@@ -102,20 +87,28 @@ export const NodeControls = ({ id }: Props) => {
|
||||
</Stack>
|
||||
</Section>
|
||||
</Box>
|
||||
{mode === ModeEnum.Settings && (
|
||||
{mode === 'SETTINGS' && (
|
||||
<NodeSettings
|
||||
key={node.id}
|
||||
node={node}
|
||||
onUpdate={onNodeUpdated}
|
||||
onDelete={onNodeDeleted}
|
||||
key={viewItem.id}
|
||||
node={viewItem}
|
||||
onModelItemUpdated={(updates) => {
|
||||
updateModelItem(viewItem.id, updates);
|
||||
}}
|
||||
onViewItemUpdated={(updates) => {
|
||||
updateViewItem(viewItem.id, updates);
|
||||
}}
|
||||
onDeleted={() => {
|
||||
uiStateActions.setItemControls(null);
|
||||
deleteViewItem(viewItem.id);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{mode === ModeEnum.ChangeIcon && (
|
||||
{mode === 'CHANGE_ICON' && (
|
||||
<Icons
|
||||
key={node.id}
|
||||
key={viewItem.id}
|
||||
iconCategories={iconCategories}
|
||||
onClick={(icon) => {
|
||||
onNodeUpdated({ icon: icon.id });
|
||||
onClick={(_icon) => {
|
||||
updateModelItem(viewItem.id, { icon: _icon.id });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1,37 +1,52 @@
|
||||
import React from 'react';
|
||||
import { Slider, Box, TextField } from '@mui/material';
|
||||
import { Node } from 'src/types';
|
||||
import { ModelItem, ViewItem } from 'src/types';
|
||||
import { MarkdownEditor } from 'src/components/MarkdownEditor/MarkdownEditor';
|
||||
import { useModelItem } from 'src/hooks/useModelItem';
|
||||
import { DeleteButton } from '../../components/DeleteButton';
|
||||
import { Section } from '../../components/Section';
|
||||
|
||||
export type NodeUpdates = {
|
||||
model: Partial<ModelItem>;
|
||||
view: Partial<ViewItem>;
|
||||
};
|
||||
|
||||
interface Props {
|
||||
node: Node;
|
||||
onUpdate: (updates: Partial<Node>) => void;
|
||||
onDelete: () => void;
|
||||
node: ViewItem;
|
||||
onModelItemUpdated: (updates: Partial<ModelItem>) => void;
|
||||
onViewItemUpdated: (updates: Partial<ViewItem>) => void;
|
||||
onDeleted: () => void;
|
||||
}
|
||||
|
||||
export const NodeSettings = ({ node, onUpdate, onDelete }: Props) => {
|
||||
export const NodeSettings = ({
|
||||
node,
|
||||
onModelItemUpdated,
|
||||
onViewItemUpdated,
|
||||
onDeleted
|
||||
}: Props) => {
|
||||
const modelItem = useModelItem(node.id);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Section title="Label">
|
||||
<Section title="Name">
|
||||
<TextField
|
||||
value={node.label}
|
||||
value={modelItem.name}
|
||||
onChange={(e) => {
|
||||
const text = e.target.value as string;
|
||||
if (node.label !== text) onUpdate({ label: text });
|
||||
if (modelItem.name !== text) onModelItemUpdated({ name: text });
|
||||
}}
|
||||
/>
|
||||
</Section>
|
||||
<Section title="Description">
|
||||
<MarkdownEditor
|
||||
value={node.description}
|
||||
value={modelItem.description}
|
||||
onChange={(text) => {
|
||||
if (node.description !== text) onUpdate({ description: text });
|
||||
if (modelItem.description !== text)
|
||||
onModelItemUpdated({ description: text });
|
||||
}}
|
||||
/>
|
||||
</Section>
|
||||
{node.label && (
|
||||
{modelItem.name && (
|
||||
<Section title="Label height">
|
||||
<Slider
|
||||
marks
|
||||
@@ -40,14 +55,15 @@ export const NodeSettings = ({ node, onUpdate, onDelete }: Props) => {
|
||||
max={280}
|
||||
value={node.labelHeight}
|
||||
onChange={(e, newHeight) => {
|
||||
onUpdate({ labelHeight: newHeight as number });
|
||||
const labelHeight = newHeight as number;
|
||||
onViewItemUpdated({ labelHeight });
|
||||
}}
|
||||
/>
|
||||
</Section>
|
||||
)}
|
||||
<Section>
|
||||
<Box>
|
||||
<DeleteButton onClick={onDelete} />
|
||||
<DeleteButton onClick={onDeleted} />
|
||||
</Box>
|
||||
</Section>
|
||||
</>
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { Rectangle } from 'src/types';
|
||||
import React from 'react';
|
||||
import { useTheme, Box } from '@mui/material';
|
||||
import { useSceneStore } from 'src/stores/sceneStore';
|
||||
import { useRectangle } from 'src/hooks/useRectangle';
|
||||
import { ColorSelector } from 'src/components/ColorSelector/ColorSelector';
|
||||
import { useUiStateStore } from 'src/stores/uiStateStore';
|
||||
import { useScene } from 'src/hooks/useScene';
|
||||
import { ControlsContainer } from '../components/ControlsContainer';
|
||||
import { Section } from '../components/Section';
|
||||
import { Header } from '../components/Header';
|
||||
import { DeleteButton } from '../components/DeleteButton';
|
||||
|
||||
interface Props {
|
||||
@@ -16,25 +14,11 @@ interface Props {
|
||||
|
||||
export const RectangleControls = ({ id }: Props) => {
|
||||
const theme = useTheme();
|
||||
const sceneActions = useSceneStore((state) => {
|
||||
return state.actions;
|
||||
});
|
||||
const uiStateActions = useUiStateStore((state) => {
|
||||
return state.actions;
|
||||
});
|
||||
const rectangle = useRectangle(id);
|
||||
|
||||
const onRectangleUpdated = useCallback(
|
||||
(updates: Partial<Rectangle>) => {
|
||||
sceneActions.updateRectangle(id, updates);
|
||||
},
|
||||
[sceneActions, id]
|
||||
);
|
||||
|
||||
const onRectangleDeleted = useCallback(() => {
|
||||
uiStateActions.setItemControls(null);
|
||||
sceneActions.deleteRectangle(id);
|
||||
}, [sceneActions, id, uiStateActions]);
|
||||
const { updateRectangle, deleteRectangle } = useScene();
|
||||
|
||||
return (
|
||||
<ControlsContainer>
|
||||
@@ -42,14 +26,19 @@ export const RectangleControls = ({ id }: Props) => {
|
||||
<ColorSelector
|
||||
colors={Object.values(theme.customVars.customPalette)}
|
||||
onChange={(color) => {
|
||||
return onRectangleUpdated({ color });
|
||||
updateRectangle(rectangle.id, { color });
|
||||
}}
|
||||
activeColor={rectangle.color}
|
||||
/>
|
||||
</Section>
|
||||
<Section>
|
||||
<Box>
|
||||
<DeleteButton onClick={onRectangleDeleted} />
|
||||
<DeleteButton
|
||||
onClick={() => {
|
||||
uiStateActions.setItemControls(null);
|
||||
deleteRectangle(rectangle.id);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Section>
|
||||
</ControlsContainer>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { TextBox, ProjectionOrientationEnum } from 'src/types';
|
||||
import React from 'react';
|
||||
import { ProjectionOrientationEnum } from 'src/types';
|
||||
import {
|
||||
Box,
|
||||
TextField,
|
||||
@@ -8,13 +8,12 @@ import {
|
||||
Slider
|
||||
} from '@mui/material';
|
||||
import { TextRotationNone as TextRotationNoneIcon } from '@mui/icons-material';
|
||||
import { useSceneStore } from 'src/stores/sceneStore';
|
||||
import { useTextBox } from 'src/hooks/useTextBox';
|
||||
import { useUiStateStore } from 'src/stores/uiStateStore';
|
||||
import { getIsoProjectionCss } from 'src/utils';
|
||||
import { useScene } from 'src/hooks/useScene';
|
||||
import { ControlsContainer } from '../components/ControlsContainer';
|
||||
import { Section } from '../components/Section';
|
||||
import { Header } from '../components/Header';
|
||||
import { DeleteButton } from '../components/DeleteButton';
|
||||
|
||||
interface Props {
|
||||
@@ -22,33 +21,19 @@ interface Props {
|
||||
}
|
||||
|
||||
export const TextBoxControls = ({ id }: Props) => {
|
||||
const sceneActions = useSceneStore((state) => {
|
||||
return state.actions;
|
||||
});
|
||||
const uiStateActions = useUiStateStore((state) => {
|
||||
return state.actions;
|
||||
});
|
||||
const textBox = useTextBox(id);
|
||||
|
||||
const onTextBoxUpdated = useCallback(
|
||||
(updates: Partial<TextBox>) => {
|
||||
sceneActions.updateTextBox(id, updates);
|
||||
},
|
||||
[sceneActions, id]
|
||||
);
|
||||
|
||||
const onTextBoxDeleted = useCallback(() => {
|
||||
uiStateActions.setItemControls(null);
|
||||
sceneActions.deleteTextBox(id);
|
||||
}, [sceneActions, id, uiStateActions]);
|
||||
const { updateTextBox, deleteTextBox } = useScene();
|
||||
|
||||
return (
|
||||
<ControlsContainer>
|
||||
<Section>
|
||||
<TextField
|
||||
value={textBox.text}
|
||||
value={textBox.content}
|
||||
onChange={(e) => {
|
||||
onTextBoxUpdated({ text: e.target.value as string });
|
||||
updateTextBox(textBox.id, { content: e.target.value as string });
|
||||
}}
|
||||
/>
|
||||
</Section>
|
||||
@@ -60,7 +45,7 @@ export const TextBoxControls = ({ id }: Props) => {
|
||||
max={0.9}
|
||||
value={textBox.fontSize}
|
||||
onChange={(e, newSize) => {
|
||||
onTextBoxUpdated({ fontSize: newSize as number });
|
||||
updateTextBox(textBox.id, { fontSize: newSize as number });
|
||||
}}
|
||||
/>
|
||||
</Section>
|
||||
@@ -72,7 +57,7 @@ export const TextBoxControls = ({ id }: Props) => {
|
||||
if (textBox.orientation === orientation || orientation === null)
|
||||
return;
|
||||
|
||||
onTextBoxUpdated({ orientation });
|
||||
updateTextBox(textBox.id, { orientation });
|
||||
}}
|
||||
>
|
||||
<ToggleButton value={ProjectionOrientationEnum.X}>
|
||||
@@ -89,7 +74,12 @@ export const TextBoxControls = ({ id }: Props) => {
|
||||
</Section>
|
||||
<Section>
|
||||
<Box>
|
||||
<DeleteButton onClick={onTextBoxDeleted} />
|
||||
<DeleteButton
|
||||
onClick={() => {
|
||||
uiStateActions.setItemControls(null);
|
||||
deleteTextBox(textBox.id);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Section>
|
||||
</ControlsContainer>
|
||||
|
||||
@@ -9,12 +9,13 @@ import {
|
||||
FolderOpen as FolderOpenIcon,
|
||||
DeleteOutline as DeleteOutlineIcon
|
||||
} from '@mui/icons-material';
|
||||
import { Model } from 'src/types/model';
|
||||
import { UiElement } from 'src/components/UiElement/UiElement';
|
||||
import { IconButton } from 'src/components/IconButton/IconButton';
|
||||
import { useUiStateStore } from 'src/stores/uiStateStore';
|
||||
import { useSceneStore } from 'src/stores/sceneStore';
|
||||
import { useModelStore } from 'src/stores/modelStore';
|
||||
import { exportAsJSON } from 'src/utils';
|
||||
import { INITIAL_SCENE } from 'src/config';
|
||||
import { INITIAL_DATA } from 'src/config';
|
||||
import { MenuItem } from './MenuItem';
|
||||
|
||||
export const MainMenu = () => {
|
||||
@@ -25,17 +26,10 @@ export const MainMenu = () => {
|
||||
const mainMenuOptions = useUiStateStore((state) => {
|
||||
return state.mainMenuOptions;
|
||||
});
|
||||
const scene = useSceneStore((state) => {
|
||||
return {
|
||||
title: state.title,
|
||||
icons: state.icons,
|
||||
nodes: state.nodes,
|
||||
connectors: state.connectors,
|
||||
textBoxes: state.textBoxes,
|
||||
rectangles: state.rectangles
|
||||
};
|
||||
const model = useModelStore((state) => {
|
||||
return state;
|
||||
});
|
||||
const sceneActions = useSceneStore((state) => {
|
||||
const modelActions = useModelStore((state) => {
|
||||
return state.actions;
|
||||
});
|
||||
const uiStateActions = useUiStateStore((state) => {
|
||||
@@ -54,7 +48,7 @@ export const MainMenu = () => {
|
||||
window.open(url, '_blank');
|
||||
}, []);
|
||||
|
||||
const onOpenScene = useCallback(async () => {
|
||||
const onOpenModel = useCallback(async () => {
|
||||
const fileInput = document.createElement('input');
|
||||
fileInput.type = 'file';
|
||||
fileInput.accept = 'application/json';
|
||||
@@ -69,8 +63,8 @@ export const MainMenu = () => {
|
||||
const fileReader = new FileReader();
|
||||
|
||||
fileReader.onload = async (e) => {
|
||||
const sceneInput = JSON.parse(e.target?.result as string);
|
||||
sceneActions.setScene(sceneInput);
|
||||
const loadedModel = JSON.parse(e.target?.result as string);
|
||||
modelActions.set(loadedModel);
|
||||
};
|
||||
fileReader.readAsText(file);
|
||||
|
||||
@@ -79,12 +73,21 @@ export const MainMenu = () => {
|
||||
|
||||
await fileInput.click();
|
||||
uiStateActions.setIsMainMenuOpen(false);
|
||||
}, [uiStateActions, sceneActions]);
|
||||
}, [uiStateActions, modelActions]);
|
||||
|
||||
const onExportAsJSON = useCallback(async () => {
|
||||
exportAsJSON(scene);
|
||||
const payload: Model = {
|
||||
icons: model.icons,
|
||||
items: model.items,
|
||||
title: model.title,
|
||||
version: model.version,
|
||||
views: model.views,
|
||||
description: model.description
|
||||
};
|
||||
|
||||
exportAsJSON(payload);
|
||||
uiStateActions.setIsMainMenuOpen(false);
|
||||
}, [scene, uiStateActions]);
|
||||
}, [model, uiStateActions]);
|
||||
|
||||
const onExportAsImage = useCallback(() => {
|
||||
uiStateActions.setIsMainMenuOpen(false);
|
||||
@@ -92,10 +95,10 @@ export const MainMenu = () => {
|
||||
}, [uiStateActions]);
|
||||
|
||||
const onClearCanvas = useCallback(() => {
|
||||
sceneActions.setScene({ ...INITIAL_SCENE, icons: scene.icons });
|
||||
modelActions.set({ ...INITIAL_DATA, icons: model.icons });
|
||||
uiStateActions.resetUiState();
|
||||
uiStateActions.setIsMainMenuOpen(false);
|
||||
}, [sceneActions, uiStateActions, scene.icons]);
|
||||
}, [modelActions, uiStateActions, model.icons]);
|
||||
|
||||
const sectionVisibility = useMemo(() => {
|
||||
return {
|
||||
@@ -140,7 +143,7 @@ export const MainMenu = () => {
|
||||
>
|
||||
<Card sx={{ py: 1 }}>
|
||||
{mainMenuOptions.includes('ACTION.OPEN') && (
|
||||
<MenuItem onClick={onOpenScene} Icon={<FolderOpenIcon />}>
|
||||
<MenuItem onClick={onOpenModel} Icon={<FolderOpenIcon />}>
|
||||
Open
|
||||
</MenuItem>
|
||||
)}
|
||||
|
||||
@@ -12,6 +12,7 @@ import { TextBoxes } from 'src/components/SceneLayers/TextBoxes/TextBoxes';
|
||||
import { SizeIndicator } from 'src/components/DebugUtils/SizeIndicator';
|
||||
import { SceneLayer } from 'src/components/SceneLayer/SceneLayer';
|
||||
import { TransformControlsManager } from 'src/components/TransformControlsManager/TransformControlsManager';
|
||||
import { useScene } from 'src/hooks/useScene';
|
||||
|
||||
export const Renderer = () => {
|
||||
const containerRef = useRef<HTMLDivElement>();
|
||||
@@ -25,7 +26,8 @@ export const Renderer = () => {
|
||||
const uiStateActions = useUiStateStore((state) => {
|
||||
return state.actions;
|
||||
});
|
||||
const { setElement: setInteractionsElement } = useInteractionManager();
|
||||
const { setInteractionsElement } = useInteractionManager();
|
||||
const { items, rectangles, connectors, textBoxes } = useScene();
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerRef.current || !interactionsRef.current) return;
|
||||
@@ -50,7 +52,7 @@ export const Renderer = () => {
|
||||
}}
|
||||
>
|
||||
<SceneLayer>
|
||||
<Rectangles />
|
||||
<Rectangles rectangles={rectangles} />
|
||||
</SceneLayer>
|
||||
<Box
|
||||
sx={{
|
||||
@@ -69,22 +71,19 @@ export const Renderer = () => {
|
||||
</SceneLayer>
|
||||
)}
|
||||
<SceneLayer>
|
||||
<Connectors />
|
||||
<Connectors connectors={connectors} />
|
||||
</SceneLayer>
|
||||
<SceneLayer>
|
||||
<TextBoxes />
|
||||
<TextBoxes textBoxes={textBoxes} />
|
||||
</SceneLayer>
|
||||
<SceneLayer>
|
||||
<ConnectorLabels />
|
||||
<ConnectorLabels connectors={connectors} />
|
||||
</SceneLayer>
|
||||
{enableDebugTools && (
|
||||
<SceneLayer>
|
||||
<SizeIndicator />
|
||||
</SceneLayer>
|
||||
)}
|
||||
<SceneLayer>
|
||||
<TransformControlsManager />
|
||||
</SceneLayer>
|
||||
{/* Interaction layer: this is where events are detected */}
|
||||
<Box
|
||||
ref={interactionsRef}
|
||||
@@ -97,7 +96,10 @@ export const Renderer = () => {
|
||||
}}
|
||||
/>
|
||||
<SceneLayer>
|
||||
<Nodes />
|
||||
<Nodes nodes={items} />
|
||||
</SceneLayer>
|
||||
<SceneLayer>
|
||||
<TransformControlsManager />
|
||||
</SceneLayer>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Box, Typography } from '@mui/material';
|
||||
import { Connector } from 'src/types';
|
||||
import { useScene } from 'src/hooks/useScene';
|
||||
import { connectorPathTileToGlobal, getTilePosition } from 'src/utils';
|
||||
import { PROJECTED_TILE_SIZE } from 'src/config';
|
||||
import { useUiStateStore } from 'src/stores/uiStateStore';
|
||||
|
||||
interface Props {
|
||||
connector: Connector;
|
||||
connector: ReturnType<typeof useScene>['connectors'][0];
|
||||
}
|
||||
|
||||
export const ConnectorLabel = ({ connector }: Props) => {
|
||||
@@ -43,7 +43,7 @@ export const ConnectorLabel = ({ connector }: Props) => {
|
||||
}}
|
||||
>
|
||||
<Typography color="text.secondary" variant="body2">
|
||||
{connector.label}
|
||||
{connector.description}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import React from 'react';
|
||||
import { useSceneStore } from 'src/stores/sceneStore';
|
||||
import { useScene } from 'src/hooks/useScene';
|
||||
import { ConnectorLabel } from './ConnectorLabel';
|
||||
|
||||
export const ConnectorLabels = () => {
|
||||
const connectors = useSceneStore((state) => {
|
||||
return state.connectors;
|
||||
});
|
||||
interface Props {
|
||||
connectors: ReturnType<typeof useScene>['connectors'];
|
||||
}
|
||||
|
||||
export const ConnectorLabels = ({ connectors }: Props) => {
|
||||
return (
|
||||
<>
|
||||
{connectors
|
||||
.filter((con) => {
|
||||
return con.label !== undefined;
|
||||
return con.description !== undefined;
|
||||
})
|
||||
.map((connector) => {
|
||||
return <ConnectorLabel key={connector.id} connector={connector} />;
|
||||
|
||||
@@ -1,33 +1,24 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { useTheme, Box } from '@mui/material';
|
||||
import { Connector as ConnectorI } from 'src/types';
|
||||
import { UNPROJECTED_TILE_SIZE } from 'src/config';
|
||||
import {
|
||||
getAnchorTile,
|
||||
CoordsUtils,
|
||||
getColorVariant,
|
||||
getAllAnchors
|
||||
} from 'src/utils';
|
||||
import { getAnchorTile, CoordsUtils, getColorVariant } from 'src/utils';
|
||||
import { Circle } from 'src/components/Circle/Circle';
|
||||
import { useSceneStore } from 'src/stores/sceneStore';
|
||||
import { Svg } from 'src/components/Svg/Svg';
|
||||
import { useIsoProjection } from 'src/hooks/useIsoProjection';
|
||||
import { useConnector } from 'src/hooks/useConnector';
|
||||
import { useScene } from 'src/hooks/useScene';
|
||||
|
||||
interface Props {
|
||||
connector: ConnectorI;
|
||||
connector: ReturnType<typeof useScene>['connectors'][0];
|
||||
}
|
||||
|
||||
export const Connector = ({ connector }: Props) => {
|
||||
export const Connector = ({ connector: _connector }: Props) => {
|
||||
const theme = useTheme();
|
||||
const { currentView } = useScene();
|
||||
const connector = useConnector(_connector.id);
|
||||
const { css, pxSize } = useIsoProjection({
|
||||
...connector.path.rectangle
|
||||
});
|
||||
const nodes = useSceneStore((state) => {
|
||||
return state.nodes;
|
||||
});
|
||||
const connectors = useSceneStore((state) => {
|
||||
return state.connectors;
|
||||
});
|
||||
|
||||
const drawOffset = useMemo(() => {
|
||||
return {
|
||||
@@ -46,7 +37,7 @@ export const Connector = ({ connector }: Props) => {
|
||||
|
||||
const anchorPositions = useMemo(() => {
|
||||
return connector.anchors.map((anchor) => {
|
||||
const position = getAnchorTile(anchor, nodes, getAllAnchors(connectors));
|
||||
const position = getAnchorTile(anchor, currentView);
|
||||
|
||||
return {
|
||||
id: anchor.id,
|
||||
@@ -57,7 +48,7 @@ export const Connector = ({ connector }: Props) => {
|
||||
(connector.path.rectangle.from.y - position.y) * UNPROJECTED_TILE_SIZE
|
||||
};
|
||||
});
|
||||
}, [connector.path.rectangle, connector.anchors, nodes, connectors]);
|
||||
}, [currentView, connector.path.rectangle, connector.anchors]);
|
||||
|
||||
const connectorWidthPx = useMemo(() => {
|
||||
return (UNPROJECTED_TILE_SIZE / 100) * connector.width;
|
||||
|
||||
@@ -1,24 +1,17 @@
|
||||
import React from 'react';
|
||||
import { useSceneStore } from 'src/stores/sceneStore';
|
||||
import { useUiStateStore } from 'src/stores/uiStateStore';
|
||||
import { useScene } from 'src/hooks/useScene';
|
||||
import { Connector } from './Connector';
|
||||
|
||||
export const Connectors = () => {
|
||||
const connectors = useSceneStore((state) => {
|
||||
return state.connectors;
|
||||
});
|
||||
const mode = useUiStateStore((state) => {
|
||||
return state.mode;
|
||||
});
|
||||
interface Props {
|
||||
connectors: ReturnType<typeof useScene>['connectors'];
|
||||
}
|
||||
|
||||
export const Connectors = ({ connectors }: Props) => {
|
||||
return (
|
||||
<>
|
||||
{connectors.map((connector) => {
|
||||
return <Connector key={connector.id} connector={connector} />;
|
||||
})}
|
||||
{mode.type === 'CONNECTOR' && mode.connector && (
|
||||
<Connector connector={mode.connector} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,34 +1,39 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Box, Typography, useTheme } from '@mui/material';
|
||||
import { Node as NodeI, TileOriginEnum } from 'src/types';
|
||||
import { PROJECTED_TILE_SIZE } from 'src/config';
|
||||
import { PROJECTED_TILE_SIZE, DEFAULT_LABEL_HEIGHT } from 'src/config';
|
||||
import { getTilePosition } from 'src/utils';
|
||||
import { useIcon } from 'src/hooks/useIcon';
|
||||
import { ViewItem } from 'src/types';
|
||||
import { MarkdownEditor } from 'src/components/MarkdownEditor/MarkdownEditor';
|
||||
import { useModelItem } from 'src/hooks/useModelItem';
|
||||
import { LabelContainer } from './LabelContainer/LabelContainer';
|
||||
|
||||
interface Props {
|
||||
node: NodeI;
|
||||
node: ViewItem;
|
||||
order: number;
|
||||
}
|
||||
|
||||
export const Node = ({ node, order }: Props) => {
|
||||
const theme = useTheme();
|
||||
const { iconComponent } = useIcon(node.icon);
|
||||
const modelItem = useModelItem(node.id);
|
||||
const { iconComponent } = useIcon(modelItem.icon);
|
||||
|
||||
const position = useMemo(() => {
|
||||
return getTilePosition({
|
||||
tile: node.tile,
|
||||
origin: TileOriginEnum.BOTTOM
|
||||
origin: 'BOTTOM'
|
||||
});
|
||||
}, [node.tile]);
|
||||
|
||||
const description = useMemo(() => {
|
||||
if (node.description === undefined || node.description === '<p><br></p>')
|
||||
if (
|
||||
modelItem.description === undefined ||
|
||||
modelItem.description === '<p><br></p>'
|
||||
)
|
||||
return null;
|
||||
|
||||
return node.description;
|
||||
}, [node.description]);
|
||||
return modelItem.description;
|
||||
}, [modelItem.description]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
@@ -44,7 +49,7 @@ export const Node = ({ node, order }: Props) => {
|
||||
top: position.y
|
||||
}}
|
||||
>
|
||||
{(node.label || description) && (
|
||||
{(modelItem.name || description) && (
|
||||
<>
|
||||
<Box
|
||||
style={{
|
||||
@@ -52,15 +57,18 @@ export const Node = ({ node, order }: Props) => {
|
||||
top: -PROJECTED_TILE_SIZE.height
|
||||
}}
|
||||
/>
|
||||
<LabelContainer labelHeight={node.labelHeight} connectorDotSize={3}>
|
||||
{node.label && (
|
||||
<Typography fontWeight={600}>{node.label}</Typography>
|
||||
<LabelContainer
|
||||
labelHeight={node.labelHeight ?? DEFAULT_LABEL_HEIGHT}
|
||||
connectorDotSize={3}
|
||||
>
|
||||
{modelItem.name && (
|
||||
<Typography fontWeight={600}>{modelItem.name}</Typography>
|
||||
)}
|
||||
{description && (
|
||||
<Box sx={{ pt: 0.2, width: 200 }}>
|
||||
<MarkdownEditor
|
||||
readOnly
|
||||
value={node.description}
|
||||
value={modelItem.description}
|
||||
styles={{
|
||||
color: theme.palette.text.secondary
|
||||
}}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React from 'react';
|
||||
import { useSceneStore } from 'src/stores/sceneStore';
|
||||
import { ViewItem } from 'src/types';
|
||||
import { Node } from './Node/Node';
|
||||
|
||||
export const Nodes = () => {
|
||||
const nodes = useSceneStore((state) => {
|
||||
return state.nodes;
|
||||
});
|
||||
interface Props {
|
||||
nodes: ViewItem[];
|
||||
}
|
||||
|
||||
export const Nodes = ({ nodes }: Props) => {
|
||||
return (
|
||||
<>
|
||||
{nodes.map((node) => {
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import React from 'react';
|
||||
import { Coords } from 'src/types';
|
||||
import { Rectangle as RectangleI } from 'src/types';
|
||||
import { IsoTileArea } from 'src/components/IsoTileArea/IsoTileArea';
|
||||
import { getColorVariant } from 'src/utils';
|
||||
import { DEFAULT_COLOR } from 'src/config';
|
||||
|
||||
interface Props {
|
||||
from: Coords;
|
||||
to: Coords;
|
||||
color: string;
|
||||
}
|
||||
type Props = RectangleI;
|
||||
|
||||
export const Rectangle = ({ from, to, color }: Props) => {
|
||||
export const Rectangle = ({ from, to, color = DEFAULT_COLOR }: Props) => {
|
||||
return (
|
||||
<IsoTileArea
|
||||
from={from}
|
||||
|
||||
@@ -1,29 +1,17 @@
|
||||
import React from 'react';
|
||||
import { useSceneStore } from 'src/stores/sceneStore';
|
||||
import { useUiStateStore } from 'src/stores/uiStateStore';
|
||||
import { DEFAULT_COLOR } from 'src/config';
|
||||
import { Rectangle as RectangleI } from 'src/types';
|
||||
import { Rectangle } from './Rectangle';
|
||||
|
||||
export const Rectangles = () => {
|
||||
const rectangles = useSceneStore((state) => {
|
||||
return state.rectangles;
|
||||
});
|
||||
const mode = useUiStateStore((state) => {
|
||||
return state.mode;
|
||||
});
|
||||
interface Props {
|
||||
rectangles: RectangleI[];
|
||||
}
|
||||
|
||||
export const Rectangles = ({ rectangles }: Props) => {
|
||||
return (
|
||||
<>
|
||||
{rectangles.map((rectangle) => {
|
||||
return <Rectangle key={rectangle.id} {...rectangle} />;
|
||||
})}
|
||||
{mode.type === 'RECTANGLE.DRAW' && mode.area && (
|
||||
<Rectangle
|
||||
from={mode.area.from}
|
||||
to={mode.area.to}
|
||||
color={DEFAULT_COLOR}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Box, Typography } from '@mui/material';
|
||||
import { toPx, CoordsUtils } from 'src/utils';
|
||||
import { TextBox as TextBoxI } from 'src/types';
|
||||
import { useIsoProjection } from 'src/hooks/useIsoProjection';
|
||||
import { useTextBoxProps } from 'src/hooks/useTextBoxProps';
|
||||
import { useScene } from 'src/hooks/useScene';
|
||||
|
||||
interface Props {
|
||||
textBox: TextBoxI;
|
||||
textBox: ReturnType<typeof useScene>['textBoxes'][0];
|
||||
}
|
||||
|
||||
export const TextBox = ({ textBox }: Props) => {
|
||||
@@ -44,7 +44,7 @@ export const TextBox = ({ textBox }: Props) => {
|
||||
...fontProps
|
||||
}}
|
||||
>
|
||||
{textBox.text}
|
||||
{textBox.content}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -1,35 +1,17 @@
|
||||
import React from 'react';
|
||||
import { textBoxInputToTextBox } from 'src/utils';
|
||||
import { useUiStateStore } from 'src/stores/uiStateStore';
|
||||
import { useSceneStore } from 'src/stores/sceneStore';
|
||||
import { TEXTBOX_DEFAULTS } from 'src/config';
|
||||
import { useScene } from 'src/hooks/useScene';
|
||||
import { TextBox } from './TextBox';
|
||||
|
||||
export const TextBoxes = () => {
|
||||
const mode = useUiStateStore((state) => {
|
||||
return state.mode;
|
||||
});
|
||||
const mouse = useUiStateStore((state) => {
|
||||
return state.mouse;
|
||||
});
|
||||
const textBoxes = useSceneStore((state) => {
|
||||
return state.textBoxes;
|
||||
});
|
||||
interface Props {
|
||||
textBoxes: ReturnType<typeof useScene>['textBoxes'];
|
||||
}
|
||||
|
||||
export const TextBoxes = ({ textBoxes }: Props) => {
|
||||
return (
|
||||
<>
|
||||
{textBoxes.map((textBox) => {
|
||||
return <TextBox key={textBox.id} textBox={textBox} />;
|
||||
})}
|
||||
{mode.type === 'TEXTBOX' && (
|
||||
<TextBox
|
||||
textBox={textBoxInputToTextBox({
|
||||
id: 'temp-textbox',
|
||||
...TEXTBOX_DEFAULTS,
|
||||
tile: mouse.position.tile
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { Stack } from '@mui/material';
|
||||
import {
|
||||
PanToolOutlined as PanToolIcon,
|
||||
@@ -11,14 +11,37 @@ import {
|
||||
import { useUiStateStore } from 'src/stores/uiStateStore';
|
||||
import { IconButton } from 'src/components/IconButton/IconButton';
|
||||
import { UiElement } from 'src/components/UiElement/UiElement';
|
||||
import { useScene } from 'src/hooks/useScene';
|
||||
import { TEXTBOX_DEFAULTS } from 'src/config';
|
||||
import { generateId } from 'src/utils';
|
||||
|
||||
export const ToolMenu = () => {
|
||||
const { createTextBox } = useScene();
|
||||
const mode = useUiStateStore((state) => {
|
||||
return state.mode;
|
||||
});
|
||||
const uiStateStoreActions = useUiStateStore((state) => {
|
||||
return state.actions;
|
||||
});
|
||||
const mousePosition = useUiStateStore((state) => {
|
||||
return state.mouse.position.tile;
|
||||
});
|
||||
|
||||
const createTextBoxProxy = useCallback(() => {
|
||||
const textBoxId = generateId();
|
||||
|
||||
createTextBox({
|
||||
...TEXTBOX_DEFAULTS,
|
||||
id: textBoxId,
|
||||
tile: mousePosition
|
||||
});
|
||||
|
||||
uiStateStoreActions.setMode({
|
||||
type: 'TEXTBOX',
|
||||
showCursor: false,
|
||||
id: textBoxId
|
||||
});
|
||||
}, [uiStateStoreActions, createTextBox, mousePosition]);
|
||||
|
||||
return (
|
||||
<UiElement>
|
||||
@@ -54,12 +77,12 @@ export const ToolMenu = () => {
|
||||
type: 'ADD_ITEM'
|
||||
});
|
||||
uiStateStoreActions.setMode({
|
||||
type: 'PLACE_ELEMENT',
|
||||
type: 'PLACE_ICON',
|
||||
showCursor: true,
|
||||
icon: null
|
||||
id: null
|
||||
});
|
||||
}}
|
||||
isActive={mode.type === 'PLACE_ELEMENT'}
|
||||
isActive={mode.type === 'PLACE_ICON'}
|
||||
/>
|
||||
<IconButton
|
||||
name="Rectangle"
|
||||
@@ -68,7 +91,7 @@ export const ToolMenu = () => {
|
||||
uiStateStoreActions.setMode({
|
||||
type: 'RECTANGLE.DRAW',
|
||||
showCursor: true,
|
||||
area: null
|
||||
id: null
|
||||
});
|
||||
}}
|
||||
isActive={mode.type === 'RECTANGLE.DRAW'}
|
||||
@@ -79,7 +102,7 @@ export const ToolMenu = () => {
|
||||
onClick={() => {
|
||||
uiStateStoreActions.setMode({
|
||||
type: 'CONNECTOR',
|
||||
connector: null,
|
||||
id: null,
|
||||
showCursor: true
|
||||
});
|
||||
}}
|
||||
@@ -88,12 +111,7 @@ export const ToolMenu = () => {
|
||||
<IconButton
|
||||
name="Text"
|
||||
Icon={<TitleIcon />}
|
||||
onClick={() => {
|
||||
uiStateStoreActions.setMode({
|
||||
type: 'TEXTBOX',
|
||||
showCursor: false
|
||||
});
|
||||
}}
|
||||
onClick={createTextBoxProxy}
|
||||
isActive={mode.type === 'TEXTBOX'}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { useNode } from 'src/hooks/useNode';
|
||||
import { useViewItem } from 'src/hooks/useViewItem';
|
||||
import { TransformControls } from './TransformControls';
|
||||
|
||||
interface Props {
|
||||
@@ -7,7 +7,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export const NodeTransformControls = ({ id }: Props) => {
|
||||
const node = useNode(id);
|
||||
const node = useViewItem(id);
|
||||
|
||||
return <TransformControls from={node.tile} to={node.tile} />;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import React from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useRectangle } from 'src/hooks/useRectangle';
|
||||
import { AnchorPosition } from 'src/types';
|
||||
import { useUiStateStore } from 'src/stores/uiStateStore';
|
||||
import { TransformControls } from './TransformControls';
|
||||
|
||||
interface Props {
|
||||
@@ -8,12 +10,27 @@ interface Props {
|
||||
|
||||
export const RectangleTransformControls = ({ id }: Props) => {
|
||||
const rectangle = useRectangle(id);
|
||||
const uiStateActions = useUiStateStore((state) => {
|
||||
return state.actions;
|
||||
});
|
||||
|
||||
const onAnchorMouseDown = useCallback(
|
||||
(key: AnchorPosition) => {
|
||||
uiStateActions.setMode({
|
||||
type: 'RECTANGLE.TRANSFORM',
|
||||
id: rectangle.id,
|
||||
selectedAnchor: key,
|
||||
showCursor: false
|
||||
});
|
||||
},
|
||||
[rectangle.id, uiStateActions]
|
||||
);
|
||||
|
||||
return (
|
||||
<TransformControls
|
||||
showCornerAnchors
|
||||
from={rectangle.from}
|
||||
to={rectangle.to}
|
||||
onAnchorMouseDown={onAnchorMouseDown}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { getTextBoxTo } from 'src/utils';
|
||||
import { getTextBoxEndTile } from 'src/utils';
|
||||
import { useTextBox } from 'src/hooks/useTextBox';
|
||||
import { TransformControls } from './TransformControls';
|
||||
|
||||
@@ -11,7 +11,7 @@ export const TextBoxTransformControls = ({ id }: Props) => {
|
||||
const textBox = useTextBox(id);
|
||||
|
||||
const to = useMemo(() => {
|
||||
return getTextBoxTo(textBox);
|
||||
return getTextBoxEndTile(textBox, textBox.size);
|
||||
}, [textBox]);
|
||||
|
||||
return <TransformControls from={textBox.tile} to={to} />;
|
||||
|
||||
@@ -1,40 +1,89 @@
|
||||
import React from 'react';
|
||||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { produce } from 'immer';
|
||||
import { Coords } from 'src/types';
|
||||
import { useTheme } from '@mui/material';
|
||||
import { useTheme, Box } from '@mui/material';
|
||||
import { getIsoProjectionCss } from 'src/utils';
|
||||
import { Svg } from 'src/components/Svg/Svg';
|
||||
import { useUiStateStore } from 'src/stores/uiStateStore';
|
||||
import { TRANSFORM_ANCHOR_SIZE, TRANSFORM_CONTROLS_COLOR } from 'src/config';
|
||||
|
||||
interface Props {
|
||||
position: Coords;
|
||||
onMouseDown: () => void;
|
||||
}
|
||||
|
||||
const strokeWidth = 2;
|
||||
|
||||
export const TransformAnchor = ({ position }: Props) => {
|
||||
export const TransformAnchor = ({ position, onMouseDown }: Props) => {
|
||||
const prevIsHoveredStateRef = useRef(false);
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const mode = useUiStateStore((state) => {
|
||||
return state.mode;
|
||||
});
|
||||
const uiActions = useUiStateStore((state) => {
|
||||
return state.actions;
|
||||
});
|
||||
const theme = useTheme();
|
||||
|
||||
const toggleCursor = useCallback(
|
||||
(state: boolean) => {
|
||||
const newMode = produce(mode, (draft) => {
|
||||
draft.showCursor = state;
|
||||
});
|
||||
|
||||
uiActions.setMode(newMode);
|
||||
},
|
||||
[mode, uiActions]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (prevIsHoveredStateRef.current !== isHovered) {
|
||||
prevIsHoveredStateRef.current = isHovered;
|
||||
toggleCursor(!isHovered);
|
||||
}
|
||||
}, [isHovered, toggleCursor]);
|
||||
|
||||
return (
|
||||
<Svg
|
||||
style={{
|
||||
<Box
|
||||
onMouseOver={() => {
|
||||
setIsHovered(true);
|
||||
}}
|
||||
onMouseOut={() => {
|
||||
setIsHovered(false);
|
||||
}}
|
||||
onMouseDown={onMouseDown}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
left: position.x - TRANSFORM_ANCHOR_SIZE / 2,
|
||||
top: position.y - TRANSFORM_ANCHOR_SIZE / 2,
|
||||
transform: getIsoProjectionCss(),
|
||||
width: TRANSFORM_ANCHOR_SIZE,
|
||||
height: TRANSFORM_ANCHOR_SIZE
|
||||
}}
|
||||
style={{
|
||||
left: position.x - TRANSFORM_ANCHOR_SIZE / 2,
|
||||
top: position.y - TRANSFORM_ANCHOR_SIZE / 2
|
||||
}}
|
||||
>
|
||||
<g transform={`translate(${strokeWidth}, ${strokeWidth})`}>
|
||||
<rect
|
||||
fill={theme.palette.common.white}
|
||||
width={TRANSFORM_ANCHOR_SIZE - strokeWidth * 2}
|
||||
height={TRANSFORM_ANCHOR_SIZE - strokeWidth * 2}
|
||||
stroke={TRANSFORM_CONTROLS_COLOR}
|
||||
strokeWidth={strokeWidth}
|
||||
rx={3}
|
||||
/>
|
||||
</g>
|
||||
</Svg>
|
||||
<Svg
|
||||
style={{
|
||||
width: TRANSFORM_ANCHOR_SIZE,
|
||||
height: TRANSFORM_ANCHOR_SIZE
|
||||
}}
|
||||
>
|
||||
<g transform={`translate(${strokeWidth}, ${strokeWidth})`}>
|
||||
<rect
|
||||
fill={
|
||||
isHovered
|
||||
? theme.palette.primary.dark
|
||||
: theme.palette.common.white
|
||||
}
|
||||
width={TRANSFORM_ANCHOR_SIZE - strokeWidth * 2}
|
||||
height={TRANSFORM_ANCHOR_SIZE - strokeWidth * 2}
|
||||
stroke={TRANSFORM_CONTROLS_COLOR}
|
||||
strokeWidth={strokeWidth}
|
||||
rx={3}
|
||||
/>
|
||||
</g>
|
||||
</Svg>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,55 +1,60 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Coords } from 'src/types';
|
||||
import { Coords, AnchorPosition } from 'src/types';
|
||||
import { Svg } from 'src/components/Svg/Svg';
|
||||
import { TRANSFORM_CONTROLS_COLOR } from 'src/config';
|
||||
import { useIsoProjection } from 'src/hooks/useIsoProjection';
|
||||
import {
|
||||
getBoundingBox,
|
||||
outermostCornerPositions,
|
||||
getTilePosition
|
||||
getTilePosition,
|
||||
convertBoundsToNamedAnchors
|
||||
} from 'src/utils';
|
||||
import { TransformAnchor } from './TransformAnchor';
|
||||
|
||||
interface Props {
|
||||
from: Coords;
|
||||
to: Coords;
|
||||
onMouseOver?: () => void;
|
||||
showCornerAnchors?: boolean;
|
||||
onAnchorMouseDown?: (anchorPosition: AnchorPosition) => void;
|
||||
}
|
||||
|
||||
const strokeWidth = 2;
|
||||
|
||||
export const TransformControls = ({
|
||||
from,
|
||||
to,
|
||||
onMouseOver,
|
||||
showCornerAnchors
|
||||
}: Props) => {
|
||||
export const TransformControls = ({ from, to, onAnchorMouseDown }: Props) => {
|
||||
const { css, pxSize } = useIsoProjection({
|
||||
from,
|
||||
to
|
||||
});
|
||||
|
||||
const anchorPositions = useMemo<Coords[]>(() => {
|
||||
if (!showCornerAnchors) return [];
|
||||
const anchors = useMemo(() => {
|
||||
if (!onAnchorMouseDown) return [];
|
||||
|
||||
const corners = getBoundingBox([from, to]);
|
||||
const cornerPositions = corners.map((corner, i) => {
|
||||
return getTilePosition({
|
||||
tile: corner,
|
||||
origin: outermostCornerPositions[i]
|
||||
});
|
||||
});
|
||||
const namedCorners = convertBoundsToNamedAnchors(corners);
|
||||
const cornerPositions = Object.entries(namedCorners).map(
|
||||
([key, value], i) => {
|
||||
const position = getTilePosition({
|
||||
tile: value,
|
||||
origin: outermostCornerPositions[i]
|
||||
});
|
||||
|
||||
return {
|
||||
position,
|
||||
onMouseDown: () => {
|
||||
onAnchorMouseDown(key as AnchorPosition);
|
||||
}
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
return cornerPositions;
|
||||
}, [showCornerAnchors, from, to]);
|
||||
}, [onAnchorMouseDown, from, to]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Svg
|
||||
style={css}
|
||||
onMouseOver={() => {
|
||||
onMouseOver?.();
|
||||
style={{
|
||||
...css,
|
||||
pointerEvents: 'none'
|
||||
}}
|
||||
>
|
||||
<g transform={`translate(${strokeWidth}, ${strokeWidth})`}>
|
||||
@@ -60,12 +65,15 @@ export const TransformControls = ({
|
||||
stroke={TRANSFORM_CONTROLS_COLOR}
|
||||
strokeDasharray={`${strokeWidth * 2} ${strokeWidth * 2}`}
|
||||
strokeWidth={strokeWidth}
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</g>
|
||||
</Svg>
|
||||
|
||||
{anchorPositions.map((position) => {
|
||||
return <TransformAnchor position={position} />;
|
||||
{anchors.map(({ position, onMouseDown }) => {
|
||||
return (
|
||||
<TransformAnchor position={position} onMouseDown={onMouseDown} />
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -10,7 +10,7 @@ export const TransformControlsManager = () => {
|
||||
});
|
||||
|
||||
switch (itemControls?.type) {
|
||||
case 'NODE':
|
||||
case 'ITEM':
|
||||
return <NodeTransformControls id={itemControls.id} />;
|
||||
case 'RECTANGLE':
|
||||
return <RectangleTransformControls id={itemControls.id} />;
|
||||
|
||||
@@ -9,7 +9,7 @@ import { ToolMenu } from 'src/components/ToolMenu/ToolMenu';
|
||||
import { useUiStateStore } from 'src/stores/uiStateStore';
|
||||
import { MainMenu } from 'src/components/MainMenu/MainMenu';
|
||||
import { ZoomControls } from 'src/components/ZoomControls/ZoomControls';
|
||||
import { useSceneStore } from 'src/stores/sceneStore';
|
||||
import { useModelStore } from 'src/stores/modelStore';
|
||||
import { DebugUtils } from 'src/components/DebugUtils/DebugUtils';
|
||||
import { useResizeObserver } from 'src/hooks/useResizeObserver';
|
||||
import { ExportImageDialog } from '../ExportImageDialog/ExportImageDialog';
|
||||
@@ -19,7 +19,7 @@ const ToolsEnum = {
|
||||
ZOOM_CONTROLS: 'ZOOM_CONTROLS',
|
||||
TOOL_MENU: 'TOOL_MENU',
|
||||
ITEM_CONTROLS: 'ITEM_CONTROLS',
|
||||
SCENE_TITLE: 'SCENE_TITLE'
|
||||
Model_TITLE: 'Model_TITLE'
|
||||
} as const;
|
||||
|
||||
interface EditorModeMapping {
|
||||
@@ -32,9 +32,9 @@ const EDITOR_MODE_MAPPING: EditorModeMapping = {
|
||||
'ZOOM_CONTROLS',
|
||||
'TOOL_MENU',
|
||||
'MAIN_MENU',
|
||||
'SCENE_TITLE'
|
||||
'Model_TITLE'
|
||||
],
|
||||
[EditorModeEnum.EXPLORABLE_READONLY]: ['ZOOM_CONTROLS', 'SCENE_TITLE'],
|
||||
[EditorModeEnum.EXPLORABLE_READONLY]: ['ZOOM_CONTROLS', 'Model_TITLE'],
|
||||
[EditorModeEnum.NON_INTERACTIVE]: []
|
||||
};
|
||||
|
||||
@@ -71,7 +71,7 @@ export const UiOverlay = () => {
|
||||
const itemControls = useUiStateStore((state) => {
|
||||
return state.itemControls;
|
||||
});
|
||||
const sceneTitle = useSceneStore((state) => {
|
||||
const ModelTitle = useModelStore((state) => {
|
||||
return state.title;
|
||||
});
|
||||
const editorMode = useUiStateStore((state) => {
|
||||
@@ -155,7 +155,7 @@ export const UiOverlay = () => {
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{availableTools.includes('SCENE_TITLE') && (
|
||||
{availableTools.includes('Model_TITLE') && (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
@@ -178,7 +178,7 @@ export const UiOverlay = () => {
|
||||
}}
|
||||
>
|
||||
<Typography fontWeight={600} color="text.secondary">
|
||||
{sceneTitle}
|
||||
{ModelTitle}
|
||||
</Typography>
|
||||
</UiElement>
|
||||
</Box>
|
||||
@@ -199,9 +199,9 @@ export const UiOverlay = () => {
|
||||
</UiElement>
|
||||
)}
|
||||
</Box>
|
||||
{mode.type === 'PLACE_ELEMENT' && mode.icon && (
|
||||
{mode.type === 'PLACE_ICON' && mode.id && (
|
||||
<SceneLayer>
|
||||
<DragAndDrop icon={mode.icon} tile={mouse.position.tile} />
|
||||
<DragAndDrop iconId={mode.id} tile={mouse.position.tile} />
|
||||
</SceneLayer>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import {
|
||||
Size,
|
||||
Coords,
|
||||
SceneInput,
|
||||
Model,
|
||||
MainMenuOptions,
|
||||
Icon,
|
||||
Connector,
|
||||
MainMenuOptions
|
||||
TextBox,
|
||||
ViewItem,
|
||||
View,
|
||||
Rectangle
|
||||
} from 'src/types';
|
||||
import { customVars } from './styles/theme';
|
||||
import { CoordsUtils } from './utils';
|
||||
|
||||
// TODO: This file could do with better organisation and convention for easier reading.
|
||||
export const UNPROJECTED_TILE_SIZE = 100;
|
||||
@@ -17,33 +22,47 @@ export const PROJECTED_TILE_SIZE = {
|
||||
width: UNPROJECTED_TILE_SIZE * TILE_PROJECTION_MULTIPLIERS.width,
|
||||
height: UNPROJECTED_TILE_SIZE * TILE_PROJECTION_MULTIPLIERS.height
|
||||
};
|
||||
|
||||
export const DEFAULT_COLOR = customVars.customPalette.blue;
|
||||
export const DEFAULT_FONT_FAMILY = 'Roboto, Arial, sans-serif';
|
||||
export const NODE_DEFAULTS = {
|
||||
label: '',
|
||||
labelHeight: 140,
|
||||
color: DEFAULT_COLOR
|
||||
|
||||
export const VIEW_DEFAULTS: Required<Omit<View, 'id' | 'description'>> = {
|
||||
name: 'New view',
|
||||
items: [],
|
||||
connectors: [],
|
||||
rectangles: [],
|
||||
textBoxes: []
|
||||
};
|
||||
|
||||
interface ConnectorDefaults {
|
||||
width: number;
|
||||
searchOffset: Coords;
|
||||
style: Connector['style'];
|
||||
}
|
||||
export const VIEW_ITEM_DEFAULTS: Required<Omit<ViewItem, 'id' | 'tile'>> = {
|
||||
labelHeight: 80
|
||||
};
|
||||
|
||||
export const CONNECTOR_DEFAULTS: ConnectorDefaults = {
|
||||
export const CONNECTOR_DEFAULTS: Required<Omit<Connector, 'id'>> = {
|
||||
width: 10,
|
||||
// The boundaries of the search area for the pathfinder algorithm
|
||||
// is the grid that encompasses the two nodes + the offset below.
|
||||
searchOffset: { x: 1, y: 1 },
|
||||
description: '',
|
||||
color: DEFAULT_COLOR,
|
||||
anchors: [],
|
||||
style: 'SOLID'
|
||||
};
|
||||
|
||||
export const TEXTBOX_DEFAULTS = {
|
||||
// The boundaries of the search area for the pathfinder algorithm
|
||||
// is the grid that encompasses the two nodes + the offset below.
|
||||
export const CONNECTOR_SEARCH_OFFSET = { x: 1, y: 1 };
|
||||
|
||||
export const TEXTBOX_DEFAULTS: Required<Omit<TextBox, 'id' | 'tile'>> = {
|
||||
orientation: 'X',
|
||||
fontSize: 0.6,
|
||||
paddingX: 0.2,
|
||||
text: 'Text',
|
||||
fontWeight: 'bold'
|
||||
content: 'Text'
|
||||
};
|
||||
|
||||
export const TEXTBOX_PADDING = 0.2;
|
||||
export const TEXTBOX_FONT_WEIGHT = 'bold';
|
||||
|
||||
export const RECTANGLE_DEFAULTS: Required<
|
||||
Omit<Rectangle, 'id' | 'from' | 'to'>
|
||||
> = {
|
||||
color: DEFAULT_COLOR
|
||||
};
|
||||
|
||||
export const ZOOM_INCREMENT = 0.2;
|
||||
@@ -51,13 +70,19 @@ export const MIN_ZOOM = 0.2;
|
||||
export const MAX_ZOOM = 1;
|
||||
export const TRANSFORM_ANCHOR_SIZE = 30;
|
||||
export const TRANSFORM_CONTROLS_COLOR = '#0392ff';
|
||||
export const INITIAL_SCENE: SceneInput = {
|
||||
export const INITIAL_DATA: Model = {
|
||||
title: 'Untitled',
|
||||
version: '',
|
||||
icons: [],
|
||||
nodes: [],
|
||||
connectors: [],
|
||||
textBoxes: [],
|
||||
rectangles: []
|
||||
items: [],
|
||||
views: []
|
||||
};
|
||||
export const INITIAL_UI_STATE = {
|
||||
zoom: 1,
|
||||
scroll: {
|
||||
position: CoordsUtils.zero(),
|
||||
offset: CoordsUtils.zero()
|
||||
}
|
||||
};
|
||||
export const MAIN_MENU_OPTIONS: MainMenuOptions = [
|
||||
'ACTION.OPEN',
|
||||
@@ -68,3 +93,12 @@ export const MAIN_MENU_OPTIONS: MainMenuOptions = [
|
||||
'LINK.GITHUB',
|
||||
'VERSION'
|
||||
];
|
||||
|
||||
export const DEFAULT_ICON: Icon = {
|
||||
id: 'default',
|
||||
name: 'block',
|
||||
isIsometric: true,
|
||||
url: ''
|
||||
};
|
||||
|
||||
export const DEFAULT_LABEL_HEIGHT = 20;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import Isoflow from 'src/Isoflow';
|
||||
import { initialScene } from '../initialScene';
|
||||
import { initialData } from '../initialData';
|
||||
|
||||
export const BasicEditor = () => {
|
||||
return <Isoflow initialScene={initialScene} />;
|
||||
return <Isoflow initialData={initialData} />;
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import Isoflow from 'src/Isoflow';
|
||||
import { initialScene } from '../initialScene';
|
||||
import { initialData } from '../initialData';
|
||||
|
||||
export const DebugTools = () => {
|
||||
return <Isoflow initialScene={initialScene} enableDebugTools height="100%" />;
|
||||
return <Isoflow initialData={initialData} enableDebugTools height="100%" />;
|
||||
};
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import React from 'react';
|
||||
import Isoflow from 'src/Isoflow';
|
||||
import { initialScene } from '../initialScene';
|
||||
import { initialData } from '../initialData';
|
||||
|
||||
export const ReadonlyMode = () => {
|
||||
return (
|
||||
<Isoflow initialScene={initialScene} editorMode="EXPLORABLE_READONLY" />
|
||||
);
|
||||
return <Isoflow initialData={initialData} editorMode="EXPLORABLE_READONLY" />;
|
||||
};
|
||||
|
||||
782
src/examples/initialData.ts
Normal file
782
src/examples/initialData.ts
Normal file
@@ -0,0 +1,782 @@
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
import { InitialData } from 'src/Isoflow';
|
||||
import { flattenCollections } from '@isoflow/isopacks/dist/utils';
|
||||
import isoflowIsopack from '@isoflow/isopacks/dist/isoflow';
|
||||
import awsIsopack from '@isoflow/isopacks/dist/aws';
|
||||
import gcpIsopack from '@isoflow/isopacks/dist/gcp';
|
||||
import azureIsopack from '@isoflow/isopacks/dist/azure';
|
||||
import kubernetesIsopack from '@isoflow/isopacks/dist/kubernetes';
|
||||
|
||||
const isopacks = flattenCollections([
|
||||
isoflowIsopack,
|
||||
awsIsopack,
|
||||
azureIsopack,
|
||||
gcpIsopack,
|
||||
kubernetesIsopack
|
||||
]);
|
||||
|
||||
// The data used in this visualisation example has been derived from the following blog post
|
||||
// https://www.altexsoft.com/blog/travel/airport-technology-management-operations-software-solutions-and-vendors/
|
||||
export const initialData: InitialData = {
|
||||
icons: isopacks
|
||||
};
|
||||
|
||||
// export const initialData: InitialData = {
|
||||
// title: 'Airport Management Software',
|
||||
// icons: isopacks,
|
||||
// items: [
|
||||
// {
|
||||
// id: 'b6cf011d-0bc2-474d-8a4b-022d24ecc5d5',
|
||||
// tile: { x: 0, y: 0 },
|
||||
// label: 'Airport Operational Database',
|
||||
// description:
|
||||
// '<p>Each airport has its own central database that stores and updates all necessary data regarding daily flights, seasonal schedules, available resources, and other flight-related information, like billing data and flight fees. AODB is a key feature for the functioning of an airport.</p><p><br></p><p>This database is connected to the rest of the airport modules: <em>airport information systems, revenue management systems, and air traffic management</em>.</p><p><br></p><p>The system can supply different information for different segments of users: passengers, airport staff, crew, or members of specific departments, authorities, business partners, or police.</p><p><br></p><p>AODB represents the information on a graphical display.</p><p><br></p><p><strong>AODB functions include:</strong></p><p>- Reference-data processing</p><p>- Seasonal scheduling</p><p>- Daily flight schedule processing</p><p>- Processing of payments</p>',
|
||||
// labelHeight: 140,
|
||||
// icon: 'storage'
|
||||
// },
|
||||
// {
|
||||
// id: '815b0205-516c-48a9-ac34-2007bb155d75',
|
||||
// tile: { x: 11, y: 3 },
|
||||
// label: 'Apron handling',
|
||||
// description:
|
||||
// '<p>Apron (or ground handling) deals with aircraft servicing. This includes passenger boarding and guidance, cargo and mail loading, and apron services. Apron services include aircraft guiding, cleaning, drainage, deicing, catering, and fueling. At this stage, the software facilitates dealing with information about the weight of the baggage and cargo load, number of passengers, boarding bridges parking, and the ground services that must be supplied to the aircraft. By entering this information into the system, their costs can be calculated and invoiced through the billing system.</p>',
|
||||
// labelHeight: 80,
|
||||
// icon: 'block'
|
||||
// },
|
||||
// {
|
||||
// id: '085f81b9-651d-47a1-94ec-c4cd02bb08c4',
|
||||
// tile: { x: 11, y: 0 },
|
||||
// label: 'ATC Tower',
|
||||
// description:
|
||||
// '<p>The Air Traffic Control Tower is a structure that delivers air and ground control of the aircraft. It ensures safety by guiding and navigating the vehicles and aircraft. It is performed by way of visual signaling, radar, and radio communication in the air and on the ground. The main focus of the tower is to make sure that all aircraft have been assigned to the right place, that passengers aren’t at risk, and that the aircraft will have a suitable passenger boarding bridge allocated on the apron.</p><p><br></p><p>The ATC tower has a control room that serves as a channel between landside (terminal) and airside operations in airports. The control room personnel are tasked with ensuring the security and safety of the passengers as well as ground handling. Usually, a control room has CCTV monitors and air traffic control systems that maintain the order in the terminal and on the apron.</p>',
|
||||
// labelHeight: 80,
|
||||
// icon: 'block'
|
||||
// },
|
||||
// {
|
||||
// id: 'e4909db0-da09-49a3-8cfd-927ce191c28d',
|
||||
// tile: { x: 11, y: -3 },
|
||||
// label: 'Aeronautical Fixed Telecommunication Network (AFTN) Systems',
|
||||
// description:
|
||||
// '<p>AFTN systems handle communication and exchange of data including navigation services. Usually, airports exchange traffic environment messages, safety messages, information about the weather, geographic material, disruptions, etc. They serve as communication between airports and aircraft.</p><p><br></p><p>Software for aeronautical telecommunications stores flight plans and flight information, entered in ICAO format and UTC. The information stored can be used for planning and statistical purposes. For airports, it’s important to understand the aircraft type and its weight to assign it to the right place on the runway. AFTN systems hold the following information:</p><p><br></p><p>- Aircraft registration</p><p>- Runway used</p><p>- Actual time of landing and departure</p><p>- Number of circuits</p><p>- Number and type of approaches</p><p>- New estimates of arrival and departure</p><p>- New flight information</p><p><br></p><p>Air traffic management is performed from an ATC tower.</p>',
|
||||
// labelHeight: 80,
|
||||
// icon: 'block'
|
||||
// },
|
||||
// {
|
||||
// id: '680a2ad0-839f-4cfc-8b35-24c557fa1e8f',
|
||||
// tile: { x: -9, y: -3 },
|
||||
// label: 'Passenger facilitation services',
|
||||
// description:
|
||||
// '<p>include passenger processing (check-in, boarding, border control) and baggage handling (tagging, dropping and handling). They follow passengers to the shuttle buses to carry them to their flights. Arrival operations include boarding control and baggage handling.</p>',
|
||||
// labelHeight: 120,
|
||||
// icon: 'user'
|
||||
// },
|
||||
// {
|
||||
// id: '8277f135-e833-40b3-8232-0461b86added',
|
||||
// tile: { x: -9, y: 3 },
|
||||
// label: 'Terminal management systems',
|
||||
// description:
|
||||
// '<p>Includes maintenance and monitoring of management systems for assets, buildings, electrical grids, environmental systems, and vertical transportation organization. It also facilitates staff communications and management.</p>',
|
||||
// labelHeight: 120,
|
||||
// icon: 'function-module'
|
||||
// },
|
||||
// {
|
||||
// id: 'ca560fc6-da8b-49cc-b4eb-fdc300ff6aed',
|
||||
// tile: { x: -9, y: 7 },
|
||||
// label: 'Maintenance and monitoring',
|
||||
// labelHeight: 80,
|
||||
// icon: 'block'
|
||||
// },
|
||||
// {
|
||||
// id: '8f02ce50-0aa4-4950-b077-c1daf5da023b',
|
||||
// tile: { x: -12, y: 7 },
|
||||
// label: 'Resource management',
|
||||
// labelHeight: 80,
|
||||
// icon: 'block'
|
||||
// },
|
||||
// {
|
||||
// id: '308dfac4-fc9a-4985-9b5c-9616e0674ac4',
|
||||
// tile: { x: -15, y: 7 },
|
||||
// label: 'Staff management',
|
||||
// description:
|
||||
// '<p>Staff modules provide the necessary information about ongoing processes in the airport, such as data on flights (in ICAO or UTC formats) and other important events to keep responsible staff members updated. Information is distributed through the airport radio system, or displayed on a PC connected via the airport LAN or on mobile devices.</p>',
|
||||
// labelHeight: 80,
|
||||
// icon: 'block'
|
||||
// },
|
||||
// {
|
||||
// id: '2420494b-312a-4ddf-b8c3-591fa55563cb',
|
||||
// tile: { x: -9, y: -8 },
|
||||
// label: 'Border control (customs and security services)',
|
||||
// description:
|
||||
// '<p>In airports, security services usually unite perimeter security, terminal security, and border controls. These services require biometric authentication and integration into government systems to allow a customs officer to view the status of a passenger.</p>',
|
||||
// labelHeight: 60,
|
||||
// icon: 'block'
|
||||
// },
|
||||
// {
|
||||
// id: '2557a6bd-0c92-4522-b704-f1f6d076be80',
|
||||
// tile: { x: -12, y: -8 },
|
||||
// label: 'Common use services (self-service check-in systems)',
|
||||
// description:
|
||||
// '<p>An airport must ensure smooth passenger flow. Various digital self-services, like check-in kiosks or automated self-service gates, make it happen. Self-service options, especially check-in kiosks, remain popular. Worldwide in 2018, passengers used kiosks to check themselves in 88 percent of the time.</p>',
|
||||
// labelHeight: 80,
|
||||
// icon: 'block'
|
||||
// },
|
||||
// {
|
||||
// id: '7c422bf4-bbd2-4364-ad9b-5250c2877695',
|
||||
// tile: { x: -15, y: -8 },
|
||||
// label: 'Baggage handling',
|
||||
// description:
|
||||
// '<p>A passenger must check a bag before it’s loaded on the aircraft. The time the baggage is loaded is displayed and tracked until the destination is reached and the bag is returned to the owners.</p>',
|
||||
// labelHeight: 80,
|
||||
// icon: 'block'
|
||||
// },
|
||||
// {
|
||||
// id: 'fd5dd3ed-b8de-41ed-a642-b8bd71aaebf8',
|
||||
// tile: { x: 5, y: 0 },
|
||||
// label: 'Airside operations',
|
||||
// description:
|
||||
// '<p>Includes systems to handle aircraft landing and navigation, airport traffic management, runway management, and ground handling safety.</p>',
|
||||
// labelHeight: 100,
|
||||
// icon: 'plane'
|
||||
// },
|
||||
// {
|
||||
// id: 'cb6b557e-01c8-4208-88f8-2076351aa53c',
|
||||
// tile: { x: 0, y: -5 },
|
||||
// label: 'Invoicing and billing',
|
||||
// description:
|
||||
// '<p>Each flight an airport handles generates a defined revenue for the airport paid by the airline operating the aircraft. Aeronautical invoicing systems make payment possible for any type and size of aircraft. It accepts payments in cash and credit in multiple currencies. The billing also extends to ATC services.</p><p><br></p><p>Depending on the aircraft type and weight and ground services provided, an airport can calculate the aeronautical fee and issue an invoice with a bill. It is calculated using the following data:</p><p><br></p><p>- Aircraft registration</p><p>- Parking time at the airport</p><p>- Airport point of departure and/or landing</p><p>- Times at the different points of entry or departure</p><p><br></p><p>The data is entered or integrated from ATC. Based on this information, the airport calculates the charges and sends the bills.</p>',
|
||||
// labelHeight: 80,
|
||||
// icon: 'paymentcard'
|
||||
// },
|
||||
// {
|
||||
// id: '1789808d-6d52-4714-aeea-954f3e4aba66',
|
||||
// tile: { x: -2, y: -11 },
|
||||
// label: 'ATC Tower Billing',
|
||||
// labelHeight: 60,
|
||||
// icon: 'block'
|
||||
// },
|
||||
// {
|
||||
// id: '5096a0f1-7548-4cba-8477-7f07e3bdc206',
|
||||
// tile: { x: 2, y: -11 },
|
||||
// label: 'Non Aeronautical revenue',
|
||||
// labelHeight: 60,
|
||||
// icon: 'block'
|
||||
// },
|
||||
// {
|
||||
// id: '0e1ecd31-ee64-49b9-a5be-3375bd3cece6',
|
||||
// tile: { x: 3, y: 13 },
|
||||
// label: 'Automatic Terminal Information Service (ATIS)',
|
||||
// description:
|
||||
// '<p>Broadcasts the weather reports, the condition of the runway, or other local information for pilots and crews.</p><p><br></p><p>Some airport software vendors offer off-the-shelf solutions to facilitate particular tasks, like maintenance, or airport operations. However, most of them provide integrated systems that comprise modules for several operations.</p>',
|
||||
// labelHeight: 100,
|
||||
// icon: 'block'
|
||||
// },
|
||||
// {
|
||||
// id: 'b0368dfe-cc6e-405a-9471-0867c4c51d08',
|
||||
// tile: { x: 0, y: 13 },
|
||||
// label: 'Flight Information Display Systems (FIDS)',
|
||||
// description:
|
||||
// '<p>Exhibits the status of boarding, gates, aircraft, flight number, and other flight details. A computer controls the screens that are connected to the data management systems and displays up-to-date information about flights in real time. Some airports have a digital FIDS in the form of apps or on their websites. Also, the displays may show other public information such as the weather, news, safety messages, menus, and advertising. Airports can choose the type, languages, and means of entering the information, whether it be manually or loaded from a central database.</p>',
|
||||
// labelHeight: 100,
|
||||
// icon: 'block'
|
||||
// },
|
||||
// {
|
||||
// id: '9f1417b9-8785-498b-bb4e-4ee9a827e611',
|
||||
// tile: { x: -3, y: 13 },
|
||||
// label: 'Public address (PA) systems',
|
||||
// description:
|
||||
// '<p>Informs passengers and airport staff about any changes and processes of importance, for instance, gates, times of arrival, calls, and alerts. Also, information can be communicated to pilots, aircraft staff, crew, etc. PA systems usually include voice messages broadcasted through loudspeakers.</p>',
|
||||
// labelHeight: 100,
|
||||
// icon: 'block'
|
||||
// },
|
||||
// {
|
||||
// id: '7f9e0f95-b490-4e03-99c7-8df10788d6df',
|
||||
// tile: { x: 0, y: 6 },
|
||||
// label: 'Information management',
|
||||
// description:
|
||||
// '<p>This subsystem is responsible for the collection and distribution of daily flight information, storing of seasonal and arrival/departure information, as well as the connection with airlines.</p>',
|
||||
// labelHeight: 80,
|
||||
// icon: 'queue'
|
||||
// },
|
||||
// {
|
||||
// id: 'bf5b31ed-97fc-4e23-9ccf-107ba9f1bb58',
|
||||
// tile: { x: -5, y: 0 },
|
||||
// label: 'Landside operations',
|
||||
// description:
|
||||
// '<p>This subsystem is aimed at serving passengers and maintenance of terminal buildings, parking facilities, and vehicular traffic circular drives. Passenger operations include baggage handling and tagging.</p>',
|
||||
// labelHeight: 180,
|
||||
// icon: 'office'
|
||||
// }
|
||||
// ],
|
||||
// connectors: [
|
||||
// {
|
||||
// id: 'be41366a-be8e-43f5-a85c-1528cc380b70',
|
||||
// color: '#a0b9f8',
|
||||
// style: 'SOLID',
|
||||
// width: 10,
|
||||
// anchors: [
|
||||
// {
|
||||
// id: '612d1f4e-588e-468e-a819-9e9b5528abe2',
|
||||
// ref: { node: '308dfac4-fc9a-4985-9b5c-9616e0674ac4' }
|
||||
// },
|
||||
// {
|
||||
// id: '82ce8f7c-1315-4bae-9966-435a3504de9a',
|
||||
// ref: { tile: { x: -15, y: 5 } }
|
||||
// }
|
||||
// ]
|
||||
// },
|
||||
// {
|
||||
// id: 'be678975-abc7-4345-82b3-2e829486fe3c',
|
||||
// color: '#a0b9f8',
|
||||
// style: 'SOLID',
|
||||
// width: 10,
|
||||
// anchors: [
|
||||
// {
|
||||
// id: '4d4868ff-53df-414b-b51f-2de31fd125e6',
|
||||
// ref: { tile: { x: -9, y: -5 } }
|
||||
// },
|
||||
// {
|
||||
// id: 'bec7a569-6f1f-46f8-8ddf-b6ace6354f88',
|
||||
// ref: { node: '680a2ad0-839f-4cfc-8b35-24c557fa1e8f' }
|
||||
// }
|
||||
// ]
|
||||
// },
|
||||
// {
|
||||
// id: 'b155cdd6-da92-4553-b295-3c1a3262169d',
|
||||
// color: '#a0b9f8',
|
||||
// style: 'SOLID',
|
||||
// width: 10,
|
||||
// anchors: [
|
||||
// {
|
||||
// id: '80a0fa48-f4b2-46e7-8ede-0c725a279b52',
|
||||
// ref: { node: 'ca560fc6-da8b-49cc-b4eb-fdc300ff6aed' }
|
||||
// },
|
||||
// {
|
||||
// id: '6681a140-8535-4c14-ab87-656476c81c06',
|
||||
// ref: { tile: { x: -9, y: 5 } }
|
||||
// }
|
||||
// ]
|
||||
// },
|
||||
// {
|
||||
// id: '1b3bcf33-ca67-461b-8f80-a70eba87e6ab',
|
||||
// color: '#a0b9f8',
|
||||
// style: 'SOLID',
|
||||
// width: 10,
|
||||
// anchors: [
|
||||
// {
|
||||
// id: 'b1d4cc02-056d-4a06-bb93-932c8630c7a0',
|
||||
// ref: { tile: { x: -9, y: -5 } }
|
||||
// },
|
||||
// {
|
||||
// id: '72eb7689-fa58-4ac1-88a9-e9208f11928a',
|
||||
// ref: { tile: { x: -12, y: -5 } }
|
||||
// },
|
||||
// {
|
||||
// id: 'e7b2facb-c137-49f1-acdd-095739b51e05',
|
||||
// ref: { tile: { x: -15, y: -5 } }
|
||||
// }
|
||||
// ]
|
||||
// },
|
||||
// {
|
||||
// id: '87b538b5-e908-495d-bb12-05f068f1b6ee',
|
||||
// color: '#a0b9f8',
|
||||
// style: 'SOLID',
|
||||
// width: 10,
|
||||
// anchors: [
|
||||
// {
|
||||
// id: 'ca0d0583-8d87-4365-ae77-e420ae6a25cf',
|
||||
// ref: { node: '2557a6bd-0c92-4522-b704-f1f6d076be80' }
|
||||
// },
|
||||
// {
|
||||
// id: 'a5a1520b-5914-414e-82aa-5ebdf3fbda16',
|
||||
// ref: { tile: { x: -12, y: -5 } }
|
||||
// }
|
||||
// ]
|
||||
// },
|
||||
// {
|
||||
// id: 'eb44bfe6-66c3-492c-949c-3f8faba4c455',
|
||||
// color: '#a0b9f8',
|
||||
// style: 'SOLID',
|
||||
// width: 10,
|
||||
// anchors: [
|
||||
// {
|
||||
// id: '54d16963-57ad-4252-ae54-42417c4116a5',
|
||||
// ref: { node: '8277f135-e833-40b3-8232-0461b86added' }
|
||||
// },
|
||||
// {
|
||||
// id: '24eacb98-43cf-4e4c-8e4e-453247edfafc',
|
||||
// ref: { tile: { x: -9, y: 5 } }
|
||||
// },
|
||||
// {
|
||||
// id: 'e7d4f48d-68a1-48be-9488-173955730d17',
|
||||
// ref: { tile: { x: -12, y: 5 } }
|
||||
// },
|
||||
// {
|
||||
// id: '95c2eca2-c3ce-4fb3-abac-241ccd7a2f56',
|
||||
// ref: { tile: { x: -15, y: 5 } }
|
||||
// }
|
||||
// ]
|
||||
// },
|
||||
// {
|
||||
// id: '110033ad-b542-4415-bb95-6bad8de33198',
|
||||
// color: '#a0b9f8',
|
||||
// style: 'SOLID',
|
||||
// width: 10,
|
||||
// anchors: [
|
||||
// {
|
||||
// id: '9ccd7127-1ed6-4ac0-9fcd-31e17c306987',
|
||||
// ref: { node: '8f02ce50-0aa4-4950-b077-c1daf5da023b' }
|
||||
// },
|
||||
// {
|
||||
// id: '4c4f7d59-2b06-46cc-8eed-f3cd2b2af9b3',
|
||||
// ref: { tile: { x: -12, y: 5 } }
|
||||
// }
|
||||
// ]
|
||||
// },
|
||||
// {
|
||||
// id: 'f7b5f65c-0503-479b-983d-6b3be7d4355b',
|
||||
// color: '#bbadfb',
|
||||
// style: 'SOLID',
|
||||
// width: 10,
|
||||
// anchors: [
|
||||
// {
|
||||
// id: 'fbeb8d12-597a-4654-92f4-41006b57beb8',
|
||||
// ref: { node: '815b0205-516c-48a9-ac34-2007bb155d75' }
|
||||
// },
|
||||
// {
|
||||
// id: '6f88cf82-23d9-4d95-9823-3c76b1956c9c',
|
||||
// ref: { tile: { x: 9, y: 3 } }
|
||||
// }
|
||||
// ]
|
||||
// },
|
||||
// {
|
||||
// id: '1c7ccb81-9f31-4cc3-90ee-2f92c4be1e8f',
|
||||
// color: '#bbadfb',
|
||||
// style: 'SOLID',
|
||||
// width: 10,
|
||||
// anchors: [
|
||||
// {
|
||||
// id: '199f843d-f8ce-4a26-b71a-1b4f799109a9',
|
||||
// ref: { node: 'e4909db0-da09-49a3-8cfd-927ce191c28d' }
|
||||
// },
|
||||
// {
|
||||
// id: 'd2d63da6-a60a-4569-af9f-5d497013bd8e',
|
||||
// ref: { tile: { x: 9, y: -3 } }
|
||||
// }
|
||||
// ]
|
||||
// },
|
||||
// {
|
||||
// id: '48435cd4-297f-4078-b5d4-79209babaa0f',
|
||||
// color: '#bbadfb',
|
||||
// style: 'SOLID',
|
||||
// width: 10,
|
||||
// anchors: [
|
||||
// {
|
||||
// id: '9316ac7f-7913-489e-8516-fb74eb9aed9c',
|
||||
// ref: { tile: { x: 9, y: 3 } }
|
||||
// },
|
||||
// {
|
||||
// id: 'd53e7165-c645-4e8f-b4c7-84ebc4369f4a',
|
||||
// ref: { tile: { x: 9, y: 0 } }
|
||||
// },
|
||||
// {
|
||||
// id: '01860aaf-3b57-45f2-b833-15fcafdd4abb',
|
||||
// ref: { tile: { x: 9, y: -3 } }
|
||||
// }
|
||||
// ]
|
||||
// },
|
||||
// {
|
||||
// id: '1a58a13e-d136-4c99-a111-143097873147',
|
||||
// color: '#bbadfb',
|
||||
// style: 'SOLID',
|
||||
// width: 10,
|
||||
// anchors: [
|
||||
// {
|
||||
// id: 'fe873a58-afdc-4c8b-b89c-b01332e6264b',
|
||||
// ref: { node: '085f81b9-651d-47a1-94ec-c4cd02bb08c4' }
|
||||
// },
|
||||
// {
|
||||
// id: '93286249-0ee8-4a0d-ae09-c17736b0535f',
|
||||
// ref: { tile: { x: 9, y: 0 } }
|
||||
// }
|
||||
// ]
|
||||
// },
|
||||
// {
|
||||
// id: '3abc77ea-357b-4671-95a7-6b17e0219032',
|
||||
// color: '#b3e5e3',
|
||||
// style: 'SOLID',
|
||||
// width: 10,
|
||||
// anchors: [
|
||||
// {
|
||||
// id: '0077477a-8978-48dc-9051-51f9322519bc',
|
||||
// ref: { node: '9f1417b9-8785-498b-bb4e-4ee9a827e611' }
|
||||
// },
|
||||
// {
|
||||
// id: 'e69310e4-f794-4771-aa5f-39291aa090ef',
|
||||
// ref: { tile: { x: -3, y: 11 } }
|
||||
// }
|
||||
// ]
|
||||
// },
|
||||
// {
|
||||
// id: '75200d73-06f4-4403-923f-32f0041c9701',
|
||||
// color: '#b3e5e3',
|
||||
// style: 'SOLID',
|
||||
// width: 10,
|
||||
// anchors: [
|
||||
// {
|
||||
// id: '9f5616ae-e638-40af-ba0b-b25f6adae2f7',
|
||||
// ref: { node: '0e1ecd31-ee64-49b9-a5be-3375bd3cece6' }
|
||||
// },
|
||||
// {
|
||||
// id: 'e86b8651-a385-4a96-90cc-2ed2b6daa9f2',
|
||||
// ref: { tile: { x: 3, y: 11 } }
|
||||
// }
|
||||
// ]
|
||||
// },
|
||||
// {
|
||||
// id: '4a7ccc74-f296-4d0b-be11-8117c990795d',
|
||||
// color: '#b3e5e3',
|
||||
// style: 'SOLID',
|
||||
// width: 10,
|
||||
// anchors: [
|
||||
// {
|
||||
// id: '7b721b82-220d-45f6-87d4-479c1b847a60',
|
||||
// ref: { node: 'b6cf011d-0bc2-474d-8a4b-022d24ecc5d5' }
|
||||
// },
|
||||
// {
|
||||
// id: '1908c227-8066-4c8d-ba55-d5822c224d52',
|
||||
// ref: { node: '7f9e0f95-b490-4e03-99c7-8df10788d6df' }
|
||||
// }
|
||||
// ]
|
||||
// },
|
||||
// {
|
||||
// id: '9f6bc159-507f-4b56-9171-d95f00490230',
|
||||
// color: '#a0b9f8',
|
||||
// style: 'SOLID',
|
||||
// width: 10,
|
||||
// anchors: [
|
||||
// {
|
||||
// id: 'b41f10e5-5d29-4cee-8102-0005cf146300',
|
||||
// ref: { node: '8277f135-e833-40b3-8232-0461b86added' }
|
||||
// },
|
||||
// {
|
||||
// id: '3f5bfd1b-f8b3-4a00-976e-6bc4382749ee',
|
||||
// ref: { tile: { x: -9, y: 0 } }
|
||||
// },
|
||||
// {
|
||||
// id: '3792f3db-edd1-4d43-9f16-0be811687c85',
|
||||
// ref: { node: 'bf5b31ed-97fc-4e23-9ccf-107ba9f1bb58' }
|
||||
// }
|
||||
// ]
|
||||
// },
|
||||
// {
|
||||
// id: '5b47caae-fbdd-4d1b-8cc5-0068536fb8b2',
|
||||
// color: '#a0b9f8',
|
||||
// style: 'SOLID',
|
||||
// width: 10,
|
||||
// anchors: [
|
||||
// {
|
||||
// id: '16ba31e6-a713-403d-b23e-4c40b19e7d66',
|
||||
// ref: { node: '680a2ad0-839f-4cfc-8b35-24c557fa1e8f' }
|
||||
// },
|
||||
// {
|
||||
// id: '59675d93-8ad7-4abd-85d4-cfe0004384c1',
|
||||
// ref: { tile: { x: -9, y: 0 } }
|
||||
// },
|
||||
// {
|
||||
// id: '1ab79aec-2587-47eb-9b2f-42e9eb44fe77',
|
||||
// ref: { node: 'bf5b31ed-97fc-4e23-9ccf-107ba9f1bb58' }
|
||||
// }
|
||||
// ]
|
||||
// },
|
||||
// {
|
||||
// id: 'a6c63183-ce9a-4d54-9487-569f2a8a8add',
|
||||
// color: '#a0b9f8',
|
||||
// style: 'SOLID',
|
||||
// width: 10,
|
||||
// anchors: [
|
||||
// {
|
||||
// id: '8a2d4f72-5f62-4531-b2f7-b0eed5e533a5',
|
||||
// ref: { node: 'bf5b31ed-97fc-4e23-9ccf-107ba9f1bb58' }
|
||||
// },
|
||||
// {
|
||||
// id: 'ab054b9f-7574-4195-9465-c4a127b8e03e',
|
||||
// ref: { node: 'b6cf011d-0bc2-474d-8a4b-022d24ecc5d5' }
|
||||
// }
|
||||
// ]
|
||||
// },
|
||||
// {
|
||||
// id: 'afa7915a-c1f6-4f40-87c0-ace0ecacf5bb',
|
||||
// color: '#b3e5e3',
|
||||
// style: 'SOLID',
|
||||
// width: 10,
|
||||
// anchors: [
|
||||
// {
|
||||
// id: '2b18ab38-7f76-4e63-ae79-ca6cfaa2ee49',
|
||||
// ref: { tile: { x: -3, y: 11 } }
|
||||
// },
|
||||
// {
|
||||
// id: 'a926bd20-a16e-4349-8e31-bb4109ab0387',
|
||||
// ref: { tile: { x: 3, y: 11 } }
|
||||
// }
|
||||
// ]
|
||||
// },
|
||||
// {
|
||||
// id: 'c1263273-a57c-4e0b-92b8-04c21b805b29',
|
||||
// color: '#b3e5e3',
|
||||
// style: 'SOLID',
|
||||
// width: 10,
|
||||
// anchors: [
|
||||
// {
|
||||
// id: 'b1ee48ad-70f7-44ff-9d84-b3893ce3dff1',
|
||||
// ref: { node: 'b0368dfe-cc6e-405a-9471-0867c4c51d08' }
|
||||
// },
|
||||
// {
|
||||
// id: 'e6148182-5a09-4e0a-85f4-1389dfb33f8b',
|
||||
// ref: { tile: { x: 0, y: 11 } }
|
||||
// }
|
||||
// ]
|
||||
// },
|
||||
// {
|
||||
// id: '5706f58e-6b29-4ec7-aa0b-c59ce6198870',
|
||||
// color: '#b3e5e3',
|
||||
// style: 'SOLID',
|
||||
// width: 10,
|
||||
// anchors: [
|
||||
// {
|
||||
// id: '5a8e0e07-7e0c-40eb-b50c-bba2a2403c2f',
|
||||
// ref: { tile: { x: 0, y: 11 } }
|
||||
// },
|
||||
// {
|
||||
// id: 'b3611f56-35c2-4cc3-8dc6-461c182937e5',
|
||||
// ref: { node: '7f9e0f95-b490-4e03-99c7-8df10788d6df' }
|
||||
// }
|
||||
// ]
|
||||
// },
|
||||
// {
|
||||
// id: '17547863-f8f3-4653-92ec-f76d3ec0835f',
|
||||
// color: '#bbadfb',
|
||||
// style: 'SOLID',
|
||||
// width: 10,
|
||||
// anchors: [
|
||||
// {
|
||||
// id: '0445d65f-05b4-47cf-9163-dd0d6eefa4af',
|
||||
// ref: { node: 'b6cf011d-0bc2-474d-8a4b-022d24ecc5d5' }
|
||||
// },
|
||||
// {
|
||||
// id: 'e77a4942-1625-4893-a4f5-634dbc528321',
|
||||
// ref: { node: 'fd5dd3ed-b8de-41ed-a642-b8bd71aaebf8' }
|
||||
// }
|
||||
// ]
|
||||
// },
|
||||
// {
|
||||
// id: '2656820b-e1fb-41fd-980f-7b94d6e9906e',
|
||||
// color: '#bbadfb',
|
||||
// style: 'SOLID',
|
||||
// width: 10,
|
||||
// anchors: [
|
||||
// {
|
||||
// id: '2717739f-a2d4-44cf-bc45-5d8ae8bb67dd',
|
||||
// ref: { node: 'fd5dd3ed-b8de-41ed-a642-b8bd71aaebf8' }
|
||||
// },
|
||||
// {
|
||||
// id: 'b0399a5a-3bc3-4864-aa1b-c2194437f644',
|
||||
// ref: { tile: { x: 9, y: 0 } }
|
||||
// }
|
||||
// ]
|
||||
// },
|
||||
// {
|
||||
// id: 'e9f0546e-d2f3-4ae3-b6dd-55d3cf774093',
|
||||
// color: '#a8dc9d',
|
||||
// style: 'SOLID',
|
||||
// width: 10,
|
||||
// anchors: [
|
||||
// {
|
||||
// id: '7cca17bf-700b-4192-be94-7b347b73a77a',
|
||||
// ref: { node: '1789808d-6d52-4714-aeea-954f3e4aba66' }
|
||||
// },
|
||||
// {
|
||||
// id: '6e8d5fb2-b995-4e0e-9465-b664ea793f50',
|
||||
// ref: { tile: { x: -2, y: -9 } }
|
||||
// }
|
||||
// ]
|
||||
// },
|
||||
// {
|
||||
// id: '6dce44fd-0fb5-4a08-9daa-1716313c59d8',
|
||||
// color: '#a8dc9d',
|
||||
// style: 'SOLID',
|
||||
// width: 10,
|
||||
// anchors: [
|
||||
// {
|
||||
// id: '5a3751f2-bd9f-4ddc-808a-f0f23a2eecd8',
|
||||
// ref: { node: '5096a0f1-7548-4cba-8477-7f07e3bdc206' }
|
||||
// },
|
||||
// {
|
||||
// id: '0f6673bf-b5c4-4e1b-b3aa-bcdca9f24e81',
|
||||
// ref: { tile: { x: 2, y: -9 } }
|
||||
// }
|
||||
// ]
|
||||
// },
|
||||
// {
|
||||
// id: '725419a0-6ca4-4d1b-b6a4-a7ae7feabcb0',
|
||||
// color: '#a8dc9d',
|
||||
// style: 'SOLID',
|
||||
// width: 10,
|
||||
// anchors: [
|
||||
// {
|
||||
// id: 'dfdda749-2f06-4a32-903e-35e336d3842d',
|
||||
// ref: { node: 'b6cf011d-0bc2-474d-8a4b-022d24ecc5d5' }
|
||||
// },
|
||||
// {
|
||||
// id: '354700b6-6b10-49cb-a943-f2eeb8e4741b',
|
||||
// ref: { node: 'cb6b557e-01c8-4208-88f8-2076351aa53c' }
|
||||
// }
|
||||
// ]
|
||||
// },
|
||||
// {
|
||||
// id: '41431713-1647-4b47-b258-9d3ec4665b77',
|
||||
// color: '#a8dc9d',
|
||||
// style: 'SOLID',
|
||||
// width: 10,
|
||||
// anchors: [
|
||||
// {
|
||||
// id: 'f579ae26-9023-4041-ba62-33da290ca83f',
|
||||
// ref: { tile: { x: 2, y: -9 } }
|
||||
// },
|
||||
// {
|
||||
// id: '2a123a41-1481-4c87-9aca-89a819ee4478',
|
||||
// ref: { tile: { x: -2, y: -9 } }
|
||||
// }
|
||||
// ]
|
||||
// },
|
||||
// {
|
||||
// id: '3410464e-d12b-4cf2-bdc4-9eb338ea1c1a',
|
||||
// color: '#a8dc9d',
|
||||
// style: 'SOLID',
|
||||
// width: 10,
|
||||
// anchors: [
|
||||
// {
|
||||
// id: '832bad08-bd8a-4a57-b849-016b0482138c',
|
||||
// ref: { node: 'cb6b557e-01c8-4208-88f8-2076351aa53c' }
|
||||
// },
|
||||
// {
|
||||
// id: 'fe2422cd-1ced-4f4a-a79c-a9f052977cc5',
|
||||
// ref: { tile: { x: 0, y: -9 } }
|
||||
// }
|
||||
// ]
|
||||
// },
|
||||
// {
|
||||
// id: '8b95c19e-e5dd-4190-a93a-93fdec1bd37f',
|
||||
// color: '#a0b9f8',
|
||||
// style: 'SOLID',
|
||||
// width: 10,
|
||||
// anchors: [
|
||||
// {
|
||||
// id: 'd1281ea9-0f2c-4e24-a1e9-d0c8a67805ba',
|
||||
// ref: { tile: { x: -15, y: -5 } }
|
||||
// },
|
||||
// {
|
||||
// id: 'c0a7d637-79aa-4400-ab70-2b874e865ad2',
|
||||
// ref: { node: '7c422bf4-bbd2-4364-ad9b-5250c2877695' }
|
||||
// }
|
||||
// ]
|
||||
// },
|
||||
// {
|
||||
// id: '7013bdc5-31f0-4e64-8604-95eba5e1ca8e',
|
||||
// color: '#a0b9f8',
|
||||
// style: 'SOLID',
|
||||
// width: 10,
|
||||
// anchors: [
|
||||
// {
|
||||
// id: 'e1d4e165-649f-48a7-93aa-2c8181289901',
|
||||
// ref: { tile: { x: -9, y: -5 } }
|
||||
// },
|
||||
// {
|
||||
// id: '1f3e5712-a5be-4d2b-ba86-a0ec2e4ab439',
|
||||
// ref: { node: '2420494b-312a-4ddf-b8c3-591fa55563cb' }
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
// ],
|
||||
// textBoxes: [
|
||||
// {
|
||||
// id: '29e365cf-5f40-4355-8a2d-db45a509b140',
|
||||
// orientation: 'Y',
|
||||
// fontSize: 0.6,
|
||||
// tile: { x: -2, y: -1 },
|
||||
// text: 'AODB'
|
||||
// },
|
||||
// {
|
||||
// id: '40964f5b-7ebd-4c0d-9666-f8ff2de65dc8',
|
||||
// orientation: 'Y',
|
||||
// fontSize: 0.6,
|
||||
// tile: { x: 8, y: -1 },
|
||||
// text: 'Airside operations'
|
||||
// },
|
||||
// {
|
||||
// id: 'c6d16ec6-7110-4082-9370-44e127984b90',
|
||||
// orientation: 'X',
|
||||
// fontSize: 0.6,
|
||||
// tile: { x: -3, y: -13 },
|
||||
// text: 'Invoicing & Billing'
|
||||
// },
|
||||
// {
|
||||
// id: '57156c5e-9b82-4652-852d-422014228b74',
|
||||
// orientation: 'X',
|
||||
// fontSize: 0.6,
|
||||
// tile: { x: -16, y: -10 },
|
||||
// text: 'Passenger facilitation'
|
||||
// },
|
||||
// {
|
||||
// id: '92637173-542c-4d25-addf-ab50eda7a573',
|
||||
// orientation: 'X',
|
||||
// fontSize: 0.6,
|
||||
// tile: { x: -16, y: 4 },
|
||||
// text: 'Terminal management'
|
||||
// },
|
||||
// {
|
||||
// id: 'e555aad0-9e21-48db-9d71-53125c33c214',
|
||||
// orientation: 'X',
|
||||
// fontSize: 0.6,
|
||||
// tile: { x: -4, y: 10 },
|
||||
// text: 'Information management'
|
||||
// }
|
||||
// ],
|
||||
// rectangles: [
|
||||
// {
|
||||
// id: '82ef2751-9ea7-40fc-b3f5-3ea8ce1e1d67',
|
||||
// color: '#fad6ac',
|
||||
// from: { x: -1, y: 1 },
|
||||
// to: { x: 1, y: -1 }
|
||||
// },
|
||||
// {
|
||||
// id: '7d730e86-7884-400d-857a-8af7f22a9937',
|
||||
// color: '#a0b9f8',
|
||||
// from: { x: -8, y: -5 },
|
||||
// to: { x: -16, y: -9 }
|
||||
// },
|
||||
// {
|
||||
// id: 'f41434d1-bbe6-4d6d-a13d-7c9ad52e8e62',
|
||||
// color: '#a0b9f8',
|
||||
// from: { x: -16, y: 8 },
|
||||
// to: { x: -8, y: 5 }
|
||||
// },
|
||||
// {
|
||||
// id: '4ffe42ed-9cd0-48a9-93a0-6099ab53a146',
|
||||
// color: '#a8dc9d',
|
||||
// from: { x: -3, y: -9 },
|
||||
// to: { x: 3, y: -12 }
|
||||
// },
|
||||
// {
|
||||
// id: 'dc93e93b-2c6c-43a1-bb82-ad93c04f7707',
|
||||
// color: '#bbadfb',
|
||||
// from: { x: 9, y: 4 },
|
||||
// to: { x: 12, y: -4 }
|
||||
// },
|
||||
// {
|
||||
// id: 'eec5c861-6192-4eb6-a378-7554bda4d9a7',
|
||||
// color: '#b3e5e3',
|
||||
// from: { x: -4, y: 14 },
|
||||
// to: { x: 4, y: 11 }
|
||||
// }
|
||||
// ]
|
||||
// };
|
||||
@@ -1,779 +0,0 @@
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
import { InitialScene } from 'src/Isoflow';
|
||||
import { flattenCollections } from '@isoflow/isopacks/dist/utils';
|
||||
import isoflowIsopack from '@isoflow/isopacks/dist/isoflow';
|
||||
import awsIsopack from '@isoflow/isopacks/dist/aws';
|
||||
import gcpIsopack from '@isoflow/isopacks/dist/gcp';
|
||||
import azureIsopack from '@isoflow/isopacks/dist/azure';
|
||||
import kubernetesIsopack from '@isoflow/isopacks/dist/kubernetes';
|
||||
|
||||
const isopacks = flattenCollections([
|
||||
isoflowIsopack,
|
||||
awsIsopack,
|
||||
azureIsopack,
|
||||
gcpIsopack,
|
||||
kubernetesIsopack
|
||||
]);
|
||||
|
||||
// The data used in this visualisation example has been derived from the following blog post
|
||||
// https://www.altexsoft.com/blog/travel/airport-technology-management-operations-software-solutions-and-vendors/
|
||||
|
||||
export const initialScene: InitialScene = {
|
||||
title: 'Airport Management Software',
|
||||
icons: isopacks,
|
||||
nodes: [
|
||||
{
|
||||
id: 'b6cf011d-0bc2-474d-8a4b-022d24ecc5d5',
|
||||
tile: { x: 0, y: 0 },
|
||||
label: 'Airport Operational Database',
|
||||
description:
|
||||
'<p>Each airport has its own central database that stores and updates all necessary data regarding daily flights, seasonal schedules, available resources, and other flight-related information, like billing data and flight fees. AODB is a key feature for the functioning of an airport.</p><p><br></p><p>This database is connected to the rest of the airport modules: <em>airport information systems, revenue management systems, and air traffic management</em>.</p><p><br></p><p>The system can supply different information for different segments of users: passengers, airport staff, crew, or members of specific departments, authorities, business partners, or police.</p><p><br></p><p>AODB represents the information on a graphical display.</p><p><br></p><p><strong>AODB functions include:</strong></p><p>- Reference-data processing</p><p>- Seasonal scheduling</p><p>- Daily flight schedule processing</p><p>- Processing of payments</p>',
|
||||
labelHeight: 140,
|
||||
icon: 'storage'
|
||||
},
|
||||
{
|
||||
id: '815b0205-516c-48a9-ac34-2007bb155d75',
|
||||
tile: { x: 11, y: 3 },
|
||||
label: 'Apron handling',
|
||||
description:
|
||||
'<p>Apron (or ground handling) deals with aircraft servicing. This includes passenger boarding and guidance, cargo and mail loading, and apron services. Apron services include aircraft guiding, cleaning, drainage, deicing, catering, and fueling. At this stage, the software facilitates dealing with information about the weight of the baggage and cargo load, number of passengers, boarding bridges parking, and the ground services that must be supplied to the aircraft. By entering this information into the system, their costs can be calculated and invoiced through the billing system.</p>',
|
||||
labelHeight: 80,
|
||||
icon: 'block'
|
||||
},
|
||||
{
|
||||
id: '085f81b9-651d-47a1-94ec-c4cd02bb08c4',
|
||||
tile: { x: 11, y: 0 },
|
||||
label: 'ATC Tower',
|
||||
description:
|
||||
'<p>The Air Traffic Control Tower is a structure that delivers air and ground control of the aircraft. It ensures safety by guiding and navigating the vehicles and aircraft. It is performed by way of visual signaling, radar, and radio communication in the air and on the ground. The main focus of the tower is to make sure that all aircraft have been assigned to the right place, that passengers aren’t at risk, and that the aircraft will have a suitable passenger boarding bridge allocated on the apron.</p><p><br></p><p>The ATC tower has a control room that serves as a channel between landside (terminal) and airside operations in airports. The control room personnel are tasked with ensuring the security and safety of the passengers as well as ground handling. Usually, a control room has CCTV monitors and air traffic control systems that maintain the order in the terminal and on the apron.</p>',
|
||||
labelHeight: 80,
|
||||
icon: 'block'
|
||||
},
|
||||
{
|
||||
id: 'e4909db0-da09-49a3-8cfd-927ce191c28d',
|
||||
tile: { x: 11, y: -3 },
|
||||
label: 'Aeronautical Fixed Telecommunication Network (AFTN) Systems',
|
||||
description:
|
||||
'<p>AFTN systems handle communication and exchange of data including navigation services. Usually, airports exchange traffic environment messages, safety messages, information about the weather, geographic material, disruptions, etc. They serve as communication between airports and aircraft.</p><p><br></p><p>Software for aeronautical telecommunications stores flight plans and flight information, entered in ICAO format and UTC. The information stored can be used for planning and statistical purposes. For airports, it’s important to understand the aircraft type and its weight to assign it to the right place on the runway. AFTN systems hold the following information:</p><p><br></p><p>- Aircraft registration</p><p>- Runway used</p><p>- Actual time of landing and departure</p><p>- Number of circuits</p><p>- Number and type of approaches</p><p>- New estimates of arrival and departure</p><p>- New flight information</p><p><br></p><p>Air traffic management is performed from an ATC tower.</p>',
|
||||
labelHeight: 80,
|
||||
icon: 'block'
|
||||
},
|
||||
{
|
||||
id: '680a2ad0-839f-4cfc-8b35-24c557fa1e8f',
|
||||
tile: { x: -9, y: -3 },
|
||||
label: 'Passenger facilitation services',
|
||||
description:
|
||||
'<p>include passenger processing (check-in, boarding, border control) and baggage handling (tagging, dropping and handling). They follow passengers to the shuttle buses to carry them to their flights. Arrival operations include boarding control and baggage handling.</p>',
|
||||
labelHeight: 120,
|
||||
icon: 'user'
|
||||
},
|
||||
{
|
||||
id: '8277f135-e833-40b3-8232-0461b86added',
|
||||
tile: { x: -9, y: 3 },
|
||||
label: 'Terminal management systems',
|
||||
description:
|
||||
'<p>Includes maintenance and monitoring of management systems for assets, buildings, electrical grids, environmental systems, and vertical transportation organization. It also facilitates staff communications and management.</p>',
|
||||
labelHeight: 120,
|
||||
icon: 'function-module'
|
||||
},
|
||||
{
|
||||
id: 'ca560fc6-da8b-49cc-b4eb-fdc300ff6aed',
|
||||
tile: { x: -9, y: 7 },
|
||||
label: 'Maintenance and monitoring',
|
||||
labelHeight: 80,
|
||||
icon: 'block'
|
||||
},
|
||||
{
|
||||
id: '8f02ce50-0aa4-4950-b077-c1daf5da023b',
|
||||
tile: { x: -12, y: 7 },
|
||||
label: 'Resource management',
|
||||
labelHeight: 80,
|
||||
icon: 'block'
|
||||
},
|
||||
{
|
||||
id: '308dfac4-fc9a-4985-9b5c-9616e0674ac4',
|
||||
tile: { x: -15, y: 7 },
|
||||
label: 'Staff management',
|
||||
description:
|
||||
'<p>Staff modules provide the necessary information about ongoing processes in the airport, such as data on flights (in ICAO or UTC formats) and other important events to keep responsible staff members updated. Information is distributed through the airport radio system, or displayed on a PC connected via the airport LAN or on mobile devices.</p>',
|
||||
labelHeight: 80,
|
||||
icon: 'block'
|
||||
},
|
||||
{
|
||||
id: '2420494b-312a-4ddf-b8c3-591fa55563cb',
|
||||
tile: { x: -9, y: -8 },
|
||||
label: 'Border control (customs and security services)',
|
||||
description:
|
||||
'<p>In airports, security services usually unite perimeter security, terminal security, and border controls. These services require biometric authentication and integration into government systems to allow a customs officer to view the status of a passenger.</p>',
|
||||
labelHeight: 60,
|
||||
icon: 'block'
|
||||
},
|
||||
{
|
||||
id: '2557a6bd-0c92-4522-b704-f1f6d076be80',
|
||||
tile: { x: -12, y: -8 },
|
||||
label: 'Common use services (self-service check-in systems)',
|
||||
description:
|
||||
'<p>An airport must ensure smooth passenger flow. Various digital self-services, like check-in kiosks or automated self-service gates, make it happen. Self-service options, especially check-in kiosks, remain popular. Worldwide in 2018, passengers used kiosks to check themselves in 88 percent of the time.</p>',
|
||||
labelHeight: 80,
|
||||
icon: 'block'
|
||||
},
|
||||
{
|
||||
id: '7c422bf4-bbd2-4364-ad9b-5250c2877695',
|
||||
tile: { x: -15, y: -8 },
|
||||
label: 'Baggage handling',
|
||||
description:
|
||||
'<p>A passenger must check a bag before it’s loaded on the aircraft. The time the baggage is loaded is displayed and tracked until the destination is reached and the bag is returned to the owners.</p>',
|
||||
labelHeight: 80,
|
||||
icon: 'block'
|
||||
},
|
||||
{
|
||||
id: 'fd5dd3ed-b8de-41ed-a642-b8bd71aaebf8',
|
||||
tile: { x: 5, y: 0 },
|
||||
label: 'Airside operations',
|
||||
description:
|
||||
'<p>Includes systems to handle aircraft landing and navigation, airport traffic management, runway management, and ground handling safety.</p>',
|
||||
labelHeight: 100,
|
||||
icon: 'plane'
|
||||
},
|
||||
{
|
||||
id: 'cb6b557e-01c8-4208-88f8-2076351aa53c',
|
||||
tile: { x: 0, y: -5 },
|
||||
label: 'Invoicing and billing',
|
||||
description:
|
||||
'<p>Each flight an airport handles generates a defined revenue for the airport paid by the airline operating the aircraft. Aeronautical invoicing systems make payment possible for any type and size of aircraft. It accepts payments in cash and credit in multiple currencies. The billing also extends to ATC services.</p><p><br></p><p>Depending on the aircraft type and weight and ground services provided, an airport can calculate the aeronautical fee and issue an invoice with a bill. It is calculated using the following data:</p><p><br></p><p>- Aircraft registration</p><p>- Parking time at the airport</p><p>- Airport point of departure and/or landing</p><p>- Times at the different points of entry or departure</p><p><br></p><p>The data is entered or integrated from ATC. Based on this information, the airport calculates the charges and sends the bills.</p>',
|
||||
labelHeight: 80,
|
||||
icon: 'paymentcard'
|
||||
},
|
||||
{
|
||||
id: '1789808d-6d52-4714-aeea-954f3e4aba66',
|
||||
tile: { x: -2, y: -11 },
|
||||
label: 'ATC Tower Billing',
|
||||
labelHeight: 60,
|
||||
icon: 'block'
|
||||
},
|
||||
{
|
||||
id: '5096a0f1-7548-4cba-8477-7f07e3bdc206',
|
||||
tile: { x: 2, y: -11 },
|
||||
label: 'Non Aeronautical revenue',
|
||||
labelHeight: 60,
|
||||
icon: 'block'
|
||||
},
|
||||
{
|
||||
id: '0e1ecd31-ee64-49b9-a5be-3375bd3cece6',
|
||||
tile: { x: 3, y: 13 },
|
||||
label: 'Automatic Terminal Information Service (ATIS)',
|
||||
description:
|
||||
'<p>Broadcasts the weather reports, the condition of the runway, or other local information for pilots and crews.</p><p><br></p><p>Some airport software vendors offer off-the-shelf solutions to facilitate particular tasks, like maintenance, or airport operations. However, most of them provide integrated systems that comprise modules for several operations.</p>',
|
||||
labelHeight: 100,
|
||||
icon: 'block'
|
||||
},
|
||||
{
|
||||
id: 'b0368dfe-cc6e-405a-9471-0867c4c51d08',
|
||||
tile: { x: 0, y: 13 },
|
||||
label: 'Flight Information Display Systems (FIDS)',
|
||||
description:
|
||||
'<p>Exhibits the status of boarding, gates, aircraft, flight number, and other flight details. A computer controls the screens that are connected to the data management systems and displays up-to-date information about flights in real time. Some airports have a digital FIDS in the form of apps or on their websites. Also, the displays may show other public information such as the weather, news, safety messages, menus, and advertising. Airports can choose the type, languages, and means of entering the information, whether it be manually or loaded from a central database.</p>',
|
||||
labelHeight: 100,
|
||||
icon: 'block'
|
||||
},
|
||||
{
|
||||
id: '9f1417b9-8785-498b-bb4e-4ee9a827e611',
|
||||
tile: { x: -3, y: 13 },
|
||||
label: 'Public address (PA) systems',
|
||||
description:
|
||||
'<p>Informs passengers and airport staff about any changes and processes of importance, for instance, gates, times of arrival, calls, and alerts. Also, information can be communicated to pilots, aircraft staff, crew, etc. PA systems usually include voice messages broadcasted through loudspeakers.</p>',
|
||||
labelHeight: 100,
|
||||
icon: 'block'
|
||||
},
|
||||
{
|
||||
id: '7f9e0f95-b490-4e03-99c7-8df10788d6df',
|
||||
tile: { x: 0, y: 6 },
|
||||
label: 'Information management',
|
||||
description:
|
||||
'<p>This subsystem is responsible for the collection and distribution of daily flight information, storing of seasonal and arrival/departure information, as well as the connection with airlines.</p>',
|
||||
labelHeight: 80,
|
||||
icon: 'queue'
|
||||
},
|
||||
{
|
||||
id: 'bf5b31ed-97fc-4e23-9ccf-107ba9f1bb58',
|
||||
tile: { x: -5, y: 0 },
|
||||
label: 'Landside operations',
|
||||
description:
|
||||
'<p>This subsystem is aimed at serving passengers and maintenance of terminal buildings, parking facilities, and vehicular traffic circular drives. Passenger operations include baggage handling and tagging.</p>',
|
||||
labelHeight: 180,
|
||||
icon: 'office'
|
||||
}
|
||||
],
|
||||
connectors: [
|
||||
{
|
||||
id: 'be41366a-be8e-43f5-a85c-1528cc380b70',
|
||||
color: '#a0b9f8',
|
||||
style: 'SOLID',
|
||||
width: 10,
|
||||
anchors: [
|
||||
{
|
||||
id: '612d1f4e-588e-468e-a819-9e9b5528abe2',
|
||||
ref: { node: '308dfac4-fc9a-4985-9b5c-9616e0674ac4' }
|
||||
},
|
||||
{
|
||||
id: '82ce8f7c-1315-4bae-9966-435a3504de9a',
|
||||
ref: { tile: { x: -15, y: 5 } }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'be678975-abc7-4345-82b3-2e829486fe3c',
|
||||
color: '#a0b9f8',
|
||||
style: 'SOLID',
|
||||
width: 10,
|
||||
anchors: [
|
||||
{
|
||||
id: '4d4868ff-53df-414b-b51f-2de31fd125e6',
|
||||
ref: { tile: { x: -9, y: -5 } }
|
||||
},
|
||||
{
|
||||
id: 'bec7a569-6f1f-46f8-8ddf-b6ace6354f88',
|
||||
ref: { node: '680a2ad0-839f-4cfc-8b35-24c557fa1e8f' }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'b155cdd6-da92-4553-b295-3c1a3262169d',
|
||||
color: '#a0b9f8',
|
||||
style: 'SOLID',
|
||||
width: 10,
|
||||
anchors: [
|
||||
{
|
||||
id: '80a0fa48-f4b2-46e7-8ede-0c725a279b52',
|
||||
ref: { node: 'ca560fc6-da8b-49cc-b4eb-fdc300ff6aed' }
|
||||
},
|
||||
{
|
||||
id: '6681a140-8535-4c14-ab87-656476c81c06',
|
||||
ref: { tile: { x: -9, y: 5 } }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '1b3bcf33-ca67-461b-8f80-a70eba87e6ab',
|
||||
color: '#a0b9f8',
|
||||
style: 'SOLID',
|
||||
width: 10,
|
||||
anchors: [
|
||||
{
|
||||
id: 'b1d4cc02-056d-4a06-bb93-932c8630c7a0',
|
||||
ref: { tile: { x: -9, y: -5 } }
|
||||
},
|
||||
{
|
||||
id: '72eb7689-fa58-4ac1-88a9-e9208f11928a',
|
||||
ref: { tile: { x: -12, y: -5 } }
|
||||
},
|
||||
{
|
||||
id: 'e7b2facb-c137-49f1-acdd-095739b51e05',
|
||||
ref: { tile: { x: -15, y: -5 } }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '87b538b5-e908-495d-bb12-05f068f1b6ee',
|
||||
color: '#a0b9f8',
|
||||
style: 'SOLID',
|
||||
width: 10,
|
||||
anchors: [
|
||||
{
|
||||
id: 'ca0d0583-8d87-4365-ae77-e420ae6a25cf',
|
||||
ref: { node: '2557a6bd-0c92-4522-b704-f1f6d076be80' }
|
||||
},
|
||||
{
|
||||
id: 'a5a1520b-5914-414e-82aa-5ebdf3fbda16',
|
||||
ref: { tile: { x: -12, y: -5 } }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'eb44bfe6-66c3-492c-949c-3f8faba4c455',
|
||||
color: '#a0b9f8',
|
||||
style: 'SOLID',
|
||||
width: 10,
|
||||
anchors: [
|
||||
{
|
||||
id: '54d16963-57ad-4252-ae54-42417c4116a5',
|
||||
ref: { node: '8277f135-e833-40b3-8232-0461b86added' }
|
||||
},
|
||||
{
|
||||
id: '24eacb98-43cf-4e4c-8e4e-453247edfafc',
|
||||
ref: { tile: { x: -9, y: 5 } }
|
||||
},
|
||||
{
|
||||
id: 'e7d4f48d-68a1-48be-9488-173955730d17',
|
||||
ref: { tile: { x: -12, y: 5 } }
|
||||
},
|
||||
{
|
||||
id: '95c2eca2-c3ce-4fb3-abac-241ccd7a2f56',
|
||||
ref: { tile: { x: -15, y: 5 } }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '110033ad-b542-4415-bb95-6bad8de33198',
|
||||
color: '#a0b9f8',
|
||||
style: 'SOLID',
|
||||
width: 10,
|
||||
anchors: [
|
||||
{
|
||||
id: '9ccd7127-1ed6-4ac0-9fcd-31e17c306987',
|
||||
ref: { node: '8f02ce50-0aa4-4950-b077-c1daf5da023b' }
|
||||
},
|
||||
{
|
||||
id: '4c4f7d59-2b06-46cc-8eed-f3cd2b2af9b3',
|
||||
ref: { tile: { x: -12, y: 5 } }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'f7b5f65c-0503-479b-983d-6b3be7d4355b',
|
||||
color: '#bbadfb',
|
||||
style: 'SOLID',
|
||||
width: 10,
|
||||
anchors: [
|
||||
{
|
||||
id: 'fbeb8d12-597a-4654-92f4-41006b57beb8',
|
||||
ref: { node: '815b0205-516c-48a9-ac34-2007bb155d75' }
|
||||
},
|
||||
{
|
||||
id: '6f88cf82-23d9-4d95-9823-3c76b1956c9c',
|
||||
ref: { tile: { x: 9, y: 3 } }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '1c7ccb81-9f31-4cc3-90ee-2f92c4be1e8f',
|
||||
color: '#bbadfb',
|
||||
style: 'SOLID',
|
||||
width: 10,
|
||||
anchors: [
|
||||
{
|
||||
id: '199f843d-f8ce-4a26-b71a-1b4f799109a9',
|
||||
ref: { node: 'e4909db0-da09-49a3-8cfd-927ce191c28d' }
|
||||
},
|
||||
{
|
||||
id: 'd2d63da6-a60a-4569-af9f-5d497013bd8e',
|
||||
ref: { tile: { x: 9, y: -3 } }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '48435cd4-297f-4078-b5d4-79209babaa0f',
|
||||
color: '#bbadfb',
|
||||
style: 'SOLID',
|
||||
width: 10,
|
||||
anchors: [
|
||||
{
|
||||
id: '9316ac7f-7913-489e-8516-fb74eb9aed9c',
|
||||
ref: { tile: { x: 9, y: 3 } }
|
||||
},
|
||||
{
|
||||
id: 'd53e7165-c645-4e8f-b4c7-84ebc4369f4a',
|
||||
ref: { tile: { x: 9, y: 0 } }
|
||||
},
|
||||
{
|
||||
id: '01860aaf-3b57-45f2-b833-15fcafdd4abb',
|
||||
ref: { tile: { x: 9, y: -3 } }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '1a58a13e-d136-4c99-a111-143097873147',
|
||||
color: '#bbadfb',
|
||||
style: 'SOLID',
|
||||
width: 10,
|
||||
anchors: [
|
||||
{
|
||||
id: 'fe873a58-afdc-4c8b-b89c-b01332e6264b',
|
||||
ref: { node: '085f81b9-651d-47a1-94ec-c4cd02bb08c4' }
|
||||
},
|
||||
{
|
||||
id: '93286249-0ee8-4a0d-ae09-c17736b0535f',
|
||||
ref: { tile: { x: 9, y: 0 } }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '3abc77ea-357b-4671-95a7-6b17e0219032',
|
||||
color: '#b3e5e3',
|
||||
style: 'SOLID',
|
||||
width: 10,
|
||||
anchors: [
|
||||
{
|
||||
id: '0077477a-8978-48dc-9051-51f9322519bc',
|
||||
ref: { node: '9f1417b9-8785-498b-bb4e-4ee9a827e611' }
|
||||
},
|
||||
{
|
||||
id: 'e69310e4-f794-4771-aa5f-39291aa090ef',
|
||||
ref: { tile: { x: -3, y: 11 } }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '75200d73-06f4-4403-923f-32f0041c9701',
|
||||
color: '#b3e5e3',
|
||||
style: 'SOLID',
|
||||
width: 10,
|
||||
anchors: [
|
||||
{
|
||||
id: '9f5616ae-e638-40af-ba0b-b25f6adae2f7',
|
||||
ref: { node: '0e1ecd31-ee64-49b9-a5be-3375bd3cece6' }
|
||||
},
|
||||
{
|
||||
id: 'e86b8651-a385-4a96-90cc-2ed2b6daa9f2',
|
||||
ref: { tile: { x: 3, y: 11 } }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '4a7ccc74-f296-4d0b-be11-8117c990795d',
|
||||
color: '#b3e5e3',
|
||||
style: 'SOLID',
|
||||
width: 10,
|
||||
anchors: [
|
||||
{
|
||||
id: '7b721b82-220d-45f6-87d4-479c1b847a60',
|
||||
ref: { node: 'b6cf011d-0bc2-474d-8a4b-022d24ecc5d5' }
|
||||
},
|
||||
{
|
||||
id: '1908c227-8066-4c8d-ba55-d5822c224d52',
|
||||
ref: { node: '7f9e0f95-b490-4e03-99c7-8df10788d6df' }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '9f6bc159-507f-4b56-9171-d95f00490230',
|
||||
color: '#a0b9f8',
|
||||
style: 'SOLID',
|
||||
width: 10,
|
||||
anchors: [
|
||||
{
|
||||
id: 'b41f10e5-5d29-4cee-8102-0005cf146300',
|
||||
ref: { node: '8277f135-e833-40b3-8232-0461b86added' }
|
||||
},
|
||||
{
|
||||
id: '3f5bfd1b-f8b3-4a00-976e-6bc4382749ee',
|
||||
ref: { tile: { x: -9, y: 0 } }
|
||||
},
|
||||
{
|
||||
id: '3792f3db-edd1-4d43-9f16-0be811687c85',
|
||||
ref: { node: 'bf5b31ed-97fc-4e23-9ccf-107ba9f1bb58' }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '5b47caae-fbdd-4d1b-8cc5-0068536fb8b2',
|
||||
color: '#a0b9f8',
|
||||
style: 'SOLID',
|
||||
width: 10,
|
||||
anchors: [
|
||||
{
|
||||
id: '16ba31e6-a713-403d-b23e-4c40b19e7d66',
|
||||
ref: { node: '680a2ad0-839f-4cfc-8b35-24c557fa1e8f' }
|
||||
},
|
||||
{
|
||||
id: '59675d93-8ad7-4abd-85d4-cfe0004384c1',
|
||||
ref: { tile: { x: -9, y: 0 } }
|
||||
},
|
||||
{
|
||||
id: '1ab79aec-2587-47eb-9b2f-42e9eb44fe77',
|
||||
ref: { node: 'bf5b31ed-97fc-4e23-9ccf-107ba9f1bb58' }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'a6c63183-ce9a-4d54-9487-569f2a8a8add',
|
||||
color: '#a0b9f8',
|
||||
style: 'SOLID',
|
||||
width: 10,
|
||||
anchors: [
|
||||
{
|
||||
id: '8a2d4f72-5f62-4531-b2f7-b0eed5e533a5',
|
||||
ref: { node: 'bf5b31ed-97fc-4e23-9ccf-107ba9f1bb58' }
|
||||
},
|
||||
{
|
||||
id: 'ab054b9f-7574-4195-9465-c4a127b8e03e',
|
||||
ref: { node: 'b6cf011d-0bc2-474d-8a4b-022d24ecc5d5' }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'afa7915a-c1f6-4f40-87c0-ace0ecacf5bb',
|
||||
color: '#b3e5e3',
|
||||
style: 'SOLID',
|
||||
width: 10,
|
||||
anchors: [
|
||||
{
|
||||
id: '2b18ab38-7f76-4e63-ae79-ca6cfaa2ee49',
|
||||
ref: { tile: { x: -3, y: 11 } }
|
||||
},
|
||||
{
|
||||
id: 'a926bd20-a16e-4349-8e31-bb4109ab0387',
|
||||
ref: { tile: { x: 3, y: 11 } }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'c1263273-a57c-4e0b-92b8-04c21b805b29',
|
||||
color: '#b3e5e3',
|
||||
style: 'SOLID',
|
||||
width: 10,
|
||||
anchors: [
|
||||
{
|
||||
id: 'b1ee48ad-70f7-44ff-9d84-b3893ce3dff1',
|
||||
ref: { node: 'b0368dfe-cc6e-405a-9471-0867c4c51d08' }
|
||||
},
|
||||
{
|
||||
id: 'e6148182-5a09-4e0a-85f4-1389dfb33f8b',
|
||||
ref: { tile: { x: 0, y: 11 } }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '5706f58e-6b29-4ec7-aa0b-c59ce6198870',
|
||||
color: '#b3e5e3',
|
||||
style: 'SOLID',
|
||||
width: 10,
|
||||
anchors: [
|
||||
{
|
||||
id: '5a8e0e07-7e0c-40eb-b50c-bba2a2403c2f',
|
||||
ref: { tile: { x: 0, y: 11 } }
|
||||
},
|
||||
{
|
||||
id: 'b3611f56-35c2-4cc3-8dc6-461c182937e5',
|
||||
ref: { node: '7f9e0f95-b490-4e03-99c7-8df10788d6df' }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '17547863-f8f3-4653-92ec-f76d3ec0835f',
|
||||
color: '#bbadfb',
|
||||
style: 'SOLID',
|
||||
width: 10,
|
||||
anchors: [
|
||||
{
|
||||
id: '0445d65f-05b4-47cf-9163-dd0d6eefa4af',
|
||||
ref: { node: 'b6cf011d-0bc2-474d-8a4b-022d24ecc5d5' }
|
||||
},
|
||||
{
|
||||
id: 'e77a4942-1625-4893-a4f5-634dbc528321',
|
||||
ref: { node: 'fd5dd3ed-b8de-41ed-a642-b8bd71aaebf8' }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '2656820b-e1fb-41fd-980f-7b94d6e9906e',
|
||||
color: '#bbadfb',
|
||||
style: 'SOLID',
|
||||
width: 10,
|
||||
anchors: [
|
||||
{
|
||||
id: '2717739f-a2d4-44cf-bc45-5d8ae8bb67dd',
|
||||
ref: { node: 'fd5dd3ed-b8de-41ed-a642-b8bd71aaebf8' }
|
||||
},
|
||||
{
|
||||
id: 'b0399a5a-3bc3-4864-aa1b-c2194437f644',
|
||||
ref: { tile: { x: 9, y: 0 } }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'e9f0546e-d2f3-4ae3-b6dd-55d3cf774093',
|
||||
color: '#a8dc9d',
|
||||
style: 'SOLID',
|
||||
width: 10,
|
||||
anchors: [
|
||||
{
|
||||
id: '7cca17bf-700b-4192-be94-7b347b73a77a',
|
||||
ref: { node: '1789808d-6d52-4714-aeea-954f3e4aba66' }
|
||||
},
|
||||
{
|
||||
id: '6e8d5fb2-b995-4e0e-9465-b664ea793f50',
|
||||
ref: { tile: { x: -2, y: -9 } }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '6dce44fd-0fb5-4a08-9daa-1716313c59d8',
|
||||
color: '#a8dc9d',
|
||||
style: 'SOLID',
|
||||
width: 10,
|
||||
anchors: [
|
||||
{
|
||||
id: '5a3751f2-bd9f-4ddc-808a-f0f23a2eecd8',
|
||||
ref: { node: '5096a0f1-7548-4cba-8477-7f07e3bdc206' }
|
||||
},
|
||||
{
|
||||
id: '0f6673bf-b5c4-4e1b-b3aa-bcdca9f24e81',
|
||||
ref: { tile: { x: 2, y: -9 } }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '725419a0-6ca4-4d1b-b6a4-a7ae7feabcb0',
|
||||
color: '#a8dc9d',
|
||||
style: 'SOLID',
|
||||
width: 10,
|
||||
anchors: [
|
||||
{
|
||||
id: 'dfdda749-2f06-4a32-903e-35e336d3842d',
|
||||
ref: { node: 'b6cf011d-0bc2-474d-8a4b-022d24ecc5d5' }
|
||||
},
|
||||
{
|
||||
id: '354700b6-6b10-49cb-a943-f2eeb8e4741b',
|
||||
ref: { node: 'cb6b557e-01c8-4208-88f8-2076351aa53c' }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '41431713-1647-4b47-b258-9d3ec4665b77',
|
||||
color: '#a8dc9d',
|
||||
style: 'SOLID',
|
||||
width: 10,
|
||||
anchors: [
|
||||
{
|
||||
id: 'f579ae26-9023-4041-ba62-33da290ca83f',
|
||||
ref: { tile: { x: 2, y: -9 } }
|
||||
},
|
||||
{
|
||||
id: '2a123a41-1481-4c87-9aca-89a819ee4478',
|
||||
ref: { tile: { x: -2, y: -9 } }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '3410464e-d12b-4cf2-bdc4-9eb338ea1c1a',
|
||||
color: '#a8dc9d',
|
||||
style: 'SOLID',
|
||||
width: 10,
|
||||
anchors: [
|
||||
{
|
||||
id: '832bad08-bd8a-4a57-b849-016b0482138c',
|
||||
ref: { node: 'cb6b557e-01c8-4208-88f8-2076351aa53c' }
|
||||
},
|
||||
{
|
||||
id: 'fe2422cd-1ced-4f4a-a79c-a9f052977cc5',
|
||||
ref: { tile: { x: 0, y: -9 } }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '8b95c19e-e5dd-4190-a93a-93fdec1bd37f',
|
||||
color: '#a0b9f8',
|
||||
style: 'SOLID',
|
||||
width: 10,
|
||||
anchors: [
|
||||
{
|
||||
id: 'd1281ea9-0f2c-4e24-a1e9-d0c8a67805ba',
|
||||
ref: { tile: { x: -15, y: -5 } }
|
||||
},
|
||||
{
|
||||
id: 'c0a7d637-79aa-4400-ab70-2b874e865ad2',
|
||||
ref: { node: '7c422bf4-bbd2-4364-ad9b-5250c2877695' }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '7013bdc5-31f0-4e64-8604-95eba5e1ca8e',
|
||||
color: '#a0b9f8',
|
||||
style: 'SOLID',
|
||||
width: 10,
|
||||
anchors: [
|
||||
{
|
||||
id: 'e1d4e165-649f-48a7-93aa-2c8181289901',
|
||||
ref: { tile: { x: -9, y: -5 } }
|
||||
},
|
||||
{
|
||||
id: '1f3e5712-a5be-4d2b-ba86-a0ec2e4ab439',
|
||||
ref: { node: '2420494b-312a-4ddf-b8c3-591fa55563cb' }
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
textBoxes: [
|
||||
{
|
||||
id: '29e365cf-5f40-4355-8a2d-db45a509b140',
|
||||
orientation: 'Y',
|
||||
fontSize: 0.6,
|
||||
tile: { x: -2, y: -1 },
|
||||
text: 'AODB'
|
||||
},
|
||||
{
|
||||
id: '40964f5b-7ebd-4c0d-9666-f8ff2de65dc8',
|
||||
orientation: 'Y',
|
||||
fontSize: 0.6,
|
||||
tile: { x: 8, y: -1 },
|
||||
text: 'Airside operations'
|
||||
},
|
||||
{
|
||||
id: 'c6d16ec6-7110-4082-9370-44e127984b90',
|
||||
orientation: 'X',
|
||||
fontSize: 0.6,
|
||||
tile: { x: -3, y: -13 },
|
||||
text: 'Invoicing & Billing'
|
||||
},
|
||||
{
|
||||
id: '57156c5e-9b82-4652-852d-422014228b74',
|
||||
orientation: 'X',
|
||||
fontSize: 0.6,
|
||||
tile: { x: -16, y: -10 },
|
||||
text: 'Passenger facilitation'
|
||||
},
|
||||
{
|
||||
id: '92637173-542c-4d25-addf-ab50eda7a573',
|
||||
orientation: 'X',
|
||||
fontSize: 0.6,
|
||||
tile: { x: -16, y: 4 },
|
||||
text: 'Terminal management'
|
||||
},
|
||||
{
|
||||
id: 'e555aad0-9e21-48db-9d71-53125c33c214',
|
||||
orientation: 'X',
|
||||
fontSize: 0.6,
|
||||
tile: { x: -4, y: 10 },
|
||||
text: 'Information management'
|
||||
}
|
||||
],
|
||||
rectangles: [
|
||||
{
|
||||
id: '82ef2751-9ea7-40fc-b3f5-3ea8ce1e1d67',
|
||||
color: '#fad6ac',
|
||||
from: { x: -1, y: 1 },
|
||||
to: { x: 1, y: -1 }
|
||||
},
|
||||
{
|
||||
id: '7d730e86-7884-400d-857a-8af7f22a9937',
|
||||
color: '#a0b9f8',
|
||||
from: { x: -8, y: -5 },
|
||||
to: { x: -16, y: -9 }
|
||||
},
|
||||
{
|
||||
id: 'f41434d1-bbe6-4d6d-a13d-7c9ad52e8e62',
|
||||
color: '#a0b9f8',
|
||||
from: { x: -16, y: 8 },
|
||||
to: { x: -8, y: 5 }
|
||||
},
|
||||
{
|
||||
id: '4ffe42ed-9cd0-48a9-93a0-6099ab53a146',
|
||||
color: '#a8dc9d',
|
||||
from: { x: -3, y: -9 },
|
||||
to: { x: 3, y: -12 }
|
||||
},
|
||||
{
|
||||
id: 'dc93e93b-2c6c-43a1-bb82-ad93c04f7707',
|
||||
color: '#bbadfb',
|
||||
from: { x: 9, y: 4 },
|
||||
to: { x: 12, y: -4 }
|
||||
},
|
||||
{
|
||||
id: 'eec5c861-6192-4eb6-a378-7554bda4d9a7',
|
||||
color: '#b3e5e3',
|
||||
from: { x: -4, y: 14 },
|
||||
to: { x: 4, y: 11 }
|
||||
}
|
||||
]
|
||||
};
|
||||
14
src/fixtures/icons.ts
Normal file
14
src/fixtures/icons.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Model } from 'src/types';
|
||||
|
||||
export const icons: Model['icons'] = [
|
||||
{
|
||||
id: 'icon1',
|
||||
name: 'Icon1',
|
||||
url: 'https://isoflow.io/static/assets/icons/networking/server.svg'
|
||||
},
|
||||
{
|
||||
id: 'icon2',
|
||||
name: 'Icon2',
|
||||
url: 'https://isoflow.io/static/assets/icons/networking/block.svg'
|
||||
}
|
||||
];
|
||||
13
src/fixtures/model.ts
Normal file
13
src/fixtures/model.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Model } from 'src/types';
|
||||
import { icons } from './icons';
|
||||
import { modelItems } from './modelItems';
|
||||
import { views } from './views';
|
||||
|
||||
export const model: Model = {
|
||||
title: 'TestModel',
|
||||
description: 'TestModelDescription',
|
||||
version: '1.0.0',
|
||||
icons,
|
||||
items: modelItems,
|
||||
views
|
||||
};
|
||||
20
src/fixtures/modelItems.ts
Normal file
20
src/fixtures/modelItems.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Model } from 'src/types';
|
||||
|
||||
export const modelItems: Model['items'] = [
|
||||
{
|
||||
id: 'node1',
|
||||
name: 'Node1',
|
||||
icon: 'icon1',
|
||||
description: 'Node1Description'
|
||||
},
|
||||
{
|
||||
id: 'node2',
|
||||
name: 'Node2',
|
||||
icon: 'icon2'
|
||||
},
|
||||
{
|
||||
id: 'node3',
|
||||
name: 'Node3',
|
||||
icon: 'icon1'
|
||||
}
|
||||
];
|
||||
@@ -1,61 +0,0 @@
|
||||
import { SceneInput } from 'src/types';
|
||||
|
||||
export const scene: SceneInput = {
|
||||
title: 'TestScene',
|
||||
icons: [
|
||||
{
|
||||
id: 'icon1',
|
||||
name: 'Icon1',
|
||||
url: 'https://isoflow.io/static/assets/icons/networking/server.svg'
|
||||
},
|
||||
{
|
||||
id: 'icon2',
|
||||
name: 'Icon2',
|
||||
url: 'https://isoflow.io/static/assets/icons/networking/block.svg'
|
||||
}
|
||||
],
|
||||
nodes: [
|
||||
{
|
||||
id: 'node1',
|
||||
label: 'Node1',
|
||||
icon: 'icon1',
|
||||
tile: {
|
||||
x: 0,
|
||||
y: 0
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'node2',
|
||||
label: 'Node2',
|
||||
icon: 'icon2',
|
||||
tile: {
|
||||
x: 1,
|
||||
y: 1
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'node3',
|
||||
label: 'Node3',
|
||||
icon: 'icon1',
|
||||
tile: {
|
||||
x: 2,
|
||||
y: 2
|
||||
}
|
||||
}
|
||||
],
|
||||
connectors: [
|
||||
{
|
||||
id: 'connector1',
|
||||
anchors: [{ ref: { node: 'node1' } }, { ref: { node: 'node2' } }]
|
||||
},
|
||||
{
|
||||
id: 'connector2',
|
||||
anchors: [
|
||||
{ id: 'anchor1', ref: { node: 'node2' } },
|
||||
{ ref: { node: 'node3' } }
|
||||
]
|
||||
}
|
||||
],
|
||||
textBoxes: [],
|
||||
rectangles: [{ id: 'rectangle1', from: { x: 0, y: 0 }, to: { x: 2, y: 2 } }]
|
||||
};
|
||||
29
src/fixtures/views.ts
Normal file
29
src/fixtures/views.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Model } from 'src/types';
|
||||
|
||||
export const views: Model['views'] = [
|
||||
{
|
||||
id: 'View1',
|
||||
name: 'View1',
|
||||
description: 'View1Description',
|
||||
items: [],
|
||||
rectangles: [
|
||||
{ id: 'rectangle1', from: { x: 0, y: 0 }, to: { x: 2, y: 2 } }
|
||||
],
|
||||
connectors: [
|
||||
{
|
||||
id: 'connector1',
|
||||
anchors: [
|
||||
{ id: 'anch1-1', ref: { item: 'node1' } },
|
||||
{ id: 'anch1-2', ref: { item: 'node2' } }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'connector2',
|
||||
anchors: [
|
||||
{ id: 'anch2-1', ref: { item: 'node2' } },
|
||||
{ id: 'anch2-2', ref: { item: 'node3' } }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
3
src/global.d.ts
vendored
3
src/global.d.ts
vendored
@@ -1,4 +1,4 @@
|
||||
import { Size, Coords, SceneInput } from 'src/types';
|
||||
import { Size, Coords } from 'src/types';
|
||||
|
||||
declare global {
|
||||
let PACKAGE_VERSION: string;
|
||||
@@ -8,7 +8,6 @@ declare global {
|
||||
Isoflow: {
|
||||
getUnprojectedBounds: () => Size & Coords;
|
||||
fitToView: () => void;
|
||||
setScene: (scene: SceneInput) => void;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useSceneStore } from 'src/stores/sceneStore';
|
||||
import { getItemById } from 'src/utils';
|
||||
import { Connector, SceneConnector } from 'src/types';
|
||||
import { CONNECTOR_DEFAULTS } from 'src/config';
|
||||
import { getItemByIdOrThrow } from 'src/utils';
|
||||
import { useScene } from 'src/hooks/useScene';
|
||||
|
||||
export const useConnector = (id: string) => {
|
||||
const connectors = useSceneStore((state) => {
|
||||
return state.connectors;
|
||||
});
|
||||
export const useConnector = (
|
||||
id: string
|
||||
): Required<Connector> & SceneConnector => {
|
||||
const { connectors } = useScene();
|
||||
|
||||
const node = useMemo(() => {
|
||||
return getItemById(connectors, id).item;
|
||||
const connector = useMemo(() => {
|
||||
const con = getItemByIdOrThrow(connectors ?? [], id).value;
|
||||
|
||||
return {
|
||||
...CONNECTOR_DEFAULTS,
|
||||
...con
|
||||
};
|
||||
}, [connectors, id]);
|
||||
|
||||
return node;
|
||||
return connector;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useSceneStore } from 'src/stores/sceneStore';
|
||||
import { useUiStateStore } from 'src/stores/uiStateStore';
|
||||
import { Size, Coords } from 'src/types';
|
||||
import {
|
||||
@@ -7,28 +6,17 @@ import {
|
||||
getBoundingBoxSize,
|
||||
sortByPosition,
|
||||
clamp,
|
||||
getAnchorTile,
|
||||
getAllAnchors,
|
||||
getTilePosition,
|
||||
CoordsUtils
|
||||
} from 'src/utils';
|
||||
import { MAX_ZOOM } from 'src/config';
|
||||
import { useScene } from 'src/hooks/useScene';
|
||||
import { useResizeObserver } from './useResizeObserver';
|
||||
|
||||
const BOUNDING_BOX_PADDING = 1;
|
||||
|
||||
export const useDiagramUtils = () => {
|
||||
const scene = useSceneStore(
|
||||
({ nodes, rectangles, connectors, icons, textBoxes }) => {
|
||||
return {
|
||||
nodes,
|
||||
rectangles,
|
||||
connectors,
|
||||
icons,
|
||||
textBoxes
|
||||
};
|
||||
}
|
||||
);
|
||||
const scene = useScene();
|
||||
const rendererEl = useUiStateStore((state) => {
|
||||
return state.rendererEl;
|
||||
});
|
||||
@@ -38,50 +26,48 @@ export const useDiagramUtils = () => {
|
||||
});
|
||||
|
||||
const getProjectBounds = useCallback((): Coords[] => {
|
||||
const items = [
|
||||
...scene.nodes,
|
||||
...scene.connectors,
|
||||
...scene.rectangles,
|
||||
...scene.textBoxes
|
||||
];
|
||||
const itemTiles = scene.items.map((item) => {
|
||||
return item.tile;
|
||||
});
|
||||
|
||||
let tiles = items.reduce<Coords[]>((acc, item) => {
|
||||
switch (item.type) {
|
||||
case 'NODE':
|
||||
return [...acc, item.tile];
|
||||
case 'CONNECTOR':
|
||||
return [
|
||||
...acc,
|
||||
...item.anchors.map((anchor) => {
|
||||
return getAnchorTile(
|
||||
anchor,
|
||||
scene.nodes,
|
||||
getAllAnchors(scene.connectors)
|
||||
);
|
||||
})
|
||||
];
|
||||
case 'RECTANGLE':
|
||||
return [...acc, item.from, item.to];
|
||||
case 'TEXTBOX':
|
||||
return [
|
||||
...acc,
|
||||
item.tile,
|
||||
CoordsUtils.add(item.tile, {
|
||||
x: item.size.width,
|
||||
y: item.size.height
|
||||
})
|
||||
];
|
||||
default:
|
||||
return acc;
|
||||
}
|
||||
const connectorTiles = scene.connectors.reduce<Coords[]>(
|
||||
(acc, connector) => {
|
||||
return [...acc, ...connector.path.tiles];
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const rectangleTiles = scene.rectangles.reduce<Coords[]>(
|
||||
(acc, rectangle) => {
|
||||
return [...acc, rectangle.from, rectangle.to];
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const textBoxTiles = scene.textBoxes.reduce<Coords[]>((acc, textBox) => {
|
||||
return [
|
||||
...acc,
|
||||
textBox.tile,
|
||||
CoordsUtils.add(textBox.tile, {
|
||||
x: textBox.size.width,
|
||||
y: textBox.size.height
|
||||
})
|
||||
];
|
||||
}, []);
|
||||
|
||||
if (tiles.length === 0) {
|
||||
let allTiles = [
|
||||
...itemTiles,
|
||||
...connectorTiles,
|
||||
...rectangleTiles,
|
||||
...textBoxTiles
|
||||
];
|
||||
|
||||
if (allTiles.length === 0) {
|
||||
const centerTile = CoordsUtils.zero();
|
||||
tiles = [centerTile, centerTile, centerTile, centerTile];
|
||||
allTiles = [centerTile, centerTile, centerTile, centerTile];
|
||||
}
|
||||
|
||||
const corners = getBoundingBox(tiles, {
|
||||
const corners = getBoundingBox(allTiles, {
|
||||
x: BOUNDING_BOX_PADDING,
|
||||
y: BOUNDING_BOX_PADDING
|
||||
});
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import React, { useMemo, useEffect } from 'react';
|
||||
import { useSceneStore } from 'src/stores/sceneStore';
|
||||
import { getItemById } from 'src/utils';
|
||||
import { useModelStore } from 'src/stores/modelStore';
|
||||
import { getItemByIdOrThrow } from 'src/utils';
|
||||
import { IsometricIcon } from 'src/components/SceneLayers/Nodes/Node/IconTypes/IsometricIcon';
|
||||
import { NonIsometricIcon } from 'src/components/SceneLayers/Nodes/Node/IconTypes/NonIsometricIcon';
|
||||
import { DEFAULT_ICON } from 'src/config';
|
||||
|
||||
export const useIcon = (id: string) => {
|
||||
export const useIcon = (id: string | undefined) => {
|
||||
const [hasLoaded, setHasLoaded] = React.useState(false);
|
||||
const icons = useSceneStore((state) => {
|
||||
const icons = useModelStore((state) => {
|
||||
return state.icons;
|
||||
});
|
||||
|
||||
const icon = useMemo(() => {
|
||||
return getItemById(icons, id).item;
|
||||
if (!id) return DEFAULT_ICON;
|
||||
|
||||
return getItemByIdOrThrow(icons, id).value;
|
||||
}, [icons, id]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useMemo, useCallback } from 'react';
|
||||
import { IconCollectionStateWithIcons, IconCollectionState } from 'src/types';
|
||||
import { useUiStateStore } from 'src/stores/uiStateStore';
|
||||
import { useSceneStore } from 'src/stores/sceneStore';
|
||||
import { useModelStore } from 'src/stores/modelStore';
|
||||
import { categoriseIcons } from 'src/utils';
|
||||
|
||||
export const useIconCategories = () => {
|
||||
const icons = useSceneStore((state) => {
|
||||
const icons = useModelStore((state) => {
|
||||
return state.icons;
|
||||
});
|
||||
const iconCategoriesState = useUiStateStore((state) => {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
import { useSceneStore } from 'src/stores/sceneStore';
|
||||
import { useModelStore } from 'src/stores/modelStore';
|
||||
import { Icon } from 'src/types';
|
||||
|
||||
export const useIconFiltering = () => {
|
||||
const [filter, setFilter] = useState<string>('');
|
||||
|
||||
const icons = useSceneStore((state) => {
|
||||
const icons = useModelStore((state) => {
|
||||
return state.icons;
|
||||
});
|
||||
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import { useMemo } from 'react';
|
||||
import {
|
||||
Coords,
|
||||
TileOriginEnum,
|
||||
Size,
|
||||
ProjectionOrientationEnum
|
||||
} from 'src/types';
|
||||
import { Coords, Size, ProjectionOrientationEnum } from 'src/types';
|
||||
import {
|
||||
getBoundingBox,
|
||||
getIsoProjectionCss,
|
||||
@@ -48,7 +43,7 @@ export const useIsoProjection = ({
|
||||
const position = useMemo(() => {
|
||||
const pos = getTilePosition({
|
||||
tile: origin,
|
||||
origin: orientation === 'Y' ? TileOriginEnum.TOP : TileOriginEnum.LEFT
|
||||
origin: orientation === 'Y' ? 'TOP' : 'LEFT'
|
||||
});
|
||||
|
||||
return pos;
|
||||
|
||||
16
src/hooks/useModelItem.ts
Normal file
16
src/hooks/useModelItem.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { useMemo } from 'react';
|
||||
import { ModelItem } from 'src/types';
|
||||
import { useModelStore } from 'src/stores/modelStore';
|
||||
import { getItemByIdOrThrow } from 'src/utils';
|
||||
|
||||
export const useModelItem = (id: string): ModelItem => {
|
||||
const model = useModelStore((state) => {
|
||||
return state;
|
||||
});
|
||||
|
||||
const modelItem = useMemo(() => {
|
||||
return getItemByIdOrThrow(model.items, id).value;
|
||||
}, [id, model.items]);
|
||||
|
||||
return modelItem;
|
||||
};
|
||||
@@ -1,15 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useSceneStore } from 'src/stores/sceneStore';
|
||||
import { getItemById } from 'src/utils';
|
||||
|
||||
export const useNode = (id: string) => {
|
||||
const nodes = useSceneStore((state) => {
|
||||
return state.nodes;
|
||||
});
|
||||
|
||||
const node = useMemo(() => {
|
||||
return getItemById(nodes, id).item;
|
||||
}, [nodes, id]);
|
||||
|
||||
return node;
|
||||
};
|
||||
@@ -1,15 +1,17 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useSceneStore } from 'src/stores/sceneStore';
|
||||
import { getItemById } from 'src/utils';
|
||||
import { getItemByIdOrThrow } from 'src/utils';
|
||||
import { useScene } from 'src/hooks/useScene';
|
||||
import { RECTANGLE_DEFAULTS } from 'src/config';
|
||||
|
||||
export const useRectangle = (id: string) => {
|
||||
const rectangles = useSceneStore((state) => {
|
||||
return state.rectangles;
|
||||
});
|
||||
const { rectangles } = useScene();
|
||||
|
||||
const rectangle = useMemo(() => {
|
||||
return getItemById(rectangles, id).item;
|
||||
return getItemByIdOrThrow(rectangles, id).value;
|
||||
}, [rectangles, id]);
|
||||
|
||||
return rectangle;
|
||||
return {
|
||||
...RECTANGLE_DEFAULTS,
|
||||
...rectangle
|
||||
};
|
||||
};
|
||||
|
||||
251
src/hooks/useScene.ts
Normal file
251
src/hooks/useScene.ts
Normal file
@@ -0,0 +1,251 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { ModelItem, ViewItem, Connector, TextBox, Rectangle } from 'src/types';
|
||||
import { useUiStateStore } from 'src/stores/uiStateStore';
|
||||
import { useModelStore } from 'src/stores/modelStore';
|
||||
import { useSceneStore } from 'src/stores/sceneStore';
|
||||
import * as reducers from 'src/stores/reducers';
|
||||
import type { State } from 'src/stores/reducers/types';
|
||||
import { getItemByIdOrThrow } from 'src/utils';
|
||||
|
||||
export const useScene = () => {
|
||||
const model = useModelStore((state) => {
|
||||
return state;
|
||||
});
|
||||
|
||||
const scene = useSceneStore((state) => {
|
||||
return state;
|
||||
});
|
||||
|
||||
const currentViewId = useUiStateStore((state) => {
|
||||
return state.view;
|
||||
});
|
||||
|
||||
const currentView = useMemo(() => {
|
||||
return getItemByIdOrThrow(model.views, currentViewId).value;
|
||||
}, [currentViewId, model.views]);
|
||||
|
||||
const items = useMemo(() => {
|
||||
return currentView.items ?? [];
|
||||
}, [currentView.items]);
|
||||
|
||||
const connectors = useMemo(() => {
|
||||
return (currentView.connectors ?? []).map((connector) => {
|
||||
const sceneConnector = scene.connectors[connector.id];
|
||||
|
||||
return {
|
||||
...connector,
|
||||
...sceneConnector
|
||||
};
|
||||
});
|
||||
}, [currentView.connectors, scene.connectors]);
|
||||
|
||||
const rectangles = useMemo(() => {
|
||||
return currentView.rectangles ?? [];
|
||||
}, [currentView.rectangles]);
|
||||
|
||||
const textBoxes = useMemo(() => {
|
||||
return (currentView.textBoxes ?? []).map((textBox) => {
|
||||
const sceneTextBox = scene.textBoxes[textBox.id];
|
||||
|
||||
return {
|
||||
...textBox,
|
||||
...sceneTextBox
|
||||
};
|
||||
});
|
||||
}, [currentView.textBoxes, scene.textBoxes]);
|
||||
|
||||
const getState = useCallback(() => {
|
||||
return {
|
||||
model: model.actions.get(),
|
||||
scene: scene.actions.get()
|
||||
};
|
||||
}, [model.actions, scene.actions]);
|
||||
|
||||
const setState = useCallback(
|
||||
(newState: State) => {
|
||||
model.actions.set(newState.model);
|
||||
scene.actions.set(newState.scene);
|
||||
},
|
||||
[model.actions, scene.actions]
|
||||
);
|
||||
|
||||
const createModelItem = useCallback(
|
||||
(newModelItem: ModelItem) => {
|
||||
const newState = reducers.createModelItem(newModelItem, getState());
|
||||
setState(newState);
|
||||
},
|
||||
[getState, setState]
|
||||
);
|
||||
|
||||
const updateModelItem = useCallback(
|
||||
(id: string, updates: Partial<ModelItem>) => {
|
||||
const newState = reducers.updateModelItem(id, updates, getState());
|
||||
setState(newState);
|
||||
},
|
||||
[getState, setState]
|
||||
);
|
||||
|
||||
const deleteModelItem = useCallback(
|
||||
(id: string) => {
|
||||
const newState = reducers.deleteModelItem(id, getState());
|
||||
setState(newState);
|
||||
},
|
||||
[getState, setState]
|
||||
);
|
||||
|
||||
const createViewItem = useCallback(
|
||||
(newViewItem: ViewItem) => {
|
||||
const newState = reducers.createViewItem(
|
||||
newViewItem,
|
||||
currentViewId,
|
||||
getState()
|
||||
);
|
||||
setState(newState);
|
||||
},
|
||||
[getState, setState, currentViewId]
|
||||
);
|
||||
|
||||
const updateViewItem = useCallback(
|
||||
(id: string, updates: Partial<ViewItem>) => {
|
||||
const newState = reducers.updateViewItem(
|
||||
id,
|
||||
updates,
|
||||
currentViewId,
|
||||
getState()
|
||||
);
|
||||
setState(newState);
|
||||
},
|
||||
[getState, setState, currentViewId]
|
||||
);
|
||||
|
||||
const deleteViewItem = useCallback(
|
||||
(id: string) => {
|
||||
const newState = reducers.deleteViewItem(id, currentViewId, getState());
|
||||
setState(newState);
|
||||
},
|
||||
[getState, setState, currentViewId]
|
||||
);
|
||||
|
||||
const createConnector = useCallback(
|
||||
(newConnector: Connector) => {
|
||||
const newState = reducers.createConnector(
|
||||
newConnector,
|
||||
currentViewId,
|
||||
getState()
|
||||
);
|
||||
setState(newState);
|
||||
},
|
||||
[getState, setState, currentViewId]
|
||||
);
|
||||
|
||||
const updateConnector = useCallback(
|
||||
(id: string, updates: Partial<Connector>) => {
|
||||
const newState = reducers.updateConnector(
|
||||
id,
|
||||
updates,
|
||||
currentViewId,
|
||||
getState()
|
||||
);
|
||||
setState(newState);
|
||||
},
|
||||
[getState, setState, currentViewId]
|
||||
);
|
||||
|
||||
const deleteConnector = useCallback(
|
||||
(id: string) => {
|
||||
const newState = reducers.deleteConnector(id, currentViewId, getState());
|
||||
setState(newState);
|
||||
},
|
||||
[getState, setState, currentViewId]
|
||||
);
|
||||
|
||||
const createTextBox = useCallback(
|
||||
(newTextBox: TextBox) => {
|
||||
const newState = reducers.createTextBox(
|
||||
newTextBox,
|
||||
currentViewId,
|
||||
getState()
|
||||
);
|
||||
setState(newState);
|
||||
},
|
||||
[getState, setState, currentViewId]
|
||||
);
|
||||
|
||||
const updateTextBox = useCallback(
|
||||
(id: string, updates: Partial<TextBox>) => {
|
||||
const newState = reducers.updateTextBox(
|
||||
id,
|
||||
updates,
|
||||
currentViewId,
|
||||
getState()
|
||||
);
|
||||
setState(newState);
|
||||
},
|
||||
[getState, setState, currentViewId]
|
||||
);
|
||||
|
||||
const deleteTextBox = useCallback(
|
||||
(id: string) => {
|
||||
const newState = reducers.deleteTextBox(id, currentViewId, getState());
|
||||
setState(newState);
|
||||
},
|
||||
[getState, setState, currentViewId]
|
||||
);
|
||||
|
||||
const createRectangle = useCallback(
|
||||
(newRectangle: Rectangle) => {
|
||||
const newState = reducers.createRectangle(
|
||||
newRectangle,
|
||||
currentViewId,
|
||||
getState()
|
||||
);
|
||||
|
||||
setState(newState);
|
||||
},
|
||||
[getState, setState, currentViewId]
|
||||
);
|
||||
|
||||
const updateRectangle = useCallback(
|
||||
(id: string, updates: Partial<Rectangle>) => {
|
||||
const newState = reducers.updateRectangle(
|
||||
id,
|
||||
updates,
|
||||
currentViewId,
|
||||
getState()
|
||||
);
|
||||
setState(newState);
|
||||
},
|
||||
[getState, setState, currentViewId]
|
||||
);
|
||||
|
||||
const deleteRectangle = useCallback(
|
||||
(id: string) => {
|
||||
const newState = reducers.deleteRectangle(id, currentViewId, getState());
|
||||
setState(newState);
|
||||
},
|
||||
[getState, setState, currentViewId]
|
||||
);
|
||||
|
||||
return {
|
||||
items,
|
||||
connectors,
|
||||
rectangles,
|
||||
textBoxes,
|
||||
currentView,
|
||||
createModelItem,
|
||||
updateModelItem,
|
||||
deleteModelItem,
|
||||
createViewItem,
|
||||
updateViewItem,
|
||||
deleteViewItem,
|
||||
createConnector,
|
||||
updateConnector,
|
||||
deleteConnector,
|
||||
createTextBox,
|
||||
updateTextBox,
|
||||
deleteTextBox,
|
||||
createRectangle,
|
||||
updateRectangle,
|
||||
deleteRectangle
|
||||
};
|
||||
};
|
||||
@@ -1,14 +1,12 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useSceneStore } from 'src/stores/sceneStore';
|
||||
import { getItemById } from 'src/utils';
|
||||
import { getItemByIdOrThrow } from 'src/utils';
|
||||
import { useScene } from 'src/hooks/useScene';
|
||||
|
||||
export const useTextBox = (id: string) => {
|
||||
const textBoxes = useSceneStore((state) => {
|
||||
return state.textBoxes;
|
||||
});
|
||||
const { textBoxes } = useScene();
|
||||
|
||||
const textBox = useMemo(() => {
|
||||
return getItemById(textBoxes, id).item;
|
||||
return getItemByIdOrThrow(textBoxes, id).value;
|
||||
}, [textBoxes, id]);
|
||||
|
||||
return textBox;
|
||||
|
||||
@@ -3,20 +3,23 @@ import { TextBox } from 'src/types';
|
||||
import {
|
||||
UNPROJECTED_TILE_SIZE,
|
||||
DEFAULT_FONT_FAMILY,
|
||||
TEXTBOX_DEFAULTS
|
||||
TEXTBOX_DEFAULTS,
|
||||
TEXTBOX_FONT_WEIGHT,
|
||||
TEXTBOX_PADDING
|
||||
} from 'src/config';
|
||||
|
||||
export const useTextBoxProps = (textBox: TextBox) => {
|
||||
const fontProps = useMemo(() => {
|
||||
return {
|
||||
fontSize: UNPROJECTED_TILE_SIZE * textBox.fontSize,
|
||||
fontSize:
|
||||
UNPROJECTED_TILE_SIZE * (textBox.fontSize ?? TEXTBOX_DEFAULTS.fontSize),
|
||||
fontFamily: DEFAULT_FONT_FAMILY,
|
||||
fontWeight: TEXTBOX_DEFAULTS.fontWeight
|
||||
fontWeight: TEXTBOX_FONT_WEIGHT
|
||||
};
|
||||
}, [textBox.fontSize]);
|
||||
|
||||
const paddingX = useMemo(() => {
|
||||
return UNPROJECTED_TILE_SIZE * TEXTBOX_DEFAULTS.paddingX;
|
||||
return UNPROJECTED_TILE_SIZE * TEXTBOX_PADDING;
|
||||
}, []);
|
||||
|
||||
return { paddingX, fontProps };
|
||||
|
||||
13
src/hooks/useViewItem.ts
Normal file
13
src/hooks/useViewItem.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { useMemo } from 'react';
|
||||
import { getItemByIdOrThrow } from 'src/utils';
|
||||
import { useScene } from 'src/hooks/useScene';
|
||||
|
||||
export const useViewItem = (id: string) => {
|
||||
const { items } = useScene();
|
||||
|
||||
const viewItem = useMemo(() => {
|
||||
return getItemByIdOrThrow(items, id).value;
|
||||
}, [items, id]);
|
||||
|
||||
return viewItem;
|
||||
};
|
||||
@@ -1,26 +1,13 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useSceneStore } from 'src/stores/sceneStore';
|
||||
import { useDiagramUtils } from 'src/hooks/useDiagramUtils';
|
||||
|
||||
export const useWindowUtils = () => {
|
||||
const scene = useSceneStore(({ nodes, rectangles, connectors, icons }) => {
|
||||
return {
|
||||
nodes,
|
||||
rectangles,
|
||||
connectors,
|
||||
icons
|
||||
};
|
||||
});
|
||||
const sceneActions = useSceneStore(({ actions }) => {
|
||||
return actions;
|
||||
});
|
||||
const { fitToView, getUnprojectedBounds } = useDiagramUtils();
|
||||
|
||||
useEffect(() => {
|
||||
window.Isoflow = {
|
||||
getUnprojectedBounds,
|
||||
fitToView,
|
||||
setScene: sceneActions.setScene
|
||||
fitToView
|
||||
};
|
||||
}, [getUnprojectedBounds, fitToView, scene, sceneActions]);
|
||||
}, [getUnprojectedBounds, fitToView]);
|
||||
};
|
||||
|
||||
@@ -2,14 +2,11 @@ import { produce } from 'immer';
|
||||
import {
|
||||
generateId,
|
||||
getItemAtTile,
|
||||
connectorInputToConnector,
|
||||
connectorToConnectorInput,
|
||||
getConnectorPath,
|
||||
getItemByIdOrThrow,
|
||||
hasMovedTile,
|
||||
setWindowCursor,
|
||||
getAllAnchors
|
||||
setWindowCursor
|
||||
} from 'src/utils';
|
||||
import { ModeActions, SceneItemTypeEnum } from 'src/types';
|
||||
import { ModeActions, Connector as ConnectorI } from 'src/types';
|
||||
|
||||
export const Connector: ModeActions = {
|
||||
entry: () => {
|
||||
@@ -21,111 +18,75 @@ export const Connector: ModeActions = {
|
||||
mousemove: ({ uiState, scene }) => {
|
||||
if (
|
||||
uiState.mode.type !== 'CONNECTOR' ||
|
||||
!uiState.mode.connector?.anchors[0] ||
|
||||
!uiState.mode.id ||
|
||||
!hasMovedTile(uiState.mouse)
|
||||
)
|
||||
return;
|
||||
|
||||
// TODO: Items at tile should take the entire scene in and return just the first item of interest
|
||||
// for efficiency
|
||||
const itemAtTile = getItemAtTile({
|
||||
tile: uiState.mouse.position.tile,
|
||||
scene
|
||||
});
|
||||
|
||||
if (itemAtTile && itemAtTile.type === 'NODE') {
|
||||
const newMode = produce(uiState.mode, (draft) => {
|
||||
if (!draft.connector) return;
|
||||
|
||||
draft.connector.anchors[1] = {
|
||||
id: generateId(),
|
||||
type: SceneItemTypeEnum.CONNECTOR_ANCHOR,
|
||||
ref: {
|
||||
type: 'NODE',
|
||||
id: itemAtTile.id
|
||||
}
|
||||
};
|
||||
|
||||
draft.connector.path = getConnectorPath({
|
||||
anchors: draft.connector.anchors,
|
||||
nodes: scene.nodes,
|
||||
allAnchors: getAllAnchors(scene.connectors)
|
||||
});
|
||||
});
|
||||
|
||||
uiState.actions.setMode(newMode);
|
||||
} else {
|
||||
const newMode = produce(uiState.mode, (draft) => {
|
||||
if (!draft.connector) return;
|
||||
|
||||
draft.connector.anchors[1] = {
|
||||
id: generateId(),
|
||||
type: SceneItemTypeEnum.CONNECTOR_ANCHOR,
|
||||
ref: {
|
||||
type: 'TILE',
|
||||
coords: uiState.mouse.position.tile
|
||||
}
|
||||
};
|
||||
|
||||
draft.connector.path = getConnectorPath({
|
||||
anchors: draft.connector.anchors,
|
||||
nodes: scene.nodes,
|
||||
allAnchors: getAllAnchors(scene.connectors)
|
||||
});
|
||||
});
|
||||
|
||||
uiState.actions.setMode(newMode);
|
||||
}
|
||||
},
|
||||
mousedown: ({ uiState, scene }) => {
|
||||
if (uiState.mode.type !== 'CONNECTOR') return;
|
||||
const connector = getItemByIdOrThrow(scene.connectors, uiState.mode.id);
|
||||
|
||||
const itemAtTile = getItemAtTile({
|
||||
tile: uiState.mouse.position.tile,
|
||||
scene
|
||||
});
|
||||
|
||||
if (itemAtTile && itemAtTile.type === 'NODE') {
|
||||
const newMode = produce(uiState.mode, (draft) => {
|
||||
draft.connector = connectorInputToConnector(
|
||||
{
|
||||
id: generateId(),
|
||||
anchors: [
|
||||
{ ref: { node: itemAtTile.id } },
|
||||
{ ref: { node: itemAtTile.id } }
|
||||
]
|
||||
},
|
||||
scene.nodes,
|
||||
getAllAnchors(scene.connectors)
|
||||
);
|
||||
if (itemAtTile?.type === 'ITEM') {
|
||||
const newConnector = produce(connector.value, (draft) => {
|
||||
draft.anchors[1] = { id: generateId(), ref: { item: itemAtTile.id } };
|
||||
});
|
||||
|
||||
uiState.actions.setMode(newMode);
|
||||
scene.updateConnector(uiState.mode.id, newConnector);
|
||||
} else {
|
||||
const newMode = produce(uiState.mode, (draft) => {
|
||||
draft.connector = connectorInputToConnector(
|
||||
{
|
||||
id: generateId(),
|
||||
anchors: [
|
||||
{ ref: { tile: uiState.mouse.position.tile } },
|
||||
{ ref: { tile: uiState.mouse.position.tile } }
|
||||
]
|
||||
},
|
||||
scene.nodes,
|
||||
getAllAnchors(scene.connectors)
|
||||
);
|
||||
const newConnector = produce(connector.value, (draft) => {
|
||||
draft.anchors[1] = {
|
||||
id: generateId(),
|
||||
ref: { tile: uiState.mouse.position.tile }
|
||||
};
|
||||
});
|
||||
|
||||
uiState.actions.setMode(newMode);
|
||||
scene.updateConnector(uiState.mode.id, newConnector);
|
||||
}
|
||||
},
|
||||
mouseup: ({ uiState, scene, isRendererInteraction }) => {
|
||||
mousedown: ({ uiState, scene, isRendererInteraction }) => {
|
||||
if (uiState.mode.type !== 'CONNECTOR' || !isRendererInteraction) return;
|
||||
|
||||
if (uiState.mode.connector && uiState.mode.connector.anchors.length >= 2) {
|
||||
scene.actions.createConnector(
|
||||
connectorToConnectorInput(uiState.mode.connector)
|
||||
);
|
||||
const newConnector: ConnectorI = {
|
||||
id: generateId(),
|
||||
anchors: []
|
||||
};
|
||||
|
||||
const itemAtTile = getItemAtTile({
|
||||
tile: uiState.mouse.position.tile,
|
||||
scene
|
||||
});
|
||||
|
||||
if (itemAtTile && itemAtTile.type === 'ITEM') {
|
||||
newConnector.anchors = [
|
||||
{ id: generateId(), ref: { item: itemAtTile.id } },
|
||||
{ id: generateId(), ref: { item: itemAtTile.id } }
|
||||
];
|
||||
} else {
|
||||
newConnector.anchors = [
|
||||
{ id: generateId(), ref: { tile: uiState.mouse.position.tile } },
|
||||
{ id: generateId(), ref: { tile: uiState.mouse.position.tile } }
|
||||
];
|
||||
}
|
||||
|
||||
scene.createConnector(newConnector);
|
||||
|
||||
uiState.actions.setMode({
|
||||
type: 'CONNECTOR',
|
||||
showCursor: true,
|
||||
id: newConnector.id
|
||||
});
|
||||
},
|
||||
mouseup: ({ uiState, scene }) => {
|
||||
if (uiState.mode.type !== 'CONNECTOR' || !uiState.mode.id) return;
|
||||
|
||||
const connector = getItemByIdOrThrow(scene.connectors, uiState.mode.id);
|
||||
|
||||
if (connector.value.path.tiles.length < 2) {
|
||||
scene.deleteConnector(uiState.mode.id);
|
||||
}
|
||||
|
||||
uiState.actions.setMode({
|
||||
|
||||
@@ -1,33 +1,30 @@
|
||||
import { produce } from 'immer';
|
||||
import {
|
||||
ConnectorAnchor,
|
||||
Connector,
|
||||
SceneConnector,
|
||||
ModeActions,
|
||||
ModeActionsAction,
|
||||
SceneItemTypeEnum,
|
||||
SceneStore,
|
||||
Coords,
|
||||
Node
|
||||
View
|
||||
} from 'src/types';
|
||||
import {
|
||||
getItemAtTile,
|
||||
hasMovedTile,
|
||||
getAnchorAtTile,
|
||||
getItemById,
|
||||
getItemByIdOrThrow,
|
||||
generateId,
|
||||
CoordsUtils,
|
||||
getAnchorTile,
|
||||
getAllAnchors,
|
||||
connectorPathTileToGlobal
|
||||
} from 'src/utils';
|
||||
import { useScene } from 'src/hooks/useScene';
|
||||
|
||||
const getAnchorOrdering = (
|
||||
anchor: ConnectorAnchor,
|
||||
connector: Connector,
|
||||
nodes: Node[],
|
||||
allAnchors: ConnectorAnchor[]
|
||||
connector: SceneConnector,
|
||||
view: View
|
||||
) => {
|
||||
const anchorTile = getAnchorTile(anchor, nodes, allAnchors);
|
||||
const anchorTile = getAnchorTile(anchor, view);
|
||||
const index = connector.path.tiles.findIndex((pathTile) => {
|
||||
const globalTile = connectorPathTileToGlobal(
|
||||
pathTile,
|
||||
@@ -45,30 +42,32 @@ const getAnchorOrdering = (
|
||||
return index;
|
||||
};
|
||||
|
||||
const getAnchor = (connectorId: string, tile: Coords, scene: SceneStore) => {
|
||||
const connector = getItemById(scene.connectors, connectorId).item;
|
||||
const getAnchor = (
|
||||
connectorId: string,
|
||||
tile: Coords,
|
||||
scene: ReturnType<typeof useScene>
|
||||
) => {
|
||||
const connector = getItemByIdOrThrow(scene.connectors, connectorId).value;
|
||||
const anchor = getAnchorAtTile(tile, connector.anchors);
|
||||
|
||||
if (!anchor) {
|
||||
const newAnchor: ConnectorAnchor = {
|
||||
id: generateId(),
|
||||
type: SceneItemTypeEnum.CONNECTOR_ANCHOR,
|
||||
ref: { type: 'TILE', coords: tile }
|
||||
ref: { tile }
|
||||
};
|
||||
|
||||
const allAnchors = getAllAnchors(scene.connectors);
|
||||
const orderedAnchors = [...connector.anchors, newAnchor]
|
||||
.map((anch) => {
|
||||
return {
|
||||
...anch,
|
||||
ordering: getAnchorOrdering(anch, connector, scene.nodes, allAnchors)
|
||||
ordering: getAnchorOrdering(anch, connector, scene.currentView)
|
||||
};
|
||||
})
|
||||
.sort((a, b) => {
|
||||
return a.ordering - b.ordering;
|
||||
});
|
||||
|
||||
scene.actions.updateConnector(connector.id, { anchors: orderedAnchors });
|
||||
scene.updateConnector(connector.id, { anchors: orderedAnchors });
|
||||
return newAnchor;
|
||||
}
|
||||
|
||||
@@ -82,19 +81,19 @@ const mousedown: ModeActionsAction = ({
|
||||
}) => {
|
||||
if (uiState.mode.type !== 'CURSOR' || !isRendererInteraction) return;
|
||||
|
||||
const item = getItemAtTile({
|
||||
const itemAtTile = getItemAtTile({
|
||||
tile: uiState.mouse.position.tile,
|
||||
scene
|
||||
});
|
||||
|
||||
if (item) {
|
||||
if (itemAtTile) {
|
||||
uiState.actions.setMode(
|
||||
produce(uiState.mode, (draft) => {
|
||||
draft.mousedownItem = item;
|
||||
draft.mousedownItem = itemAtTile;
|
||||
})
|
||||
);
|
||||
|
||||
uiState.actions.setItemControls(item);
|
||||
uiState.actions.setItemControls(itemAtTile);
|
||||
} else {
|
||||
uiState.actions.setMode(
|
||||
produce(uiState.mode, (draft) => {
|
||||
@@ -123,7 +122,11 @@ export const Cursor: ModeActions = {
|
||||
|
||||
if (item?.type === 'CONNECTOR' && uiState.mouse.mousedown) {
|
||||
const anchor = getAnchor(item.id, uiState.mouse.mousedown.tile, scene);
|
||||
item = anchor;
|
||||
|
||||
item = {
|
||||
type: 'CONNECTOR_ANCHOR',
|
||||
id: anchor.id
|
||||
};
|
||||
}
|
||||
|
||||
if (item) {
|
||||
@@ -140,9 +143,9 @@ export const Cursor: ModeActions = {
|
||||
if (uiState.mode.type !== 'CURSOR' || !isRendererInteraction) return;
|
||||
|
||||
if (uiState.mode.mousedownItem) {
|
||||
if (uiState.mode.mousedownItem.type === 'NODE') {
|
||||
if (uiState.mode.mousedownItem.type === 'ITEM') {
|
||||
uiState.actions.setItemControls({
|
||||
type: 'NODE',
|
||||
type: 'ITEM',
|
||||
id: uiState.mode.mousedownItem.id
|
||||
});
|
||||
} else if (uiState.mode.mousedownItem.type === 'RECTANGLE') {
|
||||
@@ -150,15 +153,6 @@ export const Cursor: ModeActions = {
|
||||
type: 'RECTANGLE',
|
||||
id: uiState.mode.mousedownItem.id
|
||||
});
|
||||
|
||||
uiState.actions.setMode({
|
||||
type: 'RECTANGLE.TRANSFORM',
|
||||
id: uiState.mode.mousedownItem.id,
|
||||
showCursor: true,
|
||||
selectedAnchor: null
|
||||
});
|
||||
|
||||
return;
|
||||
} else if (uiState.mode.mousedownItem.type === 'CONNECTOR') {
|
||||
uiState.actions.setItemControls({
|
||||
type: 'CONNECTOR',
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { produce } from 'immer';
|
||||
import { ModeActions, Coords, SceneItemReference, SceneStore } from 'src/types';
|
||||
import { ModeActions, Coords, ItemReference } from 'src/types';
|
||||
import { useScene } from 'src/hooks/useScene';
|
||||
import {
|
||||
getItemById,
|
||||
getItemByIdOrThrow,
|
||||
CoordsUtils,
|
||||
hasMovedTile,
|
||||
getAnchorParent,
|
||||
@@ -9,73 +10,67 @@ import {
|
||||
} from 'src/utils';
|
||||
|
||||
const dragItems = (
|
||||
items: SceneItemReference[],
|
||||
items: ItemReference[],
|
||||
tile: Coords,
|
||||
delta: Coords,
|
||||
scene: SceneStore
|
||||
scene: ReturnType<typeof useScene>
|
||||
) => {
|
||||
items.forEach((item) => {
|
||||
if (item.type === 'NODE') {
|
||||
const node = getItemById(scene.nodes, item.id).item;
|
||||
if (item.type === 'ITEM') {
|
||||
const node = getItemByIdOrThrow(scene.items, item.id).value;
|
||||
|
||||
scene.actions.updateNode(item.id, {
|
||||
scene.updateViewItem(item.id, {
|
||||
tile: CoordsUtils.add(node.tile, delta)
|
||||
});
|
||||
} else if (item.type === 'RECTANGLE') {
|
||||
const rectangle = getItemById(scene.rectangles, item.id).item;
|
||||
const rectangle = getItemByIdOrThrow(scene.rectangles, item.id).value;
|
||||
const newFrom = CoordsUtils.add(rectangle.from, delta);
|
||||
const newTo = CoordsUtils.add(rectangle.to, delta);
|
||||
|
||||
scene.actions.updateRectangle(item.id, { from: newFrom, to: newTo });
|
||||
scene.updateRectangle(item.id, { from: newFrom, to: newTo });
|
||||
} else if (item.type === 'TEXTBOX') {
|
||||
const textBox = getItemById(scene.textBoxes, item.id).item;
|
||||
const textBox = getItemByIdOrThrow(scene.textBoxes, item.id).value;
|
||||
|
||||
scene.actions.updateTextBox(item.id, {
|
||||
scene.updateTextBox(item.id, {
|
||||
tile: CoordsUtils.add(textBox.tile, delta)
|
||||
});
|
||||
} else if (item.type === 'CONNECTOR_ANCHOR') {
|
||||
const connector = getAnchorParent(item.id, scene.connectors);
|
||||
|
||||
const newConnector = produce(connector, (draft) => {
|
||||
const { item: anchor, index: anchorIndex } = getItemById(
|
||||
connector.anchors,
|
||||
item.id
|
||||
);
|
||||
const anchor = getItemByIdOrThrow(connector.anchors, item.id);
|
||||
|
||||
const itemAtTile = getItemAtTile({ tile, scene });
|
||||
|
||||
switch (itemAtTile?.type) {
|
||||
case 'NODE':
|
||||
draft.anchors[anchorIndex] = {
|
||||
...anchor,
|
||||
case 'ITEM':
|
||||
draft.anchors[anchor.index] = {
|
||||
...anchor.value,
|
||||
ref: {
|
||||
type: 'NODE',
|
||||
id: itemAtTile.id
|
||||
item: itemAtTile.id
|
||||
}
|
||||
};
|
||||
break;
|
||||
case 'CONNECTOR_ANCHOR':
|
||||
draft.anchors[anchorIndex] = {
|
||||
...anchor,
|
||||
draft.anchors[anchor.index] = {
|
||||
...anchor.value,
|
||||
ref: {
|
||||
type: 'ANCHOR',
|
||||
id: itemAtTile.id
|
||||
anchor: itemAtTile.id
|
||||
}
|
||||
};
|
||||
break;
|
||||
default:
|
||||
draft.anchors[anchorIndex] = {
|
||||
...anchor,
|
||||
draft.anchors[anchor.index] = {
|
||||
...anchor.value,
|
||||
ref: {
|
||||
type: 'TILE',
|
||||
coords: tile
|
||||
tile
|
||||
}
|
||||
};
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
scene.actions.updateConnector(connector.id, newConnector);
|
||||
scene.updateConnector(connector.id, newConnector);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
// export const Lasso: ModeActions = {
|
||||
// type: 'LASSO',
|
||||
// mousemove: ({ uiState, scene }) => {
|
||||
// mousemove: ({ uiState, Model }) => {
|
||||
// if (uiState.mode.type !== 'LASSO') return;
|
||||
|
||||
// if (uiState.mouse.mousedown === null) return;
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
// if (!uiState.mode.isDragging) {
|
||||
// const { mousedown } = uiState.mouse;
|
||||
// const items = scene.nodes.filter((node) => {
|
||||
// const items = Model.nodes.filter((node) => {
|
||||
// return CoordsUtils.isEqual(node.tile, mousedown.tile);
|
||||
// });
|
||||
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { produce } from 'immer';
|
||||
import { ModeActions } from 'src/types';
|
||||
import { getItemAtTile, generateId } from 'src/utils';
|
||||
import { generateId, getItemAtTile } from 'src/utils';
|
||||
import { VIEW_ITEM_DEFAULTS } from 'src/config';
|
||||
|
||||
export const PlaceElement: ModeActions = {
|
||||
export const PlaceIcon: ModeActions = {
|
||||
mousemove: () => {},
|
||||
mousedown: ({ uiState, scene, isRendererInteraction }) => {
|
||||
if (uiState.mode.type !== 'PLACE_ELEMENT' || !isRendererInteraction) return;
|
||||
if (uiState.mode.type !== 'PLACE_ICON' || !isRendererInteraction) return;
|
||||
|
||||
if (!uiState.mode.icon) {
|
||||
if (!uiState.mode.id) {
|
||||
const itemAtTile = getItemAtTile({
|
||||
tile: uiState.mouse.position.tile,
|
||||
scene
|
||||
@@ -23,19 +24,27 @@ export const PlaceElement: ModeActions = {
|
||||
}
|
||||
},
|
||||
mouseup: ({ uiState, scene }) => {
|
||||
if (uiState.mode.type !== 'PLACE_ELEMENT') return;
|
||||
if (uiState.mode.type !== 'PLACE_ICON') return;
|
||||
|
||||
if (uiState.mode.icon !== null) {
|
||||
scene.actions.createNode({
|
||||
id: generateId(),
|
||||
icon: uiState.mode.icon.id,
|
||||
if (uiState.mode.id !== null) {
|
||||
const modelItemId = generateId();
|
||||
|
||||
scene.createModelItem({
|
||||
id: modelItemId,
|
||||
name: 'Untitled',
|
||||
icon: uiState.mode.id
|
||||
});
|
||||
|
||||
scene.createViewItem({
|
||||
...VIEW_ITEM_DEFAULTS,
|
||||
id: modelItemId,
|
||||
tile: uiState.mouse.position.tile
|
||||
});
|
||||
}
|
||||
|
||||
uiState.actions.setMode(
|
||||
produce(uiState.mode, (draft) => {
|
||||
draft.icon = null;
|
||||
draft.id = null;
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -10,50 +10,41 @@ export const DrawRectangle: ModeActions = {
|
||||
exit: () => {
|
||||
setWindowCursor('default');
|
||||
},
|
||||
mousemove: ({ uiState }) => {
|
||||
mousemove: ({ uiState, scene }) => {
|
||||
if (
|
||||
uiState.mode.type !== 'RECTANGLE.DRAW' ||
|
||||
!hasMovedTile(uiState.mouse) ||
|
||||
!uiState.mode.area ||
|
||||
!uiState.mode.id ||
|
||||
!uiState.mouse.mousedown
|
||||
)
|
||||
return;
|
||||
|
||||
const newMode = produce(uiState.mode, (draft) => {
|
||||
if (!draft.area) return;
|
||||
|
||||
draft.area.to = uiState.mouse.position.tile;
|
||||
scene.updateRectangle(uiState.mode.id, {
|
||||
to: uiState.mouse.position.tile
|
||||
});
|
||||
|
||||
uiState.actions.setMode(newMode);
|
||||
},
|
||||
mousedown: ({ uiState }) => {
|
||||
if (uiState.mode.type !== 'RECTANGLE.DRAW') return;
|
||||
|
||||
const newMode = produce(uiState.mode, (draft) => {
|
||||
draft.area = {
|
||||
from: uiState.mouse.position.tile,
|
||||
to: uiState.mouse.position.tile
|
||||
};
|
||||
});
|
||||
|
||||
uiState.actions.setMode(newMode);
|
||||
},
|
||||
mouseup: ({ uiState, scene, isRendererInteraction }) => {
|
||||
if (
|
||||
uiState.mode.type !== 'RECTANGLE.DRAW' ||
|
||||
!uiState.mode.area ||
|
||||
!isRendererInteraction
|
||||
)
|
||||
mousedown: ({ uiState, scene, isRendererInteraction }) => {
|
||||
if (uiState.mode.type !== 'RECTANGLE.DRAW' || !isRendererInteraction)
|
||||
return;
|
||||
|
||||
scene.actions.createRectangle({
|
||||
id: generateId(),
|
||||
const newRectangleId = generateId();
|
||||
|
||||
scene.createRectangle({
|
||||
id: newRectangleId,
|
||||
color: DEFAULT_COLOR,
|
||||
from: uiState.mode.area.from,
|
||||
to: uiState.mode.area.to
|
||||
from: uiState.mouse.position.tile,
|
||||
to: uiState.mouse.position.tile
|
||||
});
|
||||
|
||||
const newMode = produce(uiState.mode, (draft) => {
|
||||
draft.id = newRectangleId;
|
||||
});
|
||||
|
||||
uiState.actions.setMode(newMode);
|
||||
},
|
||||
mouseup: ({ uiState }) => {
|
||||
if (uiState.mode.type !== 'RECTANGLE.DRAW' || !uiState.mode.id) return;
|
||||
|
||||
uiState.actions.setMode({
|
||||
type: 'CURSOR',
|
||||
showCursor: true,
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
import { produce } from 'immer';
|
||||
import {
|
||||
isWithinBounds,
|
||||
getItemById,
|
||||
getItemAtTile,
|
||||
getItemByIdOrThrow,
|
||||
getBoundingBox,
|
||||
outermostCornerPositions,
|
||||
convertBoundsToNamedAnchors,
|
||||
hasMovedTile,
|
||||
isoToScreen
|
||||
hasMovedTile
|
||||
} from 'src/utils';
|
||||
import { TRANSFORM_ANCHOR_SIZE } from 'src/config';
|
||||
import { ModeActions, AnchorPositionsEnum } from 'src/types';
|
||||
import { ModeActions } from 'src/types';
|
||||
|
||||
export const TransformRectangle: ModeActions = {
|
||||
entry: () => {},
|
||||
@@ -24,123 +18,58 @@ export const TransformRectangle: ModeActions = {
|
||||
|
||||
if (uiState.mode.selectedAnchor) {
|
||||
// User is dragging an anchor
|
||||
const { item: rectangle } = getItemById(
|
||||
const rectangle = getItemByIdOrThrow(
|
||||
scene.rectangles,
|
||||
uiState.mode.id
|
||||
);
|
||||
).value;
|
||||
const rectangleBounds = getBoundingBox([rectangle.to, rectangle.from]);
|
||||
const namedBounds = convertBoundsToNamedAnchors(rectangleBounds);
|
||||
|
||||
if (
|
||||
uiState.mode.selectedAnchor === AnchorPositionsEnum.BOTTOM_LEFT ||
|
||||
uiState.mode.selectedAnchor === AnchorPositionsEnum.TOP_RIGHT
|
||||
uiState.mode.selectedAnchor === 'BOTTOM_LEFT' ||
|
||||
uiState.mode.selectedAnchor === 'TOP_RIGHT'
|
||||
) {
|
||||
const nextBounds = getBoundingBox([
|
||||
uiState.mode.selectedAnchor === AnchorPositionsEnum.BOTTOM_LEFT
|
||||
uiState.mode.selectedAnchor === 'BOTTOM_LEFT'
|
||||
? namedBounds.TOP_RIGHT
|
||||
: namedBounds.BOTTOM_LEFT,
|
||||
uiState.mouse.position.tile
|
||||
]);
|
||||
const nextNamedBounds = convertBoundsToNamedAnchors(nextBounds);
|
||||
|
||||
scene.actions.updateRectangle(uiState.mode.id, {
|
||||
scene.updateRectangle(uiState.mode.id, {
|
||||
from: nextNamedBounds.TOP_RIGHT,
|
||||
to: nextNamedBounds.BOTTOM_LEFT
|
||||
});
|
||||
} else if (
|
||||
uiState.mode.selectedAnchor === AnchorPositionsEnum.BOTTOM_RIGHT ||
|
||||
uiState.mode.selectedAnchor === AnchorPositionsEnum.TOP_LEFT
|
||||
uiState.mode.selectedAnchor === 'BOTTOM_RIGHT' ||
|
||||
uiState.mode.selectedAnchor === 'TOP_LEFT'
|
||||
) {
|
||||
const nextBounds = getBoundingBox([
|
||||
uiState.mode.selectedAnchor === AnchorPositionsEnum.BOTTOM_RIGHT
|
||||
uiState.mode.selectedAnchor === 'BOTTOM_RIGHT'
|
||||
? namedBounds.TOP_LEFT
|
||||
: namedBounds.BOTTOM_RIGHT,
|
||||
uiState.mouse.position.tile
|
||||
]);
|
||||
const nextNamedBounds = convertBoundsToNamedAnchors(nextBounds);
|
||||
|
||||
scene.actions.updateRectangle(uiState.mode.id, {
|
||||
scene.updateRectangle(uiState.mode.id, {
|
||||
from: nextNamedBounds.TOP_LEFT,
|
||||
to: nextNamedBounds.BOTTOM_RIGHT
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
mousedown: ({ uiState, scene, rendererSize }) => {
|
||||
if (uiState.mode.type !== 'RECTANGLE.TRANSFORM') return;
|
||||
|
||||
const { item: rectangle } = getItemById(scene.rectangles, uiState.mode.id);
|
||||
|
||||
// Check if the user has mousedown'd on an anchor
|
||||
const rectangleBounds = getBoundingBox([rectangle.to, rectangle.from]);
|
||||
const anchorPositions = rectangleBounds.map((corner, i) => {
|
||||
return isoToScreen({
|
||||
tile: corner,
|
||||
origin: outermostCornerPositions[i],
|
||||
rendererSize
|
||||
});
|
||||
});
|
||||
const activeAnchorIndex = anchorPositions.findIndex((anchorPosition) => {
|
||||
return isWithinBounds(uiState.mouse.position.screen, [
|
||||
{
|
||||
x: anchorPosition.x - TRANSFORM_ANCHOR_SIZE,
|
||||
y: anchorPosition.y - TRANSFORM_ANCHOR_SIZE
|
||||
},
|
||||
{
|
||||
x: anchorPosition.x + TRANSFORM_ANCHOR_SIZE,
|
||||
y: anchorPosition.y + TRANSFORM_ANCHOR_SIZE
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
if (activeAnchorIndex !== -1) {
|
||||
const activeAnchor =
|
||||
Object.values(AnchorPositionsEnum)[activeAnchorIndex];
|
||||
|
||||
uiState.actions.setMode(
|
||||
produce(uiState.mode, (draft) => {
|
||||
draft.selectedAnchor = activeAnchor;
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the userhas mousedown'd on the rectangle itself
|
||||
const isMouseWithinRectangle = isWithinBounds(uiState.mouse.position.tile, [
|
||||
rectangle.from,
|
||||
rectangle.to
|
||||
]);
|
||||
|
||||
if (isMouseWithinRectangle) {
|
||||
uiState.actions.setMode({
|
||||
type: 'CURSOR',
|
||||
mousedownItem: rectangle,
|
||||
showCursor: true
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const itemAtTile = getItemAtTile({
|
||||
tile: uiState.mouse.position.tile,
|
||||
scene
|
||||
});
|
||||
|
||||
uiState.actions.setMode({
|
||||
type: 'CURSOR',
|
||||
mousedownItem: itemAtTile,
|
||||
showCursor: true
|
||||
});
|
||||
mousedown: () => {
|
||||
// MOUSE_DOWN is triggered by the anchor iteself (see `TransformAnchor.tsx`)
|
||||
},
|
||||
mouseup: ({ uiState }) => {
|
||||
if (uiState.mode.type !== 'RECTANGLE.TRANSFORM') return;
|
||||
|
||||
if (uiState.mode.selectedAnchor) {
|
||||
uiState.actions.setMode(
|
||||
produce(uiState.mode, (draft) => {
|
||||
draft.selectedAnchor = null;
|
||||
})
|
||||
);
|
||||
}
|
||||
uiState.actions.setMode({
|
||||
type: 'CURSOR',
|
||||
mousedownItem: null,
|
||||
showCursor: true
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { setWindowCursor, generateId } from 'src/utils';
|
||||
import { setWindowCursor } from 'src/utils';
|
||||
import { ModeActions } from 'src/types';
|
||||
import { TEXTBOX_DEFAULTS } from 'src/config';
|
||||
|
||||
export const TextBox: ModeActions = {
|
||||
entry: () => {
|
||||
@@ -9,27 +8,29 @@ export const TextBox: ModeActions = {
|
||||
exit: () => {
|
||||
setWindowCursor('default');
|
||||
},
|
||||
mousemove: () => {},
|
||||
mouseup: ({ scene, uiState, isRendererInteraction }) => {
|
||||
if (!isRendererInteraction) return;
|
||||
mousemove: ({ uiState, scene }) => {
|
||||
if (uiState.mode.type !== 'TEXTBOX' || !uiState.mode.id) return;
|
||||
|
||||
const id = generateId();
|
||||
|
||||
scene.actions.createTextBox({
|
||||
...TEXTBOX_DEFAULTS,
|
||||
id,
|
||||
scene.updateTextBox(uiState.mode.id, {
|
||||
tile: uiState.mouse.position.tile
|
||||
});
|
||||
},
|
||||
mouseup: ({ uiState, scene, isRendererInteraction }) => {
|
||||
if (uiState.mode.type !== 'TEXTBOX' || !uiState.mode.id) return;
|
||||
|
||||
if (!isRendererInteraction) {
|
||||
scene.deleteTextBox(uiState.mode.id);
|
||||
} else {
|
||||
uiState.actions.setItemControls({
|
||||
type: 'TEXTBOX',
|
||||
id: uiState.mode.id
|
||||
});
|
||||
}
|
||||
|
||||
uiState.actions.setMode({
|
||||
type: 'CURSOR',
|
||||
showCursor: true,
|
||||
mousedownItem: null
|
||||
});
|
||||
|
||||
uiState.actions.setItemControls({
|
||||
type: 'TEXTBOX',
|
||||
id
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { useSceneStore } from 'src/stores/sceneStore';
|
||||
import { useModelStore } from 'src/stores/modelStore';
|
||||
import { useUiStateStore } from 'src/stores/uiStateStore';
|
||||
import { ModeActions, State, SlimMouseEvent } from 'src/types';
|
||||
import { getMouse } from 'src/utils';
|
||||
import { useResizeObserver } from 'src/hooks/useResizeObserver';
|
||||
import { useScene } from 'src/hooks/useScene';
|
||||
import { Cursor } from './modes/Cursor';
|
||||
import { DragItems } from './modes/DragItems';
|
||||
import { DrawRectangle } from './modes/Rectangle/DrawRectangle';
|
||||
import { TransformRectangle } from './modes/Rectangle/TransformRectangle';
|
||||
import { Connector } from './modes/Connector';
|
||||
import { Pan } from './modes/Pan';
|
||||
import { PlaceElement } from './modes/PlaceElement';
|
||||
import { PlaceIcon } from './modes/PlaceIcon';
|
||||
import { TextBox } from './modes/TextBox';
|
||||
|
||||
const modes: { [k in string]: ModeActions } = {
|
||||
@@ -21,7 +22,7 @@ const modes: { [k in string]: ModeActions } = {
|
||||
'RECTANGLE.TRANSFORM': TransformRectangle,
|
||||
CONNECTOR: Connector,
|
||||
PAN: Pan,
|
||||
PLACE_ELEMENT: PlaceElement,
|
||||
PLACE_ICON: PlaceIcon,
|
||||
TEXTBOX: TextBox
|
||||
};
|
||||
|
||||
@@ -44,9 +45,10 @@ export const useInteractionManager = () => {
|
||||
const uiState = useUiStateStore((state) => {
|
||||
return state;
|
||||
});
|
||||
const scene = useSceneStore((state) => {
|
||||
const model = useModelStore((state) => {
|
||||
return state;
|
||||
});
|
||||
const scene = useScene();
|
||||
const { size: rendererSize } = useResizeObserver(uiState.rendererEl);
|
||||
|
||||
const onMouseEvent = useCallback(
|
||||
@@ -70,6 +72,7 @@ export const useInteractionManager = () => {
|
||||
uiState.actions.setMouse(nextMouse);
|
||||
|
||||
const baseState: State = {
|
||||
model,
|
||||
scene,
|
||||
uiState,
|
||||
rendererRef: rendererRef.current,
|
||||
@@ -94,7 +97,7 @@ export const useInteractionManager = () => {
|
||||
modeFunction(baseState);
|
||||
reducerTypeRef.current = uiState.mode.type;
|
||||
},
|
||||
[scene, uiState, rendererSize]
|
||||
[model, scene, uiState, rendererSize]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -146,11 +149,11 @@ export const useInteractionManager = () => {
|
||||
};
|
||||
}, [uiState.editorMode, onMouseEvent, uiState.mode.type]);
|
||||
|
||||
const setElement = useCallback((element: HTMLElement) => {
|
||||
const setInteractionsElement = useCallback((element: HTMLElement) => {
|
||||
rendererRef.current = element;
|
||||
}, []);
|
||||
|
||||
return {
|
||||
setElement
|
||||
setInteractionsElement
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// This file will be exported as it's own bundle (separate to the main bundle). This is because the main
|
||||
// bundle requires `window` to be present and so can't be imported into a Node environment.
|
||||
export { INITIAL_SCENE } from 'src/config';
|
||||
export { sceneInput } from 'src/validation/scene';
|
||||
export { INITIAL_DATA } from 'src/config';
|
||||
export { modelSchema } from 'src/validation/model';
|
||||
export const version = PACKAGE_VERSION;
|
||||
|
||||
55
src/stores/modelStore.tsx
Normal file
55
src/stores/modelStore.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import React, { createContext, useRef, useContext } from 'react';
|
||||
import { createStore, useStore } from 'zustand';
|
||||
import { ModelStore } from 'src/types';
|
||||
import { INITIAL_DATA } from 'src/config';
|
||||
|
||||
const initialState = () => {
|
||||
return createStore<ModelStore>((set, get) => {
|
||||
return {
|
||||
...INITIAL_DATA,
|
||||
actions: {
|
||||
get,
|
||||
set
|
||||
}
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const ModelContext = createContext<ReturnType<typeof initialState> | null>(
|
||||
null
|
||||
);
|
||||
|
||||
interface ProviderProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
// TODO: Typings below are pretty gnarly due to the way Zustand works.
|
||||
// see https://github.com/pmndrs/zustand/discussions/1180#discussioncomment-3439061
|
||||
export const ModelProvider = ({ children }: ProviderProps) => {
|
||||
const storeRef = useRef<ReturnType<typeof initialState>>();
|
||||
|
||||
if (!storeRef.current) {
|
||||
storeRef.current = initialState();
|
||||
}
|
||||
|
||||
return (
|
||||
<ModelContext.Provider value={storeRef.current}>
|
||||
{children}
|
||||
</ModelContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export function useModelStore<T>(
|
||||
selector: (state: ModelStore) => T,
|
||||
equalityFn?: (left: T, right: T) => boolean
|
||||
) {
|
||||
const store = useContext(ModelContext);
|
||||
|
||||
if (store === null) {
|
||||
throw new Error('Missing provider in the tree');
|
||||
}
|
||||
|
||||
const value = useStore(store, selector, equalityFn);
|
||||
|
||||
return value;
|
||||
}
|
||||
95
src/stores/reducers/connector.ts
Normal file
95
src/stores/reducers/connector.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { Connector } from 'src/types';
|
||||
import { produce } from 'immer';
|
||||
import { getItemByIdOrThrow, getConnectorPath, getAllAnchors } from 'src/utils';
|
||||
import { validateConnector } from 'src/validation/utils';
|
||||
import { State } from './types';
|
||||
|
||||
export const deleteConnector = (
|
||||
id: string,
|
||||
viewId: string,
|
||||
state: State
|
||||
): State => {
|
||||
const view = getItemByIdOrThrow(state.model.views, viewId);
|
||||
const connector = getItemByIdOrThrow(view.value.connectors ?? [], id);
|
||||
|
||||
const newState = produce(state, (draft) => {
|
||||
draft.model.views[view.index].connectors?.splice(connector.index, 1);
|
||||
delete draft.scene.connectors[connector.index];
|
||||
});
|
||||
|
||||
return newState;
|
||||
};
|
||||
|
||||
export const syncConnector = (id: string, viewId: string, state: State) => {
|
||||
const newState = produce(state, (draft) => {
|
||||
const view = getItemByIdOrThrow(draft.model.views, viewId);
|
||||
const connector = getItemByIdOrThrow(view.value.connectors ?? [], id);
|
||||
const allAnchors = getAllAnchors(view.value.connectors ?? []);
|
||||
const issues = validateConnector(connector.value, {
|
||||
view: view.value,
|
||||
allAnchors
|
||||
});
|
||||
|
||||
if (issues.length > 0) {
|
||||
const stateAfterDelete = deleteConnector(id, viewId, draft);
|
||||
draft.scene = stateAfterDelete.scene;
|
||||
draft.model = stateAfterDelete.model;
|
||||
} else {
|
||||
const path = getConnectorPath({
|
||||
anchors: connector.value.anchors,
|
||||
view: view.value
|
||||
});
|
||||
|
||||
draft.scene.connectors[connector.value.id] = { path };
|
||||
}
|
||||
});
|
||||
|
||||
return newState;
|
||||
};
|
||||
|
||||
export const updateConnector = (
|
||||
id: string,
|
||||
updates: Partial<Connector>,
|
||||
viewId: string,
|
||||
state: State
|
||||
): State => {
|
||||
const view = getItemByIdOrThrow(state.model.views, viewId);
|
||||
|
||||
const newState = produce(state, (draft) => {
|
||||
const { connectors } = draft.model.views[view.index];
|
||||
|
||||
if (!connectors) return;
|
||||
|
||||
const connector = getItemByIdOrThrow(connectors, id);
|
||||
const newConnector = { ...connector.value, ...updates };
|
||||
connectors[connector.index] = newConnector;
|
||||
|
||||
if (updates.anchors) {
|
||||
const stateAfterSync = syncConnector(id, viewId, draft);
|
||||
draft.scene = stateAfterSync.scene;
|
||||
draft.model = stateAfterSync.model;
|
||||
}
|
||||
});
|
||||
|
||||
return newState;
|
||||
};
|
||||
|
||||
export const createConnector = (
|
||||
newConnector: Connector,
|
||||
viewId: string,
|
||||
state: State
|
||||
): State => {
|
||||
const view = getItemByIdOrThrow(state.model.views, viewId);
|
||||
|
||||
const newState = produce(state, (draft) => {
|
||||
const { connectors } = draft.model.views[view.index];
|
||||
|
||||
if (!connectors) {
|
||||
draft.model.views[view.index].connectors = [newConnector];
|
||||
} else {
|
||||
draft.model.views[view.index].connectors?.push(newConnector);
|
||||
}
|
||||
});
|
||||
|
||||
return syncConnector(newConnector.id, viewId, newState);
|
||||
};
|
||||
6
src/stores/reducers/index.ts
Normal file
6
src/stores/reducers/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export * from './connector';
|
||||
export * from './modelItem';
|
||||
export * from './viewItem';
|
||||
export * from './rectangle';
|
||||
export * from './textBox';
|
||||
export * from './view';
|
||||
39
src/stores/reducers/modelItem.ts
Normal file
39
src/stores/reducers/modelItem.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { produce } from 'immer';
|
||||
import { ModelItem } from 'src/types';
|
||||
import { getItemByIdOrThrow } from 'src/utils';
|
||||
import { State } from './types';
|
||||
|
||||
export const updateModelItem = (
|
||||
id: string,
|
||||
updates: Partial<ModelItem>,
|
||||
state: State
|
||||
): State => {
|
||||
const modelItem = getItemByIdOrThrow(state.model.items, id);
|
||||
|
||||
const newState = produce(state, (draft) => {
|
||||
draft.model.items[modelItem.index] = { ...modelItem.value, ...updates };
|
||||
});
|
||||
|
||||
return newState;
|
||||
};
|
||||
|
||||
export const createModelItem = (
|
||||
newModelItem: ModelItem,
|
||||
state: State
|
||||
): State => {
|
||||
const newState = produce(state, (draft) => {
|
||||
draft.model.items.push(newModelItem);
|
||||
});
|
||||
|
||||
return updateModelItem(newModelItem.id, newModelItem, newState);
|
||||
};
|
||||
|
||||
export const deleteModelItem = (id: string, state: State): State => {
|
||||
const modelItem = getItemByIdOrThrow(state.model.items, id);
|
||||
|
||||
const newState = produce(state, (draft) => {
|
||||
delete draft.model.items[modelItem.index];
|
||||
});
|
||||
|
||||
return newState;
|
||||
};
|
||||
60
src/stores/reducers/rectangle.ts
Normal file
60
src/stores/reducers/rectangle.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { produce } from 'immer';
|
||||
import { Rectangle } from 'src/types';
|
||||
import { getItemByIdOrThrow } from 'src/utils';
|
||||
import { State } from './types';
|
||||
|
||||
export const updateRectangle = (
|
||||
id: string,
|
||||
updates: Partial<Rectangle>,
|
||||
viewId: string,
|
||||
state: State
|
||||
): State => {
|
||||
const view = getItemByIdOrThrow(state.model.views, viewId);
|
||||
|
||||
const newState = produce(state, (draft) => {
|
||||
const { rectangles } = draft.model.views[view.index];
|
||||
|
||||
if (!rectangles) return;
|
||||
|
||||
const rectangle = getItemByIdOrThrow(rectangles, id);
|
||||
const newRectangle = { ...rectangle.value, ...updates };
|
||||
rectangles[rectangle.index] = newRectangle;
|
||||
});
|
||||
|
||||
return newState;
|
||||
};
|
||||
|
||||
export const createRectangle = (
|
||||
newRectangle: Rectangle,
|
||||
viewId: string,
|
||||
state: State
|
||||
): State => {
|
||||
const view = getItemByIdOrThrow(state.model.views, viewId);
|
||||
|
||||
const newState = produce(state, (draft) => {
|
||||
const { rectangles } = draft.model.views[view.index];
|
||||
|
||||
if (!rectangles) {
|
||||
draft.model.views[view.index].rectangles = [newRectangle];
|
||||
} else {
|
||||
draft.model.views[view.index].rectangles?.push(newRectangle);
|
||||
}
|
||||
});
|
||||
|
||||
return updateRectangle(newRectangle.id, newRectangle, viewId, newState);
|
||||
};
|
||||
|
||||
export const deleteRectangle = (
|
||||
id: string,
|
||||
viewId: string,
|
||||
state: State
|
||||
): State => {
|
||||
const view = getItemByIdOrThrow(state.model.views, viewId);
|
||||
const rectangle = getItemByIdOrThrow(view.value.rectangles ?? [], id);
|
||||
|
||||
const newState = produce(state, (draft) => {
|
||||
draft.model.views[view.index].rectangles?.splice(rectangle.index, 1);
|
||||
});
|
||||
|
||||
return newState;
|
||||
};
|
||||
80
src/stores/reducers/textBox.ts
Normal file
80
src/stores/reducers/textBox.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { produce } from 'immer';
|
||||
import { TextBox } from 'src/types';
|
||||
import { getItemByIdOrThrow, getTextWidth } from 'src/utils';
|
||||
import {
|
||||
DEFAULT_FONT_FAMILY,
|
||||
TEXTBOX_DEFAULTS,
|
||||
TEXTBOX_FONT_WEIGHT
|
||||
} from 'src/config';
|
||||
import { State } from './types';
|
||||
|
||||
export const updateTextBox = (
|
||||
id: string,
|
||||
updates: Partial<TextBox>,
|
||||
viewId: string,
|
||||
state: State
|
||||
): State => {
|
||||
const view = getItemByIdOrThrow(state.model.views, viewId);
|
||||
|
||||
const newState = produce(state, (draft) => {
|
||||
const { textBoxes } = draft.model.views[view.index];
|
||||
|
||||
if (!textBoxes) return;
|
||||
|
||||
const textBox = getItemByIdOrThrow(textBoxes, id);
|
||||
const newTextBox = { ...textBox.value, ...updates };
|
||||
textBoxes[textBox.index] = newTextBox;
|
||||
|
||||
if (updates.content !== undefined || updates.fontSize !== undefined) {
|
||||
const width = getTextWidth(updates.content ?? textBox.value.content, {
|
||||
fontSize:
|
||||
updates.fontSize ??
|
||||
textBox.value.fontSize ??
|
||||
TEXTBOX_DEFAULTS.fontSize,
|
||||
fontFamily: DEFAULT_FONT_FAMILY,
|
||||
fontWeight: TEXTBOX_FONT_WEIGHT
|
||||
});
|
||||
const height = 1;
|
||||
const size = { width, height };
|
||||
|
||||
draft.scene.textBoxes[newTextBox.id] = { size };
|
||||
}
|
||||
});
|
||||
|
||||
return newState;
|
||||
};
|
||||
|
||||
export const createTextBox = (
|
||||
newTextBox: TextBox,
|
||||
viewId: string,
|
||||
state: State
|
||||
): State => {
|
||||
const view = getItemByIdOrThrow(state.model.views, viewId);
|
||||
|
||||
const newState = produce(state, (draft) => {
|
||||
const { textBoxes } = draft.model.views[view.index];
|
||||
|
||||
if (!textBoxes) {
|
||||
draft.model.views[view.index].textBoxes = [newTextBox];
|
||||
} else {
|
||||
draft.model.views[view.index].textBoxes?.push(newTextBox);
|
||||
}
|
||||
});
|
||||
|
||||
return updateTextBox(newTextBox.id, newTextBox, viewId, newState);
|
||||
};
|
||||
|
||||
export const deleteTextBox = (
|
||||
id: string,
|
||||
viewId: string,
|
||||
state: State
|
||||
): State => {
|
||||
const view = getItemByIdOrThrow(state.model.views, viewId);
|
||||
const textBox = getItemByIdOrThrow(view.value.textBoxes ?? [], id);
|
||||
|
||||
const newState = produce(state, (draft) => {
|
||||
draft.model.views[view.index].textBoxes?.splice(textBox.index, 1);
|
||||
});
|
||||
|
||||
return newState;
|
||||
};
|
||||
6
src/stores/reducers/types.ts
Normal file
6
src/stores/reducers/types.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { Model, Scene } from 'src/types';
|
||||
|
||||
export interface State {
|
||||
model: Model;
|
||||
scene: Scene;
|
||||
}
|
||||
12
src/stores/reducers/view.ts
Normal file
12
src/stores/reducers/view.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { produce } from 'immer';
|
||||
import { View } from 'src/types';
|
||||
import { State } from './types';
|
||||
|
||||
export const createView = (
|
||||
newView: View,
|
||||
model: State['model']
|
||||
): State['model'] => {
|
||||
return produce(model, (draft) => {
|
||||
draft.views.push(newView);
|
||||
});
|
||||
};
|
||||
93
src/stores/reducers/viewItem.ts
Normal file
93
src/stores/reducers/viewItem.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { produce } from 'immer';
|
||||
import { ViewItem } from 'src/types';
|
||||
import { getItemByIdOrThrow, getConnectorsByViewItem } from 'src/utils';
|
||||
import { validateView } from 'src/validation/utils';
|
||||
import { State } from './types';
|
||||
import { updateConnector, syncConnector } from './connector';
|
||||
|
||||
export const updateViewItem = (
|
||||
id: string,
|
||||
updates: Partial<ViewItem>,
|
||||
viewId: string,
|
||||
state: State
|
||||
): State => {
|
||||
const newState = produce(state, (draft) => {
|
||||
const view = getItemByIdOrThrow(draft.model.views, viewId);
|
||||
const { items } = view.value;
|
||||
|
||||
if (!items) return;
|
||||
|
||||
const viewItem = getItemByIdOrThrow(items, id);
|
||||
const newItem = { ...viewItem.value, ...updates };
|
||||
items[viewItem.index] = newItem;
|
||||
|
||||
if (updates.tile) {
|
||||
const connectorsToUpdate = getConnectorsByViewItem(
|
||||
viewItem.value.id,
|
||||
view.value.connectors ?? []
|
||||
);
|
||||
|
||||
const updatedConnectors = connectorsToUpdate.reduce((acc, connector) => {
|
||||
return updateConnector(connector.id, connector, viewId, acc);
|
||||
}, draft);
|
||||
|
||||
draft.model.views[view.index].connectors =
|
||||
updatedConnectors.model.views[view.index].connectors;
|
||||
|
||||
draft.scene.connectors = updatedConnectors.scene.connectors;
|
||||
}
|
||||
});
|
||||
|
||||
const newView = getItemByIdOrThrow(newState.model.views, viewId);
|
||||
const issues = validateView(newView.value, { model: newState.model });
|
||||
|
||||
if (issues.length > 0) {
|
||||
throw new Error(issues[0].message);
|
||||
}
|
||||
|
||||
return newState;
|
||||
};
|
||||
|
||||
export const createViewItem = (
|
||||
newViewItem: ViewItem,
|
||||
viewId: string,
|
||||
state: State
|
||||
): State => {
|
||||
const view = getItemByIdOrThrow(state.model.views, viewId);
|
||||
|
||||
const newState = produce(state, (draft) => {
|
||||
const { items } = draft.model.views[view.index];
|
||||
items.push(newViewItem);
|
||||
});
|
||||
|
||||
return updateViewItem(newViewItem.id, newViewItem, viewId, newState);
|
||||
};
|
||||
|
||||
export const deleteViewItem = (
|
||||
id: string,
|
||||
viewId: string,
|
||||
state: State
|
||||
): State => {
|
||||
const newState = produce(state, (draft) => {
|
||||
const view = getItemByIdOrThrow(draft.model.views, viewId);
|
||||
const viewItem = getItemByIdOrThrow(view.value.items, id);
|
||||
|
||||
draft.model.views[view.index].items.splice(viewItem.index, 1);
|
||||
|
||||
const connectorsToUpdate = getConnectorsByViewItem(
|
||||
viewItem.value.id,
|
||||
view.value.connectors ?? []
|
||||
);
|
||||
|
||||
const updatedConnectors = connectorsToUpdate.reduce((acc, connector) => {
|
||||
return syncConnector(connector.id, viewId, acc);
|
||||
}, draft);
|
||||
|
||||
draft.model.views[view.index].connectors =
|
||||
updatedConnectors.model.views[view.index].connectors;
|
||||
|
||||
draft.scene.connectors = updatedConnectors.scene.connectors;
|
||||
});
|
||||
|
||||
return newState;
|
||||
};
|
||||
@@ -1,229 +1,15 @@
|
||||
import React, { createContext, useRef, useContext } from 'react';
|
||||
import { createStore, useStore } from 'zustand';
|
||||
import { produce } from 'immer';
|
||||
import { SceneStore, Scene } from 'src/types';
|
||||
import {
|
||||
DEFAULT_FONT_FAMILY,
|
||||
TEXTBOX_DEFAULTS,
|
||||
INITIAL_SCENE
|
||||
} from 'src/config';
|
||||
import { sceneInput } from 'src/validation/scene';
|
||||
import {
|
||||
getItemById,
|
||||
getConnectorPath,
|
||||
rectangleInputToRectangle,
|
||||
connectorInputToConnector,
|
||||
textBoxInputToTextBox,
|
||||
sceneInputToScene,
|
||||
nodeInputToNode,
|
||||
getTextWidth,
|
||||
getAllAnchors
|
||||
} from 'src/utils';
|
||||
|
||||
export const initialScene: Scene = {
|
||||
title: INITIAL_SCENE.title,
|
||||
icons: [],
|
||||
nodes: [],
|
||||
connectors: [],
|
||||
textBoxes: [],
|
||||
rectangles: []
|
||||
};
|
||||
import { SceneStore } from 'src/types';
|
||||
|
||||
const initialState = () => {
|
||||
return createStore<SceneStore>((set, get) => {
|
||||
return {
|
||||
...initialScene,
|
||||
connectors: {},
|
||||
textBoxes: {},
|
||||
actions: {
|
||||
setScene: (scene) => {
|
||||
sceneInput.parse(scene);
|
||||
|
||||
const newScene = sceneInputToScene(scene);
|
||||
|
||||
set(newScene);
|
||||
|
||||
return newScene;
|
||||
},
|
||||
|
||||
updateScene: (scene) => {
|
||||
set({
|
||||
nodes: scene.nodes,
|
||||
connectors: scene.connectors,
|
||||
rectangles: scene.rectangles
|
||||
});
|
||||
},
|
||||
|
||||
createNode: (node) => {
|
||||
const newScene = produce(get(), (draft) => {
|
||||
draft.nodes.push(nodeInputToNode(node));
|
||||
});
|
||||
|
||||
set({ nodes: newScene.nodes });
|
||||
},
|
||||
|
||||
updateNode: (id, updates) => {
|
||||
const newScene = produce(get(), (draft) => {
|
||||
const { item: node, index } = getItemById(draft.nodes, id);
|
||||
|
||||
draft.nodes[index] = {
|
||||
...node,
|
||||
...updates
|
||||
};
|
||||
|
||||
draft.connectors.forEach((connector, i) => {
|
||||
const needsUpdate = connector.anchors.find((anchor) => {
|
||||
return anchor.ref.type === 'NODE' && anchor.ref.id === id;
|
||||
});
|
||||
|
||||
if (needsUpdate) {
|
||||
draft.connectors[i].path = getConnectorPath({
|
||||
anchors: connector.anchors,
|
||||
nodes: draft.nodes,
|
||||
allAnchors: getAllAnchors(draft.connectors)
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
set({ nodes: newScene.nodes, connectors: newScene.connectors });
|
||||
},
|
||||
|
||||
deleteNode: (id: string) => {
|
||||
const newScene = produce(get(), (draft) => {
|
||||
const { index } = getItemById(draft.nodes, id);
|
||||
|
||||
draft.nodes.splice(index, 1);
|
||||
|
||||
draft.connectors = draft.connectors.filter((connector) => {
|
||||
return !connector.anchors.find((anchor) => {
|
||||
return anchor.ref.type === 'NODE' && anchor.ref.id === id;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
set({ nodes: newScene.nodes, connectors: newScene.connectors });
|
||||
},
|
||||
|
||||
createConnector: (connector) => {
|
||||
const newScene = produce(get(), (draft) => {
|
||||
draft.connectors.push(
|
||||
connectorInputToConnector(
|
||||
connector,
|
||||
draft.nodes,
|
||||
getAllAnchors(draft.connectors)
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
set({ connectors: newScene.connectors });
|
||||
},
|
||||
|
||||
updateConnector: (id, updates) => {
|
||||
const scene = get();
|
||||
const { item: connector, index } = getItemById(scene.connectors, id);
|
||||
|
||||
const newScene = produce(scene, (draft) => {
|
||||
draft.connectors[index] = {
|
||||
...connector,
|
||||
...updates
|
||||
};
|
||||
|
||||
if (updates.anchors) {
|
||||
draft.connectors[index].path = getConnectorPath({
|
||||
anchors: updates.anchors,
|
||||
nodes: scene.nodes,
|
||||
allAnchors: getAllAnchors(scene.connectors)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
set({ connectors: newScene.connectors });
|
||||
},
|
||||
|
||||
deleteConnector: (id: string) => {
|
||||
const newScene = produce(get(), (draft) => {
|
||||
const { index } = getItemById(draft.connectors, id);
|
||||
|
||||
draft.connectors.splice(index, 1);
|
||||
});
|
||||
|
||||
set({ connectors: newScene.connectors });
|
||||
},
|
||||
|
||||
createRectangle: (rectangle) => {
|
||||
const newScene = produce(get(), (draft) => {
|
||||
draft.rectangles.push(rectangleInputToRectangle(rectangle));
|
||||
});
|
||||
|
||||
set({ rectangles: newScene.rectangles });
|
||||
},
|
||||
|
||||
createTextBox: (textBox) => {
|
||||
const newScene = produce(get(), (draft) => {
|
||||
draft.textBoxes.push(textBoxInputToTextBox(textBox));
|
||||
});
|
||||
|
||||
set({ textBoxes: newScene.textBoxes });
|
||||
},
|
||||
|
||||
updateTextBox: (id, updates) => {
|
||||
const newScene = produce(get(), (draft) => {
|
||||
const { item: textBox, index } = getItemById(draft.textBoxes, id);
|
||||
|
||||
if (updates.text !== undefined || updates.fontSize !== undefined) {
|
||||
draft.textBoxes[index].size = {
|
||||
width: getTextWidth(updates.text ?? textBox.text, {
|
||||
fontSize: updates.fontSize ?? textBox.fontSize,
|
||||
fontFamily: DEFAULT_FONT_FAMILY,
|
||||
fontWeight: TEXTBOX_DEFAULTS.fontWeight
|
||||
}),
|
||||
height: 1
|
||||
};
|
||||
}
|
||||
|
||||
draft.textBoxes[index] = {
|
||||
...textBox,
|
||||
...updates
|
||||
};
|
||||
});
|
||||
|
||||
set({ textBoxes: newScene.textBoxes });
|
||||
},
|
||||
|
||||
deleteTextBox: (id: string) => {
|
||||
const newScene = produce(get(), (draft) => {
|
||||
const { index } = getItemById(draft.textBoxes, id);
|
||||
|
||||
draft.textBoxes.splice(index, 1);
|
||||
});
|
||||
|
||||
set({ textBoxes: newScene.textBoxes });
|
||||
},
|
||||
|
||||
updateRectangle: (id, updates) => {
|
||||
const newScene = produce(get(), (draft) => {
|
||||
const { item: rectangle, index } = getItemById(
|
||||
draft.rectangles,
|
||||
id
|
||||
);
|
||||
|
||||
draft.rectangles[index] = {
|
||||
...rectangle,
|
||||
...updates
|
||||
};
|
||||
});
|
||||
|
||||
set({ rectangles: newScene.rectangles });
|
||||
},
|
||||
|
||||
deleteRectangle: (id: string) => {
|
||||
const newScene = produce(get(), (draft) => {
|
||||
const { index } = getItemById(draft.rectangles, id);
|
||||
|
||||
draft.rectangles.splice(index, 1);
|
||||
});
|
||||
|
||||
set({ rectangles: newScene.rectangles });
|
||||
}
|
||||
get,
|
||||
set
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -264,5 +50,6 @@ export function useSceneStore<T>(
|
||||
}
|
||||
|
||||
const value = useStore(store, selector, equalityFn);
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -8,10 +8,14 @@ import {
|
||||
getTileScrollPosition
|
||||
} from 'src/utils';
|
||||
import { UiStateStore } from 'src/types';
|
||||
import { INITIAL_UI_STATE } from 'src/config';
|
||||
|
||||
const initialState = () => {
|
||||
return createStore<UiStateStore>((set, get) => {
|
||||
return {
|
||||
zoom: INITIAL_UI_STATE.zoom,
|
||||
scroll: INITIAL_UI_STATE.scroll,
|
||||
view: '',
|
||||
mainMenuOptions: [],
|
||||
editorMode: 'EXPLORABLE_READONLY',
|
||||
mode: getStartingMode('EXPLORABLE_READONLY'),
|
||||
@@ -25,13 +29,11 @@ const initialState = () => {
|
||||
delta: null
|
||||
},
|
||||
itemControls: null,
|
||||
scroll: {
|
||||
position: { x: 0, y: 0 },
|
||||
offset: { x: 0, y: 0 }
|
||||
},
|
||||
enableDebugTools: false,
|
||||
zoom: 1,
|
||||
actions: {
|
||||
setView: (view) => {
|
||||
set({ view });
|
||||
},
|
||||
setMainMenuOptions: (mainMenuOptions) => {
|
||||
set({ mainMenuOptions });
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export * from './common';
|
||||
export * from './inputs';
|
||||
export * from './model';
|
||||
export * from './scene';
|
||||
export * from './ui';
|
||||
export * from './interactions';
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
import z from 'zod';
|
||||
import {
|
||||
iconInput,
|
||||
nodeInput,
|
||||
connectorAnchorInput,
|
||||
connectorInput,
|
||||
textBoxInput,
|
||||
rectangleInput,
|
||||
connectorStyleEnum
|
||||
} from 'src/validation/sceneItems';
|
||||
import { Coords } from 'src/types';
|
||||
import { sceneInput } from 'src/validation/scene';
|
||||
import type { EditorModeEnum, MainMenuOptions } from './common';
|
||||
|
||||
export type ConnectorStyleEnum = z.infer<typeof connectorStyleEnum>;
|
||||
export type IconInput = z.infer<typeof iconInput>;
|
||||
export type NodeInput = z.infer<typeof nodeInput>;
|
||||
export type ConnectorAnchorInput = z.infer<typeof connectorAnchorInput>;
|
||||
export type ConnectorInput = z.infer<typeof connectorInput>;
|
||||
export type TextBoxInput = z.infer<typeof textBoxInput>;
|
||||
export type RectangleInput = z.infer<typeof rectangleInput>;
|
||||
export type SceneInput = z.infer<typeof sceneInput>;
|
||||
|
||||
export type InitialScene = Partial<SceneInput> & {
|
||||
zoom?: number;
|
||||
scroll?: Coords;
|
||||
};
|
||||
|
||||
export interface IsoflowProps {
|
||||
initialScene?: InitialScene;
|
||||
mainMenuOptions?: MainMenuOptions;
|
||||
onSceneUpdated?: (scene: SceneInput) => void;
|
||||
width?: number | string;
|
||||
height?: number | string;
|
||||
enableDebugTools?: boolean;
|
||||
editorMode?: keyof typeof EditorModeEnum;
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
import { SceneStore, UiStateStore, Size } from 'src/types';
|
||||
import { ModelStore, UiStateStore, Size } from 'src/types';
|
||||
import { useScene } from 'src/hooks/useScene';
|
||||
|
||||
export interface State {
|
||||
scene: SceneStore;
|
||||
model: ModelStore;
|
||||
scene: ReturnType<typeof useScene>;
|
||||
uiState: UiStateStore;
|
||||
rendererRef: HTMLElement;
|
||||
rendererSize: Size;
|
||||
|
||||
53
src/types/model.ts
Normal file
53
src/types/model.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import z from 'zod';
|
||||
import { iconSchema } from 'src/validation/icons';
|
||||
import { modelSchema } from 'src/validation/model';
|
||||
import { modelItemSchema } from 'src/validation/modelItems';
|
||||
import { viewSchema, viewItemSchema } from 'src/validation/views';
|
||||
import {
|
||||
connectorSchema,
|
||||
anchorSchema,
|
||||
connectorStyleOptions
|
||||
} from 'src/validation/annotations/connector';
|
||||
import { textBoxSchema } from 'src/validation/annotations/textBox';
|
||||
import { rectangleSchema } from 'src/validation/annotations/rectangle';
|
||||
import { Coords } from 'src/types';
|
||||
import { StoreApi } from 'zustand';
|
||||
import type { EditorModeEnum, MainMenuOptions } from './common';
|
||||
|
||||
export { connectorStyleOptions } from 'src/validation/annotations/connector';
|
||||
|
||||
export type Model = z.infer<typeof modelSchema>;
|
||||
export type ModelItems = Model['items'];
|
||||
export type Views = Model['views'];
|
||||
export type Icons = Model['icons'];
|
||||
export type Icon = z.infer<typeof iconSchema>;
|
||||
export type ModelItem = z.infer<typeof modelItemSchema>;
|
||||
export type View = z.infer<typeof viewSchema>;
|
||||
export type ViewItem = z.infer<typeof viewItemSchema>;
|
||||
export type ConnectorStyle = keyof typeof connectorStyleOptions;
|
||||
export type ConnectorAnchor = z.infer<typeof anchorSchema>;
|
||||
export type Connector = z.infer<typeof connectorSchema>;
|
||||
export type TextBox = z.infer<typeof textBoxSchema>;
|
||||
export type Rectangle = z.infer<typeof rectangleSchema>;
|
||||
|
||||
export type InitialData = Partial<Model> & {
|
||||
zoom?: number;
|
||||
scroll?: Coords;
|
||||
};
|
||||
|
||||
export interface IsoflowProps {
|
||||
initialData?: InitialData;
|
||||
mainMenuOptions?: MainMenuOptions;
|
||||
onModelUpdated?: (Model: Model) => void;
|
||||
width?: number | string;
|
||||
height?: number | string;
|
||||
enableDebugTools?: boolean;
|
||||
editorMode?: keyof typeof EditorModeEnum;
|
||||
}
|
||||
|
||||
export type ModelStore = Model & {
|
||||
actions: {
|
||||
get: StoreApi<ModelStore>['getState'];
|
||||
set: StoreApi<ModelStore>['setState'];
|
||||
};
|
||||
};
|
||||
@@ -1,132 +1,56 @@
|
||||
import {
|
||||
IconInput,
|
||||
SceneInput,
|
||||
RectangleInput,
|
||||
ConnectorInput,
|
||||
TextBoxInput,
|
||||
NodeInput,
|
||||
ConnectorStyleEnum
|
||||
} from 'src/types/inputs';
|
||||
import { ProjectionOrientationEnum, Coords, Rect, Size } from 'src/types';
|
||||
import { StoreApi } from 'zustand';
|
||||
import type { Coords, Rect, Size } from './common';
|
||||
|
||||
export enum TileOriginEnum {
|
||||
CENTER = 'CENTER',
|
||||
TOP = 'TOP',
|
||||
BOTTOM = 'BOTTOM',
|
||||
LEFT = 'LEFT',
|
||||
RIGHT = 'RIGHT'
|
||||
}
|
||||
export const tileOriginOptions = {
|
||||
CENTER: 'CENTER',
|
||||
TOP: 'TOP',
|
||||
BOTTOM: 'BOTTOM',
|
||||
LEFT: 'LEFT',
|
||||
RIGHT: 'RIGHT'
|
||||
} as const;
|
||||
|
||||
export enum SceneItemTypeEnum {
|
||||
NODE = 'NODE',
|
||||
CONNECTOR = 'CONNECTOR',
|
||||
CONNECTOR_ANCHOR = 'CONNECTOR_ANCHOR',
|
||||
TEXTBOX = 'TEXTBOX',
|
||||
RECTANGLE = 'RECTANGLE'
|
||||
}
|
||||
export type TileOrigin = keyof typeof tileOriginOptions;
|
||||
|
||||
export interface Node {
|
||||
export const ItemReferenceTypeOptions = {
|
||||
ITEM: 'ITEM',
|
||||
CONNECTOR: 'CONNECTOR',
|
||||
CONNECTOR_ANCHOR: 'CONNECTOR_ANCHOR',
|
||||
TEXTBOX: 'TEXTBOX',
|
||||
RECTANGLE: 'RECTANGLE'
|
||||
} as const;
|
||||
|
||||
export type ItemReferenceType = keyof typeof ItemReferenceTypeOptions;
|
||||
|
||||
export type ItemReference = {
|
||||
type: ItemReferenceType;
|
||||
id: string;
|
||||
type: SceneItemTypeEnum.NODE;
|
||||
icon: string;
|
||||
label?: string;
|
||||
description?: string;
|
||||
labelHeight: number;
|
||||
tile: Coords;
|
||||
isSelected: boolean;
|
||||
}
|
||||
|
||||
export type ConnectorAnchorRef =
|
||||
| {
|
||||
type: 'NODE';
|
||||
id: string;
|
||||
}
|
||||
| {
|
||||
type: 'TILE';
|
||||
coords: Coords;
|
||||
}
|
||||
| {
|
||||
type: 'ANCHOR';
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type ConnectorAnchor = {
|
||||
id: string;
|
||||
type: SceneItemTypeEnum.CONNECTOR_ANCHOR;
|
||||
ref: ConnectorAnchorRef;
|
||||
};
|
||||
|
||||
export interface Connector {
|
||||
type: SceneItemTypeEnum.CONNECTOR;
|
||||
id: string;
|
||||
label?: string;
|
||||
color: string;
|
||||
width: number;
|
||||
style: ConnectorStyleEnum;
|
||||
anchors: ConnectorAnchor[];
|
||||
path: {
|
||||
tiles: Coords[];
|
||||
rectangle: Rect;
|
||||
};
|
||||
export type ConnectorPath = {
|
||||
tiles: Coords[];
|
||||
rectangle: Rect;
|
||||
};
|
||||
|
||||
export interface SceneConnector {
|
||||
path: ConnectorPath;
|
||||
}
|
||||
|
||||
export interface TextBox {
|
||||
type: SceneItemTypeEnum.TEXTBOX;
|
||||
id: string;
|
||||
fontSize: number;
|
||||
tile: Coords;
|
||||
text: string;
|
||||
orientation: keyof typeof ProjectionOrientationEnum;
|
||||
export interface SceneTextBox {
|
||||
size: Size;
|
||||
}
|
||||
|
||||
export interface Rectangle {
|
||||
type: SceneItemTypeEnum.RECTANGLE;
|
||||
id: string;
|
||||
color: string;
|
||||
from: Coords;
|
||||
to: Coords;
|
||||
export interface Scene {
|
||||
connectors: {
|
||||
[key: string]: SceneConnector;
|
||||
};
|
||||
textBoxes: {
|
||||
[key: string]: SceneTextBox;
|
||||
};
|
||||
}
|
||||
|
||||
export type SceneItem =
|
||||
| Node
|
||||
| Connector
|
||||
| TextBox
|
||||
| Rectangle
|
||||
| ConnectorAnchor;
|
||||
export type SceneItemReference = {
|
||||
type: SceneItemTypeEnum;
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type Icon = IconInput;
|
||||
|
||||
export interface SceneActions {
|
||||
setScene: (scene: SceneInput) => Scene;
|
||||
updateScene: (scene: Scene) => void;
|
||||
createNode: (node: NodeInput) => void;
|
||||
updateNode: (id: string, updates: Partial<Node>) => void;
|
||||
deleteNode: (id: string) => void;
|
||||
createConnector: (connector: ConnectorInput) => void;
|
||||
updateConnector: (id: string, updates: Partial<Connector>) => void;
|
||||
deleteConnector: (id: string) => void;
|
||||
createTextBox: (textBox: TextBoxInput) => void;
|
||||
updateTextBox: (id: string, updates: Partial<TextBox>) => void;
|
||||
deleteTextBox: (id: string) => void;
|
||||
createRectangle: (rectangle: RectangleInput) => void;
|
||||
updateRectangle: (id: string, updates: Partial<Rectangle>) => void;
|
||||
deleteRectangle: (id: string) => void;
|
||||
}
|
||||
|
||||
export type Scene = {
|
||||
title: string;
|
||||
nodes: Node[];
|
||||
connectors: Connector[];
|
||||
textBoxes: TextBox[];
|
||||
rectangles: Rectangle[];
|
||||
icons: IconInput[];
|
||||
};
|
||||
|
||||
export type SceneStore = Scene & {
|
||||
actions: SceneActions;
|
||||
actions: {
|
||||
get: StoreApi<SceneStore>['getState'];
|
||||
set: StoreApi<SceneStore>['setState'];
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,44 +1,12 @@
|
||||
import { Coords, EditorModeEnum, MainMenuOptions } from './common';
|
||||
import { Connector, SceneItemReference, TileOriginEnum } from './scene';
|
||||
import { IconInput } from './inputs';
|
||||
|
||||
interface NodeControls {
|
||||
type: 'NODE';
|
||||
id: string;
|
||||
}
|
||||
|
||||
interface ConnectorControls {
|
||||
type: 'CONNECTOR';
|
||||
id: string;
|
||||
}
|
||||
|
||||
interface TextBoxControls {
|
||||
type: 'TEXTBOX';
|
||||
id: string;
|
||||
}
|
||||
|
||||
interface ConnectorAnchorControls {
|
||||
type: 'CONNECTOR_ANCHOR';
|
||||
id: string;
|
||||
}
|
||||
|
||||
interface RectangleControls {
|
||||
type: 'RECTANGLE';
|
||||
id: string;
|
||||
}
|
||||
import { Icon } from './model';
|
||||
import { TileOrigin, ItemReference } from './scene';
|
||||
|
||||
interface AddItemControls {
|
||||
type: 'ADD_ITEM';
|
||||
}
|
||||
|
||||
export type ItemControls =
|
||||
| NodeControls
|
||||
| ConnectorControls
|
||||
| RectangleControls
|
||||
| AddItemControls
|
||||
| TextBoxControls
|
||||
| ConnectorAnchorControls
|
||||
| null;
|
||||
export type ItemControls = ItemReference | AddItemControls | null;
|
||||
|
||||
export interface Mouse {
|
||||
position: {
|
||||
@@ -64,13 +32,13 @@ export interface InteractionsDisabled {
|
||||
export interface CursorMode {
|
||||
type: 'CURSOR';
|
||||
showCursor: boolean;
|
||||
mousedownItem: SceneItemReference | null;
|
||||
mousedownItem: ItemReference | null;
|
||||
}
|
||||
|
||||
export interface DragItemsMode {
|
||||
type: 'DRAG_ITEMS';
|
||||
showCursor: boolean;
|
||||
items: SceneItemReference[];
|
||||
items: ItemReference[];
|
||||
isInitialMovement: Boolean;
|
||||
}
|
||||
|
||||
@@ -79,51 +47,51 @@ export interface PanMode {
|
||||
showCursor: boolean;
|
||||
}
|
||||
|
||||
export interface PlaceElementMode {
|
||||
type: 'PLACE_ELEMENT';
|
||||
export interface PlaceIconMode {
|
||||
type: 'PLACE_ICON';
|
||||
showCursor: boolean;
|
||||
icon: IconInput | null;
|
||||
id: string | null;
|
||||
}
|
||||
|
||||
export interface ConnectorMode {
|
||||
type: 'CONNECTOR';
|
||||
showCursor: boolean;
|
||||
connector: Connector | null;
|
||||
id: string | null;
|
||||
}
|
||||
|
||||
export interface DrawRectangleMode {
|
||||
type: 'RECTANGLE.DRAW';
|
||||
showCursor: boolean;
|
||||
area: {
|
||||
from: Coords;
|
||||
to: Coords;
|
||||
} | null;
|
||||
id: string | null;
|
||||
}
|
||||
|
||||
export enum AnchorPositionsEnum {
|
||||
BOTTOM_LEFT = 'BOTTOM_LEFT',
|
||||
BOTTOM_RIGHT = 'BOTTOM_RIGHT',
|
||||
TOP_RIGHT = 'TOP_RIGHT',
|
||||
TOP_LEFT = 'TOP_LEFT'
|
||||
}
|
||||
export const AnchorPositionOptions = {
|
||||
BOTTOM_LEFT: 'BOTTOM_LEFT',
|
||||
BOTTOM_RIGHT: 'BOTTOM_RIGHT',
|
||||
TOP_RIGHT: 'TOP_RIGHT',
|
||||
TOP_LEFT: 'TOP_LEFT'
|
||||
} as const;
|
||||
|
||||
export type AnchorPosition = keyof typeof AnchorPositionOptions;
|
||||
|
||||
export interface TransformRectangleMode {
|
||||
type: 'RECTANGLE.TRANSFORM';
|
||||
showCursor: boolean;
|
||||
id: string;
|
||||
selectedAnchor: AnchorPositionsEnum | null;
|
||||
selectedAnchor: AnchorPosition | null;
|
||||
}
|
||||
|
||||
export interface TextBoxMode {
|
||||
type: 'TEXTBOX';
|
||||
showCursor: boolean;
|
||||
id: string | null;
|
||||
}
|
||||
|
||||
export type Mode =
|
||||
| InteractionsDisabled
|
||||
| CursorMode
|
||||
| PanMode
|
||||
| PlaceElementMode
|
||||
| PlaceIconMode
|
||||
| ConnectorMode
|
||||
| DrawRectangleMode
|
||||
| TransformRectangleMode
|
||||
@@ -142,7 +110,7 @@ export interface IconCollectionState {
|
||||
}
|
||||
|
||||
export type IconCollectionStateWithIcons = IconCollectionState & {
|
||||
icons: IconInput[];
|
||||
icons: Icon[];
|
||||
};
|
||||
|
||||
export const DialogTypeEnum = {
|
||||
@@ -150,6 +118,7 @@ export const DialogTypeEnum = {
|
||||
} as const;
|
||||
|
||||
export interface UiState {
|
||||
view: string;
|
||||
mainMenuOptions: MainMenuOptions;
|
||||
editorMode: keyof typeof EditorModeEnum;
|
||||
iconCategoriesState: IconCollectionState[];
|
||||
@@ -165,6 +134,7 @@ export interface UiState {
|
||||
}
|
||||
|
||||
export interface UiStateActions {
|
||||
setView: (view: string) => void;
|
||||
setMainMenuOptions: (options: MainMenuOptions) => void;
|
||||
setEditorMode: (mode: keyof typeof EditorModeEnum) => void;
|
||||
setIconCategoriesState: (iconCategoriesState: IconCollectionState[]) => void;
|
||||
@@ -176,7 +146,7 @@ export interface UiStateActions {
|
||||
setDialog: (dialog: keyof typeof DialogTypeEnum | null) => void;
|
||||
setZoom: (zoom: number) => void;
|
||||
setScroll: (scroll: Scroll) => void;
|
||||
scrollToTile: (tile: Coords, origin?: TileOriginEnum) => void;
|
||||
scrollToTile: (tile: Coords, origin?: TileOrigin) => void;
|
||||
setItemControls: (itemControls: ItemControls) => void;
|
||||
setMouse: (mouse: Mouse) => void;
|
||||
setRendererEl: (el: HTMLDivElement) => void;
|
||||
|
||||
@@ -78,3 +78,28 @@ export const getStartingMode = (
|
||||
throw new Error('Invalid editor mode.');
|
||||
}
|
||||
};
|
||||
|
||||
export function getItemByIdOrThrow<T extends { id: string }>(
|
||||
values: T[],
|
||||
id: string
|
||||
): { value: T; index: number } {
|
||||
const index = values.findIndex((val) => {
|
||||
return val.id === id;
|
||||
});
|
||||
|
||||
if (index === -1) {
|
||||
throw new Error(`Item with id "${id}" not found.`);
|
||||
}
|
||||
|
||||
return { value: values[index], index };
|
||||
}
|
||||
|
||||
export function getItemByIndexOrThrow<T>(items: T[], index: number): T {
|
||||
const item = items[index];
|
||||
|
||||
if (!item) {
|
||||
throw new Error(`Item with index "${index}" not found.`);
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import domtoimage from 'dom-to-image';
|
||||
import FileSaver from 'file-saver';
|
||||
import { Scene, Size } from '../types';
|
||||
import { sceneToSceneInput } from './inputs';
|
||||
import { Model, Size } from '../types';
|
||||
|
||||
export const generateGenericFilename = (extension: string) => {
|
||||
return `isoflow-export-${new Date().toISOString()}.${extension}`;
|
||||
@@ -37,10 +36,8 @@ export const downloadFile = (data: Blob, filename: string) => {
|
||||
FileSaver.saveAs(data, filename);
|
||||
};
|
||||
|
||||
export const exportAsJSON = (scene: Scene) => {
|
||||
const parsedScene = sceneToSceneInput(scene);
|
||||
|
||||
const data = new Blob([JSON.stringify(parsedScene)], {
|
||||
export const exportAsJSON = (model: Model) => {
|
||||
const data = new Blob([JSON.stringify(model)], {
|
||||
type: 'application/json;charset=utf-8'
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export * from './CoordsUtils';
|
||||
export * from './SizeUtils';
|
||||
export * from './common';
|
||||
export * from './inputs';
|
||||
export * from './pathfinder';
|
||||
export * from './renderer';
|
||||
export * from './exportOptions';
|
||||
export * from './model';
|
||||
|
||||
@@ -1,303 +0,0 @@
|
||||
import {
|
||||
IconInput,
|
||||
Icon,
|
||||
SceneInput,
|
||||
NodeInput,
|
||||
ConnectorInput,
|
||||
TextBoxInput,
|
||||
ProjectionOrientationEnum,
|
||||
RectangleInput,
|
||||
SceneItemTypeEnum,
|
||||
Scene,
|
||||
Node,
|
||||
Connector,
|
||||
TextBox,
|
||||
Rectangle,
|
||||
ConnectorAnchorInput,
|
||||
ConnectorAnchor
|
||||
} from 'src/types';
|
||||
import {
|
||||
NODE_DEFAULTS,
|
||||
DEFAULT_COLOR,
|
||||
CONNECTOR_DEFAULTS,
|
||||
TEXTBOX_DEFAULTS,
|
||||
DEFAULT_FONT_FAMILY
|
||||
} from 'src/config';
|
||||
import { getConnectorPath, getTextWidth, generateId } from 'src/utils';
|
||||
|
||||
export const iconInputToIcon = (iconInput: IconInput): Icon => {
|
||||
return {
|
||||
id: iconInput.id,
|
||||
name: iconInput.name,
|
||||
url: iconInput.url,
|
||||
collection: iconInput.collection,
|
||||
isIsometric: iconInput.isIsometric ?? true
|
||||
};
|
||||
};
|
||||
|
||||
export const nodeInputToNode = (nodeInput: NodeInput): Node => {
|
||||
return {
|
||||
type: SceneItemTypeEnum.NODE,
|
||||
id: nodeInput.id,
|
||||
label: nodeInput.label ?? NODE_DEFAULTS.label,
|
||||
description: nodeInput.description,
|
||||
labelHeight: nodeInput.labelHeight ?? NODE_DEFAULTS.labelHeight,
|
||||
icon: nodeInput.icon,
|
||||
tile: nodeInput.tile,
|
||||
isSelected: false
|
||||
};
|
||||
};
|
||||
|
||||
export const rectangleInputToRectangle = (
|
||||
rectangleInput: RectangleInput
|
||||
): Rectangle => {
|
||||
return {
|
||||
type: SceneItemTypeEnum.RECTANGLE,
|
||||
id: rectangleInput.id,
|
||||
from: rectangleInput.from,
|
||||
to: rectangleInput.to,
|
||||
color: rectangleInput.color ?? DEFAULT_COLOR
|
||||
};
|
||||
};
|
||||
|
||||
const connectorAnchorInputToConnectorAnchor = (
|
||||
anchor: ConnectorAnchorInput
|
||||
): ConnectorAnchor => {
|
||||
const anchorBase: Required<Pick<ConnectorAnchor, 'id' | 'type'>> = {
|
||||
id: anchor.id ?? generateId(),
|
||||
type: SceneItemTypeEnum.CONNECTOR_ANCHOR
|
||||
};
|
||||
|
||||
if (anchor.ref.node) {
|
||||
return {
|
||||
...anchorBase,
|
||||
ref: {
|
||||
type: 'NODE',
|
||||
id: anchor.ref.node
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (anchor.ref.tile) {
|
||||
return {
|
||||
...anchorBase,
|
||||
ref: {
|
||||
type: 'TILE',
|
||||
coords: anchor.ref.tile
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (anchor.ref.anchor) {
|
||||
return {
|
||||
...anchorBase,
|
||||
ref: {
|
||||
type: 'ANCHOR',
|
||||
id: anchor.ref.anchor
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error('Could not render connector anchor');
|
||||
};
|
||||
|
||||
export const connectorInputToConnector = (
|
||||
connectorInput: ConnectorInput,
|
||||
nodes: Node[],
|
||||
allAnchors: ConnectorAnchor[]
|
||||
): Connector => {
|
||||
const anchors = connectorInput.anchors
|
||||
.map((anchor) => {
|
||||
return connectorAnchorInputToConnectorAnchor(anchor);
|
||||
})
|
||||
.filter((anchor) => {
|
||||
return anchor !== null;
|
||||
});
|
||||
|
||||
return {
|
||||
type: SceneItemTypeEnum.CONNECTOR,
|
||||
id: connectorInput.id,
|
||||
label: connectorInput.label,
|
||||
color: connectorInput.color ?? DEFAULT_COLOR,
|
||||
width: connectorInput.width ?? CONNECTOR_DEFAULTS.width,
|
||||
style: connectorInput.style ?? CONNECTOR_DEFAULTS.style,
|
||||
anchors,
|
||||
path: getConnectorPath({ anchors, nodes, allAnchors })
|
||||
};
|
||||
};
|
||||
|
||||
export const textBoxInputToTextBox = (textBoxInput: TextBoxInput): TextBox => {
|
||||
const fontSize = textBoxInput.fontSize ?? TEXTBOX_DEFAULTS.fontSize;
|
||||
|
||||
return {
|
||||
type: SceneItemTypeEnum.TEXTBOX,
|
||||
id: textBoxInput.id,
|
||||
orientation: textBoxInput.orientation ?? ProjectionOrientationEnum.X,
|
||||
fontSize,
|
||||
tile: textBoxInput.tile,
|
||||
text: textBoxInput.text,
|
||||
size: {
|
||||
width: getTextWidth(textBoxInput.text, {
|
||||
fontSize,
|
||||
fontFamily: DEFAULT_FONT_FAMILY,
|
||||
fontWeight: TEXTBOX_DEFAULTS.fontWeight
|
||||
}),
|
||||
height: 1
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const textBoxToTextBoxInput = (textBox: TextBox): TextBoxInput => {
|
||||
return {
|
||||
id: textBox.id,
|
||||
orientation: textBox.orientation,
|
||||
fontSize: textBox.fontSize,
|
||||
tile: textBox.tile,
|
||||
text: textBox.text
|
||||
};
|
||||
};
|
||||
|
||||
export const getAllAnchorsFromInput = (connectors: ConnectorInput[]) => {
|
||||
const allAnchors = connectors.reduce((acc, connectorInput) => {
|
||||
const convertedAnchors = connectorInput.anchors.map((anchor) => {
|
||||
return connectorAnchorInputToConnectorAnchor(anchor);
|
||||
});
|
||||
return [...acc, ...convertedAnchors];
|
||||
}, [] as ConnectorAnchor[]);
|
||||
|
||||
return allAnchors;
|
||||
};
|
||||
|
||||
export const sceneInputToScene = (sceneInput: SceneInput): Scene => {
|
||||
const icons = sceneInput.icons.map((icon) => {
|
||||
return iconInputToIcon(icon);
|
||||
});
|
||||
|
||||
const nodes = sceneInput.nodes.map((nodeInput) => {
|
||||
return nodeInputToNode(nodeInput);
|
||||
});
|
||||
|
||||
const rectangles = sceneInput.rectangles.map((rectangleInput) => {
|
||||
return rectangleInputToRectangle(rectangleInput);
|
||||
});
|
||||
|
||||
const textBoxes = sceneInput.textBoxes.map((textBoxInput) => {
|
||||
return textBoxInputToTextBox(textBoxInput);
|
||||
});
|
||||
|
||||
const allAnchors = getAllAnchorsFromInput(sceneInput.connectors);
|
||||
|
||||
const connectors = sceneInput.connectors.map((connectorInput) => {
|
||||
return connectorInputToConnector(connectorInput, nodes, allAnchors);
|
||||
});
|
||||
|
||||
return {
|
||||
title: sceneInput.title,
|
||||
icons,
|
||||
nodes,
|
||||
rectangles,
|
||||
connectors,
|
||||
textBoxes
|
||||
} as Scene;
|
||||
};
|
||||
|
||||
export const iconToIconInput = (icon: Icon): IconInput => {
|
||||
return {
|
||||
id: icon.id,
|
||||
name: icon.name,
|
||||
url: icon.url,
|
||||
collection: icon.collection,
|
||||
isIsometric: icon.isIsometric
|
||||
};
|
||||
};
|
||||
|
||||
export const nodeToNodeInput = (node: Node): NodeInput => {
|
||||
return {
|
||||
id: node.id,
|
||||
tile: node.tile,
|
||||
label: node.label,
|
||||
description: node.description,
|
||||
labelHeight: node.labelHeight,
|
||||
icon: node.icon
|
||||
};
|
||||
};
|
||||
|
||||
export const connectorAnchorToConnectorAnchorInput = (
|
||||
anchor: ConnectorAnchor
|
||||
): ConnectorAnchorInput | null => {
|
||||
switch (anchor.ref.type) {
|
||||
case 'NODE':
|
||||
return {
|
||||
id: anchor.id,
|
||||
ref: { node: anchor.ref.id }
|
||||
};
|
||||
case 'TILE':
|
||||
return {
|
||||
id: anchor.id,
|
||||
ref: { tile: anchor.ref.coords }
|
||||
};
|
||||
case 'ANCHOR':
|
||||
return {
|
||||
id: anchor.id,
|
||||
ref: { anchor: anchor.ref.id }
|
||||
};
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const connectorToConnectorInput = (
|
||||
connector: Connector
|
||||
): ConnectorInput => {
|
||||
const anchors = connector.anchors
|
||||
.map((anchor) => {
|
||||
return connectorAnchorToConnectorAnchorInput(anchor);
|
||||
})
|
||||
.filter((anchor): anchor is ConnectorAnchorInput => {
|
||||
return !!anchor;
|
||||
});
|
||||
|
||||
return {
|
||||
id: connector.id,
|
||||
color: connector.color,
|
||||
style: connector.style,
|
||||
width: connector.width,
|
||||
label: connector.label,
|
||||
anchors
|
||||
};
|
||||
};
|
||||
|
||||
export const rectangleToRectangleInput = (
|
||||
rectangle: Rectangle
|
||||
): RectangleInput => {
|
||||
return {
|
||||
id: rectangle.id,
|
||||
color: rectangle.color,
|
||||
from: rectangle.from,
|
||||
to: rectangle.to
|
||||
};
|
||||
};
|
||||
|
||||
export const sceneToSceneInput = (scene: Scene): SceneInput => {
|
||||
const icons: IconInput[] = scene.icons.map(iconToIconInput);
|
||||
const nodes: NodeInput[] = scene.nodes.map(nodeToNodeInput);
|
||||
const connectors: ConnectorInput[] = scene.connectors.map(
|
||||
connectorToConnectorInput,
|
||||
nodes
|
||||
);
|
||||
const textBoxes: TextBoxInput[] = scene.textBoxes.map((textBox) => {
|
||||
return textBoxToTextBoxInput(textBox);
|
||||
});
|
||||
const rectangles: RectangleInput[] = scene.rectangles.map(
|
||||
rectangleToRectangleInput
|
||||
);
|
||||
|
||||
return {
|
||||
title: scene.title,
|
||||
icons,
|
||||
nodes,
|
||||
connectors,
|
||||
textBoxes,
|
||||
rectangles
|
||||
} as SceneInput;
|
||||
};
|
||||
54
src/utils/model.ts
Normal file
54
src/utils/model.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { produce } from 'immer';
|
||||
import { Model } from 'src/types';
|
||||
import { validateModel } from 'src/validation/utils';
|
||||
import { getItemByIdOrThrow } from './common';
|
||||
|
||||
export const fixModel = (model: Model): Model => {
|
||||
const issues = validateModel(model);
|
||||
|
||||
return issues.reduce((acc, issue) => {
|
||||
if (issue.type === 'INVALID_MODEL_TO_ICON_REF') {
|
||||
return produce(acc, (draft) => {
|
||||
const { index: itemIndex } = getItemByIdOrThrow(
|
||||
draft.items,
|
||||
issue.params.modelItem
|
||||
);
|
||||
|
||||
draft.items[itemIndex].icon = undefined;
|
||||
});
|
||||
}
|
||||
|
||||
if (issue.type === 'CONNECTOR_TOO_FEW_ANCHORS') {
|
||||
return produce(acc, (draft) => {
|
||||
const view = getItemByIdOrThrow(draft.views, issue.params.view);
|
||||
|
||||
const connector = getItemByIdOrThrow(
|
||||
view.value.connectors ?? [],
|
||||
issue.params.connector
|
||||
);
|
||||
|
||||
draft.views[view.index].connectors?.splice(connector.index, 1);
|
||||
});
|
||||
}
|
||||
|
||||
if (issue.type === 'INVALID_ANCHOR_TO_ANCHOR_REF') {
|
||||
return produce(acc, (draft) => {
|
||||
const view = getItemByIdOrThrow(draft.views, issue.params.view);
|
||||
|
||||
const connector = getItemByIdOrThrow(
|
||||
view.value.connectors ?? [],
|
||||
issue.params.connector
|
||||
);
|
||||
|
||||
const anchor = getItemByIdOrThrow(
|
||||
connector.value.anchors,
|
||||
issue.params.srcAnchor
|
||||
);
|
||||
|
||||
connector.value.anchors.splice(anchor.index, 1);
|
||||
});
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, model);
|
||||
};
|
||||
@@ -5,26 +5,26 @@ import {
|
||||
ZOOM_INCREMENT,
|
||||
MAX_ZOOM,
|
||||
MIN_ZOOM,
|
||||
CONNECTOR_DEFAULTS,
|
||||
TEXTBOX_DEFAULTS
|
||||
TEXTBOX_PADDING,
|
||||
CONNECTOR_SEARCH_OFFSET
|
||||
} from 'src/config';
|
||||
import {
|
||||
Coords,
|
||||
TileOriginEnum,
|
||||
Node,
|
||||
TileOrigin,
|
||||
Connector,
|
||||
Size,
|
||||
Scroll,
|
||||
Mouse,
|
||||
ConnectorAnchor,
|
||||
SceneItem,
|
||||
Scene,
|
||||
ItemReference,
|
||||
Rect,
|
||||
ProjectionOrientationEnum,
|
||||
AnchorPositionsEnum,
|
||||
AnchorPositionOptions,
|
||||
BoundingBox,
|
||||
TextBox,
|
||||
SlimMouseEvent
|
||||
SlimMouseEvent,
|
||||
View,
|
||||
AnchorPosition
|
||||
} from 'src/types';
|
||||
import {
|
||||
CoordsUtils,
|
||||
@@ -32,8 +32,10 @@ import {
|
||||
clamp,
|
||||
roundToOneDecimalPlace,
|
||||
findPath,
|
||||
toPx
|
||||
toPx,
|
||||
getItemByIdOrThrow
|
||||
} from 'src/utils';
|
||||
import { useScene } from 'src/hooks/useScene';
|
||||
|
||||
interface ScreenToIso {
|
||||
mouse: Coords;
|
||||
@@ -74,12 +76,12 @@ export const screenToIso = ({
|
||||
|
||||
interface GetTilePosition {
|
||||
tile: Coords;
|
||||
origin?: TileOriginEnum;
|
||||
origin?: TileOrigin;
|
||||
}
|
||||
|
||||
export const getTilePosition = ({
|
||||
tile,
|
||||
origin = TileOriginEnum.CENTER
|
||||
origin = 'CENTER'
|
||||
}: GetTilePosition) => {
|
||||
const halfW = PROJECTED_TILE_SIZE.width / 2;
|
||||
const halfH = PROJECTED_TILE_SIZE.height / 2;
|
||||
@@ -90,15 +92,15 @@ export const getTilePosition = ({
|
||||
};
|
||||
|
||||
switch (origin) {
|
||||
case TileOriginEnum.TOP:
|
||||
case 'TOP':
|
||||
return CoordsUtils.add(position, { x: 0, y: -halfH });
|
||||
case TileOriginEnum.BOTTOM:
|
||||
case 'BOTTOM':
|
||||
return CoordsUtils.add(position, { x: 0, y: halfH });
|
||||
case TileOriginEnum.LEFT:
|
||||
case 'LEFT':
|
||||
return CoordsUtils.add(position, { x: -halfW, y: 0 });
|
||||
case TileOriginEnum.RIGHT:
|
||||
case 'RIGHT':
|
||||
return CoordsUtils.add(position, { x: halfW, y: 0 });
|
||||
case TileOriginEnum.CENTER:
|
||||
case 'CENTER':
|
||||
default:
|
||||
return position;
|
||||
}
|
||||
@@ -300,48 +302,30 @@ export const getMouse = ({
|
||||
return nextMouse;
|
||||
};
|
||||
|
||||
export function getItemById<T extends { id: string }>(
|
||||
items: T[],
|
||||
id: string
|
||||
): { item: T; index: number } {
|
||||
const index = items.findIndex((item) => {
|
||||
return item.id === id;
|
||||
});
|
||||
|
||||
if (index === -1) {
|
||||
throw new Error(`Item with id "${id}" not found.`);
|
||||
}
|
||||
|
||||
return { item: items[index], index };
|
||||
}
|
||||
|
||||
export const getAllAnchors = (connectors: Connector[]) => {
|
||||
return connectors.reduce((acc, connector) => {
|
||||
return [...acc, ...connector.anchors];
|
||||
}, [] as ConnectorAnchor[]);
|
||||
};
|
||||
|
||||
export const getAnchorTile = (
|
||||
anchor: ConnectorAnchor,
|
||||
nodes: Node[],
|
||||
allAnchors: ConnectorAnchor[]
|
||||
): Coords => {
|
||||
if (anchor.ref.type === 'NODE') {
|
||||
const { item: node } = getItemById(nodes, anchor.ref.id);
|
||||
return node.tile;
|
||||
export const getAnchorTile = (anchor: ConnectorAnchor, view: View): Coords => {
|
||||
if (anchor.ref.item) {
|
||||
const viewItem = getItemByIdOrThrow(view.items, anchor.ref.item).value;
|
||||
return viewItem.tile;
|
||||
}
|
||||
|
||||
if (anchor.ref.type === 'ANCHOR') {
|
||||
const anchorsWithIds = allAnchors.filter((_anchor) => {
|
||||
return _anchor.id !== undefined;
|
||||
}) as Required<ConnectorAnchor>[];
|
||||
if (anchor.ref.anchor) {
|
||||
const allAnchors = getAllAnchors(view.connectors ?? []);
|
||||
const nextAnchor = getItemByIdOrThrow(allAnchors, anchor.ref.anchor).value;
|
||||
|
||||
const nextAnchor = getItemById(anchorsWithIds, anchor.ref.id);
|
||||
|
||||
return getAnchorTile(nextAnchor.item, nodes, allAnchors);
|
||||
return getAnchorTile(nextAnchor, view);
|
||||
}
|
||||
|
||||
return anchor.ref.coords;
|
||||
if (anchor.ref.tile) {
|
||||
return anchor.ref.tile;
|
||||
}
|
||||
|
||||
throw new Error('Could not get anchor tile.');
|
||||
};
|
||||
|
||||
interface NormalisePositionFromOrigin {
|
||||
@@ -358,14 +342,12 @@ export const normalisePositionFromOrigin = ({
|
||||
|
||||
interface GetConnectorPath {
|
||||
anchors: ConnectorAnchor[];
|
||||
nodes: Node[];
|
||||
allAnchors: ConnectorAnchor[];
|
||||
view: View;
|
||||
}
|
||||
|
||||
export const getConnectorPath = ({
|
||||
anchors,
|
||||
nodes,
|
||||
allAnchors
|
||||
view
|
||||
}: GetConnectorPath): {
|
||||
tiles: Coords[];
|
||||
rectangle: Rect;
|
||||
@@ -375,14 +357,11 @@ export const getConnectorPath = ({
|
||||
`Connector needs at least two anchors (receieved: ${anchors.length})`
|
||||
);
|
||||
|
||||
const anchorPositions = anchors.map((anchor) => {
|
||||
return getAnchorTile(anchor, nodes, allAnchors);
|
||||
const anchorPosition = anchors.map((anchor) => {
|
||||
return getAnchorTile(anchor, view);
|
||||
});
|
||||
|
||||
const searchArea = getBoundingBox(
|
||||
anchorPositions,
|
||||
CONNECTOR_DEFAULTS.searchOffset
|
||||
);
|
||||
const searchArea = getBoundingBox(anchorPosition, CONNECTOR_SEARCH_OFFSET);
|
||||
|
||||
const sorted = sortByPosition(searchArea);
|
||||
const searchAreaSize = getBoundingBoxSize(searchArea);
|
||||
@@ -391,7 +370,7 @@ export const getConnectorPath = ({
|
||||
to: { x: sorted.lowX, y: sorted.lowY }
|
||||
};
|
||||
|
||||
const positionsNormalisedFromSearchArea = anchorPositions.map((position) => {
|
||||
const positionsNormalisedFromSearchArea = anchorPosition.map((position) => {
|
||||
return normalisePositionFromOrigin({
|
||||
position,
|
||||
origin: rectangle.from
|
||||
@@ -440,42 +419,47 @@ export const connectorPathTileToGlobal = (
|
||||
origin: Coords
|
||||
): Coords => {
|
||||
return CoordsUtils.subtract(
|
||||
CoordsUtils.subtract(origin, CONNECTOR_DEFAULTS.searchOffset),
|
||||
CoordsUtils.subtract(tile, CONNECTOR_DEFAULTS.searchOffset)
|
||||
CoordsUtils.subtract(origin, CONNECTOR_SEARCH_OFFSET),
|
||||
CoordsUtils.subtract(tile, CONNECTOR_SEARCH_OFFSET)
|
||||
);
|
||||
};
|
||||
|
||||
export const getTextBoxTo = (textBox: TextBox) => {
|
||||
export const getTextBoxEndTile = (textBox: TextBox, size: Size) => {
|
||||
if (textBox.orientation === ProjectionOrientationEnum.X) {
|
||||
return CoordsUtils.add(textBox.tile, {
|
||||
x: textBox.size.width,
|
||||
x: size.width,
|
||||
y: 0
|
||||
});
|
||||
}
|
||||
|
||||
return CoordsUtils.add(textBox.tile, {
|
||||
x: 0,
|
||||
y: -textBox.size.width
|
||||
y: -size.width
|
||||
});
|
||||
};
|
||||
|
||||
interface GetItemAtTile {
|
||||
tile: Coords;
|
||||
scene: Scene;
|
||||
scene: ReturnType<typeof useScene>;
|
||||
}
|
||||
|
||||
export const getItemAtTile = ({
|
||||
tile,
|
||||
scene
|
||||
}: GetItemAtTile): SceneItem | null => {
|
||||
const node = scene.nodes.find((n) => {
|
||||
return CoordsUtils.isEqual(n.tile, tile);
|
||||
}: GetItemAtTile): ItemReference | null => {
|
||||
const viewItem = scene.items.find((item) => {
|
||||
return CoordsUtils.isEqual(item.tile, tile);
|
||||
});
|
||||
|
||||
if (node) return node;
|
||||
if (viewItem) {
|
||||
return {
|
||||
type: 'ITEM',
|
||||
id: viewItem.id
|
||||
};
|
||||
}
|
||||
|
||||
const textBox = scene.textBoxes.find((tb) => {
|
||||
const textBoxTo = getTextBoxTo(tb);
|
||||
const textBoxTo = getTextBoxEndTile(tb, tb.size);
|
||||
const textBoxBounds = getBoundingBox([
|
||||
tb.tile,
|
||||
{
|
||||
@@ -490,7 +474,12 @@ export const getItemAtTile = ({
|
||||
return isWithinBounds(tile, textBoxBounds);
|
||||
});
|
||||
|
||||
if (textBox) return textBox;
|
||||
if (textBox) {
|
||||
return {
|
||||
type: 'TEXTBOX',
|
||||
id: textBox.id
|
||||
};
|
||||
}
|
||||
|
||||
const connector = scene.connectors.find((con) => {
|
||||
return con.path.tiles.find((pathTile) => {
|
||||
@@ -503,13 +492,23 @@ export const getItemAtTile = ({
|
||||
});
|
||||
});
|
||||
|
||||
if (connector) return connector;
|
||||
if (connector) {
|
||||
return {
|
||||
type: 'CONNECTOR',
|
||||
id: connector.id
|
||||
};
|
||||
}
|
||||
|
||||
const rectangle = scene.rectangles.find(({ from, to }) => {
|
||||
return isWithinBounds(tile, [from, to]);
|
||||
});
|
||||
|
||||
if (rectangle) return rectangle;
|
||||
if (rectangle) {
|
||||
return {
|
||||
type: 'RECTANGLE',
|
||||
id: rectangle.id
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -523,7 +522,7 @@ interface FontProps {
|
||||
export const getTextWidth = (text: string, fontProps: FontProps) => {
|
||||
if (!text) return 0;
|
||||
|
||||
const paddingX = TEXTBOX_DEFAULTS.paddingX * UNPROJECTED_TILE_SIZE;
|
||||
const paddingX = TEXTBOX_PADDING * UNPROJECTED_TILE_SIZE;
|
||||
const fontSizePx = toPx(fontProps.fontSize * UNPROJECTED_TILE_SIZE);
|
||||
const canvas: HTMLCanvasElement = document.createElement('canvas');
|
||||
const context = canvas.getContext('2d');
|
||||
@@ -540,26 +539,30 @@ export const getTextWidth = (text: string, fontProps: FontProps) => {
|
||||
return (metrics.width + paddingX * 2) / UNPROJECTED_TILE_SIZE - 0.8;
|
||||
};
|
||||
|
||||
export const outermostCornerPositions = [
|
||||
TileOriginEnum.BOTTOM,
|
||||
TileOriginEnum.RIGHT,
|
||||
TileOriginEnum.TOP,
|
||||
TileOriginEnum.LEFT
|
||||
export const outermostCornerPositions: TileOrigin[] = [
|
||||
'BOTTOM',
|
||||
'RIGHT',
|
||||
'TOP',
|
||||
'LEFT'
|
||||
];
|
||||
|
||||
export const convertBoundsToNamedAnchors = (boundingBox: BoundingBox) => {
|
||||
export const convertBoundsToNamedAnchors = (
|
||||
boundingBox: BoundingBox
|
||||
): {
|
||||
[key in AnchorPosition]: Coords;
|
||||
} => {
|
||||
return {
|
||||
[AnchorPositionsEnum.BOTTOM_LEFT]: boundingBox[0],
|
||||
[AnchorPositionsEnum.BOTTOM_RIGHT]: boundingBox[1],
|
||||
[AnchorPositionsEnum.TOP_RIGHT]: boundingBox[2],
|
||||
[AnchorPositionsEnum.TOP_LEFT]: boundingBox[3]
|
||||
BOTTOM_LEFT: boundingBox[0],
|
||||
BOTTOM_RIGHT: boundingBox[1],
|
||||
TOP_RIGHT: boundingBox[2],
|
||||
TOP_LEFT: boundingBox[3]
|
||||
};
|
||||
};
|
||||
|
||||
export const getAnchorAtTile = (tile: Coords, anchors: ConnectorAnchor[]) => {
|
||||
return anchors.find((anchor) => {
|
||||
return (
|
||||
anchor.ref.type === 'TILE' && CoordsUtils.isEqual(anchor.ref.coords, tile)
|
||||
return Boolean(
|
||||
anchor.ref.tile && CoordsUtils.isEqual(anchor.ref.tile, tile)
|
||||
);
|
||||
});
|
||||
};
|
||||
@@ -580,7 +583,7 @@ export const getAnchorParent = (anchorId: string, connectors: Connector[]) => {
|
||||
|
||||
export const getTileScrollPosition = (
|
||||
tile: Coords,
|
||||
origin?: TileOriginEnum
|
||||
origin?: TileOrigin
|
||||
): Coords => {
|
||||
const tilePosition = getTilePosition({ tile, origin });
|
||||
|
||||
@@ -589,3 +592,14 @@ export const getTileScrollPosition = (
|
||||
y: -tilePosition.y
|
||||
};
|
||||
};
|
||||
|
||||
export const getConnectorsByViewItem = (
|
||||
viewItemId: string,
|
||||
connectors: Connector[]
|
||||
) => {
|
||||
return connectors.filter((connector) => {
|
||||
return connector.anchors.find((anchor) => {
|
||||
return anchor.ref.item === viewItemId;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
24
src/validation/annotations/connector.ts
Normal file
24
src/validation/annotations/connector.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { z } from 'zod';
|
||||
import { coords, id, constrainedStrings, color } from '../common';
|
||||
|
||||
export const connectorStyleOptions = ['SOLID', 'DOTTED', 'DASHED'] as const;
|
||||
|
||||
export const anchorSchema = z.object({
|
||||
id,
|
||||
ref: z
|
||||
.object({
|
||||
item: id,
|
||||
anchor: id,
|
||||
tile: coords
|
||||
})
|
||||
.partial()
|
||||
});
|
||||
|
||||
export const connectorSchema = z.object({
|
||||
id,
|
||||
description: constrainedStrings.description.optional(),
|
||||
color: color.optional(),
|
||||
width: z.number().optional(),
|
||||
style: z.enum(connectorStyleOptions).optional(),
|
||||
anchors: z.array(anchorSchema)
|
||||
});
|
||||
9
src/validation/annotations/rectangle.ts
Normal file
9
src/validation/annotations/rectangle.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { z } from 'zod';
|
||||
import { id, color, coords } from '../common';
|
||||
|
||||
export const rectangleSchema = z.object({
|
||||
id,
|
||||
color: color.optional(),
|
||||
from: coords,
|
||||
to: coords
|
||||
});
|
||||
16
src/validation/annotations/textbox.ts
Normal file
16
src/validation/annotations/textbox.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { z } from 'zod';
|
||||
import { ProjectionOrientationEnum } from 'src/types/common';
|
||||
import { id, coords, constrainedStrings } from '../common';
|
||||
|
||||
export const textBoxSchema = z.object({
|
||||
id,
|
||||
tile: coords,
|
||||
content: constrainedStrings.name,
|
||||
fontSize: z.number().optional(),
|
||||
orientation: z
|
||||
.union([
|
||||
z.literal(ProjectionOrientationEnum.X),
|
||||
z.literal(ProjectionOrientationEnum.Y)
|
||||
])
|
||||
.optional()
|
||||
});
|
||||
14
src/validation/common.ts
Normal file
14
src/validation/common.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const coords = z.object({
|
||||
x: z.number(),
|
||||
y: z.number()
|
||||
});
|
||||
|
||||
export const id = z.string();
|
||||
export const color = z.string();
|
||||
|
||||
export const constrainedStrings = {
|
||||
name: z.string().max(100),
|
||||
description: z.string().max(1000)
|
||||
};
|
||||
12
src/validation/icons.ts
Normal file
12
src/validation/icons.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { z } from 'zod';
|
||||
import { id, constrainedStrings } from './common';
|
||||
|
||||
export const iconSchema = z.object({
|
||||
id,
|
||||
name: constrainedStrings.name,
|
||||
url: z.string(),
|
||||
collection: constrainedStrings.name.optional(),
|
||||
isIsometric: z.boolean().optional()
|
||||
});
|
||||
|
||||
export const iconsSchema = z.array(iconSchema);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user