refactor: Features module start

This commit is contained in:
Azgaar 2024-09-06 14:22:36 +02:00
parent ec236d146b
commit b5fede560b
9 changed files with 315 additions and 160 deletions

View file

@ -8026,6 +8026,7 @@
<script src="config/heightmap-templates.js"></script>
<script src="config/precreated-heightmaps.js"></script>
<script src="modules/heightmap-generator.js?v=1.99.00"></script>
<script src="modules/features.js?v=1.104.0"></script>
<script src="modules/ocean-layers.js?v=1.104.0"></script>
<script src="modules/river-generator.js?v=1.99.05"></script>
<script src="modules/lakes.js?v=1.99.00"></script>
@ -8042,9 +8043,10 @@
<script src="modules/zones-generator.js?v=1.100.00"></script>
<script src="modules/coa-generator.js?v=1.99.00"></script>
<script src="modules/submap.js?v=1.104.0"></script>
<script src="libs/alea.min.js"></script>
<script src="libs/polylabel.min.js"></script>
<script src="libs/lineclip.min.js"></script>
<script src="libs/alea.min.js"></script>
<script src="libs/simplify.js"></script>
<script src="modules/fonts.js?v=1.99.03"></script>
<script src="modules/ui/layers.js?v=1.101.00"></script>
<script src="modules/ui/measurers.js?v=1.99.00"></script>
@ -8102,7 +8104,7 @@
<script defer src="modules/io/cloud.js?v=1.99.00"></script>
<script defer src="modules/io/export.js?v=1.100.00"></script>
<script defer src="modules/renderers/draw-coastline.js?v=1.104.0"></script>
<script defer src="modules/renderers/draw-features.js?v=1.104.0"></script>
<script defer src="modules/renderers/draw-borders.js?v=1.104.0"></script>
<script defer src="modules/renderers/draw-heightmap.js?v=1.104.0"></script>
<script defer src="modules/renderers/draw-markers.js?v=1.104.0"></script>

103
libs/simplify.js Normal file
View file

@ -0,0 +1,103 @@
/*
(c) 2017, Vladimir Agafonkin
Simplify.js, a high-performance JS polyline simplification library
mourner.github.io/simplify-js
*/
{
// square distance between 2 points
function getSqDist([x1, y1], [x2, y2]) {
const dx = x1 - x2;
const dy = y1 - y2;
return dx * dx + dy * dy;
}
// square distance from a point to a segment
function getSqSegDist([x1, y1], [x, y], [x2, y2]) {
let dx = x2 - x;
let dy = y2 - y;
if (dx !== 0 || dy !== 0) {
const t = ((x1 - x) * dx + (y1 - y) * dy) / (dx * dx + dy * dy);
if (t > 1) {
x = x2;
y = y2;
} else if (t > 0) {
x += dx * t;
y += dy * t;
}
}
dx = x1 - x;
dy = y1 - y;
return dx * dx + dy * dy;
}
// rest of the code doesn't care about point format
// basic distance-based simplification
function simplifyRadialDist(points, sqTolerance) {
let prevPoint = points[0];
const newPoints = [prevPoint];
let point;
for (let i = 1, len = points.length; i < len; i++) {
point = points[i];
if (getSqDist(point, prevPoint) > sqTolerance) {
newPoints.push(point);
prevPoint = point;
}
}
if (point && prevPoint !== point) newPoints.push(point);
return newPoints;
}
function simplifyDPStep(points, first, last, sqTolerance, simplified) {
let maxSqDist = sqTolerance;
let index = first;
for (let i = first + 1; i < last; i++) {
const sqDist = getSqSegDist(points[i], points[first], points[last]);
if (sqDist > maxSqDist) {
index = i;
maxSqDist = sqDist;
}
}
if (maxSqDist > sqTolerance) {
if (index - first > 1) simplifyDPStep(points, first, index, sqTolerance, simplified);
simplified.push(points[index]);
if (last - index > 1) simplifyDPStep(points, index, last, sqTolerance, simplified);
}
}
// simplification using Ramer-Douglas-Peucker algorithm
function simplifyDouglasPeucker(points, sqTolerance) {
const last = points.length - 1;
const simplified = [points[0]];
simplifyDPStep(points, 0, last, sqTolerance, simplified);
simplified.push(points[last]);
return simplified;
}
// both algorithms combined for awesome performance
function simplify(points, tolerance, highestQuality = false) {
if (points.length <= 2) return points;
const sqTolerance = tolerance * tolerance;
points = highestQuality ? points : simplifyRadialDist(points, sqTolerance);
points = simplifyDouglasPeucker(points, sqTolerance);
return points;
}
window.simplify = simplify;
}

