Move grid snapping to hook and added math for hex snapping
This commit is contained in:
parent
f20173de35
commit
fbea8bd7a7
@ -5,7 +5,9 @@ import {
|
|||||||
getCellLocation,
|
getCellLocation,
|
||||||
gridClipFunction,
|
gridClipFunction,
|
||||||
shouldClipCell,
|
shouldClipCell,
|
||||||
|
getNearestCellCoordinates,
|
||||||
} from "../helpers/grid";
|
} from "../helpers/grid";
|
||||||
|
import Vector2 from "../helpers/Vector2";
|
||||||
|
|
||||||
import { useGrid } from "../contexts/GridContext";
|
import { useGrid } from "../contexts/GridContext";
|
||||||
|
|
||||||
@ -22,6 +24,8 @@ function Grid({ strokeWidth, stroke }) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const negativeGridOffset = Vector2.multiply(gridOffset, -1);
|
||||||
|
|
||||||
const shapes = [];
|
const shapes = [];
|
||||||
if (grid.type === "square") {
|
if (grid.type === "square") {
|
||||||
for (let x = 1; x < grid.size.x; x++) {
|
for (let x = 1; x < grid.size.x; x++) {
|
||||||
@ -37,7 +41,7 @@ function Grid({ strokeWidth, stroke }) {
|
|||||||
stroke={stroke}
|
stroke={stroke}
|
||||||
strokeWidth={gridStrokeWidth * strokeWidth}
|
strokeWidth={gridStrokeWidth * strokeWidth}
|
||||||
opacity={0.5}
|
opacity={0.5}
|
||||||
offset={gridOffset}
|
offset={negativeGridOffset}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -54,7 +58,7 @@ function Grid({ strokeWidth, stroke }) {
|
|||||||
stroke={stroke}
|
stroke={stroke}
|
||||||
strokeWidth={gridStrokeWidth * strokeWidth}
|
strokeWidth={gridStrokeWidth * strokeWidth}
|
||||||
opacity={0.5}
|
opacity={0.5}
|
||||||
offset={gridOffset}
|
offset={negativeGridOffset}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -73,7 +77,7 @@ function Grid({ strokeWidth, stroke }) {
|
|||||||
}
|
}
|
||||||
x={cellLocation.x}
|
x={cellLocation.x}
|
||||||
y={cellLocation.y}
|
y={cellLocation.y}
|
||||||
offset={gridOffset}
|
offset={negativeGridOffset}
|
||||||
>
|
>
|
||||||
<RegularPolygon
|
<RegularPolygon
|
||||||
sides={6}
|
sides={6}
|
||||||
@ -82,6 +86,16 @@ function Grid({ strokeWidth, stroke }) {
|
|||||||
strokeWidth={gridStrokeWidth * strokeWidth}
|
strokeWidth={gridStrokeWidth * strokeWidth}
|
||||||
opacity={0.5}
|
opacity={0.5}
|
||||||
rotation={grid.type === "hexVertical" ? 0 : 90}
|
rotation={grid.type === "hexVertical" ? 0 : 90}
|
||||||
|
onMouseDown={() => {
|
||||||
|
console.log(
|
||||||
|
getNearestCellCoordinates(
|
||||||
|
grid,
|
||||||
|
cellLocation.x,
|
||||||
|
cellLocation.y,
|
||||||
|
gridCellPixelSize
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
);
|
);
|
||||||
|
@ -7,8 +7,7 @@ import Konva from "konva";
|
|||||||
import useDataSource from "../../hooks/useDataSource";
|
import useDataSource from "../../hooks/useDataSource";
|
||||||
import useDebounce from "../../hooks/useDebounce";
|
import useDebounce from "../../hooks/useDebounce";
|
||||||
import usePrevious from "../../hooks/usePrevious";
|
import usePrevious from "../../hooks/usePrevious";
|
||||||
|
import useGridSnapping from "../../hooks/useGridSnapping";
|
||||||
import { snapNodeToGrid } from "../../helpers/grid";
|
|
||||||
|
|
||||||
import { useAuth } from "../../contexts/AuthContext";
|
import { useAuth } from "../../contexts/AuthContext";
|
||||||
import { useMapInteraction } from "../../contexts/MapInteractionContext";
|
import { useMapInteraction } from "../../contexts/MapInteractionContext";
|
||||||
@ -52,6 +51,8 @@ function MapToken({
|
|||||||
}
|
}
|
||||||
}, [tokenSourceImage]);
|
}, [tokenSourceImage]);
|
||||||
|
|
||||||
|
const snapNodeToGrid = useGridSnapping(snappingThreshold);
|
||||||
|
|
||||||
function handleDragStart(event) {
|
function handleDragStart(event) {
|
||||||
const tokenGroup = event.target;
|
const tokenGroup = event.target;
|
||||||
const tokenImage = imageRef.current;
|
const tokenImage = imageRef.current;
|
||||||
@ -88,13 +89,7 @@ function MapToken({
|
|||||||
const tokenGroup = event.target;
|
const tokenGroup = event.target;
|
||||||
// Snap to corners of grid
|
// Snap to corners of grid
|
||||||
if (map.snapToGrid) {
|
if (map.snapToGrid) {
|
||||||
snapNodeToGrid(
|
snapNodeToGrid(tokenGroup);
|
||||||
map.grid,
|
|
||||||
mapWidth,
|
|
||||||
mapHeight,
|
|
||||||
tokenGroup,
|
|
||||||
snappingThreshold
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,10 +6,10 @@ import { useAuth } from "../../contexts/AuthContext";
|
|||||||
import { useMapInteraction } from "../../contexts/MapInteractionContext";
|
import { useMapInteraction } from "../../contexts/MapInteractionContext";
|
||||||
import { useGrid } from "../../contexts/GridContext";
|
import { useGrid } from "../../contexts/GridContext";
|
||||||
|
|
||||||
import { snapNodeToGrid } from "../../helpers/grid";
|
|
||||||
import colors from "../../helpers/colors";
|
import colors from "../../helpers/colors";
|
||||||
|
|
||||||
import usePrevious from "../../hooks/usePrevious";
|
import usePrevious from "../../hooks/usePrevious";
|
||||||
|
import useGridSnapping from "../../hooks/useGridSnapping";
|
||||||
|
|
||||||
const snappingThreshold = 1 / 5;
|
const snappingThreshold = 1 / 5;
|
||||||
|
|
||||||
@ -31,6 +31,8 @@ function Note({
|
|||||||
const noteHeight = noteWidth;
|
const noteHeight = noteWidth;
|
||||||
const notePadding = noteWidth / 10;
|
const notePadding = noteWidth / 10;
|
||||||
|
|
||||||
|
const snapNodeToGrid = useGridSnapping(snappingThreshold);
|
||||||
|
|
||||||
function handleDragStart(event) {
|
function handleDragStart(event) {
|
||||||
onNoteDragStart && onNoteDragStart(event, note.id);
|
onNoteDragStart && onNoteDragStart(event, note.id);
|
||||||
}
|
}
|
||||||
@ -39,13 +41,7 @@ function Note({
|
|||||||
const noteGroup = event.target;
|
const noteGroup = event.target;
|
||||||
// Snap to corners of grid
|
// Snap to corners of grid
|
||||||
if (map.snapToGrid) {
|
if (map.snapToGrid) {
|
||||||
snapNodeToGrid(
|
snapNodeToGrid(noteGroup);
|
||||||
map.grid,
|
|
||||||
mapWidth,
|
|
||||||
mapHeight,
|
|
||||||
noteGroup,
|
|
||||||
snappingThreshold
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,14 +19,14 @@ import { getGridPixelSize, getCellPixelSize, Grid } from "../helpers/grid";
|
|||||||
*/
|
*/
|
||||||
const defaultValue = {
|
const defaultValue = {
|
||||||
grid: {
|
grid: {
|
||||||
size: { x: 0, y: 0 },
|
size: new Vector2(0, 0),
|
||||||
inset: { topLeft: { x: 0, y: 0 }, bottomRight: { x: 1, y: 1 } },
|
inset: { topLeft: new Vector2(0, 0), bottomRight: new Vector2(1, 1) },
|
||||||
type: "square",
|
type: "square",
|
||||||
},
|
},
|
||||||
gridPixelSize: new Size(0, 0),
|
gridPixelSize: new Size(0, 0),
|
||||||
gridCellPixelSize: new Size(0, 0, 0),
|
gridCellPixelSize: new Size(0, 0, 0),
|
||||||
gridCellNormalizedSize: new Size(0, 0, 0),
|
gridCellNormalizedSize: new Size(0, 0, 0),
|
||||||
gridOffset: { x: 0, y: 0 },
|
gridOffset: new Vector2(0, 0),
|
||||||
gridStrokeWidth: 0,
|
gridStrokeWidth: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -53,10 +53,10 @@ export function GridProvider({ grid, width, height, children }) {
|
|||||||
gridCellPixelSize.width / gridPixelSize.width,
|
gridCellPixelSize.width / gridPixelSize.width,
|
||||||
gridCellPixelSize.height / gridPixelSize.height
|
gridCellPixelSize.height / gridPixelSize.height
|
||||||
);
|
);
|
||||||
const gridOffset = {
|
const gridOffset = Vector2.multiply(grid.inset.topLeft, {
|
||||||
x: grid.inset.topLeft.x * width * -1,
|
x: width,
|
||||||
y: grid.inset.topLeft.y * height * -1,
|
y: height,
|
||||||
};
|
});
|
||||||
const gridStrokeWidth =
|
const gridStrokeWidth =
|
||||||
(gridCellPixelSize.width < gridCellPixelSize.height
|
(gridCellPixelSize.width < gridCellPixelSize.height
|
||||||
? gridCellPixelSize.width
|
? gridCellPixelSize.width
|
||||||
|
55
src/helpers/Vector3.js
Normal file
55
src/helpers/Vector3.js
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
/**
|
||||||
|
* Vector class with x, y, z and static helper methods
|
||||||
|
*/
|
||||||
|
class Vector3 {
|
||||||
|
/**
|
||||||
|
* @type {number} x - X component of the vector
|
||||||
|
*/
|
||||||
|
x;
|
||||||
|
/**
|
||||||
|
* @type {number} y - Y component of the vector
|
||||||
|
*/
|
||||||
|
y;
|
||||||
|
/**
|
||||||
|
* @type {number} z - Z component of the vector
|
||||||
|
*/
|
||||||
|
z;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} x
|
||||||
|
* @param {number} y
|
||||||
|
* @param {number} z
|
||||||
|
*/
|
||||||
|
constructor(x, y, z) {
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
this.z = z;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Round a Vector3 to the nearest integer while maintaining x + y + z = 0
|
||||||
|
* @param {Vector3} cube
|
||||||
|
* @returns {Vector3}
|
||||||
|
*/
|
||||||
|
static cubeRound(cube) {
|
||||||
|
var rX = Math.round(cube.x);
|
||||||
|
var rY = Math.round(cube.y);
|
||||||
|
var rZ = Math.round(cube.z);
|
||||||
|
|
||||||
|
var xDiff = Math.abs(rX - cube.x);
|
||||||
|
var yDiff = Math.abs(rY - cube.y);
|
||||||
|
var zDiff = Math.abs(rZ - cube.z);
|
||||||
|
|
||||||
|
if (xDiff > yDiff && xDiff > zDiff) {
|
||||||
|
rX = -rY - rZ;
|
||||||
|
} else if (yDiff > zDiff) {
|
||||||
|
rY = -rX - rZ;
|
||||||
|
} else {
|
||||||
|
rZ = -rX - rY;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Vector3(rX, rY, rZ);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Vector3;
|
@ -1,4 +1,5 @@
|
|||||||
import GridSizeModel from "../ml/gridSize/GridSizeModel";
|
import GridSizeModel from "../ml/gridSize/GridSizeModel";
|
||||||
|
import Vector3 from "./Vector3";
|
||||||
import Vector2 from "./Vector2";
|
import Vector2 from "./Vector2";
|
||||||
import Size from "./Size";
|
import Size from "./Size";
|
||||||
|
|
||||||
@ -56,50 +57,81 @@ export function getCellPixelSize(grid, gridWidth, gridHeight) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find the location of cell in the grid
|
* Find the 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 {Grid} grid
|
||||||
* @param {number} x X-axis location of the cell
|
* @param {number} col X-axis coordinate of the cell
|
||||||
* @param {number} y Y-axis location of the cell
|
* @param {number} row Y-axis coordinate of the cell
|
||||||
* @param {Size} cellSize Cell size in pixels
|
* @param {Size} cellSize Cell size in pixels
|
||||||
* @returns {Vector2}
|
* @returns {Vector2}
|
||||||
*/
|
*/
|
||||||
export function getCellLocation(grid, x, y, cellSize) {
|
export function getCellLocation(grid, col, row, cellSize) {
|
||||||
switch (grid.type) {
|
switch (grid.type) {
|
||||||
case "square":
|
case "square":
|
||||||
return { x: x * cellSize.width, y: y * cellSize.height };
|
return { x: col * cellSize.width, y: row * cellSize.height };
|
||||||
case "hexVertical":
|
case "hexVertical":
|
||||||
return {
|
return {
|
||||||
x: x * cellSize.width + (cellSize.width * (1 + (y % 2))) / 2,
|
x: col * cellSize.width + (cellSize.width * (1 + (row & 1))) / 2,
|
||||||
y: y * cellSize.height * (3 / 4) + cellSize.radius,
|
y: row * cellSize.height * (3 / 4) + cellSize.radius,
|
||||||
};
|
};
|
||||||
case "hexHorizontal":
|
case "hexHorizontal":
|
||||||
return {
|
return {
|
||||||
x: x * cellSize.width * (3 / 4) + cellSize.radius,
|
x: col * cellSize.width * (3 / 4) + cellSize.radius,
|
||||||
y: y * cellSize.height + (cellSize.height * (1 + (x % 2))) / 2,
|
y: row * cellSize.height + (cellSize.height * (1 + (col & 1))) / 2,
|
||||||
};
|
};
|
||||||
default:
|
default:
|
||||||
throw GRID_TYPE_NOT_IMPLEMENTED;
|
throw GRID_TYPE_NOT_IMPLEMENTED;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the coordinates of the nearest cell in the grid to a point in pixels
|
||||||
|
* @param {Grid} grid
|
||||||
|
* @param {number} x X location to look for in pixels
|
||||||
|
* @param {number} y Y location to look for in pixels
|
||||||
|
* @param {Size} cellSize Cell size in pixels
|
||||||
|
* @returns {Vector2}
|
||||||
|
*/
|
||||||
|
export function getNearestCellCoordinates(grid, x, y, cellSize) {
|
||||||
|
switch (grid.type) {
|
||||||
|
case "square":
|
||||||
|
return Vector2.roundTo({ x, y }, cellSize);
|
||||||
|
case "hexVertical":
|
||||||
|
// Find nearest cell in cube coordinates the convert to offset coordinates
|
||||||
|
const cubeXVert = ((SQRT3 / 3) * x - (1 / 3) * y) / cellSize.radius;
|
||||||
|
const cubeZVert = ((2 / 3) * y) / cellSize.radius;
|
||||||
|
const cubeYVert = -cubeXVert - cubeZVert;
|
||||||
|
const cubeVert = new Vector3(cubeXVert, cubeYVert, cubeZVert);
|
||||||
|
return hexCubeToOffset(Vector3.cubeRound(cubeVert), "hexVertical");
|
||||||
|
case "hexHorizontal":
|
||||||
|
const cubeXHorz = ((2 / 3) * x) / cellSize.radius;
|
||||||
|
const cubeZHorz = (-(1 / 3) * x + (SQRT3 / 3) * y) / cellSize.radius;
|
||||||
|
const cubeYHorz = -cubeXHorz - cubeZHorz;
|
||||||
|
const cubeHorz = new Vector3(cubeXHorz, cubeYHorz, cubeZHorz);
|
||||||
|
return hexCubeToOffset(Vector3.cubeRound(cubeHorz), "hexHorizontal");
|
||||||
|
default:
|
||||||
|
throw GRID_TYPE_NOT_IMPLEMENTED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether the cell located at `x, y` is out of bounds of the grid
|
* Whether the cell located at `x, y` is out of bounds of the grid
|
||||||
* @param {Grid} grid
|
* @param {Grid} grid
|
||||||
* @param {number} x X-axis location of the cell
|
* @param {number} col X-axis coordinate of the cell
|
||||||
* @param {number} y Y-axis location of the cell
|
* @param {number} row Y-axis coordinate of the cell
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
export function shouldClipCell(grid, x, y) {
|
export function shouldClipCell(grid, col, row) {
|
||||||
if (x < 0 || y < 0) {
|
if (col < 0 || row < 0) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
switch (grid.type) {
|
switch (grid.type) {
|
||||||
case "square":
|
case "square":
|
||||||
return false;
|
return false;
|
||||||
case "hexVertical":
|
case "hexVertical":
|
||||||
return x === grid.size.x - 1 && y % 2 !== 0;
|
return col === grid.size.x - 1 && (row & 1) !== 0;
|
||||||
case "hexHorizontal":
|
case "hexHorizontal":
|
||||||
return y === grid.size.y - 1 && x % 2 !== 0;
|
return row === grid.size.y - 1 && (col & 1) !== 0;
|
||||||
default:
|
default:
|
||||||
throw GRID_TYPE_NOT_IMPLEMENTED;
|
throw GRID_TYPE_NOT_IMPLEMENTED;
|
||||||
}
|
}
|
||||||
@ -109,25 +141,33 @@ export function shouldClipCell(grid, x, y) {
|
|||||||
* Canvas clip function for culling hex cells that overshoot/undershoot the grid
|
* Canvas clip function for culling hex cells that overshoot/undershoot the grid
|
||||||
* @param {CanvasRenderingContext2D} context The canvas context of the clip function
|
* @param {CanvasRenderingContext2D} context The canvas context of the clip function
|
||||||
* @param {Grid} grid
|
* @param {Grid} grid
|
||||||
* @param {number} x X-axis location of the cell
|
* @param {number} col X-axis coordinate of the cell
|
||||||
* @param {number} y Y-axis location of the cell
|
* @param {number} row Y-axis coordinate of the cell
|
||||||
* @param {Size} cellSize Cell size in pixels
|
* @param {Size} cellSize Cell size in pixels
|
||||||
*/
|
*/
|
||||||
export function gridClipFunction(context, grid, x, y, cellSize) {
|
export function gridClipFunction(context, grid, col, row, cellSize) {
|
||||||
// Clip the undershooting cells unless they are needed to fill out a specific grid type
|
// Clip the undershooting cells unless they are needed to fill out a specific grid type
|
||||||
if ((x < 0 && grid.type !== "hexVertical") || (x < 0 && y % 2 === 0)) {
|
if (
|
||||||
|
(col < 0 && grid.type !== "hexVertical") ||
|
||||||
|
(col < 0 && (row & 1) === 0)
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if ((y < 0 && grid.type !== "hexHorizontal") || (y < 0 && x % 2 === 0)) {
|
if (
|
||||||
|
(row < 0 && grid.type !== "hexHorizontal") ||
|
||||||
|
(row < 0 && (col & 1) === 0)
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
context.rect(
|
context.rect(
|
||||||
x < 0 ? 0 : -cellSize.radius,
|
col < 0 ? 0 : -cellSize.radius,
|
||||||
y < 0 ? 0 : -cellSize.radius,
|
row < 0 ? 0 : -cellSize.radius,
|
||||||
x > 0 && grid.type === "hexVertical"
|
col > 0 && grid.type === "hexVertical"
|
||||||
? cellSize.radius
|
? cellSize.radius
|
||||||
: cellSize.radius * 2,
|
: cellSize.radius * 2,
|
||||||
y > 0 && grid.type === "hexVertical" ? cellSize.radius * 2 : cellSize.radius
|
row > 0 && grid.type === "hexVertical"
|
||||||
|
? cellSize.radius * 2
|
||||||
|
: cellSize.radius
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -184,6 +224,56 @@ export function getGridUpdatedInset(grid, mapWidth, mapHeight) {
|
|||||||
return inset;
|
return inset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the max zoom for a grid
|
||||||
|
* @param {Grid} grid
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
export function getGridMaxZoom(grid) {
|
||||||
|
if (!grid) {
|
||||||
|
return 10;
|
||||||
|
}
|
||||||
|
// Return max grid size / 2
|
||||||
|
return Math.max(Math.max(grid.size.x, grid.size.y) / 2, 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert from a 3D cube hex representation to a 2D offset one
|
||||||
|
* @param {Vector3} cube Cube representation of the hex cell
|
||||||
|
* @param {("hexVertical"|"hexHorizontal")} type
|
||||||
|
* @returns {Vector2}
|
||||||
|
*/
|
||||||
|
function hexCubeToOffset(cube, type) {
|
||||||
|
if (type === "hexVertical") {
|
||||||
|
const x = cube.x + (cube.z + (cube.z & 1)) / 2;
|
||||||
|
const y = cube.z;
|
||||||
|
return new Vector2(x, y);
|
||||||
|
} else {
|
||||||
|
const x = cube.x;
|
||||||
|
const y = cube.z + (cube.x + (cube.x & 1)) / 2;
|
||||||
|
return new Vector2(x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Convert from a 2D offset hex representation to a 3D cube one
|
||||||
|
* @param {Vector2} offset Offset representation of the hex cell
|
||||||
|
* @param {("hexVertical"|"hexHorizontal")} type
|
||||||
|
* @returns {Vector3}
|
||||||
|
*/
|
||||||
|
function hexOffsetToCube(offset, type) {
|
||||||
|
if (type === "hexVertical") {
|
||||||
|
const x = offset.x - (offset.y - (offset.y & 1)) / 2;
|
||||||
|
const z = offset.y;
|
||||||
|
const y = -x - z;
|
||||||
|
return { x, y, z };
|
||||||
|
} else {
|
||||||
|
const x = offset.x;
|
||||||
|
const z = offset.y - (offset.x - (offset.x & 1)) / 2;
|
||||||
|
const y = -x - z;
|
||||||
|
return { x, y, z };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all factors of a number
|
* Get all factors of a number
|
||||||
* @param {number} n
|
* @param {number} n
|
||||||
@ -364,103 +454,3 @@ export async function getGridSizeFromImage(image) {
|
|||||||
|
|
||||||
return prediction;
|
return prediction;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the max zoom for a grid
|
|
||||||
* @param {Grid} grid
|
|
||||||
* @returns {number}
|
|
||||||
*/
|
|
||||||
export function getGridMaxZoom(grid) {
|
|
||||||
if (!grid) {
|
|
||||||
return 10;
|
|
||||||
}
|
|
||||||
// Return max grid size / 2
|
|
||||||
return Math.max(Math.max(grid.size.x, grid.size.y) / 2, 5);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Snap a Konva Node to a the closest grid cell
|
|
||||||
* @param {Grid} grid
|
|
||||||
* @param {number} mapWidth
|
|
||||||
* @param {number} mapHeight
|
|
||||||
* @param {Konva.Node} node
|
|
||||||
* @param {number} snappingThreshold 1 = Always snap, 0 = never snap
|
|
||||||
*/
|
|
||||||
export function snapNodeToGrid(
|
|
||||||
grid,
|
|
||||||
mapWidth,
|
|
||||||
mapHeight,
|
|
||||||
node,
|
|
||||||
snappingThreshold
|
|
||||||
) {
|
|
||||||
const offset = Vector2.multiply(grid.inset.topLeft, {
|
|
||||||
x: mapWidth,
|
|
||||||
y: mapHeight,
|
|
||||||
});
|
|
||||||
const gridSize = {
|
|
||||||
x:
|
|
||||||
(mapWidth * (grid.inset.bottomRight.x - grid.inset.topLeft.x)) /
|
|
||||||
grid.size.x,
|
|
||||||
y:
|
|
||||||
(mapHeight * (grid.inset.bottomRight.y - grid.inset.topLeft.y)) /
|
|
||||||
grid.size.y,
|
|
||||||
};
|
|
||||||
|
|
||||||
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 offset space, round, then transform back
|
|
||||||
const gridSnap = Vector2.add(
|
|
||||||
Vector2.roundTo(Vector2.subtract(corner, offset), gridSize),
|
|
||||||
offset
|
|
||||||
);
|
|
||||||
const gridDistance = Vector2.length(Vector2.subtract(gridSnap, corner));
|
|
||||||
const minComponent = Vector2.min(gridSize);
|
|
||||||
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 offset and half grid size to transform it into offset half space then transform it back
|
|
||||||
const halfGridSize = Vector2.multiply(gridSize, 0.5);
|
|
||||||
const centerSnap = Vector2.add(
|
|
||||||
Vector2.add(
|
|
||||||
Vector2.roundTo(
|
|
||||||
Vector2.subtract(Vector2.subtract(position, offset), halfGridSize),
|
|
||||||
gridSize
|
|
||||||
),
|
|
||||||
halfGridSize
|
|
||||||
),
|
|
||||||
offset
|
|
||||||
);
|
|
||||||
const centerDistance = Vector2.length(Vector2.subtract(centerSnap, position));
|
|
||||||
|
|
||||||
if (minCornerGridDistance < minCornerMinComponent * snappingThreshold) {
|
|
||||||
node.position(minGridSnap);
|
|
||||||
} else if (centerDistance < Vector2.min(gridSize) * snappingThreshold) {
|
|
||||||
node.position(centerSnap);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
92
src/hooks/useGridSnapping.js
Normal file
92
src/hooks/useGridSnapping.js
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import Konva from "konva";
|
||||||
|
|
||||||
|
import Vector2 from "../helpers/Vector2";
|
||||||
|
import { getCellLocation } from "../helpers/grid";
|
||||||
|
|
||||||
|
import { useGrid } from "../contexts/GridContext";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a function that when called will snap a node to the current grid
|
||||||
|
* @param {number} snappingThreshold 1 = Always snap, 0 = never snap
|
||||||
|
*/
|
||||||
|
function useGridSnapping(snappingThreshold) {
|
||||||
|
const { 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
|
||||||
|
);
|
||||||
|
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 centerDistance = Vector2.length(
|
||||||
|
Vector2.subtract(centerSnap, position)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (minCornerGridDistance < minCornerMinComponent * snappingThreshold) {
|
||||||
|
node.position(minGridSnap);
|
||||||
|
} else if (
|
||||||
|
centerDistance <
|
||||||
|
Vector2.min(gridCellPixelSize) * snappingThreshold
|
||||||
|
) {
|
||||||
|
node.position(centerSnap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return snapNodeToGrid;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useGridSnapping;
|
Loading…
Reference in New Issue
Block a user