Files

1859 lines
62 KiB
JavaScript
Raw Permalink Normal View History

2025-01-04 00:34:03 +01:00
import BaseGrid from "./base.mjs";
import {GRID_TYPES, MOVEMENT_DIRECTIONS} from "../constants.mjs";
import {logCompatibilityWarning} from "../utils/logging.mjs";
/**
* @typedef {object} _HexagonalGridConfiguration
* @property {boolean} [columns=false] Is this grid column-based (flat-topped) or row-based (pointy-topped)?
* @property {boolean} [even=false] Is this grid even or odd?
*/
/**
* @typedef {GridConfiguration&_HexagonalGridConfiguration} HexagonalGridConfiguration
*/
/**
* Cube coordinates in a hexagonal grid. q + r + s = 0.
* @typedef {object} HexagonalGridCube
* @property {number} q The coordinate along the E-W (columns) or SW-NE (rows) axis.
* Equal to the offset column coordinate if column orientation.
* @property {number} r The coordinate along the NE-SW (columns) or N-S (rows) axis.
* Equal to the offset row coordinate if row orientation.
* @property {number} s The coordinate along the SE-NW axis.
*/
/**
* Hex cube coordinates, an offset of a grid space, or a point with pixel coordinates.
* @typedef {GridCoordinates|HexagonalGridCube} HexagonalGridCoordinates
*/
/* -------------------------------------------- */
/**
* The hexagonal grid class.
*/
export default class HexagonalGrid extends BaseGrid {
/**
* The hexagonal grid constructor.
* @param {HexagonalGridConfiguration} config The grid configuration
*/
constructor(config) {
super(config);
const {columns, even} = config;
/**
* Is this grid column-based (flat-topped) or row-based (pointy-topped)?
* @type {boolean}
*/
this.columns = !!columns;
/**
* Is this grid even or odd?
* @type {boolean}
*/
this.even = !!even;
// Set the type and size of the grid
if ( columns ) {
if ( even ) this.type = GRID_TYPES.HEXEVENQ;
else this.type = GRID_TYPES.HEXODDQ;
this.sizeX *= (2 * Math.SQRT1_3);
} else {
if ( even ) this.type = GRID_TYPES.HEXEVENR;
else this.type = GRID_TYPES.HEXODDR;
this.sizeY *= (2 * Math.SQRT1_3);
}
}
/* -------------------------------------------- */
/**
* Returns the offset of the grid space corresponding to the given coordinates.
* @param {HexagonalGridCoordinates} coords The coordinates
* @returns {GridOffset} The offset
*/
getOffset(coords) {
if ( coords.i !== undefined ) return {i: coords.i, j: coords.j};
const cube = coords.q !== undefined ? coords : this.pointToCube(coords);
return this.cubeToOffset(HexagonalGrid.cubeRound(cube));
}
/* -------------------------------------------- */
/** @override */
getOffsetRange({x, y, width, height}) {
const x0 = x;
const y0 = y;
const {i: i00, j: j00} = this.getOffset({x: x0, y: y0});
if ( !((width > 0) && (height > 0)) ) return [i00, j00, i00, j00];
const x1 = x + width;
const y1 = y + height;
const {i: i01, j: j01} = this.getOffset({x: x1, y: y0});
const {i: i10, j: j10} = this.getOffset({x: x0, y: y1});
const {i: i11, j: j11} = this.getOffset({x: x1, y: y1});
let i0 = Math.min(i00, i01, i10, i11);
let j0 = Math.min(j00, j01, j10, j11);
let i1 = Math.max(i00, i01, i10, i11) + 1;
let j1 = Math.max(j00, j01, j10, j11) + 1;
// While the corners of the rectangle are included in this range, the edges of the rectangle might
// intersect rows or columns outside of the range. So we need to expand the range if necessary.
if ( this.columns ) {
if ( (i00 === i01) && (j00 < j01) && (!(j00 % 2) !== this.even) && (y0 < i00 * this.sizeY) ) i0--;
if ( (i10 === i11) && (j10 < j11) && (!(j00 % 2) === this.even) && (y1 > (i10 + 0.5) * this.sizeY) ) i1++;
if ( (j00 === j10) && (i00 < i10) && (x0 < ((j00 * 0.75) + 0.25) * this.sizeX) ) j0--;
if ( (j01 === j11) && (i01 < i11) && (x1 > ((j01 * 0.75) + 0.75) * this.sizeX) ) j1++;
} else {
if ( (j00 === j10) && (i00 < i10) && (!(i00 % 2) !== this.even) && (x0 < j00 * this.sizeX) ) j0--;
if ( (j01 === j11) && (i01 < i11) && (!(i00 % 2) === this.even) && (x1 > (j01 + 0.5) * this.sizeX) ) j1++;
if ( (i00 === i01) && (j00 < j01) && (y0 < ((i00 * 0.75) + 0.25) * this.sizeY) ) i0--;
if ( (i10 === i11) && (j10 < j11) && (y1 > ((i10 * 0.75) + 0.75) * this.sizeY) ) i1++;
}
return [i0, j0, i1, j1];
}
/* -------------------------------------------- */
/** @override */
getAdjacentOffsets(coords) {
return this.getAdjacentCubes(coords).map(cube => this.getOffset(cube));
}
/* -------------------------------------------- */
/** @override */
testAdjacency(coords1, coords2) {
return HexagonalGrid.cubeDistance(this.getCube(coords1), this.getCube(coords2)) === 1;
}
/* -------------------------------------------- */
/** @override */
getShiftedOffset(coords, direction) {
const offset = this.getOffset(coords);
if ( this.columns ) {
if ( !(direction & MOVEMENT_DIRECTIONS.LEFT) !== !(direction & MOVEMENT_DIRECTIONS.RIGHT) ) {
const even = (offset.j % 2 === 0) === this.even;
if ( (even && (direction & MOVEMENT_DIRECTIONS.UP)) || (!even && (direction & MOVEMENT_DIRECTIONS.DOWN)) ) {
direction &= ~(MOVEMENT_DIRECTIONS.UP | MOVEMENT_DIRECTIONS.DOWN);
}
}
} else {
if ( !(direction & MOVEMENT_DIRECTIONS.UP) !== !(direction & MOVEMENT_DIRECTIONS.DOWN) ) {
const even = (offset.i % 2 === 0) === this.even;
if ( (even && (direction & MOVEMENT_DIRECTIONS.LEFT)) || (!even && (direction & MOVEMENT_DIRECTIONS.RIGHT)) ) {
direction &= ~(MOVEMENT_DIRECTIONS.LEFT | MOVEMENT_DIRECTIONS.RIGHT);
}
}
}
if ( direction & MOVEMENT_DIRECTIONS.UP ) offset.i--;
if ( direction & MOVEMENT_DIRECTIONS.DOWN ) offset.i++;
if ( direction & MOVEMENT_DIRECTIONS.LEFT ) offset.j--;
if ( direction & MOVEMENT_DIRECTIONS.RIGHT ) offset.j++;
return offset;
}
/* -------------------------------------------- */
/** @override */
getShiftedPoint(point, direction) {
const center = this.getCenterPoint(point);
const shifted = this.getCenterPoint(this.getShiftedOffset(center, direction));
shifted.x = point.x + (shifted.x - center.x);
shifted.y = point.y + (shifted.y - center.y);
return shifted;
}
/* -------------------------------------------- */
/**
* Returns the cube coordinates of the grid space corresponding to the given coordinates.
* @param {HexagonalGridCoordinates} coords The coordinates
* @returns {HexagonalGridCube} The cube coordinates
*/
getCube(coords) {
if ( coords.i !== undefined ) return this.offsetToCube(coords);
const cube = coords.q !== undefined ? coords : this.pointToCube(coords);
return HexagonalGrid.cubeRound(cube);
}
/* -------------------------------------------- */
/**
* Returns the cube coordinates of grid spaces adjacent to the one corresponding to the given coordinates.
* @param {HexagonalGridCoordinates} coords The coordinates
* @returns {HexagonalGridCube[]} The adjacent cube coordinates
*/
getAdjacentCubes(coords) {
const {q, r, s} = this.getCube(coords);
return [
{q, r: r - 1, s: s + 1},
{q: q + 1, r: r - 1, s},
{q: q + 1, r, s: s - 1},
{q, r: r + 1, s: s - 1},
{q: q - 1, r: r + 1, s},
{q: q - 1, r, s: s + 1}
];
}
/* -------------------------------------------- */
/**
* Returns the cube coordinates of the grid space corresponding to the given coordinates
* shifted by one grid space in the given direction.
* @param {GridCoordinates} coords The coordinates
* @param {number} direction The direction (see {@link CONST.MOVEMENT_DIRECTIONS})
* @returns {HexagonalGridCube} The cube coordinates
*/
getShiftedCube(coords, direction) {
return this.getCube(this.getShiftedOffset(coords, direction));
}
/* -------------------------------------------- */
/**
* Returns the top-left point of the grid space corresponding to the given coordinates.
* If given a point, the top-left point of the grid space that contains it is returned.
* @param {HexagonalGridCoordinates} coords The coordinates
* @returns {Point} The top-left point
*/
getTopLeftPoint(coords) {
const point = this.getCenterPoint(coords);
point.x -= (this.sizeX / 2);
point.y -= (this.sizeY / 2);
return point;
}
/* -------------------------------------------- */
/**
* Returns the center point of the grid space corresponding to the given coordinates.
* If given a point, the center point of the grid space that contains it is returned.
* @param {HexagonalGridCoordinates} coords The coordinates
* @returns {Point} The center point
*/
getCenterPoint(coords) {
if ( coords.i !== undefined ) {
const {i, j} = coords;
let x;
let y;
if ( this.columns ) {
x = (2 * Math.SQRT1_3) * ((0.75 * j) + 0.5);
const even = (j + 1) % 2 === 0;
y = i + (this.even === even ? 0 : 0.5);
} else {
y = (2 * Math.SQRT1_3) * ((0.75 * i) + 0.5);
const even = (i + 1) % 2 === 0;
x = j + (this.even === even ? 0 : 0.5);
}
const size = this.size;
x *= size;
y *= size;
return {x, y};
}
const cube = coords.q !== undefined ? coords : this.pointToCube(coords);
return this.cubeToPoint(HexagonalGrid.cubeRound(cube));
}
/* -------------------------------------------- */
/** @override */
getShape() {
const scaleX = this.sizeX / 4;
const scaleY = this.sizeY / 4;
if ( this.columns ) {
const x0 = -2 * scaleX;
const x1 = -scaleX;
const x2 = scaleX;
const x3 = 2 * scaleX;
const y0 = -2 * scaleY;
const y1 = 2 * scaleY;
return [{x: x0, y: 0}, {x: x1, y: y0}, {x: x2, y: y0}, {x: x3, y: 0}, {x: x2, y: y1}, {x: x1, y: y1}];
} else {
const y0 = -2 * scaleY;
const y1 = -scaleY;
const y2 = scaleY;
const y3 = 2 * scaleY;
const x0 = -2 * scaleX;
const x1 = 2 * scaleX;
return [{x: 0, y: y0}, {x: x1, y: y1}, {x: x1, y: y2}, {x: 0, y: y3}, {x: x0, y: y2}, {x: x0, y: y1}];
}
}
/* -------------------------------------------- */
/** @override */
getVertices(coords) {
const {i, j} = this.getOffset(coords);
const scaleX = this.sizeX / 4;
const scaleY = this.sizeY / 4;
if ( this.columns ) {
const x = 3 * j;
const x0 = x * scaleX;
const x1 = (x + 1) * scaleX;
const x2 = (x + 3) * scaleX;
const x3 = (x + 4) * scaleX;
const even = (j + 1) % 2 === 0;
const y = (4 * i) - (this.even === even ? 2 : 0);
const y0 = y * scaleY;
const y1 = (y + 2) * scaleY;
const y2 = (y + 4) * scaleY;
return [{x: x0, y: y1}, {x: x1, y: y0}, {x: x2, y: y0}, {x: x3, y: y1}, {x: x2, y: y2}, {x: x1, y: y2}];
} else {
const y = 3 * i;
const y0 = y * scaleY;
const y1 = (y + 1) * scaleY;
const y2 = (y + 3) * scaleY;
const y3 = (y + 4) * scaleY;
const even = (i + 1) % 2 === 0;
const x = (4 * j) - (this.even === even ? 2 : 0);
const x0 = x * scaleX;
const x1 = (x + 2) * scaleX;
const x2 = (x + 4) * scaleX;
return [{x: x1, y: y0}, {x: x2, y: y1}, {x: x2, y: y2}, {x: x1, y: y3}, {x: x0, y: y2}, {x: x0, y: y1}];
}
}
/* -------------------------------------------- */
/** @override */
getSnappedPoint(point, {mode, resolution=1}) {
if ( mode & ~0xFFF3 ) throw new Error("Invalid snapping mode");
if ( mode === 0 ) return {x: point.x, y: point.y};
let nearest;
let distance;
const keepNearest = candidate => {
if ( !nearest ) return nearest = candidate;
const {x, y} = point;
distance ??= ((nearest.x - x) ** 2) + ((nearest.y - y) ** 2);
const d = ((candidate.x - x) ** 2) + ((candidate.y - y) ** 2);
if ( d < distance ) {
nearest = candidate;
distance = d;
}
return nearest;
};
// Symmetries and identities
if ( this.columns ) {
// Top-Left = Bottom-Left
if ( mode & 0x50 ) mode |= 0x50; // Vertex
if ( mode & 0x500 ) mode |= 0x500; // Corner
// Top-Right = Bottom-Right
if ( mode & 0xA0 ) mode |= 0xA0; // Vertex
if ( mode & 0xA00 ) mode |= 0xA00; // Corner
// Left Side = Right Vertex
if ( mode & 0x4000 ) mode |= 0xA0;
// Right Side = Left Vertex
if ( mode & 0x8000 ) mode |= 0x50;
} else {
// Top-Left = Top-Right
if ( mode & 0x30 ) mode |= 0x30; // Vertex
if ( mode & 0x300 ) mode |= 0x300; // Corner
// Bottom-Left = Bottom-Right
if ( mode & 0xC0 ) mode |= 0xC0; // Vertex
if ( mode & 0xC00 ) mode |= 0xC00; // Corner
// Top Side = Bottom Vertex
if ( mode & 0x1000 ) mode |= 0xC0;
// Bottom Side = Top Vertex
if ( mode & 0x2000 ) mode |= 0x30;
}
// Only top/bottom or left/right edges
if ( !(mode & 0x2) ) {
if ( this.columns ) {
// Top/Left side (= edge)
if ( mode & 0x3000 ) keepNearest(this.#snapToTopOrBottom(point, resolution));
} else {
// Left/Right side (= edge)
if ( mode & 0xC000 ) keepNearest(this.#snapToLeftOrRight(point, resolution));
}
}
// Any vertex (plus edge/center)
if ( (mode & 0xF0) === 0xF0 ) {
switch ( mode & 0x3 ) {
case 0x0: keepNearest(this.#snapToVertex(point, resolution)); break;
case 0x1: keepNearest(this.#snapToVertexOrCenter(point, resolution)); break;
case 0x2: keepNearest(this.#snapToEdgeOrVertex(point, resolution)); break;
case 0x3: keepNearest(this.#snapToEdgeOrVertexOrCenter(point, resolution)); break;
}
}
// A specific vertex
else if ( mode & 0xF0 ) {
// Center
if ( (mode & 0x3) === 0x1 ) {
keepNearest(this.#snapToSpecificVertexOrCenter(point, !(mode & 0x10), resolution));
} else {
// Edge and/or center
switch ( mode & 0x3 ) {
case 0x2: keepNearest(this.#snapToEdge(point, resolution)); break;
case 0x3: keepNearest(this.#snapToEdgeOrCenter(point, resolution)); break;
}
// A combination of specific vertices and corners that results in a rectangular grid
if ( ((mode & 0xF0) ^ ((mode & 0xF00) >> 4)) === 0xF0 ) {
return keepNearest(this.#snapToRectangularGrid(point, !(mode & 0x100), resolution));
}
keepNearest(this.#snapToSpecificVertex(point, !(mode & 0x10), resolution));
}
}
// Edges and/or centers
else {
switch ( mode & 0x3 ) {
case 0x1: keepNearest(this.#snapToCenter(point, resolution)); break;
case 0x2: keepNearest(this.#snapToEdge(point, resolution)); break;
case 0x3: keepNearest(this.#snapToEdgeOrCenter(point, resolution)); break;
}
}
// Any corner
if ( (mode & 0xF00) === 0xF00 ) {
keepNearest(this.#snapToCorner(point, resolution));
}
// A specific corner
else if ( mode & 0xF00 ) {
keepNearest(this.#snapToSpecificCorner(point, !(mode & 0x100), resolution));
}
return nearest;
}
/* -------------------------------------------- */
/**
* Snap the point to the nearest center of a hexagon.
* @param {Point} point The point
* @param {number} resolution The grid resolution
* @param {number} [dx=0] The x-translation of the grid
* @param {number} [dy=0] The y-translation of the grid
* @param {boolean} [columns] Flat-top instead of pointy-top?
* @param {boolean} [even] Start at a full grid space?
* @param {number} [size] The size of a grid space
* @returns {Point} The snapped point
*/
#snapToCenter({x, y}, resolution, dx=0, dy=0, columns=this.columns, even=this.even, size=this.size) {
// Subdivide the hex grid
const grid = HexagonalGrid.#TEMP_GRID;
grid.columns = columns;
grid.size = size / resolution;
// Align the subdivided grid with this hex grid
if ( columns ) {
dx += ((size - grid.size) * Math.SQRT1_3);
if ( even ) dy += (size / 2);
} else {
if ( even ) dx += (size / 2);
dy += ((size - grid.size) * Math.SQRT1_3);
}
// Get the snapped center point for the subdivision
const point = HexagonalGrid.#TEMP_POINT;
point.x = x - dx;
point.y = y - dy;
const snapped = grid.getCenterPoint(point);
snapped.x += dx;
snapped.y += dy;
return snapped;
}
/* -------------------------------------------- */
/**
* Snap the point to the nearest vertex of a hexagon.
* @param {Point} point The point
* @param {number} resolution The grid resolution
* @param {number} [dx=0] The x-offset of the grid
* @param {number} [dy=0] The y-offset of the grid
* @returns {Point} The snapped point
*/
#snapToVertex(point, resolution, dx, dy) {
const center = this.#snapToCenter(point, resolution, dx, dy);
const {x: x0, y: y0} = center;
let angle = Math.atan2(point.y - y0, point.x - x0);
if ( this.columns ) angle = Math.round(angle / (Math.PI / 3)) * (Math.PI / 3);
else angle = (Math.floor(angle / (Math.PI / 3)) + 0.5) * (Math.PI / 3);
const radius = Math.max(this.sizeX, this.sizeY) / (2 * resolution);
const vertex = center; // Reuse the object
vertex.x = x0 + (Math.cos(angle) * radius);
vertex.y = y0 + (Math.sin(angle) * radius);
return vertex;
}
/* -------------------------------------------- */
/**
* Snap the point to the nearest vertex or center of a hexagon.
* @param {Point} point The point
* @param {number} resolution The grid resolution
* @returns {Point} The snapped point
*/
#snapToVertexOrCenter(point, resolution) {
let size;
let dx = 0;
let dy = 0;
if ( this.columns ) {
size = this.sizeX / 2;
dy = size * (Math.SQRT1_3 / 2);
} else {
size = this.sizeY / 2;
dx = size * (Math.SQRT1_3 / 2);
}
return this.#snapToCenter(point, resolution, dx, dy, !this.columns, !this.even, size);
}
/* -------------------------------------------- */
/**
* Snap the point to the nearest edge of a hexagon.
* @param {Point} point The point
* @param {number} resolution The grid resolution
* @returns {Point} The snapped point
*/
#snapToEdge(point, resolution) {
const center = this.#snapToCenter(point, resolution);
const {x: x0, y: y0} = center;
let angle = Math.atan2(point.y - y0, point.x - x0);
if ( this.columns ) angle = (Math.floor(angle / (Math.PI / 3)) + 0.5) * (Math.PI / 3);
else angle = Math.round(angle / (Math.PI / 3)) * (Math.PI / 3);
const radius = Math.min(this.sizeX, this.sizeY) / (2 * resolution);
const vertex = center; // Reuse the object
vertex.x = x0 + (Math.cos(angle) * radius);
vertex.y = y0 + (Math.sin(angle) * radius);
return vertex;
}
/* -------------------------------------------- */
/**
* Snap the point to the nearest edge or center of a hexagon.
* @param {Point} point The point
* @param {number} resolution The grid resolution
* @returns {Point} The snapped point
*/
#snapToEdgeOrCenter(point, resolution) {
let size;
let dx = 0;
let dy = 0;
if ( this.columns ) {
size = this.sizeY / 2;
dx = size * Math.SQRT1_3;
} else {
size = this.sizeX / 2;
dy = size * Math.SQRT1_3;
}
return this.#snapToCenter(point, resolution, dx, dy, this.columns, false, size);
}
/* -------------------------------------------- */
/**
* Snap the point to the nearest edge or vertex of a hexagon.
* @param {Point} point The point
* @param {number} resolution The grid resolution
* @returns {Point} The snapped point
*/
#snapToEdgeOrVertex(point, resolution) {
const {x, y} = point;
point = this.#snapToCenter(point, resolution);
const {x: x0, y: y0} = point;
const dx = x - x0;
const dy = y - y0;
let angle = Math.atan2(dy, dx);
if ( this.columns ) angle = (Math.floor(angle / (Math.PI / 3)) + 0.5) * (Math.PI / 3);
else angle = Math.round(angle / (Math.PI / 3)) * (Math.PI / 3);
const s = 2 * resolution;
let radius1 = this.sizeX / s;
let radius2 = this.sizeY / s;
if ( radius1 > radius2 ) [radius1, radius2] = [radius2, radius1];
const cos = Math.cos(angle);
const sin = Math.sin(angle);
const d = (cos * dy) - (sin * dx);
if ( Math.abs(d) <= radius2 / 4 ) {
point.x = x0 + (cos * radius1);
point.y = y0 + (sin * radius1);
} else {
angle += ((Math.PI / 6) * Math.sign(d));
point.x = x0 + (Math.cos(angle) * radius2);
point.y = y0 + (Math.sin(angle) * radius2);
}
return point;
}
/* -------------------------------------------- */
/**
* Snap the point to the nearest edge, vertex, center of a hexagon.
* @param {Point} point The point
* @param {number} resolution The grid resolution
* @returns {Point} The snapped point
*/
#snapToEdgeOrVertexOrCenter(point, resolution) {
const {x, y} = point;
point = this.#snapToCenter(point, resolution);
const {x: x0, y: y0} = point;
const dx = x - x0;
const dy = y - y0;
let angle = Math.atan2(dy, dx);
if ( this.columns ) angle = (Math.floor(angle / (Math.PI / 3)) + 0.5) * (Math.PI / 3);
else angle = Math.round(angle / (Math.PI / 3)) * (Math.PI / 3);
const s = 2 * resolution;
let radius1 = this.sizeX / s;
let radius2 = this.sizeY / s;
if ( radius1 > radius2 ) [radius1, radius2] = [radius2, radius1];
const cos = Math.cos(angle);
const sin = Math.sin(angle);
const d1 = (cos * dx) + (sin * dy);
if ( d1 <= radius1 / 2 ) return point;
const d2 = (cos * dy) - (sin * dx);
if ( Math.abs(d2) <= radius2 / 4 ) {
point.x = x0 + (cos * radius1);
point.y = y0 + (sin * radius1);
} else {
angle += ((Math.PI / 6) * Math.sign(d2));
point.x = x0 + (Math.cos(angle) * radius2);
point.y = y0 + (Math.sin(angle) * radius2);
}
return point;
}
/* -------------------------------------------- */
/**
* Snap the point to the nearest corner of a hexagon.
* @param {Point} point The point
* @param {number} resolution The grid resolution
* @returns {Point} The snapped point
*/
#snapToCorner(point, resolution) {
let dx = 0;
let dy = 0;
const s = 2 * resolution;
if ( this.columns ) dy = this.sizeY / s;
else dx = this.sizeX / s;
return this.#snapToVertex(point, resolution, dx, dy);
}
/* -------------------------------------------- */
/**
* Snap the point to the nearest top/bottom-left/right vertex of a hexagon.
* @param {Point} point The point
* @param {boolean} other Bottom-right instead of top-left vertex?
* @param {number} resolution The grid resolution
* @returns {Point} The snapped point
*/
#snapToSpecificVertex(point, other, resolution) {
let dx = 0;
let dy = 0;
const s = (other ? -2 : 2) * resolution;
if ( this.columns ) dx = this.sizeX / s;
else dy = this.sizeY / s;
return this.#snapToCenter(point, resolution, dx, dy);
}
/* -------------------------------------------- */
/**
* Snap the point to the nearest top/bottom-left/right vertex or center of a hexagon.
* @param {Point} point The point
* @param {boolean} other Bottom-right instead of top-left vertex?
* @param {number} resolution The grid resolution
* @returns {Point} The snapped point
*/
#snapToSpecificVertexOrCenter(point, other, resolution) {
let dx = 0;
let dy = 0;
const s = (other ? 2 : -2) * resolution;
if ( this.columns ) dx = this.sizeX / s;
else dy = this.sizeY / s;
return this.#snapToVertex(point, resolution, dx, dy);
}
/* -------------------------------------------- */
/**
* Snap the point to the nearest top/bottom-left/right corner of a hexagon.
* @param {Point} point The point
* @param {boolean} other Bottom-right instead of top-left corner?
* @param {number} resolution The grid resolution
* @returns {Point} The snapped point
*/
#snapToSpecificCorner(point, other, resolution) {
let dx = 0;
let dy = 0;
const s = (other ? -4 : 4) * resolution;
if ( this.columns ) dx = this.sizeX / s;
else dy = this.sizeY / s;
return this.#snapToCenter(point, resolution, dx, dy);
}
/* -------------------------------------------- */
/**
* Snap the point to the nearest grid intersection of the rectanglar grid.
* @param {Point} point The point
* @param {boolean} other Align rectangles with top-left vertices instead of top-left corners?
* @param {number} resolution The grid resolution
* @returns {Point} The snapped point
*/
#snapToRectangularGrid(point, other, resolution) {
const tx = this.sizeX / 2;
const ty = this.sizeY / 2;
let sx = tx;
let sy = ty;
let dx = 0;
let dy = 0;
const d = other ? 1 / 3 : 2 / 3;
if ( this.columns ) {
sx *= 1.5;
dx = d;
} else {
sy *= 1.5;
dy = d;
}
sx /= resolution;
sy /= resolution;
return {
x: ((Math.round(((point.x - tx) / sx) + dx) - dx) * sx) + tx,
y: ((Math.round(((point.y - ty) / sy) + dy) - dy) * sy) + ty
};
}
/**
* Snap the point to the nearest top/bottom side of the bounds of a hexagon.
* @param {Point} point The point
* @param {number} resolution The grid resolution
* @returns {Point} The snapped point
*/
#snapToTopOrBottom(point, resolution) {
return this.#snapToCenter(point, resolution, 0, this.sizeY / (2 * resolution));
}
/* -------------------------------------------- */
/**
* Snap the point to the nearest left/right side of the bounds of a hexagon.
* @param {Point} point The point
* @param {number} resolution The grid resolution
* @returns {Point} The snapped point
*/
#snapToLeftOrRight(point, resolution) {
return this.#snapToCenter(point, resolution, this.sizeX / (2 * resolution), 0);
}
/* -------------------------------------------- */
/** @inheritdoc */
calculateDimensions(sceneWidth, sceneHeight, padding) {
const {columns, size} = this;
const sizeX = columns ? (2 * size) / Math.SQRT3 : size;
const sizeY = columns ? size : (2 * size) / Math.SQRT3;
const strideX = columns ? 0.75 * sizeX : sizeX;
const strideY = columns ? sizeY : 0.75 * sizeY;
// Skip padding computation for Scenes which do not include padding
if ( !padding ) {
const cols = Math.ceil(((sceneWidth + (columns ? -sizeX / 4 : sizeX / 2)) / strideX) - 1e-6);
const rows = Math.ceil(((sceneHeight + (columns ? sizeY / 2 : -sizeY / 4)) / strideY) - 1e-6);
return {width: sceneWidth, height: sceneHeight, x: 0, y: 0, rows, columns: cols};
}
// The grid size is equal to the short diagonal of the hexagon, so padding in that axis will divide evenly by the
// grid size. In the cross-axis, however, the hexagons do not stack but instead interleave. Multiplying the long
// diagonal by 75% gives us the amount of space each hexagon takes up in that axis without overlapping.
// Note: Do not replace `* (1 / strideX)` by `/ strideX` and `* (1 / strideY)` by `/ strideY`!
// It could change the result and therefore break certain scenes.
let x = Math.ceil((padding * sceneWidth) * (1 / strideX)) * strideX;
let y = Math.ceil((padding * sceneHeight) * (1 / strideY)) * strideY;
// Note: The width and height calculation needs rounded x/y. If we were to remove the rounding here,
// the result of the rounding of the width and height below would change in certain scenes.
let width = sceneWidth + (2 * Math.round(Math.ceil((padding * sceneWidth) * (1 / strideX)) / (1 / strideX)));
let height = sceneHeight + (2 * Math.round(Math.ceil((padding * sceneHeight) * (1 / strideY)) / (1 / strideY)));
// Ensure that the top-left hexagon of the scene rectangle is always a full hexagon for even grids and always a
// half hexagon for odd grids, by shifting the padding in the main axis by half a hex if the number of hexagons in
// the cross-axis is odd.
const crossEven = Math.round(columns ? x / strideX : y / strideY) % 2 === 0;
if ( !crossEven ) {
if ( columns ) {
y += (sizeY / 2);
height += sizeY;
} else {
x += (sizeX / 2);
width += sizeX;
}
}
// The height (if column orientation) or width (if row orientation) must be a multiple of the grid size, and
// the last column (if column orientation) or row (if row orientation) must be fully within the bounds.
// Note: Do not replace `* (1 / strideX)` by `/ strideX` and `* (1 / strideY)` by `/ strideY`!
// It could change the result and therefore break certain scenes.
let cols = Math.round(width * (1 / strideX));
let rows = Math.round(height * (1 / strideY));
width = cols * strideX;
height = rows * strideY;
if ( columns ) {
rows++;
width += (sizeX / 4);
} else {
cols++;
height += (sizeY / 4);
}
return {width, height, x, y, rows, columns: cols};
}
/* -------------------------------------------- */
/**
* Calculate the total size of the canvas with padding applied, as well as the top-left coordinates of the inner
* rectangle that houses the scene. (Legacy)
* @param {number} columns Column or row orientation?
* @param {number} legacySize The legacy size of the grid.
* @param {number} sceneWidth The width of the scene.
* @param {number} sceneHeight The height of the scene.
* @param {number} padding The percentage of padding.
* @returns {{width: number, height: number, x: number, y: number, rows: number, columns: number}}
* @internal
*/
static _calculatePreV10Dimensions(columns, legacySize, sceneWidth, sceneHeight, padding) {
// Note: Do not replace `* (1 / legacySize)` by `/ legacySize`!
// It could change the result and therefore break certain scenes.
const x = Math.ceil((padding * sceneWidth) * (1 / legacySize)) * legacySize;
const y = Math.ceil((padding * sceneHeight) * (1 / legacySize)) * legacySize;
const width = sceneWidth + (2 * x);
const height = sceneHeight + (2 * y);
const size = legacySize * (Math.SQRT3 / 2);
const sizeX = columns ? legacySize : size;
const sizeY = columns ? size : legacySize;
const strideX = columns ? 0.75 * sizeX : sizeX;
const strideY = columns ? sizeY : 0.75 * sizeY;
const cols = Math.floor(((width + (columns ? sizeX / 4 : sizeX)) / strideX) + 1e-6);
const rows = Math.floor(((height + (columns ? sizeY : sizeY / 4)) / strideY) + 1e-6);
return {width, height, x, y, rows, columns: cols};
}
/* -------------------------------------------- */
/** @override */
_measurePath(waypoints, {cost}, result) {
result.distance = 0;
result.spaces = 0;
result.cost = 0;
if ( waypoints.length === 0 ) return;
const from = result.waypoints[0];
from.distance = 0;
from.spaces = 0;
from.cost = 0;
// Convert to (fractional) cube coordinates
const toCube = coords => {
if ( coords.x !== undefined ) return this.pointToCube(coords);
if ( coords.i !== undefined ) return this.offsetToCube(coords);
return coords;
};
// Prepare data for the starting point
const w0 = waypoints[0];
let o0 = this.getOffset(w0);
let c0 = this.offsetToCube(o0);
let d0 = toCube(w0);
// Iterate over additional path points
for ( let i = 1; i < waypoints.length; i++ ) {
const w1 = waypoints[i];
const o1 = this.getOffset(w1);
const c1 = this.offsetToCube(o1);
const d1 = toCube(w1);
// Measure segment
const to = result.waypoints[i];
const segment = to.backward;
if ( !w1.teleport ) {
// Determine the number of hexes and cube distance
const c = HexagonalGrid.cubeDistance(c0, c1);
let d = HexagonalGrid.cubeDistance(d0, d1);
if ( d.almostEqual(c) ) d = c;
// Calculate the distance based on the cube distance
segment.distance = d * this.distance;
segment.spaces = c;
segment.cost = cost ? this.#calculateCost(c0, c1, cost) : c * this.distance;
} else {
segment.distance = 0;
segment.spaces = 0;
segment.cost = cost && ((o0.i !== o1.i) || (o0.j !== o1.j)) ? cost(o0, o1, 0) : 0;
}
// Accumulate measurements
result.distance += segment.distance;
result.spaces += segment.spaces;
result.cost += segment.cost;
// Set waypoint measurements
to.distance = result.distance;
to.spaces = result.spaces;
to.cost = result.cost;
o0 = o1;
c0 = c1;
d0 = d1;
}
}
/* -------------------------------------------- */
/**
* Calculate the cost of the direct path segment.
* @param {HexagonalGridCube} from The coordinates the segment starts from
* @param {HexagonalGridCube} to The coordinates the segment goes to
* @param {GridMeasurePathCostFunction} cost The cost function
* @returns {number} The cost of the path segment
*/
#calculateCost(from, to, cost) {
const path = this.getDirectPath([from, to]);
if ( path.length <= 1 ) return 0;
// Prepare data for the starting point
let o0 = path[0];
let c = 0;
// Iterate over additional path points
for ( let i = 1; i < path.length; i++ ) {
const o1 = path[i];
// Calculate and accumulate the cost
c += cost(o0, o1, this.distance);
o0 = o1;
}
return c;
}
/* -------------------------------------------- */
/**
* @see {@link https://www.redblobgames.com/grids/hexagons/#line-drawing}
* @override
*/
getDirectPath(waypoints) {
if ( waypoints.length === 0 ) return [];
// Prepare data for the starting point
let c0 = this.getCube(waypoints[0]);
let {q: q0, r: r0} = c0;
const path = [this.getOffset(c0)];
// Iterate over additional path points
for ( let i = 1; i < waypoints.length; i++ ) {
const c1 = this.getCube(waypoints[i]);
const {q: q1, r: r1} = c1;
if ( (q0 === q1) && (r0 === r1) ) continue;
// Walk from (q0, r0, s0) to (q1, r1, s1)
const dq = q0 - q1;
const dr = r0 - r1;
// If the path segment is collinear with some hexagon edge, we need to nudge
// the cube coordinates in the right direction so that we get a consistent, clean path.
const EPS = 1e-6;
let eq = 0;
let er = 0;
if ( this.columns ) {
// Collinear with SE-NW edges
if ( dq === dr ) {
// Prefer movement such that we have symmetry with the E-W case
er = !(q0 & 1) === this.even ? EPS : -EPS;
eq = -er;
}
// Collinear with SW-NE edges
else if ( -2 * dq === dr ) {
// Prefer movement such that we have symmetry with the E-W case
eq = !(q0 & 1) === this.even ? EPS : -EPS;
}
// Collinear with E-W edges
else if ( dq === -2 * dr ) {
// Move such we don't leave the row that we're in
er = !(q0 & 1) === this.even ? -EPS : EPS;
}
} else {
// Collinear with SE-NW edges
if ( dq === dr ) {
// Prefer movement such that we have symmetry with the S-N case
eq = !(r0 & 1) === this.even ? EPS : -EPS;
er = -eq;
}
// Collinear with SW-NE edges
else if ( dq === -2 * dr ) {
// Prefer movement such that we have symmetry with the S-N case
er = !(r0 & 1) === this.even ? EPS : -EPS;
}
// Collinear with S-N edges
else if ( -2 * dq === dr ) {
// Move such we don't leave the column that we're in
eq = !(r0 & 1) === this.even ? -EPS : EPS;
}
}
const n = HexagonalGrid.cubeDistance(c0, c1);
for ( let j = 1; j < n; j++ ) {
// Break tries on E-W (if columns) / S-N (if rows) edges
const t = (j + EPS) / n;
const q = Math.mix(q0, q1, t) + eq;
const r = Math.mix(r0, r1, t) + er;
const s = 0 - q - r;
path.push(this.getOffset({q, r, s}));
}
path.push(this.getOffset(c1));
c0 = c1;
q0 = q1;
r0 = r1;
}
return path;
}
/* -------------------------------------------- */
/** @override */
getTranslatedPoint(point, direction, distance) {
direction = Math.toRadians(direction);
const dx = Math.cos(direction);
const dy = Math.sin(direction);
let q;
let r;
if ( this.columns ) {
q = (2 * Math.SQRT1_3) * dx;
r = (-0.5 * q) + dy;
} else {
r = (2 * Math.SQRT1_3) * dy;
q = (-0.5 * r) + dx;
}
const s = distance / this.distance * this.size / ((Math.abs(r) + Math.abs(q) + Math.abs(q + r)) / 2);
return {x: point.x + (dx * s), y: point.y + (dy * s)};
}
/* -------------------------------------------- */
/** @override */
getCircle({x, y}, radius) {
// TODO: Move to BaseGrid once BaseGrid -> GridlessGrid
if ( radius <= 0 ) return [];
const r = radius / this.distance * this.size;
if ( this.columns ) {
const x0 = r * (Math.SQRT3 / 2);
const x1 = -x0;
const y0 = r;
const y1 = y0 / 2;
const y2 = -y1;
const y3 = -y0;
return [{x: x, y: y + y0}, {x: x + x1, y: y + y1}, {x: x + x1, y: y + y2},
{x: x, y: y + y3}, {x: x + x0, y: y + y2}, {x: x + x0, y: y + y1}];
} else {
const y0 = r * (Math.SQRT3 / 2);
const y1 = -y0;
const x0 = r;
const x1 = x0 / 2;
const x2 = -x1;
const x3 = -x0;
return [{x: x + x0, y: y}, {x: x + x1, y: y + y0}, {x: x + x2, y: y + y0},
{x: x + x3, y: y}, {x: x + x2, y: y + y1}, {x: x + x1, y: y + y1}];
}
}
/* -------------------------------------------- */
/* Conversion Functions */
/* -------------------------------------------- */
/**
* Round the fractional cube coordinates (q, r, s).
* @see {@link https://www.redblobgames.com/grids/hexagons/}
* @param {HexagonalGridCube} cube The fractional cube coordinates
* @returns {HexagonalGridCube} The rounded integer cube coordinates
*/
static cubeRound({q, r, s}) {
let iq = Math.round(q);
let ir = Math.round(r);
let is = Math.round(s);
const dq = Math.abs(iq - q);
const dr = Math.abs(ir - r);
const ds = Math.abs(is - s);
if ( (dq > dr) && (dq > ds) ) {
iq = -ir - is;
} else if ( dr > ds ) {
ir = -iq - is;
} else {
is = -iq - ir;
}
return {q: iq | 0, r: ir | 0, s: is | 0};
}
/* -------------------------------------------- */
/**
* Convert point coordinates (x, y) into cube coordinates (q, r, s).
* Inverse of {@link HexagonalGrid#cubeToPoint}.
* @see {@link https://www.redblobgames.com/grids/hexagons/}
* @param {Point} point The point
* @returns {HexagonalGridCube} The (fractional) cube coordinates
*/
pointToCube({x, y}) {
let q;
let r;
const size = this.size;
x /= size;
y /= size;
if ( this.columns ) {
q = ((2 * Math.SQRT1_3) * x) - (2 / 3);
r = (-0.5 * (q + (this.even ? 1 : 0))) + y;
} else {
r = ((2 * Math.SQRT1_3) * y) - (2 / 3);
q = (-0.5 * (r + (this.even ? 1 : 0))) + x;
}
return {q, r, s: 0 - q - r};
}
/* -------------------------------------------- */
/**
* Convert cube coordinates (q, r, s) into point coordinates (x, y).
* Inverse of {@link HexagonalGrid#pointToCube}.
* @see {@link https://www.redblobgames.com/grids/hexagons/}
* @param {HexagonalGridCube} cube The cube coordinates
* @returns {Point} The point coordinates
*/
cubeToPoint({q, r}) {
let x;
let y;
if ( this.columns ) {
x = (Math.SQRT3 / 2) * (q + (2 / 3));
y = (0.5 * (q + (this.even ? 1 : 0))) + r;
} else {
y = (Math.SQRT3 / 2) * (r + (2 / 3));
x = (0.5 * (r + (this.even ? 1 : 0))) + q;
}
const size = this.size;
x *= size;
y *= size;
return {x, y};
}
/* -------------------------------------------- */
/**
* Convert offset coordinates (i, j) into integer cube coordinates (q, r, s).
* Inverse of {@link HexagonalGrid#cubeToOffset}.
* @see {@link https://www.redblobgames.com/grids/hexagons/}
* @param {GridOffset} offset The offset coordinates
* @returns {HexagonalGridCube} The integer cube coordinates
*/
offsetToCube({i, j}) {
let q;
let r;
if ( this.columns ) {
q = j;
r = i - ((j + ((this.even ? 1 : -1) * (j & 1))) >> 1);
} else {
q = j - ((i + ((this.even ? 1 : -1) * (i & 1))) >> 1);
r = i;
}
return {q, r, s: 0 - q - r};
}
/* -------------------------------------------- */
/**
* Convert integer cube coordinates (q, r, s) into offset coordinates (i, j).
* Inverse of {@link HexagonalGrid#offsetToCube}.
* @see {@link https://www.redblobgames.com/grids/hexagons/}
* @param {HexagonalGridCube} cube The cube coordinates
* @returns {GridOffset} The offset coordinates
*/
cubeToOffset({q, r}) {
let i;
let j;
if ( this.columns ) {
j = q;
i = r + ((q + ((this.even ? 1 : -1) * (q & 1))) >> 1);
} else {
i = r;
j = q + ((r + ((this.even ? 1 : -1) * (r & 1))) >> 1);
}
return {i, j};
}
/* -------------------------------------------- */
/**
* Measure the distance in hexagons between two cube coordinates.
* @see {@link https://www.redblobgames.com/grids/hexagons/}
* @param {HexagonalGridCube} a The first cube coordinates
* @param {HexagonalGridCube} b The second cube coordinates
* @returns {number} The distance between the two cube coordinates in hexagons
*/
static cubeDistance(a, b) {
const dq = a.q - b.q;
const dr = a.r - b.r;
return (Math.abs(dq) + Math.abs(dr) + Math.abs(dq + dr)) / 2;
}
/* -------------------------------------------- */
/**
* Used by {@link HexagonalGrid#snapToCenter}.
* @type {Point}
*/
static #TEMP_POINT = {x: 0, y: 0};
/* -------------------------------------------- */
/**
* Used by {@link HexagonalGrid#snapToCenter}.
* Always an odd grid!
* @type {HexagonalGrid}
*/
static #TEMP_GRID = new HexagonalGrid({size: 1});
/* -------------------------------------------- */
/* Deprecations and Compatibility */
/* -------------------------------------------- */
/**
* @deprecated since v12
* @ignore
*/
static get POINTY_HEX_BORDERS() {
const msg = "HexagonalGrid.POINTY_HEX_BORDERS is deprecated without replacement.";
logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
return this.#POINTY_HEX_BORDERS;
}
/**
* @deprecated since v12
* @ignore
*/
static #POINTY_HEX_BORDERS = {
0.5: [[0, 0.25], [0.5, 0], [1, 0.25], [1, 0.75], [0.5, 1], [0, 0.75]],
1: [[0, 0.25], [0.5, 0], [1, 0.25], [1, 0.75], [0.5, 1], [0, 0.75]],
2: [
[.5, 0], [.75, 1/7], [.75, 3/7], [1, 4/7], [1, 6/7], [.75, 1], [.5, 6/7], [.25, 1], [0, 6/7], [0, 4/7],
[.25, 3/7], [.25, 1/7]
],
3: [
[.5, .1], [2/3, 0], [5/6, .1], [5/6, .3], [1, .4], [1, .6], [5/6, .7], [5/6, .9], [2/3, 1], [.5, .9], [1/3, 1],
[1/6, .9], [1/6, .7], [0, .6], [0, .4], [1/6, .3], [1/6, .1], [1/3, 0]
],
4: [
[.5, 0], [5/8, 1/13], [.75, 0], [7/8, 1/13], [7/8, 3/13], [1, 4/13], [1, 6/13], [7/8, 7/13], [7/8, 9/13],
[.75, 10/13], [.75, 12/13], [5/8, 1], [.5, 12/13], [3/8, 1], [.25, 12/13], [.25, 10/13], [1/8, 9/13],
[1/8, 7/13], [0, 6/13], [0, 4/13], [1/8, 3/13], [1/8, 1/13], [.25, 0], [3/8, 1/13]
]
};
/* -------------------------------------------- */
/**
* @deprecated since v12
* @ignore
*/
static get FLAT_HEX_BORDERS() {
const msg = "HexagonalGrid.FLAT_HEX_BORDERS is deprecated without replacement.";
logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
return this.#FLAT_HEX_BORDERS;
}
/**
* @deprecated since v12
* @ignore
*/
static #FLAT_HEX_BORDERS = {
0.5: [[0, 0.5], [0.25, 0], [0.75, 0], [1, 0.5], [0.75, 1], [0.25, 1]],
1: [[0, 0.5], [0.25, 0], [0.75, 0], [1, 0.5], [0.75, 1], [0.25, 1]],
2: [
[3/7, .25], [4/7, 0], [6/7, 0], [1, .25], [6/7, .5], [1, .75], [6/7, 1], [4/7, 1], [3/7, .75], [1/7, .75],
[0, .5], [1/7, .25]
],
3: [
[.4, 0], [.6, 0], [.7, 1/6], [.9, 1/6], [1, 1/3], [.9, .5], [1, 2/3], [.9, 5/6], [.7, 5/6], [.6, 1], [.4, 1],
[.3, 5/6], [.1, 5/6], [0, 2/3], [.1, .5], [0, 1/3], [.1, 1/6], [.3, 1/6]
],
4: [
[6/13, 0], [7/13, 1/8], [9/13, 1/8], [10/13, .25], [12/13, .25], [1, 3/8], [12/13, .5], [1, 5/8], [12/13, .75],
[10/13, .75], [9/13, 7/8], [7/13, 7/8], [6/13, 1], [4/13, 1], [3/13, 7/8], [1/13, 7/8], [0, .75], [1/13, 5/8],
[0, .5], [1/13, 3/8], [0, .25], [1/13, 1/8], [3/13, 1/8], [4/13, 0]
]
};
/* -------------------------------------------- */
/**
* @deprecated since v12
* @ignore
*/
static get pointyHexPoints() {
const msg = "HexagonalGrid.pointyHexPoints is deprecated without replacement.";
logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
return this.#POINTY_HEX_BORDERS[1];
}
/* -------------------------------------------- */
/**
* @deprecated since v12
* @ignore
*/
static get flatHexPoints() {
const msg = "HexagonalGrid.flatHexPoints is deprecated without replacement.";
logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
return this.#FLAT_HEX_BORDERS[1];
}
/* -------------------------------------------- */
/**
* @deprecated since v12
* @ignore
*/
get hexPoints() {
const msg = "HexagonalGrid#hexPoints is deprecated without replacement.";
logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
return this.columns ? this.constructor.flatHexPoints : this.constructor.pointyHexPoints;
}
/* -------------------------------------------- */
/**
* @deprecated since v12
* @ignore
*/
getPolygon(x, y, w, h, points) {
const msg = "HexagonalGrid#getPolygon is deprecated. You can get the shape of the hex with HexagonalGrid#getShape "
+ "and the polygon of any hex with HexagonalGrid#getVertices.";
logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
w = w ?? this.sizeX;
h = h ?? this.sizeY;
points ??= this.hexPoints;
const poly = [];
for ( let i=0; i < points.length; i++ ) {
poly.push(x + (w * points[i][0]), y + (h * points[i][1]));
}
return poly;
}
/* -------------------------------------------- */
/**
* @deprecated since v12
* @ignore
*/
getBorderPolygon(w, h, p) {
const msg = "HexagonalGrid#getBorderPolygon is deprecated. "
+ "If you need the shape of a Token, use Token#shape/getShape instead.";
logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
const points = this.columns ? this.constructor.FLAT_HEX_BORDERS[w] : this.constructor.POINTY_HEX_BORDERS[w];
if ( (w !== h) || !points ) return null;
const p2 = p / 2;
const p4 = p / 4;
const r = this.getRect(w, h);
return this.getPolygon(-p4, -p4, r.width + p2, r.height + p2, points);
}
/* -------------------------------------------- */
/**
* @deprecated since v12
* @ignore
*/
getRect(w, h) {
const msg = "HexagonalGrid#getRect is deprecated. If you need the size of a Token, use Token#getSize instead.";
logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
if ( !this.columns || (w < 1) ) w *= this.sizeX;
else w = (this.sizeX * .75 * (w - 1)) + this.sizeX;
if ( this.columns || (h < 1) ) h *= this.sizeY;
else h = (this.sizeY * .75 * (h - 1)) + this.sizeY;
return new PIXI.Rectangle(0, 0, w, h);
}
/* -------------------------------------------- */
/**
* @deprecated since v12
* @ignore
*/
_adjustSnapForTokenSize(x, y, token) {
const msg = "HexagonalGrid#_adjustSnapForTokenSize is deprecated.";
logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
if ( (token.document.width <= 1) && (token.document.height <= 1) ) {
const [row, col] = this.getGridPositionFromPixels(x, y);
const [x0, y0] = this.getPixelsFromGridPosition(row, col);
return [x0 + (this.sizeX / 2) - (token.w / 2), y0 + (this.sizeY / 2) - (token.h / 2)];
}
if ( this.columns && (token.document.height > 1) ) y -= this.sizeY / 2;
if ( !this.columns && (token.document.width > 1) ) x -= this.sizeX / 2;
return [x, y];
}
/* -------------------------------------------- */
/**
* @deprecated since v12
* @ignore
*/
static computeDimensions({columns, size, legacy}) {
const msg = "HexagonalGrid.computeDimensions is deprecated without replacement.";
logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
// Legacy dimensions (deprecated)
if ( legacy ) {
if ( columns ) return { width: size, height: (Math.SQRT3 / 2) * size };
return { width: (Math.SQRT3 / 2) * size, height: size };
}
// Columnar orientation
if ( columns ) return { width: (2 * size) / Math.SQRT3, height: size };
// Row orientation
return { width: size, height: (2 * size) / Math.SQRT3 };
}
/* -------------------------------------------- */
/**
* @deprecated since v12
* @ignore
*/
get columnar() {
const msg = "HexagonalGrid#columnar is deprecated in favor of HexagonalGrid#columns.";
logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
return this.columns;
}
/**
* @deprecated since v12
* @ignore
*/
set columnar(value) {
const msg = "HexagonalGrid#columnar is deprecated in favor of HexagonalGrid#columns.";
logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
this.columns = value;
}
/* -------------------------------------------- */
/**
* @deprecated since v12
* @ignore
*/
getCenter(x, y) {
const msg = "HexagonalGrid#getCenter is deprecated. Use HexagonalGrid#getCenterPoint instead.";
logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
let [x0, y0] = this.getTopLeft(x, y);
return [x0 + (this.sizeX / 2), y0 + (this.sizeY / 2)];
}
/* -------------------------------------------- */
/**
* @deprecated since v12
* @ignore
*/
getSnappedPosition(x, y, interval=1, {token}={}) {
const msg = "HexagonalGrid#getSnappedPosition is deprecated. Use HexagonalGrid#getSnappedPoint instead.";
logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
if ( interval === 0 ) return {x: Math.round(x), y: Math.round(y)};
// At precision 5, return the center or nearest vertex
if ( interval === 5) {
const w4 = this.w / 4;
const h4 = this.h / 4;
// Distance relative to center
let [xc, yc] = this.getCenter(x, y);
let dx = x - xc;
let dy = y - yc;
let ox = dx.between(-w4, w4) ? 0 : Math.sign(dx);
let oy = dy.between(-h4, h4) ? 0 : Math.sign(dy);
// Closest to the center
if ( (ox === 0) && (oy === 0) ) return {x: xc, y: yc};
// Closest vertex based on offset
if ( this.columns && (ox === 0) ) ox = Math.sign(dx) ?? -1;
if ( !this.columns && (oy === 0) ) oy = Math.sign(dy) ?? -1;
const {x: x0, y: y0 } = this.#getClosestVertex(xc, yc, ox, oy);
return {x: Math.round(x0), y: Math.round(y0)};
}
// Start with the closest top-left grid position
if ( token ) {
if ( this.columns && (token.document.height > 1) ) y += this.sizeY / 2;
if ( !this.columns && (token.document.width > 1) ) x += this.sizeX / 2;
}
const options = {
columns: this.columns,
even: this.even,
size: this.size,
width: this.sizeX,
height: this.sizeY
};
const offset = HexagonalGrid.pixelsToOffset({x, y}, options, "round");
const point = HexagonalGrid.offsetToPixels(offset, options);
// Adjust pixel coordinate for token size
let x0 = point.x;
let y0 = point.y;
if ( token ) [x0, y0] = this._adjustSnapForTokenSize(x0, y0, token);
// Snap directly at interval 1
if ( interval === 1 ) return {x: x0, y: y0};
// Round the remainder
const dx = (x - x0).toNearest(this.w / interval);
const dy = (y - y0).toNearest(this.h / interval);
return {x: Math.round(x0 + dx), y: Math.round(y0 + dy)};
}
/* -------------------------------------------- */
/**
* @deprecated since v12
* @ignore
*/
#getClosestVertex(xc, yc, ox, oy) {
const b = ox + (oy << 2); // Bit shift to make a unique reference
const vertices = this.columns
? {"-1": 0, "-5": 1, "-3": 2, 1: 3, 5: 4, 3: 5} // Flat hex vertices
: {"-5": 0, "-4": 1, "-3": 2, 5: 3, 4: 4, 3: 5}; // Pointy hex vertices
const idx = vertices[b];
const pt = this.hexPoints[idx];
return {
x: (xc - (this.sizeX / 2)) + (pt[0] * this.sizeX),
y: (yc - (this.sizeY / 2)) + (pt[1] * this.sizeY)
};
}
/* -------------------------------------------- */
/**
* @deprecated since v12
* @ignore
*/
#measureDistance(p0, p1) {
const [i0, j0] = this.getGridPositionFromPixels(p0.x, p0.y);
const [i1, j1] = this.getGridPositionFromPixels(p1.x, p1.y);
const c0 = this.getCube({i: i0, j: j0});
const c1 = this.getCube({i: i1, j: j1});
return HexagonalGrid.cubeDistance(c0, c1);
}
/* -------------------------------------------- */
/**
* @deprecated since v12
* @ignore
*/
getGridPositionFromPixels(x, y) {
const msg = "HexagonalGrid#getGridPositionFromPixels is deprecated. This function is based on the \"brick wall\" grid. "
+ " For getting the offset coordinates of the hex containing the given point use HexagonalGrid#getOffset.";
logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
let {row, col} = HexagonalGrid.pixelsToOffset({x, y}, {
columns: this.columns,
even: this.even,
size: this.size,
width: this.sizeX,
height: this.sizeY
});
return [row, col];
}
/* -------------------------------------------- */
/**
* @deprecated since v12
* @ignore
*/
getPixelsFromGridPosition(row, col) {
const msg = "HexagonalGrid#getPixelsFromGridPosition is deprecated. This function is based on the \"brick wall\" grid. "
+ " For getting the top-left coordinates of the hex at the given offset coordinates use HexagonalGrid#getTopLeftPoint.";
logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
const {x, y} = HexagonalGrid.offsetToPixels({row, col}, {
columns: this.columns,
even: this.even,
size: this.size,
width: this.sizeX,
height: this.sizeY
});
return [Math.ceil(x), Math.ceil(y)];
}
/* -------------------------------------------- */
/**
* @deprecated since v12
* @ignore
*/
shiftPosition(x, y, dx, dy, {token}={}) {
const msg = "BaseGrid#shiftPosition is deprecated. Use BaseGrid#getShiftedPoint instead.";
logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
let [row, col] = this.getGridPositionFromPixels(x, y);
// Adjust diagonal moves for offset
let isDiagonal = (dx !== 0) && (dy !== 0);
if ( isDiagonal ) {
// Column orientation
if ( this.columns ) {
let isEven = ((col+1) % 2 === 0) === this.even;
if ( isEven && (dy > 0)) dy--;
else if ( !isEven && (dy < 0)) dy++;
}
// Row orientation
else {
let isEven = ((row + 1) % 2 === 0) === this.even;
if ( isEven && (dx > 0) ) dx--;
else if ( !isEven && (dx < 0 ) ) dx++;
}
}
const [shiftX, shiftY] = this.getPixelsFromGridPosition(row+dy, col+dx);
if ( token ) return this._adjustSnapForTokenSize(shiftX, shiftY, token);
return [shiftX, shiftY];
}
/* -------------------------------------------- */
/**
* @deprecated since v12
* @ignore
*/
measureDistances(segments, options={}) {
const msg = "HexagonalGrid#measureDistances is deprecated. "
+ "Use BaseGrid#measurePath instead for non-Euclidean measurements.";
logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
if ( !options.gridSpaces ) return super.measureDistances(segments, options);
return segments.map(s => {
let r = s.ray;
return this.#measureDistance(r.A, r.B) * this.distance;
});
}
/* -------------------------------------------- */
/**
* @deprecated since v12
* @ignore
*/
_adjustPositionForTokenSize(row, col, token) {
const msg = "HexagonalGrid#_adjustPositionForTokenSize is deprecated.";
logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
if ( this.columns && (token.document.height > 1) ) row++;
if ( !this.columns && (token.document.width > 1) ) col++;
return [row, col];
}
/* -------------------------------------------- */
/**
* @deprecated since v12
* @ignore
*/
static getConfig(type, size) {
const msg = "HexagonalGrid.getConfig is deprecated without replacement.";
logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
const config = {
columns: [GRID_TYPES.HEXODDQ, GRID_TYPES.HEXEVENQ].includes(type),
even: [GRID_TYPES.HEXEVENR, GRID_TYPES.HEXEVENQ].includes(type),
size: size
};
const {width, height} = HexagonalGrid.computeDimensions(config);
config.width = width;
config.height = height;
return config;
}
/* -------------------------------------------- */
/**
* @deprecated since v12
* @ignore
*/
static offsetToCube({row, col}={}, {columns=true, even=false}={}) {
const msg = "HexagonalGrid.offsetToCube is deprecated. Use HexagonalGrid#offsetToCube instead.";
logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
return new HexagonalGrid({size: 100, columns, even}).offsetToCube({i: row, j: col});
}
/* -------------------------------------------- */
/**
* @deprecated since v12
* @ignore
*/
static cubeToOffset(cube={}, {columns=true, even=false}={}) {
const msg = "HexagonalGrid.cubeToOffset is deprecated. Use HexagonalGrid#cubeToOffset instead.";
logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
const {i: row, j: col} = new HexagonalGrid({size: 100, columns, even}).cubeToOffset(cube);
return {row, col};
}
/* -------------------------------------------- */
/**
* @deprecated since v12
* @ignore
*/
static pixelToCube({x, y}={}, config) {
const msg = "HexagonalGrid.pixelToCube is deprecated. Use HexagonalGrid#pointToCube instead.";
logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
const {size} = config;
const cx = x / (size / 2);
const cy = y / (size / 2);
// Fractional hex coordinates, might not satisfy (fx + fy + fz = 0) due to rounding
const fr = (2/3) * cx;
const fq = ((-1/3) * cx) + ((1 / Math.sqrt(3)) * cy);
const fs = ((-1/3) * cx) - ((1 / Math.sqrt(3)) * cy);
// Convert to integer triangle coordinates
const a = Math.ceil(fr - fq);
const b = Math.ceil(fq - fs);
const c = Math.ceil(fs - fr);
// Convert back to cube coordinates
return {
q: Math.round((a - c) / 3),
r: Math.round((c - b) / 3),
s: Math.round((b - a) / 3)
};
}
/* -------------------------------------------- */
/**
* @deprecated since v12
* @ignore
*/
static offsetToPixels({row, col}, {columns, even, width, height}) {
const msg = "HexagonalGrid.offsetToPixels is deprecated. Use HexagonalGrid#getTopLeftPoint instead.";
logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
let x;
let y;
// Flat-topped hexes
if ( columns ) {
x = Math.ceil(col * (width * 0.75));
const isEven = (col + 1) % 2 === 0;
y = Math.ceil((row - (even === isEven ? 0.5 : 0)) * height);
}
// Pointy-topped hexes
else {
y = Math.ceil(row * (height * 0.75));
const isEven = (row + 1) % 2 === 0;
x = Math.ceil((col - (even === isEven ? 0.5 : 0)) * width);
}
// Return the pixel coordinate
return {x, y};
}
/* -------------------------------------------- */
/**
* @deprecated since v12
* @ignore
*/
static pixelsToOffset({x, y}, config, method="floor") {
const msg = "HexagonalGrid.pixelsToOffset is deprecated without replacement. This function is based on the \"brick wall\" grid. "
+ " For getting the offset coordinates of the hex containing the given point use HexagonalGrid#getOffset.";
logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
const {columns, even, width, height} = config;
const fn = Math[method];
let row;
let col;
// Columnar orientation
if ( columns ) {
col = fn(x / (width * 0.75));
const isEven = (col + 1) % 2 === 0;
row = fn((y / height) + (even === isEven ? 0.5 : 0));
}
// Row orientation
else {
row = fn(y / (height * 0.75));
const isEven = (row + 1) % 2 === 0;
col = fn((x / width) + (even === isEven ? 0.5 : 0));
}
return {row, col};
}
/* -------------------------------------------- */
/**
* @deprecated since v12
* @ignore
*/
getAStarPath(start, goal, options) {
const msg = "HexagonalGrid#getAStarPath is deprecated without replacement.";
logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
const costs = new Map();
// Create a prioritized frontier sorted by increasing cost
const frontier = [];
const explore = (hex, from, cost) => {
const idx = frontier.findIndex(l => l.cost > cost);
if ( idx === -1 ) frontier.push({hex, cost, from});
else frontier.splice(idx, 0, {hex, cost, from});
costs.set(hex, cost);
};
explore(start, null, 0);
// Expand the frontier, exploring towards the goal
let current;
let solution;
while ( frontier.length ) {
current = frontier.shift();
if ( current.cost === Infinity ) break;
if ( current.hex.equals(goal) ) {
solution = current;
break;
}
for ( const next of current.hex.getNeighbors() ) {
const deltaCost = next.getTravelCost instanceof Function ? next.getTravelCost(current.hex, options) : 1;
const newCost = current.cost + deltaCost; // Total cost of reaching this hex
if ( costs.get(next) <= newCost ) continue; // We already made it here in the lowest-cost way
explore(next, current, newCost);
}
}
// Ensure a path was achieved
if ( !solution ) {
throw new Error("No valid path between these positions exists");
}
// Return the optimal path and cost
const path = [];
let c = solution;
while ( c.from ) {
path.unshift(c.hex);
c = c.from;
}
return {from: start, to: goal, cost: solution.cost, path};
}
}