Added undo and redo and map control validation
This commit is contained in:
parent
1f20959940
commit
24de41fee7
@ -8,6 +8,8 @@ import MapToken from "./MapToken";
|
||||
import MapDrawing from "./MapDrawing";
|
||||
import MapControls from "./MapControls";
|
||||
|
||||
import { omit } from "../helpers/shared";
|
||||
|
||||
const mapTokenClassName = "map-token";
|
||||
const zoomSpeed = -0.005;
|
||||
const minZoom = 0.1;
|
||||
@ -31,11 +33,77 @@ function Map({
|
||||
}
|
||||
}
|
||||
|
||||
const [mapTranslate, setMapTranslate] = useState({ x: 0, y: 0 });
|
||||
const [mapScale, setMapScale] = useState(1);
|
||||
/**
|
||||
* Map drawing
|
||||
*/
|
||||
|
||||
const [selectedTool, setSelectedTool] = useState("pan");
|
||||
|
||||
const [drawnShapes, setDrawnShapes] = useState([]);
|
||||
const [drawActions, setDrawActions] = useState([]);
|
||||
const [drawActionIndex, setDrawActionIndex] = useState(-1);
|
||||
function handleShapeAdd(shape) {
|
||||
setDrawActions((prevActions) => {
|
||||
const newActions = [
|
||||
...prevActions.slice(0, drawActionIndex + 1),
|
||||
{ type: "add", shape },
|
||||
];
|
||||
setDrawActionIndex(newActions.length - 1);
|
||||
return newActions;
|
||||
});
|
||||
}
|
||||
|
||||
function handleShapeRemove(shapeId) {
|
||||
setDrawActions((prevActions) => {
|
||||
const newActions = [
|
||||
...prevActions.slice(0, drawActionIndex + 1),
|
||||
{ type: "remove", shapeId },
|
||||
];
|
||||
setDrawActionIndex(newActions.length - 1);
|
||||
return newActions;
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
let shapesById = {};
|
||||
for (let i = 0; i <= drawActionIndex; i++) {
|
||||
const action = drawActions[i];
|
||||
if (action.type === "add") {
|
||||
shapesById[action.shape.id] = action.shape;
|
||||
}
|
||||
if (action.type === "remove") {
|
||||
shapesById = omit(shapesById, [action.shapeId]);
|
||||
}
|
||||
}
|
||||
setDrawnShapes(Object.values(shapesById));
|
||||
}, [drawActions, drawActionIndex]);
|
||||
|
||||
function handleDrawActionUndo() {
|
||||
setDrawActionIndex((prevIndex) => Math.max(prevIndex - 1, -1));
|
||||
}
|
||||
|
||||
function handleDrawActionRedo() {
|
||||
setDrawActionIndex((prevIndex) =>
|
||||
Math.min(prevIndex + 1, drawActions.length - 1)
|
||||
);
|
||||
}
|
||||
|
||||
const disabledTools = [];
|
||||
if (!mapData) {
|
||||
disabledTools.push("pan");
|
||||
disabledTools.push("brush");
|
||||
}
|
||||
if (drawnShapes.length === 0) {
|
||||
disabledTools.push("erase");
|
||||
}
|
||||
|
||||
/**
|
||||
* Map movement
|
||||
*/
|
||||
|
||||
const [mapTranslate, setMapTranslate] = useState({ x: 0, y: 0 });
|
||||
const [mapScale, setMapScale] = useState(1);
|
||||
|
||||
useEffect(() => {
|
||||
interact(".map")
|
||||
.gesturable({
|
||||
@ -110,6 +178,10 @@ function Map({
|
||||
};
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Member setup
|
||||
*/
|
||||
|
||||
const mapRef = useRef(null);
|
||||
const mapContainerRef = useRef();
|
||||
const rows = mapData && mapData.rows;
|
||||
@ -201,6 +273,9 @@ function Map({
|
||||
width={mapData ? mapData.width : 0}
|
||||
height={mapData ? mapData.height : 0}
|
||||
selectedTool={selectedTool}
|
||||
shapes={drawnShapes}
|
||||
onShapeAdd={handleShapeAdd}
|
||||
onShapeRemove={handleShapeRemove}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
@ -208,6 +283,11 @@ function Map({
|
||||
onMapChange={onMapChange}
|
||||
onToolChange={setSelectedTool}
|
||||
selectedTool={selectedTool}
|
||||
disabledTools={disabledTools}
|
||||
onUndo={handleDrawActionUndo}
|
||||
onRedo={handleDrawActionRedo}
|
||||
undoDisabled={drawActionIndex < 0}
|
||||
redoDisabled={drawActionIndex === drawActions.length - 1}
|
||||
/>
|
||||
</Box>
|
||||
<ProxyToken
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
import { Flex, Box, IconButton } from "theme-ui";
|
||||
|
||||
import AddMapButton from "./AddMapButton";
|
||||
@ -13,9 +13,14 @@ function MapControls({
|
||||
onMapChange,
|
||||
onToolChange,
|
||||
selectedTool,
|
||||
disabledTools,
|
||||
onUndo,
|
||||
onRedo,
|
||||
undoDisabled,
|
||||
redoDisabled,
|
||||
}) {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
const divider = (
|
||||
<Box
|
||||
my={2}
|
||||
@ -34,42 +39,71 @@ function MapControls({
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<IconButton aria-label="Expand More" title="Expand More">
|
||||
<IconButton
|
||||
aria-label={isExpanded ? "Hide Map Controls" : "Show Map Controls"}
|
||||
title={isExpanded ? "Hide Map Controls" : "Show Map Controls"}
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
sx={{
|
||||
transform: `rotate(${isExpanded ? "0" : "180deg"})`,
|
||||
display: "block",
|
||||
}}
|
||||
>
|
||||
<ExpandMoreIcon />
|
||||
</IconButton>
|
||||
<AddMapButton onMapChange={onMapChange} />
|
||||
{divider}
|
||||
<IconButton
|
||||
aria-label="Pan Tool"
|
||||
title="Pan Tool"
|
||||
onClick={() => onToolChange("pan")}
|
||||
sx={{ color: selectedTool === "pan" ? "primary" : "text" }}
|
||||
<Box
|
||||
sx={{
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
display: isExpanded ? "flex" : "none",
|
||||
}}
|
||||
>
|
||||
<PanToolIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
aria-label="Brush Tool"
|
||||
title="Brush Tool"
|
||||
onClick={() => onToolChange("brush")}
|
||||
sx={{ color: selectedTool === "brush" ? "primary" : "text" }}
|
||||
>
|
||||
<BrushToolIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
aria-label="Erase Tool"
|
||||
title="Erase Tool"
|
||||
onClick={() => onToolChange("erase")}
|
||||
sx={{ color: selectedTool === "erase" ? "primary" : "text" }}
|
||||
>
|
||||
<EraseToolIcon />
|
||||
</IconButton>
|
||||
{divider}
|
||||
<IconButton aria-label="Undo" title="Undo">
|
||||
<UndoIcon />
|
||||
</IconButton>
|
||||
<IconButton aria-label="Redo" title="Redo">
|
||||
<RedoIcon />
|
||||
</IconButton>
|
||||
<AddMapButton onMapChange={onMapChange} />
|
||||
{divider}
|
||||
<IconButton
|
||||
aria-label="Pan Tool"
|
||||
title="Pan Tool"
|
||||
onClick={() => onToolChange("pan")}
|
||||
sx={{ color: selectedTool === "pan" ? "primary" : "text" }}
|
||||
disabled={disabledTools.includes("pan")}
|
||||
>
|
||||
<PanToolIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
aria-label="Brush Tool"
|
||||
title="Brush Tool"
|
||||
onClick={() => onToolChange("brush")}
|
||||
sx={{ color: selectedTool === "brush" ? "primary" : "text" }}
|
||||
disabled={disabledTools.includes("brush")}
|
||||
>
|
||||
<BrushToolIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
aria-label="Erase Tool"
|
||||
title="Erase Tool"
|
||||
onClick={() => onToolChange("erase")}
|
||||
sx={{ color: selectedTool === "erase" ? "primary" : "text" }}
|
||||
disabled={disabledTools.includes("erase")}
|
||||
>
|
||||
<EraseToolIcon />
|
||||
</IconButton>
|
||||
{divider}
|
||||
<IconButton
|
||||
aria-label="Undo"
|
||||
title="Undo"
|
||||
onClick={() => onUndo()}
|
||||
disabled={undoDisabled}
|
||||
>
|
||||
<UndoIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
aria-label="Redo"
|
||||
title="Redo"
|
||||
onClick={() => onRedo()}
|
||||
disabled={redoDisabled}
|
||||
>
|
||||
<RedoIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
@ -1,12 +1,18 @@
|
||||
import React, { useRef, useEffect, useState } from "react";
|
||||
import simplify from "simplify-js";
|
||||
import shortid from "shortid";
|
||||
|
||||
function MapDrawing({ width, height, selectedTool }) {
|
||||
function MapDrawing({
|
||||
width,
|
||||
height,
|
||||
selectedTool,
|
||||
shapes,
|
||||
onShapeAdd,
|
||||
onShapeRemove,
|
||||
}) {
|
||||
const canvasRef = useRef();
|
||||
const containerRef = useRef();
|
||||
|
||||
const [shapes, setShapes] = useState([]);
|
||||
|
||||
function getMousePosition(event) {
|
||||
const container = containerRef.current;
|
||||
if (container) {
|
||||
@ -17,12 +23,13 @@ function MapDrawing({ width, height, selectedTool }) {
|
||||
}
|
||||
}
|
||||
|
||||
const [brushPoints, setBrushPoints] = useState([]);
|
||||
const [isMouseDown, setIsMouseDown] = useState(false);
|
||||
function handleMouseDown(event) {
|
||||
setIsMouseDown(true);
|
||||
if (selectedTool === "brush") {
|
||||
const position = getMousePosition(event);
|
||||
setShapes((prevShapes) => [...prevShapes, { points: [position] }]);
|
||||
setBrushPoints([position]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -34,41 +41,50 @@ function MapDrawing({ width, height, selectedTool }) {
|
||||
}
|
||||
if (isMouseDown && selectedTool === "brush") {
|
||||
setMousePosition(position);
|
||||
setShapes((prevShapes) => {
|
||||
const currentShape = prevShapes.slice(-1)[0];
|
||||
const otherShapes = prevShapes.slice(0, -1);
|
||||
return [...otherShapes, { points: [...currentShape.points, position] }];
|
||||
});
|
||||
setBrushPoints((prevPoints) => [...prevPoints, position]);
|
||||
}
|
||||
}
|
||||
|
||||
function handleMouseUp(event) {
|
||||
setIsMouseDown(false);
|
||||
if (selectedTool === "brush") {
|
||||
setShapes((prevShapes) => {
|
||||
const currentShape = prevShapes.slice(-1)[0];
|
||||
const otherShapes = prevShapes.slice(0, -1);
|
||||
const simplified = simplify(currentShape.points, 0.001);
|
||||
return [...otherShapes, { points: simplified }];
|
||||
});
|
||||
const simplifiedPoints = simplify(brushPoints, 0.001);
|
||||
onShapeAdd({ id: shortid.generate(), points: simplifiedPoints });
|
||||
setBrushPoints([]);
|
||||
}
|
||||
if (selectedTool === "erase" && hoveredShapeRef.current) {
|
||||
onShapeRemove(hoveredShapeRef.current.id);
|
||||
}
|
||||
}
|
||||
|
||||
const hoveredShapeRef = useRef(null);
|
||||
useEffect(() => {
|
||||
function pointsToPath(points) {
|
||||
const path = new Path2D();
|
||||
path.moveTo(points[0].x * width, points[0].y * height);
|
||||
for (let point of points.slice(1)) {
|
||||
path.lineTo(point.x * width, point.y * height);
|
||||
}
|
||||
path.closePath();
|
||||
return path;
|
||||
}
|
||||
|
||||
function drawPath(path, color, context) {
|
||||
context.fillStyle = color;
|
||||
context.strokeStyle = color;
|
||||
context.stroke(path);
|
||||
context.fill(path);
|
||||
}
|
||||
|
||||
const canvas = canvasRef.current;
|
||||
if (canvas) {
|
||||
const context = canvas.getContext("2d");
|
||||
|
||||
context.clearRect(0, 0, width, height);
|
||||
let erasedShapes = [];
|
||||
for (let [index, shape] of shapes.entries()) {
|
||||
const path = new Path2D();
|
||||
path.moveTo(shape.points[0].x * width, shape.points[0].y * height);
|
||||
for (let point of shape.points.slice(1)) {
|
||||
path.lineTo(point.x * width, point.y * height);
|
||||
}
|
||||
path.closePath();
|
||||
let color = "#000000";
|
||||
let hoveredShape = null;
|
||||
for (let shape of shapes) {
|
||||
const path = pointsToPath(shape.points);
|
||||
// Detect hover
|
||||
if (selectedTool === "erase") {
|
||||
if (
|
||||
context.isPointInPath(
|
||||
@ -77,26 +93,30 @@ function MapDrawing({ width, height, selectedTool }) {
|
||||
mousePosition.y * height
|
||||
)
|
||||
) {
|
||||
color = "#BB99FF";
|
||||
if (isMouseDown) {
|
||||
erasedShapes.push(index);
|
||||
continue;
|
||||
}
|
||||
hoveredShape = shape;
|
||||
}
|
||||
}
|
||||
context.fillStyle = color;
|
||||
context.strokeStyle = color;
|
||||
context.stroke(path);
|
||||
context.fill(path);
|
||||
drawPath(path, "#000000", context);
|
||||
}
|
||||
|
||||
if (erasedShapes.length > 0) {
|
||||
setShapes((prevShapes) =>
|
||||
prevShapes.filter((_, i) => !erasedShapes.includes(i))
|
||||
);
|
||||
if (selectedTool === "brush" && brushPoints.length > 0) {
|
||||
const path = pointsToPath(brushPoints);
|
||||
drawPath(path, "#000000", context);
|
||||
}
|
||||
if (hoveredShape) {
|
||||
const path = pointsToPath(hoveredShape.points);
|
||||
drawPath(path, "#BB99FF", context);
|
||||
}
|
||||
hoveredShapeRef.current = hoveredShape;
|
||||
}
|
||||
}, [shapes, width, height, mousePosition, isMouseDown, selectedTool]);
|
||||
}, [
|
||||
shapes,
|
||||
width,
|
||||
height,
|
||||
mousePosition,
|
||||
isMouseDown,
|
||||
selectedTool,
|
||||
brushPoints,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
@ -235,6 +235,10 @@ export default {
|
||||
"&:active": {
|
||||
color: "secondary",
|
||||
},
|
||||
"&:disabled": {
|
||||
opacity: 0.5,
|
||||
color: "text",
|
||||
},
|
||||
},
|
||||
close: {
|
||||
"&:hover": {
|
||||
|
Loading…
Reference in New Issue
Block a user