mirror of
https://github.com/stan-smith/FossFLOW.git
synced 2025-12-24 15:09:03 -05:00
refactor: migrate away from paperjs [PHASE 2]
This commit is contained in:
110
package-lock.json
generated
110
package-lock.json
generated
@@ -59,6 +59,7 @@
|
||||
"react-dom": "^18.2.0",
|
||||
"recharts": "^2.7.2",
|
||||
"style-loader": "^3.3.3",
|
||||
"svg-url-loader": "^8.0.0",
|
||||
"ts-jest": "^29.0.5",
|
||||
"ts-loader": "^9.4.2",
|
||||
"tsconfig-paths-webpack-plugin": "^4.1.0",
|
||||
@@ -4007,6 +4008,15 @@
|
||||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/big.js": {
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
|
||||
"integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/binary-extensions": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
|
||||
@@ -5482,6 +5492,15 @@
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/emojis-list": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz",
|
||||
"integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/encodeurl": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
|
||||
@@ -6837,6 +6856,26 @@
|
||||
"node": "^10.12.0 || >=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/file-loader": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz",
|
||||
"integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"loader-utils": "^2.0.0",
|
||||
"schema-utils": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.13.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/webpack"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"webpack": "^4.0.0 || ^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
@@ -10583,6 +10622,20 @@
|
||||
"node": ">=6.11.5"
|
||||
}
|
||||
},
|
||||
"node_modules/loader-utils": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz",
|
||||
"integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"big.js": "^5.2.2",
|
||||
"emojis-list": "^3.0.0",
|
||||
"json5": "^2.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/locate-path": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
||||
@@ -13028,6 +13081,21 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/svg-url-loader": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/svg-url-loader/-/svg-url-loader-8.0.0.tgz",
|
||||
"integrity": "sha512-5doSXvl18hY1fGsRLdhWAU5jgzgxJ06/gc/26cpuDnN0xOz1HmmfhkpL29SSrdIvhtxQ1UwGzmk7wTT/l48mKw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"file-loader": "~6.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"webpack": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/symbol-tree": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
|
||||
@@ -17509,6 +17577,12 @@
|
||||
"integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==",
|
||||
"dev": true
|
||||
},
|
||||
"big.js": {
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
|
||||
"integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==",
|
||||
"dev": true
|
||||
},
|
||||
"binary-extensions": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
|
||||
@@ -18603,6 +18677,12 @@
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"dev": true
|
||||
},
|
||||
"emojis-list": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz",
|
||||
"integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==",
|
||||
"dev": true
|
||||
},
|
||||
"encodeurl": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
|
||||
@@ -19620,6 +19700,16 @@
|
||||
"flat-cache": "^3.0.4"
|
||||
}
|
||||
},
|
||||
"file-loader": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz",
|
||||
"integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"loader-utils": "^2.0.0",
|
||||
"schema-utils": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"fill-range": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
@@ -22387,6 +22477,17 @@
|
||||
"integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==",
|
||||
"dev": true
|
||||
},
|
||||
"loader-utils": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz",
|
||||
"integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"big.js": "^5.2.2",
|
||||
"emojis-list": "^3.0.0",
|
||||
"json5": "^2.1.2"
|
||||
}
|
||||
},
|
||||
"locate-path": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
||||
@@ -24223,6 +24324,15 @@
|
||||
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
|
||||
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="
|
||||
},
|
||||
"svg-url-loader": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/svg-url-loader/-/svg-url-loader-8.0.0.tgz",
|
||||
"integrity": "sha512-5doSXvl18hY1fGsRLdhWAU5jgzgxJ06/gc/26cpuDnN0xOz1HmmfhkpL29SSrdIvhtxQ1UwGzmk7wTT/l48mKw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"file-loader": "~6.2.0"
|
||||
}
|
||||
},
|
||||
"symbol-tree": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"url": "https://github.com/markmanx/isoflow.git"
|
||||
},
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/App.d.ts",
|
||||
"types": "./dist/Isoflow.d.ts",
|
||||
"scripts": {
|
||||
"start": "webpack serve --config ./webpack/dev.config.js",
|
||||
"dev": "nodemon --watch ./src/ -e ts,tsx --exec npm run build",
|
||||
@@ -51,6 +51,7 @@
|
||||
"react-dom": "^18.2.0",
|
||||
"recharts": "^2.7.2",
|
||||
"style-loader": "^3.3.3",
|
||||
"svg-url-loader": "^8.0.0",
|
||||
"ts-jest": "^29.0.5",
|
||||
"ts-loader": "^9.4.2",
|
||||
"tsconfig-paths-webpack-plugin": "^4.1.0",
|
||||
|
||||
@@ -8,13 +8,14 @@ import {
|
||||
IconInput,
|
||||
NodeInput,
|
||||
ConnectorInput,
|
||||
GroupInput
|
||||
GroupInput,
|
||||
Scene
|
||||
} from 'src/types';
|
||||
import { useSceneStore, Scene } from 'src/stores/useSceneStore';
|
||||
import { useSceneStore } from 'src/stores/useSceneStore';
|
||||
import { GlobalStyles } from 'src/styles/GlobalStyles';
|
||||
import { Renderer } from 'src/renderer/Renderer';
|
||||
import { Renderer } from 'src/components/Renderer/Renderer';
|
||||
import { sceneInputtoScene, sceneToSceneInput } from 'src/utils';
|
||||
import { DefaultLabelContainer } from 'src/renderer/components/Node/DefaultLabelContainer';
|
||||
import { LabelContainer } from 'src/components/Node/LabelContainer';
|
||||
import { ItemControlsManager } from './components/ItemControls/ItemControlsManager';
|
||||
|
||||
interface Props {
|
||||
@@ -88,7 +89,7 @@ export {
|
||||
GroupInput,
|
||||
ConnectorInput,
|
||||
useIsoflow,
|
||||
DefaultLabelContainer
|
||||
LabelContainer
|
||||
};
|
||||
|
||||
export default Isoflow;
|
||||
|
||||
9
src/assets/grid-tile-bg.svg
Normal file
9
src/assets/grid-tile-bg.svg
Normal file
@@ -0,0 +1,9 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 141.38828 163.26061">
|
||||
<g stroke="#cecece" stroke-width="1" stroke-alignment="center">
|
||||
<polygon points="70.69436 122.44546 .00022 81.63018 70.69392 40.81515 141.38806 81.63043 70.69436 122.44546" fill="none"/>
|
||||
<line x1="70.69414" y1="40.81503" x2="141.38784" />
|
||||
<line y1="0" x2="70.69414" y2="40.81528" />
|
||||
<line x1="70.69414" y1="122.44559" x2=".00044" y2="163.26061" />
|
||||
<line x1="141.38828" y1="163.26061" x2="70.69414" y2="122.44533" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 526 B |
79
src/components/Connector/Connector.tsx
Normal file
79
src/components/Connector/Connector.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Box } from '@mui/material';
|
||||
import { Connector as ConnectorI, Node, Coords, Scroll, Size } from 'src/types';
|
||||
import { Svg } from 'src/components/Svg/Svg';
|
||||
import { UNPROJECTED_TILE_SIZE } from 'src/config';
|
||||
import {
|
||||
pathfinder,
|
||||
getBoundingBox,
|
||||
getBoundingBoxSize,
|
||||
getTilePosition
|
||||
} from 'src/utils';
|
||||
import { IsoTileArea } from 'src/components/IsoTileArea/IsoTileArea';
|
||||
|
||||
interface Props {
|
||||
connector: ConnectorI;
|
||||
fromNode: Node;
|
||||
toNode: Node;
|
||||
scroll: Scroll;
|
||||
zoom: number;
|
||||
}
|
||||
|
||||
// How far a connector can be outside the grid area that covers two nodes
|
||||
const BOUNDS_OFFSET: Coords = { x: 3, y: 3 };
|
||||
|
||||
export const Connector = ({
|
||||
connector,
|
||||
fromNode,
|
||||
toNode,
|
||||
zoom,
|
||||
scroll
|
||||
}: Props) => {
|
||||
const connectorParams = useMemo(() => {
|
||||
const searchArea = getBoundingBox(
|
||||
[fromNode.position, toNode.position],
|
||||
BOUNDS_OFFSET
|
||||
);
|
||||
const searchAreaSize = getBoundingBoxSize(searchArea);
|
||||
const { findPath } = pathfinder(searchAreaSize);
|
||||
|
||||
const connectorRoute = findPath([fromNode.position, toNode.position]);
|
||||
const connectorAreaSize = getBoundingBoxSize(connectorRoute);
|
||||
const unprojectedTileSize = UNPROJECTED_TILE_SIZE * zoom;
|
||||
const path = connectorRoute.reduce((acc, tile) => {
|
||||
return `${acc} ${tile.x * unprojectedTileSize},${
|
||||
tile.y * unprojectedTileSize
|
||||
}`;
|
||||
}, '');
|
||||
const position = getTilePosition({
|
||||
tile: fromNode.position,
|
||||
zoom,
|
||||
scroll
|
||||
});
|
||||
|
||||
return { path, connectorAreaSize, position };
|
||||
}, [fromNode.position, toNode.position, zoom, scroll]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
left: connectorParams.position.x,
|
||||
top: connectorParams.position.y
|
||||
}}
|
||||
>
|
||||
<IsoTileArea
|
||||
tileArea={connectorParams.connectorAreaSize}
|
||||
zoom={zoom}
|
||||
fill="none"
|
||||
>
|
||||
<polyline
|
||||
points={connectorParams.path}
|
||||
stroke="black"
|
||||
strokeWidth={10 * zoom}
|
||||
fill="none"
|
||||
/>
|
||||
</IsoTileArea>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -1,10 +1,10 @@
|
||||
import React, { useRef, useEffect, useState } from 'react';
|
||||
import gsap from 'gsap';
|
||||
import { List, Box, Card } from '@mui/material';
|
||||
import { Coords } from 'src/types';
|
||||
import { useUiStateStore } from 'src/stores/useUiStateStore';
|
||||
import { getTileScreenPosition } from 'src/renderer/utils/gridHelpers';
|
||||
import { useSceneStore } from 'src/stores/useSceneStore';
|
||||
import gsap from 'gsap';
|
||||
// import { List, Box, Card } from '@mui/material';
|
||||
// import { useUiStateStore } from 'src/stores/useUiStateStore';
|
||||
// import { getTileScreenPosition } from 'src/renderer/utils/gridHelpers';
|
||||
// import { useSceneStore } from 'src/stores/useSceneStore';
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
|
||||
75
src/components/Cursor/Cursor.tsx
Normal file
75
src/components/Cursor/Cursor.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import React, { useEffect, useRef, useCallback, useState } from 'react';
|
||||
import { Box, useTheme } from '@mui/material';
|
||||
import gsap from 'gsap';
|
||||
import { getTilePosition } from 'src/utils';
|
||||
import { Coords, TileOriginEnum, Scroll } from 'src/types';
|
||||
import { IsoTileArea } from 'src/components/IsoTileArea/IsoTileArea';
|
||||
|
||||
interface Props {
|
||||
tile: Coords;
|
||||
scroll: Scroll;
|
||||
zoom: number;
|
||||
}
|
||||
|
||||
export const Cursor = ({ tile, zoom, scroll }: Props) => {
|
||||
const theme = useTheme();
|
||||
const [isReady, setIsReady] = useState(false);
|
||||
const containerRef = useRef<HTMLDivElement>();
|
||||
|
||||
const setPosition = useCallback(
|
||||
({
|
||||
tile: _tile,
|
||||
animationDuration = 0.15
|
||||
}: {
|
||||
tile: Coords;
|
||||
animationDuration?: number;
|
||||
}) => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
const position = getTilePosition({
|
||||
tile: _tile,
|
||||
origin: TileOriginEnum.TOP,
|
||||
scroll,
|
||||
zoom
|
||||
});
|
||||
|
||||
gsap.to(containerRef.current, {
|
||||
duration: animationDuration,
|
||||
left: position.x,
|
||||
top: position.y
|
||||
});
|
||||
},
|
||||
[zoom, scroll]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerRef.current || !isReady) return;
|
||||
|
||||
setPosition({ tile });
|
||||
}, [tile, setPosition, isReady]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerRef.current || isReady) return;
|
||||
|
||||
gsap.killTweensOf(containerRef.current);
|
||||
setPosition({ tile, animationDuration: 0 });
|
||||
containerRef.current.style.opacity = '1';
|
||||
setIsReady(true);
|
||||
}, [tile, setPosition, isReady]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={containerRef}
|
||||
sx={{
|
||||
position: 'absolute'
|
||||
}}
|
||||
>
|
||||
<IsoTileArea
|
||||
fill={theme.palette.primary.main}
|
||||
tileArea={{ width: 1, height: 1 }}
|
||||
zoom={zoom}
|
||||
cornerRadius={10 * zoom}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
45
src/components/Grid/Grid.tsx
Normal file
45
src/components/Grid/Grid.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import React, { useRef, useMemo } from 'react';
|
||||
import { Box } from '@mui/material';
|
||||
import gridTileSvg from 'src/assets/grid-tile-bg.svg';
|
||||
import { Scroll } from 'src/types';
|
||||
import { getProjectedTileSize } from 'src/utils';
|
||||
|
||||
interface Props {
|
||||
scroll: Scroll;
|
||||
zoom: number;
|
||||
}
|
||||
|
||||
export const Grid = ({ zoom, scroll }: Props) => {
|
||||
const containerRef = useRef<HTMLDivElement>();
|
||||
const projectedTileSize = useMemo(() => {
|
||||
return getProjectedTileSize({ zoom });
|
||||
}, [zoom]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={containerRef}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
overflow: 'hidden',
|
||||
pointerEvents: 'none'
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
background: `repeat url("${gridTileSvg}")`,
|
||||
backgroundSize: `${projectedTileSize.width}px`,
|
||||
backgroundPosition: `calc(50% + ${
|
||||
scroll.position.x % projectedTileSize.width
|
||||
}px) calc(50% + ${scroll.position.y % projectedTileSize.height}px)`
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
58
src/components/Group/Group.tsx
Normal file
58
src/components/Group/Group.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import chroma from 'chroma-js';
|
||||
import { Box } from '@mui/material';
|
||||
import { Node, Scroll, TileOriginEnum, Group as GroupI } from 'src/types';
|
||||
import { getBoundingBox, getTilePosition, getBoundingBoxSize } from 'src/utils';
|
||||
import { IsoTileArea } from 'src/components/IsoTileArea/IsoTileArea';
|
||||
|
||||
interface Props {
|
||||
nodes: Node[];
|
||||
group: GroupI;
|
||||
zoom: number;
|
||||
scroll: Scroll;
|
||||
}
|
||||
|
||||
export const Group = ({ nodes, zoom, scroll, group }: Props) => {
|
||||
const nodePositions = useMemo(() => {
|
||||
return nodes.map((node) => {
|
||||
return node.position;
|
||||
});
|
||||
}, [nodes]);
|
||||
|
||||
const groupAttrs = useMemo(() => {
|
||||
const corners = getBoundingBox(nodePositions, { x: 1, y: 1 });
|
||||
const size = getBoundingBoxSize(corners);
|
||||
|
||||
const position = getTilePosition({
|
||||
tile: corners[2],
|
||||
zoom,
|
||||
scroll,
|
||||
origin: TileOriginEnum.TOP
|
||||
});
|
||||
|
||||
return { size, position };
|
||||
}, [nodePositions, zoom, scroll]);
|
||||
|
||||
if (!groupAttrs) return null;
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
left: groupAttrs.position.x,
|
||||
top: groupAttrs.position.y
|
||||
}}
|
||||
>
|
||||
<IsoTileArea
|
||||
tileArea={groupAttrs.size}
|
||||
fill={chroma(group.color).alpha(0.6).css()}
|
||||
zoom={zoom}
|
||||
cornerRadius={22 * zoom}
|
||||
stroke={{
|
||||
color: group.color,
|
||||
width: 1 * zoom
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
88
src/components/IsoTileArea/IsoTileArea.tsx
Normal file
88
src/components/IsoTileArea/IsoTileArea.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Box } from '@mui/material';
|
||||
import { UNPROJECTED_TILE_SIZE } from 'src/config';
|
||||
import { Size, Coords } from 'src/types';
|
||||
import { getIsoMatrixCSS, getProjectedTileSize } from 'src/utils';
|
||||
import { Svg } from 'src/components/Svg/Svg';
|
||||
|
||||
interface Props {
|
||||
tileArea: Size;
|
||||
fill: string;
|
||||
cornerRadius?: number;
|
||||
stroke?: {
|
||||
width: number;
|
||||
color: string;
|
||||
};
|
||||
zoom: number;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const IsoTileArea = ({
|
||||
tileArea,
|
||||
fill,
|
||||
cornerRadius = 0,
|
||||
stroke,
|
||||
zoom,
|
||||
children
|
||||
}: Props) => {
|
||||
const projectedTileSize = useMemo(() => {
|
||||
return getProjectedTileSize({ zoom });
|
||||
}, [zoom]);
|
||||
|
||||
const viewbox = useMemo<Size>(() => {
|
||||
return {
|
||||
width:
|
||||
(tileArea.width / 2 + tileArea.height / 2) * projectedTileSize.width,
|
||||
height:
|
||||
(tileArea.width / 2 + tileArea.height / 2) * projectedTileSize.height
|
||||
};
|
||||
}, [tileArea, projectedTileSize]);
|
||||
|
||||
const translate = useMemo<Coords>(() => {
|
||||
return { x: tileArea.width * (projectedTileSize.width / 2), y: 0 };
|
||||
}, [tileArea, projectedTileSize]);
|
||||
|
||||
const strokeParams = useMemo(() => {
|
||||
if (!stroke) return {};
|
||||
|
||||
return {
|
||||
stroke: stroke.color,
|
||||
strokeWidth: stroke.width,
|
||||
strokeAlignment: 'center',
|
||||
strokeLineJoin: 'round',
|
||||
strokeLineCap: 'round'
|
||||
};
|
||||
}, [stroke]);
|
||||
|
||||
const marginLeft = useMemo(() => {
|
||||
return -(tileArea.width * projectedTileSize.width * 0.5);
|
||||
}, [projectedTileSize.width, tileArea.width]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
marginLeft: `${marginLeft}px`
|
||||
}}
|
||||
>
|
||||
<Svg
|
||||
viewBox={`0 0 ${viewbox.width} ${viewbox.height}`}
|
||||
width={`${viewbox.width}px`}
|
||||
height={`${viewbox.height}px`}
|
||||
>
|
||||
<g transform={`translate(${translate.x}, ${translate.y})`}>
|
||||
<g transform={getIsoMatrixCSS()}>
|
||||
<rect
|
||||
width={tileArea.width * UNPROJECTED_TILE_SIZE * zoom}
|
||||
height={tileArea.height * UNPROJECTED_TILE_SIZE * zoom}
|
||||
fill={fill}
|
||||
rx={cornerRadius}
|
||||
{...strokeParams}
|
||||
/>
|
||||
{children}
|
||||
</g>
|
||||
</g>
|
||||
</Svg>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -2,25 +2,31 @@ import React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import { Button, Typography } from '@mui/material';
|
||||
import { Icon as IconInterface } from 'src/stores/useSceneStore';
|
||||
import { Icon as IconI } from 'src/types';
|
||||
|
||||
interface Props {
|
||||
icon: IconInterface;
|
||||
icon: IconI;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export const Icon = ({ icon, onClick }: Props) => (
|
||||
<Button variant="text" onClick={onClick}>
|
||||
<Stack justifyContent="center" alignItems="center" sx={{ height: '100%' }}>
|
||||
<Box
|
||||
component="img"
|
||||
src={icon.url}
|
||||
alt={`Icon ${icon.name}`}
|
||||
sx={{ width: '100%', height: 80 }}
|
||||
/>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{icon.name}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Button>
|
||||
);
|
||||
export const Icon = ({ icon, onClick }: Props) => {
|
||||
return (
|
||||
<Button variant="text" onClick={onClick}>
|
||||
<Stack
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
sx={{ height: '100%' }}
|
||||
>
|
||||
<Box
|
||||
component="img"
|
||||
src={icon.url}
|
||||
alt={`Icon ${icon.name}`}
|
||||
sx={{ width: '100%', height: 80 }}
|
||||
/>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{icon.name}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,23 +1,32 @@
|
||||
import React from 'react';
|
||||
import Grid from '@mui/material/Grid';
|
||||
import { Icon as IconInterface } from 'src/stores/useSceneStore';
|
||||
import { Icon as IconI } from 'src/types';
|
||||
import { Icon } from './Icon';
|
||||
import { Section } from '../../components/Section';
|
||||
|
||||
interface Props {
|
||||
name?: string;
|
||||
icons: IconInterface[];
|
||||
onClick: (icon: IconInterface) => void;
|
||||
icons: IconI[];
|
||||
onClick: (icon: IconI) => void;
|
||||
}
|
||||
|
||||
export const IconCategory = ({ name, icons, onClick }: Props) => (
|
||||
<Section title={name}>
|
||||
<Grid container spacing={2}>
|
||||
{icons.map((icon) => (
|
||||
<Grid item xs={3} key={icon.id}>
|
||||
<Icon icon={icon} onClick={() => onClick(icon)} />
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</Section>
|
||||
);
|
||||
export const IconCategory = ({ name, icons, onClick }: Props) => {
|
||||
return (
|
||||
<Section title={name}>
|
||||
<Grid container spacing={2}>
|
||||
{icons.map((icon) => {
|
||||
return (
|
||||
<Grid item xs={3} key={icon.id}>
|
||||
<Icon
|
||||
icon={icon}
|
||||
onClick={() => {
|
||||
return onClick(icon);
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import Grid from '@mui/material/Grid';
|
||||
import { Icon } from 'src/stores/useSceneStore';
|
||||
import { Icon } from 'src/types';
|
||||
import { IconCategory } from './IconCategory';
|
||||
|
||||
interface Props {
|
||||
@@ -13,7 +13,9 @@ export const Icons = ({ icons, onClick }: Props) => {
|
||||
const cats: { name?: string; icons: Icon[] }[] = [];
|
||||
|
||||
icons.forEach((icon) => {
|
||||
const category = cats.find((cat) => cat.name === icon.category);
|
||||
const category = cats.find((cat) => {
|
||||
return cat.name === icon.category;
|
||||
});
|
||||
|
||||
if (!category) {
|
||||
cats.push({ name: icon.category, icons: [icon] });
|
||||
@@ -37,11 +39,13 @@ export const Icons = ({ icons, onClick }: Props) => {
|
||||
|
||||
return (
|
||||
<Grid container spacing={4}>
|
||||
{categorisedIcons.map((cat) => (
|
||||
<Grid item xs={12} key={`icon-category-${cat.name}`}>
|
||||
<IconCategory {...cat} onClick={onClick} />
|
||||
</Grid>
|
||||
))}
|
||||
{categorisedIcons.map((cat) => {
|
||||
return (
|
||||
<Grid item xs={12} key={`icon-category-${cat.name}`}>
|
||||
<IconCategory {...cat} onClick={onClick} />
|
||||
</Grid>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { Tabs, Tab, Box } from '@mui/material';
|
||||
import { useSceneStore, useNodeHooks, Node } from 'src/stores/useSceneStore';
|
||||
import { Node } from 'src/types';
|
||||
import { useSceneStore, useNodeHooks } from 'src/stores/useSceneStore';
|
||||
import { ControlsContainer } from '../components/ControlsContainer';
|
||||
import { Icons } from './IconSelection/IconSelection';
|
||||
import { Header } from '../components/Header';
|
||||
@@ -12,8 +13,12 @@ interface Props {
|
||||
|
||||
export const NodeControls = ({ nodeId }: Props) => {
|
||||
const [tab, setTab] = useState(0);
|
||||
const icons = useSceneStore((state) => state.icons);
|
||||
const sceneActions = useSceneStore((state) => state.actions);
|
||||
const icons = useSceneStore((state) => {
|
||||
return state.icons;
|
||||
});
|
||||
const sceneActions = useSceneStore((state) => {
|
||||
return state.actions;
|
||||
});
|
||||
const { useGetNodeById } = useNodeHooks();
|
||||
const node = useGetNodeById(nodeId);
|
||||
|
||||
@@ -53,7 +58,9 @@ export const NodeControls = ({ nodeId }: Props) => {
|
||||
{tab === 1 && (
|
||||
<Icons
|
||||
icons={icons}
|
||||
onClick={(icon) => onNodeUpdated({ iconId: icon.id })}
|
||||
onClick={(icon) => {
|
||||
return onNodeUpdated({ iconId: icon.id });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</ControlsContainer>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Slider, useTheme } from '@mui/material';
|
||||
import { Node } from 'src/stores/useSceneStore';
|
||||
import { Node } from 'src/types';
|
||||
import { ColorSelector } from 'src/components/ColorSelector/ColorSelector';
|
||||
import { MarkdownEditor } from '../../../MarkdownEditor/MarkdownEditor';
|
||||
|
||||
@@ -26,7 +26,9 @@ export const NodeSettings = ({
|
||||
<Section title="Label">
|
||||
<MarkdownEditor
|
||||
value={label}
|
||||
onChange={(text) => onUpdate({ label: text })}
|
||||
onChange={(text) => {
|
||||
return onUpdate({ label: text });
|
||||
}}
|
||||
/>
|
||||
</Section>
|
||||
<Section title="Label height">
|
||||
@@ -36,16 +38,18 @@ export const NodeSettings = ({
|
||||
min={0}
|
||||
max={200}
|
||||
value={labelHeight}
|
||||
onChange={(e, newHeight) =>
|
||||
onUpdate({ labelHeight: newHeight as number })
|
||||
}
|
||||
onChange={(e, newHeight) => {
|
||||
return onUpdate({ labelHeight: newHeight as number });
|
||||
}}
|
||||
/>
|
||||
</Section>
|
||||
<Section title="Color">
|
||||
<ColorSelector
|
||||
activeColor={color}
|
||||
colors={Object.values(theme.customVars.diagramPalette)}
|
||||
onChange={(col) => onUpdate({ color: col })}
|
||||
onChange={(col) => {
|
||||
return onUpdate({ color: col });
|
||||
}}
|
||||
/>
|
||||
</Section>
|
||||
</>
|
||||
|
||||
83
src/components/Lasso/Lasso.tsx
Normal file
83
src/components/Lasso/Lasso.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
// import { useRef, useCallback } from 'react';
|
||||
// import { Group, Shape } from 'paper';
|
||||
// import gsap from 'gsap';
|
||||
// import { Coords } from 'src/types';
|
||||
// import { UNPROJECTED_TILE_SIZE, PIXEL_UNIT } from 'src/renderer/utils/constants';
|
||||
// import {
|
||||
// getBoundingBox,
|
||||
// sortByPosition,
|
||||
// getTileBounds
|
||||
// } from 'src/renderer/utils/gridHelpers';
|
||||
// import { applyProjectionMatrix } from 'src/renderer/utils/projection';
|
||||
|
||||
// export const useLasso = () => {
|
||||
// const containerRef = useRef(new Group());
|
||||
// const shapeRef = useRef<paper.Shape.Rectangle>();
|
||||
|
||||
// const setSelection = useCallback((startTile: Coords, endTile: Coords) => {
|
||||
// if (!shapeRef.current) return;
|
||||
|
||||
// const boundingBox = getBoundingBox([startTile, endTile]);
|
||||
|
||||
// // TODO: Enforce at least one node being passed to this getBoundingBox() to prevent null returns
|
||||
// if (!boundingBox) return;
|
||||
|
||||
// const lassoStartTile = boundingBox[3];
|
||||
// const lassoScreenPosition = getTileBounds(lassoStartTile).left;
|
||||
// const sorted = sortByPosition(boundingBox);
|
||||
// const position = { x: sorted.lowX, y: sorted.highY };
|
||||
// const size = {
|
||||
// x: sorted.highX - sorted.lowX,
|
||||
// y: sorted.highY - sorted.lowY
|
||||
// };
|
||||
|
||||
// shapeRef.current.set({
|
||||
// position,
|
||||
// size: [
|
||||
// (size.x + 1) * (UNPROJECTED_TILE_SIZE - PIXEL_UNIT * 3),
|
||||
// (size.y + 1) * (UNPROJECTED_TILE_SIZE - PIXEL_UNIT * 3)
|
||||
// ]
|
||||
// });
|
||||
|
||||
// containerRef.current.set({
|
||||
// pivot: shapeRef.current.bounds.bottomLeft,
|
||||
// position: lassoScreenPosition
|
||||
// });
|
||||
// }, []);
|
||||
|
||||
// const init = useCallback(() => {
|
||||
// containerRef.current.removeChildren();
|
||||
// containerRef.current.set({ pivot: [0, 0] });
|
||||
|
||||
// shapeRef.current = new Shape.Rectangle({
|
||||
// strokeCap: 'round',
|
||||
// fillColor: 'lightBlue',
|
||||
// size: [UNPROJECTED_TILE_SIZE, UNPROJECTED_TILE_SIZE],
|
||||
// opacity: 0.5,
|
||||
// radius: PIXEL_UNIT * 8,
|
||||
// strokeWidth: PIXEL_UNIT * 3,
|
||||
// strokeColor: 'blue',
|
||||
// dashArray: [5, 10],
|
||||
// pivot: [0, 0]
|
||||
// });
|
||||
|
||||
// gsap
|
||||
// .fromTo(
|
||||
// shapeRef.current,
|
||||
// { dashOffset: 0 },
|
||||
// { dashOffset: PIXEL_UNIT * 10, ease: 'none', duration: 0.25 }
|
||||
// )
|
||||
// .repeat(-1);
|
||||
|
||||
// containerRef.current.addChild(shapeRef.current);
|
||||
// applyProjectionMatrix(containerRef.current);
|
||||
|
||||
// return containerRef.current;
|
||||
// }, []);
|
||||
|
||||
// return {
|
||||
// init,
|
||||
// containerRef,
|
||||
// setSelection
|
||||
// };
|
||||
// };
|
||||
83
src/components/Node/LabelContainer.tsx
Normal file
83
src/components/Node/LabelContainer.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import gsap from 'gsap';
|
||||
import { Box } from '@mui/material';
|
||||
import { useResizeObserver } from 'src/hooks/useResizeObserver';
|
||||
import { Size } from 'src/types';
|
||||
|
||||
interface Props {
|
||||
labelHeight: number;
|
||||
tileSize: Size;
|
||||
children: React.ReactNode;
|
||||
connectorDotSize: number;
|
||||
}
|
||||
|
||||
export const LabelContainer = ({
|
||||
children,
|
||||
labelHeight,
|
||||
tileSize,
|
||||
connectorDotSize
|
||||
}: Props) => {
|
||||
const contentRef = useRef<HTMLDivElement>();
|
||||
const { observe, size: contentSize } = useResizeObserver();
|
||||
|
||||
useEffect(() => {
|
||||
if (!contentRef.current) return;
|
||||
|
||||
observe(contentRef.current);
|
||||
}, [observe]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!contentRef.current) return;
|
||||
|
||||
gsap.to(contentRef.current, {
|
||||
duration: 0,
|
||||
x: -contentSize.width * 0.5,
|
||||
y: -(contentSize.height + labelHeight)
|
||||
});
|
||||
}, [contentSize, labelHeight]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
width: 10,
|
||||
height: 10
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
component="svg"
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: -(labelHeight + tileSize.height / 2),
|
||||
left: -connectorDotSize / 2
|
||||
}}
|
||||
>
|
||||
<line
|
||||
x1={connectorDotSize / 2}
|
||||
y1={tileSize.height / 2}
|
||||
x2={connectorDotSize / 2}
|
||||
y2={labelHeight}
|
||||
strokeDasharray={`0, ${connectorDotSize * 2}`}
|
||||
stroke="black"
|
||||
strokeWidth={connectorDotSize}
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</Box>
|
||||
<Box
|
||||
ref={contentRef}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
bgcolor: 'common.white',
|
||||
border: '1px solid',
|
||||
borderColor: 'grey.500',
|
||||
borderRadius: 2,
|
||||
overflow: 'hidden',
|
||||
py: 1,
|
||||
px: 1.5
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
136
src/components/Node/Node.tsx
Normal file
136
src/components/Node/Node.tsx
Normal file
@@ -0,0 +1,136 @@
|
||||
import React, { useEffect, useRef, useCallback, useMemo } from 'react';
|
||||
import { Box } from '@mui/material';
|
||||
import gsap from 'gsap';
|
||||
import { Size, Coords, TileOriginEnum, Node as NodeI, Scroll } from 'src/types';
|
||||
import {
|
||||
getTilePosition,
|
||||
getProjectedTileSize,
|
||||
getColorVariant
|
||||
} from 'src/utils';
|
||||
import { useResizeObserver } from 'src/hooks/useResizeObserver';
|
||||
import { IsoTileArea } from 'src/components/IsoTileArea/IsoTileArea';
|
||||
import { LabelContainer } from './LabelContainer';
|
||||
import { MarkdownLabel } from './LabelTypes/MarkdownLabel';
|
||||
|
||||
interface Props {
|
||||
node: NodeI;
|
||||
iconUrl?: string;
|
||||
zoom: number;
|
||||
scroll: Scroll;
|
||||
}
|
||||
|
||||
export const Node = ({ node, iconUrl, zoom, scroll }: Props) => {
|
||||
const nodeRef = useRef<HTMLDivElement>();
|
||||
const iconRef = useRef<HTMLImageElement>();
|
||||
const { observe, size: iconSize } = useResizeObserver();
|
||||
|
||||
useEffect(() => {
|
||||
if (!iconRef.current) return;
|
||||
|
||||
observe(iconRef.current);
|
||||
}, [observe]);
|
||||
|
||||
const projectedTileSize = useMemo<Size>(() => {
|
||||
return getProjectedTileSize({ zoom });
|
||||
}, [zoom]);
|
||||
|
||||
const moveToTile = useCallback(
|
||||
({
|
||||
tile,
|
||||
animationDuration = 0.15
|
||||
}: {
|
||||
tile: Coords;
|
||||
animationDuration?: number;
|
||||
}) => {
|
||||
if (!nodeRef.current || !iconRef.current) return;
|
||||
|
||||
const position = getTilePosition({
|
||||
tile,
|
||||
zoom,
|
||||
scroll,
|
||||
origin: TileOriginEnum.BOTTOM
|
||||
});
|
||||
|
||||
gsap.to(iconRef.current, {
|
||||
duration: animationDuration,
|
||||
x: -iconRef.current.width * 0.5,
|
||||
y: -iconRef.current.height
|
||||
});
|
||||
|
||||
gsap.to(nodeRef.current, {
|
||||
duration: animationDuration,
|
||||
x: position.x,
|
||||
y: position.y
|
||||
});
|
||||
},
|
||||
[zoom, scroll]
|
||||
);
|
||||
|
||||
const onImageLoaded = useCallback(() => {
|
||||
if (!nodeRef.current || !iconRef.current) return;
|
||||
|
||||
gsap.killTweensOf(nodeRef.current);
|
||||
moveToTile({ tile: node.position, animationDuration: 0 });
|
||||
nodeRef.current.style.opacity = '1';
|
||||
}, [node.position, moveToTile]);
|
||||
|
||||
useEffect(() => {
|
||||
moveToTile({ tile: node.position, animationDuration: 0 });
|
||||
}, [node.position, moveToTile]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={nodeRef}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
opacity: 0
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute'
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: -projectedTileSize.height
|
||||
}}
|
||||
>
|
||||
<IsoTileArea
|
||||
tileArea={{
|
||||
width: 1,
|
||||
height: 1
|
||||
}}
|
||||
fill={node.color}
|
||||
cornerRadius={15 * zoom}
|
||||
stroke={{
|
||||
width: 1 * zoom,
|
||||
color: getColorVariant(node.color, 'dark', { grade: 1.5 })
|
||||
}}
|
||||
zoom={zoom}
|
||||
/>
|
||||
</Box>
|
||||
<LabelContainer
|
||||
labelHeight={node.labelHeight + iconSize.height}
|
||||
tileSize={projectedTileSize}
|
||||
connectorDotSize={5 * zoom}
|
||||
>
|
||||
{node.label && <MarkdownLabel label={node.label} />}
|
||||
{node.labelComponent}
|
||||
</LabelContainer>
|
||||
</Box>
|
||||
<Box
|
||||
component="img"
|
||||
ref={iconRef}
|
||||
onLoad={onImageLoaded}
|
||||
src={iconUrl}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
width: projectedTileSize.width,
|
||||
pointerEvents: 'none'
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -1,14 +1,14 @@
|
||||
import React from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { Box } from '@mui/material';
|
||||
import { Node as NodeI } from 'src/types';
|
||||
import { useUiStateStore } from 'src/stores/useUiStateStore';
|
||||
import { OriginEnum, getTilePosition } from 'src/utils';
|
||||
import { useSceneStore } from 'src/stores/useSceneStore';
|
||||
import { useInteractionManager } from 'src/interaction/useInteractionManager';
|
||||
import { TILE_SIZE } from './utils/constants';
|
||||
// import { ContextMenuLayer } from './components/ContextMenuLayer/ContextMenuLayer';
|
||||
import { Grid } from './components/Grid/Grid';
|
||||
import { Cursor } from './components/Cursor/Cursor';
|
||||
import { NodeV2 } from './components/Node/NodeV2';
|
||||
import { Grid } from 'src/components/Grid/Grid';
|
||||
import { Cursor } from 'src/components/Cursor/Cursor';
|
||||
import { Node } from 'src/components/Node/Node';
|
||||
import { Group } from 'src/components/Group/Group';
|
||||
import { Connector } from 'src/components/Connector/Connector';
|
||||
|
||||
export const Renderer = () => {
|
||||
const scene = useSceneStore(({ nodes, connectors, groups }) => {
|
||||
@@ -31,6 +31,21 @@ export const Renderer = () => {
|
||||
});
|
||||
useInteractionManager();
|
||||
|
||||
const getNodesFromIds = useCallback(
|
||||
(nodeIds: string[]) => {
|
||||
return nodeIds
|
||||
.map((nodeId) => {
|
||||
return scene.nodes.find((node) => {
|
||||
return node.id === nodeId;
|
||||
});
|
||||
})
|
||||
.filter((node) => {
|
||||
return node !== undefined;
|
||||
}) as NodeI[];
|
||||
},
|
||||
[scene.nodes]
|
||||
);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
@@ -38,60 +53,51 @@ export const Renderer = () => {
|
||||
height: '100%'
|
||||
}}
|
||||
>
|
||||
<Grid tileSize={TILE_SIZE * zoom} scroll={scroll.position} />
|
||||
{mode.showCursor && (
|
||||
<Cursor
|
||||
position={getTilePosition(mouse.position.tile, OriginEnum.TOP)}
|
||||
tileSize={TILE_SIZE * zoom}
|
||||
/>
|
||||
)}
|
||||
{scene.nodes.map((node) => {
|
||||
return (
|
||||
<NodeV2
|
||||
key={node.id}
|
||||
position={getTilePosition(node.position, OriginEnum.BOTTOM)}
|
||||
iconUrl={
|
||||
icons.find((icon) => {
|
||||
return icon.id === node.iconId;
|
||||
})?.url
|
||||
}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{/* {mode.type === 'LASSO' && (
|
||||
<Lasso
|
||||
parentContainer={renderer.lassoContainer.current as paper.Group}
|
||||
startTile={mode.selection.startTile}
|
||||
endTile={mode.selection.endTile}
|
||||
/>
|
||||
)}
|
||||
{scene.connectors.map((connector) => {
|
||||
return (
|
||||
<Connector
|
||||
key={connector.id}
|
||||
connector={connector}
|
||||
parentContainer={renderer.connectorManager.container as paper.Group}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<Grid scroll={scroll} zoom={zoom} />
|
||||
{scene.groups.map((group) => {
|
||||
const nodes = getNodesFromIds(group.nodeIds);
|
||||
|
||||
return (
|
||||
<Group
|
||||
key={group.id}
|
||||
parentContainer={renderer.groupManager.container as paper.Group}
|
||||
group={group}
|
||||
nodes={nodes}
|
||||
zoom={zoom}
|
||||
scroll={scroll}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{mode.showCursor && (
|
||||
<Cursor tile={mouse.position.tile} zoom={zoom} scroll={scroll} />
|
||||
)}
|
||||
{/* {scene.connectors.map((connector) => {
|
||||
const nodes = getNodesFromIds([connector.from, connector.to]);
|
||||
|
||||
return (
|
||||
<Connector
|
||||
connector={connector}
|
||||
fromNode={nodes[0]}
|
||||
toNode={nodes[1]}
|
||||
scroll={scroll}
|
||||
zoom={zoom}
|
||||
/>
|
||||
);
|
||||
})} */}
|
||||
{scene.nodes.map((node) => {
|
||||
return (
|
||||
<Node
|
||||
key={node.id}
|
||||
node={node}
|
||||
parentContainer={renderer.nodeManager.container as paper.Group}
|
||||
iconUrl={
|
||||
icons.find((icon) => {
|
||||
return icon.id === node.iconId;
|
||||
})?.url
|
||||
}
|
||||
zoom={zoom}
|
||||
scroll={scroll}
|
||||
/>
|
||||
);
|
||||
})} */}
|
||||
})}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
13
src/components/Svg/Svg.tsx
Normal file
13
src/components/Svg/Svg.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
|
||||
type Props = React.SVGProps<SVGSVGElement> & {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export const Svg = ({ children, ...rest }: Props) => {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" {...rest}>
|
||||
{children}
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
@@ -1,18 +1,15 @@
|
||||
import { customVars } from '../styles/theme';
|
||||
import { Size } from 'src/types';
|
||||
import { customVars } from './styles/theme';
|
||||
|
||||
export const UNPROJECTED_TILE_SIZE = 100;
|
||||
export const TILE_PROJECTION_MULTIPLIERS: Size = {
|
||||
width: 1.415,
|
||||
height: 0.819
|
||||
};
|
||||
export const DEFAULT_COLOR = customVars.diagramPalette.blue;
|
||||
|
||||
export const CONNECTOR_DEFAULTS = {
|
||||
width: 4
|
||||
};
|
||||
|
||||
export const GRID_DEFAULTS = {
|
||||
size: {
|
||||
x: 51,
|
||||
y: 51
|
||||
}
|
||||
};
|
||||
|
||||
export const NODE_DEFAULTS = {
|
||||
label: '',
|
||||
labelHeight: 100,
|
||||
@@ -53,18 +53,43 @@ export const CustomNode = () => {
|
||||
<Isoflow
|
||||
initialScene={{
|
||||
icons,
|
||||
connectors: [],
|
||||
groups: [],
|
||||
connectors: [
|
||||
{
|
||||
id: 'connector1',
|
||||
from: 'database',
|
||||
to: 'server',
|
||||
label: 'connection'
|
||||
}
|
||||
],
|
||||
groups: [
|
||||
{
|
||||
id: 'group1',
|
||||
label: 'Group 1',
|
||||
nodeIds: ['server', 'database']
|
||||
}
|
||||
],
|
||||
nodes: [
|
||||
{
|
||||
id: 'Node1',
|
||||
id: 'server',
|
||||
label: 'Requests per minute',
|
||||
labelElement: <CustomLabel />,
|
||||
labelComponent: <CustomLabel />,
|
||||
labelHeight: 40,
|
||||
iconId: 'server',
|
||||
position: {
|
||||
x: 0,
|
||||
y: 0
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'database',
|
||||
label: 'Transactions',
|
||||
labelComponent: <CustomLabel />,
|
||||
labelHeight: 40,
|
||||
iconId: 'server',
|
||||
position: {
|
||||
x: 0,
|
||||
y: 3
|
||||
}
|
||||
}
|
||||
]
|
||||
}}
|
||||
|
||||
4
src/global.d.ts
vendored
Normal file
4
src/global.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
declare module '*.svg' {
|
||||
const content: React.FunctionComponent<React.SVGAttributes<SVGElement>>;
|
||||
export default content;
|
||||
}
|
||||
35
src/hooks/useResizeObserver.ts
Normal file
35
src/hooks/useResizeObserver.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Size } from 'src/types';
|
||||
|
||||
export const useResizeObserver = () => {
|
||||
const resizeObserverRef = useRef<ResizeObserver>();
|
||||
const [size, setSize] = useState<Size>({ width: 0, height: 0 });
|
||||
|
||||
const disconnect = useCallback(() => {
|
||||
resizeObserverRef.current?.disconnect();
|
||||
}, []);
|
||||
|
||||
const observe = useCallback((element: HTMLElement) => {
|
||||
disconnect();
|
||||
|
||||
resizeObserverRef.current = new ResizeObserver(() => {
|
||||
setSize({
|
||||
width: element.clientWidth,
|
||||
height: element.clientHeight
|
||||
});
|
||||
});
|
||||
|
||||
resizeObserverRef.current.observe(element);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
disconnect();
|
||||
};
|
||||
}, [disconnect]);
|
||||
|
||||
return {
|
||||
observe,
|
||||
size
|
||||
};
|
||||
};
|
||||
@@ -1,25 +0,0 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
export const useWindowSize = () => {
|
||||
const [windowSize, setWindowSize] = useState({
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const onResize = () => {
|
||||
setWindowSize({
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight
|
||||
});
|
||||
};
|
||||
|
||||
window.addEventListener('resize', onResize);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', onResize);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return windowSize;
|
||||
};
|
||||
@@ -1,7 +1,5 @@
|
||||
import { SidebarTypeEnum } from 'src/stores/useUiStateStore';
|
||||
import { CoordsUtils } from 'src/utils';
|
||||
import { InteractionReducer } from '../types';
|
||||
import { getItemsByTile } from '../../renderer/utils/gridHelpers';
|
||||
import { SidebarTypeEnum, InteractionReducer } from 'src/types';
|
||||
import { CoordsUtils, filterNodesByTile } from 'src/utils';
|
||||
|
||||
export const Cursor: InteractionReducer = {
|
||||
mousemove: (draftState) => {
|
||||
@@ -16,7 +14,7 @@ export const Cursor: InteractionReducer = {
|
||||
|
||||
if (draftState.mode.mousedown) {
|
||||
// User is in mousedown mode
|
||||
if (draftState.mode.mousedown.items.nodes.length > 0) {
|
||||
if (draftState.mode.mousedown.items.length > 0) {
|
||||
// User's last mousedown action was on a node
|
||||
draftState.mode = {
|
||||
type: 'DRAG_ITEMS',
|
||||
@@ -42,9 +40,9 @@ export const Cursor: InteractionReducer = {
|
||||
mousedown: (draftState) => {
|
||||
if (draftState.mode.type !== 'CURSOR') return;
|
||||
|
||||
const itemsAtTile = getItemsByTile({
|
||||
const itemsAtTile = filterNodesByTile({
|
||||
tile: draftState.mouse.position.tile,
|
||||
sortedSceneItems: draftState.scene
|
||||
nodes: draftState.scene.nodes
|
||||
});
|
||||
|
||||
draftState.mode.mousedown = {
|
||||
@@ -64,7 +62,7 @@ export const Cursor: InteractionReducer = {
|
||||
|
||||
if (draftState.mode.mousedown !== null) {
|
||||
// User's last mousedown action was on a scene item
|
||||
const mousedownNode = draftState.mode.mousedown.items.nodes[0];
|
||||
const mousedownNode = draftState.mode.mousedown.items[0];
|
||||
|
||||
if (mousedownNode) {
|
||||
// The user's last mousedown action was on a node
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CoordsUtils } from 'src/utils';
|
||||
import { InteractionReducer } from '../types';
|
||||
import { InteractionReducer } from 'src/types';
|
||||
|
||||
export const DragItems: InteractionReducer = {
|
||||
mousemove: (draftState) => {
|
||||
@@ -10,7 +10,7 @@ export const DragItems: InteractionReducer = {
|
||||
!CoordsUtils.isEqual(draftState.mouse.delta.tile, CoordsUtils.zero())
|
||||
) {
|
||||
// User has moved tile since the last mouse event
|
||||
draftState.mode.items.nodes.forEach((node) => {
|
||||
draftState.mode.items.forEach((node) => {
|
||||
const nodeIndex = draftState.scene.nodes.findIndex((sceneNode) => {
|
||||
return sceneNode.id === node.id;
|
||||
});
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { CoordsUtils } from 'src/utils';
|
||||
import { isWithinBounds } from 'src/renderer/utils/gridHelpers';
|
||||
import { InteractionReducer } from '../types';
|
||||
import { CoordsUtils, isWithinBounds } from 'src/utils';
|
||||
import { InteractionReducer } from 'src/types';
|
||||
|
||||
export const Lasso: InteractionReducer = {
|
||||
mousemove: (draftState) => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CoordsUtils } from 'src/utils';
|
||||
import { InteractionReducer } from '../types';
|
||||
import { InteractionReducer } from 'src/types';
|
||||
|
||||
export const Pan: InteractionReducer = {
|
||||
mousemove: (draftState) => {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { produce } from 'immer';
|
||||
import { useSceneStore } from 'src/stores/useSceneStore';
|
||||
import { useUiStateStore, Mouse } from 'src/stores/useUiStateStore';
|
||||
import { useUiStateStore } from 'src/stores/useUiStateStore';
|
||||
import { CoordsUtils, screenToIso } from 'src/utils';
|
||||
import { InteractionReducer, Mouse, State } from 'src/types';
|
||||
import { DragItems } from './reducers/DragItems';
|
||||
import { Pan } from './reducers/Pan';
|
||||
import { Cursor } from './reducers/Cursor';
|
||||
import { Lasso } from './reducers/Lasso';
|
||||
import type { InteractionReducer } from './types';
|
||||
|
||||
const reducers: { [k in string]: InteractionReducer } = {
|
||||
CURSOR: Cursor,
|
||||
@@ -23,6 +23,9 @@ export const useInteractionManager = () => {
|
||||
const scroll = useUiStateStore((state) => {
|
||||
return state.scroll;
|
||||
});
|
||||
const zoom = useUiStateStore((state) => {
|
||||
return state.zoom;
|
||||
});
|
||||
const mouse = useUiStateStore((state) => {
|
||||
return state.mouse;
|
||||
});
|
||||
@@ -35,8 +38,8 @@ export const useInteractionManager = () => {
|
||||
const uiStateActions = useUiStateStore((state) => {
|
||||
return state.actions;
|
||||
});
|
||||
const scene = useSceneStore(({ nodes, connectors, groups }) => {
|
||||
return { nodes, connectors, groups };
|
||||
const scene = useSceneStore(({ nodes, connectors, groups, icons }) => {
|
||||
return { nodes, connectors, groups, icons };
|
||||
});
|
||||
const sceneActions = useSceneStore((state) => {
|
||||
return state.actions;
|
||||
@@ -60,7 +63,11 @@ export const useInteractionManager = () => {
|
||||
|
||||
const newPosition: Mouse['position'] = {
|
||||
screen: { x: e.clientX, y: e.clientY },
|
||||
tile: screenToIso({ x: e.clientX, y: e.clientY })
|
||||
tile: screenToIso({
|
||||
mouse: { x: e.clientX, y: e.clientY },
|
||||
zoom,
|
||||
scroll
|
||||
})
|
||||
};
|
||||
|
||||
const newDelta: Mouse['delta'] = {
|
||||
@@ -85,19 +92,18 @@ export const useInteractionManager = () => {
|
||||
mousedown: getMousedown()
|
||||
};
|
||||
|
||||
const newState = produce(
|
||||
{
|
||||
scene,
|
||||
mouse: nextMouse,
|
||||
mode,
|
||||
scroll,
|
||||
contextMenu,
|
||||
itemControls
|
||||
},
|
||||
(draft) => {
|
||||
return reducerAction(draft);
|
||||
}
|
||||
);
|
||||
const baseState: State = {
|
||||
scene,
|
||||
mouse: nextMouse,
|
||||
mode,
|
||||
scroll,
|
||||
contextMenu,
|
||||
itemControls
|
||||
};
|
||||
|
||||
const newState = produce(baseState, (draft) => {
|
||||
return reducerAction(draft);
|
||||
});
|
||||
|
||||
uiStateActions.setMouse(nextMouse);
|
||||
uiStateActions.setScroll(newState.scroll);
|
||||
@@ -108,12 +114,16 @@ export const useInteractionManager = () => {
|
||||
},
|
||||
[
|
||||
mode,
|
||||
mouse.position.screen,
|
||||
mouse.position.tile,
|
||||
mouse.mousedown,
|
||||
scroll,
|
||||
itemControls,
|
||||
uiStateActions,
|
||||
sceneActions,
|
||||
scene,
|
||||
contextMenu
|
||||
contextMenu,
|
||||
zoom
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import Paper from 'paper';
|
||||
import { Box } from '@mui/material';
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const render = () => {
|
||||
if (Paper.view) {
|
||||
if (global.requestAnimationFrame) {
|
||||
const raf = global.requestAnimationFrame(render);
|
||||
|
||||
return raf;
|
||||
}
|
||||
|
||||
Paper.view.update();
|
||||
}
|
||||
};
|
||||
|
||||
export const Initialiser = ({ children }: Props) => {
|
||||
const [isReady, setIsReady] = useState(false);
|
||||
const containerRef = useRef<HTMLCanvasElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setIsReady(false);
|
||||
|
||||
if (!containerRef.current) return;
|
||||
|
||||
Paper.settings = {
|
||||
insertItems: false,
|
||||
applyMatrix: false
|
||||
};
|
||||
|
||||
Paper.setup(containerRef.current);
|
||||
|
||||
const rafId = render();
|
||||
|
||||
setIsReady(true);
|
||||
|
||||
return () => {
|
||||
setIsReady(false);
|
||||
if (rafId) cancelAnimationFrame(rafId);
|
||||
|
||||
Paper.projects.forEach((project) => {
|
||||
return project.remove();
|
||||
});
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%'
|
||||
}}
|
||||
>
|
||||
<canvas
|
||||
ref={containerRef}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%'
|
||||
}}
|
||||
/>
|
||||
{isReady && children}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -1,44 +0,0 @@
|
||||
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 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({ x: 100, y: 100 }, fromNode.position, toNode.position);
|
||||
}, [nodes, connector, updateFromTo]);
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -1,54 +0,0 @@
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { Group, Path } from 'paper';
|
||||
import { pathfinder } from 'src/renderer/utils/pathfinder';
|
||||
import { Coords } from 'src/types';
|
||||
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
|
||||
};
|
||||
};
|
||||
@@ -1,72 +0,0 @@
|
||||
import React, { useEffect, useRef, useCallback, useState } from 'react';
|
||||
import gsap from 'gsap';
|
||||
import { Box, useTheme } from '@mui/material';
|
||||
import { getCSSMatrix } from 'src/renderer/utils/projection';
|
||||
|
||||
interface Props {
|
||||
position: { x: number; y: number };
|
||||
tileSize: number;
|
||||
}
|
||||
|
||||
// TODO: Remove tilesize
|
||||
export const Cursor = ({ position, tileSize }: Props) => {
|
||||
const theme = useTheme();
|
||||
const [isReady, setIsReady] = useState(false);
|
||||
const ref = useRef<SVGElement>();
|
||||
|
||||
const setPosition = useCallback(
|
||||
({
|
||||
position: _position,
|
||||
animationDuration = 0.15
|
||||
}: {
|
||||
position: { x: number; y: number };
|
||||
animationDuration?: number;
|
||||
}) => {
|
||||
if (!ref.current) return;
|
||||
|
||||
gsap.to(ref.current, {
|
||||
duration: animationDuration,
|
||||
left: _position.x,
|
||||
top: _position.y
|
||||
});
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!ref.current || !isReady) return;
|
||||
|
||||
setPosition({ position });
|
||||
}, [position, setPosition, isReady]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!ref.current || isReady) return;
|
||||
|
||||
gsap.killTweensOf(ref.current);
|
||||
setPosition({ position, animationDuration: 0 });
|
||||
ref.current.style.opacity = '1';
|
||||
setIsReady(true);
|
||||
}, [position, setPosition, isReady]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={ref}
|
||||
component="svg"
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
transform: getCSSMatrix({ x: -(tileSize / 2), y: -(tileSize / 2) }),
|
||||
opacity: 0
|
||||
}}
|
||||
width={tileSize}
|
||||
height={tileSize}
|
||||
>
|
||||
<rect
|
||||
width={tileSize}
|
||||
height={tileSize}
|
||||
fill={theme.palette.primary.main}
|
||||
opacity={0.7}
|
||||
rx={10}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -1,61 +0,0 @@
|
||||
import { useRef, useCallback } from 'react';
|
||||
import { Group, Shape } from 'paper';
|
||||
import gsap from 'gsap';
|
||||
import { Coords } from 'src/types';
|
||||
import { TILE_SIZE, PIXEL_UNIT } from '../../utils/constants';
|
||||
import { applyProjectionMatrix } from '../../utils/projection';
|
||||
|
||||
export const useCursor = () => {
|
||||
const container = useRef(new Group());
|
||||
|
||||
const init = useCallback(() => {
|
||||
container.current.removeChildren();
|
||||
container.current.set({ pivot: [0, 0] });
|
||||
|
||||
const rectangle = new Shape.Rectangle({
|
||||
strokeCap: 'round',
|
||||
fillColor: 'blue',
|
||||
size: [TILE_SIZE, TILE_SIZE],
|
||||
opacity: 0.5,
|
||||
radius: PIXEL_UNIT * 8,
|
||||
strokeWidth: 0,
|
||||
strokeColor: 'transparent',
|
||||
pivot: [0, 0],
|
||||
position: [0, 0],
|
||||
dashArray: null
|
||||
});
|
||||
|
||||
container.current.addChild(rectangle);
|
||||
applyProjectionMatrix(container.current);
|
||||
|
||||
return container.current;
|
||||
}, []);
|
||||
|
||||
const moveTo = useCallback(
|
||||
(position: Coords, opts?: { animationDuration?: number }) => {
|
||||
const tweenProxy = {
|
||||
x: container.current.position.x,
|
||||
y: container.current.position.y
|
||||
};
|
||||
|
||||
gsap.to(tweenProxy, {
|
||||
duration: opts?.animationDuration || 0.1,
|
||||
...position,
|
||||
onUpdate: () => {
|
||||
container.current.position.set(tweenProxy);
|
||||
}
|
||||
});
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const setVisible = useCallback((state: boolean) => {
|
||||
container.current.visible = state;
|
||||
}, []);
|
||||
|
||||
return {
|
||||
init,
|
||||
moveTo,
|
||||
setVisible
|
||||
};
|
||||
};
|
||||
@@ -1,78 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Box } from '@mui/material';
|
||||
import { PROJECTED_TILE_DIMENSIONS } from 'src/renderer/utils/constants';
|
||||
import { getCSSMatrix } from 'src/renderer/utils/projection';
|
||||
import { useWindowSize } from 'src/hooks/useWindowSize';
|
||||
|
||||
interface Props {
|
||||
tileSize: number;
|
||||
scroll: { x: number; y: number };
|
||||
}
|
||||
|
||||
export const Grid = ({ tileSize: _tileSize, scroll }: Props) => {
|
||||
const windowSize = useWindowSize();
|
||||
const tileSize = _tileSize;
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
overflow: 'hidden',
|
||||
pointerEvents: 'none'
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
width: '300%',
|
||||
height: '300%',
|
||||
left: '-100%',
|
||||
top: '-100%',
|
||||
transform: `${getCSSMatrix()}`
|
||||
}}
|
||||
>
|
||||
<Box component="svg" width="100%" height="100%">
|
||||
{/* <pattern
|
||||
id="dotpattern"
|
||||
x={`calc(50% - ${tileSize * 0.5}px)`}
|
||||
y={`calc(50% - ${tileSize * 0.5}px)`}
|
||||
width={tileSize}
|
||||
height={tileSize}
|
||||
patternUnits="userSpaceOnUse"
|
||||
>
|
||||
<circle cx="0" cy="0" r={2} fill="rgba(0, 0, 0, 0.3)" />
|
||||
</pattern> */}
|
||||
<pattern
|
||||
id="gridpattern"
|
||||
x={`${windowSize.width * 1.5 - tileSize * 0.5}px`}
|
||||
y={`${windowSize.height * 1.5 - tileSize * 0.5}px`}
|
||||
width={tileSize}
|
||||
height={tileSize}
|
||||
patternUnits="userSpaceOnUse"
|
||||
>
|
||||
<rect
|
||||
x="0"
|
||||
y="0"
|
||||
width={tileSize}
|
||||
height={tileSize}
|
||||
strokeWidth={1}
|
||||
stroke="rgba(0, 0, 0, 0.3)"
|
||||
fill="none"
|
||||
/>
|
||||
</pattern>
|
||||
<rect
|
||||
x="0"
|
||||
y="0"
|
||||
width="100%"
|
||||
height="100%"
|
||||
fill="url(#gridpattern)"
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -1,51 +0,0 @@
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import {
|
||||
Group as GroupInterface,
|
||||
useSceneStore,
|
||||
Node
|
||||
} from 'src/stores/useSceneStore';
|
||||
import { DEFAULT_COLOR } from 'src/utils/config';
|
||||
import { getColorVariant } from 'src/utils';
|
||||
import { useGroup } from './useGroup';
|
||||
|
||||
interface GroupProps {
|
||||
group: GroupInterface;
|
||||
parentContainer: paper.Group;
|
||||
}
|
||||
|
||||
export const Group = ({ group, parentContainer }: GroupProps) => {
|
||||
const { init, setTiles, setColor } = useGroup();
|
||||
const allNodes = useSceneStore((state) => {
|
||||
return state.nodes;
|
||||
});
|
||||
const groupNodes = useMemo(() => {
|
||||
const nodes = group.nodeIds.map((nodeId) => {
|
||||
return allNodes.find((node) => {
|
||||
return node.id === nodeId;
|
||||
});
|
||||
});
|
||||
|
||||
return nodes.filter((node) => {
|
||||
return node !== undefined;
|
||||
}) as Node[];
|
||||
}, [allNodes, group.nodeIds]);
|
||||
|
||||
useEffect(() => {
|
||||
const container = init();
|
||||
parentContainer.addChild(container);
|
||||
}, [init, parentContainer]);
|
||||
|
||||
useEffect(() => {
|
||||
setTiles(
|
||||
groupNodes.map((node) => {
|
||||
return node.position;
|
||||
})
|
||||
);
|
||||
}, [groupNodes, setTiles]);
|
||||
|
||||
useEffect(() => {
|
||||
setColor(DEFAULT_COLOR);
|
||||
}, [setColor]);
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -1,77 +0,0 @@
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { Group, Shape } from 'paper';
|
||||
import { Coords } from 'src/types';
|
||||
import { PIXEL_UNIT, TILE_SIZE } from 'src/renderer/utils/constants';
|
||||
import { getColorVariant } from 'src/utils';
|
||||
import {
|
||||
getBoundingBox,
|
||||
sortByPosition,
|
||||
getTileBounds
|
||||
} from '../../utils/gridHelpers';
|
||||
import { applyProjectionMatrix } from '../../utils/projection';
|
||||
|
||||
export const useGroup = () => {
|
||||
// TODO: Make sure consistent naming for all containers among all scene components
|
||||
const containerRef = useRef(new Group());
|
||||
const pathRef = useRef<paper.Shape.Rectangle>();
|
||||
|
||||
const setColor = useCallback((color: string) => {
|
||||
if (!pathRef.current) return;
|
||||
|
||||
const fillColor = getColorVariant(color, 'light', { alpha: 0.5 });
|
||||
|
||||
pathRef.current.set({ fillColor, strokeColor: color });
|
||||
}, []);
|
||||
|
||||
const setTiles = useCallback((tiles: Coords[]) => {
|
||||
if (!pathRef.current) return;
|
||||
|
||||
const corners = getBoundingBox(tiles, { x: 1, y: 1 });
|
||||
|
||||
if (corners === null) {
|
||||
containerRef.current.removeChildren();
|
||||
throw new Error('Group has no nodes');
|
||||
}
|
||||
|
||||
const sorted = sortByPosition(corners);
|
||||
const size = {
|
||||
x: sorted.highX - sorted.lowX,
|
||||
y: sorted.highY - sorted.lowY
|
||||
};
|
||||
|
||||
pathRef.current.set({
|
||||
position: [0, 0],
|
||||
radius: PIXEL_UNIT * 17,
|
||||
size: [
|
||||
(size.x + 1) * (TILE_SIZE - PIXEL_UNIT * 3),
|
||||
(size.y + 1) * (TILE_SIZE - PIXEL_UNIT * 3)
|
||||
]
|
||||
});
|
||||
|
||||
containerRef.current.set({
|
||||
pivot: pathRef.current.bounds.bottomLeft,
|
||||
position: getTileBounds(corners[3]).left
|
||||
});
|
||||
}, []);
|
||||
|
||||
// TODO: Do we really need init an init function on each component hook? Does any of them take arguments?
|
||||
const init = useCallback(() => {
|
||||
containerRef.current.removeChildren();
|
||||
|
||||
pathRef.current = new Shape.Rectangle({
|
||||
strokeCap: 'round',
|
||||
strokeWidth: PIXEL_UNIT
|
||||
});
|
||||
|
||||
containerRef.current.addChild(pathRef.current);
|
||||
applyProjectionMatrix(containerRef.current);
|
||||
|
||||
return containerRef.current;
|
||||
}, []);
|
||||
|
||||
return {
|
||||
init,
|
||||
setTiles,
|
||||
setColor
|
||||
};
|
||||
};
|
||||
@@ -1,29 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
import { Coords } from 'src/types';
|
||||
import { useLasso } from './useLasso';
|
||||
|
||||
interface Props {
|
||||
startTile: Coords;
|
||||
endTile: Coords;
|
||||
parentContainer: paper.Group;
|
||||
}
|
||||
|
||||
export const Lasso = ({ startTile, endTile, parentContainer }: Props) => {
|
||||
const lasso = useLasso();
|
||||
const { init: initLasso, setSelection } = lasso;
|
||||
|
||||
useEffect(() => {
|
||||
const container = initLasso();
|
||||
parentContainer.addChild(container);
|
||||
|
||||
return () => {
|
||||
container.remove();
|
||||
};
|
||||
}, [initLasso, parentContainer]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelection(startTile, endTile);
|
||||
}, [setSelection, startTile, endTile]);
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -1,83 +0,0 @@
|
||||
import { useRef, useCallback } from 'react';
|
||||
import { Group, Shape } from 'paper';
|
||||
import gsap from 'gsap';
|
||||
import { Coords } from 'src/types';
|
||||
import { TILE_SIZE, PIXEL_UNIT } from 'src/renderer/utils/constants';
|
||||
import {
|
||||
getBoundingBox,
|
||||
sortByPosition,
|
||||
getTileBounds
|
||||
} from 'src/renderer/utils/gridHelpers';
|
||||
import { applyProjectionMatrix } from 'src/renderer/utils/projection';
|
||||
|
||||
export const useLasso = () => {
|
||||
const containerRef = useRef(new Group());
|
||||
const shapeRef = useRef<paper.Shape.Rectangle>();
|
||||
|
||||
const setSelection = useCallback((startTile: Coords, endTile: Coords) => {
|
||||
if (!shapeRef.current) return;
|
||||
|
||||
const boundingBox = getBoundingBox([startTile, endTile]);
|
||||
|
||||
// TODO: Enforce at least one node being passed to this getBoundingBox() to prevent null returns
|
||||
if (!boundingBox) return;
|
||||
|
||||
const lassoStartTile = boundingBox[3];
|
||||
const lassoScreenPosition = getTileBounds(lassoStartTile).left;
|
||||
const sorted = sortByPosition(boundingBox);
|
||||
const position = { x: sorted.lowX, y: sorted.highY };
|
||||
const size = {
|
||||
x: sorted.highX - sorted.lowX,
|
||||
y: sorted.highY - sorted.lowY
|
||||
};
|
||||
|
||||
shapeRef.current.set({
|
||||
position,
|
||||
size: [
|
||||
(size.x + 1) * (TILE_SIZE - PIXEL_UNIT * 3),
|
||||
(size.y + 1) * (TILE_SIZE - PIXEL_UNIT * 3)
|
||||
]
|
||||
});
|
||||
|
||||
containerRef.current.set({
|
||||
pivot: shapeRef.current.bounds.bottomLeft,
|
||||
position: lassoScreenPosition
|
||||
});
|
||||
}, []);
|
||||
|
||||
const init = useCallback(() => {
|
||||
containerRef.current.removeChildren();
|
||||
containerRef.current.set({ pivot: [0, 0] });
|
||||
|
||||
shapeRef.current = new Shape.Rectangle({
|
||||
strokeCap: 'round',
|
||||
fillColor: 'lightBlue',
|
||||
size: [TILE_SIZE, TILE_SIZE],
|
||||
opacity: 0.5,
|
||||
radius: PIXEL_UNIT * 8,
|
||||
strokeWidth: PIXEL_UNIT * 3,
|
||||
strokeColor: 'blue',
|
||||
dashArray: [5, 10],
|
||||
pivot: [0, 0]
|
||||
});
|
||||
|
||||
gsap
|
||||
.fromTo(
|
||||
shapeRef.current,
|
||||
{ dashOffset: 0 },
|
||||
{ dashOffset: PIXEL_UNIT * 10, ease: 'none', duration: 0.25 }
|
||||
)
|
||||
.repeat(-1);
|
||||
|
||||
containerRef.current.addChild(shapeRef.current);
|
||||
applyProjectionMatrix(containerRef.current);
|
||||
|
||||
return containerRef.current;
|
||||
}, []);
|
||||
|
||||
return {
|
||||
init,
|
||||
containerRef,
|
||||
setSelection
|
||||
};
|
||||
};
|
||||
@@ -1,52 +0,0 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { Box } from '@mui/material';
|
||||
import { useLabelConnector } from './useLabelConnector';
|
||||
|
||||
interface Props {
|
||||
labelHeight: number;
|
||||
children: React.ReactNode;
|
||||
parentContainer: paper.Group;
|
||||
}
|
||||
|
||||
// TODO: Rename all `parentContainer` to `groupContainer`
|
||||
export const DefaultLabelContainer = ({
|
||||
children,
|
||||
labelHeight,
|
||||
parentContainer
|
||||
}: Props) => {
|
||||
const labelConnector = useLabelConnector();
|
||||
const {
|
||||
init: initLabelConnector,
|
||||
updateHeight: updateLabelHeight,
|
||||
destroy: destroyLabelConnector
|
||||
} = labelConnector;
|
||||
|
||||
useEffect(() => {
|
||||
const labelConnectorContainer = initLabelConnector();
|
||||
parentContainer.addChild(labelConnectorContainer);
|
||||
|
||||
return () => {
|
||||
destroyLabelConnector();
|
||||
};
|
||||
}, [initLabelConnector, destroyLabelConnector, parentContainer]);
|
||||
|
||||
useEffect(() => {
|
||||
updateLabelHeight(labelHeight);
|
||||
}, [labelHeight, updateLabelHeight]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
bgcolor: 'common.white',
|
||||
border: '1px solid',
|
||||
borderColor: 'grey.500',
|
||||
borderRadius: 2,
|
||||
overflow: 'hidden',
|
||||
py: 1,
|
||||
px: 1.5
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -1,133 +0,0 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { Group } from 'paper';
|
||||
import { Box } from '@mui/material';
|
||||
import gsap from 'gsap';
|
||||
import { useUiStateStore } from 'src/stores/useUiStateStore';
|
||||
import { Node as NodeInterface } from 'src/stores/useSceneStore';
|
||||
import { useNodeIcon } from './useNodeIcon';
|
||||
import { DefaultLabelContainer } from './DefaultLabelContainer';
|
||||
import { useNodeTile } from './useNodeTile';
|
||||
import { MarkdownLabel } from './LabelTypes/MarkdownLabel';
|
||||
import {
|
||||
getTilePosition,
|
||||
getTileScreenPosition
|
||||
} from '../../utils/gridHelpers';
|
||||
|
||||
export interface NodeProps {
|
||||
node: NodeInterface;
|
||||
parentContainer: paper.Group;
|
||||
}
|
||||
|
||||
export const Node = ({ node, parentContainer }: NodeProps) => {
|
||||
const [isFirstDisplay, setIsFirstDisplay] = useState(true);
|
||||
const groupRef = useRef(new Group());
|
||||
const labelRef = useRef<HTMLDivElement>();
|
||||
const labelConnectorContainer = useRef(new Group());
|
||||
const nodeIcon = useNodeIcon();
|
||||
const nodeTile = useNodeTile();
|
||||
const scroll = useUiStateStore((state) => {
|
||||
return state.scroll;
|
||||
});
|
||||
const zoom = useUiStateStore((state) => {
|
||||
return state.zoom;
|
||||
});
|
||||
const mode = useUiStateStore((state) => {
|
||||
return state.mode;
|
||||
});
|
||||
const [labelSize, setLabelSize] = useState({ width: 0, height: 0 });
|
||||
|
||||
const {
|
||||
init: initNodeIcon,
|
||||
update: updateNodeIcon,
|
||||
isLoaded: isIconLoaded
|
||||
} = nodeIcon;
|
||||
const { init: initNodeTile, updateColor, setActive } = nodeTile;
|
||||
|
||||
useEffect(() => {
|
||||
const nodeIconContainer = initNodeIcon();
|
||||
const nodeTileContainer = initNodeTile();
|
||||
|
||||
groupRef.current.removeChildren();
|
||||
groupRef.current.addChild(nodeTileContainer);
|
||||
groupRef.current.addChild(labelConnectorContainer.current);
|
||||
groupRef.current.addChild(nodeIconContainer);
|
||||
groupRef.current.pivot = nodeIconContainer.bounds.bottomCenter;
|
||||
parentContainer.addChild(groupRef.current);
|
||||
}, [initNodeIcon, parentContainer, initNodeTile]);
|
||||
|
||||
useEffect(() => {
|
||||
updateNodeIcon(node.iconId);
|
||||
}, [node.iconId, updateNodeIcon]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isIconLoaded) return;
|
||||
|
||||
const tweenValues = groupRef.current.position;
|
||||
const endState = getTilePosition(node.position);
|
||||
|
||||
gsap.to(tweenValues, {
|
||||
duration: isFirstDisplay ? 0 : 0.1,
|
||||
...endState,
|
||||
onUpdate: () => {
|
||||
groupRef.current.position.set(tweenValues);
|
||||
}
|
||||
});
|
||||
|
||||
if (isFirstDisplay) setIsFirstDisplay(false);
|
||||
}, [node.position, isFirstDisplay, isIconLoaded]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!labelRef.current) return;
|
||||
|
||||
const screenPosition = getTileScreenPosition({
|
||||
position: node.position,
|
||||
scrollPosition: scroll.position,
|
||||
zoom,
|
||||
origin: 'top'
|
||||
});
|
||||
|
||||
gsap.to(labelRef.current, {
|
||||
duration: mode.type === 'PAN' ? 0 : 0.1,
|
||||
left: screenPosition.x - labelSize.width * 0.5,
|
||||
top: screenPosition.y - labelSize.height - node.labelHeight * zoom,
|
||||
scale: zoom
|
||||
});
|
||||
}, [node.position, node.labelHeight, zoom, scroll.position, mode, labelSize]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!labelRef.current) return;
|
||||
|
||||
setLabelSize({
|
||||
width: labelRef.current.clientWidth ?? 0,
|
||||
height: labelRef.current.clientHeight ?? 0
|
||||
});
|
||||
}, [node.label, node.labelElement]);
|
||||
|
||||
useEffect(() => {
|
||||
updateColor(node.color);
|
||||
}, [node.color, updateColor]);
|
||||
|
||||
useEffect(() => {
|
||||
setActive(node.isSelected);
|
||||
}, [setActive, node.isSelected]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={labelRef}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
transformOrigin: 'bottom center'
|
||||
}}
|
||||
>
|
||||
{(node.labelElement || node.label) && (
|
||||
<DefaultLabelContainer
|
||||
labelHeight={node.labelHeight}
|
||||
parentContainer={labelConnectorContainer.current}
|
||||
>
|
||||
{node.label && <MarkdownLabel label={node.label} />}
|
||||
{node.labelElement !== undefined && node.labelElement}
|
||||
</DefaultLabelContainer>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -1,64 +0,0 @@
|
||||
import React, { useEffect, useRef, useCallback } from 'react';
|
||||
import { Box } from '@mui/material';
|
||||
import gsap from 'gsap';
|
||||
import { PROJECTED_TILE_DIMENSIONS } from 'src/renderer/utils/constants';
|
||||
|
||||
interface Props {
|
||||
iconUrl?: string;
|
||||
position: { x: number; y: number };
|
||||
}
|
||||
|
||||
export const NodeV2 = ({ iconUrl, position }: Props) => {
|
||||
const ref = useRef<HTMLImageElement>();
|
||||
|
||||
const setPosition = useCallback(
|
||||
({
|
||||
position: _position,
|
||||
animationDuration = 0.15
|
||||
}: {
|
||||
position: { x: number; y: number };
|
||||
animationDuration?: number;
|
||||
}) => {
|
||||
if (!ref.current) return;
|
||||
|
||||
gsap.to(ref.current, {
|
||||
duration: animationDuration,
|
||||
x: _position.x - PROJECTED_TILE_DIMENSIONS.width / 2,
|
||||
y:
|
||||
_position.y -
|
||||
PROJECTED_TILE_DIMENSIONS.height / 2 -
|
||||
ref.current.height
|
||||
});
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!ref.current) return;
|
||||
|
||||
setPosition({ position });
|
||||
}, [position, setPosition]);
|
||||
|
||||
const onImageLoaded = useCallback(() => {
|
||||
if (!ref.current) return;
|
||||
|
||||
gsap.killTweensOf(ref.current);
|
||||
setPosition({ position, animationDuration: 0 });
|
||||
ref.current.style.opacity = '1';
|
||||
}, [position, setPosition]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={ref}
|
||||
onLoad={onImageLoaded}
|
||||
component="img"
|
||||
src={iconUrl}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
width: PROJECTED_TILE_DIMENSIONS.width,
|
||||
pointerEvents: 'none',
|
||||
opacity: 0
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,51 +0,0 @@
|
||||
import { useRef, useCallback } from 'react';
|
||||
import { Group, Path, Point } from 'paper';
|
||||
import { useTheme } from '@mui/material';
|
||||
import {
|
||||
PIXEL_UNIT,
|
||||
PROJECTED_TILE_DIMENSIONS
|
||||
} from 'src/renderer/utils/constants';
|
||||
|
||||
export const useLabelConnector = () => {
|
||||
const theme = useTheme();
|
||||
const containerRef = useRef(new Group());
|
||||
const pathRef = useRef<paper.Path.Line>();
|
||||
|
||||
const updateHeight = useCallback((labelHeight: number) => {
|
||||
if (!pathRef.current) return;
|
||||
|
||||
pathRef.current.segments[1].point.y = -(
|
||||
labelHeight +
|
||||
PROJECTED_TILE_DIMENSIONS.height * 0.5
|
||||
);
|
||||
}, []);
|
||||
|
||||
const init = useCallback(() => {
|
||||
containerRef.current.removeChildren();
|
||||
|
||||
pathRef.current = new Path.Line({
|
||||
strokeColor: theme.palette.grey[800],
|
||||
strokeWidth: PIXEL_UNIT * 2.5,
|
||||
dashArray: [0, PIXEL_UNIT * 6],
|
||||
strokeJoin: 'round',
|
||||
strokeCap: 'round',
|
||||
dashOffset: PIXEL_UNIT * 4,
|
||||
from: new Point(0, 0)
|
||||
});
|
||||
|
||||
containerRef.current.addChild(pathRef.current);
|
||||
|
||||
return containerRef.current;
|
||||
}, [theme.palette.grey]);
|
||||
|
||||
const destroy = useCallback(() => {
|
||||
return containerRef.current.remove();
|
||||
}, []);
|
||||
|
||||
return {
|
||||
containerRef,
|
||||
init,
|
||||
updateHeight,
|
||||
destroy
|
||||
};
|
||||
};
|
||||
@@ -1,69 +0,0 @@
|
||||
import { useRef, useCallback, useState } from 'react';
|
||||
import { Group, Raster, Point } from 'paper';
|
||||
import { useSceneStore } from 'src/stores/useSceneStore';
|
||||
import { PROJECTED_TILE_DIMENSIONS } from '../../utils/constants';
|
||||
|
||||
const NODE_IMG_PADDING = 0;
|
||||
|
||||
export const useNodeIcon = () => {
|
||||
const [isLoaded, setIsLoaded] = useState(false);
|
||||
const container = useRef(new Group());
|
||||
const icons = useSceneStore((state) => {
|
||||
return state.icons;
|
||||
});
|
||||
|
||||
const update = useCallback(
|
||||
async (iconId: string) => {
|
||||
setIsLoaded(false);
|
||||
container.current.removeChildren();
|
||||
|
||||
const icon = icons.find((_icon) => {
|
||||
return _icon.id === iconId;
|
||||
});
|
||||
|
||||
if (!icon) return;
|
||||
|
||||
await new Promise((resolve) => {
|
||||
const iconRaster = new Raster();
|
||||
|
||||
iconRaster.onLoad = () => {
|
||||
if (!container.current) return;
|
||||
|
||||
iconRaster.scale(
|
||||
(PROJECTED_TILE_DIMENSIONS.width - NODE_IMG_PADDING) /
|
||||
iconRaster.bounds.width
|
||||
);
|
||||
|
||||
const raster = iconRaster.rasterize();
|
||||
|
||||
container.current.addChild(raster);
|
||||
container.current.pivot = iconRaster.bounds.bottomCenter;
|
||||
container.current.position = new Point(
|
||||
0,
|
||||
PROJECTED_TILE_DIMENSIONS.height * 0.5
|
||||
);
|
||||
|
||||
resolve(null);
|
||||
};
|
||||
|
||||
iconRaster.source = icon.url;
|
||||
setIsLoaded(true);
|
||||
});
|
||||
},
|
||||
[icons]
|
||||
);
|
||||
|
||||
const init = useCallback(() => {
|
||||
container.current.removeChildren();
|
||||
container.current = new Group();
|
||||
|
||||
return container.current;
|
||||
}, []);
|
||||
|
||||
return {
|
||||
container: container.current,
|
||||
update,
|
||||
init,
|
||||
isLoaded
|
||||
};
|
||||
};
|
||||
@@ -1,90 +0,0 @@
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { Group, Shape } from 'paper';
|
||||
import gsap from 'gsap';
|
||||
import { TILE_SIZE, PIXEL_UNIT } from 'src/renderer/utils/constants';
|
||||
import { applyProjectionMatrix } from 'src/renderer/utils/projection';
|
||||
import { getColorVariant } from 'src/utils';
|
||||
|
||||
export const useNodeTile = () => {
|
||||
const containerRef = useRef(new Group());
|
||||
const tileRef = useRef<paper.Shape.Rectangle>();
|
||||
const highlightRef = useRef<paper.Shape.Rectangle>();
|
||||
|
||||
const updateColor = useCallback((color: string) => {
|
||||
if (!tileRef.current || !highlightRef.current) return;
|
||||
|
||||
tileRef.current.set({
|
||||
fillColor: color,
|
||||
strokeColor: getColorVariant(color, 'dark', { grade: 2 })
|
||||
});
|
||||
|
||||
highlightRef.current.set({
|
||||
strokeColor: getColorVariant(color, 'dark', { grade: 2 })
|
||||
});
|
||||
}, []);
|
||||
|
||||
const setActive = useCallback((isActive: boolean) => {
|
||||
if (!highlightRef.current) return;
|
||||
|
||||
if (isActive) {
|
||||
highlightRef.current.set({ visible: true });
|
||||
|
||||
gsap
|
||||
.fromTo(
|
||||
highlightRef.current,
|
||||
{ dashOffset: 0 },
|
||||
{
|
||||
dashOffset: PIXEL_UNIT * 12,
|
||||
duration: 0.25,
|
||||
ease: 'none'
|
||||
}
|
||||
)
|
||||
.repeat(-1);
|
||||
} else {
|
||||
highlightRef.current.set({ visible: false });
|
||||
gsap.killTweensOf(highlightRef.current);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const init = useCallback(() => {
|
||||
containerRef.current.removeChildren();
|
||||
containerRef.current.set({ pivot: [0, 0] });
|
||||
|
||||
tileRef.current = new Shape.Rectangle({
|
||||
strokeCap: 'round',
|
||||
size: [TILE_SIZE * 1.2, TILE_SIZE * 1.2],
|
||||
radius: PIXEL_UNIT * 8,
|
||||
strokeWidth: 1,
|
||||
strokeColor: 'black',
|
||||
pivot: [0, 0],
|
||||
position: [0, 0],
|
||||
dashArray: null
|
||||
});
|
||||
|
||||
highlightRef.current = tileRef.current.clone().set({
|
||||
radius: PIXEL_UNIT * 12,
|
||||
strokeWidth: PIXEL_UNIT * 3,
|
||||
pivot: [0, 0],
|
||||
dashArray: [PIXEL_UNIT * 6, PIXEL_UNIT * 6],
|
||||
scaling: 1.2,
|
||||
visible: false
|
||||
});
|
||||
|
||||
containerRef.current.addChild(highlightRef.current);
|
||||
containerRef.current.addChild(tileRef.current);
|
||||
applyProjectionMatrix(containerRef.current);
|
||||
|
||||
tileRef.current.position.set(0, 0);
|
||||
highlightRef.current.position.set(0, 0);
|
||||
tileRef.current.applyMatrix = true;
|
||||
highlightRef.current.applyMatrix = true;
|
||||
|
||||
return containerRef.current;
|
||||
}, []);
|
||||
|
||||
return {
|
||||
init,
|
||||
updateColor,
|
||||
setActive
|
||||
};
|
||||
};
|
||||
@@ -1,10 +0,0 @@
|
||||
import { useRef } from 'react';
|
||||
import { Group } from 'paper';
|
||||
|
||||
export const useConnectorManager = () => {
|
||||
const containerRef = useRef(new Group());
|
||||
|
||||
return {
|
||||
container: containerRef.current
|
||||
};
|
||||
};
|
||||
@@ -1,10 +0,0 @@
|
||||
import { useRef } from 'react';
|
||||
import { Group } from 'paper';
|
||||
|
||||
export const useGroupManager = () => {
|
||||
const containerRef = useRef(new Group());
|
||||
|
||||
return {
|
||||
container: containerRef.current
|
||||
};
|
||||
};
|
||||
@@ -1,10 +0,0 @@
|
||||
import { useRef } from 'react';
|
||||
import { Group } from 'paper';
|
||||
|
||||
export const useNodeManager = () => {
|
||||
const container = useRef(new Group());
|
||||
|
||||
return {
|
||||
container: container.current
|
||||
};
|
||||
};
|
||||
@@ -1,67 +0,0 @@
|
||||
import { useCallback, useRef } from 'react';
|
||||
import Paper, { Group } from 'paper';
|
||||
import { Coords } from 'src/types';
|
||||
import { useUiStateStore } from 'src/stores/useUiStateStore';
|
||||
import { CoordsUtils } from 'src/utils';
|
||||
import { useNodeManager } from './useNodeManager';
|
||||
import { useGroupManager } from './useGroupManager';
|
||||
import { useConnectorManager } from './useConnectorManager';
|
||||
|
||||
export const useRenderer = () => {
|
||||
const container = useRef(new Group());
|
||||
const innerContainer = useRef(new Group());
|
||||
// TODO: Store layers in a giant ref object called layers? layers = { lasso: new Group(), grid: new Group() etc }
|
||||
const lassoContainer = useRef(new Group());
|
||||
const nodeManager = useNodeManager();
|
||||
const connectorManager = useConnectorManager();
|
||||
const groupManager = useGroupManager();
|
||||
const uiStateActions = useUiStateStore((state) => {
|
||||
return state.actions;
|
||||
});
|
||||
|
||||
const { setScroll } = uiStateActions;
|
||||
|
||||
const zoomTo = useCallback((zoom: number) => {
|
||||
Paper.project.activeLayer.view.zoom = zoom;
|
||||
}, []);
|
||||
|
||||
const init = useCallback(() => {
|
||||
// TODO: Grid and Cursor should be initialised in their JSX components (create if they don't exist)
|
||||
// to be inline with other initialisation patterns
|
||||
|
||||
// innerContainer.current.addChild(gridContainer);
|
||||
innerContainer.current.addChild(groupManager.container);
|
||||
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] });
|
||||
Paper.project.activeLayer.addChild(container.current);
|
||||
setScroll({
|
||||
position: CoordsUtils.zero(),
|
||||
offset: CoordsUtils.zero()
|
||||
});
|
||||
}, [setScroll, nodeManager.container, groupManager.container]);
|
||||
|
||||
const scrollTo = useCallback((to: Coords) => {
|
||||
const { center: viewCenter } = Paper.project.view.bounds;
|
||||
|
||||
const newPosition = {
|
||||
x: to.x + viewCenter.x,
|
||||
y: to.y + viewCenter.y
|
||||
};
|
||||
|
||||
container.current.position.set(newPosition);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
init,
|
||||
container,
|
||||
zoomTo,
|
||||
scrollTo,
|
||||
nodeManager,
|
||||
groupManager,
|
||||
lassoContainer,
|
||||
connectorManager
|
||||
};
|
||||
};
|
||||
@@ -1,9 +0,0 @@
|
||||
import { Size } from 'src/types';
|
||||
|
||||
export const TILE_SIZE = 100;
|
||||
export const PROJECTED_TILE_DIMENSIONS: Size = {
|
||||
width: TILE_SIZE * 1.415,
|
||||
height: TILE_SIZE * 0.819
|
||||
};
|
||||
export const PIXEL_UNIT = TILE_SIZE * 0.02;
|
||||
export const SCALING_CONST = 0.9425;
|
||||
@@ -1,232 +0,0 @@
|
||||
import Paper from 'paper';
|
||||
import { PROJECTED_TILE_DIMENSIONS } from 'src/renderer/utils/constants';
|
||||
import { Coords } from 'src/types';
|
||||
import { clamp, CoordsUtils } from 'src/utils';
|
||||
import { SortedSceneItems, Node } from 'src/stores/useSceneStore';
|
||||
import { Scroll } from 'src/stores/useUiStateStore';
|
||||
|
||||
const halfW = PROJECTED_TILE_DIMENSIONS.width * 0.5;
|
||||
const halfH = PROJECTED_TILE_DIMENSIONS.height * 0.5;
|
||||
|
||||
interface GetTileFromMouse {
|
||||
mousePosition: Coords;
|
||||
scroll: Scroll;
|
||||
gridSize: Coords;
|
||||
}
|
||||
|
||||
export const getTileFromMouse = ({
|
||||
mousePosition,
|
||||
scroll,
|
||||
gridSize
|
||||
}: GetTileFromMouse) => {
|
||||
const canvasPosition = {
|
||||
x: mousePosition.x - (scroll.position.x + Paper.view.bounds.center.x),
|
||||
y:
|
||||
mousePosition.y - (scroll.position.y + Paper.view.bounds.center.y) + halfH
|
||||
};
|
||||
|
||||
const row = Math.floor(
|
||||
((mousePosition.x - scroll.position.x) / halfW + canvasPosition.y / halfH) /
|
||||
2
|
||||
);
|
||||
const col = Math.floor(
|
||||
(canvasPosition.y / halfH - canvasPosition.x / halfW) / 2
|
||||
);
|
||||
|
||||
const halfRowNum = Math.floor(gridSize.x * 0.5);
|
||||
const halfColNum = Math.floor(gridSize.y * 0.5);
|
||||
|
||||
return {
|
||||
x: clamp(row, -halfRowNum, halfRowNum),
|
||||
y: clamp(col, -halfColNum, halfColNum)
|
||||
};
|
||||
};
|
||||
|
||||
export const getTilePosition = ({ x, y }: Coords) => {
|
||||
return { x: x * halfW - y * halfW, y: x * halfH + y * halfH };
|
||||
};
|
||||
|
||||
export const getTileBounds = (coords: Coords) => {
|
||||
const position = getTilePosition(coords);
|
||||
|
||||
return {
|
||||
left: {
|
||||
x: position.x - PROJECTED_TILE_DIMENSIONS.width * 0.5,
|
||||
y: position.y
|
||||
},
|
||||
right: {
|
||||
x: position.x + PROJECTED_TILE_DIMENSIONS.width * 0.5,
|
||||
y: position.y
|
||||
},
|
||||
top: {
|
||||
x: position.x,
|
||||
y: position.y - PROJECTED_TILE_DIMENSIONS.height * 0.5
|
||||
},
|
||||
bottom: {
|
||||
x: position.x,
|
||||
y: position.y + PROJECTED_TILE_DIMENSIONS.height * 0.5
|
||||
},
|
||||
center: { x: position.x, y: position.y }
|
||||
};
|
||||
};
|
||||
|
||||
interface GetItemsByTile {
|
||||
tile: Coords;
|
||||
sortedSceneItems: SortedSceneItems;
|
||||
}
|
||||
|
||||
// TODO: Acheive better performance with more granular functions e.g. getNodesByTile, or even getFirstNodeByTile
|
||||
export const getItemsByTile = ({
|
||||
tile,
|
||||
sortedSceneItems
|
||||
}: GetItemsByTile): { nodes: Node[] } => {
|
||||
const nodes = sortedSceneItems.nodes.filter((node) => {
|
||||
return CoordsUtils.isEqual(node.position, tile);
|
||||
});
|
||||
|
||||
return { nodes };
|
||||
};
|
||||
|
||||
interface GetItemsByTileV2 {
|
||||
tile: Coords;
|
||||
sceneItems: Node[];
|
||||
}
|
||||
|
||||
export const getItemsByTileV2 = ({ tile, sceneItems }: GetItemsByTileV2) => {
|
||||
return sceneItems.filter((item) => {
|
||||
return CoordsUtils.isEqual(item.position, tile);
|
||||
});
|
||||
};
|
||||
|
||||
interface CanvasCoordsToScreenCoords {
|
||||
position: Coords;
|
||||
scrollPosition: Coords;
|
||||
zoom: number;
|
||||
}
|
||||
|
||||
export const canvasCoordsToScreenCoords = ({
|
||||
position,
|
||||
scrollPosition,
|
||||
zoom
|
||||
}: CanvasCoordsToScreenCoords) => {
|
||||
const { width: viewW, height: viewH } = Paper.view.bounds;
|
||||
const { offsetLeft: offsetX, offsetTop: offsetY } =
|
||||
Paper.project.view.element;
|
||||
const container = Paper.project.activeLayer.children[0];
|
||||
const globalItemsGroupPosition = container.globalToLocal([0, 0]);
|
||||
const onScreenPosition = {
|
||||
x:
|
||||
(position.x +
|
||||
scrollPosition.x +
|
||||
globalItemsGroupPosition.x +
|
||||
container.position.x +
|
||||
viewW * 0.5) *
|
||||
zoom +
|
||||
offsetX,
|
||||
|
||||
y:
|
||||
(position.y +
|
||||
scrollPosition.y +
|
||||
globalItemsGroupPosition.y +
|
||||
container.position.y +
|
||||
viewH * 0.5) *
|
||||
zoom +
|
||||
offsetY
|
||||
};
|
||||
|
||||
return onScreenPosition;
|
||||
};
|
||||
|
||||
type GetTileScreenPosition = CanvasCoordsToScreenCoords & {
|
||||
origin?: 'center' | 'top' | 'bottom' | 'left' | 'right';
|
||||
};
|
||||
|
||||
export const getTileScreenPosition = ({
|
||||
position,
|
||||
scrollPosition,
|
||||
zoom,
|
||||
origin = 'center'
|
||||
}: GetTileScreenPosition) => {
|
||||
const tilePosition = getTileBounds(position)[origin];
|
||||
const onScreenPosition = canvasCoordsToScreenCoords({
|
||||
position: tilePosition,
|
||||
scrollPosition,
|
||||
zoom
|
||||
});
|
||||
|
||||
return onScreenPosition;
|
||||
};
|
||||
|
||||
export const sortByPosition = (tiles: Coords[]) => {
|
||||
const xSorted = [...tiles];
|
||||
const ySorted = [...tiles];
|
||||
xSorted.sort((a, b) => {
|
||||
return a.x - b.x;
|
||||
});
|
||||
ySorted.sort((a, b) => {
|
||||
return a.y - b.y;
|
||||
});
|
||||
|
||||
const highest = {
|
||||
byX: xSorted[xSorted.length - 1],
|
||||
byY: ySorted[ySorted.length - 1]
|
||||
};
|
||||
const lowest = { byX: xSorted[0], byY: ySorted[0] };
|
||||
|
||||
const lowX = lowest.byX.x;
|
||||
const highX = highest.byX.x;
|
||||
const lowY = lowest.byY.y;
|
||||
const highY = highest.byY.y;
|
||||
|
||||
return {
|
||||
byX: xSorted,
|
||||
byY: ySorted,
|
||||
highest,
|
||||
lowest,
|
||||
lowX,
|
||||
lowY,
|
||||
highX,
|
||||
highY
|
||||
};
|
||||
};
|
||||
|
||||
// Returns a complete set of tiles that form a grid area (takes in any number of tiles to use points to encapsulate)
|
||||
export const getGridSubset = (tiles: Coords[]) => {
|
||||
const { lowX, lowY, highX, highY } = sortByPosition(tiles);
|
||||
|
||||
const subset = [];
|
||||
|
||||
for (let x = lowX; x < highX + 1; x += 1) {
|
||||
for (let y = lowY; y < highY + 1; y += 1) {
|
||||
subset.push({ x, y });
|
||||
}
|
||||
}
|
||||
|
||||
return subset;
|
||||
};
|
||||
|
||||
export const isWithinBounds = (tile: Coords, bounds: Coords[]) => {
|
||||
const { lowX, lowY, highX, highY } = sortByPosition(bounds);
|
||||
|
||||
return tile.x >= lowX && tile.x <= highX && tile.y >= lowY && tile.y <= highY;
|
||||
};
|
||||
|
||||
// Returns the four corners of a grid that encapsulates all tiles
|
||||
// passed in (at least 1 tile needed)
|
||||
export const getBoundingBox = (
|
||||
tiles: Coords[],
|
||||
offset: Coords = CoordsUtils.zero()
|
||||
) => {
|
||||
if (tiles.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { lowX, lowY, highX, highY } = sortByPosition(tiles);
|
||||
|
||||
return [
|
||||
{ x: lowX - offset.x, y: lowY - offset.y },
|
||||
{ x: highX + offset.x, y: lowY - offset.y },
|
||||
{ x: highX + offset.x, y: highY + offset.y },
|
||||
{ x: lowX - offset.x, y: highY + offset.y }
|
||||
];
|
||||
};
|
||||
@@ -1,31 +0,0 @@
|
||||
import { Matrix, Point } from 'paper';
|
||||
|
||||
export const getProjectionMatrix = (x: number, y: number) => {
|
||||
return new Matrix([
|
||||
Math.sqrt(2) / 2,
|
||||
Math.sqrt(6) / 6,
|
||||
-(Math.sqrt(2) / 2),
|
||||
Math.sqrt(6) / 6,
|
||||
x - (Math.sqrt(2) / 2) * (x - y),
|
||||
y - (Math.sqrt(6) / 6) * (x + y - 2)
|
||||
]);
|
||||
};
|
||||
|
||||
export const applyProjectionMatrix = (
|
||||
item: paper.Item,
|
||||
pivot?: paper.Point,
|
||||
rotation?: number
|
||||
) => {
|
||||
const matrix = getProjectionMatrix(0, 0);
|
||||
matrix.rotate(rotation ?? 0, new Point(0, 0));
|
||||
item.pivot = pivot ?? new Point(0, 0);
|
||||
item.matrix = matrix;
|
||||
|
||||
return matrix;
|
||||
};
|
||||
|
||||
export const getCSSMatrix = (
|
||||
translate: { x: number; y: number } = { x: 0, y: 0 }
|
||||
) => {
|
||||
return `translate(${translate.x}px, ${translate.y}px) matrix(0.707, 0.409, -0.707, 0.409, 0, -0.816)`;
|
||||
};
|
||||
@@ -2,63 +2,8 @@ import { useCallback } from 'react';
|
||||
import { create } from 'zustand';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { produce } from 'immer';
|
||||
import { NODE_DEFAULTS } from 'src/utils/config';
|
||||
import { IconInput, SceneItemTypeEnum, Coords } from 'src/types';
|
||||
|
||||
// TODO: Move all types into a types file for easier access and less mental load over where to look
|
||||
|
||||
export interface Node {
|
||||
type: SceneItemTypeEnum.NODE;
|
||||
id: string;
|
||||
iconId: string;
|
||||
color: string;
|
||||
label: string;
|
||||
labelHeight: number;
|
||||
position: Coords;
|
||||
isSelected: boolean;
|
||||
labelElement?: React.ReactNode;
|
||||
}
|
||||
|
||||
export interface Connector {
|
||||
type: SceneItemTypeEnum.CONNECTOR;
|
||||
id: string;
|
||||
color: string;
|
||||
from: string;
|
||||
to: string;
|
||||
}
|
||||
|
||||
export interface Group {
|
||||
type: SceneItemTypeEnum.GROUP;
|
||||
id: string;
|
||||
nodeIds: string[];
|
||||
}
|
||||
|
||||
export type Icon = IconInput;
|
||||
|
||||
export interface SceneItem {
|
||||
id: string;
|
||||
type: SceneItemTypeEnum;
|
||||
}
|
||||
|
||||
// TODO: Is this needed, or do we just expost a getNodesFromTile() function?
|
||||
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
|
||||
export type Scene = SortedSceneItems & {
|
||||
icons: IconInput[];
|
||||
};
|
||||
|
||||
export interface SceneActions {
|
||||
set: (scene: Scene) => void;
|
||||
setItems: (elements: SortedSceneItems) => void;
|
||||
updateNode: (id: string, updates: Partial<Node>) => void;
|
||||
createNode: (position: Coords) => void;
|
||||
}
|
||||
import { NODE_DEFAULTS } from 'src/config';
|
||||
import { Scene, SceneActions, Node, SceneItemTypeEnum } from 'src/types';
|
||||
|
||||
export type UseSceneStore = Scene & {
|
||||
actions: SceneActions;
|
||||
@@ -75,7 +20,7 @@ export const useSceneStore = create<UseSceneStore>((set, get) => {
|
||||
set: (scene) => {
|
||||
set(scene);
|
||||
},
|
||||
setItems: (items: SortedSceneItems) => {
|
||||
setItems: (items) => {
|
||||
set({ nodes: items.nodes });
|
||||
},
|
||||
updateNode: (id, updates) => {
|
||||
|
||||
@@ -1,109 +1,12 @@
|
||||
import { create } from 'zustand';
|
||||
import { clamp, roundToOneDecimalPlace, CoordsUtils } from 'src/utils';
|
||||
import { Coords } from 'src/types';
|
||||
import { SceneItem, Node } from 'src/stores/useSceneStore';
|
||||
import { UiState, UiStateActions } from 'src/types';
|
||||
|
||||
// TODO: Move into the defaults file
|
||||
const ZOOM_INCREMENT = 0.2;
|
||||
export const MIN_ZOOM = 0.2;
|
||||
export const MAX_ZOOM = 1;
|
||||
|
||||
export enum SidebarTypeEnum {
|
||||
SINGLE_NODE = 'SINGLE_NODE',
|
||||
PROJECT_SETTINGS = 'PROJECT_SETTINGS'
|
||||
}
|
||||
|
||||
export type ItemControls =
|
||||
| {
|
||||
type: SidebarTypeEnum.SINGLE_NODE;
|
||||
nodeId: string;
|
||||
}
|
||||
| {
|
||||
type: SidebarTypeEnum.PROJECT_SETTINGS;
|
||||
}
|
||||
| null;
|
||||
|
||||
export interface Mouse {
|
||||
position: {
|
||||
screen: Coords;
|
||||
tile: Coords;
|
||||
};
|
||||
mousedown: {
|
||||
screen: Coords;
|
||||
tile: Coords;
|
||||
} | null;
|
||||
delta: {
|
||||
screen: Coords;
|
||||
tile: Coords;
|
||||
} | null;
|
||||
}
|
||||
|
||||
// TODO: Extract modes into own file for simplicity
|
||||
export interface CursorMode {
|
||||
type: 'CURSOR';
|
||||
showCursor: boolean;
|
||||
mousedown: {
|
||||
items: { nodes: Node[] };
|
||||
tile: Coords;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface PanMode {
|
||||
type: 'PAN';
|
||||
showCursor: boolean;
|
||||
}
|
||||
|
||||
export interface LassoMode {
|
||||
type: 'LASSO'; // TODO: Put these into an enum
|
||||
showCursor: boolean;
|
||||
selection: {
|
||||
startTile: Coords;
|
||||
endTile: Coords;
|
||||
items: Node[];
|
||||
};
|
||||
isDragging: boolean;
|
||||
}
|
||||
|
||||
export interface DragItemsMode {
|
||||
type: 'DRAG_ITEMS';
|
||||
showCursor: boolean;
|
||||
items: { nodes: Node[] };
|
||||
}
|
||||
|
||||
export type Mode = CursorMode | PanMode | DragItemsMode | LassoMode;
|
||||
|
||||
export type ContextMenu =
|
||||
| SceneItem
|
||||
| {
|
||||
type: 'EMPTY_TILE';
|
||||
position: Coords;
|
||||
}
|
||||
| null;
|
||||
|
||||
export interface Scroll {
|
||||
position: Coords;
|
||||
offset: Coords;
|
||||
}
|
||||
|
||||
export interface UiState {
|
||||
mode: Mode;
|
||||
itemControls: ItemControls;
|
||||
contextMenu: ContextMenu;
|
||||
zoom: number;
|
||||
scroll: Scroll;
|
||||
mouse: Mouse;
|
||||
}
|
||||
|
||||
export interface UiStateActions {
|
||||
setMode: (mode: Mode) => void;
|
||||
incrementZoom: () => void;
|
||||
decrementZoom: () => void;
|
||||
setScroll: (scroll: Scroll) => void;
|
||||
setSidebar: (itemControls: ItemControls) => void;
|
||||
setContextMenu: (contextMenu: ContextMenu) => void;
|
||||
setMouse: (mouse: Mouse) => void;
|
||||
}
|
||||
|
||||
export type UseUiStateStore = UiState & {
|
||||
actions: UiStateActions;
|
||||
};
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
export * from './common';
|
||||
export * from './inputs';
|
||||
export * from './scene';
|
||||
export * from './ui';
|
||||
export * from './interactions';
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
|
||||
export type IconInput = z.infer<typeof iconInput>;
|
||||
export type NodeInput = z.infer<typeof nodeInput> & {
|
||||
labelElement?: React.ReactNode;
|
||||
labelComponent?: React.ReactNode;
|
||||
};
|
||||
export type ConnectorInput = z.infer<typeof connectorInput>;
|
||||
export type GroupInput = z.infer<typeof groupInput>;
|
||||
|
||||
@@ -4,15 +4,15 @@ import {
|
||||
Scroll,
|
||||
ContextMenu,
|
||||
ItemControls,
|
||||
Mouse
|
||||
} from 'src/stores/useUiStateStore';
|
||||
import { SortedSceneItems } from 'src/stores/useSceneStore';
|
||||
Mouse,
|
||||
Scene
|
||||
} from 'src/types';
|
||||
|
||||
export interface State {
|
||||
mode: Mode;
|
||||
mouse: Mouse;
|
||||
scroll: Scroll;
|
||||
scene: SortedSceneItems;
|
||||
scene: Scene;
|
||||
contextMenu: ContextMenu;
|
||||
itemControls: ItemControls;
|
||||
}
|
||||
@@ -1,5 +1,65 @@
|
||||
import { Coords } from './common';
|
||||
import { IconInput } from './inputs';
|
||||
|
||||
export enum TileOriginEnum {
|
||||
CENTER = 'CENTER',
|
||||
TOP = 'TOP',
|
||||
BOTTOM = 'BOTTOM',
|
||||
LEFT = 'LEFT',
|
||||
RIGHT = 'RIGHT'
|
||||
}
|
||||
|
||||
export enum SceneItemTypeEnum {
|
||||
NODE = 'NODE',
|
||||
CONNECTOR = 'CONNECTOR',
|
||||
GROUP = 'GROUP'
|
||||
}
|
||||
|
||||
export interface Node {
|
||||
id: string;
|
||||
type: SceneItemTypeEnum.NODE;
|
||||
iconId: string;
|
||||
color: string;
|
||||
label: string;
|
||||
labelHeight: number;
|
||||
position: Coords;
|
||||
isSelected: boolean;
|
||||
labelComponent?: React.ReactNode;
|
||||
}
|
||||
|
||||
export interface Connector {
|
||||
type: SceneItemTypeEnum.CONNECTOR;
|
||||
id: string;
|
||||
color: string;
|
||||
from: string;
|
||||
to: string;
|
||||
}
|
||||
|
||||
export interface Group {
|
||||
type: SceneItemTypeEnum.GROUP;
|
||||
id: string;
|
||||
nodeIds: string[];
|
||||
color: string;
|
||||
}
|
||||
|
||||
export type SceneItem = Node | Connector | Group;
|
||||
|
||||
export type Icon = IconInput;
|
||||
|
||||
export type Scene = {
|
||||
nodes: Node[];
|
||||
connectors: Connector[];
|
||||
groups: Group[];
|
||||
icons: IconInput[];
|
||||
};
|
||||
|
||||
export interface SceneActions {
|
||||
set: (scene: Scene) => void;
|
||||
setItems: (elements: {
|
||||
nodes: Node[];
|
||||
connectors: Connector[];
|
||||
groups: Group[];
|
||||
}) => void;
|
||||
updateNode: (id: string, updates: Partial<Node>) => void;
|
||||
createNode: (position: Coords) => void;
|
||||
}
|
||||
|
||||
99
src/types/ui.ts
Normal file
99
src/types/ui.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { Coords } from './common';
|
||||
import { SceneItem } from './scene';
|
||||
|
||||
export enum SidebarTypeEnum {
|
||||
SINGLE_NODE = 'SINGLE_NODE',
|
||||
PROJECT_SETTINGS = 'PROJECT_SETTINGS'
|
||||
}
|
||||
|
||||
export type ItemControls =
|
||||
| {
|
||||
type: SidebarTypeEnum.SINGLE_NODE;
|
||||
nodeId: string;
|
||||
}
|
||||
| {
|
||||
type: SidebarTypeEnum.PROJECT_SETTINGS;
|
||||
}
|
||||
| null;
|
||||
|
||||
export interface Mouse {
|
||||
position: {
|
||||
screen: Coords;
|
||||
tile: Coords;
|
||||
};
|
||||
mousedown: {
|
||||
screen: Coords;
|
||||
tile: Coords;
|
||||
} | null;
|
||||
delta: {
|
||||
screen: Coords;
|
||||
tile: Coords;
|
||||
} | null;
|
||||
}
|
||||
|
||||
// Begin mode types
|
||||
export interface CursorMode {
|
||||
type: 'CURSOR';
|
||||
showCursor: boolean;
|
||||
mousedown: {
|
||||
items: SceneItem[];
|
||||
tile: Coords;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface PanMode {
|
||||
type: 'PAN';
|
||||
showCursor: boolean;
|
||||
}
|
||||
|
||||
export interface LassoMode {
|
||||
type: 'LASSO'; // TODO: Put these into an enum
|
||||
showCursor: boolean;
|
||||
selection: {
|
||||
startTile: Coords;
|
||||
endTile: Coords;
|
||||
items: SceneItem[];
|
||||
};
|
||||
isDragging: boolean;
|
||||
}
|
||||
|
||||
export interface DragItemsMode {
|
||||
type: 'DRAG_ITEMS';
|
||||
showCursor: boolean;
|
||||
items: SceneItem[];
|
||||
}
|
||||
|
||||
export type Mode = CursorMode | PanMode | DragItemsMode | LassoMode;
|
||||
// End mode types
|
||||
|
||||
export type ContextMenu =
|
||||
| SceneItem
|
||||
| {
|
||||
type: 'EMPTY_TILE';
|
||||
position: Coords;
|
||||
}
|
||||
| null;
|
||||
|
||||
export interface Scroll {
|
||||
position: Coords;
|
||||
offset: Coords;
|
||||
}
|
||||
|
||||
export interface UiState {
|
||||
mode: Mode;
|
||||
itemControls: ItemControls;
|
||||
contextMenu: ContextMenu;
|
||||
zoom: number;
|
||||
scroll: Scroll;
|
||||
mouse: Mouse;
|
||||
}
|
||||
|
||||
export interface UiStateActions {
|
||||
setMode: (mode: Mode) => void;
|
||||
incrementZoom: () => void;
|
||||
decrementZoom: () => void;
|
||||
setScroll: (scroll: Scroll) => void;
|
||||
setSidebar: (itemControls: ItemControls) => void;
|
||||
setContextMenu: (contextMenu: ContextMenu) => void;
|
||||
setMouse: (mouse: Mouse) => void;
|
||||
}
|
||||
@@ -1,118 +1,19 @@
|
||||
import gsap from 'gsap';
|
||||
import {
|
||||
Coords,
|
||||
SceneInput,
|
||||
NodeInput,
|
||||
ConnectorInput,
|
||||
GroupInput,
|
||||
SceneItemTypeEnum
|
||||
} from 'src/types';
|
||||
import { customVars } from 'src/styles/theme';
|
||||
import chroma from 'chroma-js';
|
||||
import { PROJECTED_TILE_DIMENSIONS } from 'src/renderer/utils/constants';
|
||||
import { Scene, Node, Connector, Group } from 'src/stores/useSceneStore';
|
||||
import { NODE_DEFAULTS } from 'src/utils/config';
|
||||
import { CoordsUtils } from 'src/utils';
|
||||
import { SceneInput, Scene } from 'src/types';
|
||||
|
||||
export const clamp = (num: number, min: number, max: number) => {
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
return num <= min ? min : num >= max ? max : num;
|
||||
};
|
||||
|
||||
export const nonZeroCoords = (coords: Coords) => {
|
||||
// For some reason, gsap doesn't like to tween x and y both to 0, so we force 0 to be just above 0.
|
||||
return {
|
||||
x: coords.x === 0 ? 0.000001 : coords.x,
|
||||
y: coords.y === 0 ? 0.000001 : coords.y
|
||||
};
|
||||
};
|
||||
|
||||
export const getRandom = (min: number, max: number) => {
|
||||
return Math.floor(Math.random() * (max - min) + min);
|
||||
};
|
||||
|
||||
export const tweenPosition = (
|
||||
item: paper.Item,
|
||||
{ x, y, duration }: { x: number; y: number; duration: number }
|
||||
) => {
|
||||
// paperjs doesn't like it when you try to tween the position of an item directly,
|
||||
// so we have to use a proxy object
|
||||
const currPosition = {
|
||||
x: item.position.x,
|
||||
y: item.position.y
|
||||
};
|
||||
|
||||
gsap.to(currPosition, {
|
||||
duration,
|
||||
overwrite: 'auto',
|
||||
x,
|
||||
y,
|
||||
onUpdate: () => {
|
||||
item.set({ position: currPosition });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const roundToOneDecimalPlace = (num: number) => {
|
||||
return Math.round(num * 10) / 10;
|
||||
};
|
||||
|
||||
export const nodeInputToNode = (nodeInput: NodeInput): Node => {
|
||||
return {
|
||||
type: SceneItemTypeEnum.NODE,
|
||||
id: nodeInput.id,
|
||||
label: nodeInput.label ?? NODE_DEFAULTS.label,
|
||||
labelElement: nodeInput.labelElement,
|
||||
labelHeight: nodeInput.labelHeight ?? NODE_DEFAULTS.labelHeight,
|
||||
color: nodeInput.color ?? NODE_DEFAULTS.color,
|
||||
iconId: nodeInput.iconId,
|
||||
position: nodeInput.position,
|
||||
isSelected: false
|
||||
};
|
||||
};
|
||||
|
||||
export const groupInputToGroup = (groupInput: GroupInput): Group => {
|
||||
return {
|
||||
type: SceneItemTypeEnum.GROUP,
|
||||
id: groupInput.id,
|
||||
nodeIds: groupInput.nodeIds
|
||||
};
|
||||
};
|
||||
|
||||
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): Scene => {
|
||||
const nodes = sceneInput.nodes.map((nodeInput) => {
|
||||
return nodeInputToNode(nodeInput);
|
||||
});
|
||||
|
||||
const groups = sceneInput.groups.map((groupInput) => {
|
||||
return groupInputToGroup(groupInput);
|
||||
});
|
||||
|
||||
const connectors = sceneInput.connectors.map((connectorInput) => {
|
||||
return connectorInputToConnector(connectorInput);
|
||||
});
|
||||
|
||||
return {
|
||||
...sceneInput,
|
||||
nodes,
|
||||
groups,
|
||||
connectors,
|
||||
icons: sceneInput.icons
|
||||
} as Scene;
|
||||
};
|
||||
|
||||
export const sceneToSceneInput = (scene: Scene): SceneInput => {
|
||||
const nodes: SceneInput['nodes'] = scene.nodes.map((node) => {
|
||||
return {
|
||||
@@ -158,66 +59,3 @@ export const getColorVariant = (
|
||||
return chroma(color).alpha(alpha).css();
|
||||
}
|
||||
};
|
||||
|
||||
export const screenToIso = ({ x, y }: { x: number; y: number }) => {
|
||||
const editorWidth = window.innerWidth;
|
||||
const editorHeight = window.innerHeight;
|
||||
const halfW = PROJECTED_TILE_DIMENSIONS.width / 2;
|
||||
const halfH = PROJECTED_TILE_DIMENSIONS.height / 2;
|
||||
|
||||
// The origin is the center of the project.
|
||||
const projectPosition = {
|
||||
x: x - editorWidth * 0.5,
|
||||
y: y - editorHeight * 0.5
|
||||
};
|
||||
|
||||
const tile = {
|
||||
x: Math.floor(
|
||||
(projectPosition.x + halfW) / PROJECTED_TILE_DIMENSIONS.width -
|
||||
projectPosition.y / PROJECTED_TILE_DIMENSIONS.height
|
||||
),
|
||||
y: -Math.floor(
|
||||
(projectPosition.y + halfH) / PROJECTED_TILE_DIMENSIONS.height +
|
||||
projectPosition.x / PROJECTED_TILE_DIMENSIONS.width
|
||||
)
|
||||
};
|
||||
|
||||
return tile;
|
||||
};
|
||||
|
||||
export enum OriginEnum {
|
||||
CENTER = 'CENTER',
|
||||
TOP = 'TOP',
|
||||
BOTTOM = 'BOTTOM',
|
||||
LEFT = 'LEFT',
|
||||
RIGHT = 'RIGHT'
|
||||
}
|
||||
|
||||
export const getTilePosition = (
|
||||
{ x, y }: { x: number; y: number },
|
||||
origin: OriginEnum = OriginEnum.CENTER
|
||||
) => {
|
||||
const editorWidth = window.innerWidth;
|
||||
const editorHeight = window.innerHeight;
|
||||
const halfW = PROJECTED_TILE_DIMENSIONS.width / 2;
|
||||
const halfH = PROJECTED_TILE_DIMENSIONS.height / 2;
|
||||
|
||||
const position: Coords = {
|
||||
x: editorWidth * 0.5 + (halfW * x - halfW * y),
|
||||
y: editorHeight * 0.5 - (halfH * x + halfH * y) + halfH
|
||||
};
|
||||
|
||||
switch (origin) {
|
||||
case OriginEnum.TOP:
|
||||
return CoordsUtils.add(position, { x: 0, y: -halfH });
|
||||
case OriginEnum.BOTTOM:
|
||||
return CoordsUtils.add(position, { x: 0, y: halfH });
|
||||
case OriginEnum.LEFT:
|
||||
return CoordsUtils.add(position, { x: -halfW, y: 0 });
|
||||
case OriginEnum.RIGHT:
|
||||
return CoordsUtils.add(position, { x: halfW, y: 0 });
|
||||
case OriginEnum.CENTER:
|
||||
default:
|
||||
return position;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
export * from './CoordsUtils';
|
||||
export * from './common';
|
||||
export * from './config';
|
||||
export * from './inputs';
|
||||
export * from './pathfinder';
|
||||
export * from './renderer';
|
||||
|
||||
70
src/utils/inputs.ts
Normal file
70
src/utils/inputs.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import {
|
||||
SceneInput,
|
||||
NodeInput,
|
||||
ConnectorInput,
|
||||
GroupInput,
|
||||
SceneItemTypeEnum,
|
||||
Scene,
|
||||
Node,
|
||||
Connector,
|
||||
Group
|
||||
} from 'src/types';
|
||||
import { NODE_DEFAULTS, DEFAULT_COLOR } from 'src/config';
|
||||
import { customVars } from 'src/styles/theme';
|
||||
|
||||
export const nodeInputToNode = (nodeInput: NodeInput): Node => {
|
||||
return {
|
||||
type: SceneItemTypeEnum.NODE,
|
||||
id: nodeInput.id,
|
||||
label: nodeInput.label ?? NODE_DEFAULTS.label,
|
||||
labelComponent: nodeInput.labelComponent,
|
||||
labelHeight: nodeInput.labelHeight ?? NODE_DEFAULTS.labelHeight,
|
||||
color: nodeInput.color ?? NODE_DEFAULTS.color,
|
||||
iconId: nodeInput.iconId,
|
||||
position: nodeInput.position,
|
||||
isSelected: false
|
||||
};
|
||||
};
|
||||
|
||||
export const groupInputToGroup = (groupInput: GroupInput): Group => {
|
||||
return {
|
||||
type: SceneItemTypeEnum.GROUP,
|
||||
id: groupInput.id,
|
||||
nodeIds: groupInput.nodeIds,
|
||||
color: groupInput.color ?? DEFAULT_COLOR
|
||||
};
|
||||
};
|
||||
|
||||
export const connectorInputToConnector = (
|
||||
connectorInput: ConnectorInput
|
||||
): Connector => {
|
||||
return {
|
||||
type: SceneItemTypeEnum.CONNECTOR,
|
||||
id: connectorInput.id,
|
||||
color: connectorInput.color ?? DEFAULT_COLOR,
|
||||
from: connectorInput.from,
|
||||
to: connectorInput.to
|
||||
};
|
||||
};
|
||||
|
||||
export const sceneInputtoScene = (sceneInput: SceneInput): Scene => {
|
||||
const nodes = sceneInput.nodes.map((nodeInput) => {
|
||||
return nodeInputToNode(nodeInput);
|
||||
});
|
||||
|
||||
const groups = sceneInput.groups.map((groupInput) => {
|
||||
return groupInputToGroup(groupInput);
|
||||
});
|
||||
|
||||
const connectors = sceneInput.connectors.map((connectorInput) => {
|
||||
return connectorInputToConnector(connectorInput);
|
||||
});
|
||||
|
||||
return {
|
||||
...sceneInput,
|
||||
nodes,
|
||||
groups,
|
||||
connectors,
|
||||
icons: sceneInput.icons
|
||||
} as Scene;
|
||||
};
|
||||
@@ -1,10 +1,10 @@
|
||||
import PF from 'pathfinding';
|
||||
import { Coords } from 'src/types';
|
||||
import { Size, Coords } from 'src/types';
|
||||
|
||||
// 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);
|
||||
export const pathfinder = (gridSize: Size) => {
|
||||
const grid = new PF.Grid(gridSize.width, gridSize.height);
|
||||
const finder = new PF.AStarFinder({
|
||||
heuristic: PF.Heuristic.manhattan,
|
||||
diagonalMovement: PF.DiagonalMovement.Always
|
||||
@@ -12,15 +12,15 @@ export const pathfinder = (gridSize: Coords) => {
|
||||
|
||||
const convertToGridXY = ({ x, y }: Coords): Coords => {
|
||||
return {
|
||||
x: x + Math.floor(gridSize.x * 0.5),
|
||||
y: y + Math.floor(gridSize.y * 0.5)
|
||||
x: x + Math.floor(gridSize.width * 0.5),
|
||||
y: y + Math.floor(gridSize.height * 0.5)
|
||||
};
|
||||
};
|
||||
|
||||
const convertToSceneXY = ({ x, y }: Coords): Coords => {
|
||||
return {
|
||||
x: x - Math.floor(gridSize.x * 0.5),
|
||||
y: y - Math.floor(gridSize.y * 0.5)
|
||||
x: x - Math.floor(gridSize.width * 0.5),
|
||||
y: y - Math.floor(gridSize.height * 0.5)
|
||||
};
|
||||
};
|
||||
|
||||
189
src/utils/renderer.ts
Normal file
189
src/utils/renderer.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
import { TILE_PROJECTION_MULTIPLIERS, UNPROJECTED_TILE_SIZE } from 'src/config';
|
||||
import { Coords, TileOriginEnum, Node, Size, Scroll } from 'src/types';
|
||||
import { CoordsUtils } from 'src/utils';
|
||||
|
||||
interface GetProjectedTileSize {
|
||||
zoom: number;
|
||||
}
|
||||
|
||||
// Gets the size of a tile at a given zoom level
|
||||
export const getProjectedTileSize = ({ zoom }: GetProjectedTileSize): Size => {
|
||||
return {
|
||||
width: UNPROJECTED_TILE_SIZE * TILE_PROJECTION_MULTIPLIERS.width * zoom,
|
||||
height: UNPROJECTED_TILE_SIZE * TILE_PROJECTION_MULTIPLIERS.height * zoom
|
||||
};
|
||||
};
|
||||
|
||||
interface ScreenToIso {
|
||||
mouse: Coords;
|
||||
zoom: number;
|
||||
scroll: Scroll;
|
||||
}
|
||||
|
||||
// converts a mouse position to a tile position
|
||||
export const screenToIso = ({ mouse, zoom, scroll }: ScreenToIso) => {
|
||||
const editorWidth = window.innerWidth;
|
||||
const editorHeight = window.innerHeight;
|
||||
const projectedTileSize = getProjectedTileSize({ zoom });
|
||||
const halfW = projectedTileSize.width / 2;
|
||||
const halfH = projectedTileSize.height / 2;
|
||||
|
||||
// The origin is the center of the project.
|
||||
const projectPosition = {
|
||||
x: mouse.x - scroll.position.x - editorWidth * 0.5,
|
||||
y: mouse.y - scroll.position.y - editorHeight * 0.5
|
||||
};
|
||||
|
||||
const tile = {
|
||||
x: Math.floor(
|
||||
(projectPosition.x + halfW) / projectedTileSize.width -
|
||||
projectPosition.y / projectedTileSize.height
|
||||
),
|
||||
y: -Math.floor(
|
||||
(projectPosition.y + halfH) / projectedTileSize.height +
|
||||
projectPosition.x / projectedTileSize.width
|
||||
)
|
||||
};
|
||||
|
||||
return tile;
|
||||
};
|
||||
|
||||
interface GetTilePosition {
|
||||
tile: Coords;
|
||||
scroll: Scroll;
|
||||
zoom: number;
|
||||
origin?: TileOriginEnum;
|
||||
}
|
||||
|
||||
export const getTilePosition = ({
|
||||
tile,
|
||||
scroll,
|
||||
zoom,
|
||||
origin = TileOriginEnum.CENTER
|
||||
}: GetTilePosition) => {
|
||||
// TODO: Refactor editorWidth to not use window width
|
||||
const editorWidth = window.innerWidth;
|
||||
const editorHeight = window.innerHeight;
|
||||
const projectedTileSize = getProjectedTileSize({ zoom });
|
||||
const halfW = projectedTileSize.width / 2;
|
||||
const halfH = projectedTileSize.height / 2;
|
||||
|
||||
const position: Coords = {
|
||||
x:
|
||||
editorWidth * 0.5 + (halfW * tile.x - halfW * tile.y) + scroll.position.x,
|
||||
y:
|
||||
editorHeight * 0.5 - (halfH * tile.x + halfH * tile.y) + scroll.position.y
|
||||
};
|
||||
|
||||
switch (origin) {
|
||||
case TileOriginEnum.TOP:
|
||||
return CoordsUtils.add(position, { x: 0, y: -halfH });
|
||||
case TileOriginEnum.BOTTOM:
|
||||
return CoordsUtils.add(position, { x: 0, y: halfH });
|
||||
case TileOriginEnum.LEFT:
|
||||
return CoordsUtils.add(position, { x: -halfW, y: 0 });
|
||||
case TileOriginEnum.RIGHT:
|
||||
return CoordsUtils.add(position, { x: halfW, y: 0 });
|
||||
case TileOriginEnum.CENTER:
|
||||
default:
|
||||
return position;
|
||||
}
|
||||
};
|
||||
|
||||
export const sortByPosition = (tiles: Coords[]) => {
|
||||
const xSorted = [...tiles];
|
||||
const ySorted = [...tiles];
|
||||
xSorted.sort((a, b) => {
|
||||
return a.x - b.x;
|
||||
});
|
||||
ySorted.sort((a, b) => {
|
||||
return a.y - b.y;
|
||||
});
|
||||
|
||||
const highest = {
|
||||
byX: xSorted[xSorted.length - 1],
|
||||
byY: ySorted[ySorted.length - 1]
|
||||
};
|
||||
const lowest = { byX: xSorted[0], byY: ySorted[0] };
|
||||
|
||||
const lowX = lowest.byX.x;
|
||||
const highX = highest.byX.x;
|
||||
const lowY = lowest.byY.y;
|
||||
const highY = highest.byY.y;
|
||||
|
||||
return {
|
||||
byX: xSorted,
|
||||
byY: ySorted,
|
||||
highest,
|
||||
lowest,
|
||||
lowX,
|
||||
lowY,
|
||||
highX,
|
||||
highY
|
||||
};
|
||||
};
|
||||
|
||||
// Returns a complete set of tiles that form a grid area (takes in any number of tiles to use points to encapsulate)
|
||||
export const getGridSubset = (tiles: Coords[]) => {
|
||||
const { lowX, lowY, highX, highY } = sortByPosition(tiles);
|
||||
|
||||
const subset = [];
|
||||
|
||||
for (let x = lowX; x < highX + 1; x += 1) {
|
||||
for (let y = lowY; y < highY + 1; y += 1) {
|
||||
subset.push({ x, y });
|
||||
}
|
||||
}
|
||||
|
||||
return subset;
|
||||
};
|
||||
|
||||
export const isWithinBounds = (tile: Coords, bounds: Coords[]) => {
|
||||
const { lowX, lowY, highX, highY } = sortByPosition(bounds);
|
||||
|
||||
return tile.x >= lowX && tile.x <= highX && tile.y >= lowY && tile.y <= highY;
|
||||
};
|
||||
|
||||
// Returns the four corners of a grid that encapsulates all tiles
|
||||
// passed in (at least 1 tile needed)
|
||||
export const getBoundingBox = (
|
||||
tiles: Coords[],
|
||||
offset: Coords = CoordsUtils.zero()
|
||||
): Coords[] => {
|
||||
const { lowX, lowY, highX, highY } = sortByPosition(tiles);
|
||||
|
||||
return [
|
||||
{ x: lowX - offset.x, y: lowY - offset.y },
|
||||
{ x: highX + offset.x, y: lowY - offset.y },
|
||||
{ x: highX + offset.x, y: highY + offset.y },
|
||||
{ x: lowX - offset.x, y: highY + offset.y }
|
||||
];
|
||||
};
|
||||
|
||||
export const getBoundingBoxSize = (boundingBox: Coords[]): Size => {
|
||||
const { lowX, lowY, highX, highY } = sortByPosition(boundingBox);
|
||||
|
||||
return {
|
||||
width: highX - lowX + 1,
|
||||
height: highY - lowY + 1
|
||||
};
|
||||
};
|
||||
|
||||
export const getIsoMatrixCSS = () => {
|
||||
return `matrix(-0.707, 0.409, 0.707, 0.409, 0, -0.816)`;
|
||||
};
|
||||
|
||||
export const getTranslateCSS = (translate: Coords = { x: 0, y: 0 }) => {
|
||||
return `translate(${translate.x}px, ${translate.y}px)`;
|
||||
};
|
||||
|
||||
interface GetNodesByTile {
|
||||
tile: Coords;
|
||||
nodes: Node[];
|
||||
}
|
||||
|
||||
export const filterNodesByTile = ({ tile, nodes }: GetNodesByTile): Node[] => {
|
||||
return nodes.filter((node) => {
|
||||
return CoordsUtils.isEqual(node.position, tile);
|
||||
});
|
||||
};
|
||||
@@ -1,11 +1,7 @@
|
||||
import { Coords } from 'src/types';
|
||||
import { getGridSubset, isWithinBounds } from '../gridHelpers';
|
||||
import { getGridSubset, isWithinBounds } from '../renderer';
|
||||
|
||||
jest.mock('paper', () => {
|
||||
return {};
|
||||
});
|
||||
|
||||
describe('Tests gridhelpers', () => {
|
||||
describe('Tests renderer utils', () => {
|
||||
test('getGridSubset() works correctly', () => {
|
||||
const gridSubset = getGridSubset([
|
||||
{ x: 5, y: 5 },
|
||||
@@ -30,5 +30,6 @@ export const connectorInput = z.object({
|
||||
export const groupInput = z.object({
|
||||
id: z.string(),
|
||||
label: z.string().nullable(),
|
||||
color: z.string().optional(),
|
||||
nodeIds: z.array(z.string())
|
||||
});
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx"
|
||||
"src/**/*.tsx",
|
||||
"src/global.d.ts"
|
||||
],
|
||||
}
|
||||
|
||||
@@ -28,6 +28,14 @@ module.exports = {
|
||||
test: /\.css$/i,
|
||||
use: ["style-loader", "css-loader"],
|
||||
},
|
||||
{
|
||||
test: /\.svg$/,
|
||||
use: [
|
||||
{
|
||||
loader: 'svg-url-loader'
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
|
||||
@@ -3,7 +3,7 @@ const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
|
||||
|
||||
module.exports = {
|
||||
mode: "production",
|
||||
entry: "./src/App.tsx",
|
||||
entry: "./src/Isoflow.tsx",
|
||||
output: {
|
||||
path: path.resolve(__dirname, "../dist"),
|
||||
filename: "index.js",
|
||||
@@ -34,6 +34,14 @@ module.exports = {
|
||||
test: /\.css$/i,
|
||||
use: ["style-loader", "css-loader"],
|
||||
},
|
||||
{
|
||||
test: /\.svg$/,
|
||||
use: [
|
||||
{
|
||||
loader: 'svg-url-loader'
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
|
||||
Reference in New Issue
Block a user