feat: implements connectors

This commit is contained in:
Mark Mankarious
2023-07-27 14:51:18 +01:00
committed by GitHub
parent 443c1f9b84
commit 43aab4758e
15 changed files with 306 additions and 17 deletions

40
package-lock.json generated
View File

@@ -18,6 +18,7 @@
"gsap": "^3.11.4",
"immer": "^10.0.2",
"paper": "^0.12.17",
"pathfinding": "^0.4.18",
"react-hook-form": "^7.43.2",
"react-quill": "^2.0.0",
"react-router-dom": "^6.8.1",
@@ -32,6 +33,7 @@
"@types/chroma-js": "^2.4.0",
"@types/jest": "^27.5.2",
"@types/jsdom": "^21.1.0",
"@types/pathfinding": "^0.0.6",
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"@types/uuid": "^9.0.2",
@@ -2725,6 +2727,12 @@
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
"integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA=="
},
"node_modules/@types/pathfinding": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/@types/pathfinding/-/pathfinding-0.0.6.tgz",
"integrity": "sha512-yk2LRAKAOW6m1g/4le8tk8go6wjNmCq2EA1l6Rw3tkBautda3FkBzgCLdSNUPiTZPkjxI/OYBwHmJWoYUSk7RQ==",
"dev": true
},
"node_modules/@types/prettier": {
"version": "2.7.2",
"resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.2.tgz",
@@ -7087,6 +7095,11 @@
"he": "bin/he"
}
},
"node_modules/heap": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/heap/-/heap-0.2.5.tgz",
"integrity": "sha512-G7HLD+WKcrOyJP5VQwYZNC3Z6FcQ7YYjEFiFoIj8PfEr73mu421o8B1N5DKUcc8K37EsJ2XXWA8DtrDz/2dReg=="
},
"node_modules/hoist-non-react-statics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
@@ -11167,6 +11180,14 @@
"node": ">=8"
}
},
"node_modules/pathfinding": {
"version": "0.4.18",
"resolved": "https://registry.npmjs.org/pathfinding/-/pathfinding-0.4.18.tgz",
"integrity": "sha512-R0TGEQ9GRcFCDvAWlJAWC+KGJ9SLbW4c0nuZRcioVlXVTlw+F5RvXQ8SQgSqI9KXWC1ew95vgmIiyaWTlCe9Ag==",
"dependencies": {
"heap": "0.2.5"
}
},
"node_modules/picocolors": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
@@ -16136,6 +16157,12 @@
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
"integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA=="
},
"@types/pathfinding": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/@types/pathfinding/-/pathfinding-0.0.6.tgz",
"integrity": "sha512-yk2LRAKAOW6m1g/4le8tk8go6wjNmCq2EA1l6Rw3tkBautda3FkBzgCLdSNUPiTZPkjxI/OYBwHmJWoYUSk7RQ==",
"dev": true
},
"@types/prettier": {
"version": "2.7.2",
"resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.2.tgz",
@@ -19397,6 +19424,11 @@
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
"dev": true
},
"heap": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/heap/-/heap-0.2.5.tgz",
"integrity": "sha512-G7HLD+WKcrOyJP5VQwYZNC3Z6FcQ7YYjEFiFoIj8PfEr73mu421o8B1N5DKUcc8K37EsJ2XXWA8DtrDz/2dReg=="
},
"hoist-non-react-statics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
@@ -22427,6 +22459,14 @@
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="
},
"pathfinding": {
"version": "0.4.18",
"resolved": "https://registry.npmjs.org/pathfinding/-/pathfinding-0.4.18.tgz",
"integrity": "sha512-R0TGEQ9GRcFCDvAWlJAWC+KGJ9SLbW4c0nuZRcioVlXVTlw+F5RvXQ8SQgSqI9KXWC1ew95vgmIiyaWTlCe9Ag==",
"requires": {
"heap": "0.2.5"
}
},
"picocolors": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",

View File

@@ -25,6 +25,7 @@
"@types/chroma-js": "^2.4.0",
"@types/jest": "^27.5.2",
"@types/jsdom": "^21.1.0",
"@types/pathfinding": "^0.0.6",
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"@types/uuid": "^9.0.2",
@@ -67,6 +68,7 @@
"gsap": "^3.11.4",
"immer": "^10.0.2",
"paper": "^0.12.17",
"pathfinding": "^0.4.18",
"react-hook-form": "^7.43.2",
"react-quill": "^2.0.0",
"react-router-dom": "^6.8.1",

