From aa4ba33a0baeda72d6a2893e1b6927b62f22dc93 Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Fri, 19 Jun 2020 18:04:58 +1000 Subject: [PATCH] Added fog polygon tool and changed fog interaction method --- src/components/map/Map.js | 2 +- src/components/map/MapDrawing.js | 1 + src/components/map/MapFog.js | 328 +++++++++++------- src/components/map/MapInteraction.js | 16 +- .../map/controls/FogSubtractToggle.js | 19 + .../map/controls/FogToolSettings.js | 64 ++-- src/helpers/konva.js | 15 + src/icons/EraseToolIcon.js | 11 +- src/icons/FogBrushIcon.js | 18 + src/icons/FogPolygonIcon.js | 18 + src/icons/FogRemoveIcon.js | 2 +- 11 files changed, 319 insertions(+), 175 deletions(-) create mode 100644 src/components/map/controls/FogSubtractToggle.js create mode 100644 src/icons/FogBrushIcon.js create mode 100644 src/icons/FogPolygonIcon.js diff --git a/src/components/map/Map.js b/src/components/map/Map.js index 35b11f7..28e07a6 100644 --- a/src/components/map/Map.js +++ b/src/components/map/Map.js @@ -48,7 +48,7 @@ function Map({ const [selectedToolId, setSelectedToolId] = useState("pan"); const [toolSettings, setToolSettings] = useState({ - fog: { type: "add", useEdgeSnapping: true, useGridSnapping: false }, + fog: { type: "polygon", useEdgeSnapping: false, useFogCut: false }, brush: { color: "darkGray", type: "stroke", diff --git a/src/components/map/MapDrawing.js b/src/components/map/MapDrawing.js index 7901f1a..3974981 100644 --- a/src/components/map/MapDrawing.js +++ b/src/components/map/MapDrawing.js @@ -159,6 +159,7 @@ function MapDrawing({ ] ); + // Move away from this as it is too slow to respond useMapBrush(isEditing, handleShapeDraw); function handleShapeOver(shape, isDown) { diff --git a/src/components/map/MapFog.js b/src/components/map/MapFog.js index 30c24a5..cace424 100644 --- a/src/components/map/MapFog.js +++ b/src/components/map/MapFog.js @@ -1,4 +1,4 @@ -import React, { useContext, useState, useCallback } from "react"; +import React, { useContext, useState, useEffect } from "react"; import shortid from "shortid"; import { Group } from "react-konva"; import useImage from "use-image"; @@ -6,6 +6,7 @@ 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 { @@ -14,8 +15,10 @@ import { getStrokeWidth, } from "../../helpers/drawing"; import colors from "../../helpers/colors"; -import useMapBrush from "../../helpers/useMapBrush"; -import { HoleyLine } from "../../helpers/konva"; +import { + HoleyLine, + getRelativePointerPositionNormalized, +} from "../../helpers/konva"; function MapFog({ shapes, @@ -28,6 +31,7 @@ function MapFog({ gridSize, }) { const { stageScale, mapWidth, mapHeight } = useContext(MapInteractionContext); + const mapStageRef = useContext(MapStageContext); const [drawingShape, setDrawingShape] = useState(null); const [isBrushDown, setIsBrushDown] = useState(false); const [editingShapes, setEditingShapes] = useState([]); @@ -40,149 +44,209 @@ 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" || - selectedToolSettings.type === "subtract" - ) { - setDrawingShape({ - type: "fog", - data: { points: [brushPosition], holes: [] }, - strokeWidth: 0.5, - color: selectedToolSettings.type === "add" ? "black" : "red", - 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" || - selectedToolSettings.type === "subtract" - ) { - 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 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], + }, + }; + }); } + if (selectedToolSettings.type === "polygon" && drawingShape) { + const brushPosition = getBrushPosition(); + setDrawingShape((prevShape) => { + return { + ...prevShape, + data: { + ...prevShape.data, + points: [...prevShape.data.points.slice(0, -1), brushPosition], + }, + }; + }); + } + } - function endShape() { - if (selectedToolSettings.type === "add" && drawingShape) { - if (drawingShape.data.points.length > 1) { - const shape = { - ...drawingShape, - data: { - ...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; + } + 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); } } - if (selectedToolSettings.type === "subtract" && drawingShape) { - if (drawingShape.data.points.length > 1) { - const shape = { - data: { - ...drawingShape.data, - points: simplifyPoints( - drawingShape.data.points, - gridSize, - // Downscale fog as smoothing doesn't currently work with edge snapping - stageScale / 2 - ), - }, - id: drawingShape.id, - type: drawingShape.type, - }; - onShapeSubtract(shape); - } - } + setDrawingShape(null); - handleBrushUp(); } - switch (brushState) { - case "first": - startShape(); - return; - case "drawing": - continueShape(); - return; - case "last": - endShape(); - return; - default: - return; + if (selectedToolSettings.type === "polygon") { + const brushPosition = getBrushPosition(); + setDrawingShape({ + type: "fog", + data: { + points: [ + ...(drawingShape ? drawingShape.data.points : [brushPosition]), + brushPosition, + ], + holes: [], + }, + strokeWidth: 0.5, + color: selectedToolSettings.useFogSubtract ? "red" : "black", + blend: false, + id: shortid.generate(), + visible: true, + }); } - }, - [ - selectedToolId, - selectedToolSettings, - gridSize, - stageScale, - onShapeAdd, - onShapeSubtract, - shapes, - drawingShape, - handleBrushUp, - ] - ); - useMapBrush(isEditing, handleShapeDraw); + 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([]); + } + + setIsBrushDown(false); + } + + function handleKeyDown(event) { + if ( + event.key === "Enter" && + selectedToolSettings.type === "polygon" && + drawingShape + ) { + 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 }); + } + + setDrawingShape(null); + } + if (event.key === "Escape" && drawingShape) { + setDrawingShape(null); + } + } + + mapStage.on("mousedown", handleBrushDown); + mapStage.on("mousemove", handleBrushMove); + mapStage.on("mouseup", handleBrushUp); + mapStage.container().addEventListener("keydown", handleKeyDown); + + return () => { + mapStage.off("mousedown", handleBrushDown); + mapStage.off("mousemove", handleBrushMove); + mapStage.off("mouseup", handleBrushUp); + mapStage.container().removeEventListener("keydown", handleKeyDown); + }; + }, [ + mapStageRef, + isEditing, + drawingShape, + editingShapes, + gridSize, + isBrushDown, + onShapeAdd, + onShapeSubtract, + onShapesEdit, + onShapesRemove, + selectedToolId, + selectedToolSettings, + shapes, + stageScale, + ]); function handleShapeOver(shape, isDown) { if (shouldHover && isDown) { diff --git a/src/components/map/MapInteraction.js b/src/components/map/MapInteraction.js index 60e929a..6e20aa5 100644 --- a/src/components/map/MapInteraction.js +++ b/src/components/map/MapInteraction.js @@ -11,7 +11,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; @@ -201,6 +203,14 @@ function MapInteraction({ map, children, controls, selectedToolId }) { mapDragPositionRef, }; + // Enable keyboard interaction for map stage container + useEffect(() => { + const container = mapStageRef.current.container(); + container.tabIndex = 1; + container.style.outline = "none"; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + return ( - {children} + + {children} + diff --git a/src/components/map/controls/FogSubtractToggle.js b/src/components/map/controls/FogSubtractToggle.js new file mode 100644 index 0000000..841ee57 --- /dev/null +++ b/src/components/map/controls/FogSubtractToggle.js @@ -0,0 +1,19 @@ +import React from "react"; +import { IconButton } from "theme-ui"; + +import FogAddIcon from "../../../icons/FogAddIcon"; +import FogSubtractIcon from "../../../icons/FogSubtractIcon"; + +function FogSubtractToggle({ useFogSubtract, onFogSubtractChange }) { + return ( + onFogSubtractChange(!useFogSubtract)} + > + {useFogSubtract ? : } + + ); +} + +export default FogSubtractToggle; diff --git a/src/components/map/controls/FogToolSettings.js b/src/components/map/controls/FogToolSettings.js index bf9539c..616fc5a 100644 --- a/src/components/map/controls/FogToolSettings.js +++ b/src/components/map/controls/FogToolSettings.js @@ -3,10 +3,10 @@ import { Flex } from "theme-ui"; import EdgeSnappingToggle from "./EdgeSnappingToggle"; import RadioIconButton from "./RadioIconButton"; -import GridSnappingToggle from "./GridSnappingToggle"; +import FogSubtractToggle from "./FogSubtractToggle"; -import FogAddIcon from "../../../icons/FogAddIcon"; -import FogSubtractIcon from "../../../icons/FogSubtractIcon"; +import FogBrushIcon from "../../../icons/FogBrushIcon"; +import FogPolygonIcon from "../../../icons/FogPolygonIcon"; import FogRemoveIcon from "../../../icons/FogRemoveIcon"; import FogToggleIcon from "../../../icons/FogToggleIcon"; @@ -24,18 +24,40 @@ function BrushToolSettings({ return ( onSettingChange({ type: "add" })} - isSelected={settings.type === "add"} + title="Fog Polygon" + onClick={() => onSettingChange({ type: "polygon" })} + isSelected={settings.type === "polygon"} > - + onSettingChange({ type: "subtract" })} - isSelected={settings.type === "subtract"} + title="Fog Brush" + onClick={() => onSettingChange({ type: "brush" })} + isSelected={settings.type === "brush"} > - + + + + + onSettingChange({ useFogSubtract }) + } + /> + {/* TODO: Re-enable edge snapping when holes are fixed */} + {/* + onSettingChange({ useEdgeSnapping }) + } + /> */} + + onSettingChange({ type: "toggle" })} + isSelected={settings.type === "toggle"} + > + - onSettingChange({ type: "toggle" })} - isSelected={settings.type === "toggle"} - > - - - - - onSettingChange({ useEdgeSnapping }) - } - /> - - onSettingChange({ useGridSnapping }) - } - /> onToolAction("fogUndo")} diff --git a/src/helpers/konva.js b/src/helpers/konva.js index d424eb1..dd799be 100644 --- a/src/helpers/konva.js +++ b/src/helpers/konva.js @@ -105,3 +105,18 @@ export function HoleyLine({ holes, ...props }) { return ; } + +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(), + }; +} diff --git a/src/icons/EraseToolIcon.js b/src/icons/EraseToolIcon.js index 710ccbf..e15389e 100644 --- a/src/icons/EraseToolIcon.js +++ b/src/icons/EraseToolIcon.js @@ -7,15 +7,10 @@ function EraseToolIcon() { height="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" + fill="currentcolor" > - - - - + + ); } diff --git a/src/icons/FogBrushIcon.js b/src/icons/FogBrushIcon.js new file mode 100644 index 0000000..827c10d --- /dev/null +++ b/src/icons/FogBrushIcon.js @@ -0,0 +1,18 @@ +import React from "react"; + +function FogBrushIcon() { + return ( + + + + + ); +} + +export default FogBrushIcon; diff --git a/src/icons/FogPolygonIcon.js b/src/icons/FogPolygonIcon.js new file mode 100644 index 0000000..cfd2bd1 --- /dev/null +++ b/src/icons/FogPolygonIcon.js @@ -0,0 +1,18 @@ +import React from "react"; + +function FogPolygonIcon() { + return ( + + + + + ); +} + +export default FogPolygonIcon; diff --git a/src/icons/FogRemoveIcon.js b/src/icons/FogRemoveIcon.js index ec1183d..9ad8330 100644 --- a/src/icons/FogRemoveIcon.js +++ b/src/icons/FogRemoveIcon.js @@ -10,7 +10,7 @@ function FogRemoveIcon() { fill="currentcolor" > - + ); }