diff --git a/src/components/map/Map.js b/src/components/map/Map.js index c3a69b7..1429bc7 100644 --- a/src/components/map/Map.js +++ b/src/components/map/Map.js @@ -7,6 +7,7 @@ import MapDrawing from "./MapDrawing"; import MapFog from "./MapFog"; import MapDice from "./MapDice"; import MapGrid from "./MapGrid"; +import MapMeasure from "./MapMeasure"; import TokenDataContext from "../../contexts/TokenDataContext"; import MapLoadingContext from "../../contexts/MapLoadingContext"; @@ -53,6 +54,9 @@ function Map({ type: "brush", useBlending: true, }, + measure: { + type: "chebyshev", + }, }); function handleToolSettingChange(tool, change) { @@ -134,6 +138,7 @@ function Map({ } if (!map) { disabledControls.push("pan"); + disabledControls.push("measure"); } if (!allowFogDrawing) { disabledControls.push("fog"); @@ -277,6 +282,14 @@ function Map({ ); + const mapMeasure = ( + + ); + return ( ); } diff --git a/src/components/map/MapControls.js b/src/components/map/MapControls.js index 8cf5f51..983bd0e 100644 --- a/src/components/map/MapControls.js +++ b/src/components/map/MapControls.js @@ -8,10 +8,12 @@ import SelectMapButton from "./SelectMapButton"; import FogToolSettings from "./controls/FogToolSettings"; 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 MeasureToolIcon from "../../icons/MeasureToolIcon"; import ExpandMoreIcon from "../../icons/ExpandMoreIcon"; function MapContols({ @@ -47,8 +49,14 @@ function MapContols({ title: "Drawing Tool", SettingsComponent: DrawingToolSettings, }, + measure: { + id: "measure", + icon: , + title: "Measure Tool", + SettingsComponent: MeasureToolSettings, + }, }; - const tools = ["pan", "fog", "drawing"]; + const tools = ["pan", "fog", "drawing", "measure"]; const sections = [ { @@ -63,7 +71,7 @@ function MapContols({ ), }, { - id: "drawing", + id: "tools", component: tools.map((tool) => ( { + 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 ( + + + + + + ); + } + + return {drawingShapeData && renderShape(drawingShapeData)}; +} + +export default MapMeasure; diff --git a/src/components/map/controls/MeasureToolSettings.js b/src/components/map/controls/MeasureToolSettings.js new file mode 100644 index 0000000..22d0f6d --- /dev/null +++ b/src/components/map/controls/MeasureToolSettings.js @@ -0,0 +1,41 @@ +import React 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"; + +function MeasureToolSettings({ settings, onSettingChange }) { + const tools = [ + { + id: "chebyshev", + title: "Grid Distance", + isSelected: settings.type === "chebyshev", + icon: , + }, + { + id: "euclidean", + title: "Line Distance", + isSelected: settings.type === "euclidean", + icon: , + }, + { + id: "manhattan", + title: "City Block Distance", + isSelected: settings.type === "manhattan", + icon: , + }, + ]; + + return ( + + onSettingChange({ type: tool.id })} + /> + + ); +} + +export default MeasureToolSettings; diff --git a/src/components/map/controls/ToolSection.js b/src/components/map/controls/ToolSection.js index d99f48a..30c24cb 100644 --- a/src/components/map/controls/ToolSection.js +++ b/src/components/map/controls/ToolSection.js @@ -96,4 +96,8 @@ function ToolSection({ collapse, tools, onToolClick }) { } } +ToolSection.defaultProps = { + collapse: false, +}; + export default ToolSection; diff --git a/src/helpers/vector2.js b/src/helpers/vector2.js index 63f3030..de79f9b 100644 --- a/src/helpers/vector2.js +++ b/src/helpers/vector2.js @@ -219,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)); + } +} diff --git a/src/icons/MeasureChebyshevIcon.js b/src/icons/MeasureChebyshevIcon.js new file mode 100644 index 0000000..5073dd2 --- /dev/null +++ b/src/icons/MeasureChebyshevIcon.js @@ -0,0 +1,18 @@ +import React from "react"; + +function MeasureChebyshevIcon() { + return ( + + + + + ); +} + +export default MeasureChebyshevIcon; diff --git a/src/icons/MeasureEuclideanIcon.js b/src/icons/MeasureEuclideanIcon.js new file mode 100644 index 0000000..6b29bf5 --- /dev/null +++ b/src/icons/MeasureEuclideanIcon.js @@ -0,0 +1,20 @@ +import React from "react"; + +function MeasureEuclideanIcon() { + return ( + + + + + + + ); +} + +export default MeasureEuclideanIcon; diff --git a/src/icons/MeasureManhattanIcon.js b/src/icons/MeasureManhattanIcon.js new file mode 100644 index 0000000..5371f99 --- /dev/null +++ b/src/icons/MeasureManhattanIcon.js @@ -0,0 +1,18 @@ +import React from "react"; + +function MeasureManhattanIcon() { + return ( + + + + + ); +} + +export default MeasureManhattanIcon; diff --git a/src/icons/MeasureToolIcon.js b/src/icons/MeasureToolIcon.js new file mode 100644 index 0000000..d62c53b --- /dev/null +++ b/src/icons/MeasureToolIcon.js @@ -0,0 +1,19 @@ +import React from "react"; + +function MeasureToolIcon() { + return ( + + + {" "} + + ); +} + +export default MeasureToolIcon;