Add hex support to useGridSnapping
This commit is contained in:
parent
fbea8bd7a7
commit
502f872fbf
@ -1,12 +1,7 @@
|
||||
import React from "react";
|
||||
import { Line, Group, RegularPolygon } from "react-konva";
|
||||
|
||||
import {
|
||||
getCellLocation,
|
||||
gridClipFunction,
|
||||
shouldClipCell,
|
||||
getNearestCellCoordinates,
|
||||
} from "../helpers/grid";
|
||||
import { getCellLocation } from "../helpers/grid";
|
||||
import Vector2 from "../helpers/Vector2";
|
||||
|
||||
import { useGrid } from "../contexts/GridContext";
|
||||
@ -25,6 +20,7 @@ function Grid({ strokeWidth, stroke }) {
|
||||
}
|
||||
|
||||
const negativeGridOffset = Vector2.multiply(gridOffset, -1);
|
||||
const finalStrokeWidth = gridStrokeWidth * strokeWidth;
|
||||
|
||||
const shapes = [];
|
||||
if (grid.type === "square") {
|
||||
@ -39,7 +35,7 @@ function Grid({ strokeWidth, stroke }) {
|
||||
gridPixelSize.height,
|
||||
]}
|
||||
stroke={stroke}
|
||||
strokeWidth={gridStrokeWidth * strokeWidth}
|
||||
strokeWidth={finalStrokeWidth}
|
||||
opacity={0.5}
|
||||
offset={negativeGridOffset}
|
||||
/>
|
||||
@ -56,54 +52,56 @@ function Grid({ strokeWidth, stroke }) {
|
||||
y * gridCellPixelSize.height,
|
||||
]}
|
||||
stroke={stroke}
|
||||
strokeWidth={gridStrokeWidth * strokeWidth}
|
||||
strokeWidth={finalStrokeWidth}
|
||||
opacity={0.5}
|
||||
offset={negativeGridOffset}
|
||||
/>
|
||||
);
|
||||
}
|
||||
} else if (grid.type === "hexVertical" || grid.type === "hexHorizontal") {
|
||||
// Start at -1 to overshoot the bounds of the grid to ensure all lines are drawn
|
||||
for (let x = -1; x < grid.size.x; x++) {
|
||||
for (let y = -1; y < grid.size.y; y++) {
|
||||
// End at grid size + 1 to overshoot the bounds of the grid to ensure all lines are drawn
|
||||
for (let x = 0; x < grid.size.x + 1; x++) {
|
||||
for (let y = 0; y < grid.size.y + 1; y++) {
|
||||
const cellLocation = getCellLocation(grid, x, y, gridCellPixelSize);
|
||||
shapes.push(
|
||||
<Group
|
||||
key={`grid_${x}_${y}`}
|
||||
clipFunc={
|
||||
shouldClipCell(grid, x, y) &&
|
||||
((context) =>
|
||||
gridClipFunction(context, grid, x, y, gridCellPixelSize))
|
||||
}
|
||||
x={cellLocation.x}
|
||||
y={cellLocation.y}
|
||||
offset={negativeGridOffset}
|
||||
>
|
||||
<RegularPolygon
|
||||
sides={6}
|
||||
radius={gridCellPixelSize.radius}
|
||||
stroke={stroke}
|
||||
strokeWidth={gridStrokeWidth * strokeWidth}
|
||||
opacity={0.5}
|
||||
rotation={grid.type === "hexVertical" ? 0 : 90}
|
||||
onMouseDown={() => {
|
||||
console.log(
|
||||
getNearestCellCoordinates(
|
||||
grid,
|
||||
cellLocation.x,
|
||||
cellLocation.y,
|
||||
gridCellPixelSize
|
||||
)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{/* Offset the hex tile to align to top left of grid */}
|
||||
<Group offset={Vector2.multiply(gridCellPixelSize, -0.5)}>
|
||||
<RegularPolygon
|
||||
sides={6}
|
||||
radius={gridCellPixelSize.radius}
|
||||
stroke={stroke}
|
||||
strokeWidth={finalStrokeWidth}
|
||||
opacity={0.5}
|
||||
rotation={grid.type === "hexVertical" ? 0 : 90}
|
||||
/>
|
||||
</Group>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return <Group>{shapes}</Group>;
|
||||
return (
|
||||
<Group
|
||||
// Clip grid to bounds to cover hex overshoot
|
||||
clipFunc={(context) => {
|
||||
context.rect(
|
||||
gridOffset.x - finalStrokeWidth / 2,
|
||||
gridOffset.y - finalStrokeWidth / 2,
|
||||
gridPixelSize.width + finalStrokeWidth,
|
||||
gridPixelSize.height + finalStrokeWidth
|
||||
);
|
||||
}}
|
||||
>
|
||||
{shapes}
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
Grid.defaultProps = {
|
||||
|
@ -178,9 +178,12 @@ function MapToken({
|
||||
}
|
||||
}
|
||||
|
||||
const tokenWidth = gridCellPixelSize.width * tokenState.size;
|
||||
const tokenHeight =
|
||||
(gridCellPixelSize.width / tokenAspectRatio) * tokenState.size;
|
||||
const minCellSize = Math.min(
|
||||
gridCellPixelSize.width,
|
||||
gridCellPixelSize.height
|
||||
);
|
||||
const tokenWidth = minCellSize * tokenState.size;
|
||||
const tokenHeight = (minCellSize / tokenAspectRatio) * tokenState.size;
|
||||
|
||||
const debouncedStageScale = useDebounce(stageScale, 50);
|
||||
const imageRef = useRef();
|
||||
|
@ -27,7 +27,11 @@ function Note({
|
||||
const { mapWidth, mapHeight, setPreventMapInteraction } = useMapInteraction();
|
||||
const { gridCellPixelSize } = useGrid();
|
||||
|
||||
const noteWidth = gridCellPixelSize.width * note.size;
|
||||
const minCellSize = Math.min(
|
||||
gridCellPixelSize.width,
|
||||
gridCellPixelSize.height
|
||||
);
|
||||
const noteWidth = minCellSize * note.size;
|
||||
const noteHeight = noteWidth;
|
||||
const notePadding = noteWidth / 10;
|
||||
|
||||
|
@ -2,6 +2,7 @@ import {
|
||||
toRadians,
|
||||
roundTo as roundToNumber,
|
||||
lerp as lerpNumber,
|
||||
floorTo as floorToNumber,
|
||||
} from "./shared";
|
||||
|
||||
/**
|
||||
@ -186,6 +187,19 @@ class Vector2 {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Floors `p` to the nearest value of `to`
|
||||
* @param {Vector2} p
|
||||
* @param {Vector2} to
|
||||
* @returns {Vector2}
|
||||
*/
|
||||
static floorTo(p, to) {
|
||||
return {
|
||||
x: floorToNumber(p.x, to.x),
|
||||
y: floorToNumber(p.y, to.y),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Vector2} a
|
||||
* @returns {Vector2} The component wise sign of `a`
|
||||
@ -473,6 +487,20 @@ class Vector2 {
|
||||
|
||||
return resampledPoints;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotate a vector 90 degrees
|
||||
* @param {Vector2} p Point
|
||||
* @param {("counterClockwise"|"clockwise")=} direction Direction to rotate the vector
|
||||
* @returns {Vector2}
|
||||
*/
|
||||
static rotate90(p, direction = "clockwise") {
|
||||
if (direction === "clockwise") {
|
||||
return { x: p.y, y: -p.x };
|
||||
} else {
|
||||
return { x: -p.y, y: p.x };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Vector2;
|
||||
|
@ -57,7 +57,7 @@ export function getCellPixelSize(grid, gridWidth, gridHeight) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the location of a cell in the grid.
|
||||
* Find the center location of a cell in the grid.
|
||||
* Hex is addressed in an offset coordinate system with even numbered columns/rows offset to the right
|
||||
* @param {Grid} grid
|
||||
* @param {number} col X-axis coordinate of the cell
|
||||
@ -68,16 +68,19 @@ export function getCellPixelSize(grid, gridWidth, gridHeight) {
|
||||
export function getCellLocation(grid, col, row, cellSize) {
|
||||
switch (grid.type) {
|
||||
case "square":
|
||||
return { x: col * cellSize.width, y: row * cellSize.height };
|
||||
return {
|
||||
x: col * cellSize.width + cellSize.width / 2,
|
||||
y: row * cellSize.height + cellSize.height / 2,
|
||||
};
|
||||
case "hexVertical":
|
||||
return {
|
||||
x: col * cellSize.width + (cellSize.width * (1 + (row & 1))) / 2,
|
||||
y: row * cellSize.height * (3 / 4) + cellSize.radius,
|
||||
x: cellSize.radius * SQRT3 * (col - 0.5 * (row & 1)),
|
||||
y: ((cellSize.radius * 3) / 2) * row,
|
||||
};
|
||||
case "hexHorizontal":
|
||||
return {
|
||||
x: col * cellSize.width * (3 / 4) + cellSize.radius,
|
||||
y: row * cellSize.height + (cellSize.height * (1 + (col & 1))) / 2,
|
||||
x: ((cellSize.radius * 3) / 2) * col,
|
||||
y: cellSize.radius * SQRT3 * (row - 0.5 * (col & 1)),
|
||||
};
|
||||
default:
|
||||
throw GRID_TYPE_NOT_IMPLEMENTED;
|
||||
@ -95,7 +98,7 @@ export function getCellLocation(grid, col, row, cellSize) {
|
||||
export function getNearestCellCoordinates(grid, x, y, cellSize) {
|
||||
switch (grid.type) {
|
||||
case "square":
|
||||
return Vector2.roundTo({ x, y }, cellSize);
|
||||
return Vector2.divide(Vector2.floorTo({ x, y }, cellSize), cellSize);
|
||||
case "hexVertical":
|
||||
// Find nearest cell in cube coordinates the convert to offset coordinates
|
||||
const cubeXVert = ((SQRT3 / 3) * x - (1 / 3) * y) / cellSize.radius;
|
||||
@ -115,62 +118,49 @@ export function getNearestCellCoordinates(grid, x, y, cellSize) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the cell located at `x, y` is out of bounds of the grid
|
||||
* Find the corners of a grid cell
|
||||
* @param {Grid} grid
|
||||
* @param {number} col X-axis coordinate of the cell
|
||||
* @param {number} row Y-axis coordinate of the cell
|
||||
* @returns {boolean}
|
||||
* @param {number} x X location of the cell in pixels
|
||||
* @param {number} y Y location of the cell in pixels
|
||||
* @param {Size} cellSize Cell size in pixels
|
||||
* @returns {Vector2[]}
|
||||
*/
|
||||
export function shouldClipCell(grid, col, row) {
|
||||
if (col < 0 || row < 0) {
|
||||
return true;
|
||||
}
|
||||
export function getCellCorners(grid, x, y, cellSize) {
|
||||
const position = new Vector2(x, y);
|
||||
switch (grid.type) {
|
||||
case "square":
|
||||
return false;
|
||||
const halfSize = Vector2.multiply(cellSize, 0.5);
|
||||
return [
|
||||
Vector2.add(position, Vector2.multiply(halfSize, { x: -1, y: -1 })),
|
||||
Vector2.add(position, Vector2.multiply(halfSize, { x: 1, y: -1 })),
|
||||
Vector2.add(position, Vector2.multiply(halfSize, { x: 1, y: 1 })),
|
||||
Vector2.add(position, Vector2.multiply(halfSize, { x: -1, y: 1 })),
|
||||
];
|
||||
case "hexVertical":
|
||||
return col === grid.size.x - 1 && (row & 1) !== 0;
|
||||
const up = Vector2.subtract(position, { x: 0, y: cellSize.radius });
|
||||
return [
|
||||
up,
|
||||
Vector2.rotate(up, position, 60),
|
||||
Vector2.rotate(up, position, 120),
|
||||
Vector2.rotate(up, position, 180),
|
||||
Vector2.rotate(up, position, 240),
|
||||
Vector2.rotate(up, position, 300),
|
||||
];
|
||||
case "hexHorizontal":
|
||||
return row === grid.size.y - 1 && (col & 1) !== 0;
|
||||
const right = Vector2.add(position, { x: cellSize.radius, y: 0 });
|
||||
return [
|
||||
right,
|
||||
Vector2.rotate(right, position, 60),
|
||||
Vector2.rotate(right, position, 120),
|
||||
Vector2.rotate(right, position, 180),
|
||||
Vector2.rotate(right, position, 240),
|
||||
Vector2.rotate(right, position, 300),
|
||||
];
|
||||
default:
|
||||
throw GRID_TYPE_NOT_IMPLEMENTED;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Canvas clip function for culling hex cells that overshoot/undershoot the grid
|
||||
* @param {CanvasRenderingContext2D} context The canvas context of the clip function
|
||||
* @param {Grid} grid
|
||||
* @param {number} col X-axis coordinate of the cell
|
||||
* @param {number} row Y-axis coordinate of the cell
|
||||
* @param {Size} cellSize Cell size in pixels
|
||||
*/
|
||||
export function gridClipFunction(context, grid, col, row, cellSize) {
|
||||
// Clip the undershooting cells unless they are needed to fill out a specific grid type
|
||||
if (
|
||||
(col < 0 && grid.type !== "hexVertical") ||
|
||||
(col < 0 && (row & 1) === 0)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
(row < 0 && grid.type !== "hexHorizontal") ||
|
||||
(row < 0 && (col & 1) === 0)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
context.rect(
|
||||
col < 0 ? 0 : -cellSize.radius,
|
||||
row < 0 ? 0 : -cellSize.radius,
|
||||
col > 0 && grid.type === "hexVertical"
|
||||
? cellSize.radius
|
||||
: cellSize.radius * 2,
|
||||
row > 0 && grid.type === "hexVertical"
|
||||
? cellSize.radius * 2
|
||||
: cellSize.radius
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the height of a grid based off of its width
|
||||
* @param {Grid} grid
|
||||
@ -243,7 +233,7 @@ export function getGridMaxZoom(grid) {
|
||||
* @param {("hexVertical"|"hexHorizontal")} type
|
||||
* @returns {Vector2}
|
||||
*/
|
||||
function hexCubeToOffset(cube, type) {
|
||||
export function hexCubeToOffset(cube, type) {
|
||||
if (type === "hexVertical") {
|
||||
const x = cube.x + (cube.z + (cube.z & 1)) / 2;
|
||||
const y = cube.z;
|
||||
@ -260,7 +250,7 @@ function hexCubeToOffset(cube, type) {
|
||||
* @param {("hexVertical"|"hexHorizontal")} type
|
||||
* @returns {Vector3}
|
||||
*/
|
||||
function hexOffsetToCube(offset, type) {
|
||||
export function hexOffsetToCube(offset, type) {
|
||||
if (type === "hexVertical") {
|
||||
const x = offset.x - (offset.y - (offset.y & 1)) / 2;
|
||||
const z = offset.y;
|
||||
|
@ -223,7 +223,7 @@ export function Trail({ position, size, duration, segments, color }) {
|
||||
const drawOffsetLine = (from, to, alpha) => {
|
||||
const forward = Vector2.normalize(Vector2.subtract(from, to));
|
||||
// Rotate the forward vector 90 degrees based off of the direction
|
||||
const side = { x: forward.y, y: -forward.x };
|
||||
const side = Vector2.rotate90(forward);
|
||||
|
||||
// Offset the `to` position by the size of the point and in the side direction
|
||||
const toSize = (alpha * size) / 2;
|
||||
|
@ -28,6 +28,10 @@ export function roundTo(x, to) {
|
||||
return Math.round(x / to) * to;
|
||||
}
|
||||
|
||||
export function floorTo(x, to) {
|
||||
return Math.floor(x / to) * to;
|
||||
}
|
||||
|
||||
export function toRadians(angle) {
|
||||
return angle * (Math.PI / 180);
|
||||
}
|
||||
|
@ -1,7 +1,13 @@
|
||||
// Load Konva for auto complete
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import Konva from "konva";
|
||||
|
||||
import Vector2 from "../helpers/Vector2";
|
||||
import { getCellLocation } from "../helpers/grid";
|
||||
import {
|
||||
getCellLocation,
|
||||
getNearestCellCoordinates,
|
||||
getCellCorners,
|
||||
} from "../helpers/grid";
|
||||
|
||||
import { useGrid } from "../contexts/GridContext";
|
||||
|
||||
@ -10,79 +16,61 @@ import { useGrid } from "../contexts/GridContext";
|
||||
* @param {number} snappingThreshold 1 = Always snap, 0 = never snap
|
||||
*/
|
||||
function useGridSnapping(snappingThreshold) {
|
||||
const { gridOffset, gridCellPixelSize } = useGrid();
|
||||
const { grid, gridOffset, gridCellPixelSize } = useGrid();
|
||||
|
||||
/**
|
||||
* @param {Konva.Node} node The node to snap
|
||||
*/
|
||||
function snapNodeToGrid(node) {
|
||||
const position = node.position();
|
||||
const halfSize = Vector2.divide({ x: node.width(), y: node.height() }, 2);
|
||||
|
||||
// Offsets to tranform the centered position into the four corners
|
||||
const cornerOffsets = [
|
||||
{ x: 0, y: 0 },
|
||||
// halfSize,
|
||||
// { x: -halfSize.x, y: -halfSize.y },
|
||||
// { x: halfSize.x, y: -halfSize.y },
|
||||
// { x: -halfSize.x, y: halfSize.y },
|
||||
];
|
||||
|
||||
// Minimum distance from a corner to the grid
|
||||
let minCornerGridDistance = Number.MAX_VALUE;
|
||||
// Minimum component of the difference between the min corner and the grid
|
||||
let minCornerMinComponent;
|
||||
// Closest grid value
|
||||
let minGridSnap;
|
||||
|
||||
// Find the closest corner to the grid
|
||||
for (let cornerOffset of cornerOffsets) {
|
||||
const corner = Vector2.add(position, cornerOffset);
|
||||
// Transform into gridOffset space, round, then transform back
|
||||
const gridSnap = Vector2.add(
|
||||
Vector2.roundTo(
|
||||
Vector2.subtract(corner, gridOffset),
|
||||
gridCellPixelSize
|
||||
),
|
||||
gridOffset
|
||||
// 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)
|
||||
);
|
||||
const gridDistance = Vector2.length(Vector2.subtract(gridSnap, corner));
|
||||
const minComponent = Vector2.min(gridCellPixelSize);
|
||||
if (gridDistance < minCornerGridDistance) {
|
||||
minCornerGridDistance = gridDistance;
|
||||
minCornerMinComponent = minComponent;
|
||||
// Move the grid value back to the center
|
||||
minGridSnap = Vector2.subtract(gridSnap, cornerOffset);
|
||||
}
|
||||
}
|
||||
|
||||
// Snap to center of grid
|
||||
// Subtract gridOffset and half grid size to transform it into gridOffset half space then transform it back
|
||||
const halfGridSize = Vector2.multiply(gridCellPixelSize, 0.5);
|
||||
const centerSnap = Vector2.add(
|
||||
Vector2.add(
|
||||
Vector2.roundTo(
|
||||
Vector2.subtract(
|
||||
Vector2.subtract(position, gridOffset),
|
||||
halfGridSize
|
||||
),
|
||||
gridCellPixelSize
|
||||
),
|
||||
halfGridSize
|
||||
),
|
||||
gridOffset
|
||||
const nearsetCell = getNearestCellCoordinates(
|
||||
grid,
|
||||
offsetPosition.x,
|
||||
offsetPosition.y,
|
||||
gridCellPixelSize
|
||||
);
|
||||
const centerDistance = Vector2.length(
|
||||
Vector2.subtract(centerSnap, position)
|
||||
const cellPosition = getCellLocation(
|
||||
grid,
|
||||
nearsetCell.x,
|
||||
nearsetCell.y,
|
||||
gridCellPixelSize
|
||||
);
|
||||
const cellCorners = getCellCorners(
|
||||
grid,
|
||||
cellPosition.x,
|
||||
cellPosition.y,
|
||||
gridCellPixelSize
|
||||
);
|
||||
|
||||
if (minCornerGridDistance < minCornerMinComponent * snappingThreshold) {
|
||||
node.position(minGridSnap);
|
||||
} else if (
|
||||
centerDistance <
|
||||
Vector2.min(gridCellPixelSize) * snappingThreshold
|
||||
) {
|
||||
node.position(centerSnap);
|
||||
const snapPoints = [cellPosition, ...cellCorners];
|
||||
|
||||
for (let snapPoint of snapPoints) {
|
||||
const distanceToSnapPoint = Vector2.distance(offsetPosition, snapPoint);
|
||||
if (
|
||||
distanceToSnapPoint <
|
||||
Vector2.min(gridCellPixelSize) * snappingThreshold
|
||||
) {
|
||||
// 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)
|
||||
);
|
||||
}
|
||||
node.position(offsetSnapPoint);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user