Move to new fog snapping with guides and vertex snapping
This commit is contained in:
parent
547a214149
commit
85270859bb
@ -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) => (
|
||||
<Line
|
||||
points={[
|
||||
guide.start.x * mapWidth,
|
||||
guide.start.y * mapHeight,
|
||||
guide.end.x * mapWidth,
|
||||
guide.end.y * mapHeight,
|
||||
]}
|
||||
stroke="hsl(260, 100%, 80%)"
|
||||
key={index}
|
||||
strokeWidth={gridStrokeWidth * 0.25}
|
||||
lineCap="round"
|
||||
lineJoin="round"
|
||||
/>
|
||||
));
|
||||
}
|
||||
|
||||
function renderSnappingVertex() {
|
||||
return (
|
||||
<Circle
|
||||
x={vertexSnapping.x * mapWidth}
|
||||
y={vertexSnapping.y * mapHeight}
|
||||
radius={gridStrokeWidth}
|
||||
stroke="hsl(260, 100%, 80%)"
|
||||
strokeWidth={gridStrokeWidth * 0.25}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<Group>
|
||||
@ -452,6 +559,8 @@ function MapFog({
|
||||
<Rect width={1} height={1} />
|
||||
{fogShapes.map(renderShape)}
|
||||
</Group>
|
||||
{shouldRenderGuides && renderGuides()}
|
||||
{shouldRenderVertexSnapping && renderSnappingVertex()}
|
||||
{drawingShape && renderShape(drawingShape)}
|
||||
{drawingShape &&
|
||||
toolSettings &&
|
||||
|
@ -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,
|
||||
|
@ -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 <GridContext.Provider value={value}>{children}</GridContext.Provider>;
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user