mirror of
https://github.com/stan-smith/FossFLOW.git
synced 2025-12-24 06:58:48 -05:00
refactor: implements more efficient calling of onSceneUpdate()
This commit is contained in:
45
package-lock.json
generated
45
package-lock.json
generated
@@ -17,7 +17,6 @@
|
||||
"chroma-js": "^2.4.2",
|
||||
"gsap": "^3.11.4",
|
||||
"immer": "^10.0.2",
|
||||
"notistack": "^3.0.1",
|
||||
"paper": "^0.12.17",
|
||||
"pathfinding": "^0.4.18",
|
||||
"react-hook-form": "^7.43.2",
|
||||
@@ -7224,14 +7223,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/goober": {
|
||||
"version": "2.1.13",
|
||||
"resolved": "https://registry.npmjs.org/goober/-/goober-2.1.13.tgz",
|
||||
"integrity": "sha512-jFj3BQeleOoy7t93E9rZ2de+ScC4lQICLwiAQmKMg9F6roKGaLSHoCDYKkWlSafg138jejvq/mTdvmnwDQgqoQ==",
|
||||
"peerDependencies": {
|
||||
"csstype": "^3.0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
|
||||
@@ -11061,27 +11052,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/notistack": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/notistack/-/notistack-3.0.1.tgz",
|
||||
"integrity": "sha512-ntVZXXgSQH5WYfyU+3HfcXuKaapzAJ8fBLQ/G618rn3yvSzEbnOB8ZSOwhX+dAORy/lw+GC2N061JA0+gYWTVA==",
|
||||
"dependencies": {
|
||||
"clsx": "^1.1.0",
|
||||
"goober": "^2.0.33"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0",
|
||||
"npm": ">=6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/notistack"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/npm-run-path": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
|
||||
@@ -20028,12 +19998,6 @@
|
||||
"slash": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"goober": {
|
||||
"version": "2.1.13",
|
||||
"resolved": "https://registry.npmjs.org/goober/-/goober-2.1.13.tgz",
|
||||
"integrity": "sha512-jFj3BQeleOoy7t93E9rZ2de+ScC4lQICLwiAQmKMg9F6roKGaLSHoCDYKkWlSafg138jejvq/mTdvmnwDQgqoQ==",
|
||||
"requires": {}
|
||||
},
|
||||
"gopd": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
|
||||
@@ -22875,15 +22839,6 @@
|
||||
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
||||
"dev": true
|
||||
},
|
||||
"notistack": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/notistack/-/notistack-3.0.1.tgz",
|
||||
"integrity": "sha512-ntVZXXgSQH5WYfyU+3HfcXuKaapzAJ8fBLQ/G618rn3yvSzEbnOB8ZSOwhX+dAORy/lw+GC2N061JA0+gYWTVA==",
|
||||
"requires": {
|
||||
"clsx": "^1.1.0",
|
||||
"goober": "^2.0.33"
|
||||
}
|
||||
},
|
||||
"npm-run-path": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
|
||||
|
||||
@@ -70,7 +70,6 @@
|
||||
"chroma-js": "^2.4.2",
|
||||
"gsap": "^3.11.4",
|
||||
"immer": "^10.0.2",
|
||||
"notistack": "^3.0.1",
|
||||
"paper": "^0.12.17",
|
||||
"pathfinding": "^0.4.18",
|
||||
"react-hook-form": "^7.43.2",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { ThemeProvider } from '@mui/material/styles';
|
||||
import { Box } from '@mui/material';
|
||||
@@ -19,6 +19,7 @@ import { Renderer } from 'src/components/Renderer/Renderer';
|
||||
import { LabelContainer } from 'src/components/Node/LabelContainer';
|
||||
import { useWindowUtils } from 'src/hooks/useWindowUtils';
|
||||
import { sceneInput as sceneValidationSchema } from 'src/validation/scene';
|
||||
import { EMPTY_SCENE } from 'src/config';
|
||||
import { ItemControlsManager } from './components/ItemControls/ItemControlsManager';
|
||||
import { UiStateProvider, useUiStateStore } from './stores/uiStateStore';
|
||||
import { SceneLayer } from './components/SceneLayer/SceneLayer';
|
||||
@@ -43,6 +44,7 @@ const App = ({
|
||||
onSceneUpdated,
|
||||
debugMode = false
|
||||
}: Props) => {
|
||||
const prevInitialScene = useRef<SceneInput>(EMPTY_SCENE);
|
||||
const [isReady, setIsReady] = useState(false);
|
||||
useWindowUtils();
|
||||
const scene = useSceneStore(({ nodes, connectors, groups, icons }) => {
|
||||
@@ -66,12 +68,14 @@ const App = ({
|
||||
|
||||
useEffect(() => {
|
||||
uiActions.setZoom(initialScene?.zoom ?? 1);
|
||||
// TODO: Rename setInteractionsEnabled to disableInteractions
|
||||
uiActions.setInteractionsEnabled(interactionsEnabledProp);
|
||||
}, [initialScene?.zoom, interactionsEnabledProp, sceneActions, uiActions]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!initialScene) return;
|
||||
if (!initialScene || prevInitialScene.current === initialScene) return;
|
||||
|
||||
prevInitialScene.current = initialScene;
|
||||
sceneActions.setScene(initialScene);
|
||||
setIsReady(true);
|
||||
}, [initialScene, sceneActions]);
|
||||
|
||||
@@ -56,6 +56,14 @@ export const DebugUtils = () => {
|
||||
: 'null'
|
||||
}
|
||||
/>
|
||||
<LineItem
|
||||
title="Mouse delta"
|
||||
value={
|
||||
mouse.delta
|
||||
? `${mouse.delta.tile.x}, ${mouse.delta.tile.y}`
|
||||
: 'null'
|
||||
}
|
||||
/>
|
||||
<LineItem
|
||||
title="Scroll"
|
||||
value={`${scroll.position.x}, ${scroll.position.y}`}
|
||||
|
||||
@@ -49,6 +49,7 @@ export const NodeControls = ({ nodeId }: Props) => {
|
||||
>
|
||||
{tab === 0 && (
|
||||
<NodeSettings
|
||||
key={node.id}
|
||||
color={node.color}
|
||||
label={node.label}
|
||||
labelHeight={node.labelHeight}
|
||||
@@ -57,9 +58,10 @@ export const NodeControls = ({ nodeId }: Props) => {
|
||||
)}
|
||||
{tab === 1 && (
|
||||
<Icons
|
||||
key={node.id}
|
||||
icons={icons}
|
||||
onClick={(icon) => {
|
||||
return onNodeUpdated({ iconId: icon.id });
|
||||
onNodeUpdated({ iconId: icon.id });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -27,7 +27,7 @@ export const NodeSettings = ({
|
||||
<MarkdownEditor
|
||||
value={label}
|
||||
onChange={(text) => {
|
||||
return onUpdate({ label: text });
|
||||
if (label !== text) onUpdate({ label: text });
|
||||
}}
|
||||
/>
|
||||
</Section>
|
||||
@@ -39,7 +39,7 @@ export const NodeSettings = ({
|
||||
max={200}
|
||||
value={labelHeight}
|
||||
onChange={(e, newHeight) => {
|
||||
return onUpdate({ labelHeight: newHeight as number });
|
||||
onUpdate({ labelHeight: newHeight as number });
|
||||
}}
|
||||
/>
|
||||
</Section>
|
||||
@@ -48,7 +48,7 @@ export const NodeSettings = ({
|
||||
activeColor={color}
|
||||
colors={Object.values(theme.customVars.diagramPalette)}
|
||||
onChange={(col) => {
|
||||
return onUpdate({ color: col });
|
||||
onUpdate({ color: col });
|
||||
}}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Size, Coords } from 'src/types';
|
||||
import { Size, Coords, SceneInput } from 'src/types';
|
||||
import { customVars } from './styles/theme';
|
||||
|
||||
export const UNPROJECTED_TILE_SIZE = 100;
|
||||
@@ -27,3 +27,9 @@ export const NODE_DEFAULTS = {
|
||||
export const ZOOM_INCREMENT = 0.2;
|
||||
export const MIN_ZOOM = 0.2;
|
||||
export const MAX_ZOOM = 1;
|
||||
export const EMPTY_SCENE: SceneInput = {
|
||||
icons: [],
|
||||
nodes: [],
|
||||
connectors: [],
|
||||
groups: []
|
||||
};
|
||||
|
||||
@@ -1,51 +1,57 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { Typography } from '@mui/material';
|
||||
import { SnackbarProvider, useSnackbar } from 'notistack';
|
||||
import React, { useCallback, useState, useMemo } from 'react';
|
||||
import { Alert, useTheme } from '@mui/material';
|
||||
import Isoflow from 'src/Isoflow';
|
||||
import { icons } from '../icons';
|
||||
|
||||
const Snackbar = () => {
|
||||
return <Typography>Scene updated!</Typography>;
|
||||
};
|
||||
|
||||
const Example = () => {
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
const onSceneUpdated = useCallback(() => {
|
||||
enqueueSnackbar(<Snackbar />, {
|
||||
autoHideDuration: 1000,
|
||||
preventDuplicate: true
|
||||
});
|
||||
}, [enqueueSnackbar]);
|
||||
|
||||
return (
|
||||
<Isoflow
|
||||
initialScene={{
|
||||
icons,
|
||||
nodes: [
|
||||
{
|
||||
id: 'server',
|
||||
label: 'Callbacks example',
|
||||
labelHeight: 40,
|
||||
iconId: 'server',
|
||||
position: {
|
||||
x: 0,
|
||||
y: 0
|
||||
}
|
||||
}
|
||||
],
|
||||
connectors: [],
|
||||
groups: []
|
||||
}}
|
||||
onSceneUpdated={onSceneUpdated}
|
||||
height="100%"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const Callbacks = () => {
|
||||
const [updatesCounter, setUpdatesCounter] = useState(0);
|
||||
const theme = useTheme();
|
||||
const onSceneUpdated = useCallback(() => {
|
||||
setUpdatesCounter((counter) => {
|
||||
return counter + 1;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const initialScene = useMemo(() => {
|
||||
return {
|
||||
icons,
|
||||
nodes: [
|
||||
{
|
||||
id: 'server',
|
||||
label: '<p>Callbacks example</p>',
|
||||
labelHeight: 40,
|
||||
iconId: 'server',
|
||||
position: {
|
||||
x: 0,
|
||||
y: 0
|
||||
}
|
||||
}
|
||||
],
|
||||
connectors: [],
|
||||
groups: []
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<SnackbarProvider maxSnack={1}>
|
||||
<Example />
|
||||
</SnackbarProvider>
|
||||
<>
|
||||
<Isoflow
|
||||
initialScene={initialScene}
|
||||
onSceneUpdated={onSceneUpdated}
|
||||
height="100%"
|
||||
/>
|
||||
<Alert
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
width: 300,
|
||||
left: theme.customVars.appPadding.x,
|
||||
bottom: theme.customVars.appPadding.y
|
||||
}}
|
||||
variant="filled"
|
||||
severity="info"
|
||||
id="alert"
|
||||
>
|
||||
{updatesCounter} scene updates
|
||||
</Alert>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,8 +4,8 @@ import { Callbacks } from './Callbacks/Callbacks';
|
||||
import { DebugTools } from './DebugTools/DebugTools';
|
||||
|
||||
const examples = [
|
||||
{ name: 'Debug tools', component: DebugTools },
|
||||
{ name: 'Callbacks', component: Callbacks }
|
||||
{ name: 'Callbacks', component: Callbacks },
|
||||
{ name: 'Debug tools', component: DebugTools }
|
||||
];
|
||||
|
||||
export const Examples = () => {
|
||||
|
||||
@@ -1,38 +1,53 @@
|
||||
import { InteractionReducer } from 'src/types';
|
||||
import { generateId } from 'src/utils';
|
||||
import { produce } from 'immer';
|
||||
import { generateId, hasMovedTile } from 'src/utils';
|
||||
import { DEFAULT_COLOR } from 'src/config';
|
||||
|
||||
export const AreaTool: InteractionReducer = {
|
||||
type: 'AREA_TOOL',
|
||||
mousemove: (draftState) => {
|
||||
mousemove: (state) => {
|
||||
if (
|
||||
draftState.mode.type !== 'AREA_TOOL' ||
|
||||
!draftState.mode.area ||
|
||||
!draftState.mouse.mousedown
|
||||
state.mode.type !== 'AREA_TOOL' ||
|
||||
!hasMovedTile(state.mouse) ||
|
||||
!state.mode.area ||
|
||||
!state.mouse.mousedown
|
||||
)
|
||||
return;
|
||||
|
||||
draftState.mode.area.to = draftState.mouse.position.tile;
|
||||
},
|
||||
mousedown: (draftState) => {
|
||||
if (draftState.mode.type !== 'AREA_TOOL') return;
|
||||
const newMode = produce(state.mode, (draftState) => {
|
||||
if (!draftState.area) return;
|
||||
|
||||
draftState.mode.area = {
|
||||
from: draftState.mouse.position.tile,
|
||||
to: draftState.mouse.position.tile
|
||||
};
|
||||
},
|
||||
mouseup: (draftState) => {
|
||||
if (draftState.mode.type !== 'AREA_TOOL' || !draftState.mode.area) return;
|
||||
|
||||
const newGroups = draftState.sceneActions.createGroup({
|
||||
id: generateId(),
|
||||
color: DEFAULT_COLOR,
|
||||
from: draftState.mode.area.from,
|
||||
to: draftState.mode.area.to
|
||||
draftState.area.to = state.mouse.position.tile;
|
||||
});
|
||||
|
||||
draftState.scene.groups = newGroups.groups;
|
||||
draftState.mode.area = null;
|
||||
state.uiStateActions.setMode(newMode);
|
||||
},
|
||||
mousedown: (state) => {
|
||||
if (state.mode.type !== 'AREA_TOOL') return;
|
||||
|
||||
const newMode = produce(state.mode, (draftState) => {
|
||||
draftState.area = {
|
||||
from: state.mouse.position.tile,
|
||||
to: state.mouse.position.tile
|
||||
};
|
||||
});
|
||||
|
||||
state.uiStateActions.setMode(newMode);
|
||||
},
|
||||
mouseup: (state) => {
|
||||
if (state.mode.type !== 'AREA_TOOL' || !state.mode.area) return;
|
||||
|
||||
state.sceneActions.createGroup({
|
||||
id: generateId(),
|
||||
color: DEFAULT_COLOR,
|
||||
from: state.mode.area.from,
|
||||
to: state.mode.area.to
|
||||
});
|
||||
|
||||
state.uiStateActions.setMode({
|
||||
type: 'CURSOR',
|
||||
showCursor: true,
|
||||
mousedown: null
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,87 +1,119 @@
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { InteractionReducer } from 'src/types';
|
||||
import { produce } from 'immer';
|
||||
import {
|
||||
generateId,
|
||||
filterNodesByTile,
|
||||
connectorInputToConnector,
|
||||
getConnectorPath
|
||||
connectorToConnectorInput,
|
||||
getConnectorPath,
|
||||
hasMovedTile
|
||||
} from 'src/utils';
|
||||
import { InteractionReducer } from 'src/types';
|
||||
|
||||
export const Connector: InteractionReducer = {
|
||||
type: 'CONNECTOR',
|
||||
mousemove: (draftState) => {
|
||||
mousemove: (state) => {
|
||||
if (
|
||||
draftState.mode.type !== 'CONNECTOR' ||
|
||||
!draftState.mode.connector?.anchors[0]
|
||||
state.mode.type !== 'CONNECTOR' ||
|
||||
!state.mode.connector?.anchors[0] ||
|
||||
!hasMovedTile(state.mouse)
|
||||
)
|
||||
return;
|
||||
|
||||
// TODO: Items at tile should take the entire scene in and return just the first item of interest
|
||||
// for efficiency
|
||||
const itemsAtTile = filterNodesByTile({
|
||||
tile: draftState.mouse.position.tile,
|
||||
nodes: draftState.scene.nodes
|
||||
tile: state.mouse.position.tile,
|
||||
nodes: state.scene.nodes
|
||||
});
|
||||
|
||||
if (itemsAtTile.length > 0) {
|
||||
const node = itemsAtTile[0];
|
||||
|
||||
draftState.mode.connector.anchors[1] = {
|
||||
type: 'NODE',
|
||||
id: node.id
|
||||
};
|
||||
const newMode = produce(state.mode, (draftState) => {
|
||||
if (!draftState.connector) return;
|
||||
|
||||
draftState.connector.anchors[1] = {
|
||||
type: 'NODE',
|
||||
id: node.id
|
||||
};
|
||||
|
||||
draftState.connector.path = getConnectorPath({
|
||||
anchors: draftState.connector.anchors,
|
||||
nodes: state.scene.nodes
|
||||
});
|
||||
});
|
||||
|
||||
state.uiStateActions.setMode(newMode);
|
||||
} else {
|
||||
draftState.mode.connector.anchors[1] = {
|
||||
type: 'TILE',
|
||||
coords: draftState.mouse.position.tile
|
||||
};
|
||||
}
|
||||
const newMode = produce(state.mode, (draftState) => {
|
||||
if (!draftState.connector) return;
|
||||
|
||||
draftState.mode.connector.path = getConnectorPath({
|
||||
anchors: draftState.mode.connector.anchors,
|
||||
nodes: draftState.scene.nodes
|
||||
});
|
||||
draftState.connector.anchors[1] = {
|
||||
type: 'TILE',
|
||||
coords: state.mouse.position.tile
|
||||
};
|
||||
|
||||
draftState.connector.path = getConnectorPath({
|
||||
anchors: draftState.connector.anchors,
|
||||
nodes: state.scene.nodes
|
||||
});
|
||||
});
|
||||
|
||||
state.uiStateActions.setMode(newMode);
|
||||
}
|
||||
},
|
||||
mousedown: (draftState) => {
|
||||
if (draftState.mode.type !== 'CONNECTOR') return;
|
||||
mousedown: (state) => {
|
||||
if (state.mode.type !== 'CONNECTOR') return;
|
||||
|
||||
const itemsAtTile = filterNodesByTile({
|
||||
tile: draftState.mouse.position.tile,
|
||||
nodes: draftState.scene.nodes
|
||||
tile: state.mouse.position.tile,
|
||||
nodes: state.scene.nodes
|
||||
});
|
||||
|
||||
if (itemsAtTile.length > 0) {
|
||||
const node = itemsAtTile[0];
|
||||
|
||||
draftState.mode.connector = connectorInputToConnector(
|
||||
{
|
||||
id: uuid(),
|
||||
anchors: [{ nodeId: node.id }, { nodeId: node.id }]
|
||||
},
|
||||
draftState.scene.nodes
|
||||
);
|
||||
const newMode = produce(state.mode, (draftState) => {
|
||||
draftState.connector = connectorInputToConnector(
|
||||
{
|
||||
id: generateId(),
|
||||
anchors: [{ nodeId: node.id }, { nodeId: node.id }]
|
||||
},
|
||||
state.scene.nodes
|
||||
);
|
||||
});
|
||||
|
||||
return;
|
||||
state.uiStateActions.setMode(newMode);
|
||||
} else {
|
||||
const newMode = produce(state.mode, (draftState) => {
|
||||
draftState.connector = connectorInputToConnector(
|
||||
{
|
||||
id: generateId(),
|
||||
anchors: [
|
||||
{ tile: state.mouse.position.tile },
|
||||
{ tile: state.mouse.position.tile }
|
||||
]
|
||||
},
|
||||
state.scene.nodes
|
||||
);
|
||||
});
|
||||
|
||||
state.uiStateActions.setMode(newMode);
|
||||
}
|
||||
|
||||
draftState.mode.connector = connectorInputToConnector(
|
||||
{
|
||||
id: uuid(),
|
||||
anchors: [
|
||||
{ tile: draftState.mouse.position.tile },
|
||||
{ tile: draftState.mouse.position.tile }
|
||||
]
|
||||
},
|
||||
draftState.scene.nodes
|
||||
);
|
||||
},
|
||||
mouseup: (draftState) => {
|
||||
if (draftState.mode.type !== 'CONNECTOR') return;
|
||||
mouseup: (state) => {
|
||||
if (state.mode.type !== 'CONNECTOR') return;
|
||||
|
||||
if (
|
||||
draftState.mode.connector &&
|
||||
draftState.mode.connector.anchors.length >= 2
|
||||
) {
|
||||
draftState.scene.connectors.push(draftState.mode.connector);
|
||||
if (state.mode.connector && state.mode.connector.anchors.length >= 2) {
|
||||
state.sceneActions.createConnector(
|
||||
connectorToConnectorInput(state.mode.connector)
|
||||
);
|
||||
}
|
||||
|
||||
draftState.mode.connector = null;
|
||||
const newMode = produce(state.mode, (draftState) => {
|
||||
draftState.connector = null;
|
||||
});
|
||||
|
||||
state.uiStateActions.setMode(newMode);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,27 +1,34 @@
|
||||
import { produce } from 'immer';
|
||||
import { ItemControlsTypeEnum, InteractionReducer } from 'src/types';
|
||||
import { CoordsUtils, filterNodesByTile } from 'src/utils';
|
||||
import {
|
||||
CoordsUtils,
|
||||
filterNodesByTile,
|
||||
getItemById,
|
||||
hasMovedTile
|
||||
} from 'src/utils';
|
||||
|
||||
export const Cursor: InteractionReducer = {
|
||||
type: 'CURSOR',
|
||||
mousemove: (draftState) => {
|
||||
if (draftState.mode.type !== 'CURSOR') return;
|
||||
|
||||
mousemove: (state) => {
|
||||
if (
|
||||
draftState.mouse.delta === null ||
|
||||
CoordsUtils.isEqual(draftState.mouse.delta?.tile, CoordsUtils.zero())
|
||||
state.mode.type !== 'CURSOR' ||
|
||||
!hasMovedTile(state.mouse) ||
|
||||
!state.mouse.delta ||
|
||||
CoordsUtils.isEqual(state.mouse.delta.tile, CoordsUtils.zero())
|
||||
)
|
||||
return;
|
||||
|
||||
// User has moved tile since the last event
|
||||
|
||||
if (draftState.mode.mousedown) {
|
||||
if (state.mode.mousedown) {
|
||||
// User is in mousedown mode
|
||||
if (draftState.mode.mousedown.items.length > 0) {
|
||||
if (state.mode.mousedown.items.length > 0) {
|
||||
// User's last mousedown action was on a node
|
||||
draftState.mode = {
|
||||
state.uiStateActions.setMode({
|
||||
type: 'DRAG_ITEMS',
|
||||
showCursor: true,
|
||||
items: draftState.mode.mousedown.items
|
||||
};
|
||||
items: state.mode.mousedown.items
|
||||
});
|
||||
}
|
||||
|
||||
// draftState.mode = {
|
||||
@@ -36,60 +43,59 @@ export const Cursor: InteractionReducer = {
|
||||
// };
|
||||
}
|
||||
},
|
||||
mousedown: (draftState) => {
|
||||
if (draftState.mode.type !== 'CURSOR' || !draftState.isRendererInteraction)
|
||||
return;
|
||||
mousedown: (state) => {
|
||||
if (state.mode.type !== 'CURSOR' || !state.isRendererInteraction) return;
|
||||
|
||||
const itemsAtTile = filterNodesByTile({
|
||||
tile: draftState.mouse.position.tile,
|
||||
nodes: draftState.scene.nodes
|
||||
tile: state.mouse.position.tile,
|
||||
nodes: state.scene.nodes
|
||||
});
|
||||
|
||||
draftState.mode.mousedown = {
|
||||
items: itemsAtTile,
|
||||
tile: draftState.mouse.position.tile
|
||||
};
|
||||
},
|
||||
mouseup: (draftState) => {
|
||||
if (draftState.mode.type !== 'CURSOR') return;
|
||||
|
||||
draftState.scene.nodes = draftState.scene.nodes.map((node) => {
|
||||
return {
|
||||
...node,
|
||||
isSelected: false
|
||||
const newMode = produce(state.mode, (draftState) => {
|
||||
draftState.mousedown = {
|
||||
items: itemsAtTile,
|
||||
tile: state.mouse.position.tile
|
||||
};
|
||||
});
|
||||
|
||||
if (draftState.mode.mousedown !== null) {
|
||||
state.uiStateActions.setMode(newMode);
|
||||
},
|
||||
mouseup: (state) => {
|
||||
if (state.mode.type !== 'CURSOR') return;
|
||||
|
||||
state.scene.nodes.forEach((node) => {
|
||||
if (node.isSelected)
|
||||
state.sceneActions.updateNode(node.id, { isSelected: false });
|
||||
});
|
||||
|
||||
if (state.mode.mousedown !== null) {
|
||||
// User's last mousedown action was on a scene item
|
||||
const mousedownNode = draftState.mode.mousedown.items[0];
|
||||
const mousedownNode = state.mode.mousedown.items[0];
|
||||
|
||||
if (mousedownNode) {
|
||||
// The user's last mousedown action was on a node
|
||||
const nodeIndex = draftState.scene.nodes.findIndex((node) => {
|
||||
return node.id === mousedownNode.id;
|
||||
});
|
||||
const { item: node } = getItemById(state.scene.nodes, mousedownNode.id);
|
||||
|
||||
if (nodeIndex === -1) return;
|
||||
|
||||
draftState.contextMenu = draftState.scene.nodes[nodeIndex];
|
||||
draftState.scene.nodes[nodeIndex].isSelected = true;
|
||||
draftState.itemControls = {
|
||||
state.uiStateActions.setContextMenu(node);
|
||||
// state.sceneActions.updateNode(node.id, { isSelected: true });
|
||||
state.uiStateActions.setItemControls({
|
||||
type: ItemControlsTypeEnum.SINGLE_NODE,
|
||||
nodeId: draftState.scene.nodes[nodeIndex].id
|
||||
};
|
||||
draftState.mode.mousedown = null;
|
||||
|
||||
return;
|
||||
nodeId: node.id
|
||||
});
|
||||
} else {
|
||||
// Empty tile selected
|
||||
state.uiStateActions.setContextMenu({
|
||||
type: 'EMPTY_TILE',
|
||||
position: state.mouse.position.tile
|
||||
});
|
||||
state.uiStateActions.setItemControls(null);
|
||||
}
|
||||
|
||||
// Empty tile selected
|
||||
draftState.contextMenu = {
|
||||
type: 'EMPTY_TILE',
|
||||
position: draftState.mouse.position.tile
|
||||
};
|
||||
draftState.itemControls = null;
|
||||
draftState.mode.mousedown = null;
|
||||
const newMode = produce(state.mode, (draftState) => {
|
||||
draftState.mousedown = null;
|
||||
});
|
||||
|
||||
state.uiStateActions.setMode(newMode);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,38 +1,38 @@
|
||||
import { produce } from 'immer';
|
||||
import { hasMovedTile } from 'src/utils';
|
||||
import { InteractionReducer } from 'src/types';
|
||||
|
||||
export const DragItems: InteractionReducer = {
|
||||
type: 'DRAG_ITEMS',
|
||||
entry: (draftState) => {
|
||||
draftState.rendererRef.style.userSelect = 'none';
|
||||
entry: (state) => {
|
||||
const renderer = state.rendererRef;
|
||||
renderer.style.userSelect = 'none';
|
||||
},
|
||||
exit: (draftState) => {
|
||||
draftState.rendererRef.style.userSelect = 'auto';
|
||||
exit: (state) => {
|
||||
const renderer = state.rendererRef;
|
||||
renderer.style.userSelect = 'auto';
|
||||
},
|
||||
mousemove: (draftState) => {
|
||||
if (draftState.mode.type !== 'DRAG_ITEMS' || !draftState.mouse.mousedown)
|
||||
mousemove: (state) => {
|
||||
if (
|
||||
state.mode.type !== 'DRAG_ITEMS' ||
|
||||
!state.mouse.mousedown ||
|
||||
!hasMovedTile
|
||||
)
|
||||
return;
|
||||
|
||||
// User is dragging
|
||||
const newScene = draftState.mode.items.reduce((acc, node) => {
|
||||
return produce(acc, (draft) => {
|
||||
const afterNodeUpdates = draftState.sceneActions.updateNode(
|
||||
node.id,
|
||||
{
|
||||
position: draftState.mouse.position.tile
|
||||
},
|
||||
acc
|
||||
);
|
||||
|
||||
draft.nodes = afterNodeUpdates.nodes;
|
||||
draft.connectors = afterNodeUpdates.connectors;
|
||||
state.mode.items.forEach((node) => {
|
||||
state.sceneActions.updateNode(node.id, {
|
||||
position: state.mouse.position.tile
|
||||
});
|
||||
}, draftState.scene);
|
||||
});
|
||||
|
||||
draftState.scene = newScene;
|
||||
draftState.contextMenu = null;
|
||||
state.uiStateActions.setContextMenu(null);
|
||||
},
|
||||
mouseup: (draftState) => {
|
||||
draftState.mode = { type: 'CURSOR', showCursor: true, mousedown: null };
|
||||
mouseup: (state) => {
|
||||
state.uiStateActions.setMode({
|
||||
type: 'CURSOR',
|
||||
showCursor: true,
|
||||
mousedown: null
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { produce } from 'immer';
|
||||
import { CoordsUtils, setWindowCursor } from 'src/utils';
|
||||
import { InteractionReducer } from 'src/types';
|
||||
|
||||
@@ -9,16 +10,17 @@ export const Pan: InteractionReducer = {
|
||||
exit: () => {
|
||||
setWindowCursor('default');
|
||||
},
|
||||
mousemove: (draftState) => {
|
||||
if (draftState.mode.type !== 'PAN') return;
|
||||
mousemove: (state) => {
|
||||
if (state.mode.type !== 'PAN') return;
|
||||
|
||||
if (draftState.mouse.mousedown !== null) {
|
||||
draftState.scroll.position = draftState.mouse.delta?.screen
|
||||
? CoordsUtils.add(
|
||||
draftState.scroll.position,
|
||||
draftState.mouse.delta.screen
|
||||
)
|
||||
: draftState.scroll.position;
|
||||
if (state.mouse.mousedown !== null) {
|
||||
const newScroll = produce(state.scroll, (draftState) => {
|
||||
draftState.position = state.mouse.delta?.screen
|
||||
? CoordsUtils.add(draftState.position, state.mouse.delta.screen)
|
||||
: draftState.position;
|
||||
});
|
||||
|
||||
state.uiStateActions.setScroll(newScroll);
|
||||
}
|
||||
},
|
||||
mousedown: () => {
|
||||
|
||||
@@ -1,44 +1,47 @@
|
||||
import { produce } from 'immer';
|
||||
import { InteractionReducer } from 'src/types';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { nodeInputToNode, filterNodesByTile } from 'src/utils';
|
||||
import { filterNodesByTile, generateId } from 'src/utils';
|
||||
|
||||
export const PlaceElement: InteractionReducer = {
|
||||
type: 'PLACE_ELEMENT',
|
||||
mousemove: () => {},
|
||||
mousedown: (draftState) => {
|
||||
if (draftState.mode.type !== 'PLACE_ELEMENT') return;
|
||||
mousedown: (state) => {
|
||||
if (state.mode.type !== 'PLACE_ELEMENT') return;
|
||||
|
||||
if (!draftState.mode.icon) {
|
||||
if (!state.mode.icon) {
|
||||
const itemsAtTile = filterNodesByTile({
|
||||
tile: draftState.mouse.position.tile,
|
||||
nodes: draftState.scene.nodes
|
||||
tile: state.mouse.position.tile,
|
||||
nodes: state.scene.nodes
|
||||
});
|
||||
|
||||
draftState.mode = {
|
||||
state.uiStateActions.setMode({
|
||||
type: 'CURSOR',
|
||||
mousedown: {
|
||||
items: itemsAtTile,
|
||||
tile: draftState.mouse.position.tile
|
||||
tile: state.mouse.position.tile
|
||||
},
|
||||
showCursor: true
|
||||
};
|
||||
|
||||
draftState.itemControls = null;
|
||||
}
|
||||
},
|
||||
mouseup: (draftState) => {
|
||||
if (draftState.mode.type !== 'PLACE_ELEMENT') return;
|
||||
|
||||
if (draftState.mode.icon !== null) {
|
||||
const newNode = nodeInputToNode({
|
||||
id: uuid(),
|
||||
iconId: draftState.mode.icon.id,
|
||||
label: 'New Node',
|
||||
position: draftState.mouse.position.tile
|
||||
});
|
||||
|
||||
draftState.mode.icon = null;
|
||||
draftState.scene.nodes.push(newNode);
|
||||
state.uiStateActions.setItemControls(null);
|
||||
}
|
||||
},
|
||||
mouseup: (state) => {
|
||||
if (state.mode.type !== 'PLACE_ELEMENT') return;
|
||||
|
||||
if (state.mode.icon !== null) {
|
||||
state.sceneActions.createNode({
|
||||
id: generateId(),
|
||||
iconId: state.mode.icon.id,
|
||||
label: 'New Node',
|
||||
position: state.mouse.position.tile
|
||||
});
|
||||
|
||||
const newMode = produce(state.mode, (draftState) => {
|
||||
draftState.icon = null;
|
||||
});
|
||||
|
||||
state.uiStateActions.setMode(newMode);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,25 +1,24 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { produce } from 'immer';
|
||||
import { useSceneStore } from 'src/stores/sceneStore';
|
||||
import { useUiStateStore } from 'src/stores/uiStateStore';
|
||||
import { InteractionReducer, State } from 'src/types';
|
||||
import { getMouse } from 'src/utils';
|
||||
import { DragItems } from './reducers/DragItems';
|
||||
import { Pan } from './reducers/Pan';
|
||||
import { Cursor } from './reducers/Cursor';
|
||||
import { Lasso } from './reducers/Lasso';
|
||||
import { PlaceElement } from './reducers/PlaceElement';
|
||||
import { Connector } from './reducers/Connector';
|
||||
import { DragItems } from './reducers/DragItems';
|
||||
import { AreaTool } from './reducers/AreaTool';
|
||||
import { Connector } from './reducers/Connector';
|
||||
import { Pan } from './reducers/Pan';
|
||||
import { PlaceElement } from './reducers/PlaceElement';
|
||||
// import { Lasso } from './reducers/Lasso';
|
||||
|
||||
const reducers: { [k in string]: InteractionReducer } = {
|
||||
CURSOR: Cursor,
|
||||
DRAG_ITEMS: DragItems,
|
||||
PAN: Pan,
|
||||
LASSO: Lasso,
|
||||
PLACE_ELEMENT: PlaceElement,
|
||||
AREA_TOOL: AreaTool,
|
||||
CONNECTOR: Connector,
|
||||
AREA_TOOL: AreaTool
|
||||
PAN: Pan,
|
||||
PLACE_ELEMENT: PlaceElement
|
||||
// LASSO: Lasso,
|
||||
};
|
||||
|
||||
export const useInteractionManager = () => {
|
||||
@@ -98,49 +97,48 @@ export const useInteractionManager = () => {
|
||||
itemControls,
|
||||
rendererRef: rendererRef.current,
|
||||
sceneActions,
|
||||
uiStateActions,
|
||||
isRendererInteraction: rendererRef.current === e.target
|
||||
};
|
||||
|
||||
const getTransitionaryState = () => {
|
||||
if (reducerTypeRef.current === reducer.type) return null;
|
||||
// const getTransitionaryState = () => {
|
||||
// if (reducerTypeRef.current === reducer.type) return null;
|
||||
|
||||
const prevReducerExitFn = reducerTypeRef.current
|
||||
? reducers[reducerTypeRef.current].exit
|
||||
: null;
|
||||
const nextReducerEntryFn = reducer.entry;
|
||||
// const prevReducerExitFn = reducerTypeRef.current
|
||||
// ? reducers[reducerTypeRef.current].exit
|
||||
// : null;
|
||||
// const nextReducerEntryFn = reducer.entry;
|
||||
|
||||
reducerTypeRef.current = reducer.type;
|
||||
// reducerTypeRef.current = reducer.type;
|
||||
|
||||
const transitionaryState: State = baseState;
|
||||
// const transitionaryState: State = baseState;
|
||||
|
||||
const setTransitionaryState = (state: State, transitionaryFn: any) => {
|
||||
return produce(state, (draft) => {
|
||||
return transitionaryFn(draft);
|
||||
});
|
||||
};
|
||||
// const setTransitionaryState = (state: State, transitionaryFn: any) => {
|
||||
// return produce(state, (draft) => {
|
||||
// return transitionaryFn(draft);
|
||||
// });
|
||||
// };
|
||||
|
||||
if (prevReducerExitFn) {
|
||||
setTransitionaryState(transitionaryState, prevReducerExitFn);
|
||||
}
|
||||
// if (prevReducerExitFn) {
|
||||
// setTransitionaryState(transitionaryState, prevReducerExitFn);
|
||||
// }
|
||||
|
||||
if (nextReducerEntryFn) {
|
||||
setTransitionaryState(transitionaryState, nextReducerEntryFn);
|
||||
}
|
||||
// if (nextReducerEntryFn) {
|
||||
// setTransitionaryState(transitionaryState, nextReducerEntryFn);
|
||||
// }
|
||||
|
||||
return null;
|
||||
};
|
||||
// return null;
|
||||
// };
|
||||
|
||||
const transitionaryState = getTransitionaryState();
|
||||
const newState = produce(transitionaryState ?? baseState, (draft) => {
|
||||
return reducerAction(draft);
|
||||
});
|
||||
// const transitionaryState = getTransitionaryState();
|
||||
reducerAction(baseState);
|
||||
|
||||
uiStateActions.setMouse(nextMouse);
|
||||
uiStateActions.setScroll(newState.scroll);
|
||||
uiStateActions.setMode(newState.mode);
|
||||
uiStateActions.setContextMenu(newState.contextMenu);
|
||||
uiStateActions.setItemControls(newState.itemControls);
|
||||
sceneActions.updateScene(newState.scene);
|
||||
// uiStateActions.setScroll(newState.scroll);
|
||||
// uiStateActions.setMode(newState.mode);
|
||||
// uiStateActions.setContextMenu(newState.contextMenu);
|
||||
// uiStateActions.setItemControls(newState.itemControls);
|
||||
// sceneActions.updateScene(newState.scene);
|
||||
},
|
||||
[
|
||||
mode,
|
||||
|
||||
@@ -7,7 +7,9 @@ import {
|
||||
getItemById,
|
||||
getConnectorPath,
|
||||
groupInputToGroup,
|
||||
sceneInputtoScene
|
||||
connectorInputToConnector,
|
||||
sceneInputtoScene,
|
||||
nodeInputToNode
|
||||
} from 'src/utils';
|
||||
|
||||
interface Actions {
|
||||
@@ -30,8 +32,6 @@ const initialState = () => {
|
||||
const newScene = sceneInputtoScene(scene);
|
||||
|
||||
set(newScene);
|
||||
|
||||
return newScene;
|
||||
},
|
||||
|
||||
updateScene: (scene) => {
|
||||
@@ -42,8 +42,16 @@ const initialState = () => {
|
||||
});
|
||||
},
|
||||
|
||||
updateNode: (id, updates, scene) => {
|
||||
const newScene = produce(scene ?? get(), (draftState) => {
|
||||
createNode: (node) => {
|
||||
const newScene = produce(get(), (draftState) => {
|
||||
draftState.nodes.push(nodeInputToNode(node));
|
||||
});
|
||||
|
||||
set({ nodes: newScene.nodes });
|
||||
},
|
||||
|
||||
updateNode: (id, updates) => {
|
||||
const newScene = produce(get(), (draftState) => {
|
||||
const { item: node, index } = getItemById(draftState.nodes, id);
|
||||
|
||||
draftState.nodes[index] = {
|
||||
@@ -66,8 +74,32 @@ const initialState = () => {
|
||||
});
|
||||
|
||||
set({ nodes: newScene.nodes, connectors: newScene.connectors });
|
||||
},
|
||||
|
||||
return newScene;
|
||||
updateConnector: (id, updates) => {
|
||||
const newScene = produce(get(), (draftState) => {
|
||||
const { item: connector, index } = getItemById(
|
||||
draftState.connectors,
|
||||
id
|
||||
);
|
||||
|
||||
draftState.connectors[index] = {
|
||||
...connector,
|
||||
...updates
|
||||
};
|
||||
});
|
||||
|
||||
set({ connectors: newScene.connectors });
|
||||
},
|
||||
|
||||
createConnector: (connector) => {
|
||||
const newScene = produce(get(), (draftState) => {
|
||||
draftState.connectors.push(
|
||||
connectorInputToConnector(connector, draftState.nodes)
|
||||
);
|
||||
});
|
||||
|
||||
set({ connectors: newScene.connectors });
|
||||
},
|
||||
|
||||
createGroup: (group) => {
|
||||
@@ -76,8 +108,6 @@ const initialState = () => {
|
||||
});
|
||||
|
||||
set({ groups: newScene.groups });
|
||||
|
||||
return newScene;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -32,6 +32,7 @@ const initialState = () => {
|
||||
debugMode: false,
|
||||
zoom: 1,
|
||||
rendererSize: { width: 0, height: 0 },
|
||||
// TODO: Are all these actions needed?
|
||||
actions: {
|
||||
setMode: (mode) => {
|
||||
set({ mode });
|
||||
|
||||
@@ -6,7 +6,8 @@ import {
|
||||
ItemControls,
|
||||
Mouse,
|
||||
Scene,
|
||||
SceneActions
|
||||
SceneActions,
|
||||
UiStateActions
|
||||
} from 'src/types';
|
||||
|
||||
export interface State {
|
||||
@@ -15,13 +16,14 @@ export interface State {
|
||||
scroll: Scroll;
|
||||
scene: Scene;
|
||||
sceneActions: SceneActions;
|
||||
uiStateActions: UiStateActions;
|
||||
contextMenu: ContextMenu;
|
||||
itemControls: ItemControls;
|
||||
rendererRef: HTMLElement;
|
||||
isRendererInteraction: boolean;
|
||||
}
|
||||
|
||||
export type InteractionReducerAction = (state: Draft<State>) => void;
|
||||
export type InteractionReducerAction = (state: State) => void;
|
||||
|
||||
export type InteractionReducer = {
|
||||
type: string;
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { Coords, Size } from './common';
|
||||
import { IconInput, SceneInput, GroupInput } from './inputs';
|
||||
import {
|
||||
IconInput,
|
||||
SceneInput,
|
||||
GroupInput,
|
||||
ConnectorInput,
|
||||
NodeInput
|
||||
} from './inputs';
|
||||
|
||||
export enum TileOriginEnum {
|
||||
CENTER = 'CENTER',
|
||||
@@ -63,8 +69,11 @@ export type Icon = IconInput;
|
||||
export interface SceneActions {
|
||||
setScene: (scene: SceneInput) => void;
|
||||
updateScene: (scene: Scene) => void;
|
||||
updateNode: (id: string, updates: Partial<Node>, scene?: Scene) => Scene;
|
||||
createGroup: (group: GroupInput) => Scene;
|
||||
updateNode: (id: string, updates: Partial<Node>) => void;
|
||||
updateConnector: (id: string, updates: Partial<Connector>) => void;
|
||||
createNode: (node: NodeInput) => void;
|
||||
createConnector: (connector: ConnectorInput) => void;
|
||||
createGroup: (group: GroupInput) => void;
|
||||
}
|
||||
|
||||
export type Scene = {
|
||||
|
||||
@@ -388,3 +388,9 @@ export const getRectangleFromSize: GetRectangleFromSize = (from, size) => {
|
||||
to: { x: from.x + size.width, y: from.y + size.height }
|
||||
};
|
||||
};
|
||||
|
||||
export const hasMovedTile = (mouse: Mouse) => {
|
||||
return (
|
||||
mouse.delta && !CoordsUtils.isEqual(mouse.delta.tile, CoordsUtils.zero())
|
||||
);
|
||||
};
|
||||
|
||||
@@ -14,8 +14,9 @@
|
||||
"strict": true,
|
||||
"module": "es6",
|
||||
"moduleResolution": "node",
|
||||
"skipLibCheck": true
|
||||
"skipLibCheck": true,
|
||||
},
|
||||
// "exclude": ["src/interaction/reducers/*"],
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
|
||||
Reference in New Issue
Block a user