mirror of
https://github.com/stan-smith/FossFLOW.git
synced 2025-12-24 06:58:48 -05:00
feat: allows layer order of rectangles to be changed
This commit is contained in:
38
src/components/ContextMenu/ContextMenu.tsx
Normal file
38
src/components/ContextMenu/ContextMenu.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import React from 'react';
|
||||
import { Menu, MenuItem } from '@mui/material';
|
||||
import { Coords } from 'src/types';
|
||||
|
||||
interface MenuItemI {
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
position: Coords;
|
||||
anchorEl?: HTMLElement;
|
||||
menuItems: MenuItemI[];
|
||||
}
|
||||
|
||||
export const ContextMenu = ({
|
||||
onClose,
|
||||
position,
|
||||
anchorEl,
|
||||
menuItems
|
||||
}: Props) => {
|
||||
return (
|
||||
<Menu
|
||||
open
|
||||
anchorEl={anchorEl}
|
||||
style={{
|
||||
left: position.x,
|
||||
top: position.y
|
||||
}}
|
||||
onClose={onClose}
|
||||
>
|
||||
{menuItems.map((item) => {
|
||||
return <MenuItem onClick={item.onClick}>{item.label}</MenuItem>;
|
||||
})}
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
66
src/components/ContextMenu/ContextMenuManager.tsx
Normal file
66
src/components/ContextMenu/ContextMenuManager.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useUiStateStore } from 'src/stores/uiStateStore';
|
||||
import { getTilePosition } from 'src/utils';
|
||||
import { useScene } from 'src/hooks/useScene';
|
||||
import { ContextMenu } from './ContextMenu';
|
||||
|
||||
interface Props {
|
||||
anchorEl?: HTMLElement;
|
||||
}
|
||||
|
||||
export const ContextMenuManager = ({ anchorEl }: Props) => {
|
||||
const scene = useScene();
|
||||
const contextMenu = useUiStateStore((state) => {
|
||||
return state.contextMenu;
|
||||
});
|
||||
|
||||
const uiStateActions = useUiStateStore((state) => {
|
||||
return state.actions;
|
||||
});
|
||||
|
||||
const onClose = useCallback(() => {
|
||||
uiStateActions.setContextMenu(null);
|
||||
}, [uiStateActions]);
|
||||
|
||||
if (!contextMenu) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ContextMenu
|
||||
anchorEl={anchorEl}
|
||||
onClose={onClose}
|
||||
position={getTilePosition({ tile: contextMenu.tile })}
|
||||
menuItems={[
|
||||
{
|
||||
label: 'Send backward',
|
||||
onClick: () => {
|
||||
scene.changeLayerOrder('SEND_BACKWARD', contextMenu.item);
|
||||
onClose();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Bring forward',
|
||||
onClick: () => {
|
||||
scene.changeLayerOrder('BRING_FORWARD', contextMenu.item);
|
||||
onClose();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Send to back',
|
||||
onClick: () => {
|
||||
scene.changeLayerOrder('SEND_TO_BACK', contextMenu.item);
|
||||
onClose();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Bring to front',
|
||||
onClick: () => {
|
||||
scene.changeLayerOrder('BRING_TO_FRONT', contextMenu.item);
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -19,10 +19,9 @@ export const DragAndDrop = ({ iconId, tile }: Props) => {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
left: tilePosition.x,
|
||||
top: tilePosition.y
|
||||
position: 'absolute'
|
||||
}}
|
||||
style={{ left: tilePosition.x, top: tilePosition.y }}
|
||||
>
|
||||
{iconComponent}
|
||||
</Box>
|
||||
|
||||
@@ -9,7 +9,7 @@ interface Props {
|
||||
export const Connectors = ({ connectors }: Props) => {
|
||||
return (
|
||||
<>
|
||||
{connectors.map((connector) => {
|
||||
{[...connectors].reverse().map((connector) => {
|
||||
return <Connector key={connector.id} connector={connector} />;
|
||||
})}
|
||||
</>
|
||||
|
||||
@@ -37,14 +37,14 @@ export const Node = ({ node, order }: Props) => {
|
||||
|
||||
return (
|
||||
<Box
|
||||
style={{
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
zIndex: order
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{ position: 'absolute' }}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: position.x,
|
||||
top: position.y
|
||||
}}
|
||||
|
||||
@@ -9,7 +9,7 @@ interface Props {
|
||||
export const Nodes = ({ nodes }: Props) => {
|
||||
return (
|
||||
<>
|
||||
{nodes.map((node) => {
|
||||
{[...nodes].reverse().map((node) => {
|
||||
return (
|
||||
<Node key={node.id} order={-node.tile.x - node.tile.y} node={node} />
|
||||
);
|
||||
|
||||
@@ -9,7 +9,7 @@ interface Props {
|
||||
export const Rectangles = ({ rectangles }: Props) => {
|
||||
return (
|
||||
<>
|
||||
{rectangles.map((rectangle) => {
|
||||
{[...rectangles].reverse().map((rectangle) => {
|
||||
return <Rectangle key={rectangle.id} {...rectangle} />;
|
||||
})}
|
||||
</>
|
||||
|
||||
@@ -9,7 +9,7 @@ interface Props {
|
||||
export const TextBoxes = ({ textBoxes }: Props) => {
|
||||
return (
|
||||
<>
|
||||
{textBoxes.map((textBox) => {
|
||||
{[...textBoxes].reverse().map((textBox) => {
|
||||
return <TextBox key={textBox.id} textBox={textBox} />;
|
||||
})}
|
||||
</>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import React, { useCallback, useMemo, useRef } from 'react';
|
||||
import { Box, useTheme, Typography } from '@mui/material';
|
||||
import { EditorModeEnum } from 'src/types';
|
||||
import { UiElement } from 'components/UiElement/UiElement';
|
||||
@@ -12,6 +12,7 @@ import { ZoomControls } from 'src/components/ZoomControls/ZoomControls';
|
||||
import { useModelStore } from 'src/stores/modelStore';
|
||||
import { DebugUtils } from 'src/components/DebugUtils/DebugUtils';
|
||||
import { useResizeObserver } from 'src/hooks/useResizeObserver';
|
||||
import { ContextMenuManager } from 'src/components/ContextMenu/ContextMenuManager';
|
||||
import { ExportImageDialog } from '../ExportImageDialog/ExportImageDialog';
|
||||
|
||||
const ToolsEnum = {
|
||||
@@ -46,6 +47,7 @@ const getEditorModeMapping = (editorMode: keyof typeof EditorModeEnum) => {
|
||||
|
||||
export const UiOverlay = () => {
|
||||
const theme = useTheme();
|
||||
const contextMenuAnchorRef = useRef();
|
||||
const { appPadding } = theme.customVars;
|
||||
const spacing = useCallback(
|
||||
(multiplier: number) => {
|
||||
@@ -199,6 +201,7 @@ export const UiOverlay = () => {
|
||||
</UiElement>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{mode.type === 'PLACE_ICON' && mode.id && (
|
||||
<SceneLayer>
|
||||
<DragAndDrop iconId={mode.id} tile={mouse.position.tile} />
|
||||
@@ -212,6 +215,11 @@ export const UiOverlay = () => {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<SceneLayer>
|
||||
<Box ref={contextMenuAnchorRef} />
|
||||
<ContextMenuManager anchorEl={contextMenuAnchorRef.current} />
|
||||
</SceneLayer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { ModelItem, ViewItem, Connector, TextBox, Rectangle } from 'src/types';
|
||||
import {
|
||||
ModelItem,
|
||||
ViewItem,
|
||||
Connector,
|
||||
TextBox,
|
||||
Rectangle,
|
||||
ItemReference,
|
||||
LayerOrderingAction
|
||||
} from 'src/types';
|
||||
import { useUiStateStore } from 'src/stores/uiStateStore';
|
||||
import { useModelStore } from 'src/stores/modelStore';
|
||||
import { useSceneStore } from 'src/stores/sceneStore';
|
||||
@@ -226,6 +234,19 @@ export const useScene = () => {
|
||||
[getState, setState, currentViewId]
|
||||
);
|
||||
|
||||
const changeLayerOrder = useCallback(
|
||||
(action: LayerOrderingAction, item: ItemReference) => {
|
||||
const newState = reducers.changeLayerOrder(
|
||||
action,
|
||||
item,
|
||||
currentViewId,
|
||||
getState()
|
||||
);
|
||||
setState(newState);
|
||||
},
|
||||
[getState, setState, currentViewId]
|
||||
);
|
||||
|
||||
return {
|
||||
items,
|
||||
connectors,
|
||||
@@ -246,6 +267,7 @@ export const useScene = () => {
|
||||
deleteTextBox,
|
||||
createRectangle,
|
||||
updateRectangle,
|
||||
deleteRectangle
|
||||
deleteRectangle,
|
||||
changeLayerOrder
|
||||
};
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useRef } from 'react';
|
||||
import { useModelStore } from 'src/stores/modelStore';
|
||||
import { useUiStateStore } from 'src/stores/uiStateStore';
|
||||
import { ModeActions, State, SlimMouseEvent } from 'src/types';
|
||||
import { getMouse } from 'src/utils';
|
||||
import { getMouse, getItemAtTile } from 'src/utils';
|
||||
import { useResizeObserver } from 'src/hooks/useResizeObserver';
|
||||
import { useScene } from 'src/hooks/useScene';
|
||||
import { Cursor } from './modes/Cursor';
|
||||
@@ -100,6 +100,27 @@ export const useInteractionManager = () => {
|
||||
[model, scene, uiState, rendererSize]
|
||||
);
|
||||
|
||||
const onContextMenu = useCallback(
|
||||
(e: SlimMouseEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
const itemAtTile = getItemAtTile({
|
||||
tile: uiState.mouse.position.tile,
|
||||
scene
|
||||
});
|
||||
|
||||
if (itemAtTile?.type === 'RECTANGLE') {
|
||||
uiState.actions.setContextMenu({
|
||||
item: itemAtTile,
|
||||
tile: uiState.mouse.position.tile
|
||||
});
|
||||
} else if (uiState.contextMenu) {
|
||||
uiState.actions.setContextMenu(null);
|
||||
}
|
||||
},
|
||||
[uiState.mouse, scene, uiState.contextMenu, uiState.actions]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (uiState.mode.type === 'INTERACTIONS_DISABLED') return;
|
||||
|
||||
@@ -135,6 +156,7 @@ export const useInteractionManager = () => {
|
||||
el.addEventListener('mousemove', onMouseEvent);
|
||||
el.addEventListener('mousedown', onMouseEvent);
|
||||
el.addEventListener('mouseup', onMouseEvent);
|
||||
el.addEventListener('contextmenu', onContextMenu);
|
||||
el.addEventListener('touchstart', onTouchStart);
|
||||
el.addEventListener('touchmove', onTouchMove);
|
||||
el.addEventListener('touchend', onTouchEnd);
|
||||
@@ -143,11 +165,12 @@ export const useInteractionManager = () => {
|
||||
el.removeEventListener('mousemove', onMouseEvent);
|
||||
el.removeEventListener('mousedown', onMouseEvent);
|
||||
el.removeEventListener('mouseup', onMouseEvent);
|
||||
el.removeEventListener('contextmenu', onContextMenu);
|
||||
el.removeEventListener('touchstart', onTouchStart);
|
||||
el.removeEventListener('touchmove', onTouchMove);
|
||||
el.removeEventListener('touchend', onTouchEnd);
|
||||
};
|
||||
}, [uiState.editorMode, onMouseEvent, uiState.mode.type]);
|
||||
}, [uiState.editorMode, onMouseEvent, uiState.mode.type, onContextMenu]);
|
||||
|
||||
const setInteractionsElement = useCallback((element: HTMLElement) => {
|
||||
rendererRef.current = element;
|
||||
|
||||
@@ -87,7 +87,7 @@ export const createConnector = (
|
||||
if (!connectors) {
|
||||
draft.model.views[view.index].connectors = [newConnector];
|
||||
} else {
|
||||
draft.model.views[view.index].connectors?.push(newConnector);
|
||||
draft.model.views[view.index].connectors?.unshift(newConnector);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -4,3 +4,4 @@ export * from './viewItem';
|
||||
export * from './rectangle';
|
||||
export * from './textBox';
|
||||
export * from './view';
|
||||
export * from './layerOrdering';
|
||||
|
||||
42
src/stores/reducers/layerOrdering.ts
Normal file
42
src/stores/reducers/layerOrdering.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { produce } from 'immer';
|
||||
import { ItemReference, LayerOrderingAction, View } from 'src/types';
|
||||
import { getItemByIdOrThrow } from 'src/utils';
|
||||
import { State } from './types';
|
||||
|
||||
export const changeLayerOrder = (
|
||||
action: LayerOrderingAction,
|
||||
item: ItemReference,
|
||||
viewId: string,
|
||||
state: State
|
||||
): State => {
|
||||
const newState = produce(state, (draft) => {
|
||||
const view = getItemByIdOrThrow(draft.model.views, viewId);
|
||||
let arr: View['rectangles'];
|
||||
|
||||
switch (item.type) {
|
||||
case 'RECTANGLE':
|
||||
arr = view.value.rectangles ?? [];
|
||||
break;
|
||||
default:
|
||||
throw new Error('Invalid item type');
|
||||
}
|
||||
|
||||
const target = getItemByIdOrThrow(arr, item.id);
|
||||
|
||||
if (action === 'SEND_BACKWARD' && target.index < arr.length - 1) {
|
||||
arr.splice(target.index, 1);
|
||||
arr.splice(target.index + 1, 0, target.value);
|
||||
} else if (action === 'SEND_TO_BACK' && target.index !== arr.length - 1) {
|
||||
arr.splice(target.index, 1);
|
||||
arr.splice(arr.length, 0, target.value);
|
||||
} else if (action === 'BRING_FORWARD' && target.index > 0) {
|
||||
arr.splice(target.index, 1);
|
||||
arr.splice(target.index - 1, 0, target.value);
|
||||
} else if (action === 'BRING_TO_FRONT' && target.index !== 0) {
|
||||
arr.splice(target.index, 1);
|
||||
arr.splice(0, 0, target.value);
|
||||
}
|
||||
});
|
||||
|
||||
return newState;
|
||||
};
|
||||
@@ -37,7 +37,7 @@ export const createRectangle = (
|
||||
if (!rectangles) {
|
||||
draft.model.views[view.index].rectangles = [newRectangle];
|
||||
} else {
|
||||
draft.model.views[view.index].rectangles?.push(newRectangle);
|
||||
draft.model.views[view.index].rectangles?.unshift(newRectangle);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ export const createTextBox = (
|
||||
if (!textBoxes) {
|
||||
draft.model.views[view.index].textBoxes = [newTextBox];
|
||||
} else {
|
||||
draft.model.views[view.index].textBoxes?.push(newTextBox);
|
||||
draft.model.views[view.index].textBoxes?.unshift(newTextBox);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ export const createViewItem = (
|
||||
|
||||
const newState = produce(state, (draft) => {
|
||||
const { items } = draft.model.views[view.index];
|
||||
items.push(newViewItem);
|
||||
items.unshift(newViewItem);
|
||||
});
|
||||
|
||||
return updateViewItem(newViewItem.id, newViewItem, viewId, newState);
|
||||
|
||||
@@ -23,6 +23,7 @@ const initialState = () => {
|
||||
isMainMenuOpen: false,
|
||||
dialog: null,
|
||||
rendererEl: null,
|
||||
contextMenu: null,
|
||||
mouse: {
|
||||
position: { screen: CoordsUtils.zero(), tile: CoordsUtils.zero() },
|
||||
mousedown: null,
|
||||
@@ -88,6 +89,9 @@ const initialState = () => {
|
||||
setItemControls: (itemControls) => {
|
||||
set({ itemControls });
|
||||
},
|
||||
setContextMenu: (contextMenu) => {
|
||||
set({ contextMenu });
|
||||
},
|
||||
setMouse: (mouse) => {
|
||||
set({ mouse });
|
||||
},
|
||||
|
||||
@@ -22,7 +22,7 @@ export type BoundingBox = [Coords, Coords, Coords, Coords];
|
||||
|
||||
export type SlimMouseEvent = Pick<
|
||||
MouseEvent,
|
||||
'clientX' | 'clientY' | 'target' | 'type'
|
||||
'clientX' | 'clientY' | 'target' | 'type' | 'preventDefault'
|
||||
>;
|
||||
|
||||
export const EditorModeEnum = {
|
||||
|
||||
@@ -6,7 +6,7 @@ interface AddItemControls {
|
||||
type: 'ADD_ITEM';
|
||||
}
|
||||
|
||||
export type ItemControls = ItemReference | AddItemControls | null;
|
||||
export type ItemControls = ItemReference | AddItemControls;
|
||||
|
||||
export interface Mouse {
|
||||
position: {
|
||||
@@ -117,6 +117,20 @@ export const DialogTypeEnum = {
|
||||
EXPORT_IMAGE: 'EXPORT_IMAGE'
|
||||
} as const;
|
||||
|
||||
export interface ContextMenu {
|
||||
item: ItemReference;
|
||||
tile: Coords;
|
||||
}
|
||||
|
||||
export const LayerOrderingActionOptions = {
|
||||
BRING_TO_FRONT: 'BRING_TO_FRONT',
|
||||
SEND_TO_BACK: 'SEND_TO_BACK',
|
||||
BRING_FORWARD: 'BRING_FORWARD',
|
||||
SEND_BACKWARD: 'SEND_BACKWARD'
|
||||
} as const;
|
||||
|
||||
export type LayerOrderingAction = keyof typeof LayerOrderingActionOptions;
|
||||
|
||||
export interface UiState {
|
||||
view: string;
|
||||
mainMenuOptions: MainMenuOptions;
|
||||
@@ -125,7 +139,8 @@ export interface UiState {
|
||||
mode: Mode;
|
||||
dialog: keyof typeof DialogTypeEnum | null;
|
||||
isMainMenuOpen: boolean;
|
||||
itemControls: ItemControls;
|
||||
itemControls: ItemControls | null;
|
||||
contextMenu: ContextMenu | null;
|
||||
zoom: number;
|
||||
scroll: Scroll;
|
||||
mouse: Mouse;
|
||||
@@ -147,7 +162,8 @@ export interface UiStateActions {
|
||||
setZoom: (zoom: number) => void;
|
||||
setScroll: (scroll: Scroll) => void;
|
||||
scrollToTile: (tile: Coords, origin?: TileOrigin) => void;
|
||||
setItemControls: (itemControls: ItemControls) => void;
|
||||
setItemControls: (itemControls: ItemControls | null) => void;
|
||||
setContextMenu: (contextMenu: ContextMenu | null) => void;
|
||||
setMouse: (mouse: Mouse) => void;
|
||||
setRendererEl: (el: HTMLDivElement) => void;
|
||||
setenableDebugTools: (enabled: boolean) => void;
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
ItemReference,
|
||||
Rect,
|
||||
ProjectionOrientationEnum,
|
||||
AnchorPositionOptions,
|
||||
BoundingBox,
|
||||
TextBox,
|
||||
SlimMouseEvent,
|
||||
|
||||
Reference in New Issue
Block a user