2020-03-20 21:46:52 +11:00
|
|
|
import React, { useRef, useEffect, useState } from "react";
|
2020-03-20 17:56:34 +11:00
|
|
|
import { Box, Image } from "theme-ui";
|
2020-03-19 17:33:57 +11:00
|
|
|
|
2020-04-23 10:09:12 +10:00
|
|
|
import ProxyToken from "../token/ProxyToken";
|
|
|
|
|
import TokenMenu from "../token/TokenMenu";
|
2020-04-18 18:11:21 +10:00
|
|
|
import MapToken from "./MapToken";
|
|
|
|
|
import MapDrawing from "./MapDrawing";
|
2020-04-28 22:05:47 +10:00
|
|
|
import MapFog from "./MapFog";
|
2020-04-18 23:31:40 +10:00
|
|
|
import MapControls from "./MapControls";
|
2020-03-20 13:33:12 +11:00
|
|
|
|
2020-04-23 10:09:12 +10:00
|
|
|
import { omit } from "../../helpers/shared";
|
2020-04-24 15:50:05 +10:00
|
|
|
import useDataSource from "../../helpers/useDataSource";
|
2020-04-27 17:29:46 +10:00
|
|
|
import MapInteraction from "./MapInteraction";
|
|
|
|
|
|
2020-04-24 15:50:05 +10:00
|
|
|
import { mapSources as defaultMapSources } from "../../maps";
|
2020-04-19 13:33:31 +10:00
|
|
|
|
2020-04-20 13:41:11 +10:00
|
|
|
const mapTokenProxyClassName = "map-token__proxy";
|
|
|
|
|
const mapTokenMenuClassName = "map-token__menu";
|
2020-03-20 13:33:12 +11:00
|
|
|
|
2020-03-20 18:06:24 +11:00
|
|
|
function Map({
|
2020-04-23 11:54:29 +10:00
|
|
|
map,
|
2020-04-23 17:23:34 +10:00
|
|
|
mapState,
|
2020-04-24 18:39:11 +10:00
|
|
|
tokens,
|
|
|
|
|
onMapTokenStateChange,
|
|
|
|
|
onMapTokenStateRemove,
|
2020-04-13 00:24:03 +10:00
|
|
|
onMapChange,
|
2020-04-23 21:54:58 +10:00
|
|
|
onMapStateChange,
|
2020-04-19 15:15:48 +10:00
|
|
|
onMapDraw,
|
2020-04-28 22:05:47 +10:00
|
|
|
onFogDraw,
|
|
|
|
|
onMapUndo,
|
|
|
|
|
onMapRedo,
|
2020-04-26 14:34:27 +10:00
|
|
|
allowDrawing,
|
|
|
|
|
allowTokenChange,
|
|
|
|
|
allowMapChange,
|
2020-03-20 18:06:24 +11:00
|
|
|
}) {
|
2020-04-24 15:50:05 +10:00
|
|
|
const mapSource = useDataSource(map, defaultMapSources);
|
|
|
|
|
|
2020-04-24 18:39:11 +10:00
|
|
|
function handleProxyDragEnd(isOnMap, tokenState) {
|
|
|
|
|
if (isOnMap && onMapTokenStateChange) {
|
|
|
|
|
onMapTokenStateChange(tokenState);
|
2020-03-20 13:33:12 +11:00
|
|
|
}
|
|
|
|
|
|
2020-04-24 18:39:11 +10:00
|
|
|
if (!isOnMap && onMapTokenStateRemove) {
|
|
|
|
|
onMapTokenStateRemove(tokenState);
|
2020-03-20 13:33:12 +11:00
|
|
|
}
|
|
|
|
|
}
|
2020-03-20 11:05:40 +11:00
|
|
|
|
2020-04-19 13:33:31 +10:00
|
|
|
/**
|
|
|
|
|
* Map drawing
|
|
|
|
|
*/
|
2020-03-20 21:46:52 +11:00
|
|
|
|
2020-04-27 17:29:46 +10:00
|
|
|
const [selectedToolId, setSelectedToolId] = useState("pan");
|
|
|
|
|
const [toolSettings, setToolSettings] = useState({
|
2020-04-28 17:03:17 +10:00
|
|
|
fog: { type: "add", useEdgeSnapping: true, useGridSnapping: false },
|
2020-04-27 17:29:46 +10:00
|
|
|
brush: {
|
|
|
|
|
color: "darkGray",
|
|
|
|
|
type: "stroke",
|
|
|
|
|
useBlending: false,
|
|
|
|
|
},
|
|
|
|
|
shape: {
|
|
|
|
|
color: "red",
|
|
|
|
|
type: "rectangle",
|
|
|
|
|
useBlending: true,
|
|
|
|
|
},
|
|
|
|
|
});
|
2020-04-29 20:55:52 +10:00
|
|
|
|
2020-04-27 17:29:46 +10:00
|
|
|
function handleToolSettingChange(tool, change) {
|
|
|
|
|
setToolSettings((prevSettings) => ({
|
|
|
|
|
...prevSettings,
|
|
|
|
|
[tool]: {
|
|
|
|
|
...prevSettings[tool],
|
|
|
|
|
...change,
|
|
|
|
|
},
|
|
|
|
|
}));
|
|
|
|
|
}
|
2020-04-19 00:24:06 +10:00
|
|
|
|
2020-04-29 20:55:52 +10:00
|
|
|
function handleToolAction(action) {
|
|
|
|
|
if (action === "eraseAll") {
|
|
|
|
|
onMapDraw({
|
|
|
|
|
type: "remove",
|
|
|
|
|
shapeIds: mapShapes.map((s) => s.id),
|
|
|
|
|
timestamp: Date.now(),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-04-28 22:05:47 +10:00
|
|
|
const [mapShapes, setMapShapes] = useState([]);
|
|
|
|
|
function handleMapShapeAdd(shape) {
|
|
|
|
|
onMapDraw({ type: "add", shapes: [shape], timestamp: Date.now() });
|
2020-04-19 13:33:31 +10:00
|
|
|
}
|
|
|
|
|
|
2020-04-28 22:05:47 +10:00
|
|
|
function handleMapShapeRemove(shapeId) {
|
|
|
|
|
onMapDraw({ type: "remove", shapeIds: [shapeId], timestamp: Date.now() });
|
2020-04-20 11:56:56 +10:00
|
|
|
}
|
|
|
|
|
|
2020-04-28 22:05:47 +10:00
|
|
|
const [fogShapes, setFogShapes] = useState([]);
|
|
|
|
|
function handleFogShapeAdd(shape) {
|
|
|
|
|
onFogDraw({ type: "add", shapes: [shape], timestamp: Date.now() });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleFogShapeRemove(shapeId) {
|
|
|
|
|
onFogDraw({ type: "remove", shapeIds: [shapeId], timestamp: Date.now() });
|
2020-04-19 13:33:31 +10:00
|
|
|
}
|
|
|
|
|
|
2020-04-29 20:40:34 +10:00
|
|
|
function handleFogShapeEdit(shape) {
|
|
|
|
|
onFogDraw({ type: "edit", shapes: [shape], timestamp: Date.now() });
|
|
|
|
|
}
|
|
|
|
|
|
2020-04-19 15:15:48 +10:00
|
|
|
// Replay the draw actions and convert them to shapes for the map drawing
|
2020-04-19 13:33:31 +10:00
|
|
|
useEffect(() => {
|
2020-04-23 17:23:34 +10:00
|
|
|
if (!mapState) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2020-04-28 22:05:47 +10:00
|
|
|
function actionsToShapes(actions, actionIndex) {
|
|
|
|
|
let shapesById = {};
|
|
|
|
|
for (let i = 0; i <= actionIndex; i++) {
|
|
|
|
|
const action = actions[i];
|
2020-04-29 20:40:34 +10:00
|
|
|
if (action.type === "add" || action.type === "edit") {
|
2020-04-28 22:05:47 +10:00
|
|
|
for (let shape of action.shapes) {
|
|
|
|
|
shapesById[shape.id] = shape;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (action.type === "remove") {
|
|
|
|
|
shapesById = omit(shapesById, action.shapeIds);
|
2020-04-20 11:56:56 +10:00
|
|
|
}
|
2020-04-19 13:33:31 +10:00
|
|
|
}
|
2020-04-28 22:05:47 +10:00
|
|
|
return Object.values(shapesById);
|
2020-04-19 13:33:31 +10:00
|
|
|
}
|
2020-04-28 22:05:47 +10:00
|
|
|
|
|
|
|
|
setMapShapes(
|
|
|
|
|
actionsToShapes(mapState.mapDrawActions, mapState.mapDrawActionIndex)
|
|
|
|
|
);
|
|
|
|
|
setFogShapes(
|
|
|
|
|
actionsToShapes(mapState.fogDrawActions, mapState.fogDrawActionIndex)
|
|
|
|
|
);
|
2020-04-23 17:23:34 +10:00
|
|
|
}, [mapState]);
|
2020-04-19 13:33:31 +10:00
|
|
|
|
2020-04-27 17:29:46 +10:00
|
|
|
const disabledControls = [];
|
|
|
|
|
if (!allowMapChange) {
|
|
|
|
|
disabledControls.push("map");
|
|
|
|
|
}
|
|
|
|
|
if (!allowDrawing) {
|
|
|
|
|
disabledControls.push("drawing");
|
|
|
|
|
}
|
2020-04-23 11:54:29 +10:00
|
|
|
if (!map) {
|
2020-04-27 17:29:46 +10:00
|
|
|
disabledControls.push("pan");
|
|
|
|
|
disabledControls.push("brush");
|
2020-04-19 13:33:31 +10:00
|
|
|
}
|
2020-04-28 22:05:47 +10:00
|
|
|
if (mapShapes.length === 0) {
|
2020-04-27 17:29:46 +10:00
|
|
|
disabledControls.push("erase");
|
2020-04-19 13:33:31 +10:00
|
|
|
}
|
2020-04-28 22:05:47 +10:00
|
|
|
if (!mapState || mapState.mapDrawActionIndex < 0) {
|
2020-04-27 17:29:46 +10:00
|
|
|
disabledControls.push("undo");
|
|
|
|
|
}
|
|
|
|
|
if (
|
|
|
|
|
!mapState ||
|
2020-04-28 22:05:47 +10:00
|
|
|
mapState.mapDrawActionIndex === mapState.mapDrawActions.length - 1
|
2020-04-27 17:29:46 +10:00
|
|
|
) {
|
|
|
|
|
disabledControls.push("redo");
|
2020-04-19 13:34:07 +10:00
|
|
|
}
|
2020-04-09 18:20:10 +10:00
|
|
|
|
2020-04-19 13:33:31 +10:00
|
|
|
/**
|
|
|
|
|
* Member setup
|
|
|
|
|
*/
|
|
|
|
|
|
2020-03-20 17:56:34 +11:00
|
|
|
const mapRef = useRef(null);
|
2020-04-27 17:29:46 +10:00
|
|
|
|
2020-04-23 11:54:29 +10:00
|
|
|
const gridX = map && map.gridX;
|
|
|
|
|
const gridY = map && map.gridY;
|
2020-04-20 15:17:56 +10:00
|
|
|
const gridSizeNormalized = { x: 1 / gridX || 0, y: 1 / gridY || 0 };
|
|
|
|
|
const tokenSizePercent = gridSizeNormalized.x * 100;
|
2020-04-23 11:54:29 +10:00
|
|
|
const aspectRatio = (map && map.width / map.height) || 1;
|
2020-03-20 17:56:34 +11:00
|
|
|
|
2020-04-13 00:24:03 +10:00
|
|
|
const mapImage = (
|
|
|
|
|
<Box
|
|
|
|
|
sx={{
|
|
|
|
|
position: "absolute",
|
|
|
|
|
top: 0,
|
|
|
|
|
right: 0,
|
|
|
|
|
bottom: 0,
|
|
|
|
|
left: 0,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Image
|
|
|
|
|
ref={mapRef}
|
2020-04-14 09:41:10 +10:00
|
|
|
className="mapImage"
|
2020-04-13 00:24:03 +10:00
|
|
|
sx={{
|
|
|
|
|
width: "100%",
|
|
|
|
|
userSelect: "none",
|
|
|
|
|
touchAction: "none",
|
|
|
|
|
}}
|
2020-04-24 15:50:05 +10:00
|
|
|
src={mapSource}
|
2020-04-13 00:24:03 +10:00
|
|
|
/>
|
|
|
|
|
</Box>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const mapTokens = (
|
|
|
|
|
<Box
|
|
|
|
|
sx={{
|
|
|
|
|
position: "absolute",
|
|
|
|
|
top: 0,
|
|
|
|
|
left: 0,
|
|
|
|
|
right: 0,
|
|
|
|
|
bottom: 0,
|
2020-04-19 17:57:49 +10:00
|
|
|
pointerEvents: "none",
|
2020-04-13 00:24:03 +10:00
|
|
|
}}
|
|
|
|
|
>
|
2020-04-23 17:23:34 +10:00
|
|
|
{mapState &&
|
2020-04-24 18:39:11 +10:00
|
|
|
Object.values(mapState.tokens).map((tokenState) => (
|
2020-04-23 17:23:34 +10:00
|
|
|
<MapToken
|
2020-04-24 18:39:11 +10:00
|
|
|
key={tokenState.id}
|
|
|
|
|
token={tokens.find((token) => token.id === tokenState.tokenId)}
|
|
|
|
|
tokenState={tokenState}
|
2020-04-23 17:23:34 +10:00
|
|
|
tokenSizePercent={tokenSizePercent}
|
|
|
|
|
className={`${mapTokenProxyClassName} ${mapTokenMenuClassName}`}
|
|
|
|
|
/>
|
|
|
|
|
))}
|
2020-04-13 00:24:03 +10:00
|
|
|
</Box>
|
|
|
|
|
);
|
|
|
|
|
|
2020-04-24 16:18:48 +10:00
|
|
|
const mapDrawing = (
|
|
|
|
|
<MapDrawing
|
|
|
|
|
width={map ? map.width : 0}
|
|
|
|
|
height={map ? map.height : 0}
|
2020-04-28 22:05:47 +10:00
|
|
|
selectedTool={selectedToolId !== "fog" ? selectedToolId : "none"}
|
2020-04-27 17:29:46 +10:00
|
|
|
toolSettings={toolSettings[selectedToolId]}
|
2020-04-28 22:05:47 +10:00
|
|
|
shapes={mapShapes}
|
|
|
|
|
onShapeAdd={handleMapShapeAdd}
|
|
|
|
|
onShapeRemove={handleMapShapeRemove}
|
|
|
|
|
gridSize={gridSizeNormalized}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const mapFog = (
|
|
|
|
|
<MapFog
|
|
|
|
|
width={map ? map.width : 0}
|
|
|
|
|
height={map ? map.height : 0}
|
|
|
|
|
isEditing={selectedToolId === "fog"}
|
|
|
|
|
toolSettings={toolSettings["fog"]}
|
|
|
|
|
shapes={fogShapes}
|
|
|
|
|
onShapeAdd={handleFogShapeAdd}
|
|
|
|
|
onShapeRemove={handleFogShapeRemove}
|
2020-04-29 20:40:34 +10:00
|
|
|
onShapeEdit={handleFogShapeEdit}
|
2020-04-24 16:18:48 +10:00
|
|
|
gridSize={gridSizeNormalized}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
|
2020-04-27 17:29:46 +10:00
|
|
|
const mapControls = (
|
|
|
|
|
<MapControls
|
|
|
|
|
onMapChange={onMapChange}
|
|
|
|
|
onMapStateChange={onMapStateChange}
|
|
|
|
|
currentMap={map}
|
|
|
|
|
onSelectedToolChange={setSelectedToolId}
|
|
|
|
|
selectedToolId={selectedToolId}
|
|
|
|
|
toolSettings={toolSettings}
|
|
|
|
|
onToolSettingChange={handleToolSettingChange}
|
2020-04-29 20:55:52 +10:00
|
|
|
onToolAction={handleToolAction}
|
2020-04-27 17:29:46 +10:00
|
|
|
disabledControls={disabledControls}
|
2020-04-28 22:05:47 +10:00
|
|
|
onUndo={onMapUndo}
|
|
|
|
|
onRedo={onMapRedo}
|
2020-04-27 17:29:46 +10:00
|
|
|
/>
|
|
|
|
|
);
|
2020-03-19 17:33:57 +11:00
|
|
|
return (
|
2020-03-20 13:33:12 +11:00
|
|
|
<>
|
2020-04-27 17:29:46 +10:00
|
|
|
<MapInteraction
|
|
|
|
|
map={map}
|
|
|
|
|
aspectRatio={aspectRatio}
|
2020-04-27 17:40:36 +10:00
|
|
|
isEnabled={selectedToolId === "pan"}
|
2020-04-27 17:29:46 +10:00
|
|
|
controls={(allowMapChange || allowDrawing) && mapControls}
|
2020-03-20 11:05:40 +11:00
|
|
|
>
|
2020-04-27 17:29:46 +10:00
|
|
|
{map && mapImage}
|
|
|
|
|
{map && mapDrawing}
|
2020-04-28 22:05:47 +10:00
|
|
|
{map && mapFog}
|
2020-04-27 17:29:46 +10:00
|
|
|
{map && mapTokens}
|
|
|
|
|
</MapInteraction>
|
2020-04-26 14:34:27 +10:00
|
|
|
{allowTokenChange && (
|
|
|
|
|
<>
|
|
|
|
|
<ProxyToken
|
|
|
|
|
tokenClassName={mapTokenProxyClassName}
|
|
|
|
|
onProxyDragEnd={handleProxyDragEnd}
|
|
|
|
|
tokens={mapState && mapState.tokens}
|
|
|
|
|
/>
|
|
|
|
|
<TokenMenu
|
|
|
|
|
tokenClassName={mapTokenMenuClassName}
|
|
|
|
|
onTokenChange={onMapTokenStateChange}
|
|
|
|
|
tokens={mapState && mapState.tokens}
|
|
|
|
|
/>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
2020-03-20 13:33:12 +11:00
|
|
|
</>
|
2020-03-19 17:33:57 +11:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default Map;
|