feat: implements connector controls

This commit is contained in:
Mark Mankarious
2023-08-18 16:41:04 +01:00
parent 73d9b522ec
commit 198a1f4e2d
8 changed files with 148 additions and 30 deletions

View File

@@ -0,0 +1,57 @@
import React, { useCallback } from 'react';
import { Connector } from 'src/types';
import { useTheme, Box } from '@mui/material';
import { useSceneStore } from 'src/stores/sceneStore';
import { useConnector } from 'src/hooks/useConnector';
import { ColorSelector } from 'src/components/ColorSelector/ColorSelector';
import { useUiStateStore } from 'src/stores/uiStateStore';
import { ControlsContainer } from '../components/ControlsContainer';
import { Section } from '../components/Section';
import { Header } from '../components/Header';
import { DeleteButton } from '../components/DeleteButton';
interface Props {
id: string;
}
export const ConnectorControls = ({ id }: Props) => {
const theme = useTheme();
const sceneActions = useSceneStore((state) => {
return state.actions;
});
const uiStateActions = useUiStateStore((state) => {
return state.actions;
});
const rectangle = useConnector(id);
const onConnectorUpdated = useCallback(
(updates: Partial<Connector>) => {
sceneActions.updateConnector(id, updates);
},
[sceneActions, id]
);
const onConnectorDeleted = useCallback(() => {
uiStateActions.setItemControls(null);
sceneActions.deleteConnector(id);
}, [sceneActions, id, uiStateActions]);
return (
<ControlsContainer header={<Header title="Connector settings" />}>
<Section>
<ColorSelector
colors={Object.values(theme.customVars.diagramPalette)}
onChange={(color) => {
return onConnectorUpdated({ color });
}}
activeColor={rectangle.color}
/>
</Section>
<Section>
<Box>
<DeleteButton onClick={onConnectorDeleted} />
</Box>
</Section>
</ControlsContainer>
);
};

View File

