From 24de41fee776e1e5631840e622aac3f210f81086 Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Sun, 19 Apr 2020 13:33:31 +1000 Subject: [PATCH] Added undo and redo and map control validation --- src/components/Map.js | 84 +++++++++++++++++++++++++++- src/components/MapControls.js | 102 ++++++++++++++++++++++------------ src/components/MapDrawing.js | 98 +++++++++++++++++++------------- src/theme.js | 4 ++ 4 files changed, 213 insertions(+), 75 deletions(-) diff --git a/src/components/Map.js b/src/components/Map.js index 2434476..f947a60 100644 --- a/src/components/Map.js +++ b/src/components/Map.js @@ -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} /> @@ -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} /> - + setIsExpanded(!isExpanded)} + sx={{ + transform: `rotate(${isExpanded ? "0" : "180deg"})`, + display: "block", + }} + > - - {divider} - onToolChange("pan")} - sx={{ color: selectedTool === "pan" ? "primary" : "text" }} + - - - onToolChange("brush")} - sx={{ color: selectedTool === "brush" ? "primary" : "text" }} - > - - - onToolChange("erase")} - sx={{ color: selectedTool === "erase" ? "primary" : "text" }} - > - - - {divider} - - - - - - + + {divider} + onToolChange("pan")} + sx={{ color: selectedTool === "pan" ? "primary" : "text" }} + disabled={disabledTools.includes("pan")} + > + + + onToolChange("brush")} + sx={{ color: selectedTool === "brush" ? "primary" : "text" }} + disabled={disabledTools.includes("brush")} + > + + + onToolChange("erase")} + sx={{ color: selectedTool === "erase" ? "primary" : "text" }} + disabled={disabledTools.includes("erase")} + > + + + {divider} + onUndo()} + disabled={undoDisabled} + > + + + onRedo()} + disabled={redoDisabled} + > + + + ); } diff --git a/src/components/MapDrawing.js b/src/components/MapDrawing.js index 451d9d6..a7cb96c 100644 --- a/src/components/MapDrawing.js +++ b/src/components/MapDrawing.js @@ -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 (