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 React, { useState, useEffect, useCallback, useRef } from "react";
|
||||||
import shortid from "shortid";
|
import shortid from "shortid";
|
||||||
import { Group, Rect } from "react-konva";
|
import { Group, Rect, Line, Circle } from "react-konva";
|
||||||
import useImage from "use-image";
|
import useImage from "use-image";
|
||||||
|
|
||||||
import diagonalPattern from "../../images/DiagonalPattern.png";
|
import diagonalPattern from "../../images/DiagonalPattern.png";
|
||||||
@ -11,7 +11,15 @@ import { useGrid } from "../../contexts/GridContext";
|
|||||||
import { useKeyboard } from "../../contexts/KeyboardContext";
|
import { useKeyboard } from "../../contexts/KeyboardContext";
|
||||||
|
|
||||||
import Vector2 from "../../helpers/Vector2";
|
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 colors from "../../helpers/colors";
|
||||||
import {
|
import {
|
||||||
HoleyLine,
|
HoleyLine,
|
||||||
@ -20,7 +28,7 @@ import {
|
|||||||
} from "../../helpers/konva";
|
} from "../../helpers/konva";
|
||||||
|
|
||||||
import useDebounce from "../../hooks/useDebounce";
|
import useDebounce from "../../hooks/useDebounce";
|
||||||
import useGridSnapping from "../../hooks/useGridSnapping";
|
import useSetting from "../../hooks/useSetting";
|
||||||
|
|
||||||
function MapFog({
|
function MapFog({
|
||||||
map,
|
map,
|
||||||
@ -39,21 +47,48 @@ function MapFog({
|
|||||||
mapHeight,
|
mapHeight,
|
||||||
interactionEmitter,
|
interactionEmitter,
|
||||||
} = useMapInteraction();
|
} = useMapInteraction();
|
||||||
const { gridCellNormalizedSize, gridStrokeWidth } = useGrid();
|
const {
|
||||||
|
grid,
|
||||||
|
gridCellNormalizedSize,
|
||||||
|
gridCellPixelSize,
|
||||||
|
gridStrokeWidth,
|
||||||
|
gridCellPixelOffset,
|
||||||
|
gridOffset,
|
||||||
|
} = useGrid();
|
||||||
|
const [gridSnappingSensitivity] = useSetting("map.gridSnappingSensitivity");
|
||||||
const mapStageRef = useMapStage();
|
const mapStageRef = useMapStage();
|
||||||
|
|
||||||
const [drawingShape, setDrawingShape] = useState(null);
|
const [drawingShape, setDrawingShape] = useState(null);
|
||||||
const [isBrushDown, setIsBrushDown] = useState(false);
|
const [isBrushDown, setIsBrushDown] = useState(false);
|
||||||
const [editingShapes, setEditingShapes] = useState([]);
|
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 =
|
const shouldHover =
|
||||||
active &&
|
active &&
|
||||||
editable &&
|
editable &&
|
||||||
(toolSettings.type === "toggle" || toolSettings.type === "remove");
|
(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(() => {
|
useEffect(() => {
|
||||||
if (!active || !editable) {
|
if (!active || !editable) {
|
||||||
@ -62,14 +97,25 @@ function MapFog({
|
|||||||
|
|
||||||
const mapStage = mapStageRef.current;
|
const mapStage = mapStageRef.current;
|
||||||
|
|
||||||
function getBrushPosition() {
|
function getBrushPosition(snapping = true) {
|
||||||
const mapImage = mapStage.findOne("#mapImage");
|
const mapImage = mapStage.findOne("#mapImage");
|
||||||
let position = getRelativePointerPosition(mapImage);
|
let position = getRelativePointerPosition(mapImage);
|
||||||
if (
|
if (snapping) {
|
||||||
map.snapToGrid &&
|
if (shouldRenderVertexSnapping) {
|
||||||
(toolSettings.type === "polygon" || toolSettings.type === "rectangle")
|
position = Vector2.multiply(vertexSnapping, {
|
||||||
) {
|
x: mapWidth,
|
||||||
position = snapPositionToGrid(position);
|
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, {
|
return Vector2.divide(position, {
|
||||||
x: mapImage.width(),
|
x: mapImage.width(),
|
||||||
@ -78,8 +124,8 @@ function MapFog({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleBrushDown() {
|
function handleBrushDown() {
|
||||||
const brushPosition = getBrushPosition();
|
|
||||||
if (toolSettings.type === "brush") {
|
if (toolSettings.type === "brush") {
|
||||||
|
const brushPosition = getBrushPosition();
|
||||||
setDrawingShape({
|
setDrawingShape({
|
||||||
type: "fog",
|
type: "fog",
|
||||||
data: {
|
data: {
|
||||||
@ -93,6 +139,7 @@ function MapFog({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (toolSettings.type === "rectangle") {
|
if (toolSettings.type === "rectangle") {
|
||||||
|
const brushPosition = getBrushPosition();
|
||||||
setDrawingShape({
|
setDrawingShape({
|
||||||
type: "fog",
|
type: "fog",
|
||||||
data: {
|
data: {
|
||||||
@ -225,20 +272,77 @@ function MapFog({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handlePolygonMove() {
|
function handlePolygonMove() {
|
||||||
if (toolSettings.type === "polygon" && drawingShape) {
|
if (
|
||||||
const brushPosition = getBrushPosition();
|
active &&
|
||||||
setDrawingShape((prevShape) => {
|
(toolSettings.type === "polygon" ||
|
||||||
if (!prevShape) {
|
toolSettings.type === "rectangle") &&
|
||||||
return;
|
!shouldRenderVertexSnapping
|
||||||
}
|
) {
|
||||||
return {
|
let guides = [];
|
||||||
...prevShape,
|
const brushPosition = getBrushPosition(false);
|
||||||
data: {
|
const absoluteBrushPosition = Vector2.multiply(brushPosition, {
|
||||||
...prevShape.data,
|
x: mapWidth,
|
||||||
points: [...prevShape.data.points.slice(0, -1), brushPosition],
|
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
|
closed
|
||||||
lineCap="round"
|
lineCap="round"
|
||||||
lineJoin="round"
|
lineJoin="round"
|
||||||
strokeWidth={gridStrokeWidth * shape.strokeWidth}
|
strokeWidth={editable ? gridStrokeWidth * shape.strokeWidth : 0}
|
||||||
opacity={editable ? 0.5 : 1}
|
opacity={editable ? (!shape.visible ? 0.2 : 0.5) : 1}
|
||||||
fillPatternImage={patternImage}
|
fillPatternImage={patternImage}
|
||||||
fillPriority={active && !shape.visible ? "pattern" : "color"}
|
fillPriority={active && !shape.visible ? "pattern" : "color"}
|
||||||
holes={holes}
|
holes={holes}
|
||||||
// Disable collision if the fog is transparent and we're not editing it
|
// Disable collision if the fog is transparent and we're not editing it
|
||||||
// This allows tokens to be moved under the fog
|
// This allows tokens to be moved under the fog
|
||||||
hitFunc={editable && !active ? () => {} : undefined}
|
hitFunc={editable && !active ? () => {} : undefined}
|
||||||
shadowColor={editable ? "rgba(0, 0, 0, 0)" : "rgba(34, 34, 34, 0.50)"}
|
// shadowColor={editable ? "rgba(0, 0, 0, 0)" : "rgba(34, 34, 34, 1)"}
|
||||||
shadowOffset={{ x: 0, y: 5 }}
|
// shadowOffset={{ x: 0, y: 5 }}
|
||||||
shadowBlur={10}
|
// 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(() => {
|
useEffect(() => {
|
||||||
function shapeVisible(shape) {
|
function shapeVisible(shape) {
|
||||||
return (active && !toolSettings.preview) || shape.visible;
|
return (active && !toolSettings.preview) || shape.visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (editable) {
|
if (editable) {
|
||||||
setFogShapes(shapes.filter(shapeVisible));
|
const visibleShapes = shapes.filter(shapeVisible);
|
||||||
|
setFogShapeBoundingBoxes(getFogShapesBoundingBoxes(visibleShapes));
|
||||||
|
setFogShapes(visibleShapes);
|
||||||
} else {
|
} else {
|
||||||
setFogShapes(mergeShapes(shapes));
|
setFogShapes(mergeFogShapes(shapes));
|
||||||
}
|
}
|
||||||
}, [shapes, editable, active, toolSettings]);
|
}, [shapes, editable, active, toolSettings]);
|
||||||
|
|
||||||
const fogGroupRef = useRef();
|
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 (
|
return (
|
||||||
<Group>
|
<Group>
|
||||||
@ -452,6 +559,8 @@ function MapFog({
|
|||||||
<Rect width={1} height={1} />
|
<Rect width={1} height={1} />
|
||||||
{fogShapes.map(renderShape)}
|
{fogShapes.map(renderShape)}
|
||||||
</Group>
|
</Group>
|
||||||
|
{shouldRenderGuides && renderGuides()}
|
||||||
|
{shouldRenderVertexSnapping && renderSnappingVertex()}
|
||||||
{drawingShape && renderShape(drawingShape)}
|
{drawingShape && renderShape(drawingShape)}
|
||||||
{drawingShape &&
|
{drawingShape &&
|
||||||
toolSettings &&
|
toolSettings &&
|
||||||
|
@ -27,6 +27,7 @@ function MapMeasure({ map, active }) {
|
|||||||
gridCellNormalizedSize,
|
gridCellNormalizedSize,
|
||||||
gridStrokeWidth,
|
gridStrokeWidth,
|
||||||
gridCellPixelSize,
|
gridCellPixelSize,
|
||||||
|
gridOffset,
|
||||||
} = useGrid();
|
} = useGrid();
|
||||||
const mapStageRef = useMapStage();
|
const mapStageRef = useMapStage();
|
||||||
const [drawingShapeData, setDrawingShapeData] = useState(null);
|
const [drawingShapeData, setDrawingShapeData] = useState(null);
|
||||||
@ -73,14 +74,20 @@ function MapMeasure({ map, active }) {
|
|||||||
gridCellNormalizedSize
|
gridCellNormalizedSize
|
||||||
);
|
);
|
||||||
// Convert back to pixel values
|
// Convert back to pixel values
|
||||||
const a = Vector2.multiply(points[0], {
|
const a = Vector2.subtract(
|
||||||
x: mapImage.width(),
|
Vector2.multiply(points[0], {
|
||||||
y: mapImage.height(),
|
x: mapImage.width(),
|
||||||
});
|
y: mapImage.height(),
|
||||||
const b = Vector2.multiply(points[1], {
|
}),
|
||||||
x: mapImage.width(),
|
gridOffset
|
||||||
y: mapImage.height(),
|
);
|
||||||
});
|
const b = Vector2.subtract(
|
||||||
|
Vector2.multiply(points[1], {
|
||||||
|
x: mapImage.width(),
|
||||||
|
y: mapImage.height(),
|
||||||
|
}),
|
||||||
|
gridOffset
|
||||||
|
);
|
||||||
const length = gridDistance(grid, a, b, gridCellPixelSize);
|
const length = gridDistance(grid, a, b, gridCellPixelSize);
|
||||||
setDrawingShapeData({
|
setDrawingShapeData({
|
||||||
length,
|
length,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useContext } from "react";
|
import React, { useContext, useCallback } from "react";
|
||||||
|
|
||||||
import Vector2 from "../helpers/Vector2";
|
import Vector2 from "../helpers/Vector2";
|
||||||
import Size from "../helpers/Size";
|
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 {Size} gridCellNormalizedSize Size of each cell normalized to the grid
|
||||||
* @property {Vector2} gridOffset Offset of the grid from the top left in pixels
|
* @property {Vector2} gridOffset Offset of the grid from the top left in pixels
|
||||||
* @property {number} gridStrokeWidth Stroke width of the grid 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),
|
gridCellNormalizedSize: new Size(0, 0, 0),
|
||||||
gridOffset: new Vector2(0, 0),
|
gridOffset: new Vector2(0, 0),
|
||||||
gridStrokeWidth: 0,
|
gridStrokeWidth: 0,
|
||||||
|
gridCellPixelOffset: new Vector2(0, 0),
|
||||||
};
|
};
|
||||||
|
|
||||||
const GridContext = React.createContext(defaultValue);
|
const GridContext = React.createContext(defaultValue);
|
||||||
@ -67,6 +69,12 @@ export function GridProvider({ grid, width, height, children }) {
|
|||||||
? gridCellPixelSize.width
|
? gridCellPixelSize.width
|
||||||
: gridCellPixelSize.height) * defaultStrokeWidth;
|
: 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 = {
|
const value = {
|
||||||
grid,
|
grid,
|
||||||
gridPixelSize,
|
gridPixelSize,
|
||||||
@ -74,6 +82,7 @@ export function GridProvider({ grid, width, height, children }) {
|
|||||||
gridCellNormalizedSize,
|
gridCellNormalizedSize,
|
||||||
gridOffset,
|
gridOffset,
|
||||||
gridStrokeWidth,
|
gridStrokeWidth,
|
||||||
|
gridCellPixelOffset,
|
||||||
};
|
};
|
||||||
|
|
||||||
return <GridContext.Provider value={value}>{children}</GridContext.Provider>;
|
return <GridContext.Provider value={value}>{children}</GridContext.Provider>;
|
||||||
|
@ -344,12 +344,21 @@ class Vector2 {
|
|||||||
return { distance: Math.sqrt(distance), point: point };
|
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
|
* Calculates an axis-aligned bounding box around an array of point
|
||||||
* @param {Vector2[]} points
|
* @param {Vector2[]} points
|
||||||
* @returns {Object}
|
* @returns {BoundingBox}
|
||||||
*/
|
*/
|
||||||
static getBounds(points) {
|
static getBoundingBox(points) {
|
||||||
let minX = Number.MAX_VALUE;
|
let minX = Number.MAX_VALUE;
|
||||||
let maxX = Number.MIN_VALUE;
|
let maxX = Number.MIN_VALUE;
|
||||||
let minY = Number.MAX_VALUE;
|
let minY = Number.MAX_VALUE;
|
||||||
@ -360,7 +369,16 @@ class Vector2 {
|
|||||||
minY = point.y < minY ? point.y : minY;
|
minY = point.y < minY ? point.y : minY;
|
||||||
maxY = point.y > maxY ? point.y : maxY;
|
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}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
static pointInPolygon(p, points) {
|
static pointInPolygon(p, points) {
|
||||||
const { minX, maxX, minY, maxY } = this.getBounds(points);
|
const bounds = this.getBoundingBox(points);
|
||||||
if (p.x < minX || p.x > maxX || p.y < minY || p.y > maxY) {
|
if (
|
||||||
|
p.x < bounds.min.x ||
|
||||||
|
p.x > bounds.max.x ||
|
||||||
|
p.y < bounds.min.y ||
|
||||||
|
p.y > bounds.max.y
|
||||||
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,7 +4,83 @@ import polygonClipping from "polygon-clipping";
|
|||||||
import Vector2 from "./Vector2";
|
import Vector2 from "./Vector2";
|
||||||
import { toDegrees } from "./shared";
|
import { toDegrees } from "./shared";
|
||||||
import { logError } from "./logging";
|
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) {
|
export function getDefaultShapeData(type, brushPosition) {
|
||||||
if (type === "line") {
|
if (type === "line") {
|
||||||
return {
|
return {
|
||||||
@ -33,6 +109,10 @@ export function getDefaultShapeData(type, brushPosition) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Vector2} cellSize
|
||||||
|
* @returns {Vector2}
|
||||||
|
*/
|
||||||
export function getGridScale(cellSize) {
|
export function getGridScale(cellSize) {
|
||||||
if (cellSize.x < cellSize.y) {
|
if (cellSize.x < cellSize.y) {
|
||||||
return { x: cellSize.y / cellSize.x, y: 1 };
|
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(
|
export function getUpdatedShapeData(
|
||||||
type,
|
type,
|
||||||
data,
|
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;
|
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(
|
return simplify(
|
||||||
points,
|
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) {
|
if (shapes.length === 0) {
|
||||||
return shapes;
|
return shapes;
|
||||||
}
|
}
|
||||||
@ -161,3 +251,267 @@ export function mergeShapes(shapes) {
|
|||||||
return 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;
|
snappingSensitivity = snappingSensitivity || defaultSnappingSensitivity;
|
||||||
|
|
||||||
const { grid, gridOffset, gridCellPixelSize } = useGrid();
|
const {
|
||||||
|
grid,
|
||||||
|
gridOffset,
|
||||||
|
gridCellPixelSize,
|
||||||
|
gridCellPixelOffset,
|
||||||
|
} = useGrid();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Vector2} node The node to snap
|
* @param {Vector2} node The node to snap
|
||||||
*/
|
*/
|
||||||
function snapPositionToGrid(position) {
|
function snapPositionToGrid(position) {
|
||||||
// Account for grid offset
|
// Account for grid offset
|
||||||
let offsetPosition = Vector2.subtract(position, gridOffset);
|
let offsetPosition = Vector2.subtract(
|
||||||
// Move hex tiles to top left
|
Vector2.subtract(position, gridOffset),
|
||||||
if (grid.type === "hexVertical" || grid.type === "hexHorizontal") {
|
gridCellPixelOffset
|
||||||
offsetPosition = Vector2.subtract(
|
);
|
||||||
offsetPosition,
|
|
||||||
Vector2.multiply(gridCellPixelSize, 0.5)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const nearsetCell = getNearestCellCoordinates(
|
const nearsetCell = getNearestCellCoordinates(
|
||||||
grid,
|
grid,
|
||||||
offsetPosition.x,
|
offsetPosition.x,
|
||||||
@ -62,14 +63,10 @@ function useGridSnapping(snappingSensitivity) {
|
|||||||
Vector2.min(gridCellPixelSize) * snappingSensitivity
|
Vector2.min(gridCellPixelSize) * snappingSensitivity
|
||||||
) {
|
) {
|
||||||
// Reverse grid offset
|
// Reverse grid offset
|
||||||
let offsetSnapPoint = Vector2.add(snapPoint, gridOffset);
|
let offsetSnapPoint = Vector2.add(
|
||||||
// Reverse offset for hex tiles
|
Vector2.add(snapPoint, gridOffset),
|
||||||
if (grid.type === "hexVertical" || grid.type === "hexHorizontal") {
|
gridCellPixelOffset
|
||||||
offsetSnapPoint = Vector2.add(
|
);
|
||||||
offsetSnapPoint,
|
|
||||||
Vector2.multiply(gridCellPixelSize, 0.5)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return offsetSnapPoint;
|
return offsetSnapPoint;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user