diff --git a/src/components/konva/Note.tsx b/src/components/konva/Note.tsx index b6ecf6b..8aa473b 100644 --- a/src/components/konva/Note.tsx +++ b/src/components/konva/Note.tsx @@ -194,6 +194,7 @@ function Note({ return ( void; + onSelectionItemsChange: SelectionItemsChangeEventHandler; +} & Konva.ShapeConfig; + +function Selection({ + selection, + onSelectionChange, + onSelectionItemsChange, + ...props +}: SelectionProps) { + const userId = useUserId(); + + const mapWidth = useMapWidth(); + const mapHeight = useMapHeight(); + const stageScale = useDebouncedStageScale(); + const gridStrokeWidth = useGridStrokeWidth(); + + const intersectingNodesRef = useRef< + { type: SelectionItemType; node: Konva.Node; id: string }[] + >([]); + const previousDragPositionRef = useRef({ x: 0, y: 0 }); + + function handleDragStart(event: Konva.KonvaEventObject) { + previousDragPositionRef.current = event.target.position(); + const stage = event.target.getStage(); + if (stage) { + for (let item of selection.items) { + const node = stage.findOne(`#${item.id}`); + if (node) { + intersectingNodesRef.current.push({ ...item, node }); + } + } + } + } + + function handleDragMove(event: Konva.KonvaEventObject) { + const deltaPosition = Vector2.subtract( + event.target.position(), + previousDragPositionRef.current + ); + for (let item of intersectingNodesRef.current) { + item.node.position(Vector2.add(item.node.position(), deltaPosition)); + } + previousDragPositionRef.current = event.target.position(); + } + + function handleDragEnd(event: Konva.KonvaEventObject) { + const tokenChanges: Record> = {}; + const noteChanges: Record> = {}; + for (let item of intersectingNodesRef.current) { + if (item.type === "token") { + tokenChanges[item.id] = { + x: item.node.x() / mapWidth, + y: item.node.y() / mapHeight, + lastModifiedBy: userId, + lastModified: Date.now(), + }; + } else { + noteChanges[item.id] = { + x: item.node.x() / mapWidth, + y: item.node.y() / mapHeight, + lastModifiedBy: userId, + lastModified: Date.now(), + }; + } + } + onSelectionItemsChange(tokenChanges, noteChanges); + onSelectionChange({ + ...selection, + x: event.target.x() / mapWidth, + y: event.target.y() / mapHeight, + }); + intersectingNodesRef.current = []; + } + + function handleClick() { + onSelectionChange(null); + } + + const strokeWidth = gridStrokeWidth / stageScale; + const defaultProps = { + stroke: colors.primary, + strokeWidth: strokeWidth, + dash: [strokeWidth / 2, strokeWidth * 2], + onDragStart: handleDragStart, + onDragMove: handleDragMove, + onDragEnd: handleDragEnd, + draggable: true, + onClick: handleClick, + onTap: handleClick, + }; + const x = selection.x * mapWidth; + const y = selection.y * mapHeight; + if (selection.type === "path") { + return ( + 0} + lineCap="round" + lineJoin="round" + x={x} + y={y} + {...defaultProps} + {...props} + /> + ); + } else { + return ( + + ); + } +} + +export default Selection; diff --git a/src/components/map/Map.tsx b/src/components/map/Map.tsx index 553f2f0..b6b83c4 100644 --- a/src/components/map/Map.tsx +++ b/src/components/map/Map.tsx @@ -38,6 +38,7 @@ import { NoteRemoveEventHander, TokenStateChangeEventHandler, NoteCreateEventHander, + SelectionItemsChangeEventHandler, } from "../../types/Events"; import useMapTokens from "../../hooks/useMapTokens"; @@ -50,6 +51,7 @@ type MapProps = { mapActions: MapActions; onMapTokenStateChange: TokenStateChangeEventHandler; onMapTokenStateRemove: TokenStateRemoveHandler; + onSelectionItemsChange: SelectionItemsChangeEventHandler; onMapChange: MapChangeEventHandler; onMapReset: MapResetEventHandler; onMapDraw: (action: Action) => void; @@ -69,6 +71,7 @@ function Map({ mapActions, onMapTokenStateChange, onMapTokenStateRemove, + onSelectionItemsChange, onMapChange, onMapReset, onMapDraw, @@ -211,6 +214,7 @@ function Map({ diff --git a/src/components/tools/NoteTool.tsx b/src/components/tools/NoteTool.tsx index ce880dd..71bc922 100644 --- a/src/components/tools/NoteTool.tsx +++ b/src/components/tools/NoteTool.tsx @@ -146,7 +146,7 @@ function NoteTool({ }); return ( - + {notes.map((note) => ( (null); + const [selection, setSelection] = useState(null); const [isBrushDown, setIsBrushDown] = useState(false); useEffect(() => { @@ -66,20 +77,24 @@ function SelectTool({ active, toolSettings }: MapSelectProps) { function handleBrushDown() { const brushPosition = getBrushPosition(); - if (!brushPosition) { + if (!brushPosition || selection) { return; } if (toolSettings.type === "path") { setSelection({ type: "path", - nodes: [], + items: [], data: { points: [brushPosition] }, + x: 0, + y: 0, }); } else { setSelection({ type: "rectangle", - nodes: [], + items: [], data: getDefaultShapeData("rectangle", brushPosition) as RectData, + x: 0, + y: 0, }); } setIsBrushDown(true); @@ -134,7 +149,77 @@ function SelectTool({ active, toolSettings }: MapSelectProps) { } function handleBrushUp() { - setSelection(null); + if (selection && mapStage) { + const tokensGroup = mapStage.findOne("#tokens"); + const notesGroup = mapStage.findOne("#notes"); + if (tokensGroup && notesGroup) { + let points: Vector2[] = []; + if (selection.type === "path") { + points = selection.data.points; + } else { + points.push({ x: selection.data.x, y: selection.data.y }); + points.push({ + x: selection.data.x + selection.data.width, + y: selection.data.y, + }); + points.push({ + x: selection.data.x + selection.data.width, + y: selection.data.y + selection.data.height, + }); + points.push({ + x: selection.data.x, + y: selection.data.y + selection.data.height, + }); + } + const intersection = new Intersection( + { + type: "path", + points: scaleAndFlattenPoints(points, { + x: mapWidth, + y: mapHeight, + }), + }, + { x: selection.x, y: selection.y }, + { x: 0, y: 0 }, + 0 + ); + + let intersectingItems: SelectionItem[] = []; + + const tokens = tokensGroup.children; + if (tokens) { + for (let token of tokens) { + if (intersection.intersects(token.position())) { + intersectingItems.push({ type: "token", id: token.id() }); + } + } + } + const notes = notesGroup.children; + if (notes) { + for (let note of notes) { + if (intersection.intersects(note.position())) { + intersectingItems.push({ type: "note", id: note.id() }); + } + } + } + + if (intersectingItems.length > 0) { + setSelection((prevSelection) => { + if (!prevSelection) { + return prevSelection; + } + return { ...prevSelection, items: intersectingItems }; + }); + } else { + setSelection(null); + } + } else { + setSelection(null); + } + } else { + setSelection(null); + } + setIsBrushDown(false); } @@ -149,47 +234,17 @@ function SelectTool({ active, toolSettings }: MapSelectProps) { }; }); - function renderSelection(selection: Selection) { - const strokeWidth = gridStrokeWidth / stageScale; - const defaultProps = { - stroke: colors.primary, - strokeWidth: strokeWidth, - dash: [strokeWidth / 2, strokeWidth * 2], - }; - if (selection.type === "path") { - return ( - [ - ...acc, - point.x * mapWidth, - point.y * mapHeight, - ], - [] - )} - tension={0.5} - closed={false} - lineCap="round" - lineJoin="round" - {...defaultProps} + return ( + + {selection && ( + - ); - } else if (selection.type === "rectangle") { - return ( - - ); - } - } - - return {selection && renderSelection(selection)}; + )} + + ); } export default SelectTool; diff --git a/src/hooks/useMapTokens.tsx b/src/hooks/useMapTokens.tsx index 03daf88..e500537 100644 --- a/src/hooks/useMapTokens.tsx +++ b/src/hooks/useMapTokens.tsx @@ -79,7 +79,7 @@ function useMapTokens( } const tokens = map && mapState && ( - + {Object.values(mapState.tokens) .sort((a, b) => sortMapTokenStates(a, b, tokenDraggingOptions)) .map((tokenState) => ( diff --git a/src/network/NetworkedMapAndTokens.tsx b/src/network/NetworkedMapAndTokens.tsx index a7e6246..702a0ec 100644 --- a/src/network/NetworkedMapAndTokens.tsx +++ b/src/network/NetworkedMapAndTokens.tsx @@ -308,6 +308,28 @@ function NetworkedMapAndTokens({ session }: { session: Session }) { addActions([{ type: "tokens", action }]); } + function handleSelectionItemsChange( + tokenChanges: Record>, + noteChanges: Record> + ) { + let tokenEdits: Partial[] = []; + for (let id in tokenChanges) { + tokenEdits.push({ ...tokenChanges[id], id }); + } + const tokenAction = new EditStatesAction(tokenEdits); + + let noteEdits: Partial[] = []; + for (let id in noteChanges) { + noteEdits.push({ ...noteChanges[id], id }); + } + const noteAction = new EditStatesAction(noteEdits); + + addActions([ + { type: "tokens", action: tokenAction }, + { type: "notes", action: noteAction }, + ]); + } + useEffect(() => { async function handlePeerData({ id, data, reply }: PeerDataEvent) { if (id === "assetRequest") { @@ -371,6 +393,7 @@ function NetworkedMapAndTokens({ session }: { session: Session }) { mapActions={mapActions} onMapTokenStateChange={handleMapTokenStateChange} onMapTokenStateRemove={handleMapTokenStateRemove} + onSelectionItemsChange={handleSelectionItemsChange} onMapChange={handleMapChange} onMapReset={handleMapReset} onMapDraw={handleMapDraw} diff --git a/src/types/Events.ts b/src/types/Events.ts index 832d58a..95661ff 100644 --- a/src/types/Events.ts +++ b/src/types/Events.ts @@ -58,3 +58,8 @@ export type StreamEndEventHandler = (stream: MediaStream) => void; export type TimerStartEventHandler = (event: Timer) => void; export type TimerStopEventHandler = () => void; + +export type SelectionItemsChangeEventHandler = ( + tokenChanges: Record>, + noteChanges: Record> +) => void; diff --git a/src/types/Select.ts b/src/types/Select.ts index 61db77d..84b4275 100644 --- a/src/types/Select.ts +++ b/src/types/Select.ts @@ -1,4 +1,3 @@ -import Konva from "konva"; import { RectData, PointsData } from "./Drawing"; export type SelectToolType = "path" | "rectangle"; @@ -7,8 +6,17 @@ export type SelectToolSettings = { type: SelectToolType; }; +export type SelectionItemType = "token" | "note"; + +export type SelectionItem = { + type: SelectionItemType; + id: string; +}; + export type BaseSelection = { - nodes: Konva.Node[]; + items: SelectionItem[]; + x: number; + y: number; }; export type RectSelection = BaseSelection & {