mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2026-02-04 17:41:23 +01:00
300 lines
No EOL
12 KiB
TypeScript
300 lines
No EOL
12 KiB
TypeScript
import polylabel from "polylabel";
|
|
import { rn } from "./numberUtils";
|
|
|
|
/**
|
|
* Generates SVG path data for filling a shape defined by a chain of vertices.
|
|
* @param {object} vertices - The vertices object containing positions.
|
|
* @param {number[]} vertexChain - An array of vertex IDs defining the shape.
|
|
* @returns {string} SVG path data for the filled shape.
|
|
*/
|
|
const getFillPath = (vertices: any, vertexChain: number[]) => {
|
|
const points = vertexChain.map(vertexId => vertices.p[vertexId]);
|
|
const firstPoint = points.shift();
|
|
return `M${firstPoint} L${points.join(" ")} Z`;
|
|
}
|
|
|
|
/**
|
|
* Generates SVG path data for borders based on a chain of vertices and a discontinuation condition.
|
|
* @param {object} vertices - The vertices object containing positions.
|
|
* @param {number[]} vertexChain - An array of vertex IDs defining the border.
|
|
* @param {(vertexId: number) => boolean} discontinue - A function that determines if the path should discontinue at a vertex.
|
|
* @returns {string} SVG path data for the border.
|
|
*/
|
|
const getBorderPath = (vertices: any, vertexChain: number[], discontinue: (vertexId: number) => boolean) => {
|
|
let discontinued = true;
|
|
let lastOperation = "";
|
|
const path = vertexChain.map(vertexId => {
|
|
if (discontinue(vertexId)) {
|
|
discontinued = true;
|
|
return "";
|
|
}
|
|
|
|
const operation = discontinued ? "M" : "L";
|
|
discontinued = false;
|
|
lastOperation = operation;
|
|
|
|
const command = operation === "L" && operation === lastOperation ? "" : operation;
|
|
return ` ${command}${vertices.p[vertexId]}`;
|
|
});
|
|
|
|
return path.join("").trim();
|
|
}
|
|
|
|
/**
|
|
* Restores the path from exit to start using the 'from' mapping.
|
|
* @param {number} exit - The ID of the exit cell.
|
|
* @param {number} start - The ID of the starting cell.
|
|
* @param {number[]} from - An array mapping each cell ID to the cell ID it came from.
|
|
* @returns {number[]} An array of cell IDs representing the path from start to exit.
|
|
*/
|
|
const restorePath = (exit: number, start: number, from: number[]) => {
|
|
const pathCells = [];
|
|
|
|
let current = exit;
|
|
let prev = exit;
|
|
|
|
while (current !== start) {
|
|
pathCells.push(current);
|
|
prev = from[current];
|
|
current = prev;
|
|
}
|
|
|
|
pathCells.push(current);
|
|
|
|
return pathCells.reverse();
|
|
}
|
|
|
|
/**
|
|
* Returns isolines (borders) for different types of cells in the graph.
|
|
* @param {object} graph - The graph object containing cells and vertices.
|
|
* @param {(cellId: number) => any} getType - A function that returns the type of a cell given its ID.
|
|
* @param {object} [options] - Options to specify which isoline formats to generate.
|
|
* @param {boolean} [options.polygons=false] - Whether to generate polygons for each type.
|
|
* @param {boolean} [options.fill=false] - Whether to generate fill paths for each type.
|
|
* @param {boolean} [options.halo=false] - Whether to generate halo paths for each type.
|
|
* @param {boolean} [options.waterGap=false] - Whether to generate water gap paths for each type.
|
|
* @returns {object} An object containing isolines for each type based on the specified options.
|
|
*/
|
|
export const getIsolines = (graph: any, getType: (cellId: number) => any, options: {polygons?: boolean, fill?: boolean, halo?: boolean, waterGap?: boolean} = {polygons: false, fill: false, halo: false, waterGap: false}): any => {
|
|
const {cells, vertices} = graph;
|
|
const isolines: any = {};
|
|
|
|
const checkedCells = new Uint8Array(cells.i.length);
|
|
const addToChecked = (cellId: number) => (checkedCells[cellId] = 1);
|
|
const isChecked = (cellId: number) => checkedCells[cellId] === 1;
|
|
|
|
for (const cellId of cells.i) {
|
|
if (isChecked(cellId) || !getType(cellId)) continue;
|
|
addToChecked(cellId);
|
|
|
|
const type = getType(cellId);
|
|
const ofSameType = (cellId: number) => getType(cellId) === type;
|
|
const ofDifferentType = (cellId: number) => getType(cellId) !== type;
|
|
|
|
const onborderCell = cells.c[cellId].find(ofDifferentType);
|
|
if (onborderCell === undefined) continue;
|
|
|
|
// check if inner lake. Note there is no shoreline for grid features
|
|
const feature = graph.features[cells.f[onborderCell]];
|
|
if (feature.type === "lake" && feature.shoreline?.every(ofSameType)) continue;
|
|
|
|
const startingVertex = cells.v[cellId].find((v: number) => vertices.c[v].some(ofDifferentType));
|
|
if (startingVertex === undefined) throw new Error(`Starting vertex for cell ${cellId} is not found`);
|
|
|
|
const vertexChain = connectVertices({vertices, startingVertex, ofSameType, addToChecked, closeRing: true});
|
|
if (vertexChain.length < 3) continue;
|
|
|
|
addIsolineTo(type, vertices, vertexChain, isolines, options);
|
|
}
|
|
|
|
return isolines;
|
|
|
|
function addIsolineTo(type: any, vertices: any, vertexChain: number[], isolines: any, options: any) {
|
|
if (!isolines[type]) isolines[type] = {};
|
|
|
|
if (options.polygons) {
|
|
if (!isolines[type].polygons) isolines[type].polygons = [];
|
|
isolines[type].polygons.push(vertexChain.map(vertexId => vertices.p[vertexId]));
|
|
}
|
|
|
|
if (options.fill) {
|
|
if (!isolines[type].fill) isolines[type].fill = "";
|
|
isolines[type].fill += getFillPath(vertices, vertexChain);
|
|
}
|
|
|
|
if (options.waterGap) {
|
|
if (!isolines[type].waterGap) isolines[type].waterGap = "";
|
|
const isLandVertex = (vertexId: number) => vertices.c[vertexId].every((i: number) => cells.h[i] >= 20);
|
|
isolines[type].waterGap += getBorderPath(vertices, vertexChain, isLandVertex);
|
|
}
|
|
|
|
if (options.halo) {
|
|
if (!isolines[type].halo) isolines[type].halo = "";
|
|
const isBorderVertex = (vertexId: number) => vertices.c[vertexId].some((i: number) => cells.b[i]);
|
|
isolines[type].halo += getBorderPath(vertices, vertexChain, isBorderVertex);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Generates SVG path data for the border of a shape defined by a chain of vertices.
|
|
* @param {number[]} cellsArray - An array of cell IDs defining the shape.
|
|
* @param {object} packedGraph - The packed graph object containing cells and vertices.
|
|
* @returns {string} SVG path data for the border of the shape.
|
|
*/
|
|
export const getVertexPath = (cellsArray: number[], packedGraph: any = {}) => {
|
|
const {cells, vertices} = packedGraph;
|
|
|
|
const cellsObj = Object.fromEntries(cellsArray.map(cellId => [cellId, true]));
|
|
const ofSameType = (cellId: number) => cellsObj[cellId];
|
|
const ofDifferentType = (cellId: number) => !cellsObj[cellId];
|
|
|
|
const checkedCells = new Uint8Array(cells.c.length);
|
|
const addToChecked = (cellId: number) => (checkedCells[cellId] = 1);
|
|
const isChecked = (cellId: number) => checkedCells[cellId] === 1;
|
|
let path = "";
|
|
|
|
for (const cellId of cellsArray) {
|
|
if (isChecked(cellId)) continue;
|
|
|
|
const onborderCell = cells.c[cellId].find(ofDifferentType);
|
|
if (onborderCell === undefined) continue;
|
|
|
|
const feature = packedGraph.features[cells.f[onborderCell]];
|
|
if (feature.type === "lake" && feature.shoreline) {
|
|
if (feature.shoreline.every(ofSameType)) continue; // inner lake
|
|
}
|
|
|
|
const startingVertex = cells.v[cellId].find((v: number) => vertices.c[v].some(ofDifferentType));
|
|
if (startingVertex === undefined) throw new Error(`Starting vertex for cell ${cellId} is not found`);
|
|
|
|
const vertexChain = connectVertices({vertices, startingVertex, ofSameType, addToChecked, closeRing: true});
|
|
if (vertexChain.length < 3) continue;
|
|
|
|
path += getFillPath(vertices, vertexChain);
|
|
}
|
|
|
|
return path;
|
|
}
|
|
|
|
/**
|
|
* Finds the poles of inaccessibility for each type of cell in the graph.
|
|
* @param {object} graph - The graph object containing cells and vertices.
|
|
* @param {(cellId: number) => any} getType - A function that returns the type of a cell given its ID.
|
|
* @returns {object} An object mapping each type to its pole of inaccessibility coordinates [x, y].
|
|
*/
|
|
export const getPolesOfInaccessibility = (graph: any, getType: (cellId: number) => any) => {
|
|
const isolines = getIsolines(graph, getType, {polygons: true});
|
|
|
|
const poles = Object.entries(isolines).map(([id, isoline]) => {
|
|
const multiPolygon = (isoline as any).polygons.sort((a: any, b: any) => b.length - a.length);
|
|
const [x, y] = polylabel(multiPolygon, 20);
|
|
return [id, [rn(x), rn(y)]];
|
|
});
|
|
|
|
return Object.fromEntries(poles);
|
|
}
|
|
|
|
/**
|
|
* Connects vertices to form a closed path based on cell type.
|
|
* @param {object} options - Options for connecting vertices.
|
|
* @param {object} options.vertices - The vertices object containing connections.
|
|
* @param {number} options.startingVertex - The ID of the starting vertex.
|
|
* @param {(cellId: number) => boolean} options.ofSameType - A function that checks if a cell is of the same type.
|
|
* @param {(cellId: number) => void} [options.addToChecked] - A function to mark cells as checked.
|
|
* @param {boolean} [options.closeRing=false] - Whether to close the path into a ring.
|
|
* @returns {number[]} An array of vertex IDs forming the connected path.
|
|
*/
|
|
export const connectVertices = ({vertices, startingVertex, ofSameType, addToChecked, closeRing}: {vertices: any, startingVertex: number, ofSameType: (cellId: number) => boolean, addToChecked?: (cellId: number) => void, closeRing?: boolean}) => {
|
|
const MAX_ITERATIONS = vertices.c.length;
|
|
const chain = []; // vertices chain to form a path
|
|
|
|
let next = startingVertex;
|
|
for (let i = 0; i === 0 || next !== startingVertex; i++) {
|
|
const previous = chain.at(-1);
|
|
const current = next;
|
|
chain.push(current);
|
|
|
|
const neibCells = vertices.c[current];
|
|
if (addToChecked) neibCells.filter(ofSameType).forEach(addToChecked);
|
|
|
|
const [c1, c2, c3] = neibCells.map(ofSameType);
|
|
const [v1, v2, v3] = vertices.v[current];
|
|
|
|
if (v1 !== previous && c1 !== c2) next = v1;
|
|
else if (v2 !== previous && c2 !== c3) next = v2;
|
|
else if (v3 !== previous && c1 !== c3) next = v3;
|
|
|
|
if (next >= vertices.c.length) {
|
|
window.ERROR && console.error("ConnectVertices: next vertex is out of bounds");
|
|
break;
|
|
}
|
|
|
|
if (next === current) {
|
|
window.ERROR && console.error("ConnectVertices: next vertex is not found");
|
|
break;
|
|
}
|
|
|
|
if (i === MAX_ITERATIONS) {
|
|
window.ERROR && console.error("ConnectVertices: max iterations reached", MAX_ITERATIONS);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (closeRing) chain.push(startingVertex);
|
|
return chain;
|
|
}
|
|
|
|
/**
|
|
* Finds the shortest path between two cells using a cost-based pathfinding algorithm.
|
|
* @param {number} start - The ID of the starting cell.
|
|
* @param {(id: number) => boolean} isExit - A function that returns true if the cell is the exit cell.
|
|
* @param {(current: number, next: number) => number} getCost - A function that returns the path cost from current cell to the next cell. Must return `Infinity` for impassable connections.
|
|
* @param {object} packedGraph - The packed graph object containing cells and their connections.
|
|
* @returns {number[] | null} An array of cell IDs of the path from start to exit, or null if no path is found or start and exit are the same.
|
|
*/
|
|
export const findPath = (start: number, isExit: (id: number) => boolean, getCost: (current: number, next: number) => number, packedGraph: any = {}): number[] | null => {
|
|
if (isExit(start)) return null;
|
|
|
|
const from = [];
|
|
const cost = [];
|
|
const queue = new window.FlatQueue();
|
|
queue.push(start, 0);
|
|
|
|
while (queue.length) {
|
|
const currentCost = queue.peekValue();
|
|
const current = queue.pop();
|
|
|
|
for (const next of packedGraph.cells.c[current]) {
|
|
if (isExit(next)) {
|
|
from[next] = current;
|
|
return restorePath(next, start, from);
|
|
}
|
|
|
|
const nextCost = getCost(current, next);
|
|
if (nextCost === Infinity) continue; // impassable cell
|
|
const totalCost = currentCost + nextCost;
|
|
|
|
if (totalCost >= cost[next]) continue; // has cheaper path
|
|
from[next] = current;
|
|
cost[next] = totalCost;
|
|
queue.push(next, totalCost);
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
declare global {
|
|
interface Window {
|
|
ERROR: boolean;
|
|
FlatQueue: any;
|
|
|
|
getIsolines: typeof getIsolines;
|
|
getPolesOfInaccessibility: typeof getPolesOfInaccessibility;
|
|
connectVertices: typeof connectVertices;
|
|
findPath: typeof findPath;
|
|
getVertexPath: typeof getVertexPath;
|
|
}
|
|
} |