@@ -3,6 +3,7 @@ import { Card, useTheme } from '@mui/material';
import { useUiStateStore } from 'src/stores/uiStateStore';
import { IconSelection } from 'src/components/ItemControls/IconSelection/IconSelection';
import { NodeControls } from './NodeControls/NodeControls';
import { ConnectorControls } from './ConnectorControls/ConnectorControls';
import { RectangleControls } from './RectangleControls/RectangleControls';
export const ItemControlsManager = () => {
@@ -15,6 +16,8 @@ export const ItemControlsManager = () => {
switch (itemControls?.type) {
case 'NODE':
return <NodeControls key={itemControls.id} id={itemControls.id} />;
case 'CONNECTOR':
return <ConnectorControls key={itemControls.id} id={itemControls.id} />;
case 'RECTANGLE':
return <RectangleControls key={itemControls.id} id={itemControls.id} />;
case 'ADD_ITEM':

15
src/hooks/useConnector.ts Normal file
View File

@@ -0,0 +1,15 @@
import { useMemo } from 'react';
import { useSceneStore } from 'src/stores/sceneStore';
import { getItemById } from 'src/utils';
export const useConnector = (id: string) => {
const connectors = useSceneStore((state) => {
return state.connectors;
});
const node = useMemo(() => {
return getItemById(connectors, id).item;
}, [connectors, id]);
return node;
};

View File

@@ -49,6 +49,11 @@ export const Cursor: ModeActions = {
type: 'RECTANGLE',
id: uiState.mode.mousedownItem.id
});
} else if (uiState.mode.mousedownItem.type === 'CONNECTOR') {
uiState.actions.setItemControls({
type: 'CONNECTOR',
id: uiState.mode.mousedownItem.id
});
}
} else {
uiState.actions.setItemControls(null);

View File

@@ -120,6 +120,16 @@ const initialState = () => {
set({ rectangles: newScene.rectangles });
},
deleteConnector: (id: string) => {
const newScene = produce(get(), (draftState) => {
const { index } = getItemById(draftState.connectors, id);
draftState.connectors.splice(index, 1);
});
set({ connectors: newScene.connectors });
},
deleteRectangle: (id: string) => {
const newScene = produce(get(), (draftState) => {
const { index } = getItemById(draftState.rectangles, id);

View File

@@ -73,14 +73,15 @@ export type Icon = IconInput;
export interface SceneActions {
setScene: (scene: SceneInput) => void;
updateScene: (scene: Scene) => void;
updateNode: (id: string, updates: Partial<Node>) => void;
deleteNode: (id: string) => void;
updateConnector: (id: string, updates: Partial<Connector>) => void;
updateRectangle: (id: string, updates: Partial<Rectangle>) => void;
deleteRectangle: (id: string) => void;
createNode: (node: NodeInput) => void;
createConnector: (connector: ConnectorInput) => void;
createRectangle: (rectangle: RectangleInput) => void;
updateNode: (id: string, updates: Partial<Node>) => void;
updateConnector: (id: string, updates: Partial<Connector>) => void;
updateRectangle: (id: string, updates: Partial<Rectangle>) => void;
deleteNode: (id: string) => void;
deleteConnector: (id: string) => void;
deleteRectangle: (id: string) => void;
}
export type Scene = {

View File

@@ -7,6 +7,11 @@ interface NodeControls {
id: string;
}
interface ConnectorControls {
type: 'CONNECTOR';
id: string;
}
interface RectangleControls {
type: 'RECTANGLE';
id: string;
@@ -18,6 +23,7 @@ interface AddItemControls {
export type ItemControls =
| NodeControls
| ConnectorControls
| RectangleControls
| AddItemControls
| null;

View File

@@ -205,30 +205,6 @@ export const getTranslateCSS = (translate: Coords = { x: 0, y: 0 }) => {
return `translate(${translate.x}px, ${translate.y}px)`;
};
interface GetItemAtTile {
tile: Coords;
scene: Scene;
}
export const getItemAtTile = ({
tile,
scene
}: GetItemAtTile): SceneItem | null => {
const node = scene.nodes.find(({ position }) => {
return CoordsUtils.isEqual(position, tile);
});
if (node) return node;
const rectangle = scene.rectangles.find(({ from, to }) => {
return isWithinBounds(tile, [from, to]);
});
if (rectangle) return rectangle;
return null;
};
export const incrementZoom = (zoom: number) => {
const newZoom = clamp(zoom + ZOOM_INCREMENT, MIN_ZOOM, MAX_ZOOM);
return roundToOneDecimalPlace(newZoom);
@@ -380,9 +356,10 @@ export const getConnectorPath = ({
const positionsNormalisedFromSearchArea = anchorPositions.map((position) => {
return normalisePositionFromOrigin({ position, origin });
});
const tiles = positionsNormalisedFromSearchArea.reduce<Coords[]>(
(acc, position, i) => {
if (i === 0) return [position];
if (i === 0) return acc;
const prev = positionsNormalisedFromSearchArea[i - 1];
const path = findPath({
@@ -416,3 +393,47 @@ export const hasMovedTile = (mouse: Mouse) => {
return !CoordsUtils.isEqual(mouse.delta.tile, CoordsUtils.zero());
};
export const connectorPathTileToGlobal = (tile: Coords, origin: Coords) => {
return CoordsUtils.subtract(
CoordsUtils.subtract(origin, CONNECTOR_DEFAULTS.searchOffset),
CoordsUtils.subtract(tile, CONNECTOR_DEFAULTS.searchOffset)
);
};
interface GetItemAtTile {
tile: Coords;
scene: Scene;
}
export const getItemAtTile = ({
tile,
scene
}: GetItemAtTile): SceneItem | null => {
const node = scene.nodes.find(({ position }) => {
return CoordsUtils.isEqual(position, tile);
});
if (node) return node;
const rectangle = scene.rectangles.find(({ from, to }) => {
return isWithinBounds(tile, [from, to]);
});
if (rectangle) return rectangle;
const connector = scene.connectors.find((con) => {
return con.path.tiles.find((pathTile) => {
const globalPathTile = connectorPathTileToGlobal(
pathTile,
con.path.origin
);
return CoordsUtils.isEqual(globalPathTile, tile);
});
});
if (connector) return connector;
return null;
};