mirror of
https://github.com/stan-smith/FossFLOW.git
synced 2025-12-24 06:58:48 -05:00
feat: implements lasso selection (UI disabled)
This commit is contained in:
@@ -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'}
|
||||
/>
|
||||
|
||||
@@ -12,7 +12,7 @@ const DataLayer = () => {
|
||||
const { updateNode } = useIsoflow();
|
||||
|
||||
const onSceneUpdated = useCallback((scene: SceneInput) => {
|
||||
console.log(scene);
|
||||
// console.log(scene);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
};
|
||||
|
||||
72
src/interaction/reducers/Lasso.ts
Normal file
72
src/interaction/reducers/Lasso.ts
Normal 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: () => {}
|
||||
};
|
||||
@@ -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: () => {}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { InteractionReducer } from '../types';
|
||||
|
||||
export const Select: InteractionReducer = {
|
||||
mousemove: () => {},
|
||||
mousedown: () => {},
|
||||
mouseup: () => {}
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
|
||||
29
src/renderer/components/Lasso/Lasso.tsx
Normal file
29
src/renderer/components/Lasso/Lasso.tsx
Normal 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;
|
||||
};
|
||||
70
src/renderer/components/Lasso/useLasso.ts
Normal file
70
src/renderer/components/Lasso/useLasso.ts
Normal 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
|
||||
};
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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)
|
||||
];
|
||||
};
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user