144
main.js
View file

@ -626,8 +626,7 @@ async function generate(options) {
grid.cells.h = await HeightmapGenerator.generate(grid);
pack = {}; // reset pack
markFeatures();
markupGridOcean();
Features.markupGrid();
addLakesInDeepDepressions();
openNearSeaLakes();
@ -638,7 +637,7 @@ async function generate(options) {
generatePrecipitation();
reGraph();
reMarkFeatures();
Features.markupPack();
drawCoastline();
createDefaultRuler();
@ -718,69 +717,6 @@ function setSeed(precreatedSeed) {
Math.random = aleaPRNG(seed);
}
// Mark features (ocean, lakes, islands) and calculate distance field
function markFeatures() {
TIME && console.time("markFeatures");
Math.random = aleaPRNG(seed); // get the same result on heightmap edit in Erase mode
const cells = grid.cells;
const heights = grid.cells.h;
cells.f = new Uint16Array(cells.i.length); // cell feature number
cells.t = new Int8Array(cells.i.length); // cell type: 1 = land coast; -1 = water near coast
grid.features = [0];
for (let i = 1, queue = [0]; queue[0] !== -1; i++) {
cells.f[queue[0]] = i; // feature number
const land = heights[queue[0]] >= 20;
let border = false; // true if feature touches map border
while (queue.length) {
const q = queue.pop();
if (cells.b[q]) border = true;
cells.c[q].forEach(c => {
const cLand = heights[c] >= 20;
if (land === cLand && !cells.f[c]) {
cells.f[c] = i;
queue.push(c);
} else if (land && !cLand) {
cells.t[q] = 1;
cells.t[c] = -1;
}
});
}
const type = land ? "island" : border ? "ocean" : "lake";
grid.features.push({i, land, border, type});
queue[0] = cells.f.findIndex(f => !f); // find unmarked cell
}
TIME && console.timeEnd("markFeatures");
}
function markupGridOcean() {
TIME && console.time("markupGridOcean");
markup(grid.cells, -2, -1, -10);
TIME && console.timeEnd("markupGridOcean");
}
// Calculate cell-distance to coast for every cell
function markup(cells, start, increment, limit) {
for (let t = start, count = Infinity; count > 0 && t > limit; t += increment) {
count = 0;
const prevT = t - increment;
for (let i = 0; i < cells.i.length; i++) {
if (cells.t[i] !== prevT) continue;
for (const c of cells.c[i]) {
if (cells.t[c]) continue;
cells.t[c] = t;
count++;
}
}
}
}
function addLakesInDeepDepressions() {
TIME && console.time("addLakesInDeepDepressions");
const {cells, features} = grid;
@ -1222,82 +1158,6 @@ function reGraph() {
TIME && console.timeEnd("reGraph");
}
// Re-mark features (ocean, lakes, islands)
function reMarkFeatures() {
TIME && console.time("reMarkFeatures");
const cells = pack.cells;
const features = (pack.features = [0]);
cells.f = new Uint16Array(cells.i.length); // cell feature number
cells.t = new Int8Array(cells.i.length); // cell type: 1 = land along coast; -1 = water along coast;
cells.haven = cells.i.length < 65535 ? new Uint16Array(cells.i.length) : new Uint32Array(cells.i.length); // cell haven (opposite water cell);
cells.harbor = new Uint8Array(cells.i.length); // cell harbor (number of adjacent water cells);
if (!cells.i.length) return; // no cells -> there is nothing to do
for (let i = 1, queue = [0]; queue[0] !== -1; i++) {
const start = queue[0]; // first cell
cells.f[start] = i; // assign feature number
const land = cells.h[start] >= 20;
let border = false; // true if feature touches map border
let cellNumber = 1; // to count cells number in a feature
while (queue.length) {
const q = queue.pop();
if (cells.b[q]) border = true;
cells.c[q].forEach(function (e) {
const eLand = cells.h[e] >= 20;
if (land && !eLand) {
cells.t[q] = 1;
cells.t[e] = -1;
if (!cells.haven[q]) defineHaven(q);
} else if (land && eLand) {
if (!cells.t[e] && cells.t[q] === 1) cells.t[e] = 2;
else if (!cells.t[q] && cells.t[e] === 1) cells.t[q] = 2;
}
if (!cells.f[e] && land === eLand) {
queue.push(e);
cells.f[e] = i;
cellNumber++;
}
});
}
const type = land ? "island" : border ? "ocean" : "lake";
let group;
if (type === "ocean") group = defineOceanGroup(cellNumber);
else if (type === "island") group = defineIslandGroup(start, cellNumber);
features.push({i, land, border, type, cells: cellNumber, firstCell: start, group});
queue[0] = cells.f.findIndex(f => !f); // find unmarked cell
}
markup(pack.cells, 3, 1, 0); // markupPackLand
markup(pack.cells, -2, -1, -10); // markupPackWater
function defineHaven(i) {
const water = cells.c[i].filter(c => cells.h[c] < 20);
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";
}
TIME && console.timeEnd("reMarkFeatures");
}
function isWetLand(moisture, temperature, height) {
if (moisture > 40 && temperature > -2 && height < 25) return true; //near coast
if (moisture > 24 && temperature > -2 && height > 24 && height < 60) return true; //off coast

View file

@ -202,7 +202,7 @@ export function resolveVersionConflicts(mapVersion) {
coastline.selectAll("path").remove();
lakes.selectAll("path").remove();
reMarkFeatures();
Features.markupPack();
drawCoastline();
createDefaultRuler();
}

142
modules/features.js Normal file
View file

@ -0,0 +1,142 @@
"use strict";
window.Features = (function () {
// calculate cell-distance_to_coast for each cell
function markup(cells, start, increment, limit) {
for (let t = start, count = Infinity; count > 0 && t > limit; t += increment) {
count = 0;
const prevT = t - increment;
for (let i = 0; i < cells.i.length; i++) {
if (cells.t[i] !== prevT) continue;
for (const c of cells.c[i]) {
if (cells.t[c]) continue;
cells.t[c] = t;
count++;
}
}
}
}
// mark features (ocean, lakes, islands) and calculate distance field
function markupGrid() {
TIME && console.time("markupGrid");
Math.random = aleaPRNG(seed); // get the same result on heightmap edit in Erase mode
const cells = grid.cells;
const heights = grid.cells.h;
cells.f = new Uint16Array(cells.i.length); // cell feature number
cells.t = new Int8Array(cells.i.length); // cell type: 1 = land coast; -1 = water near coast
grid.features = [0];
for (let i = 1, queue = [0]; queue[0] !== -1; i++) {
cells.f[queue[0]] = i; // feature number
const land = heights[queue[0]] >= 20;
let border = false; // true if feature touches map border
while (queue.length) {
const q = queue.pop();
if (cells.b[q]) border = true;
cells.c[q].forEach(c => {
const cLand = heights[c] >= 20;
if (land === cLand && !cells.f[c]) {
cells.f[c] = i;
queue.push(c);
} else if (land && !cLand) {
cells.t[q] = 1;
cells.t[c] = -1;
}
});
}
const type = land ? "island" : border ? "ocean" : "lake";
grid.features.push({i, land, border, type});
queue[0] = cells.f.findIndex(f => !f); // find unmarked cell
}
markup(grid.cells, -2, -1, -10); // markup grid water
TIME && console.timeEnd("markupGrid");
}
// define Pack features (oceans, lakes, islands) add related details
function markupPack() {
TIME && console.time("markupPack");
const {cells} = pack;
const features = [0];
cells.f = new Uint16Array(cells.i.length); // cell feature number
cells.t = new Int8Array(cells.i.length); // cell type: 1 = land along coast; -1 = water along coast;
cells.haven = cells.i.length < 65535 ? new Uint16Array(cells.i.length) : new Uint32Array(cells.i.length); // cell haven (opposite water cell);
cells.harbor = new Uint8Array(cells.i.length); // cell harbor (number of adjacent water cells);
if (!cells.i.length) return; // no cells -> there is nothing to do
for (let i = 1, queue = [0]; queue[0] !== -1; i++) {
const start = queue[0]; // first cell
cells.f[start] = i; // assign feature number
const land = cells.h[start] >= 20;
let border = false; // true if feature touches map border
let cellNumber = 1; // to count cells number in a feature
while (queue.length) {
const q = queue.pop();
if (cells.b[q]) border = true;
cells.c[q].forEach(function (e) {
const eLand = cells.h[e] >= 20;
if (land && !eLand) {
cells.t[q] = 1;
cells.t[e] = -1;
if (!cells.haven[q]) defineHaven(q);
} else if (land && eLand) {
if (!cells.t[e] && cells.t[q] === 1) cells.t[e] = 2;
else if (!cells.t[q] && cells.t[e] === 1) cells.t[q] = 2;
}
if (!cells.f[e] && land === eLand) {
queue.push(e);
cells.f[e] = i;
cellNumber++;
}
});
}
const type = land ? "island" : border ? "ocean" : "lake";
let group;
if (type === "ocean") group = defineOceanGroup(cellNumber);
else if (type === "island") group = defineIslandGroup(start, cellNumber);
features.push({i, land, border, type, cells: cellNumber, firstCell: start, group});
queue[0] = cells.f.findIndex(f => !f); // find unmarked cell
}
function defineHaven(i) {
const water = cells.c[i].filter(c => cells.h[c] < 20);
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.features = features;
markup(pack.cells, 3, 1, 0); // markup pack land
markup(pack.cells, -2, -1, -10); // markup pack water
TIME && console.timeEnd("markupPack");
}
return {markupGrid, markupPack};
})();

View file

@ -369,7 +369,7 @@ async function parseLoadedData(data, mapVersion) {
void (function parsePackData() {
reGraph();
reMarkFeatures();
Features.markupPack();
pack.features = JSON.parse(data[12]);
pack.cultures = JSON.parse(data[13]);
pack.states = JSON.parse(data[14]);

View file

@ -109,3 +109,57 @@ function drawCoastline() {
TIME && console.timeEnd("drawCoastline");
}
function drawFeatures() {
TIME && console.time("drawFeatures");
const {vertices, features} = pack;
const landMask = defs.select("#land");
const waterMask = defs.select("#water");
const lineGen = d3.line().curve(d3.curveBasisClosed);
for (const feature of features) {
if (!feature || feature.type === "ocean") continue;
const points = feature.vertices.map(vertex => vertices.p[vertex]);
const simplifiedPoints = simplify(points, 0.3);
const clippedPoints = clipPoly(simplifiedPoints, 1);
const path = round(lineGen(clippedPoints));
if (feature.type === "lake") {
landMask
.append("path")
.attr("d", path)
.attr("fill", "black")
.attr("id", "land_" + feature.i);
lakes
.select(`#${feature.group}`)
.append("path")
.attr("d", path)
.attr("id", "lake_" + feature.i)
.attr("data-f", feature.i);
} else {
landMask
.append("path")
.attr("d", path)
.attr("fill", "white")
.attr("id", "land_" + feature.i);
waterMask
.append("path")
.attr("d", path)
.attr("fill", "black")
.attr("id", "water_" + feature.i);
coastline
.select(`#${feature.group}`)
.append("path")
.attr("d", path)
.attr("id", "island_" + feature.i)
.attr("data-f", feature.i);
}
}
TIME && console.timeEnd("drawFeatures");
}

View file

@ -111,14 +111,10 @@ window.Submap = (function () {
}
stage("Detect features, ocean and generating lakes");
markFeatures();
markupGridOcean();
Features.markupGrid();
// Warning: addLakesInDeepDepressions can be very slow!
if (options.addLakesInDepressions) {
addLakesInDeepDepressions();
openNearSeaLakes();
}
addLakesInDeepDepressions();
openNearSeaLakes();
OceanLayers();
@ -130,7 +126,7 @@ window.Submap = (function () {
// remove misclassified cells
stage("Define coastline");
reMarkFeatures();
Features.markupPack();
drawCoastline();
createDefaultRuler();

View file

@ -215,8 +215,7 @@ function editHeightmap(options) {
pack.religions = [];
const erosionAllowed = allowErosion.checked;
markFeatures();
markupGridOcean();
Features.markupGrid();
if (erosionAllowed) {
addLakesInDeepDepressions();
openNearSeaLakes();
@ -225,7 +224,7 @@ function editHeightmap(options) {
calculateTemperatures();
generatePrecipitation();
reGraph();
reMarkFeatures();
Features.markupPack();
drawCoastline();
Rivers.generate(erosionAllowed);
@ -337,14 +336,13 @@ function editHeightmap(options) {
zone.selectAll("*").remove();
});
markFeatures();
markupGridOcean();
Features.markupGrid();
if (erosionAllowed) addLakesInDeepDepressions();
OceanLayers();
calculateTemperatures();
generatePrecipitation();
reGraph();
reMarkFeatures();
Features.markupPack();
drawCoastline();
if (erosionAllowed) Rivers.generate(true);