mirror of
https://github.com/stan-smith/FossFLOW.git
synced 2025-12-24 06:58:48 -05:00
feat: implements rectangle tool basics
This commit is contained in:
@@ -1,11 +1,14 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Box, useTheme } from '@mui/material';
|
||||
import { useTheme } from '@mui/material';
|
||||
import { Connector as ConnectorI } from 'src/types';
|
||||
import { UNPROJECTED_TILE_SIZE } from 'src/config';
|
||||
import { getAnchorPosition } from 'src/utils';
|
||||
import {
|
||||
getAnchorPosition,
|
||||
getRectangleFromSize,
|
||||
CoordsUtils
|
||||
} from 'src/utils';
|
||||
import { IsoTileArea } from 'src/components/IsoTileArea/IsoTileArea';
|
||||
import { Circle } from 'src/components/Circle/Circle';
|
||||
import { useGetTilePosition } from 'src/hooks/useGetTilePosition';
|
||||
import { useUiStateStore } from 'src/stores/uiStateStore';
|
||||
import { useSceneStore } from 'src/stores/sceneStore';
|
||||
|
||||
@@ -21,23 +24,27 @@ export const Connector = ({ connector }: Props) => {
|
||||
const nodes = useSceneStore((state) => {
|
||||
return state.nodes;
|
||||
});
|
||||
const { getTilePosition } = useGetTilePosition();
|
||||
|
||||
const unprojectedTileSize = useMemo(() => {
|
||||
return UNPROJECTED_TILE_SIZE * zoom;
|
||||
}, [zoom]);
|
||||
|
||||
const drawOffset = useMemo(() => {
|
||||
return {
|
||||
x: unprojectedTileSize / 2,
|
||||
y: unprojectedTileSize / 2
|
||||
};
|
||||
}, [unprojectedTileSize]);
|
||||
|
||||
const pathString = useMemo(() => {
|
||||
const unprojectedTileSize = UNPROJECTED_TILE_SIZE * zoom;
|
||||
return connector.path.tiles.reduce((acc, tile) => {
|
||||
return `${acc} ${tile.x * unprojectedTileSize},${
|
||||
tile.y * unprojectedTileSize
|
||||
return `${acc} ${tile.x * unprojectedTileSize + drawOffset.x},${
|
||||
tile.y * unprojectedTileSize + drawOffset.y
|
||||
}`;
|
||||
}, '');
|
||||
}, [zoom, connector.path.tiles]);
|
||||
|
||||
const pathOrigin = useMemo(() => {
|
||||
return getTilePosition({ tile: connector.path.origin });
|
||||
}, [getTilePosition, connector.path.origin]);
|
||||
}, [unprojectedTileSize, connector.path.tiles, drawOffset]);
|
||||
|
||||
const anchorPositions = useMemo(() => {
|
||||
const unprojectedTileSize = UNPROJECTED_TILE_SIZE * zoom;
|
||||
|
||||
return connector.anchors.map((anchor) => {
|
||||
const position = getAnchorPosition({ anchor, nodes });
|
||||
|
||||
@@ -46,36 +53,32 @@ export const Connector = ({ connector }: Props) => {
|
||||
y: (connector.path.origin.y - position.y) * unprojectedTileSize
|
||||
};
|
||||
});
|
||||
}, [connector.path.origin, connector.anchors, zoom, nodes]);
|
||||
}, [connector.path.origin, connector.anchors, nodes, unprojectedTileSize]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
id="connector"
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
left: pathOrigin.x,
|
||||
top: pathOrigin.y
|
||||
}}
|
||||
<IsoTileArea
|
||||
{...getRectangleFromSize(connector.path.origin, connector.path.areaSize)}
|
||||
origin={connector.path.origin}
|
||||
zoom={zoom}
|
||||
fill="none"
|
||||
>
|
||||
<IsoTileArea tileArea={connector.path.areaSize} zoom={zoom} fill="none">
|
||||
<polyline
|
||||
points={pathString}
|
||||
stroke={connector.color}
|
||||
strokeWidth={10 * zoom}
|
||||
fill="none"
|
||||
/>
|
||||
{anchorPositions.map((anchor) => {
|
||||
return (
|
||||
<Circle
|
||||
position={anchor}
|
||||
radius={10 * zoom}
|
||||
stroke={theme.palette.common.black}
|
||||
fill={theme.palette.common.white}
|
||||
strokeWidth={4 * zoom}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</IsoTileArea>
|
||||
</Box>
|
||||
<polyline
|
||||
points={pathString}
|
||||
stroke={connector.color}
|
||||
strokeWidth={10 * zoom}
|
||||
fill="none"
|
||||
/>
|
||||
{anchorPositions.map((anchor) => {
|
||||
return (
|
||||
<Circle
|
||||
position={CoordsUtils.add(anchor, drawOffset)}
|
||||
radius={10 * zoom}
|
||||
stroke={theme.palette.common.black}
|
||||
fill={theme.palette.common.white}
|
||||
strokeWidth={4 * zoom}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</IsoTileArea>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import React, { useRef, useMemo } from 'react';
|
||||
import { Box, useTheme } from '@mui/material';
|
||||
import { Coords, TileOriginEnum } from 'src/types';
|
||||
import React from 'react';
|
||||
import { useTheme } from '@mui/material';
|
||||
import { Coords } from 'src/types';
|
||||
import { IsoTileArea } from 'src/components/IsoTileArea/IsoTileArea';
|
||||
import { useUiStateStore } from 'src/stores/uiStateStore';
|
||||
import { useGetTilePosition } from 'src/hooks/useGetTilePosition';
|
||||
|
||||
interface Props {
|
||||
tile: Coords;
|
||||
@@ -14,28 +13,14 @@ export const Cursor = ({ tile }: Props) => {
|
||||
return state.zoom;
|
||||
});
|
||||
const theme = useTheme();
|
||||
const containerRef = useRef<HTMLDivElement>();
|
||||
const { getTilePosition } = useGetTilePosition();
|
||||
|
||||
const position = useMemo(() => {
|
||||
return getTilePosition({ tile, origin: TileOriginEnum.TOP });
|
||||
}, [getTilePosition, tile]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={containerRef}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
left: position.x,
|
||||
top: position.y
|
||||
}}
|
||||
>
|
||||
<IsoTileArea
|
||||
fill={theme.palette.primary.main}
|
||||
tileArea={{ width: 1, height: 1 }}
|
||||
zoom={zoom}
|
||||
cornerRadius={10 * zoom}
|
||||
/>
|
||||
</Box>
|
||||
<IsoTileArea
|
||||
from={tile}
|
||||
to={tile}
|
||||
fill={theme.palette.primary.main}
|
||||
zoom={zoom}
|
||||
cornerRadius={10 * zoom}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,60 +1,31 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import React from 'react';
|
||||
import chroma from 'chroma-js';
|
||||
import { Box } from '@mui/material';
|
||||
import { Node, TileOriginEnum, Group as GroupI } from 'src/types';
|
||||
import { getBoundingBox, getBoundingBoxSize } from 'src/utils';
|
||||
import { Coords } from 'src/types';
|
||||
import { IsoTileArea } from 'src/components/IsoTileArea/IsoTileArea';
|
||||
import { useGetTilePosition } from 'src/hooks/useGetTilePosition';
|
||||
import { useUiStateStore } from 'src/stores/uiStateStore';
|
||||
|
||||
interface Props {
|
||||
nodes: Node[];
|
||||
group: GroupI;
|
||||
from: Coords;
|
||||
to: Coords;
|
||||
color: string;
|
||||
}
|
||||
|
||||
export const Group = ({ nodes, group }: Props) => {
|
||||
export const Group = ({ from, to, color }: Props) => {
|
||||
const zoom = useUiStateStore((state) => {
|
||||
return state.zoom;
|
||||
});
|
||||
const { getTilePosition } = useGetTilePosition();
|
||||
const nodePositions = useMemo(() => {
|
||||
return nodes.map((node) => {
|
||||
return node.position;
|
||||
});
|
||||
}, [nodes]);
|
||||
|
||||
const groupAttrs = useMemo(() => {
|
||||
const corners = getBoundingBox(nodePositions, { x: 1, y: 1 });
|
||||
const size = getBoundingBoxSize(corners);
|
||||
|
||||
const position = getTilePosition({
|
||||
tile: corners[2],
|
||||
origin: TileOriginEnum.TOP
|
||||
});
|
||||
|
||||
return { size, position };
|
||||
}, [nodePositions, getTilePosition]);
|
||||
|
||||
if (!groupAttrs) return null;
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
left: groupAttrs.position.x,
|
||||
top: groupAttrs.position.y
|
||||
<IsoTileArea
|
||||
from={from}
|
||||
to={to}
|
||||
fill={chroma(color).alpha(0.6).css()}
|
||||
zoom={zoom}
|
||||
cornerRadius={22 * zoom}
|
||||
stroke={{
|
||||
color,
|
||||
width: 1 * zoom
|
||||
}}
|
||||
>
|
||||
<IsoTileArea
|
||||
tileArea={groupAttrs.size}
|
||||
fill={chroma(group.color).alpha(0.6).css()}
|
||||
zoom={zoom}
|
||||
cornerRadius={22 * zoom}
|
||||
stroke={{
|
||||
color: group.color,
|
||||
width: 1 * zoom
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Box } from '@mui/material';
|
||||
import { UNPROJECTED_TILE_SIZE } from 'src/config';
|
||||
import { Size, Coords } from 'src/types';
|
||||
import { getIsoMatrixCSS, getProjectedTileSize } from 'src/utils';
|
||||
import { Size, Coords, TileOriginEnum } from 'src/types';
|
||||
import {
|
||||
getIsoMatrixCSS,
|
||||
getProjectedTileSize,
|
||||
getBoundingBox
|
||||
} from 'src/utils';
|
||||
import { Svg } from 'src/components/Svg/Svg';
|
||||
import { useGetTilePosition } from 'src/hooks/useGetTilePosition';
|
||||
|
||||
interface Props {
|
||||
tileArea: Size;
|
||||
from: Coords;
|
||||
to: Coords;
|
||||
origin?: Coords;
|
||||
fill: string;
|
||||
cornerRadius?: number;
|
||||
stroke?: {
|
||||
@@ -18,29 +25,52 @@ interface Props {
|
||||
}
|
||||
|
||||
export const IsoTileArea = ({
|
||||
tileArea,
|
||||
from,
|
||||
to,
|
||||
origin: _origin,
|
||||
fill,
|
||||
cornerRadius = 0,
|
||||
stroke,
|
||||
zoom,
|
||||
children
|
||||
}: Props) => {
|
||||
const { getTilePosition } = useGetTilePosition();
|
||||
const projectedTileSize = useMemo(() => {
|
||||
return getProjectedTileSize({ zoom });
|
||||
}, [zoom]);
|
||||
|
||||
const size = useMemo(() => {
|
||||
return {
|
||||
width: Math.abs(from.x - to.x) + 1,
|
||||
height: Math.abs(from.y - to.y) + 1
|
||||
};
|
||||
}, [from, to]);
|
||||
|
||||
const origin = useMemo(() => {
|
||||
if (_origin) return _origin;
|
||||
|
||||
const boundingBox = getBoundingBox([from, to]);
|
||||
|
||||
return boundingBox[2];
|
||||
}, [from, to, _origin]);
|
||||
|
||||
const position = useMemo(() => {
|
||||
return getTilePosition({
|
||||
tile: origin,
|
||||
origin: TileOriginEnum.TOP
|
||||
});
|
||||
}, [origin, getTilePosition]);
|
||||
|
||||
const viewbox = useMemo<Size>(() => {
|
||||
return {
|
||||
width:
|
||||
(tileArea.width / 2 + tileArea.height / 2) * projectedTileSize.width,
|
||||
height:
|
||||
(tileArea.width / 2 + tileArea.height / 2) * projectedTileSize.height
|
||||
width: (size.width / 2 + size.height / 2) * projectedTileSize.width,
|
||||
height: (size.width / 2 + size.height / 2) * projectedTileSize.height
|
||||
};
|
||||
}, [tileArea, projectedTileSize]);
|
||||
}, [size, projectedTileSize]);
|
||||
|
||||
const translate = useMemo<Coords>(() => {
|
||||
return { x: tileArea.width * (projectedTileSize.width / 2), y: 0 };
|
||||
}, [tileArea, projectedTileSize]);
|
||||
return { x: size.width * (projectedTileSize.width / 2), y: 0 };
|
||||
}, [size, projectedTileSize]);
|
||||
|
||||
const strokeParams = useMemo(() => {
|
||||
if (!stroke) return {};
|
||||
@@ -52,14 +82,16 @@ export const IsoTileArea = ({
|
||||
}, [stroke]);
|
||||
|
||||
const marginLeft = useMemo(() => {
|
||||
return -(tileArea.width * projectedTileSize.width * 0.5);
|
||||
}, [projectedTileSize.width, tileArea.width]);
|
||||
return -(size.width * projectedTileSize.width * 0.5);
|
||||
}, [projectedTileSize.width, size.width]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
marginLeft: `${marginLeft}px`
|
||||
marginLeft: `${marginLeft}px`,
|
||||
left: position.x,
|
||||
top: position.y
|
||||
}}
|
||||
>
|
||||
<Svg
|
||||
@@ -70,8 +102,8 @@ export const IsoTileArea = ({
|
||||
<g transform={`translate(${translate.x}, ${translate.y})`}>
|
||||
<g transform={getIsoMatrixCSS()}>
|
||||
<rect
|
||||
width={tileArea.width * UNPROJECTED_TILE_SIZE * zoom}
|
||||
height={tileArea.height * UNPROJECTED_TILE_SIZE * zoom}
|
||||
width={size.width * UNPROJECTED_TILE_SIZE * zoom}
|
||||
height={size.height * UNPROJECTED_TILE_SIZE * zoom}
|
||||
fill={fill}
|
||||
rx={cornerRadius}
|
||||
{...strokeParams}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import React, { useEffect, useRef, useCallback } from 'react';
|
||||
import React, { useRef, useCallback, useMemo } from 'react';
|
||||
import { Box } from '@mui/material';
|
||||
import gsap from 'gsap';
|
||||
import { Coords, TileOriginEnum, Node as NodeI, IconInput } from 'src/types';
|
||||
import { getColorVariant } from 'src/utils';
|
||||
import { Node as NodeI, IconInput, TileOriginEnum } from 'src/types';
|
||||
import { getColorVariant, getRectangleFromSize } from 'src/utils';
|
||||
import { IsoTileArea } from 'src/components/IsoTileArea/IsoTileArea';
|
||||
import { useUiStateStore } from 'src/stores/uiStateStore';
|
||||
import { useGetTilePosition } from 'src/hooks/useGetTilePosition';
|
||||
import { useProjectedTileSize } from 'src/hooks/useProjectedTileSize';
|
||||
import { useGetTilePosition } from 'src/hooks/useGetTilePosition';
|
||||
import { LabelContainer } from './LabelContainer';
|
||||
import { MarkdownLabel } from './LabelTypes/MarkdownLabel';
|
||||
import { NodeIcon } from './NodeIcon';
|
||||
@@ -22,32 +21,8 @@ export const Node = ({ node, icon, order }: Props) => {
|
||||
return state.zoom;
|
||||
});
|
||||
const nodeRef = useRef<HTMLDivElement>();
|
||||
const { getTilePosition } = useGetTilePosition();
|
||||
const projectedTileSize = useProjectedTileSize();
|
||||
|
||||
const moveToTile = useCallback(
|
||||
({
|
||||
tile,
|
||||
animationDuration = 0.15
|
||||
}: {
|
||||
tile: Coords;
|
||||
animationDuration?: number;
|
||||
}) => {
|
||||
if (!nodeRef.current) return;
|
||||
|
||||
const position = getTilePosition({
|
||||
tile,
|
||||
origin: TileOriginEnum.BOTTOM
|
||||
});
|
||||
|
||||
gsap.to(nodeRef.current, {
|
||||
duration: animationDuration,
|
||||
x: position.x,
|
||||
y: position.y
|
||||
});
|
||||
},
|
||||
[getTilePosition]
|
||||
);
|
||||
const { getTilePosition } = useGetTilePosition();
|
||||
|
||||
const onImageLoaded = useCallback(() => {
|
||||
if (!nodeRef.current) return;
|
||||
@@ -55,61 +30,57 @@ export const Node = ({ node, icon, order }: Props) => {
|
||||
nodeRef.current.style.opacity = '1';
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
moveToTile({ tile: node.position, animationDuration: 0 });
|
||||
}, [node.position, moveToTile]);
|
||||
const position = useMemo(() => {
|
||||
return getTilePosition({
|
||||
tile: node.position,
|
||||
origin: TileOriginEnum.BOTTOM
|
||||
});
|
||||
}, [node.position, getTilePosition]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={nodeRef}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
zIndex: order,
|
||||
opacity: 0
|
||||
zIndex: order
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
ref={nodeRef}
|
||||
sx={{
|
||||
position: 'absolute'
|
||||
position: 'absolute',
|
||||
opacity: 0,
|
||||
left: position.x,
|
||||
top: position.y
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: -projectedTileSize.height
|
||||
}}
|
||||
>
|
||||
<IsoTileArea
|
||||
tileArea={{
|
||||
width: 1,
|
||||
height: 1
|
||||
}}
|
||||
fill={node.color}
|
||||
cornerRadius={15 * zoom}
|
||||
stroke={{
|
||||
width: 1 * zoom,
|
||||
color: getColorVariant(node.color, 'dark', { grade: 1.5 })
|
||||
}}
|
||||
zoom={zoom}
|
||||
/>
|
||||
</Box>
|
||||
<LabelContainer
|
||||
labelHeight={node.labelHeight + 100}
|
||||
tileSize={projectedTileSize}
|
||||
connectorDotSize={5 * zoom}
|
||||
>
|
||||
{node.label && <MarkdownLabel label={node.label} />}
|
||||
</LabelContainer>
|
||||
</Box>
|
||||
{icon && (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute'
|
||||
}}
|
||||
>
|
||||
<NodeIcon icon={icon} onImageLoaded={onImageLoaded} />
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: -projectedTileSize.height
|
||||
}}
|
||||
/>
|
||||
<LabelContainer
|
||||
labelHeight={node.labelHeight + 100}
|
||||
tileSize={projectedTileSize}
|
||||
connectorDotSize={5 * zoom}
|
||||
>
|
||||
{node.label && <MarkdownLabel label={node.label} />}
|
||||
</LabelContainer>
|
||||
</Box>
|
||||
)}
|
||||
{icon && (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute'
|
||||
}}
|
||||
>
|
||||
<NodeIcon icon={icon} onImageLoaded={onImageLoaded} />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { Box } from '@mui/material';
|
||||
import { Node as NodeI } from 'src/types';
|
||||
import { useUiStateStore } from 'src/stores/uiStateStore';
|
||||
import { useSceneStore } from 'src/stores/sceneStore';
|
||||
import { useInteractionManager } from 'src/interaction/useInteractionManager';
|
||||
@@ -12,6 +11,8 @@ import { Connector } from 'src/components/Connector/Connector';
|
||||
import { DebugUtils } from 'src/components/DebugUtils/DebugUtils';
|
||||
import { useResizeObserver } from 'src/hooks/useResizeObserver';
|
||||
import { SceneLayer } from 'src/components/SceneLayer/SceneLayer';
|
||||
import { IsoTileArea } from 'src/components/IsoTileArea/IsoTileArea';
|
||||
import { DEFAULT_COLOR } from 'src/config';
|
||||
|
||||
export const Renderer = () => {
|
||||
const containerRef = useRef<HTMLDivElement>();
|
||||
@@ -51,21 +52,6 @@ export const Renderer = () => {
|
||||
} = useInteractionManager();
|
||||
const { observe, disconnect, size: rendererSize } = useResizeObserver();
|
||||
|
||||
const getNodesFromIds = useCallback(
|
||||
(nodeIds: string[]) => {
|
||||
return nodeIds
|
||||
.map((nodeId) => {
|
||||
return nodes.find((node) => {
|
||||
return node.id === nodeId;
|
||||
});
|
||||
})
|
||||
.filter((node) => {
|
||||
return node !== undefined;
|
||||
}) as NodeI[];
|
||||
},
|
||||
[nodes]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
@@ -96,21 +82,25 @@ export const Renderer = () => {
|
||||
<Grid scroll={scroll} zoom={zoom} />
|
||||
<SceneLayer>
|
||||
{scene.groups.map((group) => {
|
||||
const groupNodes = getNodesFromIds(group.nodeIds);
|
||||
|
||||
return <Group key={group.id} group={group} nodes={groupNodes} />;
|
||||
return <Group key={group.id} {...group} />;
|
||||
})}
|
||||
</SceneLayer>
|
||||
<SceneLayer>
|
||||
{mode.showCursor && <Cursor tile={mouse.position.tile} />}
|
||||
</SceneLayer>
|
||||
<SceneLayer>
|
||||
{mode.type === 'AREA_TOOL' && mode.area && (
|
||||
<Group
|
||||
from={mode.area.from}
|
||||
to={mode.area.to}
|
||||
color={DEFAULT_COLOR}
|
||||
/>
|
||||
)}
|
||||
</SceneLayer>
|
||||
<SceneLayer>
|
||||
{scene.connectors.map((connector) => {
|
||||
return <Connector key={connector.id} connector={connector} />;
|
||||
})}
|
||||
{mode.type === 'CONNECTOR' && mode.connector && (
|
||||
<Connector connector={mode.connector} />
|
||||
)}
|
||||
</SceneLayer>
|
||||
<SceneLayer>
|
||||
{nodes.map((node) => {
|
||||
@@ -126,6 +116,11 @@ export const Renderer = () => {
|
||||
);
|
||||
})}
|
||||
</SceneLayer>
|
||||
<SceneLayer>
|
||||
{mode.type === 'CONNECTOR' && mode.connector && (
|
||||
<Connector connector={mode.connector} />
|
||||
)}
|
||||
</SceneLayer>
|
||||
{debugMode && (
|
||||
<SceneLayer>
|
||||
<DebugUtils />
|
||||
|
||||
@@ -8,7 +8,8 @@ import {
|
||||
NearMe as NearMeIcon,
|
||||
CenterFocusStrong as CenterFocusStrongIcon,
|
||||
Add as AddIcon,
|
||||
EastOutlined as ConnectorIcon
|
||||
EastOutlined as ConnectorIcon,
|
||||
CropSquare as CropSquareIcon
|
||||
} from '@mui/icons-material';
|
||||
import { useUiStateStore } from 'src/stores/uiStateStore';
|
||||
import { useDiagramUtils } from 'src/hooks/useDiagramUtils';
|
||||
@@ -53,6 +54,19 @@ export const ToolMenu = () => {
|
||||
}}
|
||||
size={theme.customVars.toolMenu.height}
|
||||
/>
|
||||
<IconButton
|
||||
name="Area"
|
||||
Icon={<CropSquareIcon />}
|
||||
onClick={() => {
|
||||
uiStateStoreActions.setMode({
|
||||
type: 'AREA_TOOL',
|
||||
showCursor: true,
|
||||
area: null
|
||||
});
|
||||
}}
|
||||
isActive={mode.type === 'AREA_TOOL'}
|
||||
size={theme.customVars.toolMenu.height}
|
||||
/>
|
||||
<IconButton
|
||||
name="Connector"
|
||||
Icon={<ConnectorIcon />}
|
||||
@@ -63,6 +77,7 @@ export const ToolMenu = () => {
|
||||
showCursor: true
|
||||
});
|
||||
}}
|
||||
isActive={mode.type === 'CONNECTOR'}
|
||||
size={theme.customVars.toolMenu.height}
|
||||
/>
|
||||
<IconButton
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import React from 'react';
|
||||
import Isoflow from 'src/Isoflow';
|
||||
import { icons } from '../icons';
|
||||
|
||||
export const BasicEditor = () => {
|
||||
return (
|
||||
<Isoflow
|
||||
initialScene={{
|
||||
icons,
|
||||
connectors: [],
|
||||
groups: [],
|
||||
nodes: []
|
||||
}}
|
||||
height="100%"
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,101 +0,0 @@
|
||||
import React from 'react';
|
||||
import Isoflow from 'src/Isoflow';
|
||||
import { Box, useTheme } from '@mui/material';
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import {
|
||||
AreaChart,
|
||||
Area,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
ResponsiveContainer
|
||||
} from 'recharts';
|
||||
import { icons } from '../icons';
|
||||
import graphData from './graphData';
|
||||
|
||||
const CustomLabel = () => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
width: '300px',
|
||||
height: '125px',
|
||||
pt: 2
|
||||
}}
|
||||
>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart
|
||||
data={graphData}
|
||||
margin={{
|
||||
top: 0,
|
||||
right: 0,
|
||||
left: 0,
|
||||
bottom: 0
|
||||
}}
|
||||
>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<YAxis width={25} />
|
||||
<Tooltip />
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="uv"
|
||||
fill={theme.customVars.diagramPalette.blue}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export const CustomNode = () => {
|
||||
return (
|
||||
<Isoflow
|
||||
initialScene={{
|
||||
icons,
|
||||
connectors: [
|
||||
{
|
||||
id: 'connector1',
|
||||
anchors: [
|
||||
{
|
||||
nodeId: 'server'
|
||||
},
|
||||
{
|
||||
nodeId: 'database'
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
groups: [
|
||||
{
|
||||
id: 'group1',
|
||||
nodeIds: ['server', 'database']
|
||||
}
|
||||
],
|
||||
nodes: [
|
||||
{
|
||||
id: 'server',
|
||||
label: 'Requests per minute',
|
||||
labelHeight: 40,
|
||||
iconId: 'server',
|
||||
position: {
|
||||
x: 0,
|
||||
y: 0
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'database',
|
||||
label: 'Transactions',
|
||||
labelHeight: 40,
|
||||
iconId: 'server',
|
||||
position: {
|
||||
x: 5,
|
||||
y: 3
|
||||
}
|
||||
}
|
||||
]
|
||||
}}
|
||||
height="100%"
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,23 +0,0 @@
|
||||
export default [
|
||||
{
|
||||
uv: 40
|
||||
},
|
||||
{
|
||||
uv: 30
|
||||
},
|
||||
{
|
||||
uv: 20
|
||||
},
|
||||
{
|
||||
uv: 27
|
||||
},
|
||||
{
|
||||
uv: 18
|
||||
},
|
||||
{
|
||||
uv: 23
|
||||
},
|
||||
{
|
||||
uv: 34
|
||||
}
|
||||
];
|
||||
@@ -11,7 +11,14 @@ export const DebugTools = () => {
|
||||
groups: [
|
||||
{
|
||||
id: 'group1',
|
||||
nodeIds: ['server', 'database']
|
||||
from: {
|
||||
x: 5,
|
||||
y: 5
|
||||
},
|
||||
to: {
|
||||
x: 0,
|
||||
y: 0
|
||||
}
|
||||
}
|
||||
],
|
||||
nodes: [
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { Box, Select, MenuItem, useTheme } from '@mui/material';
|
||||
import { BasicEditor } from './BasicEditor/BasicEditor';
|
||||
import { CustomNode } from './CustomNode/CustomNode';
|
||||
import { Callbacks } from './Callbacks/Callbacks';
|
||||
import { DebugTools } from './DebugTools/DebugTools';
|
||||
|
||||
const examples = [
|
||||
{ name: 'Debug tools', component: DebugTools },
|
||||
{ name: 'Live Diagrams', component: CustomNode },
|
||||
{ name: 'Callbacks', component: Callbacks },
|
||||
{ name: 'Basic Editor', component: BasicEditor }
|
||||
{ name: 'Callbacks', component: Callbacks }
|
||||
];
|
||||
|
||||
export const Examples = () => {
|
||||
|
||||
38
src/interaction/reducers/AreaTool.ts
Normal file
38
src/interaction/reducers/AreaTool.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { InteractionReducer } from 'src/types';
|
||||
import { generateId } from 'src/utils';
|
||||
import { DEFAULT_COLOR } from 'src/config';
|
||||
|
||||
export const AreaTool: InteractionReducer = {
|
||||
type: 'AREA_TOOL',
|
||||
mousemove: (draftState) => {
|
||||
if (
|
||||
draftState.mode.type !== 'AREA_TOOL' ||
|
||||
!draftState.mode.area ||
|
||||
!draftState.mouse.mousedown
|
||||
)
|
||||
return;
|
||||
|
||||
draftState.mode.area.to = draftState.mouse.position.tile;
|
||||
},
|
||||
mousedown: (draftState) => {
|
||||
if (draftState.mode.type !== 'AREA_TOOL') return;
|
||||
|
||||
draftState.mode.area = {
|
||||
from: draftState.mouse.position.tile,
|
||||
to: draftState.mouse.position.tile
|
||||
};
|
||||
},
|
||||
mouseup: (draftState) => {
|
||||
if (draftState.mode.type !== 'AREA_TOOL' || !draftState.mode.area) return;
|
||||
|
||||
const newGroups = draftState.sceneActions.createGroup({
|
||||
id: generateId(),
|
||||
color: DEFAULT_COLOR,
|
||||
from: draftState.mode.area.from,
|
||||
to: draftState.mode.area.to
|
||||
});
|
||||
|
||||
draftState.scene.groups = newGroups.groups;
|
||||
draftState.mode.area = null;
|
||||
}
|
||||
};
|
||||
@@ -10,6 +10,7 @@ import { Cursor } from './reducers/Cursor';
|
||||
import { Lasso } from './reducers/Lasso';
|
||||
import { PlaceElement } from './reducers/PlaceElement';
|
||||
import { Connector } from './reducers/Connector';
|
||||
import { AreaTool } from './reducers/AreaTool';
|
||||
|
||||
const reducers: { [k in string]: InteractionReducer } = {
|
||||
CURSOR: Cursor,
|
||||
@@ -17,7 +18,8 @@ const reducers: { [k in string]: InteractionReducer } = {
|
||||
PAN: Pan,
|
||||
LASSO: Lasso,
|
||||
PLACE_ELEMENT: PlaceElement,
|
||||
CONNECTOR: Connector
|
||||
CONNECTOR: Connector,
|
||||
AREA_TOOL: AreaTool
|
||||
};
|
||||
|
||||
export const useInteractionManager = () => {
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import React, { createContext, useRef, useContext } from 'react';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { createStore, useStore } from 'zustand';
|
||||
import { produce } from 'immer';
|
||||
import { Scene, SceneActions } from 'src/types';
|
||||
import { Scene, SceneActions, GroupInput } from 'src/types';
|
||||
import { sceneInput } from 'src/validation/scene';
|
||||
import { sceneInputtoScene, getItemById, getConnectorPath } from 'src/utils';
|
||||
import {
|
||||
sceneInputtoScene,
|
||||
getItemById,
|
||||
getConnectorPath,
|
||||
groupInputToGroup
|
||||
} from 'src/utils';
|
||||
|
||||
interface Actions {
|
||||
actions: SceneActions;
|
||||
@@ -51,6 +57,11 @@ const initialState = () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
createGroup: (group) => {
|
||||
return produce(get(), (draftState) => {
|
||||
draftState.groups.push(groupInputToGroup(group));
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -29,7 +29,7 @@ export const customVars: CustomThemeVars = {
|
||||
y: 40
|
||||
},
|
||||
toolMenu: {
|
||||
height: 50
|
||||
height: 40
|
||||
},
|
||||
diagramPalette: {
|
||||
green: '#53b435',
|
||||
|
||||
2
src/tests/fixtures/scene.ts
vendored
2
src/tests/fixtures/scene.ts
vendored
@@ -52,5 +52,5 @@ export const scene: SceneInput = {
|
||||
anchors: [{ nodeId: 'node2' }, { nodeId: 'node3' }]
|
||||
}
|
||||
],
|
||||
groups: [{ id: 'group1', nodeIds: ['node1', 'node2'] }]
|
||||
groups: [{ id: 'group1', from: { x: 0, y: 0 }, to: { x: 2, y: 2 } }]
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Coords, Size } from './common';
|
||||
import { IconInput, SceneInput } from './inputs';
|
||||
import { IconInput, SceneInput, GroupInput } from './inputs';
|
||||
|
||||
export enum TileOriginEnum {
|
||||
CENTER = 'CENTER',
|
||||
@@ -51,8 +51,9 @@ export interface Connector {
|
||||
export interface Group {
|
||||
type: SceneItemTypeEnum.GROUP;
|
||||
id: string;
|
||||
nodeIds: string[];
|
||||
color: string;
|
||||
from: Coords;
|
||||
to: Coords;
|
||||
}
|
||||
|
||||
export type SceneItem = Node | Connector | Group;
|
||||
@@ -63,6 +64,7 @@ export interface SceneActions {
|
||||
setScene: (scene: SceneInput) => void;
|
||||
updateScene: (scene: Scene) => void;
|
||||
updateNode: (id: string, updates: Partial<Node>, scene?: Scene) => Scene;
|
||||
createGroup: (group: GroupInput) => Scene;
|
||||
}
|
||||
|
||||
export type Scene = {
|
||||
|
||||
@@ -85,6 +85,15 @@ export interface ConnectorMode {
|
||||
connector: Connector | null;
|
||||
}
|
||||
|
||||
export interface AreaToolMode {
|
||||
type: 'AREA_TOOL';
|
||||
showCursor: boolean;
|
||||
area: {
|
||||
from: Coords;
|
||||
to: Coords;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export type Mode =
|
||||
| InteractionsDisabled
|
||||
| CursorMode
|
||||
@@ -92,7 +101,8 @@ export type Mode =
|
||||
| DragItemsMode
|
||||
| LassoMode
|
||||
| PlaceElementMode
|
||||
| ConnectorMode;
|
||||
| ConnectorMode
|
||||
| AreaToolMode;
|
||||
// End mode types
|
||||
|
||||
export type ContextMenu =
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import chroma from 'chroma-js';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
export const generateId = () => {
|
||||
return uuid();
|
||||
};
|
||||
|
||||
export const clamp = (num: number, min: number, max: number) => {
|
||||
return Math.max(Math.min(num, max), min);
|
||||
|
||||
@@ -32,7 +32,8 @@ export const groupInputToGroup = (groupInput: GroupInput): Group => {
|
||||
return {
|
||||
type: SceneItemTypeEnum.GROUP,
|
||||
id: groupInput.id,
|
||||
nodeIds: groupInput.nodeIds,
|
||||
from: groupInput.from,
|
||||
to: groupInput.to,
|
||||
color: groupInput.color ?? DEFAULT_COLOR
|
||||
};
|
||||
};
|
||||
@@ -145,8 +146,9 @@ export const connectorToConnectorInput = (
|
||||
export const groupToGroupInput = (group: Group): GroupInput => {
|
||||
return {
|
||||
id: group.id,
|
||||
nodeIds: group.nodeIds,
|
||||
color: group.color
|
||||
color: group.color,
|
||||
from: group.from,
|
||||
to: group.to
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -376,3 +376,15 @@ export const getConnectorPath = ({
|
||||
|
||||
return { tiles, origin, areaSize: searchAreaSize };
|
||||
};
|
||||
|
||||
type GetRectangleFromSize = (
|
||||
from: Coords,
|
||||
size: Size
|
||||
) => { from: Coords; to: Coords };
|
||||
|
||||
export const getRectangleFromSize: GetRectangleFromSize = (from, size) => {
|
||||
return {
|
||||
from,
|
||||
to: { x: from.x + size.width, y: from.y + size.height }
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
// TODO: Split into individual files
|
||||
import { z } from 'zod';
|
||||
import { iconInput, nodeInput, connectorInput, groupInput } from './sceneItems';
|
||||
import {
|
||||
findInvalidConnector,
|
||||
findInvalidGroup,
|
||||
findInvalidNode
|
||||
} from './utils';
|
||||
import { findInvalidConnector, findInvalidNode } from './utils';
|
||||
|
||||
export const sceneInput = z
|
||||
.object({
|
||||
@@ -38,17 +34,5 @@ export const sceneInput = z
|
||||
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'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -5,6 +5,11 @@ const coords = z.object({
|
||||
y: z.number()
|
||||
});
|
||||
|
||||
const size = z.object({
|
||||
width: z.number(),
|
||||
height: z.number()
|
||||
});
|
||||
|
||||
export const iconInput = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
@@ -46,5 +51,6 @@ export const connectorInput = z.object({
|
||||
export const groupInput = z.object({
|
||||
id: z.string(),
|
||||
color: z.string().optional(),
|
||||
nodeIds: z.array(z.string())
|
||||
from: coords,
|
||||
to: coords
|
||||
});
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { NodeInput, ConnectorInput, GroupInput } from 'src/types/inputs';
|
||||
import { NodeInput, ConnectorInput } from 'src/types/inputs';
|
||||
import { sceneInput } from '../scene';
|
||||
import {
|
||||
findInvalidNode,
|
||||
findInvalidConnector,
|
||||
findInvalidGroup
|
||||
} from '../utils';
|
||||
import { findInvalidNode, findInvalidConnector } from '../utils';
|
||||
import { scene } from '../../tests/fixtures/scene';
|
||||
|
||||
describe('scene validation works correctly', () => {
|
||||
@@ -47,17 +43,4 @@ describe('scene validation works correctly', () => {
|
||||
|
||||
expect(result).toEqual(invalidConnector);
|
||||
});
|
||||
|
||||
test('finds invalid groups in scene', () => {
|
||||
const { nodes } = scene;
|
||||
const invalidGroup: GroupInput = {
|
||||
id: 'invalidGroup',
|
||||
nodeIds: ['invalidNode', 'node1']
|
||||
};
|
||||
const groups: GroupInput[] = [...scene.groups, invalidGroup];
|
||||
|
||||
const result = findInvalidGroup(groups, nodes);
|
||||
|
||||
expect(result).toEqual(invalidGroup);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
import {
|
||||
ConnectorInput,
|
||||
NodeInput,
|
||||
GroupInput,
|
||||
IconInput
|
||||
} from 'src/types/inputs';
|
||||
import { ConnectorInput, NodeInput, IconInput } from 'src/types/inputs';
|
||||
|
||||
export const findInvalidNode = (nodes: NodeInput[], icons: IconInput[]) => {
|
||||
return nodes.find((node) => {
|
||||
@@ -35,14 +30,3 @@ export const findInvalidConnector = (
|
||||
return Boolean(invalidAnchor);
|
||||
});
|
||||
};
|
||||
|
||||
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