Add basic selection movement

This commit is contained in:
Mitchell McCaffrey 2021-07-21 18:56:18 +10:00
parent 20f48f173e
commit 4cee11d5ea
9 changed files with 311 additions and 60 deletions

View File

@ -194,6 +194,7 @@ function Note({
return (
<animated.Group
{...props}
id={note.id}
onClick={handleClick}
onTap={handleClick}
width={noteWidth}

View 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;

View File

@ -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>

View File

@ -146,7 +146,7 @@ function NoteTool({
});
return (
<Group>
<Group id="notes">
{notes.map((note) => (
<Note
note={note}

View File

@ -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;

View File

@ -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) => (

View File

@ -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}

View File

@ -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;

View File

@ -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 & {