View File

@@ -5,9 +5,10 @@ import ReactDOM from 'react-dom/client';
import GlobalStyles from '@mui/material/GlobalStyles';
import type {
SceneInput,
NodeInput,
ConnectorInput,
IconInput,
GroupInput,
NodeInput
GroupInput
} from 'src/validation/SceneInput';
import Isoflow, { useIsoflow } from './App';
@@ -61,6 +62,15 @@ const icons: IconInput[] = [
}
];
const connectors: ConnectorInput[] = [
{
id: 'Connector1',
label: 'Connector 1',
from: 'Node1',
to: 'Node2'
}
];
const groups: GroupInput[] = [
{
id: 'Group1',
@@ -124,7 +134,7 @@ const DataLayer = () => {
initialScene={{
icons,
nodes,
connectors: [],
connectors,
groups,
gridSize: {
width: 51,

View File

@@ -37,8 +37,8 @@ export const useInteractionManager = () => {
const uiStateActions = useUiStateStore((state) => {
return state.actions;
});
const scene = useSceneStore(({ nodes, groups }) => {
return { nodes, groups };
const scene = useSceneStore(({ nodes, connectors, groups }) => {
return { nodes, connectors, groups };
});
const gridSize = useSceneStore((state) => {
return state.gridSize;

View File

@@ -11,13 +11,14 @@ import { Node } from './components/Node/Node';
import { getTilePosition } from './utils/gridHelpers';
import { ContextMenuLayer } from './components/ContextMenuLayer/ContextMenuLayer';
import { Lasso } from './components/Lasso/Lasso';
import { Connector } from './components/Connector/Connector';
import { Group } from './components/Group/Group';
const InitialisedRenderer = () => {
const renderer = useRenderer();
const [isReady, setIsReady] = useState(false);
const scene = useSceneStore(({ nodes, groups }) => {
return { nodes, groups };
const scene = useSceneStore(({ nodes, connectors, groups }) => {
return { nodes, connectors, groups };
});
const gridSize = useSceneStore((state) => {
return state.gridSize;
@@ -107,12 +108,21 @@ const InitialisedRenderer = () => {
endTile={mode.selection.endTile}
/>
)}
{scene.connectors.map((connector) => {
return (
<Connector
key={connector.id}
connector={connector}
parentContainer={renderer.connectorManager.container as paper.Group}
/>
);
})}
{scene.groups.map((group) => {
return (
<Group
key={group.id}
group={group}
parentContainer={renderer.groupManager.container as paper.Group}
group={group}
/>
);
})}

View File

@@ -0,0 +1,47 @@
import { useEffect } from 'react';
import {
Connector as ConnectorInterface,
useSceneStore
} from 'src/stores/useSceneStore';
import { useConnector } from './useConnector';
interface ConnectorProps {
connector: ConnectorInterface;
parentContainer: paper.Group;
}
export const Connector = ({ parentContainer, connector }: ConnectorProps) => {
const { init, updateFromTo, updateColor } = useConnector();
const gridSize = useSceneStore((state) => {
return state.gridSize;
});
const nodes = useSceneStore((state) => {
return state.nodes;
});
useEffect(() => {
const container = init();
parentContainer.addChild(container);
}, [parentContainer, init]);
useEffect(() => {
updateColor(connector.color);
}, [connector, updateColor]);
useEffect(() => {
const fromNode = nodes.find((node) => {
return node.id === connector.from;
});
const toNode = nodes.find((node) => {
return node.id === connector.to;
});
if (!fromNode || !toNode) return;
updateFromTo(gridSize, fromNode.position, toNode.position);
}, [gridSize, nodes, connector, updateFromTo]);
return null;
};

View File

@@ -0,0 +1,54 @@
import { useCallback, useRef } from 'react';
import { Group, Path } from 'paper';
import { pathfinder } from 'src/renderer/utils/pathfinder';
import { Coords } from 'src/utils/Coords';
import { getTileBounds } from 'src/renderer/utils/gridHelpers';
export const useConnector = () => {
const containerRef = useRef(new Group());
const pathRef = useRef<paper.Path>();
const updateColor = useCallback((color: string) => {
if (!pathRef.current) return;
pathRef.current.set({
strokeColor: color
});
}, []);
const updateFromTo = useCallback(
(gridSize: Coords, from: Coords, to: Coords) => {
if (!pathRef.current) return;
const { findPath } = pathfinder(gridSize);
const path = findPath([from, to]);
const points = path.map((tile) => {
return getTileBounds(tile).center;
});
pathRef.current.set({
segments: points
});
},
[]
);
const init = useCallback(() => {
containerRef.current.removeChildren();
pathRef.current = new Path({
strokeWidth: 5
});
containerRef.current.addChild(pathRef.current);
return containerRef.current;
}, []);
return {
init,
updateColor,
updateFromTo
};
};

View File

@@ -0,0 +1,10 @@
import { useRef } from 'react';
import { Group } from 'paper';
export const useConnectorManager = () => {
const containerRef = useRef(new Group());
return {
container: containerRef.current
};
};

View File

@@ -6,6 +6,7 @@ import { useGrid } from './components/Grid/useGrid';
import { useNodeManager } from './useNodeManager';
import { useCursor } from './components/Cursor/useCursor';
import { useGroupManager } from './useGroupManager';
import { useConnectorManager } from './useConnectorManager';
export const useRenderer = () => {
const container = useRef(new Group());
@@ -14,6 +15,7 @@ export const useRenderer = () => {
const lassoContainer = useRef(new Group());
const grid = useGrid();
const nodeManager = useNodeManager();
const connectorManager = useConnectorManager();
const groupManager = useGroupManager();
const cursor = useCursor();
const uiStateActions = useUiStateStore((state) => {
@@ -30,6 +32,8 @@ export const useRenderer = () => {
const init = useCallback(
(gridSize: Coords) => {
// TODO: Grid and Cursor should be initialised in their JSX components (create if they don't exist)
// to be inline with other initialisation patterns
const gridContainer = initGrid(gridSize);
const cursorContainer = initCursor();
@@ -37,6 +41,7 @@ export const useRenderer = () => {
innerContainer.current.addChild(groupManager.container);
innerContainer.current.addChild(cursorContainer);
innerContainer.current.addChild(lassoContainer.current);
innerContainer.current.addChild(connectorManager.container);
innerContainer.current.addChild(nodeManager.container);
container.current.addChild(innerContainer.current);
container.current.set({ position: [0, 0] });
@@ -46,7 +51,13 @@ export const useRenderer = () => {
offset: new Coords(0, 0)
});
},
[initGrid, initCursor, setScroll, nodeManager.container]
[
initGrid,
initCursor,
setScroll,
nodeManager.container,
groupManager.container
]
);
const scrollTo = useCallback((to: Coords) => {
@@ -68,7 +79,8 @@ export const useRenderer = () => {
scrollTo,
nodeManager,
groupManager,
cursor,
lassoContainer
lassoContainer,
connectorManager,
cursor
};
};

View File

@@ -0,0 +1,67 @@
import PF from 'pathfinding';
import { Coords } from 'src/utils/Coords';
// TODO1: This file is a mess, refactor it
// TODO: Have one single place for utils
export const pathfinder = (gridSize: Coords) => {
const grid = new PF.Grid(gridSize.x, gridSize.y);
const finder = new PF.AStarFinder({
heuristic: PF.Heuristic.manhattan,
diagonalMovement: PF.DiagonalMovement.Always
});
const convertToGridXY = ({ x, y }: Coords) => {
return new Coords(
x + Math.floor(gridSize.x * 0.5),
y + Math.floor(gridSize.y * 0.5)
);
};
const convertToSceneXY = ({ x, y }: Coords) => {
return new Coords(
x - Math.floor(gridSize.x * 0.5),
y - Math.floor(gridSize.y * 0.5)
);
};
const setWalkableAt = (coords: Coords, isWalkable: boolean) => {
const { x, y } = convertToGridXY(coords);
grid.setWalkableAt(x, y, isWalkable);
};
const findPath = (tiles: Coords[]) => {
const normalisedRoute = tiles.map((tile) => {
return convertToGridXY(tile);
});
const path = normalisedRoute.reduce((acc, stop, i) => {
if (i === 0) {
return acc;
}
const workingGrid = grid.clone();
workingGrid.setWalkableAt(stop.x, stop.y, true);
const prevStop = normalisedRoute[i - 1];
const segment = finder.findPath(
prevStop.x,
prevStop.y,
stop.x,
stop.y,
workingGrid
);
return [...acc, ...(i > 1 ? segment.slice(1) : segment)];
}, [] as number[][]);
return path.map((tile) => {
return convertToSceneXY(new Coords(tile[0], tile[1]));
});
};
return {
setWalkableAt,
findPath
};
};

View File

@@ -1,7 +1,7 @@
import { Matrix, Point } from 'paper';
export const getProjectionMatrix = (x: number, y: number) =>
new Matrix([
export const getProjectionMatrix = (x: number, y: number) => {
return new Matrix([
Math.sqrt(2) / 2,
Math.sqrt(6) / 6,
-(Math.sqrt(2) / 2),
@@ -9,6 +9,7 @@ export const getProjectionMatrix = (x: number, y: number) =>
x - (Math.sqrt(2) / 2) * (x - y),
y - (Math.sqrt(6) / 6) * (x + y - 2)
]);
};
export const applyProjectionMatrix = (
item: paper.Item,

View File

@@ -9,6 +9,7 @@ import { Coords } from '../utils/Coords';
// TODO: Move all types into a types file for easier access and less mental load over where to look
export enum SceneItemTypeEnum {
NODE = 'NODE',
CONNECTOR = 'CONNECTOR',
GROUP = 'GROUP'
}
@@ -23,6 +24,14 @@ export interface Node {
isSelected: boolean;
}
export interface Connector {
type: SceneItemTypeEnum.CONNECTOR;
id: string;
color: string;
from: string;
to: string;
}
export interface Group {
type: SceneItemTypeEnum.GROUP;
id: string;
@@ -41,6 +50,7 @@ export interface SortedSceneItems {
// TODO: Decide on whether to make a Map instead of an array for easier lookup
nodes: Node[];
groups: Group[];
connectors: Connector[];
}
// TODO: This typing is super confusing to work with
@@ -64,6 +74,7 @@ export type UseSceneStore = Scene & {
export const useSceneStore = create<UseSceneStore>((set, get) => {
return {
nodes: [],
connectors: [],
groups: [],
icons: [],
gridSize: new Coords(51, 51),

View File

@@ -3,6 +3,10 @@ import { customVars } from '../styles/theme';
export const DEFAULT_COLOR = customVars.diagramPalette.blue;
export const CONNECTOR_DEFAULTS = {
width: 4
};
export const GRID_DEFAULTS = {
size: {
x: 51,

View File

@@ -1,16 +1,19 @@
import gsap from 'gsap';
import { Coords } from 'src/utils/Coords';
import { customVars } from 'src/styles/theme';
import chroma from 'chroma-js';
import type {
NodeInput,
SceneInput,
NodeInput,
ConnectorInput,
GroupInput
} from 'src/validation/SceneInput';
import {
Node,
Group,
SceneItemTypeEnum,
Scene
Scene,
Node,
Connector,
Group
} from 'src/stores/useSceneStore';
import { NODE_DEFAULTS, GRID_DEFAULTS } from 'src/utils/defaults';
@@ -79,6 +82,18 @@ export const groupInputToGroup = (groupInput: GroupInput): Group => {
};
};
export const connectorInputToConnector = (
connectorInput: ConnectorInput
): Connector => {
return {
type: SceneItemTypeEnum.CONNECTOR,
id: connectorInput.id,
color: connectorInput.color ?? customVars.diagramPalette.blue,
from: connectorInput.from,
to: connectorInput.to
};
};
export const sceneInputtoScene = (sceneInput: SceneInput) => {
const nodes = sceneInput.nodes.map((nodeInput) => {
return nodeInputToNode(nodeInput);
@@ -88,10 +103,15 @@ export const sceneInputtoScene = (sceneInput: SceneInput) => {
return groupInputToGroup(groupInput);
});
const connectors = sceneInput.connectors.map((connectorInput) => {
return connectorInputToConnector(connectorInput);
});
const scene = {
...sceneInput,
nodes,
groups,
connectors,
icons: sceneInput.icons,
gridSize: sceneInput.gridSize
? new Coords(sceneInput.gridSize.width, sceneInput.gridSize.height)

View File

@@ -23,6 +23,7 @@ export const nodeInput = z.object({
export const connectorInput = z.object({
id: z.string(),
label: z.string().nullable(),
color: z.string().optional(),
from: z.string(),
to: z.string()
});