Merge pull request #53 from mitchemmc/test/v1.10.1

Test/v1.10.1
This commit is contained in:
Mitchell McCaffrey 2021-10-21 20:38:07 +11:00 committed by GitHub
commit 2a7817006f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 435 additions and 167 deletions

View File

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

View File

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

View File

@ -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 (
<Rect
x={note.x * mapWidth}
y={note.y * mapHeight}
width={noteWidth}
height={noteHeight}
offsetX={noteWidth / 2}
offsetY={noteHeight / 2}
shadowColor="rgba(0, 0, 0, 0.16)"
shadowOffset={{ x: 3, y: 6 }}
shadowBlur={6}
cornerRadius={0.25}
fill={colors[note.color]}
/>
);
}
export default BlankNote;

View File

@ -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<MouseEvent>) {
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<number>(0);
function handlePointerDown(event: Konva.KonvaEventObject<PointerEvent>) {
if (!leftMouseButton(event)) {
return;
}
if (draggable) {
setPreventMapInteraction(true);
}
@ -120,6 +127,9 @@ function Note({
}
function handlePointerUp(event: Konva.KonvaEventObject<PointerEvent>) {
if (!leftMouseButton(event)) {
return;
}
if (draggable) {
setPreventMapInteraction(false);
}

View File

@ -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<MouseEvent>) {
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<number>(0);
function handlePointerDown(event: Konva.KonvaEventObject<PointerEvent>) {
if (!leftMouseButton(event)) {
return;
}
if (draggable) {
setPreventMapInteraction(true);
}
@ -216,6 +223,9 @@ function Token({
}
function handlePointerUp(event: Konva.KonvaEventObject<PointerEvent>) {
if (!leftMouseButton(event)) {
return;
}
if (draggable) {
setPreventMapInteraction(false);
}

View File

@ -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<HTMLDivElement>(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";

View File

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

View File

@ -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<Fog | null>(null);
const [isBrushDown, setIsBrushDown] = useState(false);
const [editingShapes, setEditingShapes] = useState<Fog[]>([]);
const [hoveredShapes, setHoveredShapes] = useState<Fog[]>([]);
// 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<MouseEvent>) {
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 (
<FogShape
fog={shape}
fill={fill}
stroke={stroke}
opacity={opacity}
strokeWidth={gridStrokeWidth * shape.strokeWidth}
hitFunc={() => {}}
/>
);
}
function renderPolygonAcceptTick(shape: Fog) {
if (shape.data.points.length === 0) {
return null;
@ -681,12 +717,12 @@ function FogTool({
<Group>
<Group>{fogShapes.map(renderShape)}</Group>
{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)}
</Group>
);
}

View File

@ -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<MeasureData | null>(null);
const [drawingShapeData, setDrawingShapeData] = useState<MeasureData | null>(
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);
}

View File

@ -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<NoteType | null>(null);
const creatingNoteRef = useRef<Konva.Group>(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 (
<Group id="notes">
{children}
<Group ref={creatingNoteRef}>
{isBrushDown && noteData && (
<Note note={noteData} map={map} selected={false} />
)}
</Group>
<Group ref={creatingNoteRef}>
{noteData && <BlankNote note={noteData} />}
</Group>
);
}

View File

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

View File

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

View File

@ -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<FullGestureState<"drag">, "event"> & {
event: React.PointerEvent<Element> | PointerEvent;
};
export type MapDragEventHandler = (props: MapDragEvent) => void;
export interface MapInteractionEvents {
dragStart: MapDragEventHandler;
drag: MapDragEventHandler;
dragEnd: MapDragEventHandler;
}
export class MapInteractionEmitter extends TypedEmitter<MapInteractionEvents> {}
type MapInteraction = {
stageScale: number;
@ -9,29 +25,33 @@ type MapInteraction = {
setPreventMapInteraction: React.Dispatch<React.SetStateAction<boolean>>;
mapWidth: number;
mapHeight: number;
interactionEmitter: EventEmitter | null;
interactionEmitter: MapInteractionEmitter | null;
};
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 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<PointerEvent>
): boolean;
export function leftMouseButton(
event: Konva.KonvaEventObject<MouseEvent>
): 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;
}

View File

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

View File

@ -9,10 +9,12 @@ import useNetworkedState, {
import Session, { SessionStatus } from "../network/Session";
import { PlayerState } from "../types/PlayerState";
export const PlayerStateContext =
React.createContext<PlayerState | undefined>(undefined);
export const PlayerUpdaterContext =
React.createContext<SetNetworkedState<PlayerState> | undefined>(undefined);
export const PlayerStateContext = React.createContext<PlayerState | undefined>(
undefined
);
export const PlayerUpdaterContext = React.createContext<
SetNetworkedState<PlayerState> | 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);
};
});

View File

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

View File

@ -1,5 +1,3 @@
![embed:](https://www.youtube.com/embed/Er_grVmqpk0)
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.
![Open Dice Tray](openDiceTray)

View File

@ -1,5 +1,3 @@
![embed:](https://www.youtube.com/embed/2e07DtB-Xrc)
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.
![Using Drawing](usingDrawing)

View File

@ -1,3 +1,5 @@
![embed:](https://www.youtube.com/embed/MCOzpQ4auqs)
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

View File

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

View File

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

View File

@ -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 = (
<NoteTool
map={map}
active={selectedToolId === "note"}
onNoteCreate={onNoteCreate}
onNoteMenuOpen={handleNoteMenuOpen}
>
<Group id="notes">
{(mapState
? Object.values(mapState.notes).sort((a, b) =>
sortNotes(a, b, noteDraggingOptions)
@ -129,7 +125,13 @@ function useMapNotes(
}
/>
))}
</NoteTool>
<NoteTool
map={map}
active={selectedToolId === "note"}
onNoteCreate={onNoteCreate}
onNoteMenuOpen={handleNoteMenuOpen}
/>
</Group>
);
const noteMenu = (

View File

@ -60,7 +60,7 @@ function useNetworkedState<S extends { readonly [x: string]: any } | null>(
const debouncedState = useDebounce(state, debounceRate);
const lastSyncedStateRef = useRef<S>();
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<S extends { readonly [x: string]: any } | null>(
});
}
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]);

View File

@ -0,0 +1,25 @@
import React, { useEffect } from "react";
function usePreventContextMenu(elementRef: React.RefObject<HTMLElement>) {
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;

View File

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

View File

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

View File

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

View File

@ -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<string, SessionPeer>;
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");
}
}

View File

@ -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() {
<Text mb={2} variant="heading" as="h1" sx={{ fontSize: 5 }}>
Release Notes
</Text>
<div id="v1101">
<Accordion heading="v1.10.1" defaultOpen>
<Markdown source={v1101} />
</Accordion>
</div>
<div id="v1100">
<Accordion heading="v1.10.0" defaultOpen>
<Markdown source={v1100} />

View File

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