x5-geometry
Version:
Geometry and word processing utilities for XNet
250 lines (249 loc) • 11.4 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.getHexSubdivisionIndices = getHexSubdivisionIndices;
exports.hexIndexToIndexArray = hexIndexToIndexArray;
exports.hexIndexToVertices = hexIndexToVertices;
exports.reorderVerticies = reorderVerticies;
exports.subdivideHexToTriangles = subdivideHexToTriangles;
exports.subdivideTriangle = subdivideTriangle;
exports.isPointInsideTriangle = isPointInsideTriangle;
exports.sign = sign;
/**
* Tests if a point is inside a hexagon and returns the subdivision indices.
* @param point - The point to test
* @param hexVertices - Six vertices of the hex in right-hand rule order
* @returns List of indices (e.g., [0, 1, 7, 31]) if inside, false if outside
*/
function getHexSubdivisionIndices(point, hexVertices) {
// Validate input
if (!point || typeof point.x !== 'number' || typeof point.y !== 'number') {
throw new Error('Point must have numeric x and y coordinates.');
}
if (!Array.isArray(hexVertices) || hexVertices.length !== 6) {
throw new Error('Expected exactly 6 vertices for the hexagon.');
}
for (const vertex of hexVertices) {
if (typeof vertex.x !== 'number' || typeof vertex.y !== 'number') {
throw new Error('Each vertex must have numeric x and y coordinates.');
}
}
// Reorder hex vertices to ensure the first vertex is the rightmost one
// This is important for consistent triangle subdivision indexing
hexVertices = reorderVerticies(hexVertices);
// Step 1: Compute the centroid and subdivide the hex into 6 triangles
const { vertices: level1Vertices, triangles: level1Triangles } = subdivideHexToTriangles(hexVertices);
// Step 2: Find which first-level triangle contains the point
let level1Index = -1;
for (let i = 0; i < level1Triangles.length; i++) {
const [v0, v1, v2] = level1Triangles[i].map(idx => level1Vertices[idx]);
if (isPointInsideTriangle(point, v0, v1, v2, true)) {
level1Index = i + 1; // Map to indices 1–6
break;
}
}
// If the point is outside all triangles, it's outside the hex
if (level1Index === -1) {
return false;
}
// Step 3: Subdivide the containing triangle (Level 2: indices 7–30)
const level1Triangle = level1Triangles[level1Index - 1].map(idx => level1Vertices[idx]);
const { vertices: level2Vertices, triangles: level2Triangles } = subdivideTriangle(level1Triangle);
// Find which second-level triangle contains the point
let level2Index = -1;
let level2Triangle = [];
for (let i = 0; i < level2Triangles.length; i++) {
const [v0, v1, v2] = level2Triangles[i].map(idx => level2Vertices[idx]);
if (isPointInsideTriangle(point, v0, v1, v2, true)) {
level2Index = 7 + (level1Index - 1) * 4 + i; // Map to indices 7–30
level2Triangle = [v0, v1, v2];
break;
}
}
// Step 4: Subdivide the second-level triangle (Level 3: indices 31–126)
const { vertices: level3Vertices, triangles: level3Triangles } = subdivideTriangle(level2Triangle);
// Find which third-level triangle contains the point
let level3Index = -1;
for (let i = 0; i < level3Triangles.length; i++) {
const [v0, v1, v2] = level3Triangles[i].map(idx => level3Vertices[idx]);
if (isPointInsideTriangle(point, v0, v1, v2, true)) {
level3Index = 31 + (level2Index - 7) * 4 + i; // Map to indices 31–126
break;
}
}
// Return the full path of indices
return [0, level1Index, level2Index, level3Index];
}
function hexIndexToIndexArray(hexIndex) {
// Convert hex index to index array
// The hexagon is divided into 6 triangles, each triangle is subdivided into 4 smaller triangles
// The first level has 6 triangles, the second level has 24 triangles (4 for each of the 6 triangles),
// and the third level has 96 triangles (4 for each of the 24 triangles)
// The hex index is 0–126, and the index array is 0–5 for the first level,
// 7–30 for the second level, and 31–126 for the third level
// Check if the hexIndex is valid
if (hexIndex < 0 || hexIndex > 126) {
throw new Error('Hex index must be between 0 and 126.');
}
if (hexIndex === 0) {
return [0];
}
if (hexIndex < 7) {
const level1Index = hexIndex - 1; // Map to indices 0–5
return [0, level1Index + 1];
}
if (hexIndex < 31) {
const level2Index = hexIndex - 7; // Map to indices 0–24
const level1Index = Math.floor(level2Index / 4); // Map to indices 0–5
return [0, level1Index + 1, level2Index + 7];
}
const level3Index = hexIndex - 31; // Map to indices 0–96
const level2Index = Math.floor(level3Index / 4); // Map to indices 0–24
const level1Index = Math.floor(level2Index / 4); // Map to indices 0–5
return [0, level1Index + 1, level2Index + 7, level3Index + 31];
}
function hexIndexToVertices(hexIndex, hexVertices) {
// Convert hex index to hex or triangle vertices
// The hexagon is divided into 6 triangles, each triangle is subdivided into 4 smaller triangles
// The first level has 6 triangles, the second level has 24 triangles (4 for each of the 6 triangles),
// and the third level has 96 triangles (4 for each of the 24 triangles)
// Check if the hexIndex is valid
if (hexIndex < 0 || hexIndex > 126) {
throw new Error('Hex index must be between 0 and 126.');
}
// Check if the hexVertices are valid
if (!Array.isArray(hexVertices) || hexVertices.length !== 6) {
throw new Error('Expected exactly 6 vertices for the hexagon.');
}
for (const vertex of hexVertices) {
if (typeof vertex.x !== 'number' || typeof vertex.y !== 'number') {
throw new Error('Each vertex must have numeric x and y coordinates.');
}
}
// Reorder hex vertices to ensure the first vertex is the rightmost one
// This is important for consistent triangle subdivision indexing
// This is done in the getHexSubdivisionIndices function
hexVertices = reorderVerticies(hexVertices);
const indexArray = hexIndexToIndexArray(hexIndex);
// Step 1: Compute the centroid and subdivide the hex into 6 triangles
const { vertices: level1Vertices, triangles: level1Triangles } = subdivideHexToTriangles(hexVertices);
// Step 2: Find which first-level triangle contains the hexIndex
if (indexArray.length === 1) {
return hexVertices;
}
const level1Index = indexArray[1] - 1; // Map to indices 0–5
const level1Triangle = level1Triangles[level1Index].map(idx => level1Vertices[idx]);
if (indexArray.length === 2) {
return level1Triangle;
}
const level2Index = indexArray[2] - 7; // Map to indices 0–24
const level2IndexInTriangle = level2Index % 4; // Map to indices 0–3
const { vertices: level2Vertices, triangles: level2Triangles } = subdivideTriangle(level1Triangle);
const level2Triangle = level2Triangles[level2IndexInTriangle].map(idx => level2Vertices[idx]);
if (indexArray.length === 3) {
return level2Triangle;
}
const level3Index = indexArray[3] - 31; // Map to indices 0–96
const level3IndexInTriangle = level3Index % 4; // Map to indices 0–3
const { vertices: level3Vertices, triangles: level3Triangles } = subdivideTriangle(level2Triangle);
const level3Triangle = level3Triangles[level3IndexInTriangle].map(idx => level3Vertices[idx]);
return level3Triangle;
}
function reorderVerticies(hexVertices) {
// function to reorder the vertices of a polygon to ensure that the first vertex is the
// righmost one. If there are two vertices with the same x, the one with the
// lowest y is the first one.
let rightLower = hexVertices[0];
let indexj = 0;
for (let i = 1; i < hexVertices.length; i++) {
if (hexVertices[i].x > rightLower.x || (hexVertices[i].x === rightLower.x && hexVertices[i].y < rightLower.y)) {
rightLower = hexVertices[i];
indexj = i;
}
}
// reorder the hex vertices
const newHexVertices = [];
for (let i = 0; i < hexVertices.length; i++) {
newHexVertices.push(hexVertices[(indexj + i) % hexVertices.length]);
}
return newHexVertices;
}
/**
* Subdivides a hexagon into 6 triangles by connecting the centroid to each vertex.
* @param hexVertices - Six vertices in right-hand rule order.
* @returns Object containing vertices and triangles
*/
function subdivideHexToTriangles(hexVertices) {
// Compute the centroid
const centroid = { x: 0, y: 0 };
for (const vertex of hexVertices) {
centroid.x += vertex.x;
centroid.y += vertex.y;
}
centroid.x /= 6;
centroid.y /= 6;
// Create the new vertex list (original 6 + centroid)
const vertices = [...hexVertices, centroid];
// Generate triangle indices in right-hand rule order
const triangles = [];
for (let i = 0; i < 6; i++) {
const nextVertex = (i + 1) % 6;
triangles.push([i, nextVertex, 6]);
}
return { vertices, triangles };
}
/**
* Subdivides a triangle into 4 smaller triangles.
* @param triangleVertices - Three vertices of the triangle.
* @returns Object containing vertices and triangles
*/
function subdivideTriangle(triangleVertices) {
// reorder the vertices to ensure that the first vertex is the rightmost one
triangleVertices = reorderVerticies(triangleVertices);
// Compute midpoints of each edge
const mid01 = {
x: (triangleVertices[0].x + triangleVertices[1].x) / 2,
y: (triangleVertices[0].y + triangleVertices[1].y) / 2
};
const mid12 = {
x: (triangleVertices[1].x + triangleVertices[2].x) / 2,
y: (triangleVertices[1].y + triangleVertices[2].y) / 2
};
const mid20 = {
x: (triangleVertices[2].x + triangleVertices[0].x) / 2,
y: (triangleVertices[2].y + triangleVertices[0].y) / 2
};
// New vertex list: original 3 + 3 midpoints
const vertices = [...triangleVertices, mid01, mid12, mid20];
// Define the 4 sub-triangles
// Indices: 0, 1, 2 are the original vertices; 3, 4, 5 are mid01, mid12, mid20
const triangles = [
[3, 4, 5], // Central triangle
[0, 3, 5], // Radial triangle 1
[3, 1, 4], // Radial triangle 2
[5, 4, 2] // Radial triangle 3
];
return { vertices, triangles };
}
/**
* Determines if a point is inside a triangle
* @param point - The point to test
* @param v1 - First vertex of the triangle
* @param v2 - Second vertex of the triangle
* @param v3 - Third vertex of the triangle
* @param includeBoundary - Whether to include points on the boundary
* @returns True if the point is inside the triangle, false otherwise
*/
function isPointInsideTriangle(point, v1, v2, v3, includeBoundary = true) {
const d1 = sign(point, v1, v2);
const d2 = sign(point, v2, v3);
const d3 = sign(point, v3, v1);
const hasNeg = (d1 < 0) || (d2 < 0) || (d3 < 0);
const hasPos = (d1 > 0) || (d2 > 0) || (d3 > 0);
if (includeBoundary) {
return !(hasNeg && hasPos);
}
return !(hasNeg || hasPos);
}
function sign(p1, p2, p3) {
return (p1.x - p3.x) * (p2.y - p3.y) - (p2.x - p3.x) * (p1.y - p3.y);
}