refactor: features - define distance fields

This commit is contained in:
Azgaar 2024-09-06 15:38:53 +02:00
parent b5fede560b
commit 6d9c86ba74

View file

@ -1,141 +1,258 @@
"use strict"; "use strict";
window.Features = (function () { window.Features = (function () {
// calculate cell-distance_to_coast for each cell const DEEPER_LAND = 3;
function markup(cells, start, increment, limit) { const LANDLOCKED = 2;
for (let t = start, count = Infinity; count > 0 && t > limit; t += increment) { const LAND_COAST = 1;
count = 0; const UNMARKED = 0;
const prevT = t - increment; const WATER_COAST = -1;
for (let i = 0; i < cells.i.length; i++) { const DEEP_WATER = -2;
if (cells.t[i] !== prevT) continue;
for (const c of cells.c[i]) { // calculate distance to coast for every cell
if (cells.t[c]) continue; function markup({distanceField, neighbors, start, increment, limit = INT8_MAX}) {
cells.t[c] = t; for (let distance = start, marked = Infinity; marked > 0 && distance !== limit; distance += increment) {
count++; marked = 0;
const prevDistance = distance - increment;
for (let cellId = 0; cellId < neighbors.length; cellId++) {
if (distanceField[cellId] !== prevDistance) continue;
for (const neighborId of neighbors[cellId]) {
if (distanceField[neighborId] !== UNMARKED) continue;
distanceField[neighborId] = distance;
marked++;
} }
} }
} }
} }
// mark features (ocean, lakes, islands) and calculate distance field // mark Grid features (ocean, lakes, islands) and calculate distance field
function markupGrid() { function markupGrid() {
TIME && console.time("markupGrid"); TIME && console.time("markupGrid");
Math.random = aleaPRNG(seed); // get the same result on heightmap edit in Erase mode Math.random = aleaPRNG(seed); // get the same result on heightmap edit in Erase mode
const cells = grid.cells; const {h: heights, c: neighbors, b: borderCells, i} = grid.cells;
const heights = grid.cells.h; const cellsNumber = i.length;
cells.f = new Uint16Array(cells.i.length); // cell feature number const distanceField = new Int8Array(cellsNumber); // gird.cells.t
cells.t = new Int8Array(cells.i.length); // cell type: 1 = land coast; -1 = water near coast const featureIds = new Uint16Array(cellsNumber); // gird.cells.f
grid.features = [0]; const features = [0];
for (let i = 1, queue = [0]; queue[0] !== -1; i++) { const queue = [0];
cells.f[queue[0]] = i; // feature number for (let featureId = 1; queue[0] !== -1; featureId++) {
const land = heights[queue[0]] >= 20; const firstCell = queue[0];
let border = false; // true if feature touches map border featureIds[firstCell] = featureId;
const land = heights[firstCell] >= 20;
let border = false; // set true if feature touches map edge
while (queue.length) { while (queue.length) {
const q = queue.pop(); const cellId = queue.pop();
if (cells.b[q]) border = true; if (borderCells[cellId]) border = true;
cells.c[q].forEach(c => { for (const neighborId of neighbors[cellId]) {
const cLand = heights[c] >= 20; const isNeibLand = heights[neighborId] >= 20;
if (land === cLand && !cells.f[c]) {
cells.f[c] = i; if (land === isNeibLand && featureIds[neighborId] === UNMARKED) {
queue.push(c); featureIds[neighborId] = featureId;
} else if (land && !cLand) { queue.push(neighborId);
cells.t[q] = 1; } else if (land && !isNeibLand) {
cells.t[c] = -1; distanceField[cellId] = LAND_COAST;
distanceField[neighborId] = WATER_COAST;
} }
}); }
} }
const type = land ? "island" : border ? "ocean" : "lake";
grid.features.push({i, land, border, type});
queue[0] = cells.f.findIndex(f => !f); // find unmarked cell const type = land ? "island" : border ? "ocean" : "lake";
features.push({i: featureId, land, border, type});
queue[0] = featureIds.findIndex(f => f === UNMARKED); // find unmarked cell
} }
markup(grid.cells, -2, -1, -10); // markup grid water // markup deep ocean cells
markup({distanceField, neighbors, start: DEEP_WATER, increment: -1, limit: -10});
grid.cells.t = distanceField;
grid.cells.f = featureIds;
grid.features = features;
TIME && console.timeEnd("markupGrid"); TIME && console.timeEnd("markupGrid");
} }
// define Pack features (oceans, lakes, islands) add related details // mark Pack features (ocean, lakes, islands), calculate distance field and add properties
function markupPack() { function markupPack() {
TIME && console.time("markupPack"); TIME && console.time("markupPack");
const {cells} = pack;
const gridCellsNumber = grid.cells.i.length;
const OCEAN_MIN_SIZE = gridCellsNumber / 25;
const SEA_MIN_SIZE = gridCellsNumber / 1000;
const CONTINENT_MIN_SIZE = gridCellsNumber / 10;
const ISLAND_MIN_SIZE = gridCellsNumber / 1000;
const {h: heights, c: neighbors, b: borderCells, i} = pack.cells;
const cellsNumber = i.length;
if (!cellsNumber) return; // no cells -> there is nothing to do
const distanceField = new Int8Array(cellsNumber); // pack.cells.t
const featureIds = new Uint16Array(cellsNumber); // pack.cells.f
const haven = createTypedArray({maxValue: cellsNumber, length: cellsNumber}); // haven: opposite water cell
const harbor = new Uint8Array(cellsNumber); // harbor: number of adjacent water cells
const features = [0]; const features = [0];
cells.f = new Uint16Array(cells.i.length); // cell feature number const defineHaven = cellId => {
cells.t = new Int8Array(cells.i.length); // cell type: 1 = land along coast; -1 = water along coast; const waterCells = neighbors[cellId].filter(isWater);
cells.haven = cells.i.length < 65535 ? new Uint16Array(cells.i.length) : new Uint32Array(cells.i.length); // cell haven (opposite water cell); const distances = waterCells.map(c => dist2(cells.p[cellId], cells.p[c]));
cells.harbor = new Uint8Array(cells.i.length); // cell harbor (number of adjacent water cells); const closest = distances.indexOf(Math.min.apply(Math, distances));
if (!cells.i.length) return; // no cells -> there is nothing to do haven[cellId] = waterCells[closest];
for (let i = 1, queue = [0]; queue[0] !== -1; i++) { harbor[cellId] = waterCells.length;
const start = queue[0]; // first cell };
cells.f[start] = i; // assign feature number
const land = cells.h[start] >= 20; const queue = [0];
for (let featureId = 1; queue[0] !== -1; featureId++) {
const firstCell = queue[0];
featureIds[firstCell] = featureId;
const land = isLand(firstCell);
let border = false; // true if feature touches map border let border = false; // true if feature touches map border
let cellNumber = 1; // to count cells number in a feature let totalCells = 1; // count cells in a feature
while (queue.length) { while (queue.length) {
const q = queue.pop(); const cellId = queue.pop();
if (cells.b[q]) border = true; if (borderCells[cellId]) border = true;
cells.c[q].forEach(function (e) {
const eLand = cells.h[e] >= 20; for (const neighborId of neighbors[cellId]) {
if (land && !eLand) { const isNeibLand = isLand(neighborId);
cells.t[q] = 1;
cells.t[e] = -1; if (land && !isNeibLand) {
if (!cells.haven[q]) defineHaven(q); distanceField[cellId] = LAND_COAST;
} else if (land && eLand) { distanceField[neighborId] = WATER_COAST;
if (!cells.t[e] && cells.t[q] === 1) cells.t[e] = 2; if (!haven[cellId]) defineHaven(cellId);
else if (!cells.t[q] && cells.t[e] === 1) cells.t[q] = 2; } else if (land && isNeibLand) {
if (distanceField[neighborId] === UNMARKED && distanceField[cellId] === LAND_COAST)
distanceField[neighborId] = LANDLOCKED;
else if (distanceField[cellId] === UNMARKED && distanceField[neighborId] === LAND_COAST)
distanceField[cellId] = LANDLOCKED;
} }
if (!cells.f[e] && land === eLand) {
queue.push(e); if (!featureIds[neighborId] && land === isNeibLand) {
cells.f[e] = i; queue.push(neighborId);
cellNumber++; featureIds[neighborId] = featureId;
totalCells++;
} }
}); }
} }
const type = land ? "island" : border ? "ocean" : "lake"; const featureVertices = getFeatureVertices({firstCell, vertices, cells, featureIds, featureId});
let group; const points = clipPoly(featureVertices.map(vertex => vertices.p[vertex]));
if (type === "ocean") group = defineOceanGroup(cellNumber); const area = d3.polygonArea(points); // feature perimiter area
else if (type === "island") group = defineIslandGroup(start, cellNumber); features.push(addFeature({firstCell, land, border, featureVertices, featureId, totalCells, area}));
features.push({i, land, border, type, cells: cellNumber, firstCell: start, group});
queue[0] = cells.f.findIndex(f => !f); // find unmarked cell queue[0] = featureIds.findIndex(f => f === UNMARKED); // find unmarked cell
} }
function defineHaven(i) { markup({distanceField, neighbors, start: DEEPER_LAND, increment: 1}); // markup pack land
const water = cells.c[i].filter(c => cells.h[c] < 20); markup({distanceField, neighbors, start: DEEP_WATER, increment: -1, limit: -10}); // markup pack water
const dist2 = water.map(c => (cells.p[i][0] - cells.p[c][0]) ** 2 + (cells.p[i][1] - cells.p[c][1]) ** 2);
const closest = water[dist2.indexOf(Math.min.apply(Math, dist2))];
cells.haven[i] = closest;
cells.harbor[i] = water.length;
}
function defineOceanGroup(number) {
if (number > grid.cells.i.length / 25) return "ocean";
if (number > grid.cells.i.length / 100) return "sea";
return "gulf";
}
function defineIslandGroup(cell, number) {
if (cell && features[cells.f[cell - 1]].type === "lake") return "lake_island";
if (number > grid.cells.i.length / 10) return "continent";
if (number > grid.cells.i.length / 1000) return "island";
return "isle";
}
pack.cells.t = distanceField;
pack.cells.f = featureIds;
pack.cells.haven = haven;
pack.cells.harbor = harbor;
pack.features = features; pack.features = features;
markup(pack.cells, 3, 1, 0); // markup pack land
markup(pack.cells, -2, -1, -10); // markup pack water
TIME && console.timeEnd("markupPack"); TIME && console.timeEnd("markupPack");
function addFeature({firstCell, land, border, featureVertices, featureId, totalCells, area}) {
const absArea = Math.abs(rn(area));
if (land) return addIsland();
if (border) return addOcean();
return addLake();
function addIsland() {
const group = defineIslandGroup();
const feature = {
i: featureId,
type: "island",
group,
land: true,
border,
cells: totalCells,
firstCell,
vertices: featureVertices,
area: absArea
};
return feature;
}
function addOcean() {
const group = defineOceanGroup();
const feature = {
i: featureId,
type: "ocean",
group,
land: false,
border: false,
cells: totalCells,
firstCell,
vertices: featureVertices,
area: absArea
};
return feature;
}
function addLake() {
const group = "freshwater"; // temp, to be defined later
const name = ""; // temp, to be defined later
// ensure lake ring is clockwise (to form a hole)
const lakeVertices = area > 0 ? featureVertices.reverse() : featureVertices;
const shoreline = getShoreline(); // land cells around lake
const height = getLakeElevation();
function getShoreline() {
const isLand = cellId => heights[cellId] >= MIN_LAND_HEIGHT;
const cellsAround = lakeVertices.map(vertex => vertices.c[vertex].filter(isLand)).flat();
return unique(cellsAround);
}
function getLakeElevation() {
const MIN_ELEVATION_DELTA = 0.1;
const minShoreHeight = d3.min(shoreline.map(cellId => heights[cellId])) || MIN_LAND_HEIGHT;
return rn(minShoreHeight - MIN_ELEVATION_DELTA, 2);
}
const feature = {
i: featureId,
type: "lake",
group,
name,
land: false,
border: false,
cells: totalCells,
firstCell,
vertices: lakeVertices,
shoreline: shoreline,
height,
area: absArea
};
return feature;
}
function defineOceanGroup() {
if (totalCells > OCEAN_MIN_SIZE) return "ocean";
if (totalCells > SEA_MIN_SIZE) return "sea";
return "gulf";
}
function defineIslandGroup() {
const prevFeature = features[featureIds[firstCell - 1]];
if (prevFeature && prevFeature.type === "lake") return "lake_island";
if (totalCells > CONTINENT_MIN_SIZE) return "continent";
if (totalCells > ISLAND_MIN_SIZE) return "island";
return "isle";
}
}
} }
return {markupGrid, markupPack}; return {markupGrid, markupPack};