mirror of
https://github.com/stan-smith/FossFLOW.git
synced 2025-12-27 00:19:14 -05:00
refactor: migrate away from paperjs [PHASE 1]
This commit is contained in:
@@ -9,7 +9,7 @@ import {
|
||||
NodeInput,
|
||||
ConnectorInput,
|
||||
GroupInput
|
||||
} from 'src/validation/SceneInput';
|
||||
} from 'src/types';
|
||||
import { useSceneStore, Scene } from 'src/stores/useSceneStore';
|
||||
import { GlobalStyles } from 'src/styles/GlobalStyles';
|
||||
import { Renderer } from 'src/renderer/Renderer';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Add as AddIcon } from '@mui/icons-material';
|
||||
import { Coords } from 'src/utils/Coords';
|
||||
import { Coords } from 'src/types';
|
||||
import { ContextMenu } from './components/ContextMenu';
|
||||
import { ContextMenuItem } from './components/ContextMenuItem';
|
||||
|
||||
@@ -9,8 +9,14 @@ interface Props {
|
||||
position: Coords;
|
||||
}
|
||||
|
||||
export const EmptyTileContextMenu = ({ onAddNode, position }: Props) => (
|
||||
<ContextMenu position={position}>
|
||||
<ContextMenuItem onClick={onAddNode} icon={<AddIcon />} label="Add node" />
|
||||
</ContextMenu>
|
||||
);
|
||||
export const EmptyTileContextMenu = ({ onAddNode, position }: Props) => {
|
||||
return (
|
||||
<ContextMenu position={position}>
|
||||
<ContextMenuItem
|
||||
onClick={onAddNode}
|
||||
icon={<AddIcon />}
|
||||
label="Add node"
|
||||
/>
|
||||
</ContextMenu>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useRef, useEffect, useState } from 'react';
|
||||
import gsap from 'gsap';
|
||||
import { List, Box, Card } from '@mui/material';
|
||||
import { Coords } from 'src/utils/Coords';
|
||||
import { Coords } from 'src/types';
|
||||
import { useUiStateStore } from 'src/stores/useUiStateStore';
|
||||
import { getTileScreenPosition } from 'src/renderer/utils/gridHelpers';
|
||||
import { useSceneStore } from 'src/stores/useSceneStore';
|
||||
@@ -18,67 +18,68 @@ const ARROW = {
|
||||
};
|
||||
|
||||
export const ContextMenu = ({ position, children }: Props) => {
|
||||
const [firstDisplay, setFirstDisplay] = useState(false);
|
||||
const container = useRef<HTMLDivElement>();
|
||||
const scroll = useUiStateStore((state) => state.scroll);
|
||||
const zoom = useUiStateStore((state) => state.zoom);
|
||||
const gridSize = useSceneStore((state) => state.gridSize);
|
||||
return null;
|
||||
// const [firstDisplay, setFirstDisplay] = useState(false);
|
||||
// const container = useRef<HTMLDivElement>();
|
||||
// const scroll = useUiStateStore((state) => state.scroll);
|
||||
// const zoom = useUiStateStore((state) => state.zoom);
|
||||
// const gridSize = useSceneStore((state) => state.gridSize);
|
||||
|
||||
const { position: scrollPosition } = scroll;
|
||||
// const { position: scrollPosition } = scroll;
|
||||
|
||||
useEffect(() => {
|
||||
if (!container.current) return;
|
||||
// useEffect(() => {
|
||||
// if (!container.current) return;
|
||||
|
||||
const screenPosition = getTileScreenPosition({
|
||||
position,
|
||||
scrollPosition,
|
||||
zoom
|
||||
});
|
||||
// const screenPosition = getTileScreenPosition({
|
||||
// position,
|
||||
// scrollPosition,
|
||||
// zoom
|
||||
// });
|
||||
|
||||
gsap.to(container.current, {
|
||||
duration: firstDisplay ? 0.1 : 0,
|
||||
x: screenPosition.x,
|
||||
y: screenPosition.y
|
||||
});
|
||||
// gsap.to(container.current, {
|
||||
// duration: firstDisplay ? 0.1 : 0,
|
||||
// x: screenPosition.x,
|
||||
// y: screenPosition.y
|
||||
// });
|
||||
|
||||
if (firstDisplay) {
|
||||
// The context menu subtly slides in from the left when it is first displayed.
|
||||
gsap.to(container.current, {
|
||||
duration: 0.2,
|
||||
opacity: 1
|
||||
});
|
||||
}
|
||||
// if (firstDisplay) {
|
||||
// // The context menu subtly slides in from the left when it is first displayed.
|
||||
// gsap.to(container.current, {
|
||||
// duration: 0.2,
|
||||
// opacity: 1
|
||||
// });
|
||||
// }
|
||||
|
||||
setFirstDisplay(true);
|
||||
}, [position, scrollPosition, zoom, firstDisplay, gridSize]);
|
||||
// setFirstDisplay(true);
|
||||
// }, [position, scrollPosition, zoom, firstDisplay, gridSize]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={container}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
opacity: 0,
|
||||
marginLeft: '15px',
|
||||
marginTop: '-20px',
|
||||
whiteSpace: 'nowrap'
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
left: -(ARROW.size - 2),
|
||||
top: ARROW.top,
|
||||
width: 0,
|
||||
height: 0,
|
||||
borderTop: `${ARROW.size}px solid transparent`,
|
||||
borderBottom: `${ARROW.size}px solid transparent`,
|
||||
borderRight: `${ARROW.size}px solid`,
|
||||
borderRightColor: COLOR
|
||||
}}
|
||||
/>
|
||||
<Card sx={{ borderRadius: 2 }}>
|
||||
<List sx={{ p: 0 }}>{children}</List>
|
||||
</Card>
|
||||
</Box>
|
||||
);
|
||||
// return (
|
||||
// <Box
|
||||
// ref={container}
|
||||
// sx={{
|
||||
// position: 'absolute',
|
||||
// opacity: 0,
|
||||
// marginLeft: '15px',
|
||||
// marginTop: '-20px',
|
||||
// whiteSpace: 'nowrap'
|
||||
// }}
|
||||
// >
|
||||
// <Box
|
||||
// sx={{
|
||||
// position: 'absolute',
|
||||
// left: -(ARROW.size - 2),
|
||||
// top: ARROW.top,
|
||||
// width: 0,
|
||||
// height: 0,
|
||||
// borderTop: `${ARROW.size}px solid transparent`,
|
||||
// borderBottom: `${ARROW.size}px solid transparent`,
|
||||
// borderRight: `${ARROW.size}px solid`,
|
||||
// borderRightColor: COLOR
|
||||
// }}
|
||||
// />
|
||||
// <Card sx={{ borderRadius: 2 }}>
|
||||
// <List sx={{ p: 0 }}>{children}</List>
|
||||
// </Card>
|
||||
// </Box>
|
||||
// );
|
||||
};
|
||||
|
||||
@@ -15,9 +15,15 @@ import { IconButton } from '../IconButton/IconButton';
|
||||
|
||||
export const ToolMenu = () => {
|
||||
const theme = useTheme();
|
||||
const zoom = useUiStateStore((state) => state.zoom);
|
||||
const mode = useUiStateStore((state) => state.mode);
|
||||
const uiStateStoreActions = useUiStateStore((state) => state.actions);
|
||||
const zoom = useUiStateStore((state) => {
|
||||
return state.zoom;
|
||||
});
|
||||
const mode = useUiStateStore((state) => {
|
||||
return state.mode;
|
||||
});
|
||||
const uiStateStoreActions = useUiStateStore((state) => {
|
||||
return state.actions;
|
||||
});
|
||||
|
||||
return (
|
||||
<Card
|
||||
@@ -32,16 +38,25 @@ export const ToolMenu = () => {
|
||||
<IconButton
|
||||
name="Select"
|
||||
Icon={<NearMeIcon />}
|
||||
onClick={() =>
|
||||
uiStateStoreActions.setMode({ type: 'CURSOR', mousedown: null })
|
||||
}
|
||||
onClick={() => {
|
||||
return uiStateStoreActions.setMode({
|
||||
type: 'CURSOR',
|
||||
showCursor: true,
|
||||
mousedown: null
|
||||
});
|
||||
}}
|
||||
size={theme.customVars.toolMenu.height}
|
||||
isActive={mode.type === 'CURSOR'}
|
||||
/>
|
||||
<IconButton
|
||||
name="Pan"
|
||||
Icon={<PanToolIcon />}
|
||||
onClick={() => uiStateStoreActions.setMode({ type: 'PAN' })}
|
||||
onClick={() => {
|
||||
return uiStateStoreActions.setMode({
|
||||
type: 'PAN',
|
||||
showCursor: false
|
||||
});
|
||||
}}
|
||||
size={theme.customVars.toolMenu.height}
|
||||
isActive={mode.type === 'PAN'}
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import Isoflow, { DefaultLabelContainer } from 'src/Isoflow';
|
||||
import Isoflow from 'src/Isoflow';
|
||||
import { Box, useTheme } from '@mui/material';
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { CustomNode } from './CustomNode/CustomNode';
|
||||
|
||||
const examples = [
|
||||
{ name: 'Basic Editor', component: BasicEditor },
|
||||
{ name: 'Custom Node Label', component: CustomNode }
|
||||
{ name: 'Live Diagrams', component: CustomNode }
|
||||
];
|
||||
|
||||
export const Examples = () => {
|
||||
|
||||
25
src/hooks/useWindowSize.ts
Normal file
25
src/hooks/useWindowSize.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
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,5 +1,5 @@
|
||||
import { SidebarTypeEnum } from 'src/stores/useUiStateStore';
|
||||
import { Coords } from 'src/utils/Coords';
|
||||
import { CoordsUtils } from 'src/utils';
|
||||
import { InteractionReducer } from '../types';
|
||||
import { getItemsByTile } from '../../renderer/utils/gridHelpers';
|
||||
|
||||
@@ -9,7 +9,7 @@ export const Cursor: InteractionReducer = {
|
||||
|
||||
if (
|
||||
draftState.mouse.delta === null ||
|
||||
draftState.mouse.delta?.tile.isEqual(Coords.zero())
|
||||
CoordsUtils.isEqual(draftState.mouse.delta?.tile, CoordsUtils.zero())
|
||||
)
|
||||
return;
|
||||
// User has moved tile since the last event
|
||||
@@ -20,6 +20,7 @@ export const Cursor: InteractionReducer = {
|
||||
// User's last mousedown action was on a node
|
||||
draftState.mode = {
|
||||
type: 'DRAG_ITEMS',
|
||||
showCursor: true,
|
||||
items: draftState.mode.mousedown.items
|
||||
};
|
||||
|
||||
@@ -28,6 +29,7 @@ export const Cursor: InteractionReducer = {
|
||||
|
||||
draftState.mode = {
|
||||
type: 'LASSO',
|
||||
showCursor: false,
|
||||
selection: {
|
||||
startTile: draftState.mode.mousedown.tile,
|
||||
endTile: draftState.mouse.position.tile,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Coords } from 'src/utils/Coords';
|
||||
import { CoordsUtils } from 'src/utils';
|
||||
import { InteractionReducer } from '../types';
|
||||
|
||||
export const DragItems: InteractionReducer = {
|
||||
@@ -7,13 +7,13 @@ export const DragItems: InteractionReducer = {
|
||||
|
||||
if (
|
||||
draftState.mouse.delta !== null &&
|
||||
!draftState.mouse.delta.tile.isEqual(Coords.fromObject({ x: 0, y: 0 }))
|
||||
!CoordsUtils.isEqual(draftState.mouse.delta.tile, CoordsUtils.zero())
|
||||
) {
|
||||
// User has moved tile since the last mouse event
|
||||
draftState.mode.items.nodes.forEach((node) => {
|
||||
const nodeIndex = draftState.scene.nodes.findIndex(
|
||||
(sceneNode) => sceneNode.id === node.id
|
||||
);
|
||||
const nodeIndex = draftState.scene.nodes.findIndex((sceneNode) => {
|
||||
return sceneNode.id === node.id;
|
||||
});
|
||||
|
||||
if (nodeIndex === -1) return;
|
||||
|
||||
@@ -25,6 +25,6 @@ export const DragItems: InteractionReducer = {
|
||||
},
|
||||
mousedown: () => {},
|
||||
mouseup: (draftState) => {
|
||||
draftState.mode = { type: 'CURSOR', mousedown: null };
|
||||
draftState.mode = { type: 'CURSOR', showCursor: true, mousedown: null };
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { Coords } from 'src/utils/Coords';
|
||||
import {
|
||||
isWithinBounds,
|
||||
getItemsByTileV2
|
||||
} from 'src/renderer/utils/gridHelpers';
|
||||
import { CoordsUtils } from 'src/utils';
|
||||
import { isWithinBounds } from 'src/renderer/utils/gridHelpers';
|
||||
import { InteractionReducer } from '../types';
|
||||
|
||||
export const Lasso: InteractionReducer = {
|
||||
@@ -14,7 +11,7 @@ export const Lasso: InteractionReducer = {
|
||||
|
||||
if (
|
||||
draftState.mouse.delta === null ||
|
||||
draftState.mouse.delta.tile.isEqual(Coords.zero())
|
||||
CoordsUtils.isEqual(draftState.mouse.delta.tile, CoordsUtils.zero())
|
||||
)
|
||||
return;
|
||||
// User has moved tile since they moused down
|
||||
@@ -22,7 +19,7 @@ export const Lasso: InteractionReducer = {
|
||||
if (!draftState.mode.isDragging) {
|
||||
const { mousedown } = draftState.mouse;
|
||||
const items = draftState.scene.nodes.filter((node) => {
|
||||
return node.position.isEqual(mousedown.tile);
|
||||
return CoordsUtils.isEqual(node.position, mousedown.tile);
|
||||
});
|
||||
|
||||
// User is creating a selection
|
||||
@@ -37,9 +34,12 @@ export const Lasso: InteractionReducer = {
|
||||
|
||||
if (draftState.mode.isDragging) {
|
||||
// User is dragging an existing selection
|
||||
draftState.mode.selection.startTile =
|
||||
draftState.mode.selection.startTile.add(draftState.mouse.delta.tile);
|
||||
draftState.mode.selection.endTile = draftState.mode.selection.endTile.add(
|
||||
draftState.mode.selection.startTile = CoordsUtils.add(
|
||||
draftState.mode.selection.startTile,
|
||||
draftState.mouse.delta.tile
|
||||
);
|
||||
draftState.mode.selection.endTile = CoordsUtils.add(
|
||||
draftState.mode.selection.endTile,
|
||||
draftState.mouse.delta.tile
|
||||
);
|
||||
}
|
||||
@@ -56,6 +56,7 @@ export const Lasso: InteractionReducer = {
|
||||
if (!isWithinSelection) {
|
||||
draftState.mode = {
|
||||
type: 'CURSOR',
|
||||
showCursor: true,
|
||||
mousedown: null
|
||||
};
|
||||
|
||||
@@ -71,6 +72,7 @@ export const Lasso: InteractionReducer = {
|
||||
|
||||
draftState.mode = {
|
||||
type: 'CURSOR',
|
||||
showCursor: true,
|
||||
mousedown: null
|
||||
};
|
||||
},
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { CoordsUtils } from 'src/utils';
|
||||
import { InteractionReducer } from '../types';
|
||||
|
||||
export const Pan: InteractionReducer = {
|
||||
@@ -6,7 +7,10 @@ export const Pan: InteractionReducer = {
|
||||
|
||||
if (draftState.mouse.mousedown !== null) {
|
||||
draftState.scroll.position = draftState.mouse.delta?.screen
|
||||
? draftState.scroll.position.add(draftState.mouse.delta.screen)
|
||||
? CoordsUtils.add(
|
||||
draftState.scroll.position,
|
||||
draftState.mouse.delta.screen
|
||||
)
|
||||
: draftState.scroll.position;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -7,13 +7,11 @@ import {
|
||||
Mouse
|
||||
} from 'src/stores/useUiStateStore';
|
||||
import { SortedSceneItems } from 'src/stores/useSceneStore';
|
||||
import { Coords } from 'src/utils/Coords';
|
||||
|
||||
export interface State {
|
||||
mode: Mode;
|
||||
mouse: Mouse;
|
||||
scroll: Scroll;
|
||||
gridSize: Coords;
|
||||
scene: SortedSceneItems;
|
||||
contextMenu: ContextMenu;
|
||||
itemControls: ItemControls;
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { produce } from 'immer';
|
||||
import { Tool } from 'paper';
|
||||
import { useSceneStore } from 'src/stores/useSceneStore';
|
||||
import { useUiStateStore } from 'src/stores/useUiStateStore';
|
||||
import { toolEventToMouseEvent } from './utils';
|
||||
import { useUiStateStore, Mouse } from 'src/stores/useUiStateStore';
|
||||
import { CoordsUtils, screenToIso } from 'src/utils';
|
||||
import { DragItems } from './reducers/DragItems';
|
||||
import { Pan } from './reducers/Pan';
|
||||
import { Cursor } from './reducers/Cursor';
|
||||
@@ -18,7 +17,6 @@ const reducers: { [k in string]: InteractionReducer } = {
|
||||
};
|
||||
|
||||
export const useInteractionManager = () => {
|
||||
const tool = useRef<paper.Tool>();
|
||||
const mode = useUiStateStore((state) => {
|
||||
return state.mode;
|
||||
});
|
||||
@@ -40,30 +38,52 @@ export const useInteractionManager = () => {
|
||||
const scene = useSceneStore(({ nodes, connectors, groups }) => {
|
||||
return { nodes, connectors, groups };
|
||||
});
|
||||
const gridSize = useSceneStore((state) => {
|
||||
return state.gridSize;
|
||||
});
|
||||
const sceneActions = useSceneStore((state) => {
|
||||
return state.actions;
|
||||
});
|
||||
|
||||
const onMouseEvent = useCallback(
|
||||
(
|
||||
eventType: 'mousedown' | 'mousemove' | 'mouseup',
|
||||
toolEvent: paper.ToolEvent
|
||||
) => {
|
||||
(e: MouseEvent) => {
|
||||
const reducer = reducers[mode.type];
|
||||
|
||||
if (!reducer) return;
|
||||
if (
|
||||
!reducer ||
|
||||
!(
|
||||
e.type === 'mousemove' ||
|
||||
e.type === 'mouseup' ||
|
||||
e.type === 'mousedown'
|
||||
)
|
||||
)
|
||||
return;
|
||||
|
||||
const reducerAction = reducer[eventType];
|
||||
const reducerAction = reducer[e.type];
|
||||
|
||||
const nextMouse = toolEventToMouseEvent({
|
||||
toolEvent,
|
||||
gridSize,
|
||||
scroll,
|
||||
prevMouse: mouse
|
||||
});
|
||||
const newPosition: Mouse['position'] = {
|
||||
screen: { x: e.clientX, y: e.clientY },
|
||||
tile: screenToIso({ x: e.clientX, y: e.clientY })
|
||||
};
|
||||
|
||||
const newDelta: Mouse['delta'] = {
|
||||
screen: CoordsUtils.subtract(newPosition.screen, mouse.position.screen),
|
||||
tile: CoordsUtils.subtract(newPosition.tile, mouse.position.tile)
|
||||
};
|
||||
|
||||
const getMousedown = (): Mouse['mousedown'] => {
|
||||
switch (e.type) {
|
||||
case 'mousedown':
|
||||
return newPosition;
|
||||
case 'mousemove':
|
||||
return mouse.mousedown;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const nextMouse: Mouse = {
|
||||
position: newPosition,
|
||||
delta: newDelta,
|
||||
mousedown: getMousedown()
|
||||
};
|
||||
|
||||
const newState = produce(
|
||||
{
|
||||
@@ -71,7 +91,6 @@ export const useInteractionManager = () => {
|
||||
mouse: nextMouse,
|
||||
mode,
|
||||
scroll,
|
||||
gridSize,
|
||||
contextMenu,
|
||||
itemControls
|
||||
},
|
||||
@@ -90,8 +109,6 @@ export const useInteractionManager = () => {
|
||||
[
|
||||
mode,
|
||||
scroll,
|
||||
mouse,
|
||||
gridSize,
|
||||
itemControls,
|
||||
uiStateActions,
|
||||
sceneActions,
|
||||
@@ -101,19 +118,14 @@ export const useInteractionManager = () => {
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
tool.current = new Tool();
|
||||
tool.current.onMouseMove = (ev: paper.ToolEvent) => {
|
||||
return onMouseEvent('mousemove', ev);
|
||||
};
|
||||
tool.current.onMouseDown = (ev: paper.ToolEvent) => {
|
||||
return onMouseEvent('mousedown', ev);
|
||||
};
|
||||
tool.current.onMouseUp = (ev: paper.ToolEvent) => {
|
||||
return onMouseEvent('mouseup', ev);
|
||||
};
|
||||
window.addEventListener('mousemove', onMouseEvent);
|
||||
window.addEventListener('mousedown', onMouseEvent);
|
||||
window.addEventListener('mouseup', onMouseEvent);
|
||||
|
||||
return () => {
|
||||
tool.current?.remove();
|
||||
window.removeEventListener('mousemove', onMouseEvent);
|
||||
window.removeEventListener('mousedown', onMouseEvent);
|
||||
window.removeEventListener('mouseup', onMouseEvent);
|
||||
};
|
||||
}, [onMouseEvent]);
|
||||
};
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
import { Coords } from 'src/utils/Coords';
|
||||
import { Mouse, Scroll } from 'src/stores/useUiStateStore';
|
||||
import { getTileFromMouse } from 'src/renderer/utils/gridHelpers';
|
||||
|
||||
interface GetMousePositionFromToolEvent {
|
||||
toolEvent: paper.ToolEvent;
|
||||
gridSize: Coords;
|
||||
scroll: Scroll;
|
||||
}
|
||||
|
||||
const getMousePositionFromToolEvent = ({
|
||||
toolEvent,
|
||||
gridSize,
|
||||
scroll
|
||||
}: GetMousePositionFromToolEvent): Mouse['position'] => {
|
||||
const screenPosition = Coords.fromObject(toolEvent.point);
|
||||
const tile = getTileFromMouse({
|
||||
mousePosition: screenPosition,
|
||||
gridSize,
|
||||
scroll
|
||||
});
|
||||
|
||||
return {
|
||||
screen: screenPosition,
|
||||
tile
|
||||
};
|
||||
};
|
||||
|
||||
interface GetDeltaFromToolEvent {
|
||||
currentPosition: Mouse['position'];
|
||||
prevPosition: Mouse['position'];
|
||||
}
|
||||
|
||||
const getDeltaFromToolEvent = ({
|
||||
currentPosition,
|
||||
prevPosition
|
||||
}: GetDeltaFromToolEvent) => {
|
||||
const delta = currentPosition.screen.subtract(prevPosition.screen);
|
||||
|
||||
if (delta.isEqual(Coords.zero())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
screen: delta,
|
||||
tile: currentPosition.tile.subtract(prevPosition.tile)
|
||||
};
|
||||
};
|
||||
|
||||
interface GetMousedownFromToolEvent {
|
||||
toolEvent: paper.ToolEvent;
|
||||
currentTile: Coords;
|
||||
prevMouse: Mouse;
|
||||
}
|
||||
|
||||
const getMousedownFromToolEvent = ({
|
||||
toolEvent,
|
||||
currentTile,
|
||||
prevMouse
|
||||
}: GetMousedownFromToolEvent) => {
|
||||
if (toolEvent.type === 'mousedown') {
|
||||
return {
|
||||
screen: Coords.fromObject(toolEvent.point),
|
||||
tile: currentTile
|
||||
};
|
||||
}
|
||||
|
||||
if (toolEvent.type === 'mousemove') {
|
||||
return prevMouse.mousedown;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
type ToolEventToMouseEvent = GetMousePositionFromToolEvent & {
|
||||
prevMouse: Mouse;
|
||||
};
|
||||
|
||||
export const toolEventToMouseEvent = ({
|
||||
toolEvent,
|
||||
gridSize,
|
||||
scroll,
|
||||
prevMouse
|
||||
}: ToolEventToMouseEvent): Mouse => {
|
||||
const position = getMousePositionFromToolEvent({
|
||||
toolEvent,
|
||||
gridSize,
|
||||
scroll
|
||||
});
|
||||
const delta = getDeltaFromToolEvent({
|
||||
currentPosition: position,
|
||||
prevPosition: prevMouse.position
|
||||
});
|
||||
const mousedown = getMousedownFromToolEvent({
|
||||
toolEvent,
|
||||
currentTile: position.tile,
|
||||
prevMouse
|
||||
});
|
||||
|
||||
return {
|
||||
position,
|
||||
delta,
|
||||
mousedown
|
||||
};
|
||||
};
|
||||
@@ -1,27 +1,21 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import Paper from 'paper';
|
||||
import gsap from 'gsap';
|
||||
import { Coords } from 'src/utils/Coords';
|
||||
import React from 'react';
|
||||
import { Box } from '@mui/material';
|
||||
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 { Initialiser } from './Initialiser';
|
||||
import { useRenderer } from './useRenderer';
|
||||
import { Node } from './components/Node/Node';
|
||||
import { getTilePosition } from './utils/gridHelpers';
|
||||
import { ContextMenuLayer } from './components/ContextMenuLayer/ContextMenuLayer';
|
||||
import { Lasso } from './components/Lasso/Lasso';
|
||||
import { Connector } from './components/Connector/Connector';
|
||||
import { Group } from './components/Group/Group';
|
||||
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';
|
||||
|
||||
const InitialisedRenderer = () => {
|
||||
const renderer = useRenderer();
|
||||
const [isReady, setIsReady] = useState(false);
|
||||
export const Renderer = () => {
|
||||
const scene = useSceneStore(({ nodes, connectors, groups }) => {
|
||||
return { nodes, connectors, groups };
|
||||
});
|
||||
const gridSize = useSceneStore((state) => {
|
||||
return state.gridSize;
|
||||
const icons = useSceneStore((state) => {
|
||||
return state.icons;
|
||||
});
|
||||
const mode = useUiStateStore((state) => {
|
||||
return state.mode;
|
||||
@@ -35,73 +29,36 @@ const InitialisedRenderer = () => {
|
||||
const scroll = useUiStateStore((state) => {
|
||||
return state.scroll;
|
||||
});
|
||||
const { activeLayer } = Paper.project;
|
||||
useInteractionManager();
|
||||
|
||||
const {
|
||||
init: initRenderer,
|
||||
zoomTo,
|
||||
container: rendererContainer,
|
||||
scrollTo
|
||||
} = renderer;
|
||||
const { position: scrollPosition } = scroll;
|
||||
|
||||
useEffect(() => {
|
||||
initRenderer(gridSize);
|
||||
setIsReady(true);
|
||||
|
||||
return () => {
|
||||
if (activeLayer) gsap.killTweensOf(activeLayer.view);
|
||||
};
|
||||
}, [initRenderer, activeLayer, gridSize.toString()]);
|
||||
|
||||
useEffect(() => {
|
||||
zoomTo(zoom);
|
||||
}, [zoom, zoomTo]);
|
||||
|
||||
useEffect(() => {
|
||||
const { center: viewCenter } = activeLayer.view.bounds;
|
||||
|
||||
const newPosition = new Coords(
|
||||
scrollPosition.x + viewCenter.x,
|
||||
scrollPosition.y + viewCenter.y
|
||||
);
|
||||
|
||||
rendererContainer.current.position.set(newPosition.x, newPosition.y);
|
||||
}, [scrollPosition, rendererContainer, activeLayer.view.bounds]);
|
||||
|
||||
useEffect(() => {
|
||||
if (mode.type !== 'CURSOR') return;
|
||||
|
||||
const { tile } = mouse.position;
|
||||
|
||||
const tilePosition = getTilePosition(tile);
|
||||
renderer.cursor.moveTo(tilePosition);
|
||||
}, [
|
||||
mode,
|
||||
mouse,
|
||||
renderer.cursor.moveTo,
|
||||
gridSize,
|
||||
scrollPosition,
|
||||
renderer.cursor,
|
||||
scroll
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
scrollTo(scrollPosition);
|
||||
}, [scrollPosition, scrollTo]);
|
||||
|
||||
useEffect(() => {
|
||||
const isCursorVisible = mode.type === 'CURSOR';
|
||||
|
||||
renderer.cursor.setVisible(isCursorVisible);
|
||||
}, [mode.type, renderer.cursor]);
|
||||
|
||||
if (!isReady) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{mode.type === 'LASSO' && (
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
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}
|
||||
@@ -134,16 +91,7 @@ const InitialisedRenderer = () => {
|
||||
parentContainer={renderer.nodeManager.container as paper.Group}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const Renderer = () => {
|
||||
return (
|
||||
<Initialiser>
|
||||
<InitialisedRenderer />
|
||||
<ContextMenuLayer />
|
||||
</Initialiser>
|
||||
})} */}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -12,9 +12,6 @@ interface ConnectorProps {
|
||||
|
||||
export const Connector = ({ parentContainer, connector }: ConnectorProps) => {
|
||||
const { init, updateFromTo, updateColor } = useConnector();
|
||||
const gridSize = useSceneStore((state) => {
|
||||
return state.gridSize;
|
||||
});
|
||||
const nodes = useSceneStore((state) => {
|
||||
return state.nodes;
|
||||
});
|
||||
@@ -40,8 +37,8 @@ export const Connector = ({ parentContainer, connector }: ConnectorProps) => {
|
||||
|
||||
if (!fromNode || !toNode) return;
|
||||
|
||||
updateFromTo(gridSize, fromNode.position, toNode.position);
|
||||
}, [gridSize, nodes, connector, updateFromTo]);
|
||||
updateFromTo({ x: 100, y: 100 }, fromNode.position, toNode.position);
|
||||
}, [nodes, connector, updateFromTo]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { Group, Path } from 'paper';
|
||||
import { pathfinder } from 'src/renderer/utils/pathfinder';
|
||||
import { Coords } from 'src/utils/Coords';
|
||||
import { Coords } from 'src/types';
|
||||
import { getTileBounds } from 'src/renderer/utils/gridHelpers';
|
||||
|
||||
export const useConnector = () => {
|
||||
|
||||
72
src/renderer/components/Cursor/Cursor.tsx
Normal file
72
src/renderer/components/Cursor/Cursor.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
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,7 +1,7 @@
|
||||
import { useRef, useCallback } from 'react';
|
||||
import { Group, Shape } from 'paper';
|
||||
import gsap from 'gsap';
|
||||
import { Coords } from 'src/utils/Coords';
|
||||
import { Coords } from 'src/types';
|
||||
import { TILE_SIZE, PIXEL_UNIT } from '../../utils/constants';
|
||||
import { applyProjectionMatrix } from '../../utils/projection';
|
||||
|
||||
@@ -33,10 +33,10 @@ export const useCursor = () => {
|
||||
|
||||
const moveTo = useCallback(
|
||||
(position: Coords, opts?: { animationDuration?: number }) => {
|
||||
const tweenProxy = new Coords(
|
||||
container.current.position.x,
|
||||
container.current.position.y
|
||||
);
|
||||
const tweenProxy = {
|
||||
x: container.current.position.x,
|
||||
y: container.current.position.y
|
||||
};
|
||||
|
||||
gsap.to(tweenProxy, {
|
||||
duration: opts?.animationDuration || 0.1,
|
||||
|
||||
78
src/renderer/components/Grid/Grid.tsx
Normal file
78
src/renderer/components/Grid/Grid.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
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,67 +0,0 @@
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { Path, Point, Group } from 'paper';
|
||||
import { Coords } from 'src/utils/Coords';
|
||||
import { applyProjectionMatrix } from '../../utils/projection';
|
||||
import { TILE_SIZE, PIXEL_UNIT, SCALING_CONST } from '../../utils/constants';
|
||||
|
||||
const LINE_STYLE = {
|
||||
color: 'rgba(0, 0, 0, 0.15)',
|
||||
width: PIXEL_UNIT * 1
|
||||
};
|
||||
|
||||
const drawGrid = (width: number, height: number) => {
|
||||
const container = new Group();
|
||||
|
||||
for (let x = 0; x <= width; x += 1) {
|
||||
const lineLength = height * TILE_SIZE;
|
||||
const start = x * TILE_SIZE - lineLength * 0.5;
|
||||
const line = new Path({
|
||||
segments: [
|
||||
[start, -lineLength * 0.5],
|
||||
[start, lineLength * 0.5]
|
||||
],
|
||||
strokeWidth: LINE_STYLE.width,
|
||||
strokeColor: LINE_STYLE.color
|
||||
});
|
||||
|
||||
container.addChild(line);
|
||||
}
|
||||
|
||||
for (let y = 0; y <= height; y += 1) {
|
||||
const lineLength = width * TILE_SIZE;
|
||||
const start = y * TILE_SIZE - lineLength * 0.5;
|
||||
const line = new Path({
|
||||
segments: [
|
||||
[-lineLength * 0.5, start],
|
||||
[lineLength * 0.5, start]
|
||||
],
|
||||
strokeWidth: LINE_STYLE.width,
|
||||
strokeColor: LINE_STYLE.color
|
||||
});
|
||||
|
||||
container.addChild(line);
|
||||
}
|
||||
|
||||
container.scaling = new Point(SCALING_CONST, SCALING_CONST);
|
||||
container.applyMatrix = true;
|
||||
applyProjectionMatrix(container);
|
||||
|
||||
return container;
|
||||
};
|
||||
|
||||
export const useGrid = () => {
|
||||
const container = useRef(new Group());
|
||||
|
||||
const init = useCallback((gridSize: Coords) => {
|
||||
container.current.removeChildren();
|
||||
const grid = drawGrid(gridSize.x, gridSize.y);
|
||||
container.current.addChild(grid);
|
||||
|
||||
return container.current;
|
||||
}, []);
|
||||
|
||||
return {
|
||||
init,
|
||||
container: container.current
|
||||
};
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { Group, Shape } from 'paper';
|
||||
import { Coords } from 'src/utils/Coords';
|
||||
import { Coords } from 'src/types';
|
||||
import { PIXEL_UNIT, TILE_SIZE } from 'src/renderer/utils/constants';
|
||||
import { getColorVariant } from 'src/utils';
|
||||
import {
|
||||
@@ -26,7 +26,7 @@ export const useGroup = () => {
|
||||
const setTiles = useCallback((tiles: Coords[]) => {
|
||||
if (!pathRef.current) return;
|
||||
|
||||
const corners = getBoundingBox(tiles, new Coords(1, 1));
|
||||
const corners = getBoundingBox(tiles, { x: 1, y: 1 });
|
||||
|
||||
if (corners === null) {
|
||||
containerRef.current.removeChildren();
|
||||
@@ -34,10 +34,10 @@ export const useGroup = () => {
|
||||
}
|
||||
|
||||
const sorted = sortByPosition(corners);
|
||||
const size = new Coords(
|
||||
sorted.highX - sorted.lowX,
|
||||
sorted.highY - sorted.lowY
|
||||
);
|
||||
const size = {
|
||||
x: sorted.highX - sorted.lowX,
|
||||
y: sorted.highY - sorted.lowY
|
||||
};
|
||||
|
||||
pathRef.current.set({
|
||||
position: [0, 0],
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect } from 'react';
|
||||
import { Coords } from 'src/utils/Coords';
|
||||
import { Coords } from 'src/types';
|
||||
import { useLasso } from './useLasso';
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useRef, useCallback } from 'react';
|
||||
import { Group, Shape } from 'paper';
|
||||
import gsap from 'gsap';
|
||||
import { Coords } from 'src/utils/Coords';
|
||||
import { Coords } from 'src/types';
|
||||
import { TILE_SIZE, PIXEL_UNIT } from 'src/renderer/utils/constants';
|
||||
import {
|
||||
getBoundingBox,
|
||||
@@ -25,11 +25,11 @@ export const useLasso = () => {
|
||||
const lassoStartTile = boundingBox[3];
|
||||
const lassoScreenPosition = getTileBounds(lassoStartTile).left;
|
||||
const sorted = sortByPosition(boundingBox);
|
||||
const position = new Coords(sorted.lowX, sorted.highY);
|
||||
const size = new Coords(
|
||||
sorted.highX - sorted.lowX,
|
||||
sorted.highY - sorted.lowY
|
||||
);
|
||||
const position = { x: sorted.lowX, y: sorted.highY };
|
||||
const size = {
|
||||
x: sorted.highX - sorted.lowX,
|
||||
y: sorted.highY - sorted.lowY
|
||||
};
|
||||
|
||||
shapeRef.current.set({
|
||||
position,
|
||||
|
||||
@@ -2,7 +2,6 @@ import React, { useEffect, useRef, useState } from 'react';
|
||||
import { Group } from 'paper';
|
||||
import { Box } from '@mui/material';
|
||||
import gsap from 'gsap';
|
||||
import { Coords } from 'src/utils/Coords';
|
||||
import { useUiStateStore } from 'src/stores/useUiStateStore';
|
||||
import { Node as NodeInterface } from 'src/stores/useSceneStore';
|
||||
import { useNodeIcon } from './useNodeIcon';
|
||||
@@ -63,7 +62,7 @@ export const Node = ({ node, parentContainer }: NodeProps) => {
|
||||
useEffect(() => {
|
||||
if (!isIconLoaded) return;
|
||||
|
||||
const tweenValues = Coords.fromObject(groupRef.current.position);
|
||||
const tweenValues = groupRef.current.position;
|
||||
const endState = getTilePosition(node.position);
|
||||
|
||||
gsap.to(tweenValues, {
|
||||
|
||||
64
src/renderer/components/Node/NodeV2.tsx
Normal file
64
src/renderer/components/Node/NodeV2.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
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
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -16,7 +16,7 @@ export const useLabelConnector = () => {
|
||||
|
||||
pathRef.current.segments[1].point.y = -(
|
||||
labelHeight +
|
||||
PROJECTED_TILE_DIMENSIONS.y * 0.5
|
||||
PROJECTED_TILE_DIMENSIONS.height * 0.5
|
||||
);
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -8,14 +8,18 @@ const NODE_IMG_PADDING = 0;
|
||||
export const useNodeIcon = () => {
|
||||
const [isLoaded, setIsLoaded] = useState(false);
|
||||
const container = useRef(new Group());
|
||||
const icons = useSceneStore((state) => state.icons);
|
||||
const icons = useSceneStore((state) => {
|
||||
return state.icons;
|
||||
});
|
||||
|
||||
const update = useCallback(
|
||||
async (iconId: string) => {
|
||||
setIsLoaded(false);
|
||||
container.current.removeChildren();
|
||||
|
||||
const icon = icons.find((_icon) => _icon.id === iconId);
|
||||
const icon = icons.find((_icon) => {
|
||||
return _icon.id === iconId;
|
||||
});
|
||||
|
||||
if (!icon) return;
|
||||
|
||||
@@ -26,7 +30,7 @@ export const useNodeIcon = () => {
|
||||
if (!container.current) return;
|
||||
|
||||
iconRaster.scale(
|
||||
(PROJECTED_TILE_DIMENSIONS.x - NODE_IMG_PADDING) /
|
||||
(PROJECTED_TILE_DIMENSIONS.width - NODE_IMG_PADDING) /
|
||||
iconRaster.bounds.width
|
||||
);
|
||||
|
||||
@@ -36,7 +40,7 @@ export const useNodeIcon = () => {
|
||||
container.current.pivot = iconRaster.bounds.bottomCenter;
|
||||
container.current.position = new Point(
|
||||
0,
|
||||
PROJECTED_TILE_DIMENSIONS.y * 0.5
|
||||
PROJECTED_TILE_DIMENSIONS.height * 0.5
|
||||
);
|
||||
|
||||
resolve(null);
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { useCallback, useRef } from 'react';
|
||||
import Paper, { Group } from 'paper';
|
||||
import { Coords } from 'src/utils/Coords';
|
||||
import { Coords } from 'src/types';
|
||||
import { useUiStateStore } from 'src/stores/useUiStateStore';
|
||||
import { useGrid } from './components/Grid/useGrid';
|
||||
import { CoordsUtils } from 'src/utils';
|
||||
import { useNodeManager } from './useNodeManager';
|
||||
import { useCursor } from './components/Cursor/useCursor';
|
||||
import { useGroupManager } from './useGroupManager';
|
||||
import { useConnectorManager } from './useConnectorManager';
|
||||
|
||||
@@ -13,52 +12,36 @@ export const useRenderer = () => {
|
||||
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 grid = useGrid();
|
||||
const nodeManager = useNodeManager();
|
||||
const connectorManager = useConnectorManager();
|
||||
const groupManager = useGroupManager();
|
||||
const cursor = useCursor();
|
||||
const uiStateActions = useUiStateStore((state) => {
|
||||
return state.actions;
|
||||
});
|
||||
|
||||
const { setScroll } = uiStateActions;
|
||||
const { init: initGrid } = grid;
|
||||
const { init: initCursor } = cursor;
|
||||
|
||||
const zoomTo = useCallback((zoom: number) => {
|
||||
Paper.project.activeLayer.view.zoom = zoom;
|
||||
}, []);
|
||||
|
||||
const init = useCallback(
|
||||
(gridSize: Coords) => {
|
||||
// TODO: Grid and Cursor should be initialised in their JSX components (create if they don't exist)
|
||||
// to be inline with other initialisation patterns
|
||||
const gridContainer = initGrid(gridSize);
|
||||
const cursorContainer = initCursor();
|
||||
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(cursorContainer);
|
||||
innerContainer.current.addChild(lassoContainer.current);
|
||||
innerContainer.current.addChild(connectorManager.container);
|
||||
innerContainer.current.addChild(nodeManager.container);
|
||||
container.current.addChild(innerContainer.current);
|
||||
container.current.set({ position: [0, 0] });
|
||||
Paper.project.activeLayer.addChild(container.current);
|
||||
setScroll({
|
||||
position: new Coords(0, 0),
|
||||
offset: new Coords(0, 0)
|
||||
});
|
||||
},
|
||||
[
|
||||
initGrid,
|
||||
initCursor,
|
||||
setScroll,
|
||||
nodeManager.container,
|
||||
groupManager.container
|
||||
]
|
||||
);
|
||||
// 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;
|
||||
@@ -74,13 +57,11 @@ export const useRenderer = () => {
|
||||
return {
|
||||
init,
|
||||
container,
|
||||
grid,
|
||||
zoomTo,
|
||||
scrollTo,
|
||||
nodeManager,
|
||||
groupManager,
|
||||
lassoContainer,
|
||||
connectorManager,
|
||||
cursor
|
||||
connectorManager
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Coords } from 'src/utils/Coords';
|
||||
import { Size } from 'src/types';
|
||||
|
||||
export const TILE_SIZE = 72;
|
||||
export const PROJECTED_TILE_DIMENSIONS = new Coords(
|
||||
TILE_SIZE + TILE_SIZE / 3,
|
||||
(TILE_SIZE + TILE_SIZE / 3) / Math.sqrt(3)
|
||||
);
|
||||
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,12 +1,12 @@
|
||||
import Paper from 'paper';
|
||||
import { PROJECTED_TILE_DIMENSIONS } from 'src/renderer/utils/constants';
|
||||
import { Coords } from 'src/utils/Coords';
|
||||
import { clamp } from 'src/utils';
|
||||
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.x * 0.5;
|
||||
const halfH = PROJECTED_TILE_DIMENSIONS.y * 0.5;
|
||||
const halfW = PROJECTED_TILE_DIMENSIONS.width * 0.5;
|
||||
const halfH = PROJECTED_TILE_DIMENSIONS.height * 0.5;
|
||||
|
||||
interface GetTileFromMouse {
|
||||
mousePosition: Coords;
|
||||
@@ -19,13 +19,15 @@ export const getTileFromMouse = ({
|
||||
scroll,
|
||||
gridSize
|
||||
}: GetTileFromMouse) => {
|
||||
const canvasPosition = new Coords(
|
||||
mousePosition.x - (scroll.position.x + Paper.view.bounds.center.x),
|
||||
mousePosition.y - (scroll.position.y + Paper.view.bounds.center.y) + halfH
|
||||
);
|
||||
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(
|
||||
(canvasPosition.x / halfW + canvasPosition.y / halfH) / 2
|
||||
((mousePosition.x - scroll.position.x) / halfW + canvasPosition.y / halfH) /
|
||||
2
|
||||
);
|
||||
const col = Math.floor(
|
||||
(canvasPosition.y / halfH - canvasPosition.x / halfW) / 2
|
||||
@@ -34,34 +36,37 @@ export const getTileFromMouse = ({
|
||||
const halfRowNum = Math.floor(gridSize.x * 0.5);
|
||||
const halfColNum = Math.floor(gridSize.y * 0.5);
|
||||
|
||||
return new Coords(
|
||||
clamp(row, -halfRowNum, halfRowNum),
|
||||
clamp(col, -halfColNum, halfColNum)
|
||||
);
|
||||
return {
|
||||
x: clamp(row, -halfRowNum, halfRowNum),
|
||||
y: clamp(col, -halfColNum, halfColNum)
|
||||
};
|
||||
};
|
||||
|
||||
export const getTilePosition = ({ x, y }: Coords) => {
|
||||
return new Coords(x * halfW - y * halfW, x * halfH + y * halfH);
|
||||
return { x: x * halfW - y * halfW, y: x * halfH + y * halfH };
|
||||
};
|
||||
|
||||
export const getTileBounds = (coords: Coords) => {
|
||||
const position = getTilePosition(coords);
|
||||
|
||||
return {
|
||||
left: new Coords(
|
||||
position.x - PROJECTED_TILE_DIMENSIONS.x * 0.5,
|
||||
position.y
|
||||
),
|
||||
right: new Coords(
|
||||
position.x + PROJECTED_TILE_DIMENSIONS.x * 0.5,
|
||||
position.y
|
||||
),
|
||||
top: new Coords(position.x, position.y - PROJECTED_TILE_DIMENSIONS.y * 0.5),
|
||||
bottom: new Coords(
|
||||
position.x,
|
||||
position.y + PROJECTED_TILE_DIMENSIONS.y * 0.5
|
||||
),
|
||||
center: new Coords(position.x, position.y)
|
||||
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 }
|
||||
};
|
||||
};
|
||||
|
||||
@@ -76,7 +81,7 @@ export const getItemsByTile = ({
|
||||
sortedSceneItems
|
||||
}: GetItemsByTile): { nodes: Node[] } => {
|
||||
const nodes = sortedSceneItems.nodes.filter((node) => {
|
||||
return node.position.isEqual(tile);
|
||||
return CoordsUtils.isEqual(node.position, tile);
|
||||
});
|
||||
|
||||
return { nodes };
|
||||
@@ -89,7 +94,7 @@ interface GetItemsByTileV2 {
|
||||
|
||||
export const getItemsByTileV2 = ({ tile, sceneItems }: GetItemsByTileV2) => {
|
||||
return sceneItems.filter((item) => {
|
||||
return item.position.isEqual(tile);
|
||||
return CoordsUtils.isEqual(item.position, tile);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -109,23 +114,25 @@ export const canvasCoordsToScreenCoords = ({
|
||||
Paper.project.view.element;
|
||||
const container = Paper.project.activeLayer.children[0];
|
||||
const globalItemsGroupPosition = container.globalToLocal([0, 0]);
|
||||
const onScreenPosition = new Coords(
|
||||
(position.x +
|
||||
scrollPosition.x +
|
||||
globalItemsGroupPosition.x +
|
||||
container.position.x +
|
||||
viewW * 0.5) *
|
||||
zoom +
|
||||
const onScreenPosition = {
|
||||
x:
|
||||
(position.x +
|
||||
scrollPosition.x +
|
||||
globalItemsGroupPosition.x +
|
||||
container.position.x +
|
||||
viewW * 0.5) *
|
||||
zoom +
|
||||
offsetX,
|
||||
|
||||
(position.y +
|
||||
scrollPosition.y +
|
||||
globalItemsGroupPosition.y +
|
||||
container.position.y +
|
||||
viewH * 0.5) *
|
||||
zoom +
|
||||
y:
|
||||
(position.y +
|
||||
scrollPosition.y +
|
||||
globalItemsGroupPosition.y +
|
||||
container.position.y +
|
||||
viewH * 0.5) *
|
||||
zoom +
|
||||
offsetY
|
||||
);
|
||||
};
|
||||
|
||||
return onScreenPosition;
|
||||
};
|
||||
@@ -191,7 +198,7 @@ export const getGridSubset = (tiles: Coords[]) => {
|
||||
|
||||
for (let x = lowX; x < highX + 1; x += 1) {
|
||||
for (let y = lowY; y < highY + 1; y += 1) {
|
||||
subset.push(new Coords(x, y));
|
||||
subset.push({ x, y });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,7 +215,7 @@ export const isWithinBounds = (tile: Coords, bounds: Coords[]) => {
|
||||
// passed in (at least 1 tile needed)
|
||||
export const getBoundingBox = (
|
||||
tiles: Coords[],
|
||||
offset: Coords = new Coords(0, 0)
|
||||
offset: Coords = CoordsUtils.zero()
|
||||
) => {
|
||||
if (tiles.length === 0) {
|
||||
return null;
|
||||
@@ -217,9 +224,9 @@ export const getBoundingBox = (
|
||||
const { lowX, lowY, highX, highY } = sortByPosition(tiles);
|
||||
|
||||
return [
|
||||
new Coords(lowX - offset.x, lowY - offset.y),
|
||||
new Coords(highX + offset.x, lowY - offset.y),
|
||||
new Coords(highX + offset.x, highY + offset.y),
|
||||
new Coords(lowX - offset.x, highY + offset.y)
|
||||
{ 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,5 +1,5 @@
|
||||
import PF from 'pathfinding';
|
||||
import { Coords } from 'src/utils/Coords';
|
||||
import { Coords } from 'src/types';
|
||||
|
||||
// TODO1: This file is a mess, refactor it
|
||||
// TODO: Have one single place for utils
|
||||
@@ -10,18 +10,18 @@ export const pathfinder = (gridSize: Coords) => {
|
||||
diagonalMovement: PF.DiagonalMovement.Always
|
||||
});
|
||||
|
||||
const convertToGridXY = ({ x, y }: Coords) => {
|
||||
return new Coords(
|
||||
x + Math.floor(gridSize.x * 0.5),
|
||||
y + Math.floor(gridSize.y * 0.5)
|
||||
);
|
||||
const convertToGridXY = ({ x, y }: Coords): Coords => {
|
||||
return {
|
||||
x: x + Math.floor(gridSize.x * 0.5),
|
||||
y: y + Math.floor(gridSize.y * 0.5)
|
||||
};
|
||||
};
|
||||
|
||||
const convertToSceneXY = ({ x, y }: Coords) => {
|
||||
return new Coords(
|
||||
x - Math.floor(gridSize.x * 0.5),
|
||||
y - Math.floor(gridSize.y * 0.5)
|
||||
);
|
||||
const convertToSceneXY = ({ x, y }: Coords): Coords => {
|
||||
return {
|
||||
x: x - Math.floor(gridSize.x * 0.5),
|
||||
y: y - Math.floor(gridSize.y * 0.5)
|
||||
};
|
||||
};
|
||||
|
||||
const setWalkableAt = (coords: Coords, isWalkable: boolean) => {
|
||||
@@ -29,7 +29,7 @@ export const pathfinder = (gridSize: Coords) => {
|
||||
grid.setWalkableAt(x, y, isWalkable);
|
||||
};
|
||||
|
||||
const findPath = (tiles: Coords[]) => {
|
||||
const findPath = (tiles: Coords[]): Coords[] => {
|
||||
const normalisedRoute = tiles.map((tile) => {
|
||||
return convertToGridXY(tile);
|
||||
});
|
||||
@@ -56,7 +56,7 @@ export const pathfinder = (gridSize: Coords) => {
|
||||
}, [] as number[][]);
|
||||
|
||||
return path.map((tile) => {
|
||||
return convertToSceneXY(new Coords(tile[0], tile[1]));
|
||||
return convertToSceneXY({ x: tile[0], y: tile[1] });
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -23,3 +23,9 @@ export const applyProjectionMatrix = (
|
||||
|
||||
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)`;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Coords } from 'src/utils/Coords';
|
||||
import { Coords } from 'src/types';
|
||||
import { getGridSubset, isWithinBounds } from '../gridHelpers';
|
||||
|
||||
jest.mock('paper', () => {
|
||||
@@ -7,27 +7,33 @@ jest.mock('paper', () => {
|
||||
|
||||
describe('Tests gridhelpers', () => {
|
||||
test('getGridSubset() works correctly', () => {
|
||||
const gridSubset = getGridSubset([new Coords(5, 5), new Coords(7, 7)]);
|
||||
const gridSubset = getGridSubset([
|
||||
{ x: 5, y: 5 },
|
||||
{ x: 7, y: 7 }
|
||||
]);
|
||||
|
||||
expect(gridSubset).toEqual([
|
||||
new Coords(5, 5),
|
||||
new Coords(5, 6),
|
||||
new Coords(5, 7),
|
||||
new Coords(6, 5),
|
||||
new Coords(6, 6),
|
||||
new Coords(6, 7),
|
||||
new Coords(7, 5),
|
||||
new Coords(7, 6),
|
||||
new Coords(7, 7)
|
||||
{ x: 5, y: 5 },
|
||||
{ x: 5, y: 6 },
|
||||
{ x: 5, y: 7 },
|
||||
{ x: 6, y: 5 },
|
||||
{ x: 6, y: 6 },
|
||||
{ x: 6, y: 7 },
|
||||
{ x: 7, y: 5 },
|
||||
{ x: 7, y: 6 },
|
||||
{ x: 7, y: 7 }
|
||||
]);
|
||||
});
|
||||
|
||||
test('isWithinBounds() works correctly', () => {
|
||||
const BOUNDS = [new Coords(4, 4), new Coords(6, 6)];
|
||||
const bounds: Coords[] = [
|
||||
{ x: 4, y: 4 },
|
||||
{ x: 6, y: 6 }
|
||||
];
|
||||
|
||||
const withinBounds = isWithinBounds(new Coords(5, 5), BOUNDS);
|
||||
const onBorder = isWithinBounds(new Coords(4, 4), BOUNDS);
|
||||
const outsideBounds = isWithinBounds(new Coords(3, 3), BOUNDS);
|
||||
const withinBounds = isWithinBounds({ x: 5, y: 5 }, bounds);
|
||||
const onBorder = isWithinBounds({ x: 4, y: 4 }, bounds);
|
||||
const outsideBounds = isWithinBounds({ x: 3, y: 3 }, bounds);
|
||||
|
||||
expect(withinBounds).toBe(true);
|
||||
expect(onBorder).toBe(true);
|
||||
|
||||
@@ -3,15 +3,9 @@ import { create } from 'zustand';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { produce } from 'immer';
|
||||
import { NODE_DEFAULTS } from 'src/utils/config';
|
||||
import { IconInput } from '../validation/SceneInput';
|
||||
import { Coords } from '../utils/Coords';
|
||||
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 enum SceneItemTypeEnum {
|
||||
NODE = 'NODE',
|
||||
CONNECTOR = 'CONNECTOR',
|
||||
GROUP = 'GROUP'
|
||||
}
|
||||
|
||||
export interface Node {
|
||||
type: SceneItemTypeEnum.NODE;
|
||||
@@ -57,7 +51,6 @@ export interface SortedSceneItems {
|
||||
// TODO: This typing is super confusing to work with
|
||||
export type Scene = SortedSceneItems & {
|
||||
icons: IconInput[];
|
||||
gridSize: Coords;
|
||||
};
|
||||
|
||||
export interface SceneActions {
|
||||
@@ -78,7 +71,6 @@ export const useSceneStore = create<UseSceneStore>((set, get) => {
|
||||
connectors: [],
|
||||
groups: [],
|
||||
icons: [],
|
||||
gridSize: new Coords(51, 51),
|
||||
actions: {
|
||||
set: (scene) => {
|
||||
set(scene);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { create } from 'zustand';
|
||||
import { clamp, roundToOneDecimalPlace } from 'src/utils';
|
||||
import { Coords } from 'src/utils/Coords';
|
||||
import { SortedSceneItems, SceneItem, Node } from 'src/stores/useSceneStore';
|
||||
import { clamp, roundToOneDecimalPlace, CoordsUtils } from 'src/utils';
|
||||
import { Coords } from 'src/types';
|
||||
import { SceneItem, Node } from 'src/stores/useSceneStore';
|
||||
|
||||
// TODO: Move into the defaults file
|
||||
const ZOOM_INCREMENT = 0.2;
|
||||
@@ -41,6 +41,7 @@ export interface Mouse {
|
||||
// TODO: Extract modes into own file for simplicity
|
||||
export interface CursorMode {
|
||||
type: 'CURSOR';
|
||||
showCursor: boolean;
|
||||
mousedown: {
|
||||
items: { nodes: Node[] };
|
||||
tile: Coords;
|
||||
@@ -49,10 +50,12 @@ export interface CursorMode {
|
||||
|
||||
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;
|
||||
@@ -63,6 +66,7 @@ export interface LassoMode {
|
||||
|
||||
export interface DragItemsMode {
|
||||
type: 'DRAG_ITEMS';
|
||||
showCursor: boolean;
|
||||
items: { nodes: Node[] };
|
||||
}
|
||||
|
||||
@@ -108,18 +112,19 @@ export const useUiStateStore = create<UseUiStateStore>((set, get) => {
|
||||
return {
|
||||
mode: {
|
||||
type: 'CURSOR',
|
||||
showCursor: true,
|
||||
mousedown: null
|
||||
},
|
||||
mouse: {
|
||||
position: { screen: new Coords(0, 0), tile: new Coords(0, 0) },
|
||||
position: { screen: CoordsUtils.zero(), tile: CoordsUtils.zero() },
|
||||
mousedown: null,
|
||||
delta: null
|
||||
},
|
||||
itemControls: null,
|
||||
contextMenu: null,
|
||||
scroll: {
|
||||
position: new Coords(0, 0),
|
||||
offset: new Coords(0, 0)
|
||||
position: { x: 0, y: 0 },
|
||||
offset: { x: 0, y: 0 }
|
||||
},
|
||||
zoom: 1,
|
||||
actions: {
|
||||
|
||||
6
src/tests/fixtures/scene.ts
vendored
6
src/tests/fixtures/scene.ts
vendored
@@ -1,10 +1,6 @@
|
||||
import { SceneInput } from 'src/validation/SceneInput';
|
||||
import { SceneInput } from 'src/types';
|
||||
|
||||
export const scene: SceneInput = {
|
||||
gridSize: {
|
||||
width: 10,
|
||||
height: 10
|
||||
},
|
||||
icons: [
|
||||
{
|
||||
id: 'icon1',
|
||||
|
||||
9
src/types/common.ts
Normal file
9
src/types/common.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export interface Coords {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface Size {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
3
src/types/index.ts
Normal file
3
src/types/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './common';
|
||||
export * from './inputs';
|
||||
export * from './scene';
|
||||
20
src/types/inputs.ts
Normal file
20
src/types/inputs.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import z from 'zod';
|
||||
import {
|
||||
iconInput,
|
||||
nodeInput,
|
||||
connectorInput,
|
||||
groupInput
|
||||
} from 'src/validation/sceneItems';
|
||||
|
||||
export type IconInput = z.infer<typeof iconInput>;
|
||||
export type NodeInput = z.infer<typeof nodeInput> & {
|
||||
labelElement?: React.ReactNode;
|
||||
};
|
||||
export type ConnectorInput = z.infer<typeof connectorInput>;
|
||||
export type GroupInput = z.infer<typeof groupInput>;
|
||||
export type SceneInput = {
|
||||
icons: IconInput[];
|
||||
nodes: NodeInput[];
|
||||
connectors: ConnectorInput[];
|
||||
groups: GroupInput[];
|
||||
};
|
||||
5
src/types/scene.ts
Normal file
5
src/types/scene.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export enum SceneItemTypeEnum {
|
||||
NODE = 'NODE',
|
||||
CONNECTOR = 'CONNECTOR',
|
||||
GROUP = 'GROUP'
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
export class Coords {
|
||||
x: number = 0;
|
||||
|
||||
y: number = 0;
|
||||
|
||||
constructor(x: number, y: number) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
set(x: number, y: number) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
setX(x: number) {
|
||||
this.x = x;
|
||||
}
|
||||
|
||||
setY(y: number) {
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
isEqual(comparator: Coords) {
|
||||
return this.x === comparator.x && this.y === comparator.y;
|
||||
}
|
||||
|
||||
subtract(operand: Coords) {
|
||||
return new Coords(this.x - operand.x, this.y - operand.y);
|
||||
}
|
||||
|
||||
subtractX(operand: number) {
|
||||
return new Coords(this.x - operand, this.y);
|
||||
}
|
||||
|
||||
subtractY(operand: number) {
|
||||
return new Coords(this.x, this.y - operand);
|
||||
}
|
||||
|
||||
add(operand: Coords) {
|
||||
return new Coords(this.x + operand.x, this.y + operand.y);
|
||||
}
|
||||
|
||||
addX(operand: number) {
|
||||
return new Coords(this.x + operand, this.y);
|
||||
}
|
||||
|
||||
addY(operand: number) {
|
||||
return new Coords(this.x, this.y + operand);
|
||||
}
|
||||
|
||||
clone() {
|
||||
return new Coords(this.x, this.y);
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `x: ${this.x}, y: ${this.y}`;
|
||||
}
|
||||
|
||||
toObject() {
|
||||
return { x: this.x, y: this.y };
|
||||
}
|
||||
|
||||
static fromObject({ x, y }: { x: number; y: number }) {
|
||||
return new Coords(x, y);
|
||||
}
|
||||
|
||||
static zero() {
|
||||
return new Coords(0, 0);
|
||||
}
|
||||
}
|
||||
27
src/utils/CoordsUtils.ts
Normal file
27
src/utils/CoordsUtils.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { Coords } from 'src/types';
|
||||
|
||||
export class CoordsUtils {
|
||||
static isEqual(base: Coords, operand: Coords) {
|
||||
return base.x === operand.x && base.y === operand.y;
|
||||
}
|
||||
|
||||
static subtract(base: Coords, operand: Coords): Coords {
|
||||
return { x: base.x - operand.x, y: base.y - operand.y };
|
||||
}
|
||||
|
||||
static add(base: Coords, operand: Coords): Coords {
|
||||
return { x: base.x + operand.x, y: base.y + operand.y };
|
||||
}
|
||||
|
||||
static multiply(base: Coords, operand: Coords): Coords {
|
||||
return { x: base.x * operand.x, y: base.y * operand.y };
|
||||
}
|
||||
|
||||
static toString(coords: Coords) {
|
||||
return `x: ${coords.x}, y: ${coords.y}`;
|
||||
}
|
||||
|
||||
static zero() {
|
||||
return { x: 0, y: 0 };
|
||||
}
|
||||
}
|
||||
223
src/utils/common.ts
Normal file
223
src/utils/common.ts
Normal file
@@ -0,0 +1,223 @@
|
||||
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';
|
||||
|
||||
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 {
|
||||
id: node.id,
|
||||
position: node.position,
|
||||
label: node.label,
|
||||
labelHeight: node.labelHeight,
|
||||
color: node.color,
|
||||
iconId: node.iconId
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
nodes,
|
||||
connectors: [],
|
||||
groups: [],
|
||||
icons: scene.icons
|
||||
} as SceneInput;
|
||||
};
|
||||
|
||||
interface GetColorVariantOpts {
|
||||
alpha?: number;
|
||||
grade?: number;
|
||||
}
|
||||
|
||||
export const getColorVariant = (
|
||||
color: string,
|
||||
variant: 'light' | 'dark',
|
||||
{ alpha = 1, grade = 0 }: GetColorVariantOpts
|
||||
) => {
|
||||
switch (variant) {
|
||||
case 'light':
|
||||
return chroma(color)
|
||||
.brighten(grade ?? 1)
|
||||
.alpha(alpha)
|
||||
.css();
|
||||
case 'dark':
|
||||
return chroma(color)
|
||||
.darken(grade ?? 1)
|
||||
.alpha(alpha)
|
||||
.css();
|
||||
default:
|
||||
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,167 +1,3 @@
|
||||
import gsap from 'gsap';
|
||||
import { Coords } from 'src/utils/Coords';
|
||||
import { customVars } from 'src/styles/theme';
|
||||
import chroma from 'chroma-js';
|
||||
import type {
|
||||
SceneInput,
|
||||
NodeInput,
|
||||
ConnectorInput,
|
||||
GroupInput
|
||||
} from 'src/validation/SceneInput';
|
||||
import {
|
||||
SceneItemTypeEnum,
|
||||
Scene,
|
||||
Node,
|
||||
Connector,
|
||||
Group
|
||||
} from 'src/stores/useSceneStore';
|
||||
import { NODE_DEFAULTS, GRID_DEFAULTS } from 'src/utils/config';
|
||||
|
||||
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 new Coords(
|
||||
coords.x === 0 ? 0.000001 : coords.x,
|
||||
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: Coords.fromObject(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,
|
||||
gridSize: sceneInput.gridSize
|
||||
? new Coords(sceneInput.gridSize.width, sceneInput.gridSize.height)
|
||||
: Coords.fromObject(GRID_DEFAULTS.size)
|
||||
} as Scene;
|
||||
};
|
||||
|
||||
export const sceneToSceneInput = (scene: Scene): SceneInput => {
|
||||
const nodes: SceneInput['nodes'] = scene.nodes.map((node) => {
|
||||
return {
|
||||
id: node.id,
|
||||
position: node.position.toObject(),
|
||||
label: node.label,
|
||||
labelHeight: node.labelHeight,
|
||||
color: node.color,
|
||||
iconId: node.iconId
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
nodes,
|
||||
connectors: [],
|
||||
groups: [],
|
||||
icons: scene.icons,
|
||||
gridSize: { width: scene.gridSize.x, height: scene.gridSize.y }
|
||||
} as SceneInput;
|
||||
};
|
||||
|
||||
interface GetColorVariantOpts {
|
||||
alpha?: number;
|
||||
grade?: number;
|
||||
}
|
||||
|
||||
export const getColorVariant = (
|
||||
color: string,
|
||||
variant: 'light' | 'dark',
|
||||
{ alpha = 1, grade = 0 }: GetColorVariantOpts
|
||||
) => {
|
||||
switch (variant) {
|
||||
case 'light':
|
||||
return chroma(color)
|
||||
.brighten(grade ?? 1)
|
||||
.alpha(alpha)
|
||||
.css();
|
||||
case 'dark':
|
||||
return chroma(color)
|
||||
.darken(grade ?? 1)
|
||||
.alpha(alpha)
|
||||
.css();
|
||||
default:
|
||||
return chroma(color).alpha(alpha).css();
|
||||
}
|
||||
};
|
||||
export * from './CoordsUtils';
|
||||
export * from './common';
|
||||
export * from './config';
|
||||
|
||||
@@ -1,141 +0,0 @@
|
||||
// TODO: Split into individual files
|
||||
import { z } from 'zod';
|
||||
|
||||
export const iconInput = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
url: z.string(),
|
||||
category: z.string().optional()
|
||||
});
|
||||
|
||||
export const nodeInput = z.object({
|
||||
id: z.string(),
|
||||
label: z.string().optional(),
|
||||
labelHeight: z.number().optional(),
|
||||
color: z.string().optional(),
|
||||
iconId: z.string(),
|
||||
position: z.object({
|
||||
x: z.number(),
|
||||
y: z.number()
|
||||
})
|
||||
});
|
||||
|
||||
export const connectorInput = z.object({
|
||||
id: z.string(),
|
||||
label: z.string().nullable(),
|
||||
color: z.string().optional(),
|
||||
from: z.string(),
|
||||
to: z.string()
|
||||
});
|
||||
|
||||
export const groupInput = z.object({
|
||||
id: z.string(),
|
||||
label: z.string().nullable(),
|
||||
nodeIds: z.array(z.string())
|
||||
});
|
||||
|
||||
export const gridSizeInput = z
|
||||
.object({
|
||||
width: z.number(),
|
||||
height: z.number()
|
||||
})
|
||||
.optional();
|
||||
|
||||
export type IconInput = z.infer<typeof iconInput>;
|
||||
export type NodeInput = z.infer<typeof nodeInput> & {
|
||||
labelElement?: React.ReactNode;
|
||||
};
|
||||
export type ConnectorInput = z.infer<typeof connectorInput>;
|
||||
export type GroupInput = z.infer<typeof groupInput>;
|
||||
export type GridSizeInput = z.infer<typeof gridSizeInput>;
|
||||
|
||||
export const findInvalidNode = (nodes: NodeInput[], icons: IconInput[]) => {
|
||||
return nodes.find((node) => {
|
||||
const validIcon = icons.find((icon) => {
|
||||
return node.iconId === icon.id;
|
||||
});
|
||||
return !validIcon;
|
||||
});
|
||||
};
|
||||
|
||||
export const findInvalidConnector = (
|
||||
connectors: ConnectorInput[],
|
||||
nodes: NodeInput[]
|
||||
) => {
|
||||
return connectors.find((con) => {
|
||||
const fromNode = nodes.find((node) => {
|
||||
return con.from === node.id;
|
||||
});
|
||||
const toNode = nodes.find((node) => {
|
||||
return con.to === node.id;
|
||||
});
|
||||
|
||||
return Boolean(!fromNode || !toNode);
|
||||
});
|
||||
};
|
||||
|
||||
export const findInvalidGroup = (groups: GroupInput[], nodes: NodeInput[]) => {
|
||||
return groups.find((grp) => {
|
||||
return grp.nodeIds.find((nodeId) => {
|
||||
const validNode = nodes.find((node) => {
|
||||
return node.id === nodeId;
|
||||
});
|
||||
return Boolean(!validNode);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const sceneInput = z
|
||||
.object({
|
||||
icons: z.array(iconInput),
|
||||
nodes: z.array(nodeInput),
|
||||
connectors: z.array(connectorInput),
|
||||
groups: z.array(groupInput),
|
||||
gridSize: gridSizeInput
|
||||
})
|
||||
.superRefine((scene, ctx) => {
|
||||
const invalidNode = findInvalidNode(scene.nodes, scene.icons);
|
||||
|
||||
if (invalidNode) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ['nodes', invalidNode.id],
|
||||
message: 'Invalid node found in scene'
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const invalidConnector = findInvalidConnector(
|
||||
scene.connectors,
|
||||
scene.nodes
|
||||
);
|
||||
|
||||
if (invalidConnector) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ['connectors', invalidConnector.id],
|
||||
message: 'Invalid connector found in scene'
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const invalidGroup = findInvalidGroup(scene.groups, scene.nodes);
|
||||
|
||||
if (invalidGroup) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ['groups', invalidGroup.id],
|
||||
message: 'Invalid group found in scene'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export type SceneInput = {
|
||||
icons: IconInput[];
|
||||
nodes: NodeInput[];
|
||||
connectors: ConnectorInput[];
|
||||
groups: GroupInput[];
|
||||
gridSize?: GridSizeInput;
|
||||
};
|
||||
54
src/validation/scene.ts
Normal file
54
src/validation/scene.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
// TODO: Split into individual files
|
||||
import { z } from 'zod';
|
||||
import { iconInput, nodeInput, connectorInput, groupInput } from './sceneItems';
|
||||
import {
|
||||
findInvalidConnector,
|
||||
findInvalidGroup,
|
||||
findInvalidNode
|
||||
} from './utils';
|
||||
|
||||
export const sceneInput = z
|
||||
.object({
|
||||
icons: z.array(iconInput),
|
||||
nodes: z.array(nodeInput),
|
||||
connectors: z.array(connectorInput),
|
||||
groups: z.array(groupInput)
|
||||
})
|
||||
.superRefine((scene, ctx) => {
|
||||
const invalidNode = findInvalidNode(scene.nodes, scene.icons);
|
||||
|
||||
if (invalidNode) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ['nodes', invalidNode.id],
|
||||
message: 'Invalid node found in scene'
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const invalidConnector = findInvalidConnector(
|
||||
scene.connectors,
|
||||
scene.nodes
|
||||
);
|
||||
|
||||
if (invalidConnector) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ['connectors', invalidConnector.id],
|
||||
message: 'Invalid connector found in scene'
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const invalidGroup = findInvalidGroup(scene.groups, scene.nodes);
|
||||
|
||||
if (invalidGroup) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ['groups', invalidGroup.id],
|
||||
message: 'Invalid group found in scene'
|
||||
});
|
||||
}
|
||||
});
|
||||
34
src/validation/sceneItems.ts
Normal file
34
src/validation/sceneItems.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import z from 'zod';
|
||||
|
||||
export const iconInput = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
url: z.string(),
|
||||
category: z.string().optional()
|
||||
});
|
||||
|
||||
export const nodeInput = z.object({
|
||||
id: z.string(),
|
||||
label: z.string().optional(),
|
||||
labelHeight: z.number().optional(),
|
||||
color: z.string().optional(),
|
||||
iconId: z.string(),
|
||||
position: z.object({
|
||||
x: z.number(),
|
||||
y: z.number()
|
||||
})
|
||||
});
|
||||
|
||||
export const connectorInput = z.object({
|
||||
id: z.string(),
|
||||
label: z.string().nullable(),
|
||||
color: z.string().optional(),
|
||||
from: z.string(),
|
||||
to: z.string()
|
||||
});
|
||||
|
||||
export const groupInput = z.object({
|
||||
id: z.string(),
|
||||
label: z.string().nullable(),
|
||||
nodeIds: z.array(z.string())
|
||||
});
|
||||
@@ -1,12 +1,10 @@
|
||||
import { NodeInput, ConnectorInput, GroupInput } from 'src/types/inputs';
|
||||
import { sceneInput } from '../scene';
|
||||
import {
|
||||
sceneInput,
|
||||
NodeInput,
|
||||
ConnectorInput,
|
||||
GroupInput,
|
||||
findInvalidNode,
|
||||
findInvalidConnector,
|
||||
findInvalidGroup
|
||||
} from '../SceneInput';
|
||||
} from '../utils';
|
||||
import { scene } from '../../tests/fixtures/scene';
|
||||
|
||||
describe('scene validation works correctly', () => {
|
||||
|
||||
42
src/validation/utils.ts
Normal file
42
src/validation/utils.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import {
|
||||
ConnectorInput,
|
||||
NodeInput,
|
||||
GroupInput,
|
||||
IconInput
|
||||
} from 'src/types/inputs';
|
||||
|
||||
export const findInvalidNode = (nodes: NodeInput[], icons: IconInput[]) => {
|
||||
return nodes.find((node) => {
|
||||
const validIcon = icons.find((icon) => {
|
||||
return node.iconId === icon.id;
|
||||
});
|
||||
return !validIcon;
|
||||
});
|
||||
};
|
||||
|
||||
export const findInvalidConnector = (
|
||||
connectors: ConnectorInput[],
|
||||
nodes: NodeInput[]
|
||||
) => {
|
||||
return connectors.find((con) => {
|
||||
const fromNode = nodes.find((node) => {
|
||||
return con.from === node.id;
|
||||
});
|
||||
const toNode = nodes.find((node) => {
|
||||
return con.to === node.id;
|
||||
});
|
||||
|
||||
return Boolean(!fromNode || !toNode);
|
||||
});
|
||||
};
|
||||
|
||||
export const findInvalidGroup = (groups: GroupInput[], nodes: NodeInput[]) => {
|
||||
return groups.find((grp) => {
|
||||
return grp.nodeIds.find((nodeId) => {
|
||||
const validNode = nodes.find((node) => {
|
||||
return node.id === nodeId;
|
||||
});
|
||||
return Boolean(!validNode);
|
||||
});
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user