feat: implements lasso selection (UI disabled)

This commit is contained in:
Mark Mankarious
2023-07-25 10:12:19 +01:00
parent 37fd9ea16c
commit a76e5e7289
18 changed files with 548 additions and 214 deletions

View File

@@ -32,7 +32,9 @@ export const ToolMenu = () => {
<IconButton
name="Select"
Icon={<NearMeIcon />}
onClick={() => uiStateStoreActions.setMode({ type: 'CURSOR' })}
onClick={() =>
uiStateStoreActions.setMode({ type: 'CURSOR', mousedownItems: null })
}
size={theme.customVars.toolMenu.height}
isActive={mode.type === 'CURSOR'}
/>

View File

@@ -12,7 +12,7 @@ const DataLayer = () => {
const { updateNode } = useIsoflow();
const onSceneUpdated = useCallback((scene: SceneInput) => {
console.log(scene);
// console.log(scene);
}, []);
useEffect(() => {

View File

@@ -1,31 +1,90 @@
import { SidebarTypeEnum } from 'src/stores/useUiStateStore';
import { Coords } from 'src/utils/Coords';
import { InteractionReducer } from '../types';
import { getItemsByTile } from '../../renderer/utils/gridHelpers';
export const Cursor: InteractionReducer = {
mousemove: () => {},
mousedown: (draftState) => {
const itemsAtTile = getItemsByTile({
tile: draftState.mouse.tile,
sceneItems: draftState.scene
});
mousemove: (draftState) => {
if (draftState.mode.type !== 'CURSOR') return;
if (itemsAtTile.nodes.length > 0) {
draftState.mode = {
type: 'DRAG_ITEMS',
items: itemsAtTile,
hasMovedTile: false
};
} else {
draftState.scene.nodes = draftState.scene.nodes.map((node) => ({
...node,
isSelected: false
}));
draftState.contextMenu = {
type: 'EMPTY_TILE',
position: draftState.mouse.tile
};
draftState.itemControls = null;
if (
draftState.mouse.delta === null ||
!draftState.mouse.delta.tile.isEqual(Coords.fromObject({ x: 0, y: 0 }))
) {
// User has moved tile since the last mousedown event
if (
draftState.mode.mousedownItems &&
draftState.mode.mousedownItems.nodes.length > 0
) {
// User's last mousedown action was on a scene item
draftState.mode = {
type: 'DRAG_ITEMS',
items: draftState.mode.mousedownItems
};
}
// WIP: Lasso selection
// if (draftState.mode.mousedownItems?.nodes.length === 0) {
// // User's last mousedown action was on an empty tile
// draftState.mode = {
// type: 'LASSO',
// selection: {
// startTile: draftState.mouse.position.tile,
// endTile: draftState.mouse.position.tile
// },
// isDragging: false
// };
// }
}
},
mouseup: () => {}
mousedown: (draftState) => {
if (draftState.mode.type !== 'CURSOR') return;
const itemsAtTile = getItemsByTile({
tile: draftState.mouse.position.tile,
sortedSceneItems: draftState.scene
});
draftState.mode.mousedownItems = itemsAtTile;
},
mouseup: (draftState) => {
if (draftState.mode.type !== 'CURSOR') return;
draftState.scene.nodes = draftState.scene.nodes.map((node) => ({
...node,
isSelected: false
}));
if (draftState.mode.mousedownItems !== null) {
// User's last mousedown action was on a scene item
const mousedownNode = draftState.mode.mousedownItems.nodes[0];
if (mousedownNode) {
// The user's last mousedown action was on a node
const nodeIndex = draftState.scene.nodes.findIndex(
(node) => node.id === mousedownNode.id
);
if (nodeIndex === -1) return;
draftState.contextMenu = draftState.scene.nodes[nodeIndex];
draftState.scene.nodes[nodeIndex].isSelected = true;
draftState.itemControls = {
type: SidebarTypeEnum.SINGLE_NODE,
nodeId: draftState.scene.nodes[nodeIndex].id
};
draftState.mode.mousedownItems = null;
return;
}
// Empty tile selected
draftState.contextMenu = {
type: 'EMPTY_TILE',
position: draftState.mouse.position.tile
};
draftState.itemControls = null;
draftState.mode.mousedownItems = null;
}
}
};

View File

@@ -1,12 +1,15 @@
import { getItemsByTile } from 'src/renderer/utils/gridHelpers';
import { SidebarTypeEnum } from 'src/stores/useUiStateStore';
import { Coords } from 'src/utils/Coords';
import { InteractionReducer } from '../types';
export const DragItems: InteractionReducer = {
mousemove: (draftState, { prevMouse }) => {
mousemove: (draftState) => {
if (draftState.mode.type !== 'DRAG_ITEMS') return;
if (!prevMouse.tile.isEqual(draftState.mouse.tile)) {
if (
draftState.mouse.delta !== null &&
!draftState.mouse.delta.tile.isEqual(Coords.fromObject({ x: 0, y: 0 }))
) {
// 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
@@ -14,47 +17,14 @@ export const DragItems: InteractionReducer = {
if (nodeIndex === -1) return;
draftState.scene.nodes[nodeIndex].position = draftState.mouse.tile;
draftState.scene.nodes[nodeIndex].position =
draftState.mouse.position.tile;
draftState.contextMenu = null;
});
draftState.mode.hasMovedTile = true;
}
},
mousedown: () => {},
mouseup: (draftState) => {
if (draftState.mode.type !== 'DRAG_ITEMS') return;
if (!draftState.mode.hasMovedTile) {
// Set the item to a selected state if the item has been clicked in place,
// but not dragged
const itemsAtTile = getItemsByTile({
tile: draftState.mouse.tile,
sceneItems: draftState.scene
});
if (itemsAtTile.nodes.length > 0) {
const firstNode = itemsAtTile.nodes[0];
const nodeIndex = draftState.scene.nodes.findIndex(
(sceneNode) => sceneNode.id === firstNode.id
);
if (nodeIndex === -1) return;
draftState.scene.nodes = draftState.scene.nodes.map((node) => ({
...node,
isSelected: false
}));
draftState.scene.nodes[nodeIndex].isSelected = true;
draftState.contextMenu = draftState.scene.nodes[nodeIndex];
draftState.itemControls = {
type: SidebarTypeEnum.SINGLE_NODE,
nodeId: draftState.scene.nodes[nodeIndex].id
};
}
}
draftState.mode = { type: 'CURSOR' };
draftState.mode = { type: 'CURSOR', mousedownItems: null };
}
};

View File

@@ -0,0 +1,72 @@
import { Coords } from 'src/utils/Coords';
import { isWithinBounds } from 'src/renderer/utils/gridHelpers';
import { InteractionReducer } from '../types';
export const Lasso: InteractionReducer = {
mousemove: (draftState) => {
if (draftState.mode.type !== 'LASSO') return;
if (draftState.mouse.mousedown === null) return;
// User has moused down (they are in dragging mode)
if (
draftState.mouse.delta === null ||
draftState.mouse.delta.tile.isEqual(Coords.zero())
)
return;
// User has moved tile since the last mousedown event
if (!draftState.mode.isDragging) {
// User is creating the selection (not dragging)
draftState.mode.selection = {
startTile: draftState.mouse.mousedown.tile,
endTile: draftState.mouse.position.tile
};
return;
}
if (draftState.mode.isDragging) {
// User is dragging the selection
draftState.mode.selection = {
startTile: draftState.mode.selection.startTile.add(
draftState.mouse.delta.tile
),
endTile: draftState.mode.selection.endTile.add(
draftState.mouse.delta.tile
)
};
}
},
mousedown: (draftState) => {
if (draftState.mode.type !== 'LASSO') return;
if (draftState.mode.selection) {
const isWithinSelection = isWithinBounds(draftState.mouse.position.tile, [
draftState.mode.selection.startTile,
draftState.mode.selection.endTile
]);
if (!isWithinSelection) {
draftState.mode = {
type: 'CURSOR',
mousedownItems: null
};
return;
}
if (isWithinSelection) {
draftState.mode.isDragging = true;
return;
}
}
draftState.mode = {
type: 'CURSOR',
mousedownItems: null
};
},
mouseup: () => {}
};

View File

@@ -2,11 +2,13 @@ import { InteractionReducer } from '../types';
export const Pan: InteractionReducer = {
mousemove: (draftState) => {
if (draftState.mouse.mouseDownAt === null) return;
if (draftState.mode.type !== 'PAN') return;
draftState.scroll.position = draftState.mouse.delta
? draftState.scroll.position.add(draftState.mouse.delta)
: draftState.scroll.position;
if (draftState.mouse.mousedown !== null) {
draftState.scroll.position = draftState.mouse.delta?.screen
? draftState.scroll.position.add(draftState.mouse.delta.screen)
: draftState.scroll.position;
}
},
mousedown: () => {},
mouseup: () => {}

View File

@@ -1,7 +0,0 @@
import { InteractionReducer } from '../types';
export const Select: InteractionReducer = {
mousemove: () => {},
mousedown: () => {},
mouseup: () => {}
};

View File

@@ -1,28 +1,25 @@
import { Draft } from 'immer';
import {
Mouse,
Mode,
Scroll,
ContextMenu,
ItemControls
ItemControls,
Mouse
} from 'src/stores/useUiStateStore';
import { SceneItems } from 'src/stores/useSceneStore';
import { SortedSceneItems } from 'src/stores/useSceneStore';
import { Coords } from 'src/utils/Coords';
export interface State {
mouse: Mouse;
mode: Mode;
mouse: Mouse;
scroll: Scroll;
gridSize: Coords;
scene: SceneItems;
scene: SortedSceneItems;
contextMenu: ContextMenu;
itemControls: ItemControls;
}
export type InteractionReducerAction = (
state: Draft<State>,
payload: { prevMouse: Mouse }
) => void;
export type InteractionReducerAction = (state: Draft<State>) => void;
export type InteractionReducer = {
mousemove: InteractionReducerAction;

View File

@@ -4,26 +4,24 @@ import { Tool } from 'paper';
import { useSceneStore } from 'src/stores/useSceneStore';
import { useUiStateStore } from 'src/stores/useUiStateStore';
import { toolEventToMouseEvent } from './utils';
import { Select } from './reducers/Select';
import { DragItems } from './reducers/DragItems';
import { Pan } from './reducers/Pan';
import { Cursor } from './reducers/Cursor';
import type { InteractionReducer, InteractionReducerAction } from './types';
import { Lasso } from './reducers/Lasso';
import type { InteractionReducer } from './types';
const reducers: {
[key in 'SELECT' | 'PAN' | 'DRAG_ITEMS' | 'CURSOR']: InteractionReducer;
} = {
const reducers: { [k in string]: InteractionReducer } = {
CURSOR: Cursor,
SELECT: Select,
DRAG_ITEMS: DragItems,
PAN: Pan
PAN: Pan,
LASSO: Lasso
};
export const useInteractionManager = () => {
const tool = useRef<paper.Tool>();
const mode = useUiStateStore((state) => state.mode);
const mouse = useUiStateStore((state) => state.mouse);
const scroll = useUiStateStore((state) => state.scroll);
const mouse = useUiStateStore((state) => state.mouse);
const itemControls = useUiStateStore((state) => state.itemControls);
const contextMenu = useUiStateStore((state) => state.contextMenu);
const uiStateActions = useUiStateStore((state) => state.actions);
@@ -32,47 +30,37 @@ export const useInteractionManager = () => {
const sceneActions = useSceneStore((state) => state.actions);
const onMouseEvent = useCallback(
(toolEvent: paper.ToolEvent) => {
(
eventType: 'mousedown' | 'mousemove' | 'mouseup',
toolEvent: paper.ToolEvent
) => {
const reducer = reducers[mode.type];
let reducerAction: InteractionReducerAction;
switch (toolEvent.type) {
case 'mousedown':
reducerAction = reducer.mousedown;
break;
case 'mousemove':
reducerAction = reducer.mousemove;
break;
case 'mouseup':
reducerAction = reducer.mouseup;
break;
default:
return;
}
if (!reducer) return;
const prevMouse = { ...mouse };
// Update mouse position
const newMouse = toolEventToMouseEvent({
const reducerAction = reducer[eventType];
const nextMouse = toolEventToMouseEvent({
toolEvent,
mouse,
gridSize,
scroll
scroll,
prevMouse: mouse
});
const newState = produce(
{
scene,
mouse: newMouse,
mouse: nextMouse,
mode,
scroll,
gridSize,
contextMenu,
itemControls
},
(draft) => reducerAction(draft, { prevMouse })
(draft) => reducerAction(draft)
);
uiStateActions.setMouse(newMouse);
uiStateActions.setMouse(nextMouse);
uiStateActions.setScroll(newState.scroll);
uiStateActions.setMode(newState.mode);
uiStateActions.setContextMenu(newState.contextMenu);
@@ -81,8 +69,8 @@ export const useInteractionManager = () => {
},
[
mode,
mouse,
scroll,
mouse,
gridSize,
itemControls,
uiStateActions,
@@ -94,11 +82,12 @@ export const useInteractionManager = () => {
useEffect(() => {
tool.current = new Tool();
tool.current.onMouseMove = onMouseEvent;
tool.current.onMouseDown = onMouseEvent;
tool.current.onMouseUp = onMouseEvent;
// tool.current.onKeyDown = onMouseEvent;
// tool.current.onKeyUp = onMouseEvent;
tool.current.onMouseMove = (ev: paper.ToolEvent) =>
onMouseEvent('mousemove', ev);
tool.current.onMouseDown = (ev: paper.ToolEvent) =>
onMouseEvent('mousedown', ev);
tool.current.onMouseUp = (ev: paper.ToolEvent) =>
onMouseEvent('mouseup', ev);
return () => {
tool.current?.remove();

View File

@@ -2,49 +2,104 @@ import { Coords } from 'src/utils/Coords';
import { Mouse, Scroll } from 'src/stores/useUiStateStore';
import { getTileFromMouse } from 'src/renderer/utils/gridHelpers';
interface ToolEventToMouseEvent {
interface GetMousePositionFromToolEvent {
toolEvent: paper.ToolEvent;
mouse: Mouse;
gridSize: Coords;
scroll: Scroll;
}
export const toolEventToMouseEvent = ({
const getMousePositionFromToolEvent = ({
toolEvent,
mouse,
gridSize,
scroll
}: ToolEventToMouseEvent) => {
const position = Coords.fromObject(toolEvent.point);
let mouseDownAt: Mouse['mouseDownAt'];
switch (toolEvent.type) {
case 'mousedown':
mouseDownAt = position;
break;
case 'mouseup':
mouseDownAt = null;
break;
default:
mouseDownAt = mouse.mouseDownAt;
break;
}
let delta: Coords | null = position.subtract(mouse.position);
if (delta.x === 0 && delta.y === 0) delta = null;
}: GetMousePositionFromToolEvent): Mouse['position'] => {
const screenPosition = Coords.fromObject(toolEvent.point);
const tile = getTileFromMouse({
mousePosition: position,
mousePosition: screenPosition,
gridSize,
scroll
});
return {
tile,
position,
mouseDownAt,
delta
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
};
};

View File

@@ -7,9 +7,10 @@ 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 { getTileFromMouse, getTilePosition } from './utils/gridHelpers';
import { Node } from './components/Node/Node';
import { getTilePosition } from './utils/gridHelpers';
import { ContextMenuLayer } from './components/ContextMenuLayer/ContextMenuLayer';
import { Lasso } from './components/Lasso/Lasso';
const InitialisedRenderer = () => {
const renderer = useRenderer();
@@ -32,7 +33,6 @@ const InitialisedRenderer = () => {
const { position: scrollPosition } = scroll;
useEffect(() => {
console.log('init renderer');
initRenderer(gridSize);
setIsReady(true);
@@ -59,17 +59,13 @@ const InitialisedRenderer = () => {
useEffect(() => {
if (mode.type !== 'CURSOR') return;
const tile = getTileFromMouse({
gridSize,
mousePosition: mouse.position,
scroll
});
const { tile } = mouse.position;
const tilePosition = getTilePosition(tile);
renderer.cursor.moveTo(tilePosition);
}, [
mode,
mouse.position,
mouse,
renderer.cursor.moveTo,
gridSize,
scrollPosition,
@@ -85,12 +81,19 @@ const InitialisedRenderer = () => {
const isCursorVisible = mode.type === 'CURSOR';
renderer.cursor.setVisible(isCursorVisible);
}, [mode.type, mouse.position, renderer.cursor]);
}, [mode.type, renderer.cursor]);
if (!isReady) return null;
return (
<>
{mode.type === 'LASSO' && (
<Lasso
parentContainer={renderer.lassoContainer.current as paper.Group}
startTile={mode.selection.startTile}
endTile={mode.selection.endTile}
/>
)}
{scene.nodes.map((node) => (
<Node
key={node.id}

View File

@@ -0,0 +1,29 @@
import { useEffect } from 'react';
import { Coords } from 'src/utils/Coords';
import { useLasso } from './useLasso';
interface Props {
startTile: Coords;
endTile: Coords;
parentContainer: paper.Group;
}
export const Lasso = ({ startTile, endTile, parentContainer }: Props) => {
const lasso = useLasso();
const { init: initLasso, setSelection } = lasso;
useEffect(() => {
const container = initLasso();
parentContainer.addChild(container);
return () => {
container.remove();
};
}, [initLasso, parentContainer]);
useEffect(() => {
setSelection(startTile, endTile);
}, [setSelection, startTile, endTile]);
return null;
};

View File

@@ -0,0 +1,70 @@
import { useRef, useCallback } from 'react';
import { Group, Shape } from 'paper';
import { Coords } from 'src/utils/Coords';
import { TILE_SIZE, PIXEL_UNIT } from 'src/renderer/utils/constants';
import {
getBoundingBox,
sortByPosition,
getTileBounds
} from 'src/renderer/utils/gridHelpers';
import { applyProjectionMatrix } from 'src/renderer/utils/projection';
export const useLasso = () => {
const containerRef = useRef(new Group());
const shapeRef = useRef<paper.Shape.Rectangle>();
const setSelection = useCallback((startTile: Coords, endTile: Coords) => {
if (!shapeRef.current) return;
const boundingBox = getBoundingBox([startTile, endTile]);
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
);
shapeRef.current.set({
position,
size: [
(size.x + 1) * (TILE_SIZE - PIXEL_UNIT * 3),
(size.y + 1) * (TILE_SIZE - PIXEL_UNIT * 3)
]
});
containerRef.current.set({
pivot: shapeRef.current.bounds.bottomLeft,
position: lassoScreenPosition
});
}, []);
const init = useCallback(() => {
containerRef.current.removeChildren();
containerRef.current.set({ pivot: [0, 0] });
shapeRef.current = new Shape.Rectangle({
strokeCap: 'round',
fillColor: 'lightBlue',
size: [TILE_SIZE, TILE_SIZE],
opacity: 0.5,
radius: PIXEL_UNIT * 8,
strokeWidth: PIXEL_UNIT * 3,
strokeColor: 'blue',
dashArray: [5, 10],
pivot: [0, 0]
});
containerRef.current.addChild(shapeRef.current);
applyProjectionMatrix(containerRef.current);
return containerRef.current;
}, []);
return {
init,
containerRef,
setSelection
};
};

View File

@@ -2,13 +2,15 @@ import { useCallback, useRef } from 'react';
import Paper, { Group } from 'paper';
import { Coords } from 'src/utils/Coords';
import { useUiStateStore } from 'src/stores/useUiStateStore';
import { useGrid } from './components/grid/useGrid';
import { useGrid } from './components/Grid/useGrid';
import { useNodeManager } from './useNodeManager';
import { useCursor } from './components/cursor/useCursor';
import { useCursor } from './components/Cursor/useCursor';
export const useRenderer = () => {
const container = useRef(new Group());
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 cursor = useCursor();
@@ -29,6 +31,7 @@ export const useRenderer = () => {
innerContainer.current.addChild(gridContainer);
innerContainer.current.addChild(cursorContainer);
innerContainer.current.addChild(lassoContainer.current);
innerContainer.current.addChild(nodeManager.container);
container.current.addChild(innerContainer.current);
container.current.set({ position: [0, 0] });
@@ -59,6 +62,7 @@ export const useRenderer = () => {
zoomTo,
scrollTo,
nodeManager,
cursor
cursor,
lassoContainer
};
};

View File

@@ -2,7 +2,7 @@ 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 { SceneItems } from 'src/stores/useSceneStore';
import { SortedSceneItems } from 'src/stores/useSceneStore';
import { Scroll } from 'src/stores/useUiStateStore';
const halfW = PROJECTED_TILE_DIMENSIONS.x * 0.5;
@@ -66,11 +66,16 @@ export const getTileBounds = (coords: Coords) => {
interface GetItemsByTile {
tile: Coords;
sceneItems: SceneItems;
sortedSceneItems: SortedSceneItems;
}
export const getItemsByTile = ({ tile, sceneItems }: GetItemsByTile) => {
const nodes = sceneItems.nodes.filter((node) => node.position.isEqual(tile));
export const getItemsByTile = ({
tile,
sortedSceneItems
}: GetItemsByTile): SortedSceneItems => {
const nodes = sortedSceneItems.nodes.filter((node) =>
node.position.isEqual(tile)
);
return { nodes };
};
@@ -131,3 +136,66 @@ export const getTileScreenPosition = ({
return onScreenPosition;
};
export const sortByPosition = (items: Coords[]) => {
const xSorted = [...items];
const ySorted = [...items];
xSorted.sort((a, b) => a.x - b.x);
ySorted.sort((a, b) => a.y - b.y);
const highest = {
byX: xSorted[xSorted.length - 1],
byY: ySorted[ySorted.length - 1]
};
const lowest = { byX: xSorted[0], byY: ySorted[0] };
const lowX = lowest.byX.x;
const highX = highest.byX.x;
const lowY = lowest.byY.y;
const highY = highest.byY.y;
return {
byX: xSorted,
byY: ySorted,
highest,
lowest,
lowX,
lowY,
highX,
highY
};
};
export const getGridSubset = (tiles: Coords[]) => {
const { lowX, lowY, highX, highY } = sortByPosition(tiles);
const subset = [];
for (let x = lowX; x < highX + 1; x += 1) {
for (let y = lowY; y < highY + 1; y += 1) {
subset.push(new Coords(x, y));
}
}
return subset;
};
export const isWithinBounds = (tile: Coords, bounds: Coords[]) => {
const { lowX, lowY, highX, highY } = sortByPosition(bounds);
return tile.x >= lowX && tile.x <= highX && tile.y >= lowY && tile.y <= highY;
};
export const getBoundingBox = (
tiles: Coords[],
offset: Coords = new Coords(0, 0)
) => {
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)
];
};

View File

@@ -10,11 +10,6 @@ export enum SceneItemTypeEnum {
NODE = 'NODE'
}
export interface SceneItem {
id: string;
type: SceneItemTypeEnum;
}
export interface Node {
type: SceneItemTypeEnum.NODE;
id: string;
@@ -28,18 +23,23 @@ export interface Node {
export type Icon = IconInput;
export interface SceneItems {
export interface SceneItem {
id: string;
type: SceneItemTypeEnum;
}
export interface SortedSceneItems {
nodes: Node[];
}
export type Scene = SceneItems & {
export type Scene = SortedSceneItems & {
icons: IconInput[];
gridSize: Coords;
};
export interface SceneActions {
set: (scene: Scene) => void;
setItems: (elements: SceneItems) => void;
setItems: (elements: SortedSceneItems) => void;
updateNode: (id: string, updates: Partial<Node>) => void;
createNode: (position: Coords) => void;
}
@@ -56,7 +56,7 @@ export const useSceneStore = create<UseSceneStore>((set, get) => ({
set: (scene) => {
set(scene);
},
setItems: (items: SceneItems) => {
setItems: (items: SortedSceneItems) => {
set({ nodes: items.nodes });
},
updateNode: (id, updates) => {

View File

@@ -1,7 +1,7 @@
import { create } from 'zustand';
import { clamp, roundToOneDecimalPlace } from 'src/utils';
import { Coords } from 'src/utils/Coords';
import { Node, SceneItem } from 'src/stores/useSceneStore';
import { SortedSceneItems, SceneItem } from 'src/stores/useSceneStore';
const ZOOM_INCREMENT = 0.2;
export const MIN_ZOOM = 0.2;
@@ -22,23 +22,45 @@ export type ItemControls =
}
| null;
export type Mode =
| {
type: 'CURSOR';
}
| {
type: 'SELECT';
}
| {
type: 'PAN';
}
| {
type: 'DRAG_ITEMS';
items: {
nodes: Pick<Node, 'id' | 'type'>[];
};
hasMovedTile: boolean;
};
export interface Mouse {
position: {
screen: Coords;
tile: Coords;
};
mousedown: {
screen: Coords;
tile: Coords;
} | null;
delta: {
screen: Coords;
tile: Coords;
} | null;
}
export interface CursorMode {
type: 'CURSOR';
mousedownItems: SortedSceneItems | null;
}
export interface PanMode {
type: 'PAN';
}
export interface CreateLassoMode {
type: 'LASSO'; // TODO: Put these into an enum
selection: {
startTile: Coords;
endTile: Coords;
};
isDragging: boolean;
}
export interface DragItemsMode {
type: 'DRAG_ITEMS';
items: SortedSceneItems;
}
export type Mode = CursorMode | PanMode | DragItemsMode | CreateLassoMode;
export type ContextMenu =
| SceneItem
@@ -48,13 +70,6 @@ export type ContextMenu =
}
| null;
export interface Mouse {
position: Coords;
tile: Coords;
mouseDownAt: Coords | null;
delta: Coords | null;
}
export interface Scroll {
position: Coords;
offset: Coords;
@@ -74,9 +89,9 @@ export interface UiStateActions {
incrementZoom: () => void;
decrementZoom: () => void;
setScroll: (scroll: Scroll) => void;
setMouse: (mouse: Mouse) => void;
setSidebar: (itemControls: ItemControls) => void;
setContextMenu: (contextMenu: ContextMenu) => void;
setMouse: (mouse: Mouse) => void;
}
export type UseUiStateStore = UiState & {
@@ -84,19 +99,21 @@ export type UseUiStateStore = UiState & {
};
export const useUiStateStore = create<UseUiStateStore>((set, get) => ({
mode: { type: 'CURSOR' },
mode: {
type: 'CURSOR',
mousedownItems: null
},
mouse: {
position: { screen: new Coords(0, 0), tile: new Coords(0, 0) },
mousedown: null,
delta: null
},
itemControls: null,
contextMenu: null,
scroll: {
position: new Coords(0, 0),
offset: new Coords(0, 0)
},
mouse: {
position: new Coords(0, 0),
tile: new Coords(0, 0),
mouseDownAt: null,
delta: null
},
zoom: 1,
actions: {
setMode: (mode) => {
@@ -115,14 +132,14 @@ export const useUiStateStore = create<UseUiStateStore>((set, get) => ({
setScroll: ({ position, offset }) => {
set({ scroll: { position, offset: offset ?? get().scroll.offset } });
},
setMouse: ({ position, delta, mouseDownAt, tile }) => {
set({ mouse: { position, delta, mouseDownAt, tile } });
},
setSidebar: (itemControls) => {
set({ itemControls });
},
setContextMenu: (contextMenu) => {
set({ contextMenu });
},
setMouse: (mouse) => {
set({ mouse });
}
}
}));

View File

@@ -64,4 +64,8 @@ export class Coords {
static fromObject({ x, y }: { x: number; y: number }) {
return new Coords(x, y);
}
static zero() {
return new Coords(0, 0);
}
}