commit
fac104367a
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "owlbear-rodeo",
|
||||
"version": "1.3.3",
|
||||
"version": "1.4.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@msgpack/msgpack": "^1.12.1",
|
||||
@ -16,12 +16,13 @@
|
||||
"fake-indexeddb": "^3.0.0",
|
||||
"interactjs": "^1.9.7",
|
||||
"konva": "^6.0.0",
|
||||
"normalize-wheel": "^1.0.1",
|
||||
"polygon-clipping": "^0.14.3",
|
||||
"raw.macro": "^0.3.0",
|
||||
"react": "^16.13.0",
|
||||
"react-dom": "^16.13.0",
|
||||
"react-konva": "^16.13.0-3",
|
||||
"react-markdown": "^4.3.1",
|
||||
"react-media": "^2.0.0-rc.1",
|
||||
"react-modal": "^3.11.2",
|
||||
"react-resize-detector": "^4.2.3",
|
||||
"react-router-dom": "^5.1.2",
|
||||
|
@ -15,6 +15,7 @@ function LoadingOverlay() {
|
||||
alignItems: "center",
|
||||
top: 0,
|
||||
left: 0,
|
||||
flexDirection: "column",
|
||||
}}
|
||||
bg="muted"
|
||||
>
|
||||
|
@ -30,14 +30,20 @@ function StyledModal({
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{allowClose && (
|
||||
<Close
|
||||
m={0}
|
||||
sx={{ position: "absolute", top: 0, right: 0 }}
|
||||
onClick={onRequestClose}
|
||||
/>
|
||||
)}
|
||||
{/* Stop keyboard events when modal is open to prevent shortcuts from triggering */}
|
||||
<div
|
||||
onKeyDown={(e) => e.stopPropagation()}
|
||||
onKeyUp={(e) => e.stopPropagation()}
|
||||
>
|
||||
{children}
|
||||
{allowClose && (
|
||||
<Close
|
||||
m={0}
|
||||
sx={{ position: "absolute", top: 0, right: 0 }}
|
||||
onClick={onRequestClose}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
@ -7,15 +7,15 @@ import MapDrawing from "./MapDrawing";
|
||||
import MapFog from "./MapFog";
|
||||
import MapDice from "./MapDice";
|
||||
import MapGrid from "./MapGrid";
|
||||
import MapMeasure from "./MapMeasure";
|
||||
import MapLoadingOverlay from "./MapLoadingOverlay";
|
||||
|
||||
import TokenDataContext from "../../contexts/TokenDataContext";
|
||||
import MapLoadingContext from "../../contexts/MapLoadingContext";
|
||||
|
||||
import TokenMenu from "../token/TokenMenu";
|
||||
import TokenDragOverlay from "../token/TokenDragOverlay";
|
||||
import LoadingOverlay from "../LoadingOverlay";
|
||||
|
||||
import { omit } from "../../helpers/shared";
|
||||
import { drawActionsToShapes } from "../../helpers/drawing";
|
||||
|
||||
function Map({
|
||||
map,
|
||||
@ -35,7 +35,6 @@ function Map({
|
||||
disabledTokens,
|
||||
}) {
|
||||
const { tokensById } = useContext(TokenDataContext);
|
||||
const { isLoading } = useContext(MapLoadingContext);
|
||||
|
||||
const gridX = map && map.gridX;
|
||||
const gridY = map && map.gridY;
|
||||
@ -47,17 +46,15 @@ function Map({
|
||||
|
||||
const [selectedToolId, setSelectedToolId] = useState("pan");
|
||||
const [toolSettings, setToolSettings] = useState({
|
||||
fog: { type: "add", useEdgeSnapping: true, useGridSnapping: false },
|
||||
brush: {
|
||||
color: "darkGray",
|
||||
type: "stroke",
|
||||
useBlending: false,
|
||||
},
|
||||
shape: {
|
||||
fog: { type: "polygon", useEdgeSnapping: false, useFogCut: false },
|
||||
drawing: {
|
||||
color: "red",
|
||||
type: "rectangle",
|
||||
type: "brush",
|
||||
useBlending: true,
|
||||
},
|
||||
measure: {
|
||||
type: "chebyshev",
|
||||
},
|
||||
});
|
||||
|
||||
function handleToolSettingChange(tool, change) {
|
||||
@ -108,6 +105,10 @@ function Map({
|
||||
onFogDraw({ type: "add", shapes: [shape] });
|
||||
}
|
||||
|
||||
function handleFogShapeSubtract(shape) {
|
||||
onFogDraw({ type: "subtract", shapes: [shape] });
|
||||
}
|
||||
|
||||
function handleFogShapesRemove(shapeIds) {
|
||||
onFogDraw({ type: "remove", shapeIds });
|
||||
}
|
||||
@ -121,59 +122,38 @@ function Map({
|
||||
if (!mapState) {
|
||||
return;
|
||||
}
|
||||
function actionsToShapes(actions, actionIndex) {
|
||||
let shapesById = {};
|
||||
for (let i = 0; i <= actionIndex; i++) {
|
||||
const action = actions[i];
|
||||
if (action.type === "add" || action.type === "edit") {
|
||||
for (let shape of action.shapes) {
|
||||
shapesById[shape.id] = shape;
|
||||
}
|
||||
}
|
||||
if (action.type === "remove") {
|
||||
shapesById = omit(shapesById, action.shapeIds);
|
||||
}
|
||||
}
|
||||
return Object.values(shapesById);
|
||||
}
|
||||
|
||||
setMapShapes(
|
||||
actionsToShapes(mapState.mapDrawActions, mapState.mapDrawActionIndex)
|
||||
drawActionsToShapes(mapState.mapDrawActions, mapState.mapDrawActionIndex)
|
||||
);
|
||||
setFogShapes(
|
||||
actionsToShapes(mapState.fogDrawActions, mapState.fogDrawActionIndex)
|
||||
drawActionsToShapes(mapState.fogDrawActions, mapState.fogDrawActionIndex)
|
||||
);
|
||||
}, [mapState]);
|
||||
|
||||
const disabledControls = [];
|
||||
if (!allowMapDrawing) {
|
||||
disabledControls.push("brush");
|
||||
disabledControls.push("shape");
|
||||
disabledControls.push("erase");
|
||||
disabledControls.push("drawing");
|
||||
}
|
||||
if (!map) {
|
||||
disabledControls.push("pan");
|
||||
}
|
||||
if (mapShapes.length === 0) {
|
||||
disabledControls.push("erase");
|
||||
disabledControls.push("measure");
|
||||
}
|
||||
if (!allowFogDrawing) {
|
||||
disabledControls.push("fog");
|
||||
}
|
||||
|
||||
const disabledSettings = { fog: [], brush: [], shape: [], erase: [] };
|
||||
const disabledSettings = { fog: [], drawing: [] };
|
||||
if (mapShapes.length === 0) {
|
||||
disabledSettings.drawing.push("erase");
|
||||
}
|
||||
if (!mapState || mapState.mapDrawActionIndex < 0) {
|
||||
disabledSettings.brush.push("undo");
|
||||
disabledSettings.shape.push("undo");
|
||||
disabledSettings.erase.push("undo");
|
||||
disabledSettings.drawing.push("undo");
|
||||
}
|
||||
if (
|
||||
!mapState ||
|
||||
mapState.mapDrawActionIndex === mapState.mapDrawActions.length - 1
|
||||
) {
|
||||
disabledSettings.brush.push("redo");
|
||||
disabledSettings.shape.push("redo");
|
||||
disabledSettings.erase.push("redo");
|
||||
disabledSettings.drawing.push("redo");
|
||||
}
|
||||
if (!mapState || mapState.fogDrawActionIndex < 0) {
|
||||
disabledSettings.fog.push("undo");
|
||||
@ -287,6 +267,7 @@ function Map({
|
||||
<MapFog
|
||||
shapes={fogShapes}
|
||||
onShapeAdd={handleFogShapeAdd}
|
||||
onShapeSubtract={handleFogShapeSubtract}
|
||||
onShapesRemove={handleFogShapesRemove}
|
||||
onShapesEdit={handleFogShapesEdit}
|
||||
selectedToolId={selectedToolId}
|
||||
@ -299,6 +280,14 @@ function Map({
|
||||
<MapGrid map={map} gridSize={gridSizeNormalized} />
|
||||
);
|
||||
|
||||
const mapMeasure = (
|
||||
<MapMeasure
|
||||
active={selectedToolId === "measure"}
|
||||
gridSize={gridSizeNormalized}
|
||||
selectedToolSettings={toolSettings[selectedToolId]}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<MapInteraction
|
||||
map={map}
|
||||
@ -308,15 +297,18 @@ function Map({
|
||||
{tokenMenu}
|
||||
{tokenDragOverlay}
|
||||
<MapDice />
|
||||
{isLoading && <LoadingOverlay />}
|
||||
<MapLoadingOverlay />
|
||||
</>
|
||||
}
|
||||
selectedToolId={selectedToolId}
|
||||
onSelectedToolChange={setSelectedToolId}
|
||||
disabledControls={disabledControls}
|
||||
>
|
||||
{mapGrid}
|
||||
{mapDrawing}
|
||||
{mapTokens}
|
||||
{mapFog}
|
||||
{mapMeasure}
|
||||
</MapInteraction>
|
||||
);
|
||||
}
|
||||
|
@ -7,15 +7,13 @@ import Divider from "../Divider";
|
||||
import SelectMapButton from "./SelectMapButton";
|
||||
|
||||
import FogToolSettings from "./controls/FogToolSettings";
|
||||
import BrushToolSettings from "./controls/BrushToolSettings";
|
||||
import ShapeToolSettings from "./controls/ShapeToolSettings";
|
||||
import EraseToolSettings from "./controls/EraseToolSettings";
|
||||
import DrawingToolSettings from "./controls/DrawingToolSettings";
|
||||
import MeasureToolSettings from "./controls/MeasureToolSettings";
|
||||
|
||||
import PanToolIcon from "../../icons/PanToolIcon";
|
||||
import FogToolIcon from "../../icons/FogToolIcon";
|
||||
import BrushToolIcon from "../../icons/BrushToolIcon";
|
||||
import ShapeToolIcon from "../../icons/ShapeToolIcon";
|
||||
import EraseToolIcon from "../../icons/EraseToolIcon";
|
||||
import MeasureToolIcon from "../../icons/MeasureToolIcon";
|
||||
import ExpandMoreIcon from "../../icons/ExpandMoreIcon";
|
||||
|
||||
function MapContols({
|
||||
@ -45,26 +43,20 @@ function MapContols({
|
||||
title: "Fog Tool",
|
||||
SettingsComponent: FogToolSettings,
|
||||
},
|
||||
brush: {
|
||||
id: "brush",
|
||||
drawing: {
|
||||
id: "drawing",
|
||||
icon: <BrushToolIcon />,
|
||||
title: "Brush Tool",
|
||||
SettingsComponent: BrushToolSettings,
|
||||
title: "Drawing Tool",
|
||||
SettingsComponent: DrawingToolSettings,
|
||||
},
|
||||
shape: {
|
||||
id: "shape",
|
||||
icon: <ShapeToolIcon />,
|
||||
title: "Shape Tool",
|
||||
SettingsComponent: ShapeToolSettings,
|
||||
},
|
||||
erase: {
|
||||
id: "erase",
|
||||
icon: <EraseToolIcon />,
|
||||
title: "Erase tool",
|
||||
SettingsComponent: EraseToolSettings,
|
||||
measure: {
|
||||
id: "measure",
|
||||
icon: <MeasureToolIcon />,
|
||||
title: "Measure Tool",
|
||||
SettingsComponent: MeasureToolSettings,
|
||||
},
|
||||
};
|
||||
const tools = ["pan", "fog", "brush", "shape", "erase"];
|
||||
const tools = ["pan", "fog", "drawing", "measure"];
|
||||
|
||||
const sections = [
|
||||
{
|
||||
@ -79,7 +71,7 @@ function MapContols({
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "drawing",
|
||||
id: "tools",
|
||||
component: tools.map((tool) => (
|
||||
<RadioIconButton
|
||||
key={tool}
|
||||
|
@ -1,8 +1,9 @@
|
||||
import React, { useContext, useState, useCallback } from "react";
|
||||
import React, { useContext, useState, useEffect } from "react";
|
||||
import shortid from "shortid";
|
||||
import { Group, Line, Rect, Circle } from "react-konva";
|
||||
|
||||
import MapInteractionContext from "../../contexts/MapInteractionContext";
|
||||
import MapStageContext from "../../contexts/MapStageContext";
|
||||
|
||||
import { compare as comparePoints } from "../../helpers/vector2";
|
||||
import {
|
||||
@ -12,9 +13,9 @@ import {
|
||||
simplifyPoints,
|
||||
getStrokeWidth,
|
||||
} from "../../helpers/drawing";
|
||||
import { getRelativePointerPositionNormalized } from "../../helpers/konva";
|
||||
|
||||
import colors from "../../helpers/colors";
|
||||
import useMapBrush from "../../helpers/useMapBrush";
|
||||
|
||||
function MapDrawing({
|
||||
shapes,
|
||||
@ -24,69 +25,76 @@ function MapDrawing({
|
||||
selectedToolSettings,
|
||||
gridSize,
|
||||
}) {
|
||||
const { stageScale, mapWidth, mapHeight } = useContext(MapInteractionContext);
|
||||
const { stageScale, mapWidth, mapHeight, interactionEmitter } = useContext(
|
||||
MapInteractionContext
|
||||
);
|
||||
const mapStageRef = useContext(MapStageContext);
|
||||
const [drawingShape, setDrawingShape] = useState(null);
|
||||
const [isBrushDown, setIsBrushDown] = useState(false);
|
||||
const [erasingShapes, setErasingShapes] = useState([]);
|
||||
|
||||
const shouldHover = selectedToolId === "erase";
|
||||
const isEditing =
|
||||
selectedToolId === "brush" ||
|
||||
selectedToolId === "shape" ||
|
||||
selectedToolId === "erase";
|
||||
const shouldHover =
|
||||
selectedToolSettings && selectedToolSettings.type === "erase";
|
||||
const isEditing = selectedToolId === "drawing";
|
||||
const isBrush =
|
||||
selectedToolSettings &&
|
||||
(selectedToolSettings.type === "brush" ||
|
||||
selectedToolSettings.type === "paint");
|
||||
const isShape =
|
||||
selectedToolSettings &&
|
||||
(selectedToolSettings.type === "line" ||
|
||||
selectedToolSettings.type === "rectangle" ||
|
||||
selectedToolSettings.type === "circle" ||
|
||||
selectedToolSettings.type === "triangle");
|
||||
|
||||
const handleBrushUp = useCallback(() => {
|
||||
setIsBrushDown(false);
|
||||
if (erasingShapes.length > 0) {
|
||||
onShapesRemove(erasingShapes.map((shape) => shape.id));
|
||||
setErasingShapes([]);
|
||||
useEffect(() => {
|
||||
if (!isEditing) {
|
||||
return;
|
||||
}
|
||||
}, [erasingShapes, onShapesRemove]);
|
||||
const mapStage = mapStageRef.current;
|
||||
|
||||
const handleShapeDraw = useCallback(
|
||||
(brushState, mapBrushPosition) => {
|
||||
function startShape() {
|
||||
const brushPosition = getBrushPositionForTool(
|
||||
mapBrushPosition,
|
||||
selectedToolId,
|
||||
selectedToolSettings,
|
||||
gridSize,
|
||||
shapes
|
||||
);
|
||||
const commonShapeData = {
|
||||
color: selectedToolSettings && selectedToolSettings.color,
|
||||
blend: selectedToolSettings && selectedToolSettings.useBlending,
|
||||
id: shortid.generate(),
|
||||
};
|
||||
if (selectedToolId === "brush") {
|
||||
setDrawingShape({
|
||||
type: "path",
|
||||
pathType: selectedToolSettings.type,
|
||||
data: { points: [brushPosition] },
|
||||
strokeWidth: selectedToolSettings.type === "stroke" ? 1 : 0,
|
||||
...commonShapeData,
|
||||
});
|
||||
} else if (selectedToolId === "shape") {
|
||||
setDrawingShape({
|
||||
type: "shape",
|
||||
shapeType: selectedToolSettings.type,
|
||||
data: getDefaultShapeData(selectedToolSettings.type, brushPosition),
|
||||
strokeWidth: 0,
|
||||
...commonShapeData,
|
||||
});
|
||||
}
|
||||
setIsBrushDown(true);
|
||||
function getBrushPosition() {
|
||||
const mapImage = mapStage.findOne("#mapImage");
|
||||
return getBrushPositionForTool(
|
||||
getRelativePointerPositionNormalized(mapImage),
|
||||
selectedToolId,
|
||||
selectedToolSettings,
|
||||
gridSize,
|
||||
shapes
|
||||
);
|
||||
}
|
||||
|
||||
function handleBrushDown() {
|
||||
const brushPosition = getBrushPosition();
|
||||
const commonShapeData = {
|
||||
color: selectedToolSettings && selectedToolSettings.color,
|
||||
blend: selectedToolSettings && selectedToolSettings.useBlending,
|
||||
id: shortid.generate(),
|
||||
};
|
||||
if (isBrush) {
|
||||
setDrawingShape({
|
||||
type: "path",
|
||||
pathType: selectedToolSettings.type === "brush" ? "stroke" : "fill",
|
||||
data: { points: [brushPosition] },
|
||||
strokeWidth: selectedToolSettings.type === "brush" ? 1 : 0,
|
||||
...commonShapeData,
|
||||
});
|
||||
} else if (isShape) {
|
||||
setDrawingShape({
|
||||
type: "shape",
|
||||
shapeType: selectedToolSettings.type,
|
||||
data: getDefaultShapeData(selectedToolSettings.type, brushPosition),
|
||||
strokeWidth: selectedToolSettings.type === "line" ? 1 : 0,
|
||||
...commonShapeData,
|
||||
});
|
||||
}
|
||||
setIsBrushDown(true);
|
||||
}
|
||||
|
||||
function continueShape() {
|
||||
const brushPosition = getBrushPositionForTool(
|
||||
mapBrushPosition,
|
||||
selectedToolId,
|
||||
selectedToolSettings,
|
||||
gridSize,
|
||||
shapes
|
||||
);
|
||||
if (selectedToolId === "brush") {
|
||||
function handleBrushMove() {
|
||||
const brushPosition = getBrushPosition();
|
||||
if (isBrushDown && drawingShape) {
|
||||
if (isBrush) {
|
||||
setDrawingShape((prevShape) => {
|
||||
const prevPoints = prevShape.data.points;
|
||||
if (
|
||||
@ -108,7 +116,7 @@ function MapDrawing({
|
||||
data: { points: simplified },
|
||||
};
|
||||
});
|
||||
} else if (selectedToolId === "shape") {
|
||||
} else if (isShape) {
|
||||
setDrawingShape((prevShape) => ({
|
||||
...prevShape,
|
||||
data: getUpdatedShapeData(
|
||||
@ -120,46 +128,52 @@ function MapDrawing({
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function endShape() {
|
||||
if (selectedToolId === "brush" && drawingShape) {
|
||||
if (drawingShape.data.points.length > 1) {
|
||||
onShapeAdd(drawingShape);
|
||||
}
|
||||
} else if (selectedToolId === "shape" && drawingShape) {
|
||||
function handleBrushUp() {
|
||||
if (isBrush && drawingShape) {
|
||||
if (drawingShape.data.points.length > 1) {
|
||||
onShapeAdd(drawingShape);
|
||||
}
|
||||
setDrawingShape(null);
|
||||
handleBrushUp();
|
||||
} else if (isShape && drawingShape) {
|
||||
onShapeAdd(drawingShape);
|
||||
}
|
||||
|
||||
switch (brushState) {
|
||||
case "first":
|
||||
startShape();
|
||||
return;
|
||||
case "drawing":
|
||||
continueShape();
|
||||
return;
|
||||
case "last":
|
||||
endShape();
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
if (erasingShapes.length > 0) {
|
||||
onShapesRemove(erasingShapes.map((shape) => shape.id));
|
||||
setErasingShapes([]);
|
||||
}
|
||||
},
|
||||
[
|
||||
selectedToolId,
|
||||
selectedToolSettings,
|
||||
gridSize,
|
||||
stageScale,
|
||||
onShapeAdd,
|
||||
shapes,
|
||||
drawingShape,
|
||||
handleBrushUp,
|
||||
]
|
||||
);
|
||||
|
||||
useMapBrush(isEditing, handleShapeDraw);
|
||||
setDrawingShape(null);
|
||||
setIsBrushDown(false);
|
||||
}
|
||||
|
||||
interactionEmitter.on("dragStart", handleBrushDown);
|
||||
interactionEmitter.on("drag", handleBrushMove);
|
||||
interactionEmitter.on("dragEnd", handleBrushUp);
|
||||
|
||||
return () => {
|
||||
interactionEmitter.off("dragStart", handleBrushDown);
|
||||
interactionEmitter.off("drag", handleBrushMove);
|
||||
interactionEmitter.off("dragEnd", handleBrushUp);
|
||||
};
|
||||
}, [
|
||||
drawingShape,
|
||||
erasingShapes,
|
||||
gridSize,
|
||||
isBrush,
|
||||
isBrushDown,
|
||||
isEditing,
|
||||
isShape,
|
||||
mapStageRef,
|
||||
onShapeAdd,
|
||||
onShapesRemove,
|
||||
selectedToolId,
|
||||
selectedToolSettings,
|
||||
shapes,
|
||||
stageScale,
|
||||
interactionEmitter,
|
||||
]);
|
||||
|
||||
function handleShapeOver(shape, isDown) {
|
||||
if (shouldHover && isDown) {
|
||||
@ -178,6 +192,7 @@ function MapDrawing({
|
||||
onTouchStart: () => handleShapeOver(shape, true),
|
||||
fill: colors[shape.color] || shape.color,
|
||||
opacity: shape.blend ? 0.5 : 1,
|
||||
id: shape.id,
|
||||
};
|
||||
if (shape.type === "path") {
|
||||
return (
|
||||
@ -232,6 +247,24 @@ function MapDrawing({
|
||||
{...defaultProps}
|
||||
/>
|
||||
);
|
||||
} else if (shape.shapeType === "line") {
|
||||
return (
|
||||
<Line
|
||||
points={shape.data.points.reduce(
|
||||
(acc, point) => [...acc, point.x * mapWidth, point.y * mapHeight],
|
||||
[]
|
||||
)}
|
||||
strokeWidth={getStrokeWidth(
|
||||
shape.strokeWidth,
|
||||
gridSize,
|
||||
mapWidth,
|
||||
mapHeight
|
||||
)}
|
||||
stroke={colors[shape.color] || shape.color}
|
||||
lineCap="round"
|
||||
{...defaultProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,12 @@
|
||||
import React, { useContext, useState, useCallback } from "react";
|
||||
import React, { useContext, useState, useEffect, useCallback } from "react";
|
||||
import shortid from "shortid";
|
||||
import { Group, Line } from "react-konva";
|
||||
import { Group } from "react-konva";
|
||||
import useImage from "use-image";
|
||||
|
||||
import diagonalPattern from "../../images/DiagonalPattern.png";
|
||||
|
||||
import MapInteractionContext from "../../contexts/MapInteractionContext";
|
||||
import MapStageContext from "../../contexts/MapStageContext";
|
||||
|
||||
import { compare as comparePoints } from "../../helpers/vector2";
|
||||
import {
|
||||
@ -13,20 +14,27 @@ import {
|
||||
simplifyPoints,
|
||||
getStrokeWidth,
|
||||
} from "../../helpers/drawing";
|
||||
|
||||
import colors from "../../helpers/colors";
|
||||
import useMapBrush from "../../helpers/useMapBrush";
|
||||
import {
|
||||
HoleyLine,
|
||||
getRelativePointerPositionNormalized,
|
||||
Tick,
|
||||
} from "../../helpers/konva";
|
||||
|
||||
function MapFog({
|
||||
shapes,
|
||||
onShapeAdd,
|
||||
onShapeSubtract,
|
||||
onShapesRemove,
|
||||
onShapesEdit,
|
||||
selectedToolId,
|
||||
selectedToolSettings,
|
||||
gridSize,
|
||||
}) {
|
||||
const { stageScale, mapWidth, mapHeight } = useContext(MapInteractionContext);
|
||||
const { stageScale, mapWidth, mapHeight, interactionEmitter } = useContext(
|
||||
MapInteractionContext
|
||||
);
|
||||
const mapStageRef = useContext(MapStageContext);
|
||||
const [drawingShape, setDrawingShape] = useState(null);
|
||||
const [isBrushDown, setIsBrushDown] = useState(false);
|
||||
const [editingShapes, setEditingShapes] = useState([]);
|
||||
@ -39,120 +47,272 @@ function MapFog({
|
||||
|
||||
const [patternImage] = useImage(diagonalPattern);
|
||||
|
||||
const handleBrushUp = useCallback(() => {
|
||||
setIsBrushDown(false);
|
||||
if (editingShapes.length > 0) {
|
||||
if (selectedToolSettings.type === "remove") {
|
||||
onShapesRemove(editingShapes.map((shape) => shape.id));
|
||||
} else if (selectedToolSettings.type === "toggle") {
|
||||
onShapesEdit(
|
||||
editingShapes.map((shape) => ({ ...shape, visible: !shape.visible }))
|
||||
);
|
||||
}
|
||||
setEditingShapes([]);
|
||||
useEffect(() => {
|
||||
if (!isEditing) {
|
||||
return;
|
||||
}
|
||||
}, [editingShapes, onShapesRemove, onShapesEdit, selectedToolSettings]);
|
||||
|
||||
const handleShapeDraw = useCallback(
|
||||
(brushState, mapBrushPosition) => {
|
||||
function startShape() {
|
||||
const brushPosition = getBrushPositionForTool(
|
||||
mapBrushPosition,
|
||||
selectedToolId,
|
||||
selectedToolSettings,
|
||||
gridSize,
|
||||
shapes
|
||||
);
|
||||
if (selectedToolSettings.type === "add") {
|
||||
setDrawingShape({
|
||||
type: "fog",
|
||||
data: { points: [brushPosition] },
|
||||
strokeWidth: 0.5,
|
||||
color: "black",
|
||||
blend: false,
|
||||
id: shortid.generate(),
|
||||
visible: true,
|
||||
});
|
||||
}
|
||||
setIsBrushDown(true);
|
||||
const mapStage = mapStageRef.current;
|
||||
|
||||
function getBrushPosition() {
|
||||
const mapImage = mapStage.findOne("#mapImage");
|
||||
return getBrushPositionForTool(
|
||||
getRelativePointerPositionNormalized(mapImage),
|
||||
selectedToolId,
|
||||
selectedToolSettings,
|
||||
gridSize,
|
||||
shapes
|
||||
);
|
||||
}
|
||||
|
||||
function handleBrushDown() {
|
||||
const brushPosition = getBrushPosition();
|
||||
if (selectedToolSettings.type === "brush") {
|
||||
setDrawingShape({
|
||||
type: "fog",
|
||||
data: {
|
||||
points: [brushPosition],
|
||||
holes: [],
|
||||
},
|
||||
strokeWidth: 0.5,
|
||||
color: selectedToolSettings.useFogSubtract ? "red" : "black",
|
||||
blend: false,
|
||||
id: shortid.generate(),
|
||||
visible: true,
|
||||
});
|
||||
}
|
||||
setIsBrushDown(true);
|
||||
}
|
||||
|
||||
function continueShape() {
|
||||
const brushPosition = getBrushPositionForTool(
|
||||
mapBrushPosition,
|
||||
selectedToolId,
|
||||
selectedToolSettings,
|
||||
gridSize,
|
||||
shapes
|
||||
);
|
||||
if (selectedToolSettings.type === "add") {
|
||||
setDrawingShape((prevShape) => {
|
||||
const prevPoints = prevShape.data.points;
|
||||
if (
|
||||
comparePoints(
|
||||
prevPoints[prevPoints.length - 1],
|
||||
brushPosition,
|
||||
0.001
|
||||
)
|
||||
) {
|
||||
return prevShape;
|
||||
}
|
||||
return {
|
||||
...prevShape,
|
||||
data: { points: [...prevPoints, brushPosition] },
|
||||
};
|
||||
});
|
||||
}
|
||||
function handleBrushMove() {
|
||||
if (
|
||||
selectedToolSettings.type === "brush" &&
|
||||
isBrushDown &&
|
||||
drawingShape
|
||||
) {
|
||||
const brushPosition = getBrushPosition();
|
||||
setDrawingShape((prevShape) => {
|
||||
const prevPoints = prevShape.data.points;
|
||||
if (
|
||||
comparePoints(
|
||||
prevPoints[prevPoints.length - 1],
|
||||
brushPosition,
|
||||
0.001
|
||||
)
|
||||
) {
|
||||
return prevShape;
|
||||
}
|
||||
return {
|
||||
...prevShape,
|
||||
data: {
|
||||
...prevShape.data,
|
||||
points: [...prevPoints, brushPosition],
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function endShape() {
|
||||
if (selectedToolSettings.type === "add" && drawingShape) {
|
||||
if (drawingShape.data.points.length > 1) {
|
||||
const shape = {
|
||||
...drawingShape,
|
||||
data: {
|
||||
points: simplifyPoints(
|
||||
drawingShape.data.points,
|
||||
gridSize,
|
||||
// Downscale fog as smoothing doesn't currently work with edge snapping
|
||||
stageScale / 2
|
||||
),
|
||||
},
|
||||
};
|
||||
function handleBrushUp() {
|
||||
if (selectedToolSettings.type === "brush" && drawingShape) {
|
||||
const subtract = selectedToolSettings.useFogSubtract;
|
||||
|
||||
if (drawingShape.data.points.length > 1) {
|
||||
let shapeData = {};
|
||||
if (subtract) {
|
||||
shapeData = { id: drawingShape.id, type: drawingShape.type };
|
||||
} else {
|
||||
shapeData = { ...drawingShape, color: "black" };
|
||||
}
|
||||
const shape = {
|
||||
...shapeData,
|
||||
data: {
|
||||
...drawingShape.data,
|
||||
points: simplifyPoints(
|
||||
drawingShape.data.points,
|
||||
gridSize,
|
||||
// Downscale fog as smoothing doesn't currently work with edge snapping
|
||||
stageScale / 2
|
||||
),
|
||||
},
|
||||
};
|
||||
if (subtract) {
|
||||
onShapeSubtract(shape);
|
||||
} else {
|
||||
onShapeAdd(shape);
|
||||
}
|
||||
}
|
||||
setDrawingShape(null);
|
||||
handleBrushUp();
|
||||
}
|
||||
|
||||
switch (brushState) {
|
||||
case "first":
|
||||
startShape();
|
||||
return;
|
||||
case "drawing":
|
||||
continueShape();
|
||||
return;
|
||||
case "last":
|
||||
endShape();
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
// Erase
|
||||
if (editingShapes.length > 0) {
|
||||
if (selectedToolSettings.type === "remove") {
|
||||
onShapesRemove(editingShapes.map((shape) => shape.id));
|
||||
} else if (selectedToolSettings.type === "toggle") {
|
||||
onShapesEdit(
|
||||
editingShapes.map((shape) => ({
|
||||
...shape,
|
||||
visible: !shape.visible,
|
||||
}))
|
||||
);
|
||||
}
|
||||
setEditingShapes([]);
|
||||
}
|
||||
},
|
||||
[
|
||||
selectedToolId,
|
||||
selectedToolSettings,
|
||||
gridSize,
|
||||
stageScale,
|
||||
onShapeAdd,
|
||||
shapes,
|
||||
drawingShape,
|
||||
handleBrushUp,
|
||||
]
|
||||
);
|
||||
|
||||
useMapBrush(isEditing, handleShapeDraw);
|
||||
setIsBrushDown(false);
|
||||
}
|
||||
|
||||
function handlePolygonClick() {
|
||||
if (selectedToolSettings.type === "polygon") {
|
||||
const brushPosition = getBrushPosition();
|
||||
setDrawingShape((prevDrawingShape) => {
|
||||
if (prevDrawingShape) {
|
||||
return {
|
||||
...prevDrawingShape,
|
||||
data: {
|
||||
...prevDrawingShape.data,
|
||||
points: [...prevDrawingShape.data.points, brushPosition],
|
||||
},
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
type: "fog",
|
||||
data: {
|
||||
points: [brushPosition, brushPosition],
|
||||
holes: [],
|
||||
},
|
||||
strokeWidth: 0.5,
|
||||
color: selectedToolSettings.useFogSubtract ? "red" : "black",
|
||||
blend: false,
|
||||
id: shortid.generate(),
|
||||
visible: true,
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function handlePolygonMove() {
|
||||
if (selectedToolSettings.type === "polygon" && drawingShape) {
|
||||
const brushPosition = getBrushPosition();
|
||||
setDrawingShape((prevShape) => {
|
||||
if (!prevShape) {
|
||||
return;
|
||||
}
|
||||
return {
|
||||
...prevShape,
|
||||
data: {
|
||||
...prevShape.data,
|
||||
points: [...prevShape.data.points.slice(0, -1), brushPosition],
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
interactionEmitter.on("dragStart", handleBrushDown);
|
||||
interactionEmitter.on("drag", handleBrushMove);
|
||||
interactionEmitter.on("dragEnd", handleBrushUp);
|
||||
// Use mouse events for polygon and erase to allow for single clicks
|
||||
mapStage.on("mousedown touchstart", handlePolygonMove);
|
||||
mapStage.on("mousemove touchmove", handlePolygonMove);
|
||||
mapStage.on("click tap", handlePolygonClick);
|
||||
|
||||
return () => {
|
||||
interactionEmitter.off("dragStart", handleBrushDown);
|
||||
interactionEmitter.off("drag", handleBrushMove);
|
||||
interactionEmitter.off("dragEnd", handleBrushUp);
|
||||
mapStage.off("mousedown touchstart", handlePolygonMove);
|
||||
mapStage.off("mousemove touchmove", handlePolygonMove);
|
||||
mapStage.off("click tap", handlePolygonClick);
|
||||
};
|
||||
}, [
|
||||
mapStageRef,
|
||||
isEditing,
|
||||
drawingShape,
|
||||
editingShapes,
|
||||
gridSize,
|
||||
isBrushDown,
|
||||
onShapeAdd,
|
||||
onShapeSubtract,
|
||||
onShapesEdit,
|
||||
onShapesRemove,
|
||||
selectedToolId,
|
||||
selectedToolSettings,
|
||||
shapes,
|
||||
stageScale,
|
||||
interactionEmitter,
|
||||
]);
|
||||
|
||||
const finishDrawingPolygon = useCallback(() => {
|
||||
const subtract = selectedToolSettings.useFogSubtract;
|
||||
const data = {
|
||||
...drawingShape.data,
|
||||
// Remove the last point as it hasn't been placed yet
|
||||
points: drawingShape.data.points.slice(0, -1),
|
||||
};
|
||||
if (subtract) {
|
||||
onShapeSubtract({
|
||||
id: drawingShape.id,
|
||||
type: drawingShape.type,
|
||||
data: data,
|
||||
});
|
||||
} else {
|
||||
onShapeAdd({ ...drawingShape, data: data, color: "black" });
|
||||
}
|
||||
|
||||
setDrawingShape(null);
|
||||
}, [selectedToolSettings, drawingShape, onShapeSubtract, onShapeAdd]);
|
||||
|
||||
// Add keyboard shortcuts
|
||||
useEffect(() => {
|
||||
function handleKeyDown({ key }) {
|
||||
if (
|
||||
key === "Enter" &&
|
||||
selectedToolSettings.type === "polygon" &&
|
||||
drawingShape
|
||||
) {
|
||||
finishDrawingPolygon();
|
||||
}
|
||||
if (key === "Escape" && drawingShape) {
|
||||
setDrawingShape(null);
|
||||
}
|
||||
if (key === "Alt" && drawingShape) {
|
||||
updateShapeColor();
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeyUp({ key }) {
|
||||
if (key === "Alt" && drawingShape) {
|
||||
updateShapeColor();
|
||||
}
|
||||
}
|
||||
|
||||
function updateShapeColor() {
|
||||
setDrawingShape((prevShape) => {
|
||||
if (!prevShape) {
|
||||
return;
|
||||
}
|
||||
return {
|
||||
...prevShape,
|
||||
color: selectedToolSettings.useFogSubtract ? "black" : "red",
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
interactionEmitter.on("keyDown", handleKeyDown);
|
||||
interactionEmitter.on("keyUp", handleKeyUp);
|
||||
return () => {
|
||||
interactionEmitter.off("keyDown", handleKeyDown);
|
||||
interactionEmitter.off("keyUp", handleKeyUp);
|
||||
};
|
||||
}, [
|
||||
finishDrawingPolygon,
|
||||
interactionEmitter,
|
||||
drawingShape,
|
||||
selectedToolSettings,
|
||||
]);
|
||||
|
||||
function handleShapeOver(shape, isDown) {
|
||||
if (shouldHover && isDown) {
|
||||
@ -162,18 +322,23 @@ function MapFog({
|
||||
}
|
||||
}
|
||||
|
||||
function reducePoints(acc, point) {
|
||||
return [...acc, point.x * mapWidth, point.y * mapHeight];
|
||||
}
|
||||
|
||||
function renderShape(shape) {
|
||||
const points = shape.data.points.reduce(reducePoints, []);
|
||||
const holes =
|
||||
shape.data.holes &&
|
||||
shape.data.holes.map((hole) => hole.reduce(reducePoints, []));
|
||||
return (
|
||||
<Line
|
||||
<HoleyLine
|
||||
key={shape.id}
|
||||
onMouseMove={() => handleShapeOver(shape, isBrushDown)}
|
||||
onTouchOver={() => handleShapeOver(shape, isBrushDown)}
|
||||
onMouseDown={() => handleShapeOver(shape, true)}
|
||||
onTouchStart={() => handleShapeOver(shape, true)}
|
||||
points={shape.data.points.reduce(
|
||||
(acc, point) => [...acc, point.x * mapWidth, point.y * mapHeight],
|
||||
[]
|
||||
)}
|
||||
points={points}
|
||||
stroke={colors[shape.color] || shape.color}
|
||||
fill={colors[shape.color] || shape.color}
|
||||
closed
|
||||
@ -188,6 +353,7 @@ function MapFog({
|
||||
opacity={isEditing ? 0.5 : 1}
|
||||
fillPatternImage={patternImage}
|
||||
fillPriority={isEditing && !shape.visible ? "pattern" : "color"}
|
||||
holes={holes}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -200,10 +366,37 @@ function MapFog({
|
||||
return renderShape(editingShape);
|
||||
}
|
||||
|
||||
function renderPolygonAcceptTick(shape) {
|
||||
if (shape.data.points.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const isCross = shape.data.points.length < 4;
|
||||
return (
|
||||
<Tick
|
||||
x={shape.data.points[0].x * mapWidth}
|
||||
y={shape.data.points[0].y * mapHeight}
|
||||
scale={1 / stageScale}
|
||||
cross={isCross}
|
||||
onClick={(e) => {
|
||||
e.cancelBubble = true;
|
||||
if (isCross) {
|
||||
setDrawingShape(null);
|
||||
} else {
|
||||
finishDrawingPolygon();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Group>
|
||||
{shapes.map(renderShape)}
|
||||
{drawingShape && renderShape(drawingShape)}
|
||||
{drawingShape &&
|
||||
selectedToolSettings &&
|
||||
selectedToolSettings.type === "polygon" &&
|
||||
renderPolygonAcceptTick(drawingShape)}
|
||||
{editingShapes.length > 0 && editingShapes.map(renderEditingShape)}
|
||||
</Group>
|
||||
);
|
||||
|
@ -71,7 +71,7 @@ function MapGrid({ map, gridSize }) {
|
||||
points={[x * lineSpacingX, 0, x * lineSpacingX, mapHeight]}
|
||||
stroke={isImageLight ? "black" : "white"}
|
||||
strokeWidth={getStrokeWidth(0.1, gridSize, mapWidth, mapHeight)}
|
||||
opacity={0.8}
|
||||
opacity={0.5}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -82,7 +82,7 @@ function MapGrid({ map, gridSize }) {
|
||||
points={[0, y * lineSpacingY, mapWidth, y * lineSpacingY]}
|
||||
stroke={isImageLight ? "black" : "white"}
|
||||
strokeWidth={getStrokeWidth(0.1, gridSize, mapWidth, mapHeight)}
|
||||
opacity={0.8}
|
||||
opacity={0.5}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import { useGesture } from "react-use-gesture";
|
||||
import ReactResizeDetector from "react-resize-detector";
|
||||
import useImage from "use-image";
|
||||
import { Stage, Layer, Image } from "react-konva";
|
||||
import { EventEmitter } from "events";
|
||||
|
||||
import usePreventOverscroll from "../../helpers/usePreventOverscroll";
|
||||
import useDataSource from "../../helpers/useDataSource";
|
||||
@ -11,7 +12,9 @@ import useDataSource from "../../helpers/useDataSource";
|
||||
import { mapSources as defaultMapSources } from "../../maps";
|
||||
|
||||
import { MapInteractionProvider } from "../../contexts/MapInteractionContext";
|
||||
import MapStageContext from "../../contexts/MapStageContext";
|
||||
import MapStageContext, {
|
||||
MapStageProvider,
|
||||
} from "../../contexts/MapStageContext";
|
||||
import AuthContext from "../../contexts/AuthContext";
|
||||
|
||||
const wheelZoomSpeed = -0.001;
|
||||
@ -19,22 +22,26 @@ const touchZoomSpeed = 0.005;
|
||||
const minZoom = 0.1;
|
||||
const maxZoom = 5;
|
||||
|
||||
function MapInteraction({ map, children, controls, selectedToolId }) {
|
||||
function MapInteraction({
|
||||
map,
|
||||
children,
|
||||
controls,
|
||||
selectedToolId,
|
||||
onSelectedToolChange,
|
||||
disabledControls,
|
||||
}) {
|
||||
const mapSource = useDataSource(map, defaultMapSources);
|
||||
const [mapSourceImage] = useImage(mapSource);
|
||||
|
||||
const [stageWidth, setStageWidth] = useState(1);
|
||||
const [stageHeight, setStageHeight] = useState(1);
|
||||
const [stageScale, setStageScale] = useState(1);
|
||||
// "none" | "first" | "dragging" | "last"
|
||||
const [stageDragState, setStageDragState] = useState("none");
|
||||
const [preventMapInteraction, setPreventMapInteraction] = useState(false);
|
||||
|
||||
const stageWidthRef = useRef(stageWidth);
|
||||
const stageHeightRef = useRef(stageHeight);
|
||||
// Avoid state udpates when panning the map by using a ref and updating the konva element directly
|
||||
const stageTranslateRef = useRef({ x: 0, y: 0 });
|
||||
const mapDragPositionRef = useRef({ x: 0, y: 0 });
|
||||
|
||||
// Reset transform when map changes
|
||||
useEffect(() => {
|
||||
@ -54,36 +61,20 @@ function MapInteraction({ map, children, controls, selectedToolId }) {
|
||||
}
|
||||
}, [map]);
|
||||
|
||||
// Convert a client space XY to be normalized to the map image
|
||||
function getMapDragPosition(xy) {
|
||||
const [x, y] = xy;
|
||||
const container = containerRef.current;
|
||||
const mapImage = mapImageRef.current;
|
||||
if (container && mapImage) {
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
const mapRect = mapImage.getClientRect();
|
||||
|
||||
const offsetX = x - containerRect.left - mapRect.x;
|
||||
const offsetY = y - containerRect.top - mapRect.y;
|
||||
|
||||
const normalizedX = offsetX / mapRect.width;
|
||||
const normalizedY = offsetY / mapRect.height;
|
||||
|
||||
return { x: normalizedX, y: normalizedY };
|
||||
}
|
||||
}
|
||||
|
||||
const pinchPreviousDistanceRef = useRef();
|
||||
const pinchPreviousOriginRef = useRef();
|
||||
const isInteractingCanvas = useRef(false);
|
||||
const isInteractingWithCanvas = useRef(false);
|
||||
const previousSelectedToolRef = useRef(selectedToolId);
|
||||
|
||||
const [interactionEmitter] = useState(new EventEmitter());
|
||||
|
||||
const bind = useGesture({
|
||||
onWheelStart: ({ event }) => {
|
||||
isInteractingCanvas.current =
|
||||
isInteractingWithCanvas.current =
|
||||
event.target === mapLayerRef.current.getCanvas()._canvas;
|
||||
},
|
||||
onWheel: ({ delta }) => {
|
||||
if (preventMapInteraction || !isInteractingCanvas.current) {
|
||||
if (preventMapInteraction || !isInteractingWithCanvas.current) {
|
||||
return;
|
||||
}
|
||||
const newScale = Math.min(
|
||||
@ -92,6 +83,11 @@ function MapInteraction({ map, children, controls, selectedToolId }) {
|
||||
);
|
||||
setStageScale(newScale);
|
||||
},
|
||||
onPinchStart: () => {
|
||||
// Change to pan tool when pinching and zooming
|
||||
previousSelectedToolRef.current = selectedToolId;
|
||||
onSelectedToolChange("pan");
|
||||
},
|
||||
onPinch: ({ da, origin, first }) => {
|
||||
const [distance] = da;
|
||||
const [originX, originY] = origin;
|
||||
@ -125,12 +121,19 @@ function MapInteraction({ map, children, controls, selectedToolId }) {
|
||||
pinchPreviousDistanceRef.current = distance;
|
||||
pinchPreviousOriginRef.current = { x: originX, y: originY };
|
||||
},
|
||||
onPinchEnd: () => {
|
||||
onSelectedToolChange(previousSelectedToolRef.current);
|
||||
},
|
||||
onDragStart: ({ event }) => {
|
||||
isInteractingCanvas.current =
|
||||
isInteractingWithCanvas.current =
|
||||
event.target === mapLayerRef.current.getCanvas()._canvas;
|
||||
},
|
||||
onDrag: ({ delta, xy, first, last, pinching }) => {
|
||||
if (preventMapInteraction || pinching || !isInteractingCanvas.current) {
|
||||
onDrag: ({ delta, first, last, pinching }) => {
|
||||
if (
|
||||
preventMapInteraction ||
|
||||
pinching ||
|
||||
!isInteractingWithCanvas.current
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -147,15 +150,14 @@ function MapInteraction({ map, children, controls, selectedToolId }) {
|
||||
layer.draw();
|
||||
stageTranslateRef.current = newTranslate;
|
||||
}
|
||||
mapDragPositionRef.current = getMapDragPosition(xy);
|
||||
const newDragState = first ? "first" : last ? "last" : "dragging";
|
||||
if (stageDragState !== newDragState) {
|
||||
setStageDragState(newDragState);
|
||||
if (first) {
|
||||
interactionEmitter.emit("dragStart");
|
||||
} else if (last) {
|
||||
interactionEmitter.emit("dragEnd");
|
||||
} else {
|
||||
interactionEmitter.emit("drag");
|
||||
}
|
||||
},
|
||||
onDragEnd: () => {
|
||||
setStageDragState("none");
|
||||
},
|
||||
});
|
||||
|
||||
function handleResize(width, height) {
|
||||
@ -165,13 +167,53 @@ function MapInteraction({ map, children, controls, selectedToolId }) {
|
||||
stageHeightRef.current = height;
|
||||
}
|
||||
|
||||
function handleKeyDown(event) {
|
||||
// Change to pan tool when pressing space
|
||||
if (event.key === " " && selectedToolId === "pan") {
|
||||
// Stop active state on pan icon from being selected
|
||||
event.preventDefault();
|
||||
}
|
||||
if (
|
||||
event.key === " " &&
|
||||
selectedToolId !== "pan" &&
|
||||
!disabledControls.includes("pan")
|
||||
) {
|
||||
event.preventDefault();
|
||||
previousSelectedToolRef.current = selectedToolId;
|
||||
onSelectedToolChange("pan");
|
||||
}
|
||||
|
||||
// Basic keyboard shortcuts
|
||||
if (event.key === "w" && !disabledControls.includes("pan")) {
|
||||
onSelectedToolChange("pan");
|
||||
}
|
||||
if (event.key === "d" && !disabledControls.includes("drawing")) {
|
||||
onSelectedToolChange("drawing");
|
||||
}
|
||||
if (event.key === "f" && !disabledControls.includes("fog")) {
|
||||
onSelectedToolChange("fog");
|
||||
}
|
||||
if (event.key === "m" && !disabledControls.includes("measure")) {
|
||||
onSelectedToolChange("measure");
|
||||
}
|
||||
|
||||
interactionEmitter.emit("keyDown", event);
|
||||
}
|
||||
|
||||
function handleKeyUp(event) {
|
||||
if (event.key === " " && selectedToolId === "pan") {
|
||||
onSelectedToolChange(previousSelectedToolRef.current);
|
||||
}
|
||||
interactionEmitter.emit("keyUp", event);
|
||||
}
|
||||
|
||||
function getCursorForTool(tool) {
|
||||
switch (tool) {
|
||||
case "pan":
|
||||
return "move";
|
||||
case "fog":
|
||||
case "brush":
|
||||
case "shape":
|
||||
case "drawing":
|
||||
case "measure":
|
||||
return "crosshair";
|
||||
default:
|
||||
return "default";
|
||||
@ -194,11 +236,10 @@ function MapInteraction({ map, children, controls, selectedToolId }) {
|
||||
stageScale,
|
||||
stageWidth,
|
||||
stageHeight,
|
||||
stageDragState,
|
||||
setPreventMapInteraction,
|
||||
mapWidth,
|
||||
mapHeight,
|
||||
mapDragPositionRef,
|
||||
interactionEmitter,
|
||||
};
|
||||
|
||||
return (
|
||||
@ -208,10 +249,14 @@ function MapInteraction({ map, children, controls, selectedToolId }) {
|
||||
position: "relative",
|
||||
cursor: getCursorForTool(selectedToolId),
|
||||
touchAction: "none",
|
||||
outline: "none",
|
||||
}}
|
||||
ref={containerRef}
|
||||
{...bind()}
|
||||
className="map"
|
||||
tabIndex={1}
|
||||
onKeyDown={handleKeyDown}
|
||||
onKeyUp={handleKeyUp}
|
||||
>
|
||||
<ReactResizeDetector handleWidth handleHeight onResize={handleResize}>
|
||||
<Stage
|
||||
@ -234,7 +279,9 @@ function MapInteraction({ map, children, controls, selectedToolId }) {
|
||||
{/* Forward auth context to konva elements */}
|
||||
<AuthContext.Provider value={auth}>
|
||||
<MapInteractionProvider value={mapInteraction}>
|
||||
{children}
|
||||
<MapStageProvider value={mapStageRef}>
|
||||
{children}
|
||||
</MapStageProvider>
|
||||
</MapInteractionProvider>
|
||||
</AuthContext.Provider>
|
||||
</Layer>
|
||||
|
63
src/components/map/MapLoadingOverlay.js
Normal file
63
src/components/map/MapLoadingOverlay.js
Normal file
@ -0,0 +1,63 @@
|
||||
import React, { useContext, useEffect, useRef } from "react";
|
||||
import { Box, Progress } from "theme-ui";
|
||||
|
||||
import Spinner from "../Spinner";
|
||||
import MapLoadingContext from "../../contexts/MapLoadingContext";
|
||||
|
||||
function MapLoadingOverlay() {
|
||||
const { isLoading, loadingProgressRef } = useContext(MapLoadingContext);
|
||||
|
||||
const requestRef = useRef();
|
||||
const progressBarRef = useRef();
|
||||
|
||||
// Use an animation frame to update the progress bar
|
||||
// This bypasses react allowing the animation to be smooth
|
||||
useEffect(() => {
|
||||
function animate() {
|
||||
if (!isLoading) {
|
||||
return;
|
||||
}
|
||||
requestRef.current = requestAnimationFrame(animate);
|
||||
if (progressBarRef.current) {
|
||||
progressBarRef.current.value = loadingProgressRef.current;
|
||||
}
|
||||
}
|
||||
|
||||
requestRef.current = requestAnimationFrame(animate);
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(requestRef.current);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isLoading]);
|
||||
|
||||
return (
|
||||
isLoading && (
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
top: 0,
|
||||
left: 0,
|
||||
flexDirection: "column",
|
||||
}}
|
||||
bg="muted"
|
||||
>
|
||||
<Spinner />
|
||||
<Progress
|
||||
ref={progressBarRef}
|
||||
max={1}
|
||||
value={0}
|
||||
m={2}
|
||||
sx={{ width: "32px" }}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default MapLoadingOverlay;
|
142
src/components/map/MapMeasure.js
Normal file
142
src/components/map/MapMeasure.js
Normal file
@ -0,0 +1,142 @@
|
||||
import React, { useContext, useState, useEffect } from "react";
|
||||
import { Group, Line, Text, Label, Tag } from "react-konva";
|
||||
|
||||
import MapInteractionContext from "../../contexts/MapInteractionContext";
|
||||
import MapStageContext from "../../contexts/MapStageContext";
|
||||
|
||||
import {
|
||||
getBrushPositionForTool,
|
||||
getDefaultShapeData,
|
||||
getUpdatedShapeData,
|
||||
getStrokeWidth,
|
||||
} from "../../helpers/drawing";
|
||||
import { getRelativePointerPositionNormalized } from "../../helpers/konva";
|
||||
import * as Vector2 from "../../helpers/vector2";
|
||||
|
||||
function MapMeasure({ selectedToolSettings, active, gridSize }) {
|
||||
const { stageScale, mapWidth, mapHeight, interactionEmitter } = useContext(
|
||||
MapInteractionContext
|
||||
);
|
||||
const mapStageRef = useContext(MapStageContext);
|
||||
const [drawingShapeData, setDrawingShapeData] = useState(null);
|
||||
const [isBrushDown, setIsBrushDown] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!active) {
|
||||
return;
|
||||
}
|
||||
const mapStage = mapStageRef.current;
|
||||
|
||||
function getBrushPosition() {
|
||||
const mapImage = mapStage.findOne("#mapImage");
|
||||
return getBrushPositionForTool(
|
||||
getRelativePointerPositionNormalized(mapImage),
|
||||
"drawing",
|
||||
{ type: "line" },
|
||||
gridSize,
|
||||
[]
|
||||
);
|
||||
}
|
||||
|
||||
function handleBrushDown() {
|
||||
const brushPosition = getBrushPosition();
|
||||
const { points } = getDefaultShapeData("line", brushPosition);
|
||||
const length = 0;
|
||||
setDrawingShapeData({ length, points });
|
||||
setIsBrushDown(true);
|
||||
}
|
||||
|
||||
function handleBrushMove() {
|
||||
const brushPosition = getBrushPosition();
|
||||
if (isBrushDown && drawingShapeData) {
|
||||
const { points } = getUpdatedShapeData(
|
||||
"line",
|
||||
drawingShapeData,
|
||||
brushPosition,
|
||||
gridSize
|
||||
);
|
||||
const length = Vector2.distance(
|
||||
Vector2.divide(points[0], gridSize),
|
||||
Vector2.divide(points[1], gridSize),
|
||||
selectedToolSettings.type
|
||||
);
|
||||
setDrawingShapeData({
|
||||
length,
|
||||
points,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function handleBrushUp() {
|
||||
setDrawingShapeData(null);
|
||||
setIsBrushDown(false);
|
||||
}
|
||||
|
||||
interactionEmitter.on("dragStart", handleBrushDown);
|
||||
interactionEmitter.on("drag", handleBrushMove);
|
||||
interactionEmitter.on("dragEnd", handleBrushUp);
|
||||
|
||||
return () => {
|
||||
interactionEmitter.off("dragStart", handleBrushDown);
|
||||
interactionEmitter.off("drag", handleBrushMove);
|
||||
interactionEmitter.off("dragEnd", handleBrushUp);
|
||||
};
|
||||
}, [
|
||||
drawingShapeData,
|
||||
gridSize,
|
||||
isBrushDown,
|
||||
mapStageRef,
|
||||
interactionEmitter,
|
||||
active,
|
||||
selectedToolSettings,
|
||||
]);
|
||||
|
||||
function renderShape(shapeData) {
|
||||
const linePoints = shapeData.points.reduce(
|
||||
(acc, point) => [...acc, point.x * mapWidth, point.y * mapHeight],
|
||||
[]
|
||||
);
|
||||
|
||||
const lineCenter = Vector2.multiply(
|
||||
Vector2.divide(Vector2.add(shapeData.points[0], shapeData.points[1]), 2),
|
||||
{ x: mapWidth, y: mapHeight }
|
||||
);
|
||||
|
||||
return (
|
||||
<Group>
|
||||
<Line
|
||||
points={linePoints}
|
||||
strokeWidth={getStrokeWidth(1.5, gridSize, mapWidth, mapHeight)}
|
||||
stroke="hsla(230, 25%, 18%, 0.8)"
|
||||
lineCap="round"
|
||||
/>
|
||||
<Line
|
||||
points={linePoints}
|
||||
strokeWidth={getStrokeWidth(0.25, gridSize, mapWidth, mapHeight)}
|
||||
stroke="white"
|
||||
lineCap="round"
|
||||
/>
|
||||
<Label
|
||||
x={lineCenter.x}
|
||||
y={lineCenter.y}
|
||||
offsetX={26}
|
||||
offsetY={26}
|
||||
scaleX={1 / stageScale}
|
||||
scaleY={1 / stageScale}
|
||||
>
|
||||
<Tag fill="hsla(230, 25%, 18%, 0.8)" cornerRadius={4} />
|
||||
<Text
|
||||
text={shapeData.length.toFixed(2)}
|
||||
fill="white"
|
||||
fontSize={24}
|
||||
padding={4}
|
||||
/>
|
||||
</Label>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
return <Group>{drawingShapeData && renderShape(drawingShapeData)}</Group>;
|
||||
}
|
||||
|
||||
export default MapMeasure;
|
@ -79,7 +79,13 @@ function MapMenu({
|
||||
}}
|
||||
contentRef={handleModalContent}
|
||||
>
|
||||
{children}
|
||||
{/* Stop keyboard events when modal is open to prevent shortcuts from triggering */}
|
||||
<div
|
||||
onKeyDown={(e) => e.stopPropagation()}
|
||||
onKeyUp={(e) => e.stopPropagation()}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
@ -3,6 +3,8 @@ import { Flex, Box, Label, Input, Checkbox, IconButton } from "theme-ui";
|
||||
|
||||
import ExpandMoreIcon from "../../icons/ExpandMoreIcon";
|
||||
|
||||
import { isEmpty } from "../../helpers/shared";
|
||||
|
||||
import Divider from "../Divider";
|
||||
|
||||
function MapSettings({
|
||||
@ -24,6 +26,9 @@ function MapSettings({
|
||||
}
|
||||
}
|
||||
|
||||
const mapEmpty = !map || isEmpty(map);
|
||||
const mapStateEmpty = !mapState || isEmpty(mapState);
|
||||
|
||||
return (
|
||||
<Flex sx={{ flexDirection: "column" }}>
|
||||
<Flex>
|
||||
@ -36,7 +41,7 @@ function MapSettings({
|
||||
onChange={(e) =>
|
||||
onSettingsChange("gridX", parseInt(e.target.value))
|
||||
}
|
||||
disabled={!map || map.type === "default"}
|
||||
disabled={mapEmpty || map.type === "default"}
|
||||
min={1}
|
||||
my={1}
|
||||
/>
|
||||
@ -50,7 +55,7 @@ function MapSettings({
|
||||
onChange={(e) =>
|
||||
onSettingsChange("gridY", parseInt(e.target.value))
|
||||
}
|
||||
disabled={!map || map.type === "default"}
|
||||
disabled={mapEmpty || map.type === "default"}
|
||||
min={1}
|
||||
my={1}
|
||||
/>
|
||||
@ -64,7 +69,7 @@ function MapSettings({
|
||||
name="name"
|
||||
value={(map && map.name) || ""}
|
||||
onChange={(e) => onSettingsChange("name", e.target.value)}
|
||||
disabled={!map || map.type === "default"}
|
||||
disabled={mapEmpty || map.type === "default"}
|
||||
my={1}
|
||||
/>
|
||||
</Box>
|
||||
@ -72,7 +77,7 @@ function MapSettings({
|
||||
<Label>
|
||||
<Checkbox
|
||||
checked={map && map.showGrid}
|
||||
disabled={!map || map.type === "default"}
|
||||
disabled={mapEmpty || map.type === "default"}
|
||||
onChange={(e) => onSettingsChange("showGrid", e.target.checked)}
|
||||
/>
|
||||
Show Grid
|
||||
@ -84,24 +89,28 @@ function MapSettings({
|
||||
<Flex my={1}>
|
||||
<Label>
|
||||
<Checkbox
|
||||
checked={mapState && mapState.editFlags.includes("fog")}
|
||||
disabled={!mapState}
|
||||
checked={!mapStateEmpty && mapState.editFlags.includes("fog")}
|
||||
disabled={mapStateEmpty}
|
||||
onChange={(e) => handleFlagChange(e, "fog")}
|
||||
/>
|
||||
Fog
|
||||
</Label>
|
||||
<Label>
|
||||
<Checkbox
|
||||
checked={mapState && mapState.editFlags.includes("drawing")}
|
||||
disabled={mapState === null}
|
||||
checked={
|
||||
!mapStateEmpty && mapState.editFlags.includes("drawing")
|
||||
}
|
||||
disabled={mapStateEmpty}
|
||||
onChange={(e) => handleFlagChange(e, "drawing")}
|
||||
/>
|
||||
Drawings
|
||||
</Label>
|
||||
<Label>
|
||||
<Checkbox
|
||||
checked={mapState && mapState.editFlags.includes("tokens")}
|
||||
disabled={!mapState}
|
||||
checked={
|
||||
!mapStateEmpty && mapState.editFlags.includes("tokens")
|
||||
}
|
||||
disabled={mapStateEmpty}
|
||||
onChange={(e) => handleFlagChange(e, "tokens")}
|
||||
/>
|
||||
Tokens
|
||||
@ -122,7 +131,6 @@ function MapSettings({
|
||||
}}
|
||||
aria-label={showMore ? "Show Less" : "Show More"}
|
||||
title={showMore ? "Show Less" : "Show More"}
|
||||
disabled={!map}
|
||||
>
|
||||
<ExpandMoreIcon />
|
||||
</IconButton>
|
||||
|
@ -58,20 +58,23 @@ function MapTiles({
|
||||
>
|
||||
<AddIcon large />
|
||||
</Flex>
|
||||
{maps.map((map) => (
|
||||
<MapTile
|
||||
key={map.id}
|
||||
map={map}
|
||||
mapState={
|
||||
selectedMap && map.id === selectedMap.id && selectedMapState
|
||||
}
|
||||
isSelected={selectedMap && map.id === selectedMap.id}
|
||||
onMapSelect={onMapSelect}
|
||||
onMapRemove={onMapRemove}
|
||||
onMapReset={onMapReset}
|
||||
onDone={onDone}
|
||||
/>
|
||||
))}
|
||||
{maps.map((map) => {
|
||||
const isSelected = selectedMap && map.id === selectedMap.id;
|
||||
return (
|
||||
<MapTile
|
||||
key={map.id}
|
||||
// TODO: Move to selected map here and fix url error
|
||||
// when done is clicked
|
||||
map={map}
|
||||
mapState={isSelected && selectedMapState}
|
||||
isSelected={isSelected}
|
||||
onMapSelect={onMapSelect}
|
||||
onMapRemove={onMapRemove}
|
||||
onMapReset={onMapReset}
|
||||
onDone={onDone}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
</SimpleBar>
|
||||
{databaseStatus === "disabled" && (
|
||||
|
@ -140,7 +140,7 @@ function MapToken({
|
||||
}
|
||||
|
||||
function handlePointerOut() {
|
||||
if (!draggable) {
|
||||
if (tokenOpacity !== 1.0) {
|
||||
setTokenOpacity(1.0);
|
||||
}
|
||||
}
|
||||
|
@ -19,12 +19,9 @@ function SelectMapButton({
|
||||
currentMapState && updateMapState(currentMapState.mapId, currentMapState);
|
||||
setIsModalOpen(true);
|
||||
}
|
||||
function closeModal() {
|
||||
setIsModalOpen(false);
|
||||
}
|
||||
|
||||
function handleDone() {
|
||||
closeModal();
|
||||
setIsModalOpen(false);
|
||||
}
|
||||
|
||||
return (
|
||||
@ -38,7 +35,6 @@ function SelectMapButton({
|
||||
</IconButton>
|
||||
<SelectMapModal
|
||||
isOpen={isModalOpen}
|
||||
onRequestClose={closeModal}
|
||||
onDone={handleDone}
|
||||
onMapChange={onMapChange}
|
||||
onMapStateChange={onMapStateChange}
|
||||
|
@ -1,61 +0,0 @@
|
||||
import React from "react";
|
||||
import { Flex } from "theme-ui";
|
||||
|
||||
import ColorControl from "./ColorControl";
|
||||
import AlphaBlendToggle from "./AlphaBlendToggle";
|
||||
import RadioIconButton from "./RadioIconButton";
|
||||
|
||||
import BrushStrokeIcon from "../../../icons/BrushStrokeIcon";
|
||||
import BrushFillIcon from "../../../icons/BrushFillIcon";
|
||||
|
||||
import UndoButton from "./UndoButton";
|
||||
import RedoButton from "./RedoButton";
|
||||
|
||||
import Divider from "../../Divider";
|
||||
|
||||
function BrushToolSettings({
|
||||
settings,
|
||||
onSettingChange,
|
||||
onToolAction,
|
||||
disabledActions,
|
||||
}) {
|
||||
return (
|
||||
<Flex sx={{ alignItems: "center" }}>
|
||||
<ColorControl
|
||||
color={settings.color}
|
||||
onColorChange={(color) => onSettingChange({ color })}
|
||||
/>
|
||||
<Divider vertical />
|
||||
<RadioIconButton
|
||||
title="Brush Type Stroke"
|
||||
onClick={() => onSettingChange({ type: "stroke" })}
|
||||
isSelected={settings.type === "stroke"}
|
||||
>
|
||||
<BrushStrokeIcon />
|
||||
</RadioIconButton>
|
||||
<RadioIconButton
|
||||
title="Brush Type Fill"
|
||||
onClick={() => onSettingChange({ type: "fill" })}
|
||||
isSelected={settings.type === "fill"}
|
||||
>
|
||||
<BrushFillIcon />
|
||||
</RadioIconButton>
|
||||
<Divider vertical />
|
||||
<AlphaBlendToggle
|
||||
useBlending={settings.useBlending}
|
||||
onBlendingChange={(useBlending) => onSettingChange({ useBlending })}
|
||||
/>
|
||||
<Divider vertical />
|
||||
<UndoButton
|
||||
onClick={() => onToolAction("mapUndo")}
|
||||
disabled={disabledActions.includes("undo")}
|
||||
/>
|
||||
<RedoButton
|
||||
onClick={() => onToolAction("mapRedo")}
|
||||
disabled={disabledActions.includes("redo")}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
export default BrushToolSettings;
|
171
src/components/map/controls/DrawingToolSettings.js
Normal file
171
src/components/map/controls/DrawingToolSettings.js
Normal file
@ -0,0 +1,171 @@
|
||||
import React, { useEffect, useContext } from "react";
|
||||
import { Flex, IconButton } from "theme-ui";
|
||||
import { useMedia } from "react-media";
|
||||
|
||||
import ColorControl from "./ColorControl";
|
||||
import AlphaBlendToggle from "./AlphaBlendToggle";
|
||||
import RadioIconButton from "./RadioIconButton";
|
||||
import ToolSection from "./ToolSection";
|
||||
|
||||
import BrushIcon from "../../../icons/BrushToolIcon";
|
||||
import BrushPaintIcon from "../../../icons/BrushPaintIcon";
|
||||
import BrushLineIcon from "../../../icons/BrushLineIcon";
|
||||
import BrushRectangleIcon from "../../../icons/BrushRectangleIcon";
|
||||
import BrushCircleIcon from "../../../icons/BrushCircleIcon";
|
||||
import BrushTriangleIcon from "../../../icons/BrushTriangleIcon";
|
||||
import EraseAllIcon from "../../../icons/EraseAllIcon";
|
||||
import EraseIcon from "../../../icons/EraseToolIcon";
|
||||
|
||||
import UndoButton from "./UndoButton";
|
||||
import RedoButton from "./RedoButton";
|
||||
|
||||
import Divider from "../../Divider";
|
||||
|
||||
import MapInteractionContext from "../../../contexts/MapInteractionContext";
|
||||
|
||||
function DrawingToolSettings({
|
||||
settings,
|
||||
onSettingChange,
|
||||
onToolAction,
|
||||
disabledActions,
|
||||
}) {
|
||||
const { interactionEmitter } = useContext(MapInteractionContext);
|
||||
|
||||
// Keyboard shotcuts
|
||||
useEffect(() => {
|
||||
function handleKeyDown({ key, ctrlKey, metaKey, shiftKey }) {
|
||||
if (key === "b") {
|
||||
onSettingChange({ type: "brush" });
|
||||
} else if (key === "p") {
|
||||
onSettingChange({ type: "paint" });
|
||||
} else if (key === "l") {
|
||||
onSettingChange({ type: "line" });
|
||||
} else if (key === "r") {
|
||||
onSettingChange({ type: "rectangle" });
|
||||
} else if (key === "c") {
|
||||
onSettingChange({ type: "circle" });
|
||||
} else if (key === "t") {
|
||||
onSettingChange({ type: "triangle" });
|
||||
} else if (key === "e") {
|
||||
onSettingChange({ type: "erase" });
|
||||
} else if (key === "o") {
|
||||
onSettingChange({ useBlending: !settings.useBlending });
|
||||
} else if (
|
||||
(key === "z" || key === "Z") &&
|
||||
(ctrlKey || metaKey) &&
|
||||
shiftKey &&
|
||||
!disabledActions.includes("redo")
|
||||
) {
|
||||
onToolAction("mapRedo");
|
||||
} else if (
|
||||
key === "z" &&
|
||||
(ctrlKey || metaKey) &&
|
||||
!shiftKey &&
|
||||
!disabledActions.includes("undo")
|
||||
) {
|
||||
onToolAction("mapUndo");
|
||||
}
|
||||
}
|
||||
|
||||
interactionEmitter.on("keyDown", handleKeyDown);
|
||||
return () => {
|
||||
interactionEmitter.off("keyDown", handleKeyDown);
|
||||
};
|
||||
});
|
||||
|
||||
// Change to brush if on erase and it gets disabled
|
||||
useEffect(() => {
|
||||
if (settings.type === "erase" && disabledActions.includes("erase")) {
|
||||
onSettingChange({ type: "brush" });
|
||||
}
|
||||
}, [disabledActions, settings, onSettingChange]);
|
||||
|
||||
const isSmallScreen = useMedia({ query: "(max-width: 799px)" });
|
||||
|
||||
const tools = [
|
||||
{
|
||||
id: "brush",
|
||||
title: "Brush",
|
||||
isSelected: settings.type === "brush",
|
||||
icon: <BrushIcon />,
|
||||
},
|
||||
{
|
||||
id: "paint",
|
||||
title: "Paint",
|
||||
isSelected: settings.type === "paint",
|
||||
icon: <BrushPaintIcon />,
|
||||
},
|
||||
{
|
||||
id: "line",
|
||||
title: "Line",
|
||||
isSelected: settings.type === "line",
|
||||
icon: <BrushLineIcon />,
|
||||
},
|
||||
{
|
||||
id: "rectangle",
|
||||
title: "Rectangle",
|
||||
isSelected: settings.type === "rectangle",
|
||||
icon: <BrushRectangleIcon />,
|
||||
},
|
||||
{
|
||||
id: "circle",
|
||||
title: "Circle",
|
||||
isSelected: settings.type === "circle",
|
||||
icon: <BrushCircleIcon />,
|
||||
},
|
||||
{
|
||||
id: "triangle",
|
||||
title: "Triangle",
|
||||
isSelected: settings.type === "triangle",
|
||||
icon: <BrushTriangleIcon />,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Flex sx={{ alignItems: "center" }}>
|
||||
<ColorControl
|
||||
color={settings.color}
|
||||
onColorChange={(color) => onSettingChange({ color })}
|
||||
/>
|
||||
<Divider vertical />
|
||||
<ToolSection
|
||||
tools={tools}
|
||||
onToolClick={(tool) => onSettingChange({ type: tool.id })}
|
||||
collapse={isSmallScreen}
|
||||
/>
|
||||
<Divider vertical />
|
||||
<RadioIconButton
|
||||
title="Erase"
|
||||
onClick={() => onSettingChange({ type: "erase" })}
|
||||
isSelected={settings.type === "erase"}
|
||||
disabled={disabledActions.includes("erase")}
|
||||
>
|
||||
<EraseIcon />
|
||||
</RadioIconButton>
|
||||
<IconButton
|
||||
aria-label="Erase All"
|
||||
title="Erase All"
|
||||
onClick={() => onToolAction("eraseAll")}
|
||||
disabled={disabledActions.includes("erase")}
|
||||
>
|
||||
<EraseAllIcon />
|
||||
</IconButton>
|
||||
<Divider vertical />
|
||||
<AlphaBlendToggle
|
||||
useBlending={settings.useBlending}
|
||||
onBlendingChange={(useBlending) => onSettingChange({ useBlending })}
|
||||
/>
|
||||
<Divider vertical />
|
||||
<UndoButton
|
||||
onClick={() => onToolAction("mapUndo")}
|
||||
disabled={disabledActions.includes("undo")}
|
||||
/>
|
||||
<RedoButton
|
||||
onClick={() => onToolAction("mapRedo")}
|
||||
disabled={disabledActions.includes("redo")}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
export default DrawingToolSettings;
|
@ -1,34 +0,0 @@
|
||||
import React from "react";
|
||||
import { Flex, IconButton } from "theme-ui";
|
||||
|
||||
import EraseAllIcon from "../../../icons/EraseAllIcon";
|
||||
|
||||
import UndoButton from "./UndoButton";
|
||||
import RedoButton from "./RedoButton";
|
||||
|
||||
import Divider from "../../Divider";
|
||||
|
||||
function EraseToolSettings({ onToolAction, disabledActions }) {
|
||||
return (
|
||||
<Flex sx={{ alignItems: "center" }}>
|
||||
<IconButton
|
||||
aria-label="Erase All"
|
||||
title="Erase All"
|
||||
onClick={() => onToolAction("eraseAll")}
|
||||
>
|
||||
<EraseAllIcon />
|
||||
</IconButton>
|
||||
<Divider vertical />
|
||||
<UndoButton
|
||||
onClick={() => onToolAction("mapUndo")}
|
||||
disabled={disabledActions.includes("undo")}
|
||||
/>
|
||||
<RedoButton
|
||||
onClick={() => onToolAction("mapRedo")}
|
||||
disabled={disabledActions.includes("redo")}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
export default EraseToolSettings;
|
@ -1,33 +1,124 @@
|
||||
import React from "react";
|
||||
import React, { useContext, useEffect } from "react";
|
||||
import { Flex } from "theme-ui";
|
||||
import { useMedia } from "react-media";
|
||||
|
||||
import EdgeSnappingToggle from "./EdgeSnappingToggle";
|
||||
import RadioIconButton from "./RadioIconButton";
|
||||
import GridSnappingToggle from "./GridSnappingToggle";
|
||||
|
||||
import FogAddIcon from "../../../icons/FogAddIcon";
|
||||
import FogBrushIcon from "../../../icons/FogBrushIcon";
|
||||
import FogPolygonIcon from "../../../icons/FogPolygonIcon";
|
||||
import FogRemoveIcon from "../../../icons/FogRemoveIcon";
|
||||
import FogToggleIcon from "../../../icons/FogToggleIcon";
|
||||
import FogAddIcon from "../../../icons/FogAddIcon";
|
||||
import FogSubtractIcon from "../../../icons/FogSubtractIcon";
|
||||
|
||||
import UndoButton from "./UndoButton";
|
||||
import RedoButton from "./RedoButton";
|
||||
|
||||
import Divider from "../../Divider";
|
||||
|
||||
import MapInteractionContext from "../../../contexts/MapInteractionContext";
|
||||
import ToolSection from "./ToolSection";
|
||||
|
||||
function BrushToolSettings({
|
||||
settings,
|
||||
onSettingChange,
|
||||
onToolAction,
|
||||
disabledActions,
|
||||
}) {
|
||||
const { interactionEmitter } = useContext(MapInteractionContext);
|
||||
|
||||
// Keyboard shortcuts
|
||||
useEffect(() => {
|
||||
function handleKeyDown({ key, ctrlKey, metaKey, shiftKey }) {
|
||||
if (key === "Alt") {
|
||||
onSettingChange({ useFogSubtract: !settings.useFogSubtract });
|
||||
} else if (key === "p") {
|
||||
onSettingChange({ type: "polygon" });
|
||||
} else if (key === "b") {
|
||||
onSettingChange({ type: "brush" });
|
||||
} else if (key === "t") {
|
||||
onSettingChange({ type: "toggle" });
|
||||
} else if (key === "r") {
|
||||
onSettingChange({ type: "remove" });
|
||||
} else if (key === "s") {
|
||||
onSettingChange({ useEdgeSnapping: !settings.useEdgeSnapping });
|
||||
} else if (
|
||||
(key === "z" || key === "Z") &&
|
||||
(ctrlKey || metaKey) &&
|
||||
shiftKey &&
|
||||
!disabledActions.includes("redo")
|
||||
) {
|
||||
onToolAction("fogRedo");
|
||||
} else if (
|
||||
key === "z" &&
|
||||
(ctrlKey || metaKey) &&
|
||||
!shiftKey &&
|
||||
!disabledActions.includes("undo")
|
||||
) {
|
||||
onToolAction("fogUndo");
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeyUp({ key }) {
|
||||
if (key === "Alt") {
|
||||
onSettingChange({ useFogSubtract: !settings.useFogSubtract });
|
||||
}
|
||||
}
|
||||
|
||||
interactionEmitter.on("keyDown", handleKeyDown);
|
||||
interactionEmitter.on("keyUp", handleKeyUp);
|
||||
return () => {
|
||||
interactionEmitter.off("keyDown", handleKeyDown);
|
||||
interactionEmitter.off("keyUp", handleKeyUp);
|
||||
};
|
||||
});
|
||||
|
||||
const isSmallScreen = useMedia({ query: "(max-width: 799px)" });
|
||||
const drawTools = [
|
||||
{
|
||||
id: "polygon",
|
||||
title: "Fog Polygon",
|
||||
isSelected: settings.type === "polygon",
|
||||
icon: <FogPolygonIcon />,
|
||||
},
|
||||
{
|
||||
id: "brush",
|
||||
title: "Fog Brush",
|
||||
isSelected: settings.type === "brush",
|
||||
icon: <FogBrushIcon />,
|
||||
},
|
||||
];
|
||||
|
||||
const modeTools = [
|
||||
{
|
||||
id: "add",
|
||||
title: "Add Fog",
|
||||
isSelected: !settings.useFogSubtract,
|
||||
icon: <FogAddIcon />,
|
||||
},
|
||||
{
|
||||
id: "subtract",
|
||||
title: "Subtract Fog",
|
||||
isSelected: settings.useFogSubtract,
|
||||
icon: <FogSubtractIcon />,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Flex sx={{ alignItems: "center" }}>
|
||||
<ToolSection
|
||||
tools={drawTools}
|
||||
onToolClick={(tool) => onSettingChange({ type: tool.id })}
|
||||
collapse={isSmallScreen}
|
||||
/>
|
||||
<Divider vertical />
|
||||
<RadioIconButton
|
||||
title="Add Fog"
|
||||
onClick={() => onSettingChange({ type: "add" })}
|
||||
isSelected={settings.type === "add"}
|
||||
title="Toggle Fog"
|
||||
onClick={() => onSettingChange({ type: "toggle" })}
|
||||
isSelected={settings.type === "toggle"}
|
||||
>
|
||||
<FogAddIcon />
|
||||
<FogToggleIcon />
|
||||
</RadioIconButton>
|
||||
<RadioIconButton
|
||||
title="Remove Fog"
|
||||
@ -36,13 +127,14 @@ function BrushToolSettings({
|
||||
>
|
||||
<FogRemoveIcon />
|
||||
</RadioIconButton>
|
||||
<RadioIconButton
|
||||
title="Toggle Fog"
|
||||
onClick={() => onSettingChange({ type: "toggle" })}
|
||||
isSelected={settings.type === "toggle"}
|
||||
>
|
||||
<FogToggleIcon />
|
||||
</RadioIconButton>
|
||||
<Divider vertical />
|
||||
<ToolSection
|
||||
tools={modeTools}
|
||||
onToolClick={(tool) =>
|
||||
onSettingChange({ useFogSubtract: tool.id === "subtract" })
|
||||
}
|
||||
collapse={isSmallScreen}
|
||||
/>
|
||||
<Divider vertical />
|
||||
<EdgeSnappingToggle
|
||||
useEdgeSnapping={settings.useEdgeSnapping}
|
||||
@ -50,12 +142,6 @@ function BrushToolSettings({
|
||||
onSettingChange({ useEdgeSnapping })
|
||||
}
|
||||
/>
|
||||
<GridSnappingToggle
|
||||
useGridSnapping={settings.useGridSnapping}
|
||||
onGridSnappingChange={(useGridSnapping) =>
|
||||
onSettingChange({ useGridSnapping })
|
||||
}
|
||||
/>
|
||||
<Divider vertical />
|
||||
<UndoButton
|
||||
onClick={() => onToolAction("fogUndo")}
|
||||
|
@ -1,21 +0,0 @@
|
||||
import React from "react";
|
||||
import { IconButton } from "theme-ui";
|
||||
|
||||
import GridOnIcon from "../../../icons/GridOnIcon";
|
||||
import GridOffIcon from "../../../icons/GridOffIcon";
|
||||
|
||||
function GridSnappingToggle({ useGridSnapping, onGridSnappingChange }) {
|
||||
return (
|
||||
<IconButton
|
||||
aria-label={
|
||||
useGridSnapping ? "Disable Grid Snapping" : "Enable Grid Snapping"
|
||||
}
|
||||
title={useGridSnapping ? "Disable Grid Snapping" : "Enable Grid Snapping"}
|
||||
onClick={() => onGridSnappingChange(!useGridSnapping)}
|
||||
>
|
||||
{useGridSnapping ? <GridOnIcon /> : <GridOffIcon />}
|
||||
</IconButton>
|
||||
);
|
||||
}
|
||||
|
||||
export default GridSnappingToggle;
|
65
src/components/map/controls/MeasureToolSettings.js
Normal file
65
src/components/map/controls/MeasureToolSettings.js
Normal file
@ -0,0 +1,65 @@
|
||||
import React, { useEffect, useContext } from "react";
|
||||
import { Flex } from "theme-ui";
|
||||
|
||||
import ToolSection from "./ToolSection";
|
||||
import MeasureChebyshevIcon from "../../../icons/MeasureChebyshevIcon";
|
||||
import MeasureEuclideanIcon from "../../../icons/MeasureEuclideanIcon";
|
||||
import MeasureManhattanIcon from "../../../icons/MeasureManhattanIcon";
|
||||
|
||||
import MapInteractionContext from "../../../contexts/MapInteractionContext";
|
||||
|
||||
function MeasureToolSettings({ settings, onSettingChange }) {
|
||||
const { interactionEmitter } = useContext(MapInteractionContext);
|
||||
|
||||
// Keyboard shortcuts
|
||||
useEffect(() => {
|
||||
function handleKeyDown({ key }) {
|
||||
if (key === "g") {
|
||||
onSettingChange({ type: "chebyshev" });
|
||||
} else if (key === "l") {
|
||||
onSettingChange({ type: "euclidean" });
|
||||
} else if (key === "c") {
|
||||
onSettingChange({ type: "manhattan" });
|
||||
}
|
||||
}
|
||||
interactionEmitter.on("keyDown", handleKeyDown);
|
||||
|
||||
return () => {
|
||||
interactionEmitter.off("keyDown", handleKeyDown);
|
||||
};
|
||||
});
|
||||
|
||||
const tools = [
|
||||
{
|
||||
id: "chebyshev",
|
||||
title: "Grid Distance",
|
||||
isSelected: settings.type === "chebyshev",
|
||||
icon: <MeasureChebyshevIcon />,
|
||||
},
|
||||
{
|
||||
id: "euclidean",
|
||||
title: "Line Distance",
|
||||
isSelected: settings.type === "euclidean",
|
||||
icon: <MeasureEuclideanIcon />,
|
||||
},
|
||||
{
|
||||
id: "manhattan",
|
||||
title: "City Block Distance",
|
||||
isSelected: settings.type === "manhattan",
|
||||
icon: <MeasureManhattanIcon />,
|
||||
},
|
||||
];
|
||||
|
||||
// TODO Add keyboard shortcuts
|
||||
|
||||
return (
|
||||
<Flex sx={{ alignItems: "center" }}>
|
||||
<ToolSection
|
||||
tools={tools}
|
||||
onToolClick={(tool) => onSettingChange({ type: tool.id })}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
export default MeasureToolSettings;
|
@ -1,69 +0,0 @@
|
||||
import React from "react";
|
||||
import { Flex } from "theme-ui";
|
||||
|
||||
import ColorControl from "./ColorControl";
|
||||
import AlphaBlendToggle from "./AlphaBlendToggle";
|
||||
import RadioIconButton from "./RadioIconButton";
|
||||
|
||||
import ShapeRectangleIcon from "../../../icons/ShapeRectangleIcon";
|
||||
import ShapeCircleIcon from "../../../icons/ShapeCircleIcon";
|
||||
import ShapeTriangleIcon from "../../../icons/ShapeTriangleIcon";
|
||||
|
||||
import UndoButton from "./UndoButton";
|
||||
import RedoButton from "./RedoButton";
|
||||
|
||||
import Divider from "../../Divider";
|
||||
|
||||
function ShapeToolSettings({
|
||||
settings,
|
||||
onSettingChange,
|
||||
onToolAction,
|
||||
disabledActions,
|
||||
}) {
|
||||
return (
|
||||
<Flex sx={{ alignItems: "center" }}>
|
||||
<ColorControl
|
||||
color={settings.color}
|
||||
onColorChange={(color) => onSettingChange({ color })}
|
||||
/>
|
||||
<Divider vertical />
|
||||
<RadioIconButton
|
||||
title="Shape Type Rectangle"
|
||||
onClick={() => onSettingChange({ type: "rectangle" })}
|
||||
isSelected={settings.type === "rectangle"}
|
||||
>
|
||||
<ShapeRectangleIcon />
|
||||
</RadioIconButton>
|
||||
<RadioIconButton
|
||||
title="Shape Type Circle"
|
||||
onClick={() => onSettingChange({ type: "circle" })}
|
||||
isSelected={settings.type === "circle"}
|
||||
>
|
||||
<ShapeCircleIcon />
|
||||
</RadioIconButton>
|
||||
<RadioIconButton
|
||||
title="Shape Type Triangle"
|
||||
onClick={() => onSettingChange({ type: "triangle" })}
|
||||
isSelected={settings.type === "triangle"}
|
||||
>
|
||||
<ShapeTriangleIcon />
|
||||
</RadioIconButton>
|
||||
<Divider vertical />
|
||||
<AlphaBlendToggle
|
||||
useBlending={settings.useBlending}
|
||||
onBlendingChange={(useBlending) => onSettingChange({ useBlending })}
|
||||
/>
|
||||
<Divider vertical />
|
||||
<UndoButton
|
||||
onClick={() => onToolAction("mapUndo")}
|
||||
disabled={disabledActions.includes("undo")}
|
||||
/>
|
||||
<RedoButton
|
||||
onClick={() => onToolAction("mapRedo")}
|
||||
disabled={disabledActions.includes("redo")}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
export default ShapeToolSettings;
|
103
src/components/map/controls/ToolSection.js
Normal file
103
src/components/map/controls/ToolSection.js
Normal file
@ -0,0 +1,103 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Box, Flex } from "theme-ui";
|
||||
|
||||
import RadioIconButton from "./RadioIconButton";
|
||||
|
||||
// Section of map tools with the option to collapse into a vertical list
|
||||
function ToolSection({ collapse, tools, onToolClick }) {
|
||||
const [showMore, setShowMore] = useState(false);
|
||||
const [collapsedTool, setCollapsedTool] = useState();
|
||||
|
||||
useEffect(() => {
|
||||
const selectedTool = tools.find((tool) => tool.isSelected);
|
||||
if (selectedTool) {
|
||||
setCollapsedTool(selectedTool);
|
||||
} else {
|
||||
setCollapsedTool(
|
||||
(prevTool) => prevTool && { ...prevTool, isSelected: false }
|
||||
);
|
||||
}
|
||||
}, [tools]);
|
||||
|
||||
function handleToolClick(tool) {
|
||||
if (collapse && tool.isSelected) {
|
||||
setShowMore(!showMore);
|
||||
} else if (collapse && !tool.isSelected) {
|
||||
setShowMore(false);
|
||||
}
|
||||
onToolClick(tool);
|
||||
}
|
||||
|
||||
function renderTool(tool) {
|
||||
return (
|
||||
<RadioIconButton
|
||||
title={tool.title}
|
||||
onClick={() => handleToolClick(tool)}
|
||||
key={tool.id}
|
||||
isSelected={tool.isSelected}
|
||||
>
|
||||
{tool.icon}
|
||||
</RadioIconButton>
|
||||
);
|
||||
}
|
||||
|
||||
if (collapse) {
|
||||
if (!collapsedTool) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Box sx={{ position: "relative" }}>
|
||||
{renderTool(collapsedTool)}
|
||||
{/* Render chevron when more tools is available */}
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
width: 0,
|
||||
height: 0,
|
||||
borderTop: "4px solid",
|
||||
borderTopColor: "text",
|
||||
borderLeft: "4px solid transparent",
|
||||
borderRight: "4px solid transparent",
|
||||
transform: "translate(0, -4px) rotate(-45deg)",
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
/>
|
||||
{showMore && (
|
||||
<Flex
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: "40px",
|
||||
left: "50%",
|
||||
transform: "translateX(-50%)",
|
||||
flexDirection: "column",
|
||||
borderRadius: "4px",
|
||||
}}
|
||||
bg="overlay"
|
||||
p={2}
|
||||
>
|
||||
{tools.filter((tool) => !tool.isSelected).map(renderTool)}
|
||||
</Flex>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
} else {
|
||||
return tools.map((tool) => (
|
||||
<RadioIconButton
|
||||
title={tool.title}
|
||||
onClick={() => handleToolClick(tool)}
|
||||
key={tool.id}
|
||||
isSelected={tool.isSelected}
|
||||
>
|
||||
{tool.icon}
|
||||
</RadioIconButton>
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
ToolSection.defaultProps = {
|
||||
collapse: false,
|
||||
};
|
||||
|
||||
export default ToolSection;
|
@ -2,6 +2,7 @@ import React from "react";
|
||||
import { Flex, Box, Input, IconButton, Label, Checkbox } from "theme-ui";
|
||||
|
||||
import ExpandMoreIcon from "../../icons/ExpandMoreIcon";
|
||||
import { isEmpty } from "../../helpers/shared";
|
||||
|
||||
function TokenSettings({
|
||||
token,
|
||||
@ -9,6 +10,7 @@ function TokenSettings({
|
||||
showMore,
|
||||
onShowMoreChange,
|
||||
}) {
|
||||
const tokenEmpty = !token || isEmpty(token);
|
||||
return (
|
||||
<Flex sx={{ flexDirection: "column" }}>
|
||||
<Flex>
|
||||
@ -21,7 +23,7 @@ function TokenSettings({
|
||||
onChange={(e) =>
|
||||
onSettingsChange("defaultSize", parseInt(e.target.value))
|
||||
}
|
||||
disabled={!token || token.type === "default"}
|
||||
disabled={tokenEmpty || token.type === "default"}
|
||||
min={1}
|
||||
my={1}
|
||||
/>
|
||||
@ -35,7 +37,7 @@ function TokenSettings({
|
||||
name="name"
|
||||
value={(token && token.name) || ""}
|
||||
onChange={(e) => onSettingsChange("name", e.target.value)}
|
||||
disabled={!token || token.type === "default"}
|
||||
disabled={tokenEmpty || token.type === "default"}
|
||||
my={1}
|
||||
/>
|
||||
</Box>
|
||||
@ -44,7 +46,7 @@ function TokenSettings({
|
||||
<Label>
|
||||
<Checkbox
|
||||
checked={token && token.isVehicle}
|
||||
disabled={!token || token.type === "default"}
|
||||
disabled={tokenEmpty || token.type === "default"}
|
||||
onChange={(e) =>
|
||||
onSettingsChange("isVehicle", e.target.checked)
|
||||
}
|
||||
@ -56,7 +58,7 @@ function TokenSettings({
|
||||
<Label>
|
||||
<Checkbox
|
||||
checked={token && token.hideInSidebar}
|
||||
disabled={!token || token.type === "default"}
|
||||
disabled={tokenEmpty || token.type === "default"}
|
||||
onChange={(e) =>
|
||||
onSettingsChange("hideInSidebar", e.target.checked)
|
||||
}
|
||||
@ -79,7 +81,6 @@ function TokenSettings({
|
||||
}}
|
||||
aria-label={showMore ? "Show Less" : "Show More"}
|
||||
title={showMore ? "Show Less" : "Show More"}
|
||||
disabled={!token}
|
||||
>
|
||||
<ExpandMoreIcon />
|
||||
</IconButton>
|
||||
|
@ -4,11 +4,10 @@ const MapInteractionContext = React.createContext({
|
||||
stageScale: 1,
|
||||
stageWidth: 1,
|
||||
stageHeight: 1,
|
||||
stageDragState: "none",
|
||||
setPreventMapInteraction: () => {},
|
||||
mapWidth: 1,
|
||||
mapHeight: 1,
|
||||
mapDragPositionRef: { current: undefined },
|
||||
interactionEmitter: null,
|
||||
});
|
||||
export const MapInteractionProvider = MapInteractionContext.Provider;
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React, { useState } from "react";
|
||||
import React, { useState, useRef } from "react";
|
||||
import { omit, isEmpty } from "../helpers/shared";
|
||||
|
||||
const MapLoadingContext = React.createContext();
|
||||
|
||||
@ -13,12 +14,36 @@ export function MapLoadingProvider({ children }) {
|
||||
setLoadingAssetCount((prevLoadingAssets) => prevLoadingAssets - 1);
|
||||
}
|
||||
|
||||
const assetProgressRef = useRef({});
|
||||
const loadingProgressRef = useRef(null);
|
||||
function assetProgressUpdate({ id, count, total }) {
|
||||
if (count === total) {
|
||||
assetProgressRef.current = omit(assetProgressRef.current, [id]);
|
||||
} else {
|
||||
assetProgressRef.current = {
|
||||
...assetProgressRef.current,
|
||||
[id]: { count, total },
|
||||
};
|
||||
}
|
||||
if (!isEmpty(assetProgressRef.current)) {
|
||||
let total = 0;
|
||||
let count = 0;
|
||||
for (let progress of Object.values(assetProgressRef.current)) {
|
||||
total += progress.total;
|
||||
count += progress.count;
|
||||
}
|
||||
loadingProgressRef.current = count / total;
|
||||
}
|
||||
}
|
||||
|
||||
const isLoading = loadingAssetCount > 0;
|
||||
|
||||
const value = {
|
||||
assetLoadStart,
|
||||
assetLoadFinish,
|
||||
isLoading,
|
||||
assetProgressUpdate,
|
||||
loadingProgressRef,
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -100,6 +100,23 @@ function loadVersions(db) {
|
||||
map.showGrid = false;
|
||||
});
|
||||
});
|
||||
// v1.4.0 - Added fog subtraction
|
||||
db.version(5)
|
||||
.stores({})
|
||||
.upgrade((tx) => {
|
||||
return tx
|
||||
.table("states")
|
||||
.toCollection()
|
||||
.modify((state) => {
|
||||
for (let fogAction of state.fogDrawActions) {
|
||||
if (fogAction.type === "add" || fogAction.type === "edit") {
|
||||
for (let shape of fogAction.shapes) {
|
||||
shape.data.holes = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Get the dexie database used in DatabaseContext
|
||||
|
@ -17,3 +17,7 @@ The good news is that Safari will still work if the two devices are connected to
|
||||
### WebRTC not supported.
|
||||
|
||||
Owlbear Rodeo uses WebRTC to communicate between players. Ensure your browser supports WebRTC. A list of supported browsers can be found [here](https://caniuse.com/#feat=rtcpeerconnection).
|
||||
|
||||
### Unable to connect to party.
|
||||
|
||||
This can happen when your internet connection is stable but a peer to peer connection wasn't able to be established between party members. Refreshing the page can help in fixing this.
|
||||
|
73
src/docs/releaseNotes/v1.4.0.md
Normal file
73
src/docs/releaseNotes/v1.4.0.md
Normal file
@ -0,0 +1,73 @@
|
||||
# v1.4.0
|
||||
|
||||
## Major Changes
|
||||
|
||||
### Reworked Fog Tools
|
||||
|
||||
The fog tool now has more options for supporting different interaction paradigms and allowing for greater accuracy when creating fog shapes.
|
||||
|
||||
- The new fog polygon tool now allows you to click on the map to easily place down the perimeter for a fog shape. Each point will snap to the grid allowing a quick method for creating straight shapes that align with the map. This replaces the old grid snap option.
|
||||
- When drawing fog shapes there is now the option to set the shape to subtract instead of add. This can be used to carve out existing fog shapes to create more complicated geometry or for creating holes in the fog to show players only portions of an area.
|
||||
|
||||
### New Measure Tool
|
||||
|
||||
A new measure tool has been added to allow you to easily find out how far areas of the map are from one another. This tool has three options for calculating distance.
|
||||
|
||||
- Grid Distance (default) or Chebyshev distance, this is the distance on a grid and is the metric use in D&D.
|
||||
- Line Distance or Euclidean distance is the actual distance between the two points of the measure tool.
|
||||
- City Block Distance or Manhattan distance is the distance when only travelling in the horizontal or vertical directions.
|
||||
|
||||
### Keyboard Shortcut Support
|
||||
|
||||
When interacting with the map we now support keyboard shortcuts for quickly switching between tools. A list of supported shortcuts is listed below.
|
||||
|
||||
**Tools**
|
||||
|
||||
- Pan (W) (The space bar can also be held to quickly toggle to the pan tool when in another tool.)
|
||||
- Fog (F)
|
||||
- Draw (D)
|
||||
- Measure (M)
|
||||
|
||||
**Fog**
|
||||
|
||||
- Polygon (P)
|
||||
- Brush (B)
|
||||
- Toggle (T)
|
||||
- Remove (R)
|
||||
- Add / Subtract (The Alt key can be held to toggle to the other setting e.g. while drawing with the add brush tool, holding Alt will change to the subtract option.)
|
||||
- Edge Snapping (S)
|
||||
- Undo (Ctrl / Cmd + Z)
|
||||
- Redo (Ctrl / Cmd + Shift + Z)
|
||||
|
||||
**Drawing**
|
||||
|
||||
- Brush (B)
|
||||
- Paint (P)
|
||||
- Line (L)
|
||||
- Rectangle (R)
|
||||
- Circle (C)
|
||||
- Triangle (T)
|
||||
- Erase (E)
|
||||
- Toggle Blending (B)
|
||||
- Undo (Ctrl / Cmd + Z)
|
||||
- Redo (Ctrl / Cmd + Shift + Z)
|
||||
|
||||
**Measure**
|
||||
|
||||
- Grid (G)
|
||||
- Line (L)
|
||||
- City Block (C)
|
||||
|
||||
## Minor Changes
|
||||
|
||||
- The brush tool, shape tool and erase tool have been combined in to one drawing tool. Having these tools combined should hopefully make the drawing experience a little simpler.
|
||||
- Added a new line tool that will allow you to draw staight lines.
|
||||
- Fixed performance regression for drawing tools that was introduced in v1.3.0.
|
||||
- Fixed performance issues when editing map and token settings.
|
||||
- Added a notification for when a user can connect to the server but not to other party members.
|
||||
- Fixed a bug that lead to a token getting stuck to the cursor when moving.
|
||||
- Added a new loading indicator showing the progress of map and tokens when downloading them from other party members.
|
||||
- Fixed a bug that stopped the undo and redo buttons for fog editing being synced to other party members.
|
||||
|
||||
[Reddit]()
|
||||
[Twitter]()
|
@ -29,6 +29,12 @@ class Peer extends SimplePeer {
|
||||
chunk.count++;
|
||||
this.currentChunks[unpacked.id] = chunk;
|
||||
|
||||
this.emit("dataProgress", {
|
||||
id: unpacked.id,
|
||||
count: chunk.count,
|
||||
total: chunk.total,
|
||||
});
|
||||
|
||||
// All chunks have been loaded
|
||||
if (chunk.count === chunk.total) {
|
||||
// Merge chunks with a blob
|
||||
@ -46,16 +52,19 @@ class Peer extends SimplePeer {
|
||||
}
|
||||
|
||||
send(data) {
|
||||
const packedData = encode(data);
|
||||
|
||||
if (packedData.byteLength > MAX_BUFFER_SIZE) {
|
||||
const chunks = this.chunk(packedData);
|
||||
for (let chunk of chunks) {
|
||||
super.send(encode(chunk));
|
||||
try {
|
||||
const packedData = encode(data);
|
||||
if (packedData.byteLength > MAX_BUFFER_SIZE) {
|
||||
const chunks = this.chunk(packedData);
|
||||
for (let chunk of chunks) {
|
||||
super.send(encode(chunk));
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
super.send(packedData);
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
super.send(packedData);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,8 @@
|
||||
import simplify from "simplify-js";
|
||||
import polygonClipping from "polygon-clipping";
|
||||
|
||||
import * as Vector2 from "./vector2";
|
||||
import { toDegrees } from "./shared";
|
||||
import { toDegrees, omit } from "./shared";
|
||||
|
||||
const snappingThreshold = 1 / 5;
|
||||
export function getBrushPositionForTool(
|
||||
@ -12,27 +13,58 @@ export function getBrushPositionForTool(
|
||||
shapes
|
||||
) {
|
||||
let position = brushPosition;
|
||||
if (tool === "shape") {
|
||||
const snapped = Vector2.roundTo(position, gridSize);
|
||||
const useGridSnappning =
|
||||
(tool === "drawing" &&
|
||||
(toolSettings.type === "line" ||
|
||||
toolSettings.type === "rectangle" ||
|
||||
toolSettings.type === "circle" ||
|
||||
toolSettings.type === "triangle")) ||
|
||||
(tool === "fog" && toolSettings.type === "polygon");
|
||||
|
||||
if (useGridSnappning) {
|
||||
// Snap to corners of grid
|
||||
const gridSnap = Vector2.roundTo(position, gridSize);
|
||||
const gridDistance = Vector2.length(Vector2.subtract(gridSnap, position));
|
||||
|
||||
// Snap to center of grid
|
||||
const centerSnap = Vector2.add(
|
||||
Vector2.roundTo(position, gridSize),
|
||||
Vector2.multiply(gridSize, 0.5)
|
||||
);
|
||||
const centerDistance = Vector2.length(
|
||||
Vector2.subtract(centerSnap, position)
|
||||
);
|
||||
const minGrid = Vector2.min(gridSize);
|
||||
const distance = Vector2.length(Vector2.subtract(snapped, position));
|
||||
if (distance < minGrid * snappingThreshold) {
|
||||
position = snapped;
|
||||
if (gridDistance < minGrid * snappingThreshold) {
|
||||
position = gridSnap;
|
||||
} else if (centerDistance < minGrid * snappingThreshold) {
|
||||
position = centerSnap;
|
||||
}
|
||||
}
|
||||
if (tool === "fog" && toolSettings.type === "add") {
|
||||
if (toolSettings.useGridSnapping) {
|
||||
position = Vector2.roundTo(position, gridSize);
|
||||
}
|
||||
if (toolSettings.useEdgeSnapping) {
|
||||
const minGrid = Vector2.min(gridSize);
|
||||
let closestDistance = Number.MAX_VALUE;
|
||||
let closestPosition = position;
|
||||
// Find the closest point on all fog shapes
|
||||
for (let shape of shapes) {
|
||||
if (shape.type === "fog") {
|
||||
const points = shape.data.points;
|
||||
const isInShape = Vector2.pointInPolygon(position, points);
|
||||
|
||||
const useEdgeSnapping = tool === "fog" && toolSettings.useEdgeSnapping;
|
||||
|
||||
if (useEdgeSnapping) {
|
||||
const minGrid = Vector2.min(gridSize);
|
||||
let closestDistance = Number.MAX_VALUE;
|
||||
let closestPosition = position;
|
||||
// Find the closest point on all fog shapes
|
||||
for (let shape of shapes) {
|
||||
if (shape.type === "fog") {
|
||||
// Include shape points and holes
|
||||
let pointArray = [shape.data.points, ...shape.data.holes];
|
||||
|
||||
// Check whether the position is in the shape but not any holes
|
||||
let isInShape = Vector2.pointInPolygon(position, shape.data.points);
|
||||
if (shape.data.holes.length > 0) {
|
||||
for (let hole of shape.data.holes) {
|
||||
if (Vector2.pointInPolygon(position, hole)) {
|
||||
isInShape = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let points of pointArray) {
|
||||
// Find the closest point to each line of the shape
|
||||
for (let i = 0; i < points.length; i++) {
|
||||
const a = points[i];
|
||||
@ -54,15 +86,22 @@ export function getBrushPositionForTool(
|
||||
}
|
||||
}
|
||||
}
|
||||
position = closestPosition;
|
||||
}
|
||||
position = closestPosition;
|
||||
}
|
||||
|
||||
return position;
|
||||
}
|
||||
|
||||
export function getDefaultShapeData(type, brushPosition) {
|
||||
if (type === "circle") {
|
||||
if (type === "line") {
|
||||
return {
|
||||
points: [
|
||||
{ x: brushPosition.x, y: brushPosition.y },
|
||||
{ x: brushPosition.x, y: brushPosition.y },
|
||||
],
|
||||
};
|
||||
} else if (type === "circle") {
|
||||
return { x: brushPosition.x, y: brushPosition.y, radius: 0 };
|
||||
} else if (type === "rectangle") {
|
||||
return {
|
||||
@ -94,7 +133,11 @@ export function getGridScale(gridSize) {
|
||||
|
||||
export function getUpdatedShapeData(type, data, brushPosition, gridSize) {
|
||||
const gridScale = getGridScale(gridSize);
|
||||
if (type === "circle") {
|
||||
if (type === "line") {
|
||||
return {
|
||||
points: [data.points[0], { x: brushPosition.x, y: brushPosition.y }],
|
||||
};
|
||||
} else if (type === "circle") {
|
||||
const dif = Vector2.subtract(brushPosition, {
|
||||
x: data.x,
|
||||
y: data.y,
|
||||
@ -120,7 +163,7 @@ export function getUpdatedShapeData(type, data, brushPosition, gridSize) {
|
||||
const length = Vector2.length(scaled);
|
||||
const direction = Vector2.normalize(scaled);
|
||||
// Get the angle for a triangle who's width is the same as it's length
|
||||
const angle = Math.atan(length / 2 / length);
|
||||
const angle = Math.atan(length / 2 / (length === 0 ? 1 : length));
|
||||
const sideLength = length / Math.cos(angle);
|
||||
|
||||
const leftDir = Vector2.rotateDirection(direction, toDegrees(angle));
|
||||
@ -155,3 +198,53 @@ export function simplifyPoints(points, gridSize, scale) {
|
||||
(Vector2.min(gridSize) * defaultSimplifySize) / scale
|
||||
);
|
||||
}
|
||||
|
||||
export function drawActionsToShapes(actions, actionIndex) {
|
||||
let shapesById = {};
|
||||
for (let i = 0; i <= actionIndex; i++) {
|
||||
const action = actions[i];
|
||||
if (action.type === "add" || action.type === "edit") {
|
||||
for (let shape of action.shapes) {
|
||||
shapesById[shape.id] = shape;
|
||||
}
|
||||
}
|
||||
if (action.type === "remove") {
|
||||
shapesById = omit(shapesById, action.shapeIds);
|
||||
}
|
||||
if (action.type === "subtract") {
|
||||
const actionGeom = action.shapes.map((actionShape) => [
|
||||
actionShape.data.points.map(({ x, y }) => [x, y]),
|
||||
]);
|
||||
let subtractedShapes = {};
|
||||
for (let shape of Object.values(shapesById)) {
|
||||
const shapePoints = shape.data.points.map(({ x, y }) => [x, y]);
|
||||
const shapeHoles = shape.data.holes.map((hole) =>
|
||||
hole.map(({ x, y }) => [x, y])
|
||||
);
|
||||
let shapeGeom = [[shapePoints, ...shapeHoles]];
|
||||
const difference = polygonClipping.difference(shapeGeom, actionGeom);
|
||||
for (let i = 0; i < difference.length; i++) {
|
||||
let newId = difference.length > 1 ? `${shape.id}-${i}` : shape.id;
|
||||
// Holes detected
|
||||
let holes = [];
|
||||
if (difference[i].length > 1) {
|
||||
for (let j = 1; j < difference[i].length; j++) {
|
||||
holes.push(difference[i][j].map(([x, y]) => ({ x, y })));
|
||||
}
|
||||
}
|
||||
|
||||
subtractedShapes[newId] = {
|
||||
...shape,
|
||||
id: newId,
|
||||
data: {
|
||||
points: difference[i][0].map(([x, y]) => ({ x, y })),
|
||||
holes,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
shapesById = subtractedShapes;
|
||||
}
|
||||
}
|
||||
return Object.values(shapesById);
|
||||
}
|
||||
|
156
src/helpers/konva.js
Normal file
156
src/helpers/konva.js
Normal file
@ -0,0 +1,156 @@
|
||||
import React, { useState } from "react";
|
||||
import { Line, Group, Path, Circle } from "react-konva";
|
||||
|
||||
// Holes should be wound in the opposite direction as the containing points array
|
||||
export function HoleyLine({ holes, ...props }) {
|
||||
// Converted from https://github.com/rfestag/konva/blob/master/src/shapes/Line.ts
|
||||
function drawLine(points, context, shape) {
|
||||
const length = points.length;
|
||||
const tension = shape.tension();
|
||||
const closed = shape.closed();
|
||||
const bezier = shape.bezier();
|
||||
|
||||
if (!length) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.moveTo(points[0], points[1]);
|
||||
|
||||
if (tension !== 0 && length > 4) {
|
||||
const tensionPoints = shape.getTensionPoints();
|
||||
const tensionLength = tensionPoints.length;
|
||||
let n = closed ? 0 : 4;
|
||||
|
||||
if (!closed) {
|
||||
context.quadraticCurveTo(
|
||||
tensionPoints[0],
|
||||
tensionPoints[1],
|
||||
tensionPoints[2],
|
||||
tensionPoints[3]
|
||||
);
|
||||
}
|
||||
|
||||
while (n < tensionLength - 2) {
|
||||
context.bezierCurveTo(
|
||||
tensionPoints[n++],
|
||||
tensionPoints[n++],
|
||||
tensionPoints[n++],
|
||||
tensionPoints[n++],
|
||||
tensionPoints[n++],
|
||||
tensionPoints[n++]
|
||||
);
|
||||
}
|
||||
|
||||
if (!closed) {
|
||||
context.quadraticCurveTo(
|
||||
tensionPoints[tensionLength - 2],
|
||||
tensionPoints[tensionLength - 1],
|
||||
points[length - 2],
|
||||
points[length - 1]
|
||||
);
|
||||
}
|
||||
} else if (bezier) {
|
||||
// no tension but bezier
|
||||
let n = 2;
|
||||
|
||||
while (n < length) {
|
||||
context.bezierCurveTo(
|
||||
points[n++],
|
||||
points[n++],
|
||||
points[n++],
|
||||
points[n++],
|
||||
points[n++],
|
||||
points[n++]
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// no tension
|
||||
for (let n = 2; n < length; n += 2) {
|
||||
context.lineTo(points[n], points[n + 1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw points and holes
|
||||
function sceneFunc(context, shape) {
|
||||
const points = shape.points();
|
||||
const closed = shape.closed();
|
||||
|
||||
if (!points.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.beginPath();
|
||||
drawLine(points, context, shape);
|
||||
|
||||
context.beginPath();
|
||||
drawLine(points, context, shape);
|
||||
|
||||
// closed e.g. polygons and blobs
|
||||
if (closed) {
|
||||
context.closePath();
|
||||
if (holes && holes.length) {
|
||||
for (let hole of holes) {
|
||||
drawLine(hole, context, shape);
|
||||
context.closePath();
|
||||
}
|
||||
}
|
||||
context.fillStrokeShape(shape);
|
||||
} else {
|
||||
// open e.g. lines and splines
|
||||
context.strokeShape(shape);
|
||||
}
|
||||
}
|
||||
|
||||
return <Line sceneFunc={sceneFunc} {...props} />;
|
||||
}
|
||||
|
||||
export function Tick({ x, y, scale, onClick, cross }) {
|
||||
const [fill, setFill] = useState("white");
|
||||
function handleEnter() {
|
||||
setFill("hsl(260, 100%, 80%)");
|
||||
}
|
||||
|
||||
function handleLeave() {
|
||||
setFill("white");
|
||||
}
|
||||
return (
|
||||
<Group
|
||||
x={x}
|
||||
y={y}
|
||||
scaleX={scale}
|
||||
scaleY={scale}
|
||||
onMouseEnter={handleEnter}
|
||||
onMouseLeave={handleLeave}
|
||||
onClick={onClick}
|
||||
onTap={onClick}
|
||||
>
|
||||
<Circle radius={12} fill="hsla(230, 25%, 18%, 0.8)" />
|
||||
<Path
|
||||
offsetX={12}
|
||||
offsetY={12}
|
||||
fill={fill}
|
||||
data={
|
||||
cross
|
||||
? "M18.3 5.71c-.39-.39-1.02-.39-1.41 0L12 10.59 7.11 5.7c-.39-.39-1.02-.39-1.41 0-.39.39-.39 1.02 0 1.41L10.59 12 5.7 16.89c-.39.39-.39 1.02 0 1.41.39.39 1.02.39 1.41 0L12 13.41l4.89 4.89c.39.39 1.02.39 1.41 0 .39-.39.39-1.02 0-1.41L13.41 12l4.89-4.89c.38-.38.38-1.02 0-1.4z"
|
||||
: "M9 16.2l-3.5-3.5c-.39-.39-1.01-.39-1.4 0-.39.39-.39 1.01 0 1.4l4.19 4.19c.39.39 1.02.39 1.41 0L20.3 7.7c.39-.39.39-1.01 0-1.4-.39-.39-1.01-.39-1.4 0L9 16.2z"
|
||||
}
|
||||
/>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
export function getRelativePointerPosition(node) {
|
||||
let transform = node.getAbsoluteTransform().copy();
|
||||
transform.invert();
|
||||
let posision = node.getStage().getPointerPosition();
|
||||
return transform.point(posision);
|
||||
}
|
||||
|
||||
export function getRelativePointerPositionNormalized(node) {
|
||||
const relativePosition = getRelativePointerPosition(node);
|
||||
return {
|
||||
x: relativePosition.x / node.width(),
|
||||
y: relativePosition.y / node.height(),
|
||||
};
|
||||
}
|
@ -50,3 +50,7 @@ export function logImage(url, width, height) {
|
||||
].join(" ");
|
||||
console.log("%c ", style);
|
||||
}
|
||||
|
||||
export function isEmpty(obj) {
|
||||
return Object.keys(obj).length === 0 && obj.constructor === Object;
|
||||
}
|
||||
|
@ -1,71 +0,0 @@
|
||||
import { useContext, useRef, useEffect } from "react";
|
||||
|
||||
import MapInteractionContext from "../contexts/MapInteractionContext";
|
||||
|
||||
import { compare } from "./vector2";
|
||||
|
||||
import usePrevious from "./usePrevious";
|
||||
|
||||
/**
|
||||
* @callback onBrushUpdate
|
||||
* @param {string} drawState "first" | "drawing" | "last"
|
||||
* @param {Object} brushPosition the normalized x and y coordinates of the brush on the map
|
||||
*/
|
||||
|
||||
/**
|
||||
* Helper to get the maps drag position as it changes
|
||||
* @param {boolean} shouldUpdate
|
||||
* @param {onBrushUpdate} onBrushUpdate
|
||||
*/
|
||||
function useMapBrush(shouldUpdate, onBrushUpdate) {
|
||||
const { stageDragState, mapDragPositionRef } = useContext(
|
||||
MapInteractionContext
|
||||
);
|
||||
|
||||
const requestRef = useRef();
|
||||
const previousDragState = usePrevious(stageDragState);
|
||||
const previousBrushPositionRef = useRef(mapDragPositionRef.current);
|
||||
|
||||
useEffect(() => {
|
||||
function updateBrush(forceUpdate) {
|
||||
const drawState =
|
||||
stageDragState === "dragging" ? "drawing" : stageDragState;
|
||||
const brushPosition = mapDragPositionRef.current;
|
||||
const previousBrushPostition = previousBrushPositionRef.current;
|
||||
// Only update brush when it has moved
|
||||
if (
|
||||
!compare(brushPosition, previousBrushPostition, 0.0001) ||
|
||||
forceUpdate
|
||||
) {
|
||||
onBrushUpdate(drawState, brushPosition);
|
||||
previousBrushPositionRef.current = brushPosition;
|
||||
}
|
||||
}
|
||||
|
||||
function animate() {
|
||||
if (!shouldUpdate) {
|
||||
return;
|
||||
}
|
||||
requestRef.current = requestAnimationFrame(animate);
|
||||
updateBrush(false);
|
||||
}
|
||||
|
||||
requestRef.current = requestAnimationFrame(animate);
|
||||
|
||||
if (stageDragState !== previousDragState && shouldUpdate) {
|
||||
updateBrush(true);
|
||||
}
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(requestRef.current);
|
||||
};
|
||||
}, [
|
||||
shouldUpdate,
|
||||
onBrushUpdate,
|
||||
stageDragState,
|
||||
mapDragPositionRef,
|
||||
previousDragState,
|
||||
]);
|
||||
}
|
||||
|
||||
export default useMapBrush;
|
@ -13,6 +13,7 @@ function useSession(
|
||||
onPeerConnected,
|
||||
onPeerDisconnected,
|
||||
onPeerData,
|
||||
onPeerDataProgress,
|
||||
onPeerTrackAdded,
|
||||
onPeerTrackRemoved,
|
||||
onPeerError
|
||||
@ -76,6 +77,10 @@ function useSession(
|
||||
onPeerData && onPeerData({ peer, data });
|
||||
}
|
||||
|
||||
function handleDataProgress({ id, count, total }) {
|
||||
onPeerDataProgress && onPeerDataProgress({ id, count, total });
|
||||
}
|
||||
|
||||
function handleTrack(track, stream) {
|
||||
onPeerTrackAdded && onPeerTrackAdded({ peer, track, stream });
|
||||
track.addEventListener("mute", () => {
|
||||
@ -96,6 +101,7 @@ function useSession(
|
||||
peer.connection.on("signal", handleSignal);
|
||||
peer.connection.on("connect", handleConnect);
|
||||
peer.connection.on("dataComplete", handleDataComplete);
|
||||
peer.connection.on("dataProgress", handleDataProgress);
|
||||
peer.connection.on("track", handleTrack);
|
||||
peer.connection.on("close", handleClose);
|
||||
peer.connection.on("error", handleError);
|
||||
@ -105,6 +111,7 @@ function useSession(
|
||||
handleSignal,
|
||||
handleConnect,
|
||||
handleDataComplete,
|
||||
handleDataProgress,
|
||||
handleTrack,
|
||||
handleClose,
|
||||
handleError,
|
||||
@ -118,6 +125,7 @@ function useSession(
|
||||
handleSignal,
|
||||
handleConnect,
|
||||
handleDataComplete,
|
||||
handleDataProgress,
|
||||
handleTrack,
|
||||
handleClose,
|
||||
handleError,
|
||||
@ -125,6 +133,7 @@ function useSession(
|
||||
peer.connection.off("signal", handleSignal);
|
||||
peer.connection.off("connect", handleConnect);
|
||||
peer.connection.off("dataComplete", handleDataComplete);
|
||||
peer.connection.off("dataProgress", handleDataProgress);
|
||||
peer.connection.off("track", handleTrack);
|
||||
peer.connection.off("close", handleClose);
|
||||
peer.connection.off("error", handleError);
|
||||
@ -135,6 +144,7 @@ function useSession(
|
||||
onPeerConnected,
|
||||
onPeerDisconnected,
|
||||
onPeerData,
|
||||
onPeerDataProgress,
|
||||
onPeerTrackAdded,
|
||||
onPeerTrackRemoved,
|
||||
onPeerError,
|
||||
@ -143,16 +153,19 @@ function useSession(
|
||||
// Setup event listeners for the socket
|
||||
useEffect(() => {
|
||||
function addPeer(id, initiator, sync) {
|
||||
const connection = new Peer({
|
||||
initiator,
|
||||
trickle: true,
|
||||
config: { iceServers },
|
||||
});
|
||||
|
||||
setPeers((prevPeers) => ({
|
||||
...prevPeers,
|
||||
[id]: { id, connection, initiator, sync },
|
||||
}));
|
||||
try {
|
||||
const connection = new Peer({
|
||||
initiator,
|
||||
trickle: true,
|
||||
config: { iceServers },
|
||||
});
|
||||
setPeers((prevPeers) => ({
|
||||
...prevPeers,
|
||||
[id]: { id, connection, initiator, sync },
|
||||
}));
|
||||
} catch (error) {
|
||||
onPeerError && onPeerError({ error });
|
||||
}
|
||||
}
|
||||
|
||||
function handlePartyMemberJoined(id) {
|
||||
@ -214,7 +227,7 @@ function useSession(
|
||||
socket.off("signal", handleSignal);
|
||||
socket.off("auth error", handleAuthError);
|
||||
};
|
||||
}, [peers, setAuthenticationStatus, iceServers, joinParty]);
|
||||
}, [peers, setAuthenticationStatus, iceServers, joinParty, onPeerError]);
|
||||
|
||||
return { peers, socket, connected };
|
||||
}
|
||||
|
@ -10,6 +10,9 @@ export function length(p) {
|
||||
|
||||
export function normalize(p) {
|
||||
const l = length(p);
|
||||
if (l === 0) {
|
||||
return { x: 0, y: 0 };
|
||||
}
|
||||
return divide(p, l);
|
||||
}
|
||||
|
||||
@ -216,3 +219,22 @@ export function pointInPolygon(p, points) {
|
||||
export function compare(a, b, threshold) {
|
||||
return lengthSquared(subtract(a, b)) < threshold * threshold;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the distance between two vectors
|
||||
* @param {Vector2} a
|
||||
* @param {Vector2} b
|
||||
* @param {string} type - "chebyshev" | "euclidean" | "manhattan"
|
||||
*/
|
||||
export function distance(a, b, type) {
|
||||
switch (type) {
|
||||
case "chebyshev":
|
||||
return Math.max(Math.abs(a.x - b.x), Math.abs(a.y - b.y));
|
||||
case "euclidean":
|
||||
return length(subtract(a, b));
|
||||
case "manhattan":
|
||||
return Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
|
||||
default:
|
||||
return length(subtract(a, b));
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
|
||||
function ShapeCircleIcon() {
|
||||
function BrushCircleIcon() {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@ -15,4 +15,4 @@ function ShapeCircleIcon() {
|
||||
);
|
||||
}
|
||||
|
||||
export default ShapeCircleIcon;
|
||||
export default BrushCircleIcon;
|
18
src/icons/BrushLineIcon.js
Normal file
18
src/icons/BrushLineIcon.js
Normal file
@ -0,0 +1,18 @@
|
||||
import React from "react";
|
||||
|
||||
function BrushLineIcon() {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
fill="currentcolor"
|
||||
>
|
||||
<path d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M4.222 16.95L16.95 4.222a2 2 0 112.828 2.828L7.05 19.778a2 2 0 11-2.828-2.828z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default BrushLineIcon;
|
18
src/icons/BrushPaintIcon.js
Normal file
18
src/icons/BrushPaintIcon.js
Normal file
@ -0,0 +1,18 @@
|
||||
import React from "react";
|
||||
|
||||
function BrushPaintIcon() {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
fill="currentcolor"
|
||||
>
|
||||
<path d="M0 0h24v24H0V0z" fill="none" />
|
||||
<path d="M18 4V3c0-.55-.45-1-1-1H5c-.55 0-1 .45-1 1v4c0 .55.45 1 1 1h12c.55 0 1-.45 1-1V6h1v4h-9c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h2c.55 0 1-.45 1-1v-9h7c.55 0 1-.45 1-1V5c0-.55-.45-1-1-1h-2z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default BrushPaintIcon;
|
@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
|
||||
function ShapeRectangleIcon() {
|
||||
function BrushRectangleIcon() {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@ -15,4 +15,4 @@ function ShapeRectangleIcon() {
|
||||
);
|
||||
}
|
||||
|
||||
export default ShapeRectangleIcon;
|
||||
export default BrushRectangleIcon;
|
@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
|
||||
function ShapeTriangleIcon() {
|
||||
function BrushTriangleIcon() {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@ -15,4 +15,4 @@ function ShapeTriangleIcon() {
|
||||
);
|
||||
}
|
||||
|
||||
export default ShapeTriangleIcon;
|
||||
export default BrushTriangleIcon;
|
@ -7,15 +7,10 @@ function EraseToolIcon() {
|
||||
height="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentcolor"
|
||||
>
|
||||
<g fill="none" fillRule="evenodd">
|
||||
<path d="M0 0h24v24H0z" />
|
||||
<path
|
||||
d="M3.212 12.303c-1 1-1.182 2.455-.404 3.233l5.656 5.656c.778.778 2.233.596 3.233-.404l9.091-9.091c1-1 1.182-2.455.404-3.233l-5.656-5.656c-.778-.778-2.233-.596-3.233.404l-9.091 9.091zm6.667-2.424l4.242 4.242c.39.39.485.93.212 1.202l-3.96 3.96c-.272.272-.813.177-1.201-.212l-4.243-4.243c-.389-.388-.484-.93-.212-1.202l3.96-3.96c.272-.272.813-.176 1.202.213z"
|
||||
fill="currentcolor"
|
||||
fillRule="nonzero"
|
||||
/>
|
||||
</g>
|
||||
<path d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M3.212 12.303c-1 1-1.182 2.455-.404 3.233l5.656 5.656c.778.778 2.233.596 3.233-.404l9.091-9.091c1-1 1.182-2.455.404-3.233l-5.656-5.656c-.778-.778-2.233-.596-3.233.404l-9.091 9.091zm6.667-2.424l4.242 4.242c.39.39.485.93.212 1.202l-3.96 3.96c-.272.272-.813.177-1.201-.212l-4.243-4.243c-.389-.388-.484-.93-.212-1.202l3.96-3.96c.272-.272.813-.176 1.202.213z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
|
||||
function BrushFillIcon() {
|
||||
function FogBrushIcon() {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@ -15,4 +15,4 @@ function BrushFillIcon() {
|
||||
);
|
||||
}
|
||||
|
||||
export default BrushFillIcon;
|
||||
export default FogBrushIcon;
|
18
src/icons/FogPolygonIcon.js
Normal file
18
src/icons/FogPolygonIcon.js
Normal file
@ -0,0 +1,18 @@
|
||||
import React from "react";
|
||||
|
||||
function FogPolygonIcon() {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
fill="currentcolor"
|
||||
>
|
||||
<path d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M15.858 20a1 1 0 110 2h-8a1 1 0 010-2h8zm-5-17.998v9.516a2 2 0 102 0V2.12l5.725 10.312a1 1 0 01.049.87l-.058.117-3.093 5.333a1 1 0 01-.865.498H8.941a1 1 0 01-.875-.516L5.125 13.41a1 1 0 01-.002-.965l5.735-10.443z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default FogPolygonIcon;
|
@ -10,7 +10,7 @@ function FogRemoveIcon() {
|
||||
fill="currentcolor"
|
||||
>
|
||||
<path d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M12 4a7.49 7.49 0 017.35 6.04c2.6.18 4.65 2.32 4.65 4.96 0 2.76-2.24 5-5 5H6c-3.31 0-6-2.69-6-6 0-3.09 2.34-5.64 5.35-5.96A7.496 7.496 0 0112 4zm4 8H8c-.55 0-1 .45-1 1s.45 1 1 1h8c.55 0 1-.45 1-1s-.45-1-1-1z" />
|
||||
<path d="M3.212 12.303c-1 1-1.182 2.455-.404 3.233l5.656 5.656c.778.778 2.233.596 3.233-.404l9.091-9.091c1-1 1.182-2.455.404-3.233l-5.656-5.656c-.778-.778-2.233-.596-3.233.404l-9.091 9.091zm6.667-2.424l4.242 4.242c.39.39.485.93.212 1.202l-3.96 3.96c-.272.272-.813.177-1.201-.212l-4.243-4.243c-.389-.388-.484-.93-.212-1.202l3.96-3.96c.272-.272.813-.176 1.202.213z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
18
src/icons/FogSubtractIcon.js
Normal file
18
src/icons/FogSubtractIcon.js
Normal file
@ -0,0 +1,18 @@
|
||||
import React from "react";
|
||||
|
||||
function FogRemoveIcon() {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
fill="currentcolor"
|
||||
>
|
||||
<path d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M12 4a7.49 7.49 0 017.35 6.04c2.6.18 4.65 2.32 4.65 4.96 0 2.76-2.24 5-5 5H6c-3.31 0-6-2.69-6-6 0-3.09 2.34-5.64 5.35-5.96A7.496 7.496 0 0112 4zm4 8H8c-.55 0-1 .45-1 1s.45 1 1 1h8c.55 0 1-.45 1-1s-.45-1-1-1z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default FogRemoveIcon;
|
@ -1,18 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
function GridOffIcon() {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
fill="currentcolor"
|
||||
>
|
||||
<path d="M0 0h24v24H0V0z" fill="none" />
|
||||
<path d="M8 4v.89l2 2V4h4v4h-2.89l2 2H14v.89l2 2V10h4v4h-2.89l2 2H20v.89l2 2V4c0-1.1-.9-2-2-2H5.11l2 2H8zm8 0h3c.55 0 1 .45 1 1v3h-4V4zm6.16 17.88L2.12 1.84c-.39-.39-1.02-.39-1.41 0-.39.39-.39 1.02 0 1.41L2 4.55V20c0 1.1.9 2 2 2h15.45l1.3 1.3c.39.39 1.02.39 1.41 0 .39-.39.39-1.03 0-1.42zM10 12.55L11.45 14H10v-1.45zm-6-6L5.45 8H4V6.55zM8 20H5c-.55 0-1-.45-1-1v-3h4v4zm0-6H4v-4h3.45l.55.55V14zm6 6h-4v-4h3.45l.55.55V20zm2 0v-1.45L17.45 20H16z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default GridOffIcon;
|
@ -1,18 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
function GridOnIcon() {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
fill="currentcolor"
|
||||
>
|
||||
<path d="M0 0h24v24H0V0z" fill="none" />
|
||||
<path d="M20 2H4c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zM8 20H5c-.55 0-1-.45-1-1v-3h4v4zm0-6H4v-4h4v4zm0-6H4V5c0-.55.45-1 1-1h3v4zm6 12h-4v-4h4v4zm0-6h-4v-4h4v4zm0-6h-4V4h4v4zm5 12h-3v-4h4v3c0 .55-.45 1-1 1zm1-6h-4v-4h4v4zm0-6h-4V4h3c.55 0 1 .45 1 1v3z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default GridOnIcon;
|
18
src/icons/MeasureChebyshevIcon.js
Normal file
18
src/icons/MeasureChebyshevIcon.js
Normal file
@ -0,0 +1,18 @@
|
||||
import React from "react";
|
||||
|
||||
function MeasureChebyshevIcon() {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
fill="currentcolor"
|
||||
>
|
||||
<path d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M4 8h4V4H4v4zm6 12h4v-4h-4v4zm-6 0h4v-4H4v4zm0-6h4v-4H4v4zm6 0h4v-4h-4v4zm6-10v4h4V4h-4zm-6 4h4V4h-4v4zm6 6h4v-4h-4v4zm0 6h4v-4h-4v4z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default MeasureChebyshevIcon;
|
20
src/icons/MeasureEuclideanIcon.js
Normal file
20
src/icons/MeasureEuclideanIcon.js
Normal file
@ -0,0 +1,20 @@
|
||||
import React from "react";
|
||||
|
||||
function MeasureEuclideanIcon() {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
fill="currentcolor"
|
||||
>
|
||||
<path d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M6.36,18.78L6.61,21l1.62-1.54l2.77-7.6c-0.68-0.17-1.28-0.51-1.77-0.98L6.36,18.78z" />
|
||||
<path d="M14.77,10.88c-0.49,0.47-1.1,0.81-1.77,0.98l2.77,7.6L17.39,21l0.26-2.22L14.77,10.88z" />
|
||||
<path d="M14.94,8.6c0.3-1.56-0.6-2.94-1.94-3.42V4c0-0.55-0.45-1-1-1h0c-0.55,0-1,0.45-1,1v1.18C9.84,5.6,9,6.7,9,8 c0,1.84,1.66,3.3,3.56,2.95C13.74,10.73,14.71,9.78,14.94,8.6z M12,9c-0.55,0-1-0.45-1-1c0-0.55,0.45-1,1-1s1,0.45,1,1 C13,8.55,12.55,9,12,9z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default MeasureEuclideanIcon;
|
18
src/icons/MeasureManhattanIcon.js
Normal file
18
src/icons/MeasureManhattanIcon.js
Normal file
@ -0,0 +1,18 @@
|
||||
import React from "react";
|
||||
|
||||
function MeasureManhattanIcon() {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
fill="currentcolor"
|
||||
>
|
||||
<path d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M17,11V5c0-1.1-0.9-2-2-2H9C7.9,3,7,3.9,7,5v2H5C3.9,7,3,7.9,3,9v10c0,1.1,0.9,2,2,2h5c0.55,0,1-0.45,1-1v-3h2v3 c0,0.55,0.45,1,1,1h5c1.1,0,2-0.9,2-2v-6c0-1.1-0.9-2-2-2H17z M7,19H5v-2h2V19z M7,15H5v-2h2V15z M7,11H5V9h2V11z M11,15H9v-2h2V15 z M11,11H9V9h2V11z M11,7H9V5h2V7z M15,15h-2v-2h2V15z M15,11h-2V9h2V11z M15,7h-2V5h2V7z M19,19h-2v-2h2V19z M19,15h-2v-2h2V15z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default MeasureManhattanIcon;
|
19
src/icons/MeasureToolIcon.js
Normal file
19
src/icons/MeasureToolIcon.js
Normal file
@ -0,0 +1,19 @@
|
||||
import React from "react";
|
||||
|
||||
function MeasureToolIcon() {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
fill="currentcolor"
|
||||
transform="scale(-1 1)"
|
||||
>
|
||||
<path d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M17.66,17.66l-0.71,0.71c-0.2,0.2-0.51,0.2-0.71,0l0,0c-0.2-0.2-0.2-0.51,0-0.71l0.71-0.71l-1.94-1.94l-0.71,0.71 c-0.2,0.2-0.51,0.2-0.71,0l0,0c-0.2-0.2-0.2-0.51,0-0.71l0.71-0.71l-1.94-1.94l-0.71,0.71c-0.2,0.2-0.51,0.2-0.71,0l0,0 c-0.2-0.2-0.2-0.51,0-0.71l0.71-0.71L9.7,9.7l-0.71,0.71c-0.2,0.2-0.51,0.2-0.71,0l0,0c-0.2-0.2-0.2-0.51,0-0.71l0.71-0.71 L7.05,7.05L6.34,7.76c-0.2,0.2-0.51,0.2-0.71,0l0,0c-0.2-0.2-0.2-0.51,0-0.71l0.71-0.71L4.85,4.85C4.54,4.54,4,4.76,4,5.21V18 c0,1.1,0.9,2,2,2h12.79c0.45,0,0.67-0.54,0.35-0.85L17.66,17.66z M7,16v-4.76L12.76,17H8C7.45,17,7,16.55,7,16z" />{" "}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default MeasureToolIcon;
|
@ -1,20 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
function ShapeToolIcon() {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
fill="currentcolor"
|
||||
>
|
||||
<path d="M0 0h24v24H0V0z" fill="none" />
|
||||
<path d="M11.15 3.4L7.43 9.48c-.41.66.07 1.52.85 1.52h7.43c.78 0 1.26-.86.85-1.52L12.85 3.4c-.39-.64-1.31-.64-1.7 0z" />
|
||||
<circle cx="17.5" cy="17.5" r="4.5" />
|
||||
<path d="M4 21.5h6c.55 0 1-.45 1-1v-6c0-.55-.45-1-1-1H4c-.55 0-1 .45-1 1v6c0 .55.45 1 1 1z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default ShapeToolIcon;
|
@ -12,6 +12,8 @@ import blobToBuffer from "../helpers/blobToBuffer";
|
||||
import MapDataContext from "../contexts/MapDataContext";
|
||||
import AuthContext from "../contexts/AuthContext";
|
||||
|
||||
import { isEmpty } from "../helpers/shared";
|
||||
|
||||
const defaultMapSize = 22;
|
||||
const defaultMapProps = {
|
||||
// Grid type
|
||||
@ -22,7 +24,6 @@ const defaultMapProps = {
|
||||
|
||||
function SelectMapModal({
|
||||
isOpen,
|
||||
onRequestClose,
|
||||
onDone,
|
||||
onMapChange,
|
||||
onMapStateChange,
|
||||
@ -147,7 +148,8 @@ function SelectMapModal({
|
||||
}
|
||||
}
|
||||
|
||||
function handleMapSelect(map) {
|
||||
async function handleMapSelect(map) {
|
||||
await applyMapChanges();
|
||||
setSelectedMapId(map.id);
|
||||
}
|
||||
|
||||
@ -161,8 +163,8 @@ function SelectMapModal({
|
||||
|
||||
async function handleDone() {
|
||||
if (selectedMapId) {
|
||||
onMapChange(selectedMap, selectedMapState);
|
||||
onDone();
|
||||
await applyMapChanges();
|
||||
onMapChange(selectedMapWithChanges, selectedMapStateWithChanges);
|
||||
}
|
||||
onDone();
|
||||
}
|
||||
@ -171,17 +173,43 @@ function SelectMapModal({
|
||||
* Map settings
|
||||
*/
|
||||
const [showMoreSettings, setShowMoreSettings] = useState(false);
|
||||
// Local cache of map setting changes
|
||||
// Applied when done is clicked or map selection is changed
|
||||
const [mapSettingChanges, setMapSettingChanges] = useState({});
|
||||
const [mapStateSettingChanges, setMapStateSettingChanges] = useState({});
|
||||
|
||||
async function handleMapSettingsChange(key, value) {
|
||||
await updateMap(selectedMapId, { [key]: value });
|
||||
function handleMapSettingsChange(key, value) {
|
||||
setMapSettingChanges((prevChanges) => ({ ...prevChanges, [key]: value }));
|
||||
}
|
||||
|
||||
async function handleMapStateSettingsChange(key, value) {
|
||||
await updateMapState(selectedMapId, { [key]: value });
|
||||
function handleMapStateSettingsChange(key, value) {
|
||||
setMapStateSettingChanges((prevChanges) => ({
|
||||
...prevChanges,
|
||||
[key]: value,
|
||||
}));
|
||||
}
|
||||
|
||||
async function applyMapChanges() {
|
||||
if (
|
||||
selectedMapId &&
|
||||
(!isEmpty(mapSettingChanges) || !isEmpty(mapStateSettingChanges))
|
||||
) {
|
||||
await updateMap(selectedMapId, mapSettingChanges);
|
||||
await updateMapState(selectedMapId, mapStateSettingChanges);
|
||||
|
||||
setMapSettingChanges({});
|
||||
setMapStateSettingChanges({});
|
||||
}
|
||||
}
|
||||
|
||||
const selectedMapWithChanges = { ...selectedMap, ...mapSettingChanges };
|
||||
const selectedMapStateWithChanges = {
|
||||
...selectedMapState,
|
||||
...mapStateSettingChanges,
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onRequestClose={onRequestClose}>
|
||||
<Modal isOpen={isOpen} onRequestClose={handleDone}>
|
||||
<ImageDrop onDrop={handleImagesUpload} dropText="Drop map to upload">
|
||||
<input
|
||||
onChange={(event) => handleImagesUpload(event.target.files)}
|
||||
@ -203,15 +231,15 @@ function SelectMapModal({
|
||||
maps={ownedMaps}
|
||||
onMapAdd={openImageDialog}
|
||||
onMapRemove={handleMapRemove}
|
||||
selectedMap={selectedMap}
|
||||
selectedMapState={selectedMapState}
|
||||
selectedMap={selectedMapWithChanges}
|
||||
selectedMapState={selectedMapStateWithChanges}
|
||||
onMapSelect={handleMapSelect}
|
||||
onMapReset={handleMapReset}
|
||||
onDone={handleDone}
|
||||
/>
|
||||
<MapSettings
|
||||
map={selectedMap}
|
||||
mapState={selectedMapState}
|
||||
map={selectedMapWithChanges}
|
||||
mapState={selectedMapStateWithChanges}
|
||||
onSettingsChange={handleMapSettingsChange}
|
||||
onStateSettingsChange={handleMapStateSettingsChange}
|
||||
showMore={showMoreSettings}
|
||||
|
@ -11,6 +11,7 @@ import blobToBuffer from "../helpers/blobToBuffer";
|
||||
|
||||
import TokenDataContext from "../contexts/TokenDataContext";
|
||||
import AuthContext from "../contexts/AuthContext";
|
||||
import { isEmpty } from "../helpers/shared";
|
||||
|
||||
function SelectTokensModal({ isOpen, onRequestClose }) {
|
||||
const { userId } = useContext(AuthContext);
|
||||
@ -34,6 +35,7 @@ function SelectTokensModal({ isOpen, onRequestClose }) {
|
||||
|
||||
function handleTokenAdd(token) {
|
||||
addToken(token);
|
||||
setSelectedTokenId(token.id);
|
||||
}
|
||||
|
||||
async function handleImagesUpload(files) {
|
||||
@ -69,8 +71,8 @@ function SelectTokensModal({ isOpen, onRequestClose }) {
|
||||
handleTokenAdd({
|
||||
file: buffer,
|
||||
name,
|
||||
type: "file",
|
||||
id: shortid.generate(),
|
||||
type: "file",
|
||||
created: Date.now(),
|
||||
lastModified: Date.now(),
|
||||
owner: userId,
|
||||
@ -86,7 +88,8 @@ function SelectTokensModal({ isOpen, onRequestClose }) {
|
||||
});
|
||||
}
|
||||
|
||||
function handleTokenSelect(token) {
|
||||
async function handleTokenSelect(token) {
|
||||
await applyTokenChanges();
|
||||
setSelectedTokenId(token.id);
|
||||
}
|
||||
|
||||
@ -100,12 +103,28 @@ function SelectTokensModal({ isOpen, onRequestClose }) {
|
||||
*/
|
||||
const [showMoreSettings, setShowMoreSettings] = useState(false);
|
||||
|
||||
async function handleTokenSettingsChange(key, value) {
|
||||
await updateToken(selectedTokenId, { [key]: value });
|
||||
const [tokenSettingChanges, setTokenSettingChanges] = useState({});
|
||||
|
||||
function handleTokenSettingsChange(key, value) {
|
||||
setTokenSettingChanges((prevChanges) => ({ ...prevChanges, [key]: value }));
|
||||
}
|
||||
|
||||
async function applyTokenChanges() {
|
||||
if (selectedTokenId && !isEmpty(tokenSettingChanges)) {
|
||||
await updateToken(selectedTokenId, tokenSettingChanges);
|
||||
setTokenSettingChanges({});
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRequestClose() {
|
||||
await applyTokenChanges();
|
||||
onRequestClose();
|
||||
}
|
||||
|
||||
const selectedTokenWithChanges = { ...selectedToken, ...tokenSettingChanges };
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onRequestClose={onRequestClose}>
|
||||
<Modal isOpen={isOpen} onRequestClose={handleRequestClose}>
|
||||
<ImageDrop onDrop={handleImagesUpload} dropText="Drop token to upload">
|
||||
<input
|
||||
onChange={(event) => handleImagesUpload(event.target.files)}
|
||||
@ -126,12 +145,12 @@ function SelectTokensModal({ isOpen, onRequestClose }) {
|
||||
<TokenTiles
|
||||
tokens={ownedTokens}
|
||||
onTokenAdd={openImageDialog}
|
||||
selectedToken={selectedToken}
|
||||
selectedToken={selectedTokenWithChanges}
|
||||
onTokenSelect={handleTokenSelect}
|
||||
onTokenRemove={handleTokenRemove}
|
||||
/>
|
||||
<TokenSettings
|
||||
token={selectedToken}
|
||||
token={selectedTokenWithChanges}
|
||||
showMore={showMoreSettings}
|
||||
onSettingsChange={handleTokenSettingsChange}
|
||||
onShowMoreChange={setShowMoreSettings}
|
||||
@ -139,7 +158,7 @@ function SelectTokensModal({ isOpen, onRequestClose }) {
|
||||
<Button
|
||||
variant="primary"
|
||||
disabled={imageLoading}
|
||||
onClick={onRequestClose}
|
||||
onClick={handleRequestClose}
|
||||
>
|
||||
Done
|
||||
</Button>
|
||||
|
@ -33,13 +33,16 @@ function Game() {
|
||||
const { authenticationStatus, userId, nickname, setNickname } = useContext(
|
||||
AuthContext
|
||||
);
|
||||
const { assetLoadStart, assetLoadFinish } = useContext(MapLoadingContext);
|
||||
const { assetLoadStart, assetLoadFinish, assetProgressUpdate } = useContext(
|
||||
MapLoadingContext
|
||||
);
|
||||
|
||||
const { peers, socket, connected } = useSession(
|
||||
gameId,
|
||||
handlePeerConnected,
|
||||
handlePeerDisconnected,
|
||||
handlePeerData,
|
||||
handlePeerDataProgress,
|
||||
handlePeerTrackAdded,
|
||||
handlePeerTrackRemoved,
|
||||
handlePeerError
|
||||
@ -145,7 +148,7 @@ function Game() {
|
||||
});
|
||||
}
|
||||
|
||||
function updateDrawActionIndex(change, indexKey, actionsKey, peerId) {
|
||||
function updateDrawActionIndex(change, indexKey, actionsKey) {
|
||||
const newIndex = Math.min(
|
||||
Math.max(currentMapState[indexKey] + change, -1),
|
||||
currentMapState[actionsKey].length - 1
|
||||
@ -201,7 +204,7 @@ function Game() {
|
||||
"fogDrawActions"
|
||||
);
|
||||
for (let peer of Object.values(peers)) {
|
||||
peer.connection.send({ id: "fogDrawIndex", data: index });
|
||||
peer.connection.send({ id: "mapFogIndex", data: index });
|
||||
}
|
||||
}
|
||||
|
||||
@ -212,7 +215,7 @@ function Game() {
|
||||
"fogDrawActions"
|
||||
);
|
||||
for (let peer of Object.values(peers)) {
|
||||
peer.connection.send({ id: "fogDrawIndex", data: index });
|
||||
peer.connection.send({ id: "mapFogIndex", data: index });
|
||||
}
|
||||
}
|
||||
|
||||
@ -322,7 +325,6 @@ function Game() {
|
||||
if (cachedMap && cachedMap.lastModified === newMap.lastModified) {
|
||||
setCurrentMap(cachedMap);
|
||||
} else {
|
||||
assetLoadStart();
|
||||
peer.connection.send({ id: "mapRequest", data: newMap.id });
|
||||
}
|
||||
} else {
|
||||
@ -336,7 +338,6 @@ function Game() {
|
||||
}
|
||||
// A new map response with a file attached
|
||||
if (data.id === "mapResponse") {
|
||||
assetLoadFinish();
|
||||
if (data.data && data.data.type === "file") {
|
||||
const newMap = { ...data.data, file: data.data.file };
|
||||
putMap(newMap).then(() => {
|
||||
@ -357,7 +358,6 @@ function Game() {
|
||||
!cachedToken ||
|
||||
cachedToken.lastModified !== newToken.lastModified
|
||||
) {
|
||||
assetLoadStart();
|
||||
peer.connection.send({
|
||||
id: "tokenRequest",
|
||||
data: newToken.id,
|
||||
@ -370,7 +370,6 @@ function Game() {
|
||||
peer.connection.send({ id: "tokenResponse", data: token });
|
||||
}
|
||||
if (data.id === "tokenResponse") {
|
||||
assetLoadFinish();
|
||||
const newToken = data.data;
|
||||
if (newToken && newToken.type === "file") {
|
||||
putToken(newToken);
|
||||
@ -414,6 +413,16 @@ function Game() {
|
||||
}
|
||||
}
|
||||
|
||||
function handlePeerDataProgress({ id, total, count }) {
|
||||
if (count === 1) {
|
||||
assetLoadStart();
|
||||
}
|
||||
if (total === count) {
|
||||
assetLoadFinish();
|
||||
}
|
||||
assetProgressUpdate({ id, total, count });
|
||||
}
|
||||
|
||||
function handlePeerDisconnected(peer) {
|
||||
setPartyNicknames((prevNicknames) => omit(prevNicknames, [peer.id]));
|
||||
}
|
||||
@ -422,7 +431,9 @@ function Game() {
|
||||
function handlePeerError({ error, peer }) {
|
||||
console.error(error.code);
|
||||
if (error.code === "ERR_WEBRTC_SUPPORT") {
|
||||
setPeerError("WebRTC not supported");
|
||||
setPeerError("WebRTC not supported.");
|
||||
} else if (error.code === "ERR_CREATE_OFFER") {
|
||||
setPeerError("Unable to connect to party.");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -51,7 +51,7 @@ function Home() {
|
||||
Join Game
|
||||
</Button>
|
||||
<Text variant="caption" as="p" sx={{ textAlign: "center" }}>
|
||||
Beta v1.3.3
|
||||
Beta v1.4.0
|
||||
</Text>
|
||||
<Button
|
||||
m={2}
|
||||
|
@ -12,6 +12,7 @@ const v130 = raw("../docs/releaseNotes/v1.3.0.md");
|
||||
const v131 = raw("../docs/releaseNotes/v1.3.1.md");
|
||||
const v132 = raw("../docs/releaseNotes/v1.3.2.md");
|
||||
const v133 = raw("../docs/releaseNotes/v1.3.3.md");
|
||||
const v140 = raw("../docs/releaseNotes/v1.4.0.md");
|
||||
|
||||
function ReleaseNotes() {
|
||||
return (
|
||||
@ -34,6 +35,9 @@ function ReleaseNotes() {
|
||||
<Text mb={2} variant="heading" as="h1" sx={{ fontSize: 5 }}>
|
||||
Release Notes
|
||||
</Text>
|
||||
<div id="v140">
|
||||
<Markdown source={v140} />
|
||||
</div>
|
||||
<div id="v133">
|
||||
<Markdown source={v133} />
|
||||
</div>
|
||||
|
@ -163,6 +163,10 @@ export default {
|
||||
img: {
|
||||
maxWidth: "100%",
|
||||
},
|
||||
progress: {
|
||||
color: "text",
|
||||
backgroundColor: "overlay",
|
||||
},
|
||||
},
|
||||
prism: {
|
||||
".comment,.prolog,.doctype,.cdata,.punctuation,.operator,.entity,.url": {
|
||||
|
46
yarn.lock
46
yarn.lock
@ -877,6 +877,13 @@
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.4"
|
||||
|
||||
"@babel/runtime@^7.9.6":
|
||||
version "7.10.3"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.10.3.tgz#670d002655a7c366540c67f6fd3342cd09500364"
|
||||
integrity sha512-RzGO0RLSdokm9Ipe/YD+7ww8X2Ro79qiXZF3HU9ljrM+qnJmH1Vqth+hbiQZy761LnMJTMitHDuKVYTk3k4dLw==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.4"
|
||||
|
||||
"@babel/template@^7.4.0", "@babel/template@^7.8.3":
|
||||
version "7.8.3"
|
||||
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.8.3.tgz#e02ad04fe262a657809327f578056ca15fd4d1b8"
|
||||
@ -6812,6 +6819,13 @@ json-stringify-safe@~5.0.1:
|
||||
resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
|
||||
integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=
|
||||
|
||||
json2mq@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/json2mq/-/json2mq-0.2.0.tgz#b637bd3ba9eabe122c83e9720483aeb10d2c904a"
|
||||
integrity sha1-tje9O6nqvhIsg+lyBIOusQ0skEo=
|
||||
dependencies:
|
||||
string-convert "^0.2.0"
|
||||
|
||||
json3@^3.3.2:
|
||||
version "3.3.3"
|
||||
resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.3.tgz#7fc10e375fc5ae42c4705a5cc0aa6f62be305b81"
|
||||
@ -7627,11 +7641,6 @@ normalize-url@^3.0.0:
|
||||
resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-3.3.0.tgz#b2e1c4dc4f7c6d57743df733a4f5978d18650559"
|
||||
integrity sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg==
|
||||
|
||||
normalize-wheel@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/normalize-wheel/-/normalize-wheel-1.0.1.tgz#aec886affdb045070d856447df62ecf86146ec45"
|
||||
integrity sha1-rsiGr/2wRQcNhWRH32Ls+GFG7EU=
|
||||
|
||||
npm-run-path@^2.0.0:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
|
||||
@ -8251,6 +8260,13 @@ pnp-webpack-plugin@1.6.0:
|
||||
dependencies:
|
||||
ts-pnp "^1.1.2"
|
||||
|
||||
polygon-clipping@^0.14.3:
|
||||
version "0.14.3"
|
||||
resolved "https://registry.yarnpkg.com/polygon-clipping/-/polygon-clipping-0.14.3.tgz#02affe4e2aaee69f686ea9dcd5f9566dd4c941af"
|
||||
integrity sha512-bIaMFYIsHOShMN0JZsvkfk66S7gKMQMlYUpV7LIx+WOBYvJT09eCgoAv2JCDNj6SofA4+o5tlmV76zzUiG4aBQ==
|
||||
dependencies:
|
||||
splaytree "^3.0.1"
|
||||
|
||||
portfinder@^1.0.25:
|
||||
version "1.0.25"
|
||||
resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.25.tgz#254fd337ffba869f4b9d37edc298059cb4d35eca"
|
||||
@ -9285,6 +9301,16 @@ react-markdown@^4.3.1:
|
||||
unist-util-visit "^1.3.0"
|
||||
xtend "^4.0.1"
|
||||
|
||||
react-media@^2.0.0-rc.1:
|
||||
version "2.0.0-rc.1"
|
||||
resolved "https://registry.yarnpkg.com/react-media/-/react-media-2.0.0-rc.1.tgz#1a8ed86ddb7f79fd1c70516e825179eb497adf9c"
|
||||
integrity sha512-g9NN2NkmQBJn6Z6prKm3/SuG90p+rWiTIxHjXGhzhf00uRncVB5QmJTZgf2GVz1Bj93b1XwkdRZkYRAN8uMoBQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.9.6"
|
||||
invariant "^2.2.2"
|
||||
json2mq "^0.2.0"
|
||||
prop-types "^15.5.10"
|
||||
|
||||
react-modal@^3.11.2:
|
||||
version "3.11.2"
|
||||
resolved "https://registry.yarnpkg.com/react-modal/-/react-modal-3.11.2.tgz#bad911976d4add31aa30dba8a41d11e21c4ac8a4"
|
||||
@ -10433,6 +10459,11 @@ spdy@^4.0.1:
|
||||
select-hose "^2.0.0"
|
||||
spdy-transport "^3.0.0"
|
||||
|
||||
splaytree@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/splaytree/-/splaytree-3.0.1.tgz#038d8e6d597a3a1893e8e82693a0f4413b3559f7"
|
||||
integrity sha512-WvQIHRDXLSVn72xjlIG/WGhv/4QO3m+iY2TpVdFRaXd1+/vkNlpkpw1QaNH5taghF9eWXDHWMWnzXyicR8d6ig==
|
||||
|
||||
split-string@^3.0.1, split-string@^3.0.2:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2"
|
||||
@ -10545,6 +10576,11 @@ strict-uri-encode@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713"
|
||||
integrity sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=
|
||||
|
||||
string-convert@^0.2.0:
|
||||
version "0.2.1"
|
||||
resolved "https://registry.yarnpkg.com/string-convert/-/string-convert-0.2.1.tgz#6982cc3049fbb4cd85f8b24568b9d9bf39eeff97"
|
||||
integrity sha1-aYLMMEn7tM2F+LJFaLnZvznu/5c=
|
||||
|
||||
string-length@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/string-length/-/string-length-2.0.0.tgz#d40dbb686a3ace960c1cffca562bf2c45f8363ed"
|
||||
|
Loading…
Reference in New Issue
Block a user