diff --git a/src/components/map/MapFog.js b/src/components/map/MapFog.js
index 0e8fdea..bec840d 100644
--- a/src/components/map/MapFog.js
+++ b/src/components/map/MapFog.js
@@ -1,6 +1,6 @@
import React, { useState, useEffect, useCallback, useRef } from "react";
import shortid from "shortid";
-import { Group, Rect } from "react-konva";
+import { Group, Rect, Line, Circle } from "react-konva";
import useImage from "use-image";
import diagonalPattern from "../../images/DiagonalPattern.png";
@@ -11,7 +11,15 @@ import { useGrid } from "../../contexts/GridContext";
import { useKeyboard } from "../../contexts/KeyboardContext";
import Vector2 from "../../helpers/Vector2";
-import { simplifyPoints, mergeShapes } from "../../helpers/drawing";
+import {
+ simplifyPoints,
+ mergeFogShapes,
+ getFogShapesBoundingBoxes,
+ getGuidesFromBoundingBoxes,
+ getGuidesFromGridCell,
+ findBestGuides,
+ getSnappingVertex,
+} from "../../helpers/drawing";
import colors from "../../helpers/colors";
import {
HoleyLine,
@@ -20,7 +28,7 @@ import {
} from "../../helpers/konva";
import useDebounce from "../../hooks/useDebounce";
-import useGridSnapping from "../../hooks/useGridSnapping";
+import useSetting from "../../hooks/useSetting";
function MapFog({
map,
@@ -39,21 +47,48 @@ function MapFog({
mapHeight,
interactionEmitter,
} = useMapInteraction();
- const { gridCellNormalizedSize, gridStrokeWidth } = useGrid();
+ const {
+ grid,
+ gridCellNormalizedSize,
+ gridCellPixelSize,
+ gridStrokeWidth,
+ gridCellPixelOffset,
+ gridOffset,
+ } = useGrid();
+ const [gridSnappingSensitivity] = useSetting("map.gridSnappingSensitivity");
const mapStageRef = useMapStage();
const [drawingShape, setDrawingShape] = useState(null);
const [isBrushDown, setIsBrushDown] = useState(false);
const [editingShapes, setEditingShapes] = useState([]);
+ // Shapes that have been merged for fog
+ const [fogShapes, setFogShapes] = useState(shapes);
+ // Bounding boxes for guides
+ const [fogShapeBoundingBoxes, setFogShapeBoundingBoxes] = useState([]);
+ const [guides, setGuides] = useState([]);
+ const [vertexSnapping, setVertexSnapping] = useState();
+
const shouldHover =
active &&
editable &&
(toolSettings.type === "toggle" || toolSettings.type === "remove");
- const [patternImage] = useImage(diagonalPattern);
+ const shouldRenderGuides =
+ active &&
+ editable &&
+ (toolSettings.type === "rectangle" || toolSettings.type === "polygon") &&
+ !vertexSnapping;
+ const shouldRenderVertexSnapping =
+ active &&
+ editable &&
+ (toolSettings.type === "rectangle" ||
+ toolSettings.type === "polygon" ||
+ toolSettings.type === "brush") &&
+ toolSettings.useEdgeSnapping &&
+ vertexSnapping;
- const snapPositionToGrid = useGridSnapping();
+ const [patternImage] = useImage(diagonalPattern);
useEffect(() => {
if (!active || !editable) {
@@ -62,14 +97,25 @@ function MapFog({
const mapStage = mapStageRef.current;
- function getBrushPosition() {
+ function getBrushPosition(snapping = true) {
const mapImage = mapStage.findOne("#mapImage");
let position = getRelativePointerPosition(mapImage);
- if (
- map.snapToGrid &&
- (toolSettings.type === "polygon" || toolSettings.type === "rectangle")
- ) {
- position = snapPositionToGrid(position);
+ if (snapping) {
+ if (shouldRenderVertexSnapping) {
+ position = Vector2.multiply(vertexSnapping, {
+ x: mapWidth,
+ y: mapHeight,
+ });
+ } else if (shouldRenderGuides) {
+ for (let guide of guides) {
+ if (guide.orientation === "vertical") {
+ position.x = guide.start.x * mapWidth;
+ }
+ if (guide.orientation === "horizontal") {
+ position.y = guide.start.y * mapHeight;
+ }
+ }
+ }
}
return Vector2.divide(position, {
x: mapImage.width(),
@@ -78,8 +124,8 @@ function MapFog({
}
function handleBrushDown() {
- const brushPosition = getBrushPosition();
if (toolSettings.type === "brush") {
+ const brushPosition = getBrushPosition();
setDrawingShape({
type: "fog",
data: {
@@ -93,6 +139,7 @@ function MapFog({
});
}
if (toolSettings.type === "rectangle") {
+ const brushPosition = getBrushPosition();
setDrawingShape({
type: "fog",
data: {
@@ -225,20 +272,77 @@ function MapFog({
}
function handlePolygonMove() {
- if (toolSettings.type === "polygon" && drawingShape) {
- const brushPosition = getBrushPosition();
- setDrawingShape((prevShape) => {
- if (!prevShape) {
- return;
- }
- return {
- ...prevShape,
- data: {
- ...prevShape.data,
- points: [...prevShape.data.points.slice(0, -1), brushPosition],
- },
- };
+ if (
+ active &&
+ (toolSettings.type === "polygon" ||
+ toolSettings.type === "rectangle") &&
+ !shouldRenderVertexSnapping
+ ) {
+ let guides = [];
+ const brushPosition = getBrushPosition(false);
+ const absoluteBrushPosition = Vector2.multiply(brushPosition, {
+ x: mapWidth,
+ y: mapHeight,
});
+ if (map.snapToGrid) {
+ guides.push(
+ ...getGuidesFromGridCell(
+ absoluteBrushPosition,
+ grid,
+ gridCellPixelSize,
+ gridOffset,
+ gridCellPixelOffset,
+ gridSnappingSensitivity,
+ { x: mapWidth, y: mapHeight }
+ )
+ );
+ }
+
+ guides.push(
+ ...getGuidesFromBoundingBoxes(
+ brushPosition,
+ fogShapeBoundingBoxes,
+ gridCellNormalizedSize,
+ gridSnappingSensitivity
+ )
+ );
+
+ setGuides(findBestGuides(brushPosition, guides));
+ }
+ if (
+ active &&
+ toolSettings.useEdgeSnapping &&
+ (toolSettings.type === "polygon" ||
+ toolSettings.type === "rectangle" ||
+ toolSettings.type === "brush")
+ ) {
+ const brushPosition = getBrushPosition(false);
+ setVertexSnapping(
+ getSnappingVertex(
+ brushPosition,
+ fogShapes,
+ fogShapeBoundingBoxes,
+ gridCellNormalizedSize,
+ Math.min(0.4 / stageScale, 0.4)
+ )
+ );
+ }
+ if (toolSettings.type === "polygon") {
+ const brushPosition = getBrushPosition();
+ if (toolSettings.type === "polygon" && drawingShape) {
+ setDrawingShape((prevShape) => {
+ if (!prevShape) {
+ return;
+ }
+ return {
+ ...prevShape,
+ data: {
+ ...prevShape.data,
+ points: [...prevShape.data.points.slice(0, -1), brushPosition],
+ },
+ };
+ });
+ }
}
}
@@ -356,17 +460,17 @@ function MapFog({
closed
lineCap="round"
lineJoin="round"
- strokeWidth={gridStrokeWidth * shape.strokeWidth}
- opacity={editable ? 0.5 : 1}
+ strokeWidth={editable ? gridStrokeWidth * shape.strokeWidth : 0}
+ opacity={editable ? (!shape.visible ? 0.2 : 0.5) : 1}
fillPatternImage={patternImage}
fillPriority={active && !shape.visible ? "pattern" : "color"}
holes={holes}
// Disable collision if the fog is transparent and we're not editing it
// This allows tokens to be moved under the fog
hitFunc={editable && !active ? () => {} : undefined}
- shadowColor={editable ? "rgba(0, 0, 0, 0)" : "rgba(34, 34, 34, 0.50)"}
- shadowOffset={{ x: 0, y: 5 }}
- shadowBlur={10}
+ // shadowColor={editable ? "rgba(0, 0, 0, 0)" : "rgba(34, 34, 34, 1)"}
+ // shadowOffset={{ x: 0, y: 5 }}
+ // shadowBlur={10}
/>
);
}
@@ -402,48 +506,51 @@ function MapFog({
);
}
- const [fogShapes, setFogShapes] = useState(shapes);
+ function renderGuides() {
+ return guides.map((guide, index) => (
+
+ ));
+ }
+
+ function renderSnappingVertex() {
+ return (
+
+ );
+ }
+
useEffect(() => {
function shapeVisible(shape) {
return (active && !toolSettings.preview) || shape.visible;
}
if (editable) {
- setFogShapes(shapes.filter(shapeVisible));
+ const visibleShapes = shapes.filter(shapeVisible);
+ setFogShapeBoundingBoxes(getFogShapesBoundingBoxes(visibleShapes));
+ setFogShapes(visibleShapes);
} else {
- setFogShapes(mergeShapes(shapes));
+ setFogShapes(mergeFogShapes(shapes));
}
}, [shapes, editable, active, toolSettings]);
const fogGroupRef = useRef();
- const debouncedStageScale = useDebounce(stageScale, 50);
-
- useEffect(() => {
- const fogGroup = fogGroupRef.current;
-
- if (!editable) {
- const canvas = fogGroup.getChildren()[0].getCanvas();
- const pixelRatio = canvas.pixelRatio || 1;
-
- // Constrain fog buffer to the map resolution
- const fogRect = fogGroup.getClientRect();
- const maxMapSize = map ? Math.max(map.width, map.height) : 4096; // Default to 4096
- const maxFogSize =
- Math.max(fogRect.width, fogRect.height) / debouncedStageScale;
- const maxPixelRatio = maxMapSize / maxFogSize;
-
- fogGroup.cache({
- pixelRatio: Math.min(
- Math.max(debouncedStageScale * pixelRatio, 1),
- maxPixelRatio
- ),
- });
- } else {
- fogGroup.clearCache();
- }
-
- fogGroup.getLayer().draw();
- }, [fogShapes, editable, active, debouncedStageScale, mapWidth, map]);
return (
@@ -452,6 +559,8 @@ function MapFog({
{fogShapes.map(renderShape)}
+ {shouldRenderGuides && renderGuides()}
+ {shouldRenderVertexSnapping && renderSnappingVertex()}
{drawingShape && renderShape(drawingShape)}
{drawingShape &&
toolSettings &&
diff --git a/src/components/map/MapMeasure.js b/src/components/map/MapMeasure.js
index 18d28d2..120bfef 100644
--- a/src/components/map/MapMeasure.js
+++ b/src/components/map/MapMeasure.js
@@ -27,6 +27,7 @@ function MapMeasure({ map, active }) {
gridCellNormalizedSize,
gridStrokeWidth,
gridCellPixelSize,
+ gridOffset,
} = useGrid();
const mapStageRef = useMapStage();
const [drawingShapeData, setDrawingShapeData] = useState(null);
@@ -73,14 +74,20 @@ function MapMeasure({ map, active }) {
gridCellNormalizedSize
);
// Convert back to pixel values
- const a = Vector2.multiply(points[0], {
- x: mapImage.width(),
- y: mapImage.height(),
- });
- const b = Vector2.multiply(points[1], {
- x: mapImage.width(),
- y: mapImage.height(),
- });
+ const a = Vector2.subtract(
+ Vector2.multiply(points[0], {
+ x: mapImage.width(),
+ y: mapImage.height(),
+ }),
+ gridOffset
+ );
+ const b = Vector2.subtract(
+ Vector2.multiply(points[1], {
+ x: mapImage.width(),
+ y: mapImage.height(),
+ }),
+ gridOffset
+ );
const length = gridDistance(grid, a, b, gridCellPixelSize);
setDrawingShapeData({
length,
diff --git a/src/contexts/GridContext.js b/src/contexts/GridContext.js
index d7df4c1..d8e9ca9 100644
--- a/src/contexts/GridContext.js
+++ b/src/contexts/GridContext.js
@@ -1,4 +1,4 @@
-import React, { useContext } from "react";
+import React, { useContext, useCallback } from "react";
import Vector2 from "../helpers/Vector2";
import Size from "../helpers/Size";
@@ -13,6 +13,7 @@ import { getGridPixelSize, getCellPixelSize, Grid } from "../helpers/grid";
* @property {Size} gridCellNormalizedSize Size of each cell normalized to the grid
* @property {Vector2} gridOffset Offset of the grid from the top left in pixels
* @property {number} gridStrokeWidth Stroke width of the grid in pixels
+ * @property {Vector2} gridCellPixelOffset Offset of the grid cells to convert the center position of hex cells to the top left
*/
/**
@@ -33,6 +34,7 @@ const defaultValue = {
gridCellNormalizedSize: new Size(0, 0, 0),
gridOffset: new Vector2(0, 0),
gridStrokeWidth: 0,
+ gridCellPixelOffset: new Vector2(0, 0),
};
const GridContext = React.createContext(defaultValue);
@@ -67,6 +69,12 @@ export function GridProvider({ grid, width, height, children }) {
? gridCellPixelSize.width
: gridCellPixelSize.height) * defaultStrokeWidth;
+ let gridCellPixelOffset = { x: 0, y: 0 };
+ // Move hex tiles to top left
+ if (grid.type === "hexVertical" || grid.type === "hexHorizontal") {
+ gridCellPixelOffset = Vector2.multiply(gridCellPixelSize, 0.5);
+ }
+
const value = {
grid,
gridPixelSize,
@@ -74,6 +82,7 @@ export function GridProvider({ grid, width, height, children }) {
gridCellNormalizedSize,
gridOffset,
gridStrokeWidth,
+ gridCellPixelOffset,
};
return {children};
diff --git a/src/helpers/Vector2.js b/src/helpers/Vector2.js
index 4fd842b..ee3ccf3 100644
--- a/src/helpers/Vector2.js
+++ b/src/helpers/Vector2.js
@@ -344,12 +344,21 @@ class Vector2 {
return { distance: Math.sqrt(distance), point: point };
}
+ /**
+ * @typedef BoundingBox
+ * @property {Vector2} min
+ * @property {Vector2} max
+ * @property {number} width
+ * @property {number} height
+ * @property {Vector2} center
+ */
+
/**
* Calculates an axis-aligned bounding box around an array of point
* @param {Vector2[]} points
- * @returns {Object}
+ * @returns {BoundingBox}
*/
- static getBounds(points) {
+ static getBoundingBox(points) {
let minX = Number.MAX_VALUE;
let maxX = Number.MIN_VALUE;
let minY = Number.MAX_VALUE;
@@ -360,7 +369,16 @@ class Vector2 {
minY = point.y < minY ? point.y : minY;
maxY = point.y > maxY ? point.y : maxY;
}
- return { minX, maxX, minY, maxY };
+ let width = maxX - minX;
+ let height = maxY - minY;
+ let center = { x: (minX + maxX) / 2, y: (minY + maxY) / 2 };
+ return {
+ min: { x: minX, y: minY },
+ max: { x: maxX, y: maxY },
+ width,
+ height,
+ center,
+ };
}
/**
@@ -372,8 +390,13 @@ class Vector2 {
* @returns {boolean}
*/
static pointInPolygon(p, points) {
- const { minX, maxX, minY, maxY } = this.getBounds(points);
- if (p.x < minX || p.x > maxX || p.y < minY || p.y > maxY) {
+ const bounds = this.getBoundingBox(points);
+ if (
+ p.x < bounds.min.x ||
+ p.x > bounds.max.x ||
+ p.y < bounds.min.y ||
+ p.y > bounds.max.y
+ ) {
return false;
}
diff --git a/src/helpers/drawing.js b/src/helpers/drawing.js
index 4db521b..45a6ab1 100644
--- a/src/helpers/drawing.js
+++ b/src/helpers/drawing.js
@@ -4,7 +4,83 @@ import polygonClipping from "polygon-clipping";
import Vector2 from "./Vector2";
import { toDegrees } from "./shared";
import { logError } from "./logging";
+import { getNearestCellCoordinates, getCellLocation } from "./grid";
+/**
+ * @typedef PointsData
+ * @property {Vector2[]} points
+ */
+
+/**
+ * @typedef RectData
+ * @property {number} x
+ * @property {number} y
+ * @property {number} width
+ * @property {number} height
+ */
+
+/**
+ * @typedef CircleData
+ * @property {number} x
+ * @property {number} y
+ * @property {number} radius
+ */
+/**
+ * @typedef FogData
+ * @property {Vector2[]} points
+ * @property {Vector2[]} holes
+ */
+
+/**
+ * @typedef {(PointsData|RectData|CircleData)} ShapeData
+ */
+
+/**
+ * @typedef {("line"|"rectangle"|"circle"|"triangle")} ShapeType
+ */
+
+/**
+ * @typedef {("fill"|"stroke")} PathType
+ */
+
+/**
+ * @typedef Path
+ * @property {boolean} blend
+ * @property {string} color
+ * @property {PointsData} data
+ * @property {string} id
+ * @property {PathType} pathType
+ * @property {number} strokeWidth
+ * @property {"path"} type
+ */
+
+/**
+ * @typedef Shape
+ * @property {boolean} blend
+ * @property {string} color
+ * @property {ShapeData} data
+ * @property {string} id
+ * @property {ShapeType} shapeType
+ * @property {number} strokeWidth
+ * @property {"shape"} type
+ */
+
+/**
+ * @typedef Fog
+ * @property {string} color
+ * @property {FogData} data
+ * @property {string} id
+ * @property {number} strokeWidth
+ * @property {"fog"} type
+ * @property {boolean} visible
+ */
+
+/**
+ *
+ * @param {ShapeType} type
+ * @param {Vector2} brushPosition
+ * @returns {ShapeData}
+ */
export function getDefaultShapeData(type, brushPosition) {
if (type === "line") {
return {
@@ -33,6 +109,10 @@ export function getDefaultShapeData(type, brushPosition) {
}
}
+/**
+ * @param {Vector2} cellSize
+ * @returns {Vector2}
+ */
export function getGridScale(cellSize) {
if (cellSize.x < cellSize.y) {
return { x: cellSize.y / cellSize.x, y: 1 };
@@ -43,6 +123,14 @@ export function getGridScale(cellSize) {
}
}
+/**
+ *
+ * @param {ShapeType} type
+ * @param {ShapeData} data
+ * @param {Vector2} brushPosition
+ * @param {Vector2} gridCellNormalizedSize
+ * @returns {ShapeData}
+ */
export function getUpdatedShapeData(
type,
data,
@@ -99,24 +187,26 @@ export function getUpdatedShapeData(
}
}
-const defaultStrokeWidth = 1 / 10;
-export function getStrokeWidth(multiplier, gridSize, mapWidth, mapHeight) {
- const gridPixelSize = Vector2.multiply(gridSize, {
- x: mapWidth,
- y: mapHeight,
- });
- return Vector2.min(gridPixelSize) * defaultStrokeWidth * multiplier;
-}
-
const defaultSimplifySize = 1 / 100;
-export function simplifyPoints(points, gridCellNormalizedSize, scale) {
+/**
+ * Simplify points to a grid size
+ * @param {Vector2[]} points
+ * @param {Vector2} gridCellSize
+ * @param {number} scale
+ */
+export function simplifyPoints(points, gridCellSize, scale) {
return simplify(
points,
- (Vector2.min(gridCellNormalizedSize) * defaultSimplifySize) / scale
+ (Vector2.min(gridCellSize) * defaultSimplifySize) / scale
);
}
-export function mergeShapes(shapes) {
+/**
+ * Merges overlapping fog shapes
+ * @param {Fog[]} shapes
+ * @returns {Fog[]}
+ */
+export function mergeFogShapes(shapes) {
if (shapes.length === 0) {
return shapes;
}
@@ -161,3 +251,267 @@ export function mergeShapes(shapes) {
return shapes;
}
}
+
+/**
+ * @param {Fog[]} shapes
+ * @returns {Vector2.BoundingBox[]}
+ */
+export function getFogShapesBoundingBoxes(shapes) {
+ let boxes = [];
+ for (let shape of shapes) {
+ boxes.push(Vector2.getBoundingBox(shape.data.points));
+ }
+ return boxes;
+}
+
+/**
+ * @typedef Edge
+ * @property {Vector2} start
+ * @property {Vector2} end
+ */
+
+/**
+ * @typedef Guide
+ * @property {Vector2} start
+ * @property {Vector2} end
+ * @property {("horizontal"|"vertical")} orientation
+ * @property {number}
+ */
+
+/**
+ * @param {Vector2} brushPosition Brush position in pixels
+ * @param {Vector2} grid
+ * @param {Vector2} gridCellSize Grid cell size in pixels
+ * @param {Vector2} gridOffset
+ * @param {Vector2} gridCellOffset
+ * @param {number} snappingSensitivity
+ * @param {Vector2} mapSize
+ * @returns {Guide[]}
+ */
+export function getGuidesFromGridCell(
+ brushPosition,
+ grid,
+ gridCellSize,
+ gridOffset,
+ gridCellOffset,
+ snappingSensitivity,
+ mapSize
+) {
+ let boundingBoxes = [];
+ // Add map bounds
+ boundingBoxes.push(
+ Vector2.getBoundingBox([
+ { x: 0, y: 0 },
+ { x: 1, y: 1 },
+ ])
+ );
+ let offsetPosition = Vector2.subtract(
+ Vector2.subtract(brushPosition, gridOffset),
+ gridCellOffset
+ );
+ const cellCoords = getNearestCellCoordinates(
+ grid,
+ offsetPosition.x,
+ offsetPosition.y,
+ gridCellSize
+ );
+ let cellPosition = getCellLocation(
+ grid,
+ cellCoords.x,
+ cellCoords.y,
+ gridCellSize
+ );
+ cellPosition = Vector2.add(
+ Vector2.add(cellPosition, gridOffset),
+ gridCellOffset
+ );
+ // Normalize values so output is normalized
+ cellPosition = Vector2.divide(cellPosition, mapSize);
+ const gridCellNormalizedSize = Vector2.divide(gridCellSize, mapSize);
+ const brushPositionNorm = Vector2.divide(brushPosition, mapSize);
+ const boundingBox = Vector2.getBoundingBox([
+ {
+ x: cellPosition.x - gridCellNormalizedSize.x / 2,
+ y: cellPosition.y - gridCellNormalizedSize.y / 2,
+ },
+ {
+ x: cellPosition.x + gridCellNormalizedSize.x / 2,
+ y: cellPosition.y + gridCellNormalizedSize.y / 2,
+ },
+ ]);
+ boundingBoxes.push(boundingBox);
+ return getGuidesFromBoundingBoxes(
+ brushPositionNorm,
+ boundingBoxes,
+ gridCellNormalizedSize,
+ snappingSensitivity
+ );
+}
+
+/**
+ * @param {Vector2} brushPosition
+ * @param {Vector2.BoundingBox[]} boundingBoxes
+ * @param {Vector2} gridCellSize
+ * @param {number} snappingSensitivity
+ * @returns {Guide[]}
+ */
+export function getGuidesFromBoundingBoxes(
+ brushPosition,
+ boundingBoxes,
+ gridCellSize,
+ snappingSensitivity
+) {
+ let horizontalEdges = [];
+ let verticalEdges = [];
+ for (let bounds of boundingBoxes) {
+ horizontalEdges.push({
+ start: { x: bounds.min.x, y: bounds.min.y },
+ end: { x: bounds.max.x, y: bounds.min.y },
+ });
+ horizontalEdges.push({
+ start: { x: bounds.min.x, y: bounds.center.y },
+ end: { x: bounds.max.x, y: bounds.center.y },
+ });
+ horizontalEdges.push({
+ start: { x: bounds.min.x, y: bounds.max.y },
+ end: { x: bounds.max.x, y: bounds.max.y },
+ });
+
+ verticalEdges.push({
+ start: { x: bounds.min.x, y: bounds.min.y },
+ end: { x: bounds.min.x, y: bounds.max.y },
+ });
+ verticalEdges.push({
+ start: { x: bounds.center.x, y: bounds.min.y },
+ end: { x: bounds.center.x, y: bounds.max.y },
+ });
+ verticalEdges.push({
+ start: { x: bounds.max.x, y: bounds.min.y },
+ end: { x: bounds.max.x, y: bounds.max.y },
+ });
+ }
+ let guides = [];
+ for (let edge of verticalEdges) {
+ const distance = Math.abs(brushPosition.x - edge.start.x);
+ if (distance / gridCellSize.x < snappingSensitivity) {
+ guides.push({ ...edge, distance, orientation: "vertical" });
+ }
+ }
+ for (let edge of horizontalEdges) {
+ const distance = Math.abs(brushPosition.y - edge.start.y);
+ if (distance / gridCellSize.y < snappingSensitivity) {
+ guides.push({ ...edge, distance, orientation: "horizontal" });
+ }
+ }
+ return guides;
+}
+
+/**
+ * @param {Vector2} brushPosition
+ * @param {Guide[]} guides
+ * @returns {Guide[]}
+ */
+export function findBestGuides(brushPosition, guides) {
+ let bestGuides = [];
+ let verticalGuide = guides
+ .filter((guide) => guide.orientation === "vertical")
+ .sort((a, b) => a.distance - b.distance)[0];
+ let horizontalGuide = guides
+ .filter((guide) => guide.orientation === "horizontal")
+ .sort((a, b) => a.distance - b.distance)[0];
+
+ // Offset edges to match brush position
+ if (verticalGuide && !horizontalGuide) {
+ verticalGuide.start.y = Math.min(verticalGuide.start.y, brushPosition.y);
+ verticalGuide.end.y = Math.max(verticalGuide.end.y, brushPosition.y);
+ bestGuides.push(verticalGuide);
+ }
+ if (horizontalGuide && !verticalGuide) {
+ horizontalGuide.start.x = Math.min(
+ horizontalGuide.start.x,
+ brushPosition.x
+ );
+ horizontalGuide.end.x = Math.max(horizontalGuide.end.x, brushPosition.x);
+ bestGuides.push(horizontalGuide);
+ }
+ if (horizontalGuide && verticalGuide) {
+ verticalGuide.start.y = Math.min(
+ verticalGuide.start.y,
+ horizontalGuide.start.y
+ );
+ verticalGuide.end.y = Math.max(
+ verticalGuide.end.y,
+ horizontalGuide.start.y
+ );
+
+ horizontalGuide.start.x = Math.min(
+ horizontalGuide.start.x,
+ verticalGuide.start.x
+ );
+ horizontalGuide.end.x = Math.max(
+ horizontalGuide.end.x,
+ verticalGuide.start.x
+ );
+
+ bestGuides.push(horizontalGuide);
+ bestGuides.push(verticalGuide);
+ }
+ return bestGuides;
+}
+
+/**
+ * @param {Vector2} brushPosition
+ * @param {Fog[]} shapes
+ * @param {Vector2.BoundingBox} boundingBoxes
+ * @param {Vector2} gridCellSize
+ * @param {number} snappingSensitivity
+ */
+export function getSnappingVertex(
+ brushPosition,
+ shapes,
+ boundingBoxes,
+ gridCellSize,
+ snappingSensitivity
+) {
+ const minGrid = Vector2.min(gridCellSize);
+ const snappingDistance = minGrid * snappingSensitivity;
+
+ let closestDistance = Number.MAX_VALUE;
+ let closestPosition;
+
+ for (let i = 0; i < shapes.length; i++) {
+ // TODO: Check bounds before checking all points
+ // const bounds = boundingBoxes[i];
+ const shape = shapes[i];
+ // Include shape points and holes
+ let pointArray = [shape.data.points, ...shape.data.holes];
+
+ for (let points of pointArray) {
+ // Find the closest point to each edge of the shape
+ for (let i = 0; i < points.length; i++) {
+ const a = points[i];
+ // Wrap around points to the start to account for closed shape
+ const b = points[(i + 1) % points.length];
+
+ let { distance, point } = Vector2.distanceToLine(brushPosition, a, b);
+ // Bias towards vertices
+ distance += snappingDistance / 2;
+ const isCloseToShape = distance < snappingDistance;
+ if (isCloseToShape && distance < closestDistance) {
+ closestPosition = point;
+ closestDistance = distance;
+ }
+ }
+ // Find cloest vertex
+ for (let point of points) {
+ const distance = Vector2.distance(point, brushPosition);
+ const isCloseToShape = distance < snappingDistance;
+ if (isCloseToShape && distance < closestDistance) {
+ closestPosition = point;
+ closestDistance = distance;
+ }
+ }
+ }
+ }
+ return closestPosition;
+}
diff --git a/src/hooks/useGridSnapping.js b/src/hooks/useGridSnapping.js
index 23a720f..ddaaee9 100644
--- a/src/hooks/useGridSnapping.js
+++ b/src/hooks/useGridSnapping.js
@@ -19,21 +19,22 @@ function useGridSnapping(snappingSensitivity) {
);
snappingSensitivity = snappingSensitivity || defaultSnappingSensitivity;
- const { grid, gridOffset, gridCellPixelSize } = useGrid();
+ const {
+ grid,
+ gridOffset,
+ gridCellPixelSize,
+ gridCellPixelOffset,
+ } = useGrid();
/**
* @param {Vector2} node The node to snap
*/
function snapPositionToGrid(position) {
// Account for grid offset
- let offsetPosition = Vector2.subtract(position, gridOffset);
- // Move hex tiles to top left
- if (grid.type === "hexVertical" || grid.type === "hexHorizontal") {
- offsetPosition = Vector2.subtract(
- offsetPosition,
- Vector2.multiply(gridCellPixelSize, 0.5)
- );
- }
+ let offsetPosition = Vector2.subtract(
+ Vector2.subtract(position, gridOffset),
+ gridCellPixelOffset
+ );
const nearsetCell = getNearestCellCoordinates(
grid,
offsetPosition.x,
@@ -62,14 +63,10 @@ function useGridSnapping(snappingSensitivity) {
Vector2.min(gridCellPixelSize) * snappingSensitivity
) {
// Reverse grid offset
- let offsetSnapPoint = Vector2.add(snapPoint, gridOffset);
- // Reverse offset for hex tiles
- if (grid.type === "hexVertical" || grid.type === "hexHorizontal") {
- offsetSnapPoint = Vector2.add(
- offsetSnapPoint,
- Vector2.multiply(gridCellPixelSize, 0.5)
- );
- }
+ let offsetSnapPoint = Vector2.add(
+ Vector2.add(snapPoint, gridOffset),
+ gridCellPixelOffset
+ );
return offsetSnapPoint;
}
}