mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2025-12-17 17:51:24 +01:00
fix: skip coastline out points wip
This commit is contained in:
parent
3215b6f0d2
commit
19d7f239c1
9 changed files with 262 additions and 64 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
import * as d3 from "d3";
|
import * as d3 from "d3";
|
||||||
|
|
||||||
import {simplify} from "scripts/simplify";
|
import {simplify} from "scripts/simplify";
|
||||||
import {clipPoly} from "utils/lineUtils";
|
import {filterOutOfCanvasPoints} from "utils/lineUtils";
|
||||||
import {round} from "utils/stringUtils";
|
import {round} from "utils/stringUtils";
|
||||||
|
|
||||||
export function drawCoastline(vertices: IGraphVertices, features: TPackFeatures) {
|
export function drawCoastline(vertices: IGraphVertices, features: TPackFeatures) {
|
||||||
|
|
@ -11,14 +11,38 @@ export function drawCoastline(vertices: IGraphVertices, features: TPackFeatures)
|
||||||
const lineGen = d3.line().curve(d3.curveBasisClosed);
|
const lineGen = d3.line().curve(d3.curveBasisClosed);
|
||||||
const SIMPLIFICATION_TOLERANCE = 0.5; // px
|
const SIMPLIFICATION_TOLERANCE = 0.5; // px
|
||||||
|
|
||||||
|
// map edge rectangle
|
||||||
|
debug
|
||||||
|
.append("rect")
|
||||||
|
.attr("x", 0)
|
||||||
|
.attr("y", 0)
|
||||||
|
.attr("width", graphWidth)
|
||||||
|
.attr("height", graphHeight)
|
||||||
|
.attr("fill", "none")
|
||||||
|
.attr("stroke", "black")
|
||||||
|
.attr("stroke-width", 0.1);
|
||||||
|
|
||||||
for (const feature of features) {
|
for (const feature of features) {
|
||||||
if (!feature) continue;
|
if (!feature) continue;
|
||||||
if (feature.type === "ocean") continue;
|
if (feature.type === "ocean") continue;
|
||||||
|
|
||||||
const points = clipPoly(feature.vertices.map(vertex => vertices.p[vertex]));
|
const points = feature.vertices.map(vertex => vertices.p[vertex]);
|
||||||
const simplifiedPoints = simplify(points, SIMPLIFICATION_TOLERANCE);
|
const filteredPoints = filterOutOfCanvasPoints(points);
|
||||||
|
const simplifiedPoints = simplify(filteredPoints, SIMPLIFICATION_TOLERANCE);
|
||||||
const path = round(lineGen(simplifiedPoints)!);
|
const path = round(lineGen(simplifiedPoints)!);
|
||||||
|
|
||||||
|
points.forEach(([x, y]) => {
|
||||||
|
debug.append("circle").attr("cx", x).attr("cy", y).attr("r", 0.3).attr("fill", "red");
|
||||||
|
});
|
||||||
|
|
||||||
|
filteredPoints.forEach(([x, y]) => {
|
||||||
|
debug.append("circle").attr("cx", x).attr("cy", y).attr("r", 0.3).attr("fill", "blue");
|
||||||
|
});
|
||||||
|
|
||||||
|
simplifiedPoints.forEach(([x, y]) => {
|
||||||
|
debug.append("circle").attr("cx", x).attr("cy", y).attr("r", 0.3).attr("fill", "green");
|
||||||
|
});
|
||||||
|
|
||||||
if (feature.type === "lake") {
|
if (feature.type === "lake") {
|
||||||
landMask
|
landMask
|
||||||
.append("path")
|
.append("path")
|
||||||
|
|
|
||||||
107
src/scripts/curves/unused-basisFramedClosed.js
Normal file
107
src/scripts/curves/unused-basisFramedClosed.js
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
// Custom fork of d3.curveBasisClosed
|
||||||
|
// The idea is to not interpolate (curve) line along the frame
|
||||||
|
// points = [[0, 833],[0, 0],[1007, 0],[1007, 833]]
|
||||||
|
// d3.line().curve(d3.curveBasisClosed)(points) // => M 167.8,138.8 C 335.7,0,671.3,0,839.2,138.8 C1007,277.7,1007,555.3,839.2,694.2C671.3,833,335.7,833,167.8,694.2 C0,555.3,0,277.7,167.8,138.8
|
||||||
|
// d3.line().curve(d3.curveBasisFramedClosed)(points) // => M 0,833 L 0,0 L 1007,0 L 1007,833
|
||||||
|
|
||||||
|
const PRECISION = 2;
|
||||||
|
const round = number => Number(number.toFixed(PRECISION));
|
||||||
|
|
||||||
|
function noop() {}
|
||||||
|
|
||||||
|
function point(that, x, y) {
|
||||||
|
that._context.bezierCurveTo(
|
||||||
|
round((2 * that._x0 + that._x1) / 3),
|
||||||
|
round((2 * that._y0 + that._y1) / 3),
|
||||||
|
round((that._x0 + 2 * that._x1) / 3),
|
||||||
|
round((that._y0 + 2 * that._y1) / 3),
|
||||||
|
round((that._x0 + 4 * that._x1 + x) / 6),
|
||||||
|
round((that._y0 + 4 * that._y1 + y) / 6)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isEdge(x, y) {
|
||||||
|
return x <= 0 || y <= 0 || x >= graphWidth || y >= graphHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
function BasisFramedClosed(context) {
|
||||||
|
this._context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
BasisFramedClosed.prototype = {
|
||||||
|
areaStart: noop,
|
||||||
|
areaEnd: noop,
|
||||||
|
lineStart: function () {
|
||||||
|
this._x0 = this._x1 = this._x2 = this._x3 = this._x4 = this._y0 = this._y1 = this._y2 = this._y3 = this._y4 = NaN;
|
||||||
|
this._point = 0;
|
||||||
|
},
|
||||||
|
lineEnd: function () {
|
||||||
|
switch (this._point) {
|
||||||
|
case 1: {
|
||||||
|
this._context.moveTo(this._x2, this._y2);
|
||||||
|
this._context.closePath();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 2: {
|
||||||
|
this._context.moveTo(round((this._x2 + 2 * this._x3) / 3), round((this._y2 + 2 * this._y3) / 3));
|
||||||
|
this._context.lineTo(round((this._x3 + 2 * this._x2) / 3), round((this._y3 + 2 * this._y2) / 3));
|
||||||
|
this._context.closePath();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 3: {
|
||||||
|
this.point(this._x2, this._y2);
|
||||||
|
this.point(this._x3, this._y3);
|
||||||
|
this.point(this._x4, this._y4);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
point: function (x, y) {
|
||||||
|
const edge = isEdge(x, y);
|
||||||
|
if (x <= 0) x -= 20;
|
||||||
|
if (y <= 0) y -= 20;
|
||||||
|
if (x >= graphWidth) x += 20;
|
||||||
|
if (y >= graphHeight) y += 20;
|
||||||
|
|
||||||
|
switch (this._point) {
|
||||||
|
case 0:
|
||||||
|
this._point = 1;
|
||||||
|
this._x2 = x;
|
||||||
|
this._y2 = y;
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
this._point = 2;
|
||||||
|
this._x3 = x;
|
||||||
|
this._y3 = y;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
this._point = 3;
|
||||||
|
this._x4 = x;
|
||||||
|
this._y4 = y;
|
||||||
|
|
||||||
|
if (edge) {
|
||||||
|
this._context.moveTo(round((this._x1 + x) / 2), round((this._y1 + y) / 2));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._context.moveTo(round((this._x0 + 4 * this._x1 + x) / 6), round((this._y0 + 4 * this._y1 + y) / 6));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (edge) {
|
||||||
|
this._context.lineTo(x, y);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
point(this, x, y);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
this._x0 = this._x1;
|
||||||
|
this._x1 = x;
|
||||||
|
this._y0 = this._y1;
|
||||||
|
this._y1 = y;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export function curveBasisFramedClosed(context) {
|
||||||
|
return new BasisFramedClosed(context);
|
||||||
|
}
|
||||||
|
|
@ -10,11 +10,8 @@ import {clearLegend, dragLegendBox} from "modules/legend";
|
||||||
export function setDefaultEventHandlers() {
|
export function setDefaultEventHandlers() {
|
||||||
window.Zoom.setZoomBehavior();
|
window.Zoom.setZoomBehavior();
|
||||||
|
|
||||||
viewbox
|
viewbox.style("cursor", "default").on(".drag", null).on("click", handleMapClick);
|
||||||
.style("cursor", "default")
|
//.on("touchmove mousemove", onMouseMove);
|
||||||
.on(".drag", null)
|
|
||||||
.on("click", handleMapClick)
|
|
||||||
.on("touchmove mousemove", onMouseMove);
|
|
||||||
|
|
||||||
scaleBar.on("mousemove", () => tip("Click to open Units Editor")).on("click", () => openDialog("unitsEditor"));
|
scaleBar.on("mousemove", () => tip("Click to open Units Editor")).on("click", () => openDialog("unitsEditor"));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,6 @@ import {debounce} from "utils/functionUtils";
|
||||||
import {rn} from "utils/numberUtils";
|
import {rn} from "utils/numberUtils";
|
||||||
import {generateSeed} from "utils/probabilityUtils";
|
import {generateSeed} from "utils/probabilityUtils";
|
||||||
import {byId} from "utils/shorthands";
|
import {byId} from "utils/shorthands";
|
||||||
import {showStatistics} from "../statistics";
|
|
||||||
import {createGrid} from "./grid";
|
import {createGrid} from "./grid";
|
||||||
import {createPack} from "./pack/pack";
|
import {createPack} from "./pack/pack";
|
||||||
import {getInputValue, setInputValue} from "utils/nodeUtils";
|
import {getInputValue, setInputValue} from "utils/nodeUtils";
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ export interface ILakeClimateData extends IPackFeatureLake {
|
||||||
|
|
||||||
export const getClimateData = function (
|
export const getClimateData = function (
|
||||||
lakes: IPackFeatureLake[],
|
lakes: IPackFeatureLake[],
|
||||||
heights: number[],
|
heights: Float32Array,
|
||||||
drainableLakes: Dict<boolean>,
|
drainableLakes: Dict<boolean>,
|
||||||
gridReference: IPack["cells"]["g"],
|
gridReference: IPack["cells"]["g"],
|
||||||
precipitation: IGrid["cells"]["prec"],
|
precipitation: IGrid["cells"]["prec"],
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import * as d3 from "d3";
|
import * as d3 from "d3";
|
||||||
|
|
||||||
import {TIME, WARN} from "config/logging";
|
import {INFO, TIME, WARN} from "config/logging";
|
||||||
import {rn} from "utils/numberUtils";
|
import {rn} from "utils/numberUtils";
|
||||||
import {aleaPRNG} from "scripts/aleaPRNG";
|
import {aleaPRNG} from "scripts/aleaPRNG";
|
||||||
import {DISTANCE_FIELD, MAX_HEIGHT, MIN_LAND_HEIGHT} from "config/generation";
|
import {DISTANCE_FIELD, MAX_HEIGHT, MIN_LAND_HEIGHT} from "config/generation";
|
||||||
|
|
@ -8,6 +8,7 @@ import {getInputNumber} from "utils/nodeUtils";
|
||||||
import {pick} from "utils/functionUtils";
|
import {pick} from "utils/functionUtils";
|
||||||
import {byId} from "utils/shorthands";
|
import {byId} from "utils/shorthands";
|
||||||
import {mergeLakeData, getClimateData, ILakeClimateData} from "./lakes";
|
import {mergeLakeData, getClimateData, ILakeClimateData} from "./lakes";
|
||||||
|
import {drawArrow} from "utils/debugUtils";
|
||||||
|
|
||||||
const {Rivers} = window;
|
const {Rivers} = window;
|
||||||
const {LAND_COAST} = DISTANCE_FIELD;
|
const {LAND_COAST} = DISTANCE_FIELD;
|
||||||
|
|
@ -96,7 +97,7 @@ export function generateRivers(
|
||||||
}
|
}
|
||||||
|
|
||||||
lake.outlet = riverIds[lakeCell];
|
lake.outlet = riverIds[lakeCell];
|
||||||
flowDown(cellId, flux[lakeCell], lake.outlet);
|
flowDown(lakeCell, cellId, flux[lakeCell], lake.outlet);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lakesDrainingToCell.length && lakesDrainingToCell[0].outlet) {
|
if (lakesDrainingToCell.length && lakesDrainingToCell[0].outlet) {
|
||||||
|
|
@ -127,15 +128,6 @@ export function generateRivers(
|
||||||
// cells is depressed
|
// cells is depressed
|
||||||
if (currentCellHeights[cellId] <= currentCellHeights[min]) return;
|
if (currentCellHeights[cellId] <= currentCellHeights[min]) return;
|
||||||
|
|
||||||
debug
|
|
||||||
.append("line")
|
|
||||||
.attr("x1", cells.p[cellId][0])
|
|
||||||
.attr("y1", cells.p[cellId][1])
|
|
||||||
.attr("x2", cells.p[min][0])
|
|
||||||
.attr("y2", cells.p[min][1])
|
|
||||||
.attr("stroke", "#333")
|
|
||||||
.attr("stroke-width", 0.1);
|
|
||||||
|
|
||||||
if (flux[cellId] < MIN_FLUX_TO_FORM_RIVER) {
|
if (flux[cellId] < MIN_FLUX_TO_FORM_RIVER) {
|
||||||
// flux is too small to operate as a river
|
// flux is too small to operate as a river
|
||||||
if (currentCellHeights[min] >= MIN_LAND_HEIGHT) flux[min] += flux[cellId];
|
if (currentCellHeights[min] >= MIN_LAND_HEIGHT) flux[min] += flux[cellId];
|
||||||
|
|
@ -149,12 +141,14 @@ export function generateRivers(
|
||||||
nextRiverId++;
|
nextRiverId++;
|
||||||
}
|
}
|
||||||
|
|
||||||
flowDown(min, flux[cellId], riverIds[cellId]);
|
flowDown(cellId, min, flux[cellId], riverIds[cellId]);
|
||||||
});
|
});
|
||||||
|
|
||||||
return {flux, lakeData};
|
return {flux, lakeData};
|
||||||
|
|
||||||
function flowDown(toCell: number, fromFlux: number, riverId: number) {
|
function flowDown(fromCell: number, toCell: number, fromFlux: number, riverId: number) {
|
||||||
|
// drawArrow(cells.p[fromCell], cells.p[toCell]);
|
||||||
|
|
||||||
const toFlux = flux[toCell] - confluence[toCell];
|
const toFlux = flux[toCell] - confluence[toCell];
|
||||||
const toRiver = riverIds[toCell];
|
const toRiver = riverIds[toCell];
|
||||||
|
|
||||||
|
|
@ -262,7 +256,7 @@ export function generateRivers(
|
||||||
return {r, conf, rivers};
|
return {r, conf, rivers};
|
||||||
}
|
}
|
||||||
|
|
||||||
function downcutRivers(heights: number[]) {
|
function downcutRivers(heights: Float32Array) {
|
||||||
const MAX_DOWNCUT = 5;
|
const MAX_DOWNCUT = 5;
|
||||||
const MIN_HEIGHT_TO_DOWNCUT = 35;
|
const MIN_HEIGHT_TO_DOWNCUT = 35;
|
||||||
|
|
||||||
|
|
@ -284,10 +278,10 @@ export function generateRivers(
|
||||||
|
|
||||||
// add distance to water value to land cells to make map less depressed
|
// add distance to water value to land cells to make map less depressed
|
||||||
const applyDistanceField = ({h, c, t}: Pick<IPack["cells"], "h" | "c" | "t">) => {
|
const applyDistanceField = ({h, c, t}: Pick<IPack["cells"], "h" | "c" | "t">) => {
|
||||||
return Array.from(h).map((height, index) => {
|
return new Float32Array(h.length).map((_, index) => {
|
||||||
if (height < MIN_LAND_HEIGHT || t[index] < LAND_COAST) return height;
|
if (h[index] < MIN_LAND_HEIGHT || t[index] < LAND_COAST) return h[index];
|
||||||
const mean = d3.mean(c[index].map(c => t[c])) || 0;
|
const mean = d3.mean(c[index].map(c => t[c])) || 0;
|
||||||
return height + t[index] / 100 + mean / 10000;
|
return h[index] + t[index] / 100 + mean / 10000;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -295,52 +289,55 @@ const applyDistanceField = ({h, c, t}: Pick<IPack["cells"], "h" | "c" | "t">) =>
|
||||||
const resolveDepressions = function (
|
const resolveDepressions = function (
|
||||||
cells: Pick<IPack["cells"], "i" | "c" | "b" | "f">,
|
cells: Pick<IPack["cells"], "i" | "c" | "b" | "f">,
|
||||||
features: TPackFeatures,
|
features: TPackFeatures,
|
||||||
heights: number[]
|
initialCellHeights: Float32Array
|
||||||
): [number[], Dict<boolean>] {
|
): [Float32Array, Dict<boolean>] {
|
||||||
|
TIME && console.time("resolveDepressions");
|
||||||
|
|
||||||
const MAX_INTERATIONS = getInputNumber("resolveDepressionsStepsOutput");
|
const MAX_INTERATIONS = getInputNumber("resolveDepressionsStepsOutput");
|
||||||
const checkLakeMaxIteration = MAX_INTERATIONS * 0.85;
|
const checkLakeMaxIteration = MAX_INTERATIONS * 0.85;
|
||||||
const elevateLakeMaxIteration = MAX_INTERATIONS * 0.75;
|
const elevateLakeMaxIteration = MAX_INTERATIONS * 0.75;
|
||||||
|
|
||||||
const ELEVATION_LIMIT = getInputNumber("lakeElevationLimitOutput");
|
|
||||||
|
|
||||||
const LAND_ELEVATION_INCREMENT = 0.1;
|
const LAND_ELEVATION_INCREMENT = 0.1;
|
||||||
const LAKE_ELEVATION_INCREMENT = 0.2;
|
const LAKE_ELEVATION_INCREMENT = 0.2;
|
||||||
|
|
||||||
const lakes = features.filter(feature => feature && feature.type === "lake") as IPackFeatureLake[];
|
const lakes = features.filter(feature => feature && feature.type === "lake") as IPackFeatureLake[];
|
||||||
lakes.sort((a, b) => a.height - b.height); // lowest lakes go first
|
lakes.sort((a, b) => a.height - b.height); // lowest lakes go first
|
||||||
|
|
||||||
const currentCellHeights = Array.from(heights);
|
|
||||||
const currentLakeHeights = Object.fromEntries(lakes.map(({i, height}) => [i, height]));
|
|
||||||
|
|
||||||
const getHeight = (i: number) => currentLakeHeights[cells.f[i]] || currentCellHeights[i];
|
const getHeight = (i: number) => currentLakeHeights[cells.f[i]] || currentCellHeights[i];
|
||||||
const getMinHeight = (cellsIds: number[]) => Math.min(...cellsIds.map(getHeight));
|
const getMinHeight = (cellsIds: number[]) => Math.min(...cellsIds.map(getHeight));
|
||||||
|
const getMinLandHeight = (cellsIds: number[]) => Math.min(...cellsIds.map(i => currentCellHeights[i]));
|
||||||
|
|
||||||
const drainableLakes = checkLakesDrainability();
|
const landCells = cells.i.filter(i => initialCellHeights[i] >= MIN_LAND_HEIGHT && !cells.b[i]);
|
||||||
|
landCells.sort((a, b) => initialCellHeights[a] - initialCellHeights[b]); // lowest cells go first
|
||||||
const landCells = cells.i.filter(i => heights[i] >= MIN_LAND_HEIGHT && !cells.b[i]);
|
|
||||||
landCells.sort((a, b) => heights[a] - heights[b]); // lowest cells go first
|
|
||||||
|
|
||||||
|
const currentCellHeights = Float32Array.from(initialCellHeights);
|
||||||
|
const currentLakeHeights = Object.fromEntries(lakes.map(({i, height}) => [i, height]));
|
||||||
|
const currentDrainableLakes = checkLakesDrainability();
|
||||||
const depressions: number[] = [];
|
const depressions: number[] = [];
|
||||||
|
|
||||||
for (let iteration = 0; iteration && depressions.at(-1) && iteration < MAX_INTERATIONS; iteration++) {
|
let bestDepressions = Infinity;
|
||||||
|
let bestCellHeights: typeof currentCellHeights | null = null;
|
||||||
|
let bestDrainableLakes: typeof currentDrainableLakes | null = null;
|
||||||
|
|
||||||
|
for (let iteration = 0; depressions.at(-1) !== 0 && iteration < MAX_INTERATIONS; iteration++) {
|
||||||
let depressionsLeft = 0;
|
let depressionsLeft = 0;
|
||||||
|
|
||||||
// elevate potentially drainable lakes
|
// elevate potentially drainable lakes
|
||||||
if (iteration < checkLakeMaxIteration) {
|
if (iteration < checkLakeMaxIteration) {
|
||||||
for (const lake of lakes) {
|
for (const lake of lakes) {
|
||||||
if (drainableLakes[lake.i] !== true) continue;
|
if (currentDrainableLakes[lake.i] !== true) continue;
|
||||||
|
|
||||||
const minShoreHeight = getMinHeight(lake.shoreline);
|
const minShoreHeight = getMinLandHeight(lake.shoreline);
|
||||||
if (minShoreHeight >= MAX_HEIGHT || lake.height > minShoreHeight) continue;
|
if (minShoreHeight >= MAX_HEIGHT || currentLakeHeights[lake.i] > minShoreHeight) continue;
|
||||||
|
|
||||||
if (iteration > elevateLakeMaxIteration) {
|
if (iteration > elevateLakeMaxIteration) {
|
||||||
for (const shoreCellId of lake.shoreline) {
|
|
||||||
// reset heights
|
// reset heights
|
||||||
currentCellHeights[shoreCellId] = heights[shoreCellId];
|
for (const shoreCellId of lake.shoreline) {
|
||||||
currentLakeHeights[lake.i] = lake.height;
|
currentCellHeights[shoreCellId] = initialCellHeights[shoreCellId];
|
||||||
}
|
}
|
||||||
|
currentLakeHeights[lake.i] = lake.height;
|
||||||
|
|
||||||
drainableLakes[lake.i] = false;
|
currentDrainableLakes[lake.i] = false;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -358,23 +355,36 @@ const resolveDepressions = function (
|
||||||
}
|
}
|
||||||
|
|
||||||
depressions.push(depressionsLeft);
|
depressions.push(depressionsLeft);
|
||||||
|
if (depressionsLeft < bestDepressions) {
|
||||||
// check depression resolving progress
|
bestDepressions = depressionsLeft;
|
||||||
if (depressions.length > 5) {
|
bestCellHeights = Float32Array.from(currentCellHeights);
|
||||||
const depressionsInitial = depressions.at(0) || 0;
|
bestDrainableLakes = structuredClone(currentDrainableLakes);
|
||||||
const depressiosRecently = depressions.at(-6) || 0;
|
|
||||||
|
|
||||||
const isProgressingOverall = depressionsInitial < depressionsLeft;
|
|
||||||
if (!isProgressingOverall) return [heights, drainableLakes];
|
|
||||||
|
|
||||||
const isProgressingRecently = depressiosRecently < depressionsLeft;
|
|
||||||
if (!isProgressingRecently) return [currentCellHeights, drainableLakes];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TIME && console.timeEnd("resolveDepressions");
|
||||||
|
|
||||||
|
const depressionsLeft = depressions.at(-1);
|
||||||
|
if (depressionsLeft) {
|
||||||
|
if (bestCellHeights && bestDrainableLakes) {
|
||||||
|
WARN &&
|
||||||
|
console.warn(`Cannot resolve all depressions. Depressions: ${depressions[0]}. Best result: ${bestDepressions}`);
|
||||||
|
return [bestCellHeights, bestDrainableLakes];
|
||||||
|
}
|
||||||
|
|
||||||
|
WARN && console.warn(`Cannot resolve depressions. Depressions: ${depressionsLeft}`);
|
||||||
|
return [initialCellHeights, {}];
|
||||||
|
}
|
||||||
|
|
||||||
|
INFO &&
|
||||||
|
console.info(`ⓘ Resolved all depressions. Depressions: ${depressions[0]}. Interations: ${depressions.length}`);
|
||||||
|
return [currentCellHeights, currentDrainableLakes];
|
||||||
|
|
||||||
// define lakes that potentially can be open (drained into another water body)
|
// define lakes that potentially can be open (drained into another water body)
|
||||||
function checkLakesDrainability() {
|
function checkLakesDrainability() {
|
||||||
const canBeDrained: Dict<boolean> = {}; // all false by default
|
const canBeDrained: Dict<boolean> = {}; // all false by default
|
||||||
|
|
||||||
|
const ELEVATION_LIMIT = getInputNumber("lakeElevationLimitOutput");
|
||||||
const drainAllLakes = ELEVATION_LIMIT === MAX_HEIGHT - MIN_LAND_HEIGHT;
|
const drainAllLakes = ELEVATION_LIMIT === MAX_HEIGHT - MIN_LAND_HEIGHT;
|
||||||
|
|
||||||
for (const lake of lakes) {
|
for (const lake of lakes) {
|
||||||
|
|
@ -385,7 +395,8 @@ const resolveDepressions = function (
|
||||||
|
|
||||||
canBeDrained[lake.i] = false;
|
canBeDrained[lake.i] = false;
|
||||||
const minShoreHeight = getMinHeight(lake.shoreline);
|
const minShoreHeight = getMinHeight(lake.shoreline);
|
||||||
const minHeightShoreCell = lake.shoreline.find(cellId => heights[cellId] === minShoreHeight) || lake.shoreline[0];
|
const minHeightShoreCell =
|
||||||
|
lake.shoreline.find(cellId => initialCellHeights[cellId] === minShoreHeight) || lake.shoreline[0];
|
||||||
|
|
||||||
const queue = [minHeightShoreCell];
|
const queue = [minHeightShoreCell];
|
||||||
const checked = [];
|
const checked = [];
|
||||||
|
|
@ -397,9 +408,9 @@ const resolveDepressions = function (
|
||||||
|
|
||||||
for (const neibCellId of cells.c[cellId]) {
|
for (const neibCellId of cells.c[cellId]) {
|
||||||
if (checked[neibCellId]) continue;
|
if (checked[neibCellId]) continue;
|
||||||
if (heights[neibCellId] >= breakableHeight) continue;
|
if (initialCellHeights[neibCellId] >= breakableHeight) continue;
|
||||||
|
|
||||||
if (heights[neibCellId] < MIN_LAND_HEIGHT) {
|
if (initialCellHeights[neibCellId] < MIN_LAND_HEIGHT) {
|
||||||
const waterFeatureMet = features[cells.f[neibCellId]];
|
const waterFeatureMet = features[cells.f[neibCellId]];
|
||||||
const isOceanMet = waterFeatureMet && waterFeatureMet.type === "ocean";
|
const isOceanMet = waterFeatureMet && waterFeatureMet.type === "ocean";
|
||||||
const isLakeMet = waterFeatureMet && waterFeatureMet.type === "lake";
|
const isLakeMet = waterFeatureMet && waterFeatureMet.type === "lake";
|
||||||
|
|
@ -418,8 +429,4 @@ const resolveDepressions = function (
|
||||||
|
|
||||||
return canBeDrained;
|
return canBeDrained;
|
||||||
}
|
}
|
||||||
|
|
||||||
depressions && WARN && console.warn(`Unresolved depressions: ${depressions}. Edit heightmap to fix`);
|
|
||||||
|
|
||||||
return [currentCellHeights, drainableLakes];
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,27 @@ export function unique<T>(array: T[]) {
|
||||||
return [...new Set(array)];
|
return [...new Set(array)];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function sliceFragment<T>(array: T[], index: number, zone: number) {
|
||||||
|
if (zone + 1 + zone >= array.length) {
|
||||||
|
return array.slice();
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = index - zone;
|
||||||
|
const end = index + zone;
|
||||||
|
|
||||||
|
if (start < 0) {
|
||||||
|
return array.slice(0, end).concat(array.slice(start));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (end >= array.length) {
|
||||||
|
return array.slice(start).concat(array.slice(0, end % array.length));
|
||||||
|
}
|
||||||
|
|
||||||
|
return array.slice(start, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.sliceFragment = sliceFragment;
|
||||||
|
|
||||||
interface ICreateTypesArrayLength {
|
interface ICreateTypesArrayLength {
|
||||||
maxValue: number;
|
maxValue: number;
|
||||||
length: number;
|
length: number;
|
||||||
|
|
|
||||||
17
src/utils/debugUtils.ts
Normal file
17
src/utils/debugUtils.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
// utils used for debugging (not in PROD) only
|
||||||
|
export function drawArrow([x1, y1]: TPoint, [x2, y2]: TPoint, width = 1, color = "#444"): void {
|
||||||
|
const angle = Math.atan2(y2 - y1, x2 - x1);
|
||||||
|
const normal = angle + Math.PI / 2;
|
||||||
|
|
||||||
|
const [xMid, yMid] = [(x1 + x2) / 2, (y1 + y2) / 2];
|
||||||
|
|
||||||
|
const [xLeft, yLeft] = [xMid + width * Math.cos(normal), yMid + width * Math.sin(normal)];
|
||||||
|
const [xRight, yRight] = [xMid - width * Math.cos(normal), yMid - width * Math.sin(normal)];
|
||||||
|
|
||||||
|
debug
|
||||||
|
.append("path")
|
||||||
|
.attr("d", `M${x1},${y1} L${xMid},${yMid} ${xLeft},${yLeft} ${x2},${y2} ${xRight},${yRight} ${xMid},${yMid} Z`)
|
||||||
|
.attr("fill", color)
|
||||||
|
.attr("stroke", color)
|
||||||
|
.attr("stroke-width", width / 2);
|
||||||
|
}
|
||||||
|
|
@ -52,3 +52,29 @@ export function getMiddlePoint(cell1: number, cell2: number) {
|
||||||
|
|
||||||
return [x, y];
|
return [x, y];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getOffCanvasSide([x, y]: TPoint) {
|
||||||
|
if (y <= 0) return "top";
|
||||||
|
if (y >= graphHeight) return "bottom";
|
||||||
|
if (x <= 0) return "left";
|
||||||
|
if (x >= graphWidth) return "right";
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove intermediate out-of-canvas points from polyline
|
||||||
|
export function filterOutOfCanvasPoints(points: TPoints) {
|
||||||
|
const pointsOutSide = points.map(getOffCanvasSide);
|
||||||
|
const SAFE_ZONE = 3;
|
||||||
|
|
||||||
|
const filterOutCanvasPoint = (i: number) => {
|
||||||
|
const pointSide = pointsOutSide[i];
|
||||||
|
if (pointSide === false) return true;
|
||||||
|
if (pointsOutSide.slice(i - SAFE_ZONE, i + SAFE_ZONE).some(side => !side || side !== pointSide)) return true;
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = points.filter((_, i) => filterOutCanvasPoint(i));
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue