From 371c775578d1570c21f5bab9bfd2cbc03bc38e85 Mon Sep 17 00:00:00 2001 From: Marc Emmanuel Date: Wed, 21 Jan 2026 23:04:23 +0100 Subject: [PATCH] refactor: Migrate lakes functionality to lakes.ts and update related interfaces --- src/index.html | 1 - src/modules/PackedGraph.ts | 18 +- src/modules/biomes.ts | 3 +- src/modules/features.ts | 24 ++- src/modules/index.ts | 1 + .../modules/lakes.js => src/modules/lakes.ts | 176 ++++++++++-------- src/modules/river-generator.ts | 21 ++- src/utils/polyfills.ts | 2 +- 8 files changed, 136 insertions(+), 110 deletions(-) rename public/modules/lakes.js => src/modules/lakes.ts (65%) diff --git a/src/index.html b/src/index.html index 5d9cf35a..8896a2e6 100644 --- a/src/index.html +++ b/src/index.html @@ -8469,7 +8469,6 @@ - diff --git a/src/modules/PackedGraph.ts b/src/modules/PackedGraph.ts index 1430c860..d6295430 100644 --- a/src/modules/PackedGraph.ts +++ b/src/modules/PackedGraph.ts @@ -1,22 +1,27 @@ import { PackedGraphFeature } from "./features"; import { River } from "./river-generator"; + +type TypedArray = Uint8Array | Uint16Array | Uint32Array | Int8Array | Int16Array | Float32Array | Float64Array; + export interface PackedGraph { cells: { i: number[]; // cell indices c: number[][]; // neighboring cells v: number[][]; // neighboring vertices + p: [number, number][]; // cell polygon points b: boolean[]; // cell is on border - h: Uint8Array; // cell heights - t: Uint8Array; // cell terrain types + h: TypedArray; // cell heights + t: TypedArray; // cell terrain types r: Uint16Array; // river id passing through cell f: Uint16Array; // feature id occupying cell - fl: Uint16Array | Uint8Array; // flux presence in cell - conf: Uint16Array | Uint8Array; // cell water confidence - haven: Uint8Array; // cell is a haven + fl: TypedArray; // flux presence in cell + conf: TypedArray; // cell water confidence + haven: TypedArray; // cell is a haven g: number[]; // cell ground type culture: number[]; // cell culture id - p: [number, number][]; // cell polygon points + biome: TypedArray; // cell biome id + harbor: TypedArray; // cell harbour presence }; vertices: { i: number[]; // vertex indices @@ -24,6 +29,7 @@ export interface PackedGraph { v: number[][]; // neighboring vertices x: number[]; // x coordinates y: number[]; // y coordinates + p: [number, number][]; // vertex points }; rivers: River[]; features: PackedGraphFeature[]; diff --git a/src/modules/biomes.ts b/src/modules/biomes.ts index 49bc24b8..ba6e58a9 100644 --- a/src/modules/biomes.ts +++ b/src/modules/biomes.ts @@ -1,10 +1,11 @@ import { range, mean } from "d3"; import { rn } from "../utils"; +import { PackedGraph } from "./PackedGraph"; declare global { var Biomes: BiomesModule; - var pack: any; + var pack: PackedGraph; var grid: any; var TIME: boolean; diff --git a/src/modules/features.ts b/src/modules/features.ts index 75367278..e6b935c6 100644 --- a/src/modules/features.ts +++ b/src/modules/features.ts @@ -1,15 +1,17 @@ import { clipPoly, connectVertices, createTypedArray, distanceSquared, isLand, isWater, rn, TYPED_ARRAY_MAX_VALUES,unique } from "../utils"; import Alea from "alea"; import { polygonArea } from "d3"; +import { LakesModule } from "./lakes"; +import { PackedGraph } from "./PackedGraph"; declare global { interface Window { Features: any; } var TIME: boolean; - var Lakes: any; + var Lakes: LakesModule; var grid: any; - var pack: any; + var pack: PackedGraph; var seed: string; } @@ -30,11 +32,15 @@ export interface PackedGraphFeature { temp: number; flux: number; evaporation: number; - inlets: number[]; - outlet: number; - river: number; - enteringFlux: number; - closed: boolean; + name: string; + + // River related + inlets?: number[]; + outlet?: number; + river?: number; + enteringFlux?: number; + closed?: boolean; + outCell?: number; } export interface GridFeature { @@ -210,7 +216,7 @@ class FeatureModule { if (type === "lake") { if (area > 0) feature.vertices = (feature.vertices as number[]).reverse(); feature.shoreline = unique((feature.vertices as number[]).map(vertex => vertices.c[vertex].filter((index: number) => isLand(index, this.packedGraph))).flat() || []); - feature.height = Lakes.getHeight(feature); + feature.height = Lakes.getHeight(feature as PackedGraphFeature); } return { @@ -278,7 +284,7 @@ class FeatureModule { this.packedGraph.cells.f = featureIds; this.packedGraph.cells.haven = haven; this.packedGraph.cells.harbor = harbor; - this.packedGraph.features = [0, ...features]; + this.packedGraph.features = [0 as unknown as PackedGraphFeature, ...features]; TIME && console.timeEnd("markupPack"); } diff --git a/src/modules/index.ts b/src/modules/index.ts index 21836080..c15b421a 100644 --- a/src/modules/index.ts +++ b/src/modules/index.ts @@ -1,6 +1,7 @@ import "./voronoi"; import "./heightmap-generator"; import "./features"; +import "./lakes"; import "./ocean-layers"; import "./river-generator"; import "./biomes" \ No newline at end of file diff --git a/public/modules/lakes.js b/src/modules/lakes.ts similarity index 65% rename from public/modules/lakes.js rename to src/modules/lakes.ts index 8ce18793..1665a3ab 100644 --- a/public/modules/lakes.js +++ b/src/modules/lakes.ts @@ -1,12 +1,98 @@ -"use strict"; +import { PackedGraphFeature } from "./features"; +import { min, mean } from "d3"; +import { byId, +rn } from "../utils"; +import { PackedGraph } from "./PackedGraph"; -window.Lakes = (function () { - const LAKE_ELEVATION_DELTA = 0.1; +declare global { + var Lakes: LakesModule; + var pack: PackedGraph; + var Names: any; + + var heightExponentInput: HTMLInputElement; +} + +export class LakesModule { + private LAKE_ELEVATION_DELTA = 0.1; + + getHeight(feature: PackedGraphFeature) { + const heights = pack.cells.h; + const minShoreHeight = min(feature.shoreline.map(cellId => heights[cellId])) || 20; + return rn(minShoreHeight - this.LAKE_ELEVATION_DELTA, 2); + }; + + defineNames() { + pack.features.forEach((feature: PackedGraphFeature) => { + if (feature.type !== "lake") return; + feature.name = this.getName(feature); + }); + }; + + getName(feature: PackedGraphFeature): string { + const landCell = feature.shoreline[0]; + const culture = pack.cells.culture[landCell]; + return Names.getCulture(culture); + }; + + cleanupLakeData = function () { + for (const feature of pack.features) { + if (feature.type !== "lake") continue; + delete feature.river; + delete feature.enteringFlux; + delete feature.outCell; + delete feature.closed; + feature.height = rn(feature.height, 3); + + const inlets = feature.inlets?.filter(r => pack.rivers.find(river => river.i === r)); + if (!inlets || !inlets.length) delete feature.inlets; + else feature.inlets = inlets; + + const outlet = feature.outlet && pack.rivers.find(river => river.i === feature.outlet); + if (!outlet) delete feature.outlet; + } + }; + + defineClimateData(heights: Uint8Array) { + const {cells, features} = pack; + const lakeOutCells = new Uint16Array(cells.i.length); + + const getFlux = (lake: PackedGraphFeature) => { + return lake.shoreline.reduce((acc, c) => acc + grid.cells.prec[cells.g[c]], 0); + } + + const getLakeTemp = (lake: PackedGraphFeature) => { + if (lake.cells < 6) return grid.cells.temp[cells.g[lake.firstCell]]; + return rn(mean(lake.shoreline.map(c => grid.cells.temp[cells.g[c]])) as number, 1); + } + + const getLakeEvaporation = (lake: PackedGraphFeature) => { + const height = (lake.height - 18) ** Number(heightExponentInput.value); // height in meters + const evaporation = ((700 * (lake.temp + 0.006 * height)) / 50 + 75) / (80 - lake.temp); // based on Penman formula, [1-11] + return rn(evaporation * lake.cells); + } + + const getLowestShoreCell = (lake: PackedGraphFeature) => { + return lake.shoreline.sort((a, b) => heights[a] - heights[b])[0]; + } + + features.forEach(feature => { + if (feature.type !== "lake") return; + feature.flux = getFlux(feature); + feature.temp = getLakeTemp(feature); + feature.evaporation = getLakeEvaporation(feature); + if (feature.closed) return; // no outlet for lakes in depressed areas + + feature.outCell = getLowestShoreCell(feature); + lakeOutCells[feature.outCell as number] = feature.i; + }); + + return lakeOutCells; + }; // check if lake can be potentially open (not in deep depression) - const detectCloseLakes = h => { + detectCloseLakes(h: Uint8Array) { const {cells} = pack; - const ELEVATION_LIMIT = +byId("lakeElevationLimitOutput").value; + const ELEVATION_LIMIT = +(byId("lakeElevationLimitOutput") as HTMLInputElement)?.value; pack.features.forEach(feature => { if (feature.type !== "lake") return; @@ -25,7 +111,7 @@ window.Lakes = (function () { checked[lowestShorelineCell] = true; while (queue.length && isDeep) { - const cellId = queue.pop(); + const cellId: number = queue.pop() as number; for (const neibCellId of cells.c[cellId]) { if (checked[neibCellId]) continue; @@ -44,80 +130,6 @@ window.Lakes = (function () { feature.closed = isDeep; }); }; +} - const defineClimateData = function (heights) { - const {cells, features} = pack; - const lakeOutCells = new Uint16Array(cells.i.length); - - features.forEach(feature => { - if (feature.type !== "lake") return; - feature.flux = getFlux(feature); - feature.temp = getLakeTemp(feature); - feature.evaporation = getLakeEvaporation(feature); - if (feature.closed) return; // no outlet for lakes in depressed areas - - feature.outCell = getLowestShoreCell(feature); - lakeOutCells[feature.outCell] = feature.i; - }); - - return lakeOutCells; - - function getFlux(lake) { - return lake.shoreline.reduce((acc, c) => acc + grid.cells.prec[cells.g[c]], 0); - } - - function getLakeTemp(lake) { - if (lake.cells < 6) return grid.cells.temp[cells.g[lake.firstCell]]; - return rn(d3.mean(lake.shoreline.map(c => grid.cells.temp[cells.g[c]])), 1); - } - - function getLakeEvaporation(lake) { - const height = (lake.height - 18) ** heightExponentInput.value; // height in meters - const evaporation = ((700 * (lake.temp + 0.006 * height)) / 50 + 75) / (80 - lake.temp); // based on Penman formula, [1-11] - return rn(evaporation * lake.cells); - } - - function getLowestShoreCell(lake) { - return lake.shoreline.sort((a, b) => heights[a] - heights[b])[0]; - } - }; - - const cleanupLakeData = function () { - for (const feature of pack.features) { - if (feature.type !== "lake") continue; - delete feature.river; - delete feature.enteringFlux; - delete feature.outCell; - delete feature.closed; - feature.height = rn(feature.height, 3); - - const inlets = feature.inlets?.filter(r => pack.rivers.find(river => river.i === r)); - if (!inlets || !inlets.length) delete feature.inlets; - else feature.inlets = inlets; - - const outlet = feature.outlet && pack.rivers.find(river => river.i === feature.outlet); - if (!outlet) delete feature.outlet; - } - }; - - const getHeight = function (feature) { - const heights = pack.cells.h; - const minShoreHeight = d3.min(feature.shoreline.map(cellId => heights[cellId])) || 20; - return rn(minShoreHeight - LAKE_ELEVATION_DELTA, 2); - }; - - const defineNames = function () { - pack.features.forEach(feature => { - if (feature.type !== "lake") return; - feature.name = getName(feature); - }); - }; - - const getName = function (feature) { - const landCell = feature.shoreline[0]; - const culture = pack.cells.culture[landCell]; - return Names.getCulture(culture); - }; - - return {defineClimateData, cleanupLakeData, detectCloseLakes, getHeight, defineNames, getName}; -})(); +window.Lakes = new LakesModule(); \ No newline at end of file diff --git a/src/modules/river-generator.ts b/src/modules/river-generator.ts index 98a567fd..7a6ed447 100644 --- a/src/modules/river-generator.ts +++ b/src/modules/river-generator.ts @@ -7,6 +7,7 @@ rn,round, rw} from "../utils"; import { PackedGraphFeature } from "./features"; import { PackedGraph } from "./PackedGraph"; +import { LakesModule } from "./lakes"; declare global { interface Window { @@ -16,7 +17,7 @@ declare global { var WARN: boolean; var graphHeight: number; var graphWidth: number; - var pack: any; + var pack: PackedGraph; var rivers: Selection; var pointsInput: HTMLInputElement; @@ -25,7 +26,7 @@ declare global { var TIME: boolean; var Names: any; - var Lakes: any; + var Lakes: LakesModule; } export interface River { @@ -114,8 +115,8 @@ class RiverModule { const sameRiver = cells.c[lakeCell].some((c: number) => cells.r[c] === lake.river); if (sameRiver) { - cells.r[lakeCell] = lake.river; - addCellToRiver(lakeCell, lake.river); + cells.r[lakeCell] = lake.river as number; + addCellToRiver(lakeCell, lake.river as number); } else { cells.r[lakeCell] = riverNext; addCellToRiver(lakeCell, riverNext); @@ -132,7 +133,7 @@ class RiverModule { for (const lake of lakes) { if (!Array.isArray(lake.inlets)) continue; for (const inlet of lake.inlets) { - riverParents[inlet] = outlet; + riverParents[inlet] = outlet as number; } } @@ -199,7 +200,7 @@ class RiverModule { // pour water to the water body const waterBody = features[cells.f[toCell]]; if (waterBody.type === "lake") { - if (!waterBody.river || fromFlux > waterBody.enteringFlux) { + if (!waterBody.river || fromFlux > (waterBody.enteringFlux as number)) { waterBody.river = river; waterBody.enteringFlux = fromFlux; } @@ -320,16 +321,16 @@ class RiverModule { TIME && console.timeEnd("generateRivers"); }; - alterHeights() { + alterHeights(): Uint8Array { const {h, c, t} = this.pack.cells as {h: Uint8Array, c: number[][], t: Uint8Array}; - return Array.from(h).map((h, i) => { + return Uint8Array.from(Array.from(h).map((h, i) => { if (h < 20 || t[i] < 1) return h; return h + t[i] / 100 + (mean(c[i].map(c => t[c])) || 0) / 10000; - }); + })); }; // depression filling algorithm (for a correct water flux modeling) - resolveDepressions(h: number[]) { + resolveDepressions(h: Uint8Array) { const {cells, features} = this.pack; const maxIterations = +(document.getElementById("resolveDepressionsStepsOutput") as HTMLInputElement)?.value; const checkLakeMaxIteration = maxIterations * 0.85; diff --git a/src/utils/polyfills.ts b/src/utils/polyfills.ts index 18f5f1bd..89a0d2d9 100644 --- a/src/utils/polyfills.ts +++ b/src/utils/polyfills.ts @@ -44,7 +44,7 @@ declare global { } interface Array { - flat(depth?: number): T[]; + flat(depth?: number): T; at(index: number): T | undefined; }