Add basic selection movement
This commit is contained in:
parent
20f48f173e
commit
4cee11d5ea
@ -194,6 +194,7 @@ function Note({
|
||||
return (
|
||||
<animated.Group
|
||||
{...props}
|
||||
id={note.id}
|
||||
onClick={handleClick}
|
||||
onTap={handleClick}
|
||||
width={noteWidth}
|
||||
|
155
src/components/konva/Selection.tsx
Normal file
155
src/components/konva/Selection.tsx
Normal file
@ -0,0 +1,155 @@
|
||||
import Konva from "konva";
|
||||
import { Line, Rect } from "react-konva";
|
||||
|
||||
import colors from "../../helpers/colors";
|
||||
import { scaleAndFlattenPoints } from "../../helpers/konva";
|
||||
|
||||
import { useGridStrokeWidth } from "../../contexts/GridContext";
|
||||
import {
|
||||
useDebouncedStageScale,
|
||||
useMapHeight,
|
||||
useMapWidth,
|
||||
} from "../../contexts/MapInteractionContext";
|
||||
import { useUserId } from "../../contexts/UserIdContext";
|
||||
|
||||
import {
|
||||
Selection as SelectionType,
|
||||
SelectionItemType,
|
||||
} from "../../types/Select";
|
||||
import { useRef } from "react";
|
||||
import Vector2 from "../../helpers/Vector2";
|
||||
import { SelectionItemsChangeEventHandler } from "../../types/Events";
|
||||
import { TokenState } from "../../types/TokenState";
|
||||
import { Note } from "../../types/Note";
|
||||
|
||||
type SelectionProps = {
|
||||
selection: SelectionType;
|
||||
onSelectionChange: (selection: SelectionType | null) => 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<DragEvent>) {
|
||||
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<DragEvent>) {
|
||||
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<DragEvent>) {
|
||||
const tokenChanges: Record<string, Partial<TokenState>> = {};
|
||||
const noteChanges: Record<string, Partial<Note>> = {};
|
||||
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 (
|
||||
<Line
|
||||
points={scaleAndFlattenPoints(selection.data.points, {
|
||||
x: mapWidth,
|
||||
y: mapHeight,
|
||||
})}
|
||||
tension={0.5}
|
||||
closed={selection.items.length > 0}
|
||||
lineCap="round"
|
||||
lineJoin="round"
|
||||
x={x}
|
||||
y={y}
|
||||
{...defaultProps}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Rect
|
||||
x={x}
|
||||
y={y}
|
||||
offsetX={-selection.data.x * mapWidth}
|
||||
offsetY={-selection.data.y * mapHeight}
|
||||
width={selection.data.width * mapWidth}
|
||||
height={selection.data.height * mapHeight}
|
||||
lineCap="round"
|
||||
lineJoin="round"
|
||||
{...defaultProps}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Selection;
|
@ -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<DrawingState>) => void;
|
||||
@ -69,6 +71,7 @@ function Map({
|
||||
mapActions,
|
||||
onMapTokenStateChange,
|
||||
onMapTokenStateRemove,
|
||||
onSelectionItemsChange,
|
||||
onMapChange,
|
||||
onMapReset,
|
||||
onMapDraw,
|
||||
@ -211,6 +214,7 @@ function Map({
|
||||
<SelectTool
|
||||
active={selectedToolId === "select"}
|
||||
toolSettings={settings.select}
|
||||
onSelectionItemsChange={onSelectionItemsChange}
|
||||
/>
|
||||
</MapInteraction>
|
||||
</Box>
|
||||
|
@ -146,7 +146,7 @@ function NoteTool({
|
||||
});
|
||||
|
||||
return (
|
||||
<Group>
|
||||
<Group id="notes">
|
||||
{notes.map((note) => (
|
||||
<Note
|
||||
note={note}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Group, Line, Rect } from "react-konva";
|
||||
import { Group } from "react-konva";
|
||||
|
||||
import {
|
||||
useDebouncedStageScale,
|
||||
@ -15,32 +15,43 @@ import {
|
||||
simplifyPoints,
|
||||
} from "../../helpers/drawing";
|
||||
import Vector2 from "../../helpers/Vector2";
|
||||
import colors from "../../helpers/colors";
|
||||
import { getRelativePointerPosition } from "../../helpers/konva";
|
||||
|
||||
import { Selection, SelectToolSettings } from "../../types/Select";
|
||||
import { RectData } from "../../types/Drawing";
|
||||
import {
|
||||
useGridCellNormalizedSize,
|
||||
useGridStrokeWidth,
|
||||
} from "../../contexts/GridContext";
|
||||
getRelativePointerPosition,
|
||||
scaleAndFlattenPoints,
|
||||
} from "../../helpers/konva";
|
||||
import { Intersection } from "../../helpers/token";
|
||||
|
||||
import {
|
||||
Selection as SelectionType,
|
||||
SelectionItem,
|
||||
SelectToolSettings,
|
||||
} from "../../types/Select";
|
||||
import { RectData } from "../../types/Drawing";
|
||||
import { useGridCellNormalizedSize } from "../../contexts/GridContext";
|
||||
import Konva from "konva";
|
||||
import Selection from "../konva/Selection";
|
||||
import { SelectionItemsChangeEventHandler } from "../../types/Events";
|
||||
|
||||
type MapSelectProps = {
|
||||
active: boolean;
|
||||
toolSettings: SelectToolSettings;
|
||||
onSelectionItemsChange: SelectionItemsChangeEventHandler;
|
||||
};
|
||||
|
||||
function SelectTool({ active, toolSettings }: MapSelectProps) {
|
||||
function SelectTool({
|
||||
active,
|
||||
toolSettings,
|
||||
onSelectionItemsChange,
|
||||
}: MapSelectProps) {
|
||||
const stageScale = useDebouncedStageScale();
|
||||
const mapWidth = useMapWidth();
|
||||
const mapHeight = useMapHeight();
|
||||
const interactionEmitter = useInteractionEmitter();
|
||||
|
||||
const gridCellNormalizedSize = useGridCellNormalizedSize();
|
||||
const gridStrokeWidth = useGridStrokeWidth();
|
||||
|
||||
const mapStageRef = useMapStage();
|
||||
const [selection, setSelection] = useState<Selection | null>(null);
|
||||
const [selection, setSelection] = useState<SelectionType | null>(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() {
|
||||
if (selection && mapStage) {
|
||||
const tokensGroup = mapStage.findOne<Konva.Group>("#tokens");
|
||||
const notesGroup = mapStage.findOne<Konva.Group>("#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 (
|
||||
<Line
|
||||
points={selection.data.points.reduce(
|
||||
(acc: number[], point) => [
|
||||
...acc,
|
||||
point.x * mapWidth,
|
||||
point.y * mapHeight,
|
||||
],
|
||||
[]
|
||||
<Group>
|
||||
{selection && (
|
||||
<Selection
|
||||
selection={selection}
|
||||
onSelectionChange={setSelection}
|
||||
onSelectionItemsChange={onSelectionItemsChange}
|
||||
/>
|
||||
)}
|
||||
tension={0.5}
|
||||
closed={false}
|
||||
lineCap="round"
|
||||
lineJoin="round"
|
||||
{...defaultProps}
|
||||
/>
|
||||
</Group>
|
||||
);
|
||||
} else if (selection.type === "rectangle") {
|
||||
return (
|
||||
<Rect
|
||||
x={selection.data.x * mapWidth}
|
||||
y={selection.data.y * mapHeight}
|
||||
width={selection.data.width * mapWidth}
|
||||
height={selection.data.height * mapHeight}
|
||||
lineCap="round"
|
||||
lineJoin="round"
|
||||
{...defaultProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return <Group>{selection && renderSelection(selection)}</Group>;
|
||||
}
|
||||
|
||||
export default SelectTool;
|
||||
|
@ -79,7 +79,7 @@ function useMapTokens(
|
||||
}
|
||||
|
||||
const tokens = map && mapState && (
|
||||
<Group>
|
||||
<Group id="tokens">
|
||||
{Object.values(mapState.tokens)
|
||||
.sort((a, b) => sortMapTokenStates(a, b, tokenDraggingOptions))
|
||||
.map((tokenState) => (
|
||||
|
@ -308,6 +308,28 @@ function NetworkedMapAndTokens({ session }: { session: Session }) {
|
||||
addActions([{ type: "tokens", action }]);
|
||||
}
|
||||
|
||||
function handleSelectionItemsChange(
|
||||
tokenChanges: Record<string, Partial<TokenState>>,
|
||||
noteChanges: Record<string, Partial<Note>>
|
||||
) {
|
||||
let tokenEdits: Partial<TokenState>[] = [];
|
||||
for (let id in tokenChanges) {
|
||||
tokenEdits.push({ ...tokenChanges[id], id });
|
||||
}
|
||||
const tokenAction = new EditStatesAction(tokenEdits);
|
||||
|
||||
let noteEdits: Partial<Note>[] = [];
|
||||
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}
|
||||
|
@ -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<string, Partial<TokenState>>,
|
||||
noteChanges: Record<string, Partial<Note>>
|
||||
) => void;
|
||||
|
@ -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 & {
|
||||
|
Loading…
Reference in New Issue
Block a user