diff --git a/.env.production b/.env.production
index 6709c31..af31a79 100644
--- a/.env.production
+++ b/.env.production
@@ -1,5 +1,5 @@
-REACT_APP_BROKER_URL=https://prod.owlbear.rodeo
-REACT_APP_ICE_SERVERS_URL=https://prod.owlbear.rodeo/iceservers
+REACT_APP_BROKER_URL=https://stage.owlbear.rodeo
+REACT_APP_ICE_SERVERS_URL=https://stage.owlbear.rodeo/iceservers
REACT_APP_STRIPE_API_KEY=pk_live_MJjzi5djj524Y7h3fL5PNh4e00a852XD51
REACT_APP_STRIPE_URL=https://payment.owlbear.rodeo
REACT_APP_VERSION=$npm_package_version
diff --git a/package.json b/package.json
index ec2ec96..21e4591 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "owlbear-rodeo",
- "version": "1.10.0.2",
+ "version": "1.10.1",
"private": true,
"dependencies": {
"@babylonjs/core": "^4.2.0",
@@ -67,6 +67,7 @@
"socket.io-msgpack-parser": "^3.0.1",
"source-map-explorer": "^2.5.2",
"theme-ui": "^0.10.0",
+ "tiny-typed-emitter": "^2.1.0",
"use-image": "^1.0.8",
"uuid": "^8.3.2",
"webrtc-adapter": "^8.1.0"
diff --git a/src/components/konva/BlankNote.tsx b/src/components/konva/BlankNote.tsx
new file mode 100644
index 0000000..f4a78e1
--- /dev/null
+++ b/src/components/konva/BlankNote.tsx
@@ -0,0 +1,47 @@
+import { Rect } from "react-konva";
+
+import {
+ useMapWidth,
+ useMapHeight,
+} from "../../contexts/MapInteractionContext";
+import { useGridCellPixelSize } from "../../contexts/GridContext";
+
+import colors from "../../helpers/colors";
+
+import { Note as NoteType } from "../../types/Note";
+
+type NoteProps = {
+ note: NoteType;
+};
+
+function BlankNote({ note }: NoteProps) {
+ const mapWidth = useMapWidth();
+ const mapHeight = useMapHeight();
+
+ const gridCellPixelSize = useGridCellPixelSize();
+
+ const minCellSize = Math.min(
+ gridCellPixelSize.width,
+ gridCellPixelSize.height
+ );
+ const noteWidth = minCellSize * note.size;
+ const noteHeight = noteWidth;
+
+ return (
+
+ );
+}
+
+export default BlankNote;
diff --git a/src/components/konva/Note.tsx b/src/components/konva/Note.tsx
index e961854..c73a6cc 100644
--- a/src/components/konva/Note.tsx
+++ b/src/components/konva/Note.tsx
@@ -8,6 +8,7 @@ import {
useSetPreventMapInteraction,
useMapWidth,
useMapHeight,
+ leftMouseButton,
} from "../../contexts/MapInteractionContext";
import { useGridCellPixelSize } from "../../contexts/GridContext";
@@ -102,6 +103,9 @@ function Note({
}
function handleClick(event: Konva.KonvaEventObject) {
+ if (!leftMouseButton(event)) {
+ return;
+ }
if (draggable) {
const noteNode = event.target;
onNoteMenuOpen && onNoteMenuOpen(note.id, noteNode, true);
@@ -111,6 +115,9 @@ function Note({
// Store note pointer down time to check for a click when note is locked
const notePointerDownTimeRef = useRef(0);
function handlePointerDown(event: Konva.KonvaEventObject) {
+ if (!leftMouseButton(event)) {
+ return;
+ }
if (draggable) {
setPreventMapInteraction(true);
}
@@ -120,6 +127,9 @@ function Note({
}
function handlePointerUp(event: Konva.KonvaEventObject) {
+ if (!leftMouseButton(event)) {
+ return;
+ }
if (draggable) {
setPreventMapInteraction(false);
}
diff --git a/src/components/konva/Token.tsx b/src/components/konva/Token.tsx
index 5695a03..5f44556 100644
--- a/src/components/konva/Token.tsx
+++ b/src/components/konva/Token.tsx
@@ -12,6 +12,7 @@ import {
useSetPreventMapInteraction,
useMapWidth,
useMapHeight,
+ leftMouseButton,
} from "../../contexts/MapInteractionContext";
import { useGridCellPixelSize } from "../../contexts/GridContext";
import { useDataURL } from "../../contexts/AssetsContext";
@@ -197,7 +198,10 @@ function Token({
setAttachmentOverCharacter(false);
}
- function handleClick() {
+ function handleClick(event: Konva.KonvaEventObject) {
+ if (!leftMouseButton(event)) {
+ return;
+ }
if (selectable && draggable && transformRootRef.current) {
onTokenMenuOpen(tokenState.id, transformRootRef.current, true);
}
@@ -207,6 +211,9 @@ function Token({
// Store token pointer down time to check for a click when token is locked
const tokenPointerDownTimeRef = useRef(0);
function handlePointerDown(event: Konva.KonvaEventObject) {
+ if (!leftMouseButton(event)) {
+ return;
+ }
if (draggable) {
setPreventMapInteraction(true);
}
@@ -216,6 +223,9 @@ function Token({
}
function handlePointerUp(event: Konva.KonvaEventObject) {
+ if (!leftMouseButton(event)) {
+ return;
+ }
if (draggable) {
setPreventMapInteraction(false);
}
diff --git a/src/components/map/MapInteraction.tsx b/src/components/map/MapInteraction.tsx
index 031a761..001b1f4 100644
--- a/src/components/map/MapInteraction.tsx
+++ b/src/components/map/MapInteraction.tsx
@@ -3,17 +3,20 @@ import { Box } from "theme-ui";
import ReactResizeDetector from "react-resize-detector";
import { Stage, Layer, Image, Group } from "react-konva";
import Konva from "konva";
-import { EventEmitter } from "events";
import useMapImage from "../../hooks/useMapImage";
import usePreventOverscroll from "../../hooks/usePreventOverscroll";
import useStageInteraction from "../../hooks/useStageInteraction";
import useImageCenter from "../../hooks/useImageCenter";
+import usePreventContextMenu from "../../hooks/usePreventContextMenu";
import { getGridMaxZoom } from "../../helpers/grid";
import KonvaBridge from "../../helpers/KonvaBridge";
-import { MapInteractionProvider } from "../../contexts/MapInteractionContext";
+import {
+ MapInteractionEmitter,
+ MapInteractionProvider,
+} from "../../contexts/MapInteractionContext";
import { useMapStage } from "../../contexts/MapStageContext";
import { GridProvider } from "../../contexts/GridContext";
import { useKeyboard } from "../../contexts/KeyboardContext";
@@ -72,6 +75,7 @@ function MapInteraction({
const containerRef = useRef(null);
usePreventOverscroll(containerRef);
+ usePreventContextMenu(containerRef);
const [mapWidth, mapHeight] = useImageCenter(
map,
@@ -85,8 +89,9 @@ function MapInteraction({
);
const previousSelectedToolRef = useRef(selectedToolId);
+ const [currentMouseButtons, setCurentMouseButtons] = useState(0);
- const [interactionEmitter] = useState(new EventEmitter());
+ const [interactionEmitter] = useState(new MapInteractionEmitter());
useStageInteraction(
mapStageRef,
@@ -106,13 +111,17 @@ function MapInteraction({
onPinchEnd: () => {
onSelectedToolChange(previousSelectedToolRef.current);
},
- onDrag: ({ first, last }) => {
+ onDrag: (props) => {
+ const { first, last, buttons } = props;
+ if (buttons !== currentMouseButtons) {
+ setCurentMouseButtons(buttons);
+ }
if (first) {
- interactionEmitter.emit("dragStart");
+ interactionEmitter.emit("dragStart", props);
} else if (last) {
- interactionEmitter.emit("dragEnd");
+ interactionEmitter.emit("dragEnd", props);
} else {
- interactionEmitter.emit("drag");
+ interactionEmitter.emit("drag", props);
}
},
}
@@ -143,6 +152,11 @@ function MapInteraction({
useKeyboard(handleKeyDown, handleKeyUp);
function getCursorForTool(tool: MapToolId) {
+ if (currentMouseButtons === 2) {
+ return "crosshair";
+ } else if (currentMouseButtons > 2) {
+ return "move";
+ }
switch (tool) {
case "move":
return "move";
diff --git a/src/components/tools/DrawingTool.tsx b/src/components/tools/DrawingTool.tsx
index f155da4..fa529cd 100644
--- a/src/components/tools/DrawingTool.tsx
+++ b/src/components/tools/DrawingTool.tsx
@@ -7,6 +7,8 @@ import {
useMapWidth,
useMapHeight,
useInteractionEmitter,
+ leftMouseButton,
+ MapDragEvent,
} from "../../contexts/MapInteractionContext";
import { useMapStage } from "../../contexts/MapStageContext";
import {
@@ -103,7 +105,10 @@ function DrawingTool({
});
}
- function handleBrushDown() {
+ function handleBrushDown(props: MapDragEvent) {
+ if (!leftMouseButton(props)) {
+ return;
+ }
const brushPosition = getBrushPosition();
if (!brushPosition) {
return;
@@ -135,7 +140,10 @@ function DrawingTool({
setIsBrushDown(true);
}
- function handleBrushMove() {
+ function handleBrushMove(props: MapDragEvent) {
+ if (!leftMouseButton(props)) {
+ return;
+ }
const brushPosition = getBrushPosition();
if (!brushPosition) {
return;
@@ -186,7 +194,10 @@ function DrawingTool({
}
}
- function handleBrushUp() {
+ function handleBrushUp(props: MapDragEvent) {
+ if (!leftMouseButton(props)) {
+ return;
+ }
if (isBrush && drawing && drawing.type === "path") {
if (drawing.data.points.length > 1) {
onShapeAdd(drawing);
diff --git a/src/components/tools/FogTool.tsx b/src/components/tools/FogTool.tsx
index 16a760a..94494e8 100644
--- a/src/components/tools/FogTool.tsx
+++ b/src/components/tools/FogTool.tsx
@@ -3,6 +3,7 @@ import shortid from "shortid";
import { Group, Line } from "react-konva";
import useImage from "use-image";
import Color from "color";
+import Konva from "konva";
import diagonalPattern from "../../images/DiagonalPattern.png";
@@ -11,6 +12,8 @@ import {
useMapWidth,
useMapHeight,
useInteractionEmitter,
+ MapDragEvent,
+ leftMouseButton,
} from "../../contexts/MapInteractionContext";
import { useMapStage } from "../../contexts/MapStageContext";
import {
@@ -104,7 +107,7 @@ function FogTool({
const [drawingShape, setDrawingShape] = useState(null);
const [isBrushDown, setIsBrushDown] = useState(false);
- const [editingShapes, setEditingShapes] = useState([]);
+ const [hoveredShapes, setHoveredShapes] = useState([]);
// Shapes that have been merged for fog
const [fogShapes, setFogShapes] = useState(shapes);
@@ -160,7 +163,10 @@ function FogTool({
});
}
- function handleBrushDown() {
+ function handleBrushDown(props: MapDragEvent) {
+ if (!leftMouseButton(props)) {
+ return;
+ }
if (toolSettings.type === "brush") {
const brushPosition = getBrushPosition();
if (!brushPosition) {
@@ -203,7 +209,10 @@ function FogTool({
setIsBrushDown(true);
}
- function handleBrushMove() {
+ function handleBrushMove(props: MapDragEvent) {
+ if (!leftMouseButton(props)) {
+ return;
+ }
if (toolSettings.type === "brush" && isBrushDown && drawingShape) {
const brushPosition = getBrushPosition();
if (!brushPosition) {
@@ -258,7 +267,10 @@ function FogTool({
}
}
- function handleBrushUp() {
+ function handleBrushUp(props: MapDragEvent) {
+ if (!leftMouseButton(props)) {
+ return;
+ }
if (
(toolSettings.type === "brush" || toolSettings.type === "rectangle") &&
drawingShape
@@ -318,7 +330,10 @@ function FogTool({
setIsBrushDown(false);
}
- function handlePointerClick() {
+ function handlePointerClick(event: Konva.KonvaEventObject) {
+ if (!leftMouseButton(event)) {
+ return;
+ }
if (toolSettings.type === "polygon") {
const brushPosition = getBrushPosition();
if (brushPosition) {
@@ -553,25 +568,25 @@ function FogTool({
function eraseHoveredShapes() {
// Erase
- if (editingShapes.length > 0) {
+ if (hoveredShapes.length > 0) {
if (toolSettings.type === "remove") {
- onShapesRemove(editingShapes.map((shape) => shape.id));
+ onShapesRemove(hoveredShapes.map((shape) => shape.id));
} else if (toolSettings.type === "toggle") {
onShapesEdit(
- editingShapes.map((shape) => ({
+ hoveredShapes.map((shape) => ({
id: shape.id,
visible: !shape.visible,
}))
);
}
- setEditingShapes([]);
+ setHoveredShapes([]);
}
}
function handleShapeOver(shape: Fog, isDown: boolean) {
if (shouldHover && isDown) {
- if (editingShapes.findIndex((s) => s.id === shape.id) === -1) {
- setEditingShapes((prevShapes) => [...prevShapes, shape]);
+ if (hoveredShapes.findIndex((s) => s.id === shape.id) === -1) {
+ setHoveredShapes((prevShapes) => [...prevShapes, shape]);
}
}
}
@@ -609,7 +624,7 @@ function FogTool({
);
}
- function renderEditingShape(shape: Fog) {
+ function renderHoveredShape(shape: Fog) {
const editingShape: Fog = {
...shape,
color: "primary",
@@ -617,6 +632,27 @@ function FogTool({
return renderShape(editingShape);
}
+ function renderDrawingShape(shape: Fog) {
+ const opacity = editable ? editOpacity : 1;
+ const stroke =
+ editable && active
+ ? colors.lightGray
+ : colors[shape.color] || shape.color;
+ const fill = new Color(colors[shape.color] || shape.color)
+ .alpha(opacity)
+ .string();
+ return (
+ {}}
+ />
+ );
+ }
+
function renderPolygonAcceptTick(shape: Fog) {
if (shape.data.points.length === 0) {
return null;
@@ -681,12 +717,12 @@ function FogTool({
{fogShapes.map(renderShape)}
{shouldRenderGuides && renderGuides()}
- {drawingShape && renderShape(drawingShape)}
+ {drawingShape && renderDrawingShape(drawingShape)}
{drawingShape &&
toolSettings &&
toolSettings.type === "polygon" &&
renderPolygonAcceptTick(drawingShape)}
- {editingShapes.length > 0 && editingShapes.map(renderEditingShape)}
+ {hoveredShapes.length > 0 && hoveredShapes.map(renderHoveredShape)}
);
}
diff --git a/src/components/tools/MeasureTool.tsx b/src/components/tools/MeasureTool.tsx
index 7e352b3..d57f851 100644
--- a/src/components/tools/MeasureTool.tsx
+++ b/src/components/tools/MeasureTool.tsx
@@ -1,7 +1,11 @@
import { useState, useEffect } from "react";
import { Group } from "react-konva";
-import { useInteractionEmitter } from "../../contexts/MapInteractionContext";
+import {
+ useInteractionEmitter,
+ MapDragEvent,
+ leftMouseButton,
+} from "../../contexts/MapInteractionContext";
import { useMapStage } from "../../contexts/MapStageContext";
import {
useGrid,
@@ -40,8 +44,9 @@ function MeasureTool({ map, active }: MapMeasureProps) {
const gridOffset = useGridOffset();
const mapStageRef = useMapStage();
- const [drawingShapeData, setDrawingShapeData] =
- useState(null);
+ const [drawingShapeData, setDrawingShapeData] = useState(
+ null
+ );
const [isBrushDown, setIsBrushDown] = useState(false);
const gridScale = parseGridScale(active ? grid.measurement.scale : null);
@@ -75,7 +80,10 @@ function MeasureTool({ map, active }: MapMeasureProps) {
});
}
- function handleBrushDown() {
+ function handleBrushDown(props: MapDragEvent) {
+ if (!leftMouseButton(props)) {
+ return;
+ }
const brushPosition = getBrushPosition();
if (!brushPosition) {
return;
@@ -89,7 +97,10 @@ function MeasureTool({ map, active }: MapMeasureProps) {
setIsBrushDown(true);
}
- function handleBrushMove() {
+ function handleBrushMove(props: MapDragEvent) {
+ if (!leftMouseButton(props)) {
+ return;
+ }
const brushPosition = getBrushPosition();
if (isBrushDown && drawingShapeData && brushPosition && mapImage) {
const { points } = getUpdatedShapeData(
@@ -123,7 +134,10 @@ function MeasureTool({ map, active }: MapMeasureProps) {
}
}
- function handleBrushUp() {
+ function handleBrushUp(props: MapDragEvent) {
+ if (!leftMouseButton(props)) {
+ return;
+ }
setDrawingShapeData(null);
setIsBrushDown(false);
}
diff --git a/src/components/tools/NoteTool.tsx b/src/components/tools/NoteTool.tsx
index 98195d8..b9455a0 100644
--- a/src/components/tools/NoteTool.tsx
+++ b/src/components/tools/NoteTool.tsx
@@ -3,7 +3,11 @@ import shortid from "shortid";
import { Group } from "react-konva";
import Konva from "konva";
-import { useInteractionEmitter } from "../../contexts/MapInteractionContext";
+import {
+ useInteractionEmitter,
+ MapDragEvent,
+ leftMouseButton,
+} from "../../contexts/MapInteractionContext";
import { useMapStage } from "../../contexts/MapStageContext";
import { useUserId } from "../../contexts/UserIdContext";
@@ -12,7 +16,7 @@ import { getRelativePointerPosition } from "../../helpers/konva";
import useGridSnapping from "../../hooks/useGridSnapping";
-import Note from "../konva/Note";
+import BlankNote from "../konva/BlankNote";
import { Map } from "../../types/Map";
import { Note as NoteType } from "../../types/Note";
@@ -28,20 +32,12 @@ type MapNoteProps = {
active: boolean;
onNoteCreate: NoteCreateEventHander;
onNoteMenuOpen: NoteMenuOpenEventHandler;
- children: React.ReactNode;
};
-function NoteTool({
- map,
- active,
- onNoteCreate,
- onNoteMenuOpen,
- children,
-}: MapNoteProps) {
+function NoteTool({ map, active, onNoteCreate, onNoteMenuOpen }: MapNoteProps) {
const interactionEmitter = useInteractionEmitter();
const userId = useUserId();
const mapStageRef = useMapStage();
- const [isBrushDown, setIsBrushDown] = useState(false);
const [noteData, setNoteData] = useState(null);
const creatingNoteRef = useRef(null);
@@ -72,7 +68,10 @@ function NoteTool({
});
}
- function handleBrushDown() {
+ function handleBrushDown(props: MapDragEvent) {
+ if (!leftMouseButton(props)) {
+ return;
+ }
const brushPosition = getBrushPosition();
if (!brushPosition || !userId) {
return;
@@ -91,10 +90,15 @@ function NoteTool({
textOnly: false,
rotation: 0,
});
- setIsBrushDown(true);
+ if (creatingNoteRef.current) {
+ creatingNoteRef.current.visible(true);
+ }
}
- function handleBrushMove() {
+ function handleBrushMove(props: MapDragEvent) {
+ if (!leftMouseButton(props)) {
+ return;
+ }
if (noteData) {
const brushPosition = getBrushPosition();
if (!brushPosition) {
@@ -110,17 +114,20 @@ function NoteTool({
y: brushPosition.y,
};
});
- setIsBrushDown(true);
}
}
- function handleBrushUp() {
+ function handleBrushUp(props: MapDragEvent) {
+ if (!leftMouseButton(props)) {
+ return;
+ }
if (noteData && creatingNoteRef.current) {
onNoteCreate([noteData]);
onNoteMenuOpen(noteData.id, creatingNoteRef.current, true);
+ // Hide creating note tool here as settings noteData to null
+ // was causing performance issues in FireFox
+ creatingNoteRef.current.visible(false);
}
- setNoteData(null);
- setIsBrushDown(false);
}
interactionEmitter?.on("dragStart", handleBrushDown);
@@ -135,13 +142,8 @@ function NoteTool({
});
return (
-
- {children}
-
- {isBrushDown && noteData && (
-
- )}
-
+
+ {noteData && }
);
}
diff --git a/src/components/tools/PointerTool.tsx b/src/components/tools/PointerTool.tsx
index de30bf0..63a3cb2 100644
--- a/src/components/tools/PointerTool.tsx
+++ b/src/components/tools/PointerTool.tsx
@@ -1,10 +1,13 @@
-import { useEffect } from "react";
+import { useEffect, useRef } from "react";
import { Group } from "react-konva";
import {
useMapWidth,
useMapHeight,
useInteractionEmitter,
+ MapDragEvent,
+ leftMouseButton,
+ rightMouseButton,
} from "../../contexts/MapInteractionContext";
import { useMapStage } from "../../contexts/MapStageContext";
import { useGridStrokeWidth } from "../../contexts/GridContext";
@@ -41,11 +44,9 @@ function PointerTool({
const gridStrokeWidth = useGridStrokeWidth();
const mapStageRef = useMapStage();
- useEffect(() => {
- if (!active) {
- return;
- }
+ const brushDownRef = useRef(false);
+ useEffect(() => {
const mapStage = mapStageRef.current;
function getBrushPosition() {
@@ -56,19 +57,27 @@ function PointerTool({
return getRelativePointerPositionNormalized(mapImage);
}
- function handleBrushDown() {
- const brushPosition = getBrushPosition();
- brushPosition && onPointerDown?.(brushPosition);
+ function handleBrushDown(props: MapDragEvent) {
+ if ((leftMouseButton(props) && active) || rightMouseButton(props)) {
+ const brushPosition = getBrushPosition();
+ brushPosition && onPointerDown?.(brushPosition);
+ brushDownRef.current = true;
+ }
}
function handleBrushMove() {
- const brushPosition = getBrushPosition();
- brushPosition && visible && onPointerMove?.(brushPosition);
+ if (brushDownRef.current) {
+ const brushPosition = getBrushPosition();
+ brushPosition && visible && onPointerMove?.(brushPosition);
+ }
}
function handleBrushUp() {
- const brushPosition = getBrushPosition();
- brushPosition && onPointerUp?.(brushPosition);
+ if (brushDownRef.current) {
+ const brushPosition = getBrushPosition();
+ brushPosition && onPointerUp?.(brushPosition);
+ brushDownRef.current = false;
+ }
}
interactionEmitter?.on("dragStart", handleBrushDown);
diff --git a/src/components/tools/SelectTool.tsx b/src/components/tools/SelectTool.tsx
index 2a29ca8..2c9be79 100644
--- a/src/components/tools/SelectTool.tsx
+++ b/src/components/tools/SelectTool.tsx
@@ -7,6 +7,8 @@ import {
useMapWidth,
useMapHeight,
useInteractionEmitter,
+ MapDragEvent,
+ leftMouseButton,
} from "../../contexts/MapInteractionContext";
import { useMapStage } from "../../contexts/MapStageContext";
@@ -96,7 +98,10 @@ function SelectTool({
});
}
- function handleBrushDown() {
+ function handleBrushDown(props: MapDragEvent) {
+ if (!leftMouseButton(props)) {
+ return;
+ }
const brushPosition = getBrushPosition();
if (!brushPosition || preventSelectionRef.current) {
return;
@@ -121,7 +126,10 @@ function SelectTool({
setIsBrushDown(true);
}
- function handleBrushMove() {
+ function handleBrushMove(props: MapDragEvent) {
+ if (!leftMouseButton(props)) {
+ return;
+ }
const brushPosition = getBrushPosition();
if (!brushPosition || preventSelectionRef.current) {
return;
@@ -172,7 +180,10 @@ function SelectTool({
}
}
- function handleBrushUp() {
+ function handleBrushUp(props: MapDragEvent) {
+ if (!leftMouseButton(props)) {
+ return;
+ }
if (preventSelectionRef.current) {
return;
}
diff --git a/src/contexts/MapInteractionContext.tsx b/src/contexts/MapInteractionContext.tsx
index 7362c86..6009e20 100644
--- a/src/contexts/MapInteractionContext.tsx
+++ b/src/contexts/MapInteractionContext.tsx
@@ -1,6 +1,22 @@
import React, { useContext } from "react";
-import { EventEmitter } from "stream";
+import { FullGestureState } from "react-use-gesture/dist/types";
import useDebounce from "../hooks/useDebounce";
+import { TypedEmitter } from "tiny-typed-emitter";
+import Konva from "konva";
+
+export type MapDragEvent = Omit, "event"> & {
+ event: React.PointerEvent | PointerEvent;
+};
+
+export type MapDragEventHandler = (props: MapDragEvent) => void;
+
+export interface MapInteractionEvents {
+ dragStart: MapDragEventHandler;
+ drag: MapDragEventHandler;
+ dragEnd: MapDragEventHandler;
+}
+
+export class MapInteractionEmitter extends TypedEmitter {}
type MapInteraction = {
stageScale: number;
@@ -9,29 +25,33 @@ type MapInteraction = {
setPreventMapInteraction: React.Dispatch>;
mapWidth: number;
mapHeight: number;
- interactionEmitter: EventEmitter | null;
+ interactionEmitter: MapInteractionEmitter | null;
};
-export const StageScaleContext =
- React.createContext(undefined);
-export const DebouncedStageScaleContext =
- React.createContext(undefined);
-export const StageWidthContext =
- React.createContext(undefined);
-export const StageHeightContext =
- React.createContext(undefined);
-export const SetPreventMapInteractionContext =
- React.createContext(
- undefined
- );
-export const MapWidthContext =
- React.createContext(undefined);
-export const MapHeightContext =
- React.createContext(undefined);
-export const InteractionEmitterContext =
- React.createContext(
- undefined
- );
+export const StageScaleContext = React.createContext<
+ MapInteraction["stageScale"] | undefined
+>(undefined);
+export const DebouncedStageScaleContext = React.createContext<
+ MapInteraction["stageScale"] | undefined
+>(undefined);
+export const StageWidthContext = React.createContext<
+ MapInteraction["stageWidth"] | undefined
+>(undefined);
+export const StageHeightContext = React.createContext<
+ MapInteraction["stageHeight"] | undefined
+>(undefined);
+export const SetPreventMapInteractionContext = React.createContext<
+ MapInteraction["setPreventMapInteraction"] | undefined
+>(undefined);
+export const MapWidthContext = React.createContext<
+ MapInteraction["mapWidth"] | undefined
+>(undefined);
+export const MapHeightContext = React.createContext<
+ MapInteraction["mapHeight"] | undefined
+>(undefined);
+export const InteractionEmitterContext = React.createContext<
+ MapInteraction["interactionEmitter"] | undefined
+>(undefined);
export function MapInteractionProvider({
value,
@@ -152,3 +172,30 @@ export function useDebouncedStageScale() {
}
return context;
}
+
+export function leftMouseButton(event: MapDragEvent): boolean;
+export function leftMouseButton(
+ event: Konva.KonvaEventObject
+): boolean;
+export function leftMouseButton(
+ event: Konva.KonvaEventObject
+): boolean;
+
+export function leftMouseButton(event: any) {
+ if (event.evt) {
+ // Konva events
+ // Check for undefined (touch) and mouse left click (0)
+ return event.evt.button === undefined || event.evt.button === 0;
+ } else {
+ // Drag event
+ return event.buttons <= 1;
+ }
+}
+
+export function middleMouseButton(event: MapDragEvent) {
+ return event.buttons === 4;
+}
+
+export function rightMouseButton(event: MapDragEvent) {
+ return event.buttons === 2;
+}
diff --git a/src/contexts/PartyContext.tsx b/src/contexts/PartyContext.tsx
index e8ffc9d..370610e 100644
--- a/src/contexts/PartyContext.tsx
+++ b/src/contexts/PartyContext.tsx
@@ -23,10 +23,10 @@ export function PartyProvider({ session, children }: PartyProviderProps) {
}
}
- session.socket?.on("party_state", handleSocketPartyState);
+ session.socket.on("party_state", handleSocketPartyState);
return () => {
- session.socket?.off("party_state", handleSocketPartyState);
+ session.socket.off("party_state", handleSocketPartyState);
};
});
diff --git a/src/contexts/PlayerContext.tsx b/src/contexts/PlayerContext.tsx
index ef19607..ea4a867 100644
--- a/src/contexts/PlayerContext.tsx
+++ b/src/contexts/PlayerContext.tsx
@@ -9,10 +9,12 @@ import useNetworkedState, {
import Session, { SessionStatus } from "../network/Session";
import { PlayerState } from "../types/PlayerState";
-export const PlayerStateContext =
- React.createContext(undefined);
-export const PlayerUpdaterContext =
- React.createContext | undefined>(undefined);
+export const PlayerStateContext = React.createContext(
+ undefined
+);
+export const PlayerUpdaterContext = React.createContext<
+ SetNetworkedState | undefined
+>(undefined);
type PlayerProviderProps = {
session: Session;
@@ -104,13 +106,13 @@ export function PlayerProvider({ session, children }: PlayerProviderProps) {
}
session.on("status", handleSocketStatus);
- session.socket?.on("connect", handleSocketConnect);
- session.socket?.io.on("reconnect", handleSocketConnect);
+ session.socket.on("connect", handleSocketConnect);
+ session.socket.io.on("reconnect", handleSocketConnect);
return () => {
session.off("status", handleSocketStatus);
- session.socket?.off("connect", handleSocketConnect);
- session.socket?.io.off("reconnect", handleSocketConnect);
+ session.socket.off("connect", handleSocketConnect);
+ session.socket.io.off("reconnect", handleSocketConnect);
};
});
diff --git a/src/docs/howTo/shortcuts.md b/src/docs/howTo/shortcuts.md
index b7c0a48..f963c50 100644
--- a/src/docs/howTo/shortcuts.md
+++ b/src/docs/howTo/shortcuts.md
@@ -1,21 +1,23 @@
## General
-| Shortcut | Description |
-| ---------------- | ----------------------- |
-| W | Move Tool |
-| Space Bar (Hold) | Move Tool |
-| F | Fog Tool |
-| D | Drawing Tool |
-| M | Measure Tool |
-| Q | Pointer Tool |
-| N | Note Tool |
-| + | Zoom In |
-| - | Zoom Out |
-| Shift + Zoom | Precision Zoom |
-| Ctrl + Z | Undo |
-| Ctrl + Shift + Z | Redo |
-| Ctrl (Hold) | Disabled Grid Snapping |
-| Alt + Drag | Duplicate Token or Note |
+| Shortcut | Description |
+| ------------------- | ----------------------- |
+| W | Move Tool |
+| Space Bar (Hold) | Move Tool |
+| Middle Mouse Button | Move Tool |
+| F | Fog Tool |
+| D | Drawing Tool |
+| M | Measure Tool |
+| Q | Pointer Tool |
+| Right Mouse Button | Pointer Tool |
+| N | Note Tool |
+| + | Zoom In |
+| - | Zoom Out |
+| Shift + Zoom | Precision Zoom |
+| Ctrl + Z | Undo |
+| Ctrl + Shift + Z | Redo |
+| Ctrl (Hold) | Disabled Grid Snapping |
+| Alt + Drag | Duplicate Token or Note |
## Select Tool
diff --git a/src/docs/howTo/usingDice.md b/src/docs/howTo/usingDice.md
index 5079516..5d9c918 100644
--- a/src/docs/howTo/usingDice.md
+++ b/src/docs/howTo/usingDice.md
@@ -1,5 +1,3 @@
-
-
Owlbear Rodeo supports a physically simulated 3D dice tray and dice. To access these features click the Show Dice Tray icon in the top left of the map view.

diff --git a/src/docs/howTo/usingDrawing.md b/src/docs/howTo/usingDrawing.md
index ffb6456..29b9d90 100644
--- a/src/docs/howTo/usingDrawing.md
+++ b/src/docs/howTo/usingDrawing.md
@@ -1,5 +1,3 @@
-
-
The Drawing Tool allows you to draw on top of a map. To access the Drawing Tool click the Drawing Tool button in the top right of the map view.

diff --git a/src/docs/howTo/usingTokens.md b/src/docs/howTo/usingTokens.md
index 003a07b..f9fa37e 100644
--- a/src/docs/howTo/usingTokens.md
+++ b/src/docs/howTo/usingTokens.md
@@ -1,3 +1,5 @@
+
+
Once you have a map shared between a party all players can drag tokens from the Token List on the right hand side of the screen. Tokens can then be used to represent players, monsters or any other object that needs to be moved around the map.
## Default Tokens
diff --git a/src/docs/releaseNotes/v1.10.1.md b/src/docs/releaseNotes/v1.10.1.md
new file mode 100644
index 0000000..dced762
--- /dev/null
+++ b/src/docs/releaseNotes/v1.10.1.md
@@ -0,0 +1,20 @@
+## Minor Changes
+
+This is a small release fixing a few bugs and adding some QoL improvements.
+
+- Updated alternating diagonals measurement to fix inaccuracy with diagonal distances.
+- Added support for middle mouse button move shortcut.
+- Added support for right mouse button pointer shortcut.
+- Optimised note creation to reduce lag when creating a note on FireFox.
+- Fixed a bug with using the polygon fog tool on touch devices.
+- Fixed a bug that would cause phantom players to be shown in the party view on a server error.
+
+We are currently working on some big changes that will take some time so expect smaller releases like this one until that is ready. If you'd like to keep up with the development of these changes check out our [Patreon](https://patreon.com/owlbearrodeo) where we are sharing previews of what's to come.
+
+[Reddit]()
+[Twitter]()
+[Patreon]()
+
+---
+
+October 21 2021
diff --git a/src/helpers/grid.ts b/src/helpers/grid.ts
index 626164a..2c5db2d 100644
--- a/src/helpers/grid.ts
+++ b/src/helpers/grid.ts
@@ -325,7 +325,7 @@ export function gridDistance(
const delta = Vector2.abs(Vector2.subtract(aCoord, bCoord));
const max = Vector2.componentMax(delta);
const min = Vector2.componentMin(delta);
- return max - min + Math.floor(1.5 * min);
+ return max + Math.floor(0.5 * min);
} else if (grid.measurement.type === "euclidean") {
return Vector2.magnitude(
Vector2.divide(Vector2.subtract(a, b), cellSize)
diff --git a/src/hooks/useMapNotes.tsx b/src/hooks/useMapNotes.tsx
index 43647da..1392d2b 100644
--- a/src/hooks/useMapNotes.tsx
+++ b/src/hooks/useMapNotes.tsx
@@ -1,4 +1,5 @@
import Konva from "konva";
+import { Group } from "react-konva";
import { KonvaEventObject } from "konva/lib/Node";
import { useState } from "react";
import { v4 as uuid } from "uuid";
@@ -95,12 +96,7 @@ function useMapNotes(
useBlur(handleBlur);
const notes = (
-
+
{(mapState
? Object.values(mapState.notes).sort((a, b) =>
sortNotes(a, b, noteDraggingOptions)
@@ -129,7 +125,13 @@ function useMapNotes(
}
/>
))}
-
+
+
);
const noteMenu = (
diff --git a/src/hooks/useNetworkedState.ts b/src/hooks/useNetworkedState.ts
index 8f201f6..7417767 100644
--- a/src/hooks/useNetworkedState.ts
+++ b/src/hooks/useNetworkedState.ts
@@ -60,7 +60,7 @@ function useNetworkedState(
const debouncedState = useDebounce(state, debounceRate);
const lastSyncedStateRef = useRef();
useEffect(() => {
- if (session.socket && dirtyRef.current) {
+ if (dirtyRef.current) {
// If partial updates enabled, send just the changes to the socket
if (
lastSyncedStateRef.current &&
@@ -112,11 +112,11 @@ function useNetworkedState(
});
}
- session.socket?.on(eventName, handleSocketEvent);
- session.socket?.on(`${eventName}_update`, handleSocketUpdateEvent);
+ session.socket.on(eventName, handleSocketEvent);
+ session.socket.on(`${eventName}_update`, handleSocketUpdateEvent);
return () => {
- session.socket?.off(eventName, handleSocketEvent);
- session.socket?.off(`${eventName}_update`, handleSocketUpdateEvent);
+ session.socket.off(eventName, handleSocketEvent);
+ session.socket.off(`${eventName}_update`, handleSocketUpdateEvent);
};
}, [session.socket, eventName, partialUpdatesKey]);
diff --git a/src/hooks/usePreventContextMenu.ts b/src/hooks/usePreventContextMenu.ts
new file mode 100644
index 0000000..7073836
--- /dev/null
+++ b/src/hooks/usePreventContextMenu.ts
@@ -0,0 +1,25 @@
+import React, { useEffect } from "react";
+
+function usePreventContextMenu(elementRef: React.RefObject) {
+ useEffect(() => {
+ // Stop conext menu i.e. right click dialog
+ function preventContextMenu(event: MouseEvent) {
+ event.preventDefault();
+ return false;
+ }
+ const element = elementRef.current;
+ if (element) {
+ element.addEventListener("contextmenu", preventContextMenu, {
+ passive: false,
+ });
+ }
+
+ return () => {
+ if (element) {
+ element.removeEventListener("contextmenu", preventContextMenu);
+ }
+ };
+ }, [elementRef]);
+}
+
+export default usePreventContextMenu;
diff --git a/src/hooks/useStageInteraction.ts b/src/hooks/useStageInteraction.ts
index 88ad511..b005bfe 100644
--- a/src/hooks/useStageInteraction.ts
+++ b/src/hooks/useStageInteraction.ts
@@ -67,6 +67,7 @@ function useStageInteraction(
return;
}
const { event, last } = props;
+ // Prevent double zoom on wheel end
if (!last) {
const { pixelY } = normalizeWheel(event);
@@ -178,7 +179,7 @@ function useStageInteraction(
gesture.onDragStart && gesture.onDragStart(props);
},
onDrag: (props) => {
- const { delta, pinching } = props;
+ const { delta, pinching, buttons } = props;
const stage = stageRef.current;
if (
preventInteraction ||
@@ -191,7 +192,8 @@ function useStageInteraction(
const [dx, dy] = delta;
const stageTranslate = stageTranslateRef.current;
- if (tool === "move") {
+ // Move with move tool and left click or any mouse button but right click
+ if ((tool === "move" && buttons < 2) || buttons > 2) {
const newTranslate = {
x: stageTranslate.x + dx,
y: stageTranslate.y + dy,
diff --git a/src/network/NetworkedMapAndTokens.tsx b/src/network/NetworkedMapAndTokens.tsx
index 997c53e..e6e09f9 100644
--- a/src/network/NetworkedMapAndTokens.tsx
+++ b/src/network/NetworkedMapAndTokens.tsx
@@ -202,12 +202,12 @@ function NetworkedMapAndTokens({ session }: { session: Session }) {
) {
// Clear map before sending new one
setCurrentMap(null);
- session.socket?.emit("map", null);
+ session.socket.emit("map", null);
setCurrentMapState(newMapState, true, true);
setCurrentMap(newMap);
- session.socket?.emit("map", newMap);
+ session.socket.emit("map", newMap);
if (!newMap || !newMapState) {
setAssetManifest(null, true, true);
@@ -395,12 +395,12 @@ function NetworkedMapAndTokens({ session }: { session: Session }) {
session.on("peerData", handlePeerData);
session.on("peerDataProgress", handlePeerDataProgress);
- session.socket?.on("map", handleSocketMap);
+ session.socket.on("map", handleSocketMap);
return () => {
session.off("peerData", handlePeerData);
session.off("peerDataProgress", handlePeerDataProgress);
- session.socket?.off("map", handleSocketMap);
+ session.socket.off("map", handleSocketMap);
};
});
diff --git a/src/network/NetworkedMapPointer.tsx b/src/network/NetworkedMapPointer.tsx
index cd44086..5ba07e2 100644
--- a/src/network/NetworkedMapPointer.tsx
+++ b/src/network/NetworkedMapPointer.tsx
@@ -74,7 +74,7 @@ function NetworkedMapPointer({ session, active }: NetworkedMapPointerProps) {
sessionRef.current &&
sessionRef.current.socket
) {
- sessionRef.current.socket.emit(
+ sessionRef.current.socket.volatile.emit(
"player_pointer",
ownPointerUpdateRef.current
);
@@ -152,10 +152,10 @@ function NetworkedMapPointer({ session, active }: NetworkedMapPointerProps) {
}
}
- session.socket?.on("player_pointer", handleSocketPlayerPointer);
+ session.socket.on("player_pointer", handleSocketPlayerPointer);
return () => {
- session.socket?.off("player_pointer", handleSocketPlayerPointer);
+ session.socket.off("player_pointer", handleSocketPlayerPointer);
};
}, [session]);
diff --git a/src/network/Session.ts b/src/network/Session.ts
index a85b8ad..5912c3d 100644
--- a/src/network/Session.ts
+++ b/src/network/Session.ts
@@ -1,4 +1,4 @@
-import io, { Socket } from "socket.io-client";
+import io from "socket.io-client";
import msgParser from "socket.io-msgpack-parser";
import { EventEmitter } from "events";
@@ -32,10 +32,11 @@ export type PeerReply = (id: string, data: PeerData, chunkId?: string) => void;
class Session extends EventEmitter {
/**
* The socket io connection
- *
- * @type {io.Socket}
*/
- socket?: Socket;
+ socket = io(process.env.REACT_APP_BROKER_URL!, {
+ withCredentials: true,
+ parser: msgParser,
+ });
/**
* A mapping of socket ids to session peers
@@ -45,7 +46,7 @@ class Session extends EventEmitter {
peers: Record;
get id() {
- return this.socket?.id || "";
+ return this.socket.id;
}
_iceServers: RTCIceServer[] = [];
@@ -76,14 +77,6 @@ class Session extends EventEmitter {
const data = await response.json();
this._iceServers = data.iceServers;
- if (!process.env.REACT_APP_BROKER_URL) {
- return;
- }
- this.socket = io(process.env.REACT_APP_BROKER_URL, {
- withCredentials: true,
- parser: msgParser,
- });
-
this.socket.on("player_joined", this._handlePlayerJoined.bind(this));
this.socket.on("player_left", this._handlePlayerLeft.bind(this));
this.socket.on("joined_game", this._handleJoinedGame.bind(this));
@@ -95,14 +88,14 @@ class Session extends EventEmitter {
this.socket.on("force_update", this._handleForceUpdate.bind(this));
this.emit("status", "ready");
- } catch (error) {
+ } catch (error: any) {
logError(error);
this.emit("status", "offline");
}
}
disconnect() {
- this.socket?.disconnect();
+ this.socket.disconnect();
}
/**
@@ -191,7 +184,7 @@ class Session extends EventEmitter {
this._gameId = gameId;
this._password = password;
- this.socket?.emit(
+ this.socket.emit(
"join_game",
gameId,
password,
@@ -224,7 +217,7 @@ class Session extends EventEmitter {
};
const handleSignal = (signal: SignalData) => {
- this.socket?.emit("signal", JSON.stringify({ to: peer.id, signal }));
+ this.socket.emit("signal", JSON.stringify({ to: peer.id, signal }));
};
const handleConnect = () => {
@@ -309,7 +302,7 @@ class Session extends EventEmitter {
this.peers[id] = peer;
return true;
- } catch (error) {
+ } catch (error: any) {
logError(error);
this.emit("peerError", { error });
for (let peer of Object.values(this.peers)) {
@@ -367,13 +360,14 @@ class Session extends EventEmitter {
}
_handleSocketReconnect() {
+ this.socket.sendBuffer = [];
if (this._gameId) {
this.joinGame(this._gameId, this._password);
}
}
_handleForceUpdate() {
- this.socket?.disconnect();
+ this.socket.disconnect();
this.emit("status", "needs_update");
}
}
diff --git a/src/routes/ReleaseNotes.tsx b/src/routes/ReleaseNotes.tsx
index 1d240a0..9dabf12 100644
--- a/src/routes/ReleaseNotes.tsx
+++ b/src/routes/ReleaseNotes.tsx
@@ -27,6 +27,7 @@ const v180 = raw("../docs/releaseNotes/v1.8.0.md");
const v181 = raw("../docs/releaseNotes/v1.8.1.md");
const v190 = raw("../docs/releaseNotes/v1.9.0.md");
const v1100 = raw("../docs/releaseNotes/v1.10.0.md");
+const v1101 = raw("../docs/releaseNotes/v1.10.1.md");
function ReleaseNotes() {
const location = useLocation();
@@ -51,6 +52,11 @@ function ReleaseNotes() {
Release Notes
+
diff --git a/yarn.lock b/yarn.lock
index 9e67f53..0ea68eb 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -13181,6 +13181,11 @@ tiny-invariant@^1.0.2:
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875"
integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==
+tiny-typed-emitter@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz#b3b027fdd389ff81a152c8e847ee2f5be9fad7b5"
+ integrity sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==
+
tiny-warning@^1.0.0, tiny-warning@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"