refactor: renames node.position to node.tile

This commit is contained in:
Mark Mankarious
2023-10-04 17:57:30 +01:00
parent 5d6f3d0aaf
commit 41e8de6cc7
25 changed files with 58 additions and 282 deletions

View File

@@ -18,22 +18,22 @@ The `initialScene` object contains the following properties:
name: string;
url: string;
collection?: string;
isIsometric?: boolean;
}
```
**Notes on icons:**
- `collection` is an optional property that can be used to group icons together in the icon picker. All icons with the same `collection` will be grouped together.
- For a list of standard icon `id`s, see [Isopacks](/docs/isopacks#icon-ids).
- `collection` is an optional property that can be used to group icons together in the icon picker. All icons with the same `collection` will be grouped together under a heading.
## `Node`
```js
{
id: string;
iconId: string;
icon: string;
label?: string;
labelHeight?: number;
position: {
tile: {
x: number;
y: number;
};
@@ -100,29 +100,29 @@ Open this example in [CodeSandbox](https://codesandbox.io/p/sandbox/github/markm
nodes: [
{
id: 'database',
iconId: 'storage',
icon: 'storage',
label: 'Database',
position: {
tile: {
x: 2,
y: -2
}
},
{
id: 'server',
iconId: 'server',
icon: 'server',
label: 'Server',
labelHeight: 100,
position: {
tile: {
x: 2,
y: 2
}
},
{
id: 'client',
iconId: 'laptop',
icon: 'laptop',
label: 'Client',
labelHeight: 100,
position: {
tile: {
x: -1,
y: 0
}
@@ -165,6 +165,6 @@ Open this example in [CodeSandbox](https://codesandbox.io/p/sandbox/github/markm
Examples of common errors are as follows:
- A `connector` references a `nodeId` that does not exist in the `nodes` array.
- A `node` references an `iconId` that does not exist in the `icons` array.
- A `node` references an `icon` that does not exist in the `icons` array.
- A `rectangle` has a `from` but not a `to` property.
- A `connector` has less than 2 anchors.

View File

@@ -2,14 +2,14 @@ import React from 'react';
import { Coords } from 'src/types';
interface Props {
position: Coords;
tile: Coords;
radius?: number;
}
export const Circle = ({
position,
tile,
radius,
...rest
}: Props & React.SVGProps<SVGCircleElement>) => {
return <circle cx={position.x} cy={position.y} r={radius} {...rest} />;
return <circle cx={tile.x} cy={tile.y} r={radius} {...rest} />;
};

View File

@@ -1,34 +0,0 @@
import React from 'react';
import { Box } from '@mui/material';
import { useUiStateStore } from 'src/stores/uiStateStore';
import { NodeContextMenu } from 'src/components/ContextMenu/NodeContextMenu';
import { EmptyTileContextMenu } from 'src/components/ContextMenu/EmptyTileContextMenu';
export const ContextMenuLayer = () => {
const contextMenu = useUiStateStore((state) => {
return state.contextMenu;
});
return (
<Box
sx={{
position: 'absolute',
top: 0,
left: 0,
width: 0,
height: 0
}}
>
{contextMenu?.type === 'NODE' && (
<NodeContextMenu key={contextMenu.id} nodeId={contextMenu.id} />
)}
{contextMenu?.type === 'EMPTY_TILE' && (
<EmptyTileContextMenu
key={contextMenu.position.toString()}
onAddNode={() => {}}
position={contextMenu.position}
/>
)}
</Box>
);
};

View File

@@ -1,22 +0,0 @@
import React from 'react';
import { Add as AddIcon } from '@mui/icons-material';
import { Coords } from 'src/types';
import { ContextMenu } from './components/ContextMenu';
import { ContextMenuItem } from './components/ContextMenuItem';
interface Props {
onAddNode: () => void;
position: Coords;
}
export const EmptyTileContextMenu = ({ onAddNode, position }: Props) => {
return (
<ContextMenu position={position}>
<ContextMenuItem
onClick={onAddNode}
icon={<AddIcon />}
label="Add node"
/>
</ContextMenu>
);
};

View File

@@ -1,33 +0,0 @@
import React from 'react';
import {
ArrowRightAlt as ConnectIcon,
Delete as DeleteIcon
} from '@mui/icons-material';
import { useNode } from 'src/hooks/useNode';
import { ContextMenu } from './components/ContextMenu';
import { ContextMenuItem } from './components/ContextMenuItem';
interface Props {
nodeId: string;
}
export const NodeContextMenu = ({ nodeId }: Props) => {
const node = useNode(nodeId);
if (!node) return null;
return (
<ContextMenu position={node.position}>
<ContextMenuItem
onClick={() => {}}
icon={<ConnectIcon />}
label="Connect"
/>
<ContextMenuItem
onClick={() => {}}
icon={<DeleteIcon />}
label="Remove"
/>
</ContextMenu>
);
};

View File

@@ -1,85 +0,0 @@
import React, { useRef, useEffect, useState } from 'react';
import { Coords } from 'src/types';
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;
position: Coords;
}
const COLOR = 'grey.900';
const ARROW = {
size: 11,
top: 8
};
export const ContextMenu = ({ position, children }: Props) => {
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;
// useEffect(() => {
// if (!container.current) return;
// const screenPosition = getTileScreenPosition({
// position,
// scrollPosition,
// zoom
// });
// 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
// });
// }
// 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>
// );
};

View File

@@ -1,50 +0,0 @@
import React, { useCallback } from 'react';
import ListItem from '@mui/material/ListItem';
import ListItemButton from '@mui/material/ListItemButton';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';
import { useUiStateStore } from 'src/stores/uiStateStore';
interface Props {
onClick: () => void;
icon: React.ReactNode;
label: string;
}
export const ContextMenuItem = ({ onClick, icon, label }: Props) => {
const uiStateActions = useUiStateStore((state) => state.actions);
const onClickProxy = useCallback(() => {
onClick();
uiStateActions.setContextMenu(null);
}, [onClick, uiStateActions]);
return (
<ListItem
sx={{
p: 0,
'&:not(:last-child)': {
borderBottom: '1px solid',
borderBottomColor: 'grey.800'
}
}}
>
<ListItemButton onClick={onClickProxy} sx={{ py: 0.5, px: 2 }}>
<ListItemIcon
sx={{
pr: 1,
minWidth: 'auto',
color: 'grey.400',
svg: {
maxWidth: 18,
maxHeight: 18
}
}}
>
{icon}
</ListItemIcon>
<ListItemText secondary={label} />
</ListItemButton>
</ListItem>
);
};

View File

@@ -65,7 +65,7 @@ export const NodeControls = ({ id }: Props) => {
key={node.id}
iconCategories={iconCategories}
onClick={(icon) => {
onNodeUpdated({ iconId: icon.id });
onNodeUpdated({ icon: icon.id });
}}
/>
)}

View File

@@ -123,13 +123,13 @@ export const Connector = ({ connector }: Props) => {
return (
<>
<Circle
position={CoordsUtils.add(anchor, drawOffset)}
tile={CoordsUtils.add(anchor, drawOffset)}
radius={18 * zoom}
fill={theme.palette.common.white}
fillOpacity={0.7}
/>
<Circle
position={CoordsUtils.add(anchor, drawOffset)}
tile={CoordsUtils.add(anchor, drawOffset)}
radius={12 * zoom}
stroke={theme.palette.common.black}
fill={theme.palette.common.white}

View File

@@ -15,14 +15,14 @@ interface Props {
export const Node = ({ node, order }: Props) => {
const { projectedTileSize } = useTileSize();
const { getTilePosition } = useGetTilePosition();
const { iconComponent } = useIcon(node.iconId);
const { iconComponent } = useIcon(node.icon);
const position = useMemo(() => {
return getTilePosition({
tile: node.position,
tile: node.tile,
origin: TileOriginEnum.BOTTOM
});
}, [node.position, getTilePosition]);
}, [node.tile, getTilePosition]);
const label = useMemo(() => {
if (node.label === undefined || node.label === '<p><br></p>') return null;

View File

@@ -13,7 +13,7 @@ export const Nodes = () => {
return (
<Node
key={node.id}
order={-node.position.x - node.position.y}
order={-node.tile.x - node.tile.y}
node={node}
/>
);

View File

@@ -9,5 +9,5 @@ interface Props {
export const NodeTransformControls = ({ id }: Props) => {
const node = useNode(id);
return <TransformControls from={node.position} to={node.position} />;
return <TransformControls from={node.tile} to={node.tile} />;
};

View File

@@ -8,9 +8,9 @@ const initialScene: InitialScene = {
nodes: [
{
id: 'server',
label: '<p>This is an example of tracking changes to the scene.</p>',
iconId: 'server',
position: {
label: '<p>Changes to the scene are tracked bottom left.</p>',
icon: 'server',
tile: {
x: 0,
y: 0
}

View File

@@ -20,29 +20,29 @@ export const initialScene: InitialScene = {
nodes: [
{
id: 'database',
iconId: 'storage',
icon: 'storage',
label: 'Database',
position: {
tile: {
x: 2,
y: -2
}
},
{
id: 'server',
iconId: 'server',
icon: 'server',
label: 'Server',
labelHeight: 100,
position: {
tile: {
x: 2,
y: 2
}
},
{
id: 'client',
iconId: 'laptop',
icon: 'laptop',
label: 'Client',
labelHeight: 100,
position: {
tile: {
x: -1,
y: 0
}

View File

@@ -39,7 +39,7 @@ export const useDiagramUtils = () => {
const positions = items.reduce<Coords[]>((acc, item) => {
switch (item.type) {
case 'NODE':
return [...acc, item.position];
return [...acc, item.tile];
case 'CONNECTOR':
return [
...acc,

View File

@@ -12,7 +12,7 @@ const dragItems = (
const node = getItemById(scene.nodes, item.id).item;
scene.actions.updateNode(item.id, {
position: CoordsUtils.add(node.position, delta)
tile: CoordsUtils.add(node.tile, delta)
});
} else if (item.type === 'RECTANGLE') {
const rectangle = getItemById(scene.rectangles, item.id).item;

View File

@@ -19,7 +19,7 @@
// if (!uiState.mode.isDragging) {
// const { mousedown } = uiState.mouse;
// const items = scene.nodes.filter((node) => {
// return CoordsUtils.isEqual(node.position, mousedown.tile);
// return CoordsUtils.isEqual(node.tile, mousedown.tile);
// });
// // User is creating a selection

View File

@@ -28,8 +28,8 @@ export const PlaceElement: ModeActions = {
if (uiState.mode.icon !== null) {
scene.actions.createNode({
id: generateId(),
iconId: uiState.mode.icon.id,
position: uiState.mouse.position.tile
icon: uiState.mode.icon.id,
tile: uiState.mouse.position.tile
});
}

View File

@@ -17,8 +17,8 @@ export const scene: SceneInput = {
{
id: 'node1',
label: 'Node1',
iconId: 'icon1',
position: {
icon: 'icon1',
tile: {
x: 0,
y: 0
}
@@ -26,8 +26,8 @@ export const scene: SceneInput = {
{
id: 'node2',
label: 'Node2',
iconId: 'icon2',
position: {
icon: 'icon2',
tile: {
x: 1,
y: 1
}
@@ -35,8 +35,8 @@ export const scene: SceneInput = {
{
id: 'node3',
label: 'Node3',
iconId: 'icon1',
position: {
icon: 'icon1',
tile: {
x: 2,
y: 2
}

View File

@@ -27,10 +27,10 @@ export enum SceneItemTypeEnum {
export interface Node {
id: string;
type: SceneItemTypeEnum.NODE;
iconId: string;
icon: string;
label: string;
labelHeight: number;
position: Coords;
tile: Coords;
isSelected: boolean;
}

View File

@@ -41,8 +41,8 @@ export const nodeInputToNode = (nodeInput: NodeInput): Node => {
id: nodeInput.id,
label: nodeInput.label ?? NODE_DEFAULTS.label,
labelHeight: nodeInput.labelHeight ?? NODE_DEFAULTS.labelHeight,
iconId: nodeInput.iconId,
position: nodeInput.position,
icon: nodeInput.icon,
tile: nodeInput.tile,
isSelected: false
};
};
@@ -201,10 +201,10 @@ export const iconToIconInput = (icon: Icon): IconInput => {
export const nodeToNodeInput = (node: Node): NodeInput => {
return {
id: node.id,
position: node.position,
tile: node.tile,
label: node.label,
labelHeight: node.labelHeight,
iconId: node.iconId
icon: node.icon
};
};

View File

@@ -347,7 +347,7 @@ export const getAnchorPosition = ({
}: GetAnchorPositions): Coords => {
if (anchor.ref.type === 'NODE') {
const { item: node } = getItemById(nodes, anchor.ref.id);
return node.position;
return node.tile;
}
if (anchor.ref.type === 'ANCHOR') {
@@ -481,8 +481,8 @@ export const getItemAtTile = ({
tile,
scene
}: GetItemAtTile): SceneItem | null => {
const node = scene.nodes.find(({ position }) => {
return CoordsUtils.isEqual(position, tile);
const node = scene.nodes.find((n) => {
return CoordsUtils.isEqual(n.tile, tile);
});
if (node) return node;

View File

@@ -18,8 +18,8 @@ export const nodeInput = z.object({
id: z.string(),
label: z.string().optional(),
labelHeight: z.number().optional(),
iconId: z.string(),
position: coords
icon: z.string(),
tile: coords
});
export const connectorAnchorInput = z

View File

@@ -13,11 +13,11 @@ describe('scene validation works correctly', () => {
);
});
test('node with invalid iconId fails validation', () => {
test('node with invalid icon fails validation', () => {
const invalidNode = {
id: 'invalidNode',
iconId: 'doesntExist',
position: { x: -1, y: -1 }
icon: 'doesntExist',
tile: { x: -1, y: -1 }
};
const scene = produce(sceneFixture, (draft) => {
draft.nodes.push(invalidNode);

View File

@@ -9,10 +9,10 @@ import { getAllAnchorsFromInput, getItemById } from 'src/utils';
export const ensureValidNode = (node: NodeInput, scene: SceneInput) => {
try {
getItemById(scene.icons, node.iconId);
getItemById(scene.icons, node.icon);
} catch (e) {
throw new Error(
`Found invalid node [id: "${node.id}"] referencing an invalid icon [id: "${node.iconId}"]`
`Found invalid node [id: "${node.id}"] referencing an invalid icon [id: "${node.icon}"]`
);
}
};