mirror of
https://github.com/stan-smith/FossFLOW.git
synced 2025-12-24 06:58:48 -05:00
working lasso rectangle mode
This commit is contained in:
@@ -12,6 +12,7 @@ interface Props {
|
||||
stroke?: {
|
||||
width: number;
|
||||
color: string;
|
||||
dashArray?: string;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -30,10 +31,16 @@ export const IsoTileArea = ({
|
||||
const strokeParams = useMemo(() => {
|
||||
if (!stroke) return {};
|
||||
|
||||
return {
|
||||
const params: Record<string, any> = {
|
||||
stroke: stroke.color,
|
||||
strokeWidth: stroke.width
|
||||
};
|
||||
|
||||
if (stroke.dashArray) {
|
||||
params.strokeDasharray = stroke.dashArray;
|
||||
}
|
||||
|
||||
return params;
|
||||
}, [stroke]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,83 +1,29 @@
|
||||
// import { useRef, useCallback } from 'react';
|
||||
// import { Rectangle, Shape } from 'paper';
|
||||
// import gsap from 'gsap';
|
||||
// import { Coords } from 'src/types';
|
||||
// import { UNPROJECTED_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';
|
||||
import React from 'react';
|
||||
import { useUiStateStore } from 'src/stores/uiStateStore';
|
||||
import { IsoTileArea } from 'src/components/IsoTileArea/IsoTileArea';
|
||||
|
||||
// export const useLasso = () => {
|
||||
// const containerRef = useRef(new Rectangle());
|
||||
// const shapeRef = useRef<paper.Shape.Rectangle>();
|
||||
export const Lasso = () => {
|
||||
const mode = useUiStateStore((state) => {
|
||||
return state.mode;
|
||||
});
|
||||
|
||||
// const setSelection = useCallback((startTile: Coords, endTile: Coords) => {
|
||||
// if (!shapeRef.current) return;
|
||||
if (mode.type !== 'LASSO' || !mode.selection) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// const boundingBox = getBoundingBox([startTile, endTile]);
|
||||
const { startTile, endTile } = mode.selection;
|
||||
|
||||
// // TODO: Enforce at least one node being passed to this getBoundingBox() to prevent null returns
|
||||
// if (!boundingBox) return;
|
||||
|
||||
// const lassoStartTile = boundingBox[3];
|
||||
// const lassoScreenPosition = getTileBounds(lassoStartTile).left;
|
||||
// const sorted = sortByPosition(boundingBox);
|
||||
// const position = { x: sorted.lowX, y: sorted.highY };
|
||||
// const size = {
|
||||
// x: sorted.highX - sorted.lowX,
|
||||
// y: sorted.highY - sorted.lowY
|
||||
// };
|
||||
|
||||
// shapeRef.current.set({
|
||||
// position,
|
||||
// size: [
|
||||
// (size.x + 1) * (UNPROJECTED_TILE_SIZE - PIXEL_UNIT * 3),
|
||||
// (size.y + 1) * (UNPROJECTED_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: [UNPROJECTED_TILE_SIZE, UNPROJECTED_TILE_SIZE],
|
||||
// opacity: 0.5,
|
||||
// radius: PIXEL_UNIT * 8,
|
||||
// strokeWidth: PIXEL_UNIT * 3,
|
||||
// strokeColor: 'blue',
|
||||
// dashArray: [5, 10],
|
||||
// pivot: [0, 0]
|
||||
// });
|
||||
|
||||
// gsap
|
||||
// .fromTo(
|
||||
// shapeRef.current,
|
||||
// { dashOffset: 0 },
|
||||
// { dashOffset: PIXEL_UNIT * 10, ease: 'none', duration: 0.25 }
|
||||
// )
|
||||
// .repeat(-1);
|
||||
|
||||
// containerRef.current.addChild(shapeRef.current);
|
||||
// applyProjectionMatrix(containerRef.current);
|
||||
|
||||
// return containerRef.current;
|
||||
// }, []);
|
||||
|
||||
// return {
|
||||
// init,
|
||||
// containerRef,
|
||||
// setSelection
|
||||
// };
|
||||
// };
|
||||
return (
|
||||
<IsoTileArea
|
||||
from={startTile}
|
||||
to={endTile}
|
||||
fill="rgba(33, 150, 243, 0.15)"
|
||||
cornerRadius={8}
|
||||
stroke={{
|
||||
color: '#2196f3',
|
||||
width: 2,
|
||||
dashArray: '8 4'
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@ import { TextBoxes } from 'src/components/SceneLayers/TextBoxes/TextBoxes';
|
||||
import { SizeIndicator } from 'src/components/DebugUtils/SizeIndicator';
|
||||
import { SceneLayer } from 'src/components/SceneLayer/SceneLayer';
|
||||
import { TransformControlsManager } from 'src/components/TransformControlsManager/TransformControlsManager';
|
||||
import { Lasso } from 'src/components/Lasso/Lasso';
|
||||
import { useScene } from 'src/hooks/useScene';
|
||||
import { RendererProps } from 'src/types/rendererProps';
|
||||
|
||||
@@ -59,6 +60,9 @@ export const Renderer = ({ showGrid, backgroundColor }: RendererProps) => {
|
||||
<SceneLayer>
|
||||
<Rectangles rectangles={rectangles} />
|
||||
</SceneLayer>
|
||||
<SceneLayer>
|
||||
<Lasso />
|
||||
</SceneLayer>
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
|
||||
@@ -9,7 +9,8 @@ import {
|
||||
Title as TitleIcon,
|
||||
Undo as UndoIcon,
|
||||
Redo as RedoIcon,
|
||||
Help as HelpIcon
|
||||
Help as HelpIcon,
|
||||
HighlightAltOutlined as LassoIcon
|
||||
} from '@mui/icons-material';
|
||||
import { useUiStateStore } from 'src/stores/uiStateStore';
|
||||
import { IconButton } from 'src/components/IconButton/IconButton';
|
||||
@@ -92,6 +93,19 @@ export const ToolMenu = () => {
|
||||
}}
|
||||
isActive={mode.type === 'CURSOR' || mode.type === 'DRAG_ITEMS'}
|
||||
/>
|
||||
<IconButton
|
||||
name={`Lasso select${hotkeys.lasso ? ` (${hotkeys.lasso.toUpperCase()})` : ''}`}
|
||||
Icon={<LassoIcon />}
|
||||
onClick={() => {
|
||||
uiStateStoreActions.setMode({
|
||||
type: 'LASSO',
|
||||
showCursor: true,
|
||||
selection: null,
|
||||
isDragging: false
|
||||
});
|
||||
}}
|
||||
isActive={mode.type === 'LASSO'}
|
||||
/>
|
||||
<IconButton
|
||||
name={`Pan${hotkeys.pan ? ` (${hotkeys.pan.toUpperCase()})` : ''}`}
|
||||
Icon={<PanToolIcon />}
|
||||
|
||||
@@ -7,6 +7,7 @@ export interface HotkeyMapping {
|
||||
rectangle: string | null;
|
||||
connector: string | null;
|
||||
text: string | null;
|
||||
lasso: string | null;
|
||||
}
|
||||
|
||||
export const HOTKEY_PROFILES: Record<HotkeyProfile, HotkeyMapping> = {
|
||||
@@ -16,7 +17,8 @@ export const HOTKEY_PROFILES: Record<HotkeyProfile, HotkeyMapping> = {
|
||||
addItem: 'e',
|
||||
rectangle: 'r',
|
||||
connector: 't',
|
||||
text: 'y'
|
||||
text: 'y',
|
||||
lasso: 'l'
|
||||
},
|
||||
smnrct: {
|
||||
select: 's',
|
||||
@@ -24,7 +26,8 @@ export const HOTKEY_PROFILES: Record<HotkeyProfile, HotkeyMapping> = {
|
||||
addItem: 'n',
|
||||
rectangle: 'r',
|
||||
connector: 'c',
|
||||
text: 't'
|
||||
text: 't',
|
||||
lasso: 'l'
|
||||
},
|
||||
none: {
|
||||
select: null,
|
||||
@@ -32,7 +35,8 @@ export const HOTKEY_PROFILES: Record<HotkeyProfile, HotkeyMapping> = {
|
||||
addItem: null,
|
||||
rectangle: null,
|
||||
connector: null,
|
||||
text: null
|
||||
text: null,
|
||||
lasso: null
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -225,16 +225,21 @@ export const useScene = () => {
|
||||
);
|
||||
|
||||
const updateViewItem = useCallback(
|
||||
(id: string, updates: Partial<ViewItem>) => {
|
||||
if (!model?.actions || !scene?.actions || !currentViewId) return;
|
||||
(id: string, updates: Partial<ViewItem>, currentState?: State) => {
|
||||
if (!model?.actions || !scene?.actions || !currentViewId) return getState();
|
||||
|
||||
saveToHistoryBeforeChange();
|
||||
if (!transactionInProgress.current) {
|
||||
saveToHistoryBeforeChange();
|
||||
}
|
||||
|
||||
const stateToUse = currentState || getState();
|
||||
const newState = reducers.view({
|
||||
action: 'UPDATE_VIEWITEM',
|
||||
payload: { id, ...updates },
|
||||
ctx: { viewId: currentViewId, state: getState() }
|
||||
ctx: { viewId: currentViewId, state: stateToUse }
|
||||
});
|
||||
setState(newState);
|
||||
return newState; // Return for chaining
|
||||
},
|
||||
[
|
||||
getState,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { produce } from 'immer';
|
||||
import { ModeActions, Coords, ItemReference } from 'src/types';
|
||||
import { useScene } from 'src/hooks/useScene';
|
||||
import type { State } from 'src/stores/reducers/types';
|
||||
import {
|
||||
getItemByIdOrThrow,
|
||||
CoordsUtils,
|
||||
@@ -19,7 +20,7 @@ const dragItems = (
|
||||
// Separate items from other draggable elements
|
||||
const itemRefs = items.filter(item => item.type === 'ITEM');
|
||||
const otherRefs = items.filter(item => item.type !== 'ITEM');
|
||||
|
||||
|
||||
// If there are items being dragged, find nearest unoccupied tiles for them
|
||||
if (itemRefs.length > 0) {
|
||||
const itemsWithTargets = itemRefs.map(item => {
|
||||
@@ -29,19 +30,24 @@ const dragItems = (
|
||||
targetTile: CoordsUtils.add(node.tile, delta)
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
// Find nearest unoccupied tiles for all items
|
||||
const newTiles = findNearestUnoccupiedTilesForGroup(
|
||||
itemsWithTargets,
|
||||
scene,
|
||||
itemRefs.map(item => item.id) // Exclude the items being dragged
|
||||
);
|
||||
|
||||
|
||||
// If we found valid positions for all items, move them
|
||||
if (newTiles) {
|
||||
itemRefs.forEach((item, index) => {
|
||||
scene.updateViewItem(item.id, {
|
||||
tile: newTiles[index]
|
||||
// Wrap all updates in a transaction to prevent history issues
|
||||
scene.transaction(() => {
|
||||
// Chain state updates to avoid race conditions
|
||||
let currentState: State | undefined;
|
||||
itemRefs.forEach((item, index) => {
|
||||
currentState = scene.updateViewItem(item.id, {
|
||||
tile: newTiles[index]
|
||||
}, currentState);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,79 +1,122 @@
|
||||
// import { CoordsUtils, isWithinBounds } from 'src/utils';
|
||||
// import { ModeActions } from 'src/types';
|
||||
import { produce } from 'immer';
|
||||
import { ModeActions, ItemReference } from 'src/types';
|
||||
import { CoordsUtils, isWithinBounds, hasMovedTile } from 'src/utils';
|
||||
|
||||
// export const Lasso: ModeActions = {
|
||||
// type: 'LASSO',
|
||||
// mousemove: ({ uiState, Model }) => {
|
||||
// if (uiState.mode.type !== 'LASSO') return;
|
||||
// Helper to find all items within the lasso bounds
|
||||
const getItemsInBounds = (
|
||||
startTile: { x: number; y: number },
|
||||
endTile: { x: number; y: number },
|
||||
scene: any
|
||||
): ItemReference[] => {
|
||||
const items: ItemReference[] = [];
|
||||
|
||||
// if (uiState.mouse.mousedown === null) return;
|
||||
// // User is in mousedown mode
|
||||
// Check all nodes/items
|
||||
scene.items.forEach((item: any) => {
|
||||
if (isWithinBounds(item.tile, [startTile, endTile])) {
|
||||
items.push({ type: 'ITEM', id: item.id });
|
||||
}
|
||||
});
|
||||
|
||||
// if (
|
||||
// uiState.mouse.delta === null ||
|
||||
// CoordsUtils.isEqual(uiState.mouse.delta.tile, CoordsUtils.zero())
|
||||
// )
|
||||
// return;
|
||||
// // User has moved tile since they moused down
|
||||
// Check all rectangles
|
||||
scene.rectangles.forEach((rectangle: any) => {
|
||||
// Check if rectangle's center or any corner is within bounds
|
||||
if (
|
||||
isWithinBounds(rectangle.from, [startTile, endTile]) ||
|
||||
isWithinBounds(rectangle.to, [startTile, endTile])
|
||||
) {
|
||||
items.push({ type: 'RECTANGLE', id: rectangle.id });
|
||||
}
|
||||
});
|
||||
|
||||
// if (!uiState.mode.isDragging) {
|
||||
// const { mousedown } = uiState.mouse;
|
||||
// const items = Model.nodes.filter((node) => {
|
||||
// return CoordsUtils.isEqual(node.tile, mousedown.tile);
|
||||
// });
|
||||
// Check all text boxes
|
||||
scene.textBoxes.forEach((textBox: any) => {
|
||||
if (isWithinBounds(textBox.tile, [startTile, endTile])) {
|
||||
items.push({ type: 'TEXTBOX', id: textBox.id });
|
||||
}
|
||||
});
|
||||
|
||||
// // User is creating a selection
|
||||
// uiState.mode.selection = {
|
||||
// startTile: uiState.mouse.mousedown.tile,
|
||||
// endTile: uiState.mouse.position.tile,
|
||||
// items
|
||||
// };
|
||||
return items;
|
||||
};
|
||||
|
||||
// return;
|
||||
// }
|
||||
export const Lasso: ModeActions = {
|
||||
mousemove: ({ uiState, scene }) => {
|
||||
if (uiState.mode.type !== 'LASSO' || !uiState.mouse.mousedown) return;
|
||||
|
||||
// if (uiState.mode.isDragging) {
|
||||
// // User is dragging an existing selection
|
||||
// uiState.mode.selection.startTile = CoordsUtils.add(
|
||||
// uiState.mode.selection.startTile,
|
||||
// uiState.mouse.delta.tile
|
||||
// );
|
||||
// uiState.mode.selection.endTile = CoordsUtils.add(
|
||||
// uiState.mode.selection.endTile,
|
||||
// uiState.mouse.delta.tile
|
||||
// );
|
||||
// }
|
||||
// },
|
||||
// mousedown: (draft) => {
|
||||
// if (draft.mode.type !== 'LASSO') return;
|
||||
if (!hasMovedTile(uiState.mouse)) return;
|
||||
|
||||
// if (draft.mode.selection) {
|
||||
// const isWithinSelection = isWithinBounds(draft.mouse.position.tile, [
|
||||
// draft.mode.selection.startTile,
|
||||
// draft.mode.selection.endTile
|
||||
// ]);
|
||||
if (uiState.mode.isDragging && uiState.mode.selection) {
|
||||
// User is dragging an existing selection - switch to DRAG_ITEMS mode
|
||||
uiState.actions.setMode({
|
||||
type: 'DRAG_ITEMS',
|
||||
showCursor: true,
|
||||
items: uiState.mode.selection.items,
|
||||
isInitialMovement: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// if (!isWithinSelection) {
|
||||
// draft.mode = {
|
||||
// type: 'CURSOR',
|
||||
// showCursor: true,
|
||||
// mousedown: null
|
||||
// };
|
||||
// User is creating/updating the selection box
|
||||
const startTile = uiState.mouse.mousedown.tile;
|
||||
const endTile = uiState.mouse.position.tile;
|
||||
const items = getItemsInBounds(startTile, endTile, scene);
|
||||
|
||||
// return;
|
||||
// }
|
||||
uiState.actions.setMode(
|
||||
produce(uiState.mode, (draft) => {
|
||||
if (draft.type === 'LASSO') {
|
||||
draft.selection = {
|
||||
startTile,
|
||||
endTile,
|
||||
items
|
||||
};
|
||||
}
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
// if (isWithinSelection) {
|
||||
// draft.mode.isDragging = true;
|
||||
mousedown: ({ uiState }) => {
|
||||
if (uiState.mode.type !== 'LASSO') return;
|
||||
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
// If there's an existing selection, check if click is within it
|
||||
if (uiState.mode.selection) {
|
||||
const isWithinSelection = isWithinBounds(uiState.mouse.position.tile, [
|
||||
uiState.mode.selection.startTile,
|
||||
uiState.mode.selection.endTile
|
||||
]);
|
||||
|
||||
// draft.mode = {
|
||||
// type: 'CURSOR',
|
||||
// showCursor: true,
|
||||
// mousedown: null
|
||||
// };
|
||||
// }
|
||||
// };
|
||||
if (isWithinSelection) {
|
||||
// Clicked within selection - prepare to drag
|
||||
uiState.actions.setMode(
|
||||
produce(uiState.mode, (draft) => {
|
||||
if (draft.type === 'LASSO') {
|
||||
draft.isDragging = true;
|
||||
}
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Clicked outside selection - clear it and stay in LASSO mode
|
||||
uiState.actions.setMode(
|
||||
produce(uiState.mode, (draft) => {
|
||||
if (draft.type === 'LASSO') {
|
||||
draft.selection = null;
|
||||
draft.isDragging = false;
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
mouseup: ({ uiState }) => {
|
||||
if (uiState.mode.type !== 'LASSO') return;
|
||||
|
||||
// Reset dragging state but keep selection
|
||||
uiState.actions.setMode(
|
||||
produce(uiState.mode, (draft) => {
|
||||
if (draft.type === 'LASSO') {
|
||||
draft.isDragging = false;
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -17,6 +17,7 @@ import { Connector } from './modes/Connector';
|
||||
import { Pan } from './modes/Pan';
|
||||
import { PlaceIcon } from './modes/PlaceIcon';
|
||||
import { TextBox } from './modes/TextBox';
|
||||
import { Lasso } from './modes/Lasso';
|
||||
import { usePanHandlers } from './usePanHandlers';
|
||||
|
||||
const modes: { [k in string]: ModeActions } = {
|
||||
@@ -27,7 +28,8 @@ const modes: { [k in string]: ModeActions } = {
|
||||
CONNECTOR: Connector,
|
||||
PAN: Pan,
|
||||
PLACE_ICON: PlaceIcon,
|
||||
TEXTBOX: TextBox
|
||||
TEXTBOX: TextBox,
|
||||
LASSO: Lasso
|
||||
};
|
||||
|
||||
const getModeFunction = (mode: ModeActions, e: SlimMouseEvent) => {
|
||||
@@ -162,6 +164,14 @@ export const useInteractionManager = () => {
|
||||
showCursor: false,
|
||||
id: textBoxId
|
||||
});
|
||||
} else if (hotkeyMapping.lasso && key === hotkeyMapping.lasso) {
|
||||
e.preventDefault();
|
||||
uiState.actions.setMode({
|
||||
type: 'LASSO',
|
||||
showCursor: true,
|
||||
selection: null,
|
||||
isDragging: false
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -308,7 +318,7 @@ export const useInteractionManager = () => {
|
||||
el.addEventListener('touchstart', onTouchStart);
|
||||
el.addEventListener('touchmove', onTouchMove);
|
||||
el.addEventListener('touchend', onTouchEnd);
|
||||
uiState.rendererEl?.addEventListener('wheel', onScroll);
|
||||
uiState.rendererEl?.addEventListener('wheel', onScroll, { passive: true });
|
||||
|
||||
return () => {
|
||||
el.removeEventListener('mousemove', onMouseEvent);
|
||||
|
||||
@@ -95,6 +95,17 @@ export interface TextBoxMode {
|
||||
id: string | null;
|
||||
}
|
||||
|
||||
export interface LassoMode {
|
||||
type: 'LASSO';
|
||||
showCursor: boolean;
|
||||
selection: {
|
||||
startTile: Coords;
|
||||
endTile: Coords;
|
||||
items: ItemReference[];
|
||||
} | null;
|
||||
isDragging: boolean;
|
||||
}
|
||||
|
||||
export type Mode =
|
||||
| InteractionsDisabled
|
||||
| CursorMode
|
||||
@@ -104,7 +115,8 @@ export type Mode =
|
||||
| DrawRectangleMode
|
||||
| TransformRectangleMode
|
||||
| DragItemsMode
|
||||
| TextBoxMode;
|
||||
| TextBoxMode
|
||||
| LassoMode;
|
||||
// End mode types
|
||||
|
||||
export interface Scroll {
|
||||
|
||||
Reference in New Issue
Block a user