Files
2026-09-25 17:02:24 +03:00

1259 lines
57 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Web Worker — runs the pure computation pipeline off the main thread.
// Handles: generate, reapply, editRecompute commands.
import { makeRng } from './rng.js';
import { SimplexNoise } from './simplex-noise.js';
import { setDelaunator, buildSphere, generateTriangleCenters, SphereMesh, computeNeighborDist } from './sphere-mesh.js';
import { generateCoarsePlates, projectCoarsePlates } from './coarse-plates.js';
import { smoothAndReconnectPlates } from './plates.js';
import { assignElevation } from './elevation.js';
import { buildSuperPlates } from './super-plates.js';
import { warpTerrain, smoothElevation, erodeComposite, sharpenRidges, applySoilCreep } from './terrain-post.js';
import { computeWind } from './wind.js';
import { computeOceanCurrents } from './ocean.js';
import { computePrecipitation } from './precipitation.js';
import { computeTemperature } from './temperature.js';
import { classifyKoppen } from './koppen.js';
import { computeTerrainMetrics } from './terrain-metrics.js';
import { parseLegend, sampleClassesToMesh, resolveStrokes, roughenCoast, reconcileClasses,
buildUpliftField, solveUplift, toElevation } from './painted.js';
import { sampleMarksToMesh, jitterPerRegion } from './painted-overlay.js';
import Delaunator from 'https://cdn.jsdelivr.net/npm/delaunator@5.0.1/+esm';
setDelaunator(Delaunator);
// Retained state between commands (avoids re-sending mesh for reapply/edit)
let W = null;
function progress(pct, label) {
self.postMessage({ type: 'progress', pct, label });
}
// Compute triangle elevations from region elevations
function computeTriangleElevations(mesh, r_elevation) {
const t_elevation = new Float32Array(mesh.numTriangles);
for (let t = 0; t < mesh.numTriangles; t++) {
const s0 = 3 * t;
const a = mesh.s_begin_r(s0), b = mesh.s_begin_r(s0 + 1), c = mesh.s_begin_r(s0 + 2);
t_elevation[t] = (r_elevation[a] + r_elevation[b] + r_elevation[c]) / 3;
}
return t_elevation;
}
// Run terrain post-processing with per-step timing
function runPostProcessing(mesh, r_xyz, r_elevation, params, neighborDist, seed, r_hotspot) {
const { smoothing, glacialErosion, hydraulicErosion, thermalErosion, ridgeSharpening, terrainWarp } = params;
const timing = [];
// Terrain warp — first step, before ocean detection or smoothing
if (terrainWarp > 0) {
const t0 = performance.now();
warpTerrain(mesh, r_elevation, r_xyz, seed, terrainWarp, r_hotspot);
timing.push({ stage: `Terrain warp (strength=${terrainWarp.toFixed(2)})`, ms: performance.now() - t0 });
}
const r_isOcean = new Uint8Array(mesh.numRegions);
for (let r = 0; r < mesh.numRegions; r++) {
if (r_elevation[r] <= 0) r_isOcean[r] = 1;
}
const preErosion = new Float32Array(r_elevation);
if (smoothing > 0) {
const smoothIters = Math.round(1 + smoothing * 4);
const smoothStr = 0.2 + smoothing * 0.5;
const t0 = performance.now();
smoothElevation(mesh, r_elevation, r_isOcean, smoothIters, smoothStr);
timing.push({ stage: `Smoothing (${smoothIters} iters, str=${smoothStr.toFixed(2)})`, ms: performance.now() - t0 });
}
if (glacialErosion > 0 || hydraulicErosion > 0 || thermalErosion > 0) {
const gIters = Math.round(glacialErosion * 10);
const hIters = Math.round(hydraulicErosion * 20);
const hK = hydraulicErosion * 0.0006;
const tIters = Math.round(thermalErosion * 10);
const talusSlope = 1.2 - thermalErosion * 0.4;
const kThermal = thermalErosion * 0.15;
const t0 = performance.now();
erodeComposite(mesh, r_elevation, r_xyz, r_isOcean,
hIters, hK, 0.5, 1.0,
tIters, talusSlope, kThermal,
gIters, glacialErosion,
neighborDist);
timing.push({ stage: `Erosion composite (h=${hIters}, t=${tIters}, g=${gIters})`, ms: performance.now() - t0 });
}
if (ridgeSharpening > 0) {
const rsIters = Math.round(1 + ridgeSharpening * 3);
const rsStr = ridgeSharpening * 0.08;
const t0 = performance.now();
sharpenRidges(mesh, r_elevation, r_isOcean, rsIters, rsStr);
timing.push({ stage: `Ridge sharpening (${rsIters} iters)`, ms: performance.now() - t0 });
}
{
const t0 = performance.now();
applySoilCreep(mesh, r_elevation, r_isOcean, 3, 0.1125);
timing.push({ stage: 'Soil creep (3 iters)', ms: performance.now() - t0 });
}
const dl_erosionDelta = new Float32Array(mesh.numRegions);
for (let r = 0; r < mesh.numRegions; r++) {
dl_erosionDelta[r] = r_elevation[r] - preErosion[r];
}
return { dl_erosionDelta, postTiming: timing };
}
function getClimateParams(data) {
const temperatureOffset = data?.temperatureOffset ?? W?.temperatureOffset ?? 0;
const precipitationOffset = data?.precipitationOffset ?? W?.precipitationOffset ?? 0;
const landCoverage = data?.landCoverage ?? W?.landCoverage ?? 0.3;
if (W) { W.temperatureOffset = temperatureOffset; W.precipitationOffset = precipitationOffset; W.landCoverage = landCoverage; }
return { temperatureOffset, precipitationOffset, landCoverage };
}
function buildClimateFields(windResult, oceanResult, precipResult, tempResult) {
return {
r_wind_east_summer: windResult?.r_wind_east_summer ?? null,
r_wind_north_summer: windResult?.r_wind_north_summer ?? null,
r_wind_east_winter: windResult?.r_wind_east_winter ?? null,
r_wind_north_winter: windResult?.r_wind_north_winter ?? null,
itczLons: windResult?.itczLons ?? null,
itczLatsSummer: windResult?.itczLatsSummer ?? null,
itczLatsWinter: windResult?.itczLatsWinter ?? null,
r_ocean_current_east_summer: oceanResult?.r_ocean_current_east_summer ?? null,
r_ocean_current_north_summer: oceanResult?.r_ocean_current_north_summer ?? null,
r_ocean_current_east_winter: oceanResult?.r_ocean_current_east_winter ?? null,
r_ocean_current_north_winter: oceanResult?.r_ocean_current_north_winter ?? null,
r_ocean_speed_summer: oceanResult?.r_ocean_speed_summer ?? null,
r_ocean_speed_winter: oceanResult?.r_ocean_speed_winter ?? null,
r_ocean_warmth_summer: oceanResult?.r_ocean_warmth_summer ?? null,
r_ocean_warmth_winter: oceanResult?.r_ocean_warmth_winter ?? null,
r_precip_summer: precipResult?.r_precip_summer ?? null,
r_precip_winter: precipResult?.r_precip_winter ?? null,
r_temperature_summer: tempResult?.r_temperature_summer ?? null,
r_temperature_winter: tempResult?.r_temperature_winter ?? null,
};
}
function handleGenerate(data) {
const { N, P, jitter, nMag, numContinents, smoothing, hydraulicErosion, thermalErosion, ridgeSharpening, glacialErosion, terrainWarp, continentSizeVariety = 0, temperatureOffset = 0, precipitationOffset = 0, landCoverage = 0.3, seed: overrideSeed, toggledIndices, skipClimate } = data;
const spread = 5;
const timing = []; // top-level pipeline timing
try {
const tTotal0 = performance.now();
progress(0, 'Shaping the world\u2026');
const seed = overrideSeed ?? Math.floor(Math.random() * 16777216);
const rng = makeRng(seed);
let t0 = performance.now();
const { mesh, r_xyz } = buildSphere(N, jitter, rng);
timing.push({ stage: 'Sphere mesh (Fibonacci + Delaunay + pole)', ms: performance.now() - t0 });
t0 = performance.now();
const neighborDist = computeNeighborDist(mesh, r_xyz);
timing.push({ stage: 'Neighbor distances', ms: performance.now() - t0 });
t0 = performance.now();
const t_xyz = generateTriangleCenters(mesh, r_xyz);
timing.push({ stage: 'Triangle centers', ms: performance.now() - t0 });
progress(10, 'Generating coarse plates\u2026');
t0 = performance.now();
const { coarseMesh, coarse_xyz, coarse_r_plate, coarsePlateSeeds, coarsePlateVec, coarsePlateIsOcean } =
generateCoarsePlates(seed, P, numContinents, continentSizeVariety, landCoverage);
timing.push({ stage: `Coarse plates (${P} plates, ${numContinents} continents)`, ms: performance.now() - t0 });
progress(20, 'Projecting plates\u2026');
t0 = performance.now();
const r_plate = projectCoarsePlates(mesh, r_xyz, coarseMesh, coarse_xyz, coarse_r_plate, seed, P);
timing.push({ stage: 'Project coarse → hi-res', ms: performance.now() - t0 });
progress(25, 'Smoothing boundaries\u2026');
t0 = performance.now();
smoothAndReconnectPlates(mesh, r_plate, coarsePlateSeeds, 3);
timing.push({ stage: 'Smooth projected plates', ms: performance.now() - t0 });
const plateSeeds = coarsePlateSeeds;
const plateVec = coarsePlateVec;
const plateIsOcean = coarsePlateIsOcean;
const originalPlateIsOcean = new Set(plateIsOcean);
if (toggledIndices && toggledIndices.length > 0) {
const seedArr = Array.from(plateSeeds);
for (const i of toggledIndices) {
if (i < seedArr.length) {
const r = seedArr[i];
if (plateIsOcean.has(r)) plateIsOcean.delete(r);
else plateIsOcean.add(r);
}
}
}
const plateDensity = {};
const plateDensityLand = {};
const plateDensityOcean = {};
for (const r of plateSeeds) {
const drng = makeRng(r + 777);
plateDensityOcean[r] = 3.0 + drng() * 0.5;
plateDensityLand[r] = 2.4 + drng() * 0.5;
plateDensity[r] = plateIsOcean.has(r) ? plateDensityOcean[r] : plateDensityLand[r];
}
const noise = new SimplexNoise(seed);
// Build super plates for broad orogenic belts (skip if too few plates)
let superPlateData = null;
if (P >= 8) {
t0 = performance.now();
superPlateData = buildSuperPlates(mesh, r_plate, plateSeeds, plateVec, plateIsOcean, plateDensity);
timing.push({ stage: `Super plates (${superPlateData.numSuperPlates} groups from ${P} plates)`, ms: performance.now() - t0 });
}
progress(35, 'Raising mountains\u2026');
t0 = performance.now();
const { r_elevation, mountain_r, coastline_r, ocean_r, r_stress, debugLayers, _timing } =
assignElevation(mesh, r_xyz, plateIsOcean, r_plate, plateVec, plateSeeds, noise, nMag, seed, spread, plateDensity, superPlateData);
timing.push({ stage: 'Elevation (collisions + stress + distance fields + assignment)', ms: performance.now() - t0 });
const prePostElev = new Float32Array(r_elevation);
progress(60, 'Eroding terrain\u2026');
t0 = performance.now();
const { dl_erosionDelta, postTiming } = runPostProcessing(mesh, r_xyz, r_elevation, { smoothing, glacialErosion, hydraulicErosion, thermalErosion, ridgeSharpening, terrainWarp }, neighborDist, seed, debugLayers.hotspot);
timing.push({ stage: 'Terrain post-processing (total)', ms: performance.now() - t0 });
debugLayers.erosionDelta = dl_erosionDelta;
let windResult = null, oceanResult = null, precipResult = null, tempResult = null;
if (!skipClimate) {
progress(70, 'Simulating wind patterns\u2026');
t0 = performance.now();
windResult = computeWind(mesh, r_xyz, r_elevation, plateIsOcean, r_plate, noise);
timing.push({ stage: 'Wind simulation', ms: performance.now() - t0 });
if (windResult._windTiming) timing.push(...windResult._windTiming);
debugLayers.pressureSummer = windResult.r_pressure_summer;
debugLayers.pressureWinter = windResult.r_pressure_winter;
debugLayers.windSpeedSummer = windResult.r_wind_speed_summer;
debugLayers.windSpeedWinter = windResult.r_wind_speed_winter;
debugLayers.continentality = windResult.r_continentality;
progress(78, 'Computing ocean currents\u2026');
t0 = performance.now();
oceanResult = computeOceanCurrents(mesh, r_xyz, r_elevation, windResult);
timing.push({ stage: 'Ocean currents', ms: performance.now() - t0 });
if (oceanResult._oceanTiming) timing.push(...oceanResult._oceanTiming);
progress(82, 'Computing precipitation\u2026');
t0 = performance.now();
precipResult = computePrecipitation(mesh, r_xyz, r_elevation, windResult, oceanResult, precipitationOffset, landCoverage);
timing.push({ stage: 'Precipitation', ms: performance.now() - t0 });
if (precipResult._precipTiming) timing.push(...precipResult._precipTiming);
debugLayers.precipSummer = precipResult.r_precip_summer;
debugLayers.precipWinter = precipResult.r_precip_winter;
debugLayers.rainShadowSummer = precipResult.r_rainshadow_summer;
debugLayers.rainShadowWinter = precipResult.r_rainshadow_winter;
progress(86, 'Computing temperature\u2026');
t0 = performance.now();
tempResult = computeTemperature(mesh, r_xyz, r_elevation, windResult, oceanResult, precipResult, temperatureOffset);
timing.push({ stage: 'Temperature', ms: performance.now() - t0 });
if (tempResult._tempTiming) timing.push(...tempResult._tempTiming);
debugLayers.tempSummer = tempResult.r_temperature_summer;
debugLayers.tempWinter = tempResult.r_temperature_winter;
t0 = performance.now();
debugLayers.koppen = classifyKoppen(mesh, r_elevation, tempResult, precipResult);
timing.push({ stage: 'Köppen classification', ms: performance.now() - t0 });
}
progress(skipClimate ? 75 : 90, 'Computing triangle elevations\u2026');
t0 = performance.now();
const t_elevation = computeTriangleElevations(mesh, r_elevation);
timing.push({ stage: 'Triangle elevations', ms: performance.now() - t0 });
t0 = performance.now();
// Retain state for reapply/edit (clone what we'll transfer)
W = {
mesh, r_xyz: new Float32Array(r_xyz), t_xyz: new Float32Array(t_xyz),
neighborDist,
r_plate: new Int32Array(r_plate), plateSeeds: new Set(plateSeeds), plateVec,
plateIsOcean: new Set(plateIsOcean), originalPlateIsOcean: new Set(originalPlateIsOcean),
plateDensity: Object.assign({}, plateDensity),
plateDensityLand: Object.assign({}, plateDensityLand),
plateDensityOcean: Object.assign({}, plateDensityOcean),
prePostElev: new Float32Array(prePostElev),
r_elevation_final: new Float32Array(r_elevation),
seed, nMag, noise, P,
mountain_r: new Set(mountain_r), coastline_r: new Set(coastline_r), ocean_r: new Set(ocean_r),
r_stress: new Float32Array(r_stress),
temperatureOffset, precipitationOffset, landCoverage,
cachedWind: windResult, cachedOcean: oceanResult
};
timing.push({ stage: 'Clone state for retention', ms: performance.now() - t0 });
// Compute terrain quality metrics using retained-state clones
// (the originals will be transferred and neutered below).
let terrainMetrics = null;
try {
terrainMetrics = computeTerrainMetrics({
mesh: W.mesh,
r_xyz: W.r_xyz,
r_elevation: W.r_elevation_final,
r_plate: W.r_plate,
plateIsOcean: Array.from(W.plateIsOcean),
r_stress: W.r_stress,
debugLayers,
prePostElev: W.prePostElev,
});
} catch (e) {
terrainMetrics = { _error: e.message };
}
const tWorkerTotal = performance.now() - tTotal0;
// Build result — typed arrays we no longer need are transferred (zero-copy).
// mesh.triangles/halfedges are NOT transferred because W.mesh retains them.
const result = {
type: 'done',
triangles: mesh.triangles,
halfedges: mesh.halfedges,
numRegions: mesh.numRegions,
r_xyz, t_xyz, r_plate,
plateSeeds: Array.from(plateSeeds),
plateVec,
plateIsOcean: Array.from(plateIsOcean),
originalPlateIsOcean: Array.from(originalPlateIsOcean),
plateDensity, plateDensityLand, plateDensityOcean,
prePostElev,
r_elevation, t_elevation,
mountain_r: Array.from(mountain_r),
coastline_r: Array.from(coastline_r),
ocean_r: Array.from(ocean_r),
r_stress,
...buildClimateFields(windResult, oceanResult, precipResult, tempResult),
skipClimate: !!skipClimate,
seed, nMag,
debugLayers,
_timing, // elevation sub-stages from assignElevation
_pipelineTiming: timing, // top-level pipeline stages
_postTiming: postTiming, // post-processing sub-stages
_workerTotal: tWorkerTotal,
_params: { N, P, jitter, nMag, numContinents, smoothing, terrainWarp, hydraulicErosion, thermalErosion, ridgeSharpening, glacialErosion, continentSizeVariety, temperatureOffset, precipitationOffset, landCoverage, seed },
terrainMetrics
};
// Transfer arrays the worker no longer needs (cloned copies kept in W)
const transferList = [
r_xyz.buffer, t_xyz.buffer, r_plate.buffer,
prePostElev.buffer, r_elevation.buffer, t_elevation.buffer,
r_stress.buffer
];
self.postMessage(result, transferList);
} catch (err) {
self.postMessage({ type: 'error', message: err.message, stack: err.stack });
}
}
function handleReapply(data) {
if (!W) { self.postMessage({ type: 'error', message: 'No retained state for reapply' }); return; }
const skipClimate = !!data.skipClimate;
const { temperatureOffset, precipitationOffset, landCoverage } = getClimateParams(data);
try {
const tTotal0 = performance.now();
progress(0, 'Reapplying terrain\u2026');
let t0 = performance.now();
const r_elevation = new Float32Array(W.prePostElev);
const tClone = performance.now() - t0;
progress(20, 'Eroding terrain\u2026');
t0 = performance.now();
const { dl_erosionDelta, postTiming } = runPostProcessing(W.mesh, W.r_xyz, r_elevation, data, W.neighborDist, W.seed);
const tPost = performance.now() - t0;
// Update retained final elevation for deferred climate
W.r_elevation_final = new Float32Array(r_elevation);
let windResult = null, oceanResult = null, precipResult = null, tempResult = null;
let tWind = 0, tOcean = 0, tPrecip = 0, tTemp = 0;
if (!skipClimate) {
progress(60, 'Simulating wind patterns\u2026');
t0 = performance.now();
windResult = computeWind(W.mesh, W.r_xyz, r_elevation, W.plateIsOcean, W.r_plate, W.noise);
tWind = performance.now() - t0;
progress(75, 'Computing ocean currents\u2026');
t0 = performance.now();
oceanResult = computeOceanCurrents(W.mesh, W.r_xyz, r_elevation, windResult);
tOcean = performance.now() - t0;
progress(80, 'Computing precipitation\u2026');
t0 = performance.now();
precipResult = computePrecipitation(W.mesh, W.r_xyz, r_elevation, windResult, oceanResult, precipitationOffset, landCoverage);
tPrecip = performance.now() - t0;
progress(85, 'Computing temperature\u2026');
t0 = performance.now();
tempResult = computeTemperature(W.mesh, W.r_xyz, r_elevation, windResult, oceanResult, precipResult, temperatureOffset);
tTemp = performance.now() - t0;
W.cachedWind = windResult;
W.cachedOcean = oceanResult;
} else {
W.cachedWind = null;
W.cachedOcean = null;
}
progress(skipClimate ? 70 : 90, 'Computing triangle elevations\u2026');
t0 = performance.now();
const t_elevation = computeTriangleElevations(W.mesh, r_elevation);
const tTriElev = performance.now() - t0;
const tWorkerTotal = performance.now() - tTotal0;
const result = {
type: 'reapplyDone',
skipClimate,
r_elevation,
t_elevation,
erosionDelta: dl_erosionDelta,
...buildClimateFields(windResult, oceanResult, precipResult, tempResult),
windDebugLayers: windResult ? {
pressureSummer: windResult.r_pressure_summer,
pressureWinter: windResult.r_pressure_winter,
windSpeedSummer: windResult.r_wind_speed_summer,
windSpeedWinter: windResult.r_wind_speed_winter,
precipSummer: precipResult.r_precip_summer,
precipWinter: precipResult.r_precip_winter,
rainShadowSummer: precipResult.r_rainshadow_summer,
rainShadowWinter: precipResult.r_rainshadow_winter,
tempSummer: tempResult.r_temperature_summer,
tempWinter: tempResult.r_temperature_winter,
koppen: classifyKoppen(W.mesh, r_elevation, tempResult, precipResult)
} : null,
_reapplyTiming: {
clone: tClone,
postProcessing: tPost,
wind: tWind,
ocean: tOcean,
precipitation: tPrecip,
temperature: tTemp,
triangleElevations: tTriElev,
workerTotal: tWorkerTotal
},
_postTiming: postTiming
};
self.postMessage(result, [r_elevation.buffer, t_elevation.buffer, dl_erosionDelta.buffer]);
} catch (err) {
self.postMessage({ type: 'error', message: err.message, stack: err.stack });
}
}
function handleEditRecompute(data) {
if (!W) { self.postMessage({ type: 'error', message: 'No retained state for editRecompute' }); return; }
const skipClimate = !!data.skipClimate;
const { temperatureOffset, precipitationOffset, landCoverage } = getClimateParams(data);
try {
const tTotal0 = performance.now();
progress(0, 'Rebuilding elevation\u2026');
// Update retained plate state
W.plateIsOcean = new Set(data.plateIsOcean);
W.plateDensity = Object.assign({}, data.plateDensity);
const { mesh, r_xyz, plateIsOcean, r_plate, plateVec, plateSeeds, noise, seed } = W;
const nMag = data.nMag;
const spread = 5;
// Rebuild super plates from updated plate ocean/density state
let superPlateData = null;
if ((W.P || 0) >= 8) {
superPlateData = buildSuperPlates(mesh, r_plate, plateSeeds, plateVec, plateIsOcean, W.plateDensity);
}
let t0 = performance.now();
const { r_elevation, mountain_r, coastline_r, ocean_r, r_stress, debugLayers, _timing } =
assignElevation(mesh, r_xyz, plateIsOcean, r_plate, plateVec, plateSeeds, noise, nMag, seed, spread, W.plateDensity, superPlateData);
const tElev = performance.now() - t0;
const prePostElev = new Float32Array(r_elevation);
progress(50, 'Eroding terrain\u2026');
t0 = performance.now();
const { dl_erosionDelta, postTiming } = runPostProcessing(mesh, r_xyz, r_elevation, data, W.neighborDist, W.seed, debugLayers.hotspot);
const tPost = performance.now() - t0;
debugLayers.erosionDelta = dl_erosionDelta;
// Update retained final elevation for deferred climate
W.r_elevation_final = new Float32Array(r_elevation);
let windResult = null, oceanResult = null, precipResult = null, tempResult = null;
let tWind = 0, tOcean = 0, tPrecip = 0, tTemp = 0;
if (!skipClimate) {
progress(65, 'Simulating wind patterns\u2026');
t0 = performance.now();
windResult = computeWind(mesh, r_xyz, r_elevation, plateIsOcean, r_plate, W.noise);
tWind = performance.now() - t0;
debugLayers.pressureSummer = windResult.r_pressure_summer;
debugLayers.pressureWinter = windResult.r_pressure_winter;
debugLayers.windSpeedSummer = windResult.r_wind_speed_summer;
debugLayers.windSpeedWinter = windResult.r_wind_speed_winter;
debugLayers.continentality = windResult.r_continentality;
progress(78, 'Computing ocean currents\u2026');
t0 = performance.now();
oceanResult = computeOceanCurrents(mesh, r_xyz, r_elevation, windResult);
tOcean = performance.now() - t0;
progress(82, 'Computing precipitation\u2026');
t0 = performance.now();
precipResult = computePrecipitation(mesh, r_xyz, r_elevation, windResult, oceanResult, precipitationOffset, landCoverage);
tPrecip = performance.now() - t0;
debugLayers.precipSummer = precipResult.r_precip_summer;
debugLayers.precipWinter = precipResult.r_precip_winter;
debugLayers.rainShadowSummer = precipResult.r_rainshadow_summer;
debugLayers.rainShadowWinter = precipResult.r_rainshadow_winter;
progress(86, 'Computing temperature\u2026');
t0 = performance.now();
tempResult = computeTemperature(mesh, r_xyz, r_elevation, windResult, oceanResult, precipResult, temperatureOffset);
tTemp = performance.now() - t0;
debugLayers.tempSummer = tempResult.r_temperature_summer;
debugLayers.tempWinter = tempResult.r_temperature_winter;
debugLayers.koppen = classifyKoppen(mesh, r_elevation, tempResult, precipResult);
W.cachedWind = windResult;
W.cachedOcean = oceanResult;
} else {
W.cachedWind = null;
W.cachedOcean = null;
}
progress(skipClimate ? 75 : 90, 'Computing triangle elevations\u2026');
t0 = performance.now();
const t_elevation = computeTriangleElevations(mesh, r_elevation);
const tTriElev = performance.now() - t0;
// Update retained state
t0 = performance.now();
W.prePostElev = new Float32Array(prePostElev);
W.mountain_r = new Set(mountain_r);
W.coastline_r = new Set(coastline_r);
W.ocean_r = new Set(ocean_r);
W.r_stress = new Float32Array(r_stress);
const tRetain = performance.now() - t0;
const tWorkerTotal = performance.now() - tTotal0;
const result = {
type: 'editDone',
skipClimate,
prePostElev,
r_elevation,
t_elevation,
mountain_r: Array.from(mountain_r),
coastline_r: Array.from(coastline_r),
ocean_r: Array.from(ocean_r),
r_stress,
...buildClimateFields(windResult, oceanResult, precipResult, tempResult),
debugLayers,
_editTiming: {
elevation: tElev,
postProcessing: tPost,
wind: tWind,
ocean: tOcean,
precipitation: tPrecip,
temperature: tTemp,
triangleElevations: tTriElev,
retainState: tRetain,
workerTotal: tWorkerTotal
},
_timing, // elevation sub-stages
_postTiming: postTiming
};
self.postMessage(result, [
prePostElev.buffer, r_elevation.buffer, t_elevation.buffer, r_stress.buffer
]);
} catch (err) {
self.postMessage({ type: 'error', message: err.message, stack: err.stack });
}
}
function handleComputeClimate(data) {
if (!W) { self.postMessage({ type: 'error', message: 'No retained state for computeClimate' }); return; }
const { temperatureOffset, precipitationOffset, landCoverage } = getClimateParams(data);
try {
const tTotal0 = performance.now();
const { mesh, r_xyz, r_elevation_final, plateIsOcean, r_plate, noise } = W;
let windResult = W.cachedWind;
let oceanResult = W.cachedOcean;
let tWind = 0, tOcean = 0;
let t0;
if (!windResult) {
progress(0, 'Simulating wind patterns\u2026');
t0 = performance.now();
windResult = computeWind(mesh, r_xyz, r_elevation_final, plateIsOcean, r_plate, noise);
tWind = performance.now() - t0;
progress(30, 'Computing ocean currents\u2026');
t0 = performance.now();
oceanResult = computeOceanCurrents(mesh, r_xyz, r_elevation_final, windResult);
tOcean = performance.now() - t0;
W.cachedWind = windResult;
W.cachedOcean = oceanResult;
}
progress(50, 'Computing precipitation\u2026');
t0 = performance.now();
const precipResult = computePrecipitation(mesh, r_xyz, r_elevation_final, windResult, oceanResult, precipitationOffset, landCoverage);
const tPrecip = performance.now() - t0;
progress(70, 'Computing temperature\u2026');
t0 = performance.now();
const tempResult = computeTemperature(mesh, r_xyz, r_elevation_final, windResult, oceanResult, precipResult, temperatureOffset);
const tTemp = performance.now() - t0;
progress(88, 'Classifying climates\u2026');
t0 = performance.now();
const koppen = classifyKoppen(mesh, r_elevation_final, tempResult, precipResult);
const tKoppen = performance.now() - t0;
const tWorkerTotal = performance.now() - tTotal0;
const climateDebugLayers = {
pressureSummer: windResult.r_pressure_summer,
pressureWinter: windResult.r_pressure_winter,
windSpeedSummer: windResult.r_wind_speed_summer,
windSpeedWinter: windResult.r_wind_speed_winter,
continentality: windResult.r_continentality,
precipSummer: precipResult.r_precip_summer,
precipWinter: precipResult.r_precip_winter,
rainShadowSummer: precipResult.r_rainshadow_summer,
rainShadowWinter: precipResult.r_rainshadow_winter,
tempSummer: tempResult.r_temperature_summer,
tempWinter: tempResult.r_temperature_winter,
koppen
};
progress(95, 'Done');
self.postMessage({
type: 'climateDone',
r_wind_east_summer: windResult.r_wind_east_summer,
r_wind_north_summer: windResult.r_wind_north_summer,
r_wind_east_winter: windResult.r_wind_east_winter,
r_wind_north_winter: windResult.r_wind_north_winter,
itczLons: windResult.itczLons,
itczLatsSummer: windResult.itczLatsSummer,
itczLatsWinter: windResult.itczLatsWinter,
r_ocean_current_east_summer: oceanResult.r_ocean_current_east_summer,
r_ocean_current_north_summer: oceanResult.r_ocean_current_north_summer,
r_ocean_current_east_winter: oceanResult.r_ocean_current_east_winter,
r_ocean_current_north_winter: oceanResult.r_ocean_current_north_winter,
r_ocean_speed_summer: oceanResult.r_ocean_speed_summer,
r_ocean_speed_winter: oceanResult.r_ocean_speed_winter,
r_ocean_warmth_summer: oceanResult.r_ocean_warmth_summer,
r_ocean_warmth_winter: oceanResult.r_ocean_warmth_winter,
r_precip_summer: precipResult.r_precip_summer,
r_precip_winter: precipResult.r_precip_winter,
r_temperature_summer: tempResult.r_temperature_summer,
r_temperature_winter: tempResult.r_temperature_winter,
climateDebugLayers,
_climateTiming: {
wind: tWind,
ocean: tOcean,
precipitation: tPrecip,
temperature: tTemp,
koppen: tKoppen,
workerTotal: tWorkerTotal
}
});
} catch (err) {
self.postMessage({ type: 'error', message: err.message, stack: err.stack });
}
}
// ─── Heightmap import ───────────────────────────────────────────────
/** Bilinear interpolation with equirectangular wrapping. */
function sampleBilinear(pixels, imgW, imgH, px, py) {
// Clamp vertically, wrap horizontally
py = Math.max(0, Math.min(py, imgH - 1));
const x0 = Math.floor(px), y0 = Math.floor(py);
const x1 = (x0 + 1) % imgW; // horizontal wrap
const y1 = Math.min(y0 + 1, imgH - 1); // vertical clamp
const fx = px - x0, fy = py - y0;
const v00 = pixels[y0 * imgW + ((x0 % imgW) + imgW) % imgW];
const v10 = pixels[y0 * imgW + x1];
const v01 = pixels[y1 * imgW + ((x0 % imgW) + imgW) % imgW];
const v11 = pixels[y1 * imgW + x1];
return (v00 * (1 - fx) * (1 - fy) +
v10 * fx * (1 - fy) +
v01 * (1 - fx) * fy +
v11 * fx * fy);
}
/**
* Convert grayscale 0–255 to internal elevation.
* 0 → -0.5 (ocean floor)
* 1–255 → inverse of 6·t² so grayscale maps linearly to km.
* Simple sqrt inversion: t = sqrt((v-1) / 254).
*/
function grayscaleToElevation(v) {
if (v < 1) return -0.5; // ocean (black pixels; catches interpolated fractional values too)
return Math.sqrt((v - 1) / 254);
}
/**
* Sample an equirectangular grayscale heightmap onto sphere mesh regions.
* Returns r_elevation (Float32Array).
*/
function sampleHeightmap(mesh, r_xyz, imageData, imgW, imgH) {
const r_elevation = new Float32Array(mesh.numRegions);
for (let r = 0; r < mesh.numRegions; r++) {
const x = r_xyz[3 * r], y = r_xyz[3 * r + 1], z = r_xyz[3 * r + 2];
const lat = Math.asin(Math.max(-1, Math.min(1, y)));
const lon = Math.atan2(x, z);
// Map lat/lon → pixel coords (equirectangular)
const px = (lon / Math.PI + 1) * 0.5 * imgW; // 0..W
const py = (0.5 - lat / Math.PI) * imgH; // 0..H
const gray = sampleBilinear(imageData, imgW, imgH, px, py);
r_elevation[r] = grayscaleToElevation(gray);
}
return r_elevation;
}
/**
* BFS flood fill to derive synthetic plates from elevation.
* Creates one "plate" per connected land mass and one per connected ocean basin.
*/
function deriveSyntheticPlates(mesh, r_elevation) {
const N = mesh.numRegions;
const r_plate = new Int32Array(N).fill(-1);
const plateSeeds = new Set();
const plateIsOcean = new Set();
const plateVec = {};
const { adjOffset, adjList } = mesh;
let plateId = 0;
for (let r = 0; r < N; r++) {
if (r_plate[r] >= 0) continue;
const isOcean = r_elevation[r] <= 0;
// BFS from this region
r_plate[r] = r; // use r as the plate seed
plateSeeds.add(r);
plateVec[r] = [0, 0, 0]; // zero velocity
if (isOcean) plateIsOcean.add(r);
const queue = [r];
let head = 0;
while (head < queue.length) {
const cur = queue[head++];
const end = adjOffset[cur + 1];
for (let ni = adjOffset[cur]; ni < end; ni++) {
const nb = adjList[ni];
if (r_plate[nb] >= 0) continue;
const nbOcean = r_elevation[nb] <= 0;
if (nbOcean === isOcean) {
r_plate[nb] = r;
queue.push(nb);
}
}
}
plateId++;
}
return { r_plate, plateSeeds, plateIsOcean, plateVec };
}
function handleImportHeightmap(data) {
const { N, jitter, grayscale, imageWidth, imageHeight, smoothing, hydraulicErosion, thermalErosion, ridgeSharpening, glacialErosion, terrainWarp, temperatureOffset = 0, precipitationOffset = 0, landCoverage = 0.3, seed: overrideSeed, skipClimate } = data;
const timing = [];
try {
const tTotal0 = performance.now();
progress(0, 'Building sphere mesh\u2026');
const seed = overrideSeed ?? Math.floor(Math.random() * 16777216);
const rng = makeRng(seed);
let t0 = performance.now();
const { mesh, r_xyz } = buildSphere(N, jitter, rng);
timing.push({ stage: 'Sphere mesh', ms: performance.now() - t0 });
t0 = performance.now();
const neighborDist = computeNeighborDist(mesh, r_xyz);
timing.push({ stage: 'Neighbor distances', ms: performance.now() - t0 });
t0 = performance.now();
const t_xyz = generateTriangleCenters(mesh, r_xyz);
timing.push({ stage: 'Triangle centers', ms: performance.now() - t0 });
progress(20, 'Sampling heightmap\u2026');
t0 = performance.now();
const r_elevation = sampleHeightmap(mesh, r_xyz, grayscale, imageWidth, imageHeight);
timing.push({ stage: 'Sample heightmap', ms: performance.now() - t0 });
const prePostElev = new Float32Array(r_elevation);
progress(35, 'Processing terrain\u2026');
t0 = performance.now();
const { dl_erosionDelta, postTiming } = runPostProcessing(mesh, r_xyz, r_elevation, { smoothing, glacialErosion, hydraulicErosion, thermalErosion, ridgeSharpening, terrainWarp }, neighborDist, seed);
timing.push({ stage: 'Terrain post-processing', ms: performance.now() - t0 });
progress(50, 'Deriving plates\u2026');
t0 = performance.now();
const { r_plate, plateSeeds, plateIsOcean, plateVec } = deriveSyntheticPlates(mesh, r_elevation);
timing.push({ stage: 'Synthetic plates', ms: performance.now() - t0 });
// Classify regions
const mountain_r = new Set();
const coastline_r = new Set();
const ocean_r = new Set();
for (let r = 0; r < mesh.numRegions; r++) {
if (r_elevation[r] <= 0) {
ocean_r.add(r);
} else if (r_elevation[r] > 0.5) {
mountain_r.add(r);
}
// Coastline: land cell adjacent to ocean
if (r_elevation[r] > 0) {
const end = mesh.adjOffset[r + 1];
for (let ni = mesh.adjOffset[r]; ni < end; ni++) {
if (r_elevation[mesh.adjList[ni]] <= 0) {
coastline_r.add(r);
break;
}
}
}
}
const r_stress = new Float32Array(mesh.numRegions); // no stress for imports
const debugLayers = { erosionDelta: dl_erosionDelta };
const nMag = 0;
let windResult = null, oceanResult = null, precipResult = null, tempResult = null;
if (!skipClimate) {
const noise = new SimplexNoise(seed);
progress(60, 'Simulating wind patterns\u2026');
t0 = performance.now();
windResult = computeWind(mesh, r_xyz, r_elevation, plateIsOcean, r_plate, noise);
timing.push({ stage: 'Wind simulation', ms: performance.now() - t0 });
debugLayers.pressureSummer = windResult.r_pressure_summer;
debugLayers.pressureWinter = windResult.r_pressure_winter;
debugLayers.windSpeedSummer = windResult.r_wind_speed_summer;
debugLayers.windSpeedWinter = windResult.r_wind_speed_winter;
debugLayers.continentality = windResult.r_continentality;
progress(72, 'Computing ocean currents\u2026');
t0 = performance.now();
oceanResult = computeOceanCurrents(mesh, r_xyz, r_elevation, windResult);
timing.push({ stage: 'Ocean currents', ms: performance.now() - t0 });
progress(80, 'Computing precipitation\u2026');
t0 = performance.now();
precipResult = computePrecipitation(mesh, r_xyz, r_elevation, windResult, oceanResult, precipitationOffset, landCoverage);
timing.push({ stage: 'Precipitation', ms: performance.now() - t0 });
debugLayers.precipSummer = precipResult.r_precip_summer;
debugLayers.precipWinter = precipResult.r_precip_winter;
debugLayers.rainShadowSummer = precipResult.r_rainshadow_summer;
debugLayers.rainShadowWinter = precipResult.r_rainshadow_winter;
progress(88, 'Computing temperature\u2026');
t0 = performance.now();
tempResult = computeTemperature(mesh, r_xyz, r_elevation, windResult, oceanResult, precipResult, temperatureOffset);
timing.push({ stage: 'Temperature', ms: performance.now() - t0 });
debugLayers.tempSummer = tempResult.r_temperature_summer;
debugLayers.tempWinter = tempResult.r_temperature_winter;
t0 = performance.now();
debugLayers.koppen = classifyKoppen(mesh, r_elevation, tempResult, precipResult);
timing.push({ stage: 'Köppen classification', ms: performance.now() - t0 });
}
progress(skipClimate ? 75 : 92, 'Computing triangle elevations\u2026');
t0 = performance.now();
const t_elevation = computeTriangleElevations(mesh, r_elevation);
timing.push({ stage: 'Triangle elevations', ms: performance.now() - t0 });
// Retain state for reapply
t0 = performance.now();
W = {
mesh, r_xyz: new Float32Array(r_xyz), t_xyz: new Float32Array(t_xyz),
neighborDist,
r_plate: new Int32Array(r_plate), plateSeeds: new Set(plateSeeds), plateVec,
plateIsOcean: new Set(plateIsOcean), originalPlateIsOcean: new Set(plateIsOcean),
plateDensity: {}, plateDensityLand: {}, plateDensityOcean: {},
prePostElev: new Float32Array(prePostElev),
r_elevation_final: new Float32Array(r_elevation),
seed, nMag, noise: new SimplexNoise(seed),
mountain_r: new Set(mountain_r), coastline_r: new Set(coastline_r), ocean_r: new Set(ocean_r),
r_stress: new Float32Array(r_stress),
cachedWind: windResult, cachedOcean: oceanResult
};
timing.push({ stage: 'Clone state for retention', ms: performance.now() - t0 });
const tWorkerTotal = performance.now() - tTotal0;
// Build result — same shape as handleGenerate's 'done' message
const result = {
type: 'done',
triangles: mesh.triangles,
halfedges: mesh.halfedges,
numRegions: mesh.numRegions,
r_xyz, t_xyz, r_plate,
plateSeeds: Array.from(plateSeeds),
plateVec,
plateIsOcean: Array.from(plateIsOcean),
originalPlateIsOcean: Array.from(plateIsOcean),
plateDensity: {}, plateDensityLand: {}, plateDensityOcean: {},
prePostElev,
r_elevation, t_elevation,
mountain_r: Array.from(mountain_r),
coastline_r: Array.from(coastline_r),
ocean_r: Array.from(ocean_r),
r_stress,
...buildClimateFields(windResult, oceanResult, precipResult, tempResult),
skipClimate: !!skipClimate,
seed, nMag,
debugLayers,
_timing: [],
_pipelineTiming: timing,
_postTiming: postTiming,
_workerTotal: tWorkerTotal,
_params: { N, P: 0, jitter, nMag, numContinents: 0, smoothing, terrainWarp, hydraulicErosion, thermalErosion, ridgeSharpening, glacialErosion, seed }
};
const transferList = [
r_xyz.buffer, t_xyz.buffer, r_plate.buffer,
prePostElev.buffer, r_elevation.buffer, t_elevation.buffer,
r_stress.buffer
];
self.postMessage(result, transferList);
} catch (err) {
self.postMessage({ type: 'error', message: err.message, stack: err.stack });
}
}
// ─── Painted-map import ─────────────────────────────────────────────
//
// The painting says where the land rises and how fast (a class per colour, from the legend);
// the stream-power solve in painted.js turns that into terrain with real rivers and divides.
// Afterwards it is an imported world like any other: the sculpting sliders, climate and export
// all run on the result, and the class, uplift, erodibility, drainage, slope and basin layers
// ride along as debug layers.
function handleImportPainted(data) {
const { N, jitter, classRaster, imageWidth, imageHeight, legend: legendJson, painted: pp, overlay: ovIn = null,
smoothing, hydraulicErosion, thermalErosion, ridgeSharpening, glacialErosion, terrainWarp,
temperatureOffset = 0, precipitationOffset = 0, skipClimate } = data;
const timing = [];
try {
const tTotal0 = performance.now();
const legend = parseLegend(legendJson);
const seed = (pp.seed | 0) || 7945;
const rng = makeRng(seed);
progress(0, 'Building sphere mesh…');
let t0 = performance.now();
const { mesh, r_xyz } = buildSphere(N, jitter, rng);
timing.push({ stage: 'Sphere mesh', ms: performance.now() - t0 });
t0 = performance.now();
const neighborDist = computeNeighborDist(mesh, r_xyz);
const t_xyz = generateTriangleCenters(mesh, r_xyz);
timing.push({ stage: 'Neighbor distances + triangle centers', ms: performance.now() - t0 });
progress(8, 'Reading the painting…');
t0 = performance.now();
const { r_class, r_lat } = sampleClassesToMesh(mesh, r_xyz, classRaster, imageWidth, imageHeight, legend.classes.length);
const strokes = resolveStrokes(mesh, r_class, r_lat, legend);
timing.push({ stage: 'Vote classes onto regions', ms: performance.now() - t0 });
let r_land = new Uint8Array(mesh.numRegions);
for (let r = 0; r < mesh.numRegions; r++) r_land[r] = legend.classes[r_class[r]].sea ? 0 : 1;
// The overlay's marks voted onto the mesh: shown by the hover, and read for one thing only - the
// coast_jitter multiplier that scales the roughening per region (painted-overlay.js).
let r_mark = null, r_jitter = null, ovStats = null;
if (ovIn && ovIn.marks && ovIn.marks.length) {
t0 = performance.now();
r_mark = sampleMarksToMesh(mesh, r_xyz, ovIn.marks, ovIn.w, ovIn.h, ovIn.markJitter.length - 1);
const jit = jitterPerRegion(mesh, r_mark, ovIn.markJitter);
r_jitter = jit.r_jitter;
ovStats = { marked: jit.marked, pinned: jit.pinned };
timing.push({ stage: 'Vote overlay marks onto regions', ms: performance.now() - t0 });
}
progress(14, 'Roughening the coastline…');
t0 = performance.now();
const coast = roughenCoast(mesh, r_xyz, r_land, +pp.coastDetail || 0, seed, r_jitter);
r_land = coast.r_land;
const reclassed = reconcileClasses(mesh, r_class, r_land, legend);
timing.push({ stage: 'Coast roughening', ms: performance.now() - t0 });
progress(18, 'Painting the uplift…');
t0 = performance.now();
const up = buildUpliftField(mesh, r_xyz, r_class, r_land, legend, {
seed,
circumferenceKm: +pp.circumferenceKm || 100,
massifWavelengthKm: +pp.massifWavelengthKm || 0,
lithologyWavelengthKm: +pp.lithologyWavelengthKm || 0,
lithology: Array.isArray(pp.lithology) ? pp.lithology : [],
variation: +pp.variation || 0,
});
timing.push({ stage: 'Uplift field', ms: performance.now() - t0 });
if (up.landCount === 0) throw new Error('The painting has no land: every region voted a sea class.');
if (!(up.rateMax > 0)) throw new Error('Every land class has an uplift rate of 0, so there is nothing to solve.');
const steps = Math.max(1, pp.steps | 0);
const solved = solveUplift(mesh, neighborDist, r_land, up.r_rate, up.r_k, up.rateMax,
{ steps, seed, m: 0.5, diffusion: 0.04 },
(step, total) => progress(20 + 45 * step / total, `Solving uplift… step ${step} of ${total}`));
timing.push({ stage: `Stream-power solve (${steps} steps)`, ms: solved.solveMs });
progress(66, 'Scaling relief…');
t0 = performance.now();
const el = toElevation(mesh, solved, r_land, r_class, legend, up.hop, {
peakKm: Math.max(0.1, +pp.peakKm || 4.5),
oceanDepthKm: Math.max(0.05, +pp.oceanDepthKm || 4),
avgEdgeKm: up.avgEdgeKm,
});
const r_elevation = el.r_elevation;
timing.push({ stage: 'Scale to elevation', ms: performance.now() - t0 });
const prePostElev = new Float32Array(r_elevation);
progress(70, 'Processing terrain…');
t0 = performance.now();
const { dl_erosionDelta, postTiming } = runPostProcessing(mesh, r_xyz, r_elevation, { smoothing, glacialErosion, hydraulicErosion, thermalErosion, ridgeSharpening, terrainWarp }, neighborDist, seed);
timing.push({ stage: 'Terrain post-processing', ms: performance.now() - t0 });
progress(74, 'Deriving plates…');
t0 = performance.now();
const { r_plate, plateSeeds, plateIsOcean, plateVec } = deriveSyntheticPlates(mesh, r_elevation);
timing.push({ stage: 'Synthetic plates', ms: performance.now() - t0 });
const mountain_r = new Set();
const coastline_r = new Set();
const ocean_r = new Set();
for (let r = 0; r < mesh.numRegions; r++) {
if (r_elevation[r] <= 0) { ocean_r.add(r); continue; }
if (r_elevation[r] > 0.5) mountain_r.add(r);
const end = mesh.adjOffset[r + 1];
for (let ni = mesh.adjOffset[r]; ni < end; ni++) {
if (r_elevation[mesh.adjList[ni]] <= 0) { coastline_r.add(r); break; }
}
}
// The painted layers. Sea is -1 so the colour maps can leave it dark.
const paintUplift = new Float32Array(mesh.numRegions);
const paintK = new Float32Array(mesh.numRegions);
const classCounts = new Int32Array(legend.classes.length);
for (let r = 0; r < mesh.numRegions; r++) {
classCounts[r_class[r]]++;
paintUplift[r] = r_land[r] ? up.r_rate[r] : -1;
paintK[r] = r_land[r] ? up.r_k[r] : -1;
}
const landCoverage = up.landCount / mesh.numRegions;
const r_stress = new Float32Array(mesh.numRegions);
const debugLayers = {
erosionDelta: dl_erosionDelta,
paintClass: r_class,
paintUplift,
paintK,
flow: el.flowLog,
slope: el.slopeDeg,
basins: el.basin,
// The Overlay layer colours by class (dimmed) and drapes the sheet over it; overlayMark is what
// the hover names.
paintOverlay: r_class,
overlayMark: r_mark,
};
const nMag = 0;
let windResult = null, oceanResult = null, precipResult = null, tempResult = null;
if (!skipClimate) {
const noise = new SimplexNoise(seed);
progress(78, 'Simulating wind patterns…');
t0 = performance.now();
windResult = computeWind(mesh, r_xyz, r_elevation, plateIsOcean, r_plate, noise);
timing.push({ stage: 'Wind simulation', ms: performance.now() - t0 });
debugLayers.pressureSummer = windResult.r_pressure_summer;
debugLayers.pressureWinter = windResult.r_pressure_winter;
debugLayers.windSpeedSummer = windResult.r_wind_speed_summer;
debugLayers.windSpeedWinter = windResult.r_wind_speed_winter;
debugLayers.continentality = windResult.r_continentality;
progress(84, 'Computing ocean currents…');
t0 = performance.now();
oceanResult = computeOceanCurrents(mesh, r_xyz, r_elevation, windResult);
timing.push({ stage: 'Ocean currents', ms: performance.now() - t0 });
progress(88, 'Computing precipitation…');
t0 = performance.now();
precipResult = computePrecipitation(mesh, r_xyz, r_elevation, windResult, oceanResult, precipitationOffset, landCoverage);
timing.push({ stage: 'Precipitation', ms: performance.now() - t0 });
debugLayers.precipSummer = precipResult.r_precip_summer;
debugLayers.precipWinter = precipResult.r_precip_winter;
debugLayers.rainShadowSummer = precipResult.r_rainshadow_summer;
debugLayers.rainShadowWinter = precipResult.r_rainshadow_winter;
progress(92, 'Computing temperature…');
t0 = performance.now();
tempResult = computeTemperature(mesh, r_xyz, r_elevation, windResult, oceanResult, precipResult, temperatureOffset);
timing.push({ stage: 'Temperature', ms: performance.now() - t0 });
debugLayers.tempSummer = tempResult.r_temperature_summer;
debugLayers.tempWinter = tempResult.r_temperature_winter;
t0 = performance.now();
debugLayers.koppen = classifyKoppen(mesh, r_elevation, tempResult, precipResult);
timing.push({ stage: 'Köppen classification', ms: performance.now() - t0 });
}
progress(skipClimate ? 90 : 96, 'Computing triangle elevations…');
t0 = performance.now();
const t_elevation = computeTriangleElevations(mesh, r_elevation);
timing.push({ stage: 'Triangle elevations', ms: performance.now() - t0 });
// Retain state for reapply and deferred climate
t0 = performance.now();
W = {
mesh, r_xyz: new Float32Array(r_xyz), t_xyz: new Float32Array(t_xyz),
neighborDist,
r_plate: new Int32Array(r_plate), plateSeeds: new Set(plateSeeds), plateVec,
plateIsOcean: new Set(plateIsOcean), originalPlateIsOcean: new Set(plateIsOcean),
plateDensity: {}, plateDensityLand: {}, plateDensityOcean: {},
prePostElev: new Float32Array(prePostElev),
r_elevation_final: new Float32Array(r_elevation),
seed, nMag, noise: new SimplexNoise(seed),
mountain_r: new Set(mountain_r), coastline_r: new Set(coastline_r), ocean_r: new Set(ocean_r),
r_stress: new Float32Array(r_stress),
cachedWind: windResult, cachedOcean: oceanResult,
temperatureOffset, precipitationOffset, landCoverage,
};
timing.push({ stage: 'Clone state for retention', ms: performance.now() - t0 });
const tWorkerTotal = performance.now() - tTotal0;
const painted = {
legend: {
classes: legend.classes.map(c => ({
name: c.name, rgb: c.rgb, sea: c.sea, depthM: c.depthM, upliftMmYr: c.upliftMmYr,
kMult: c.kMult, snow: c.snow, derived: c.derived, stroke: c.stroke,
used: classCounts[c.index] > 0,
})),
},
stats: {
regions: mesh.numRegions,
land: up.landCount,
landFraction: landCoverage,
classCounts: Array.from(classCounts),
strokesToEdge: strokes.edgeAssigned,
strokesDissolved: strokes.dissolved,
coastFlipped: coast.flipped,
overlay: ovStats,
reclassed,
rateMax: up.rateMax,
kmScale: el.kmScale,
p995: el.p995,
maxKm: el.maxKm,
solveMs: solved.solveMs,
steps,
avgEdgeKm: up.avgEdgeKm,
scale: up.scale,
},
params: { ...pp, seed },
};
const result = {
type: 'done',
triangles: mesh.triangles,
halfedges: mesh.halfedges,
numRegions: mesh.numRegions,
r_xyz, t_xyz, r_plate,
plateSeeds: Array.from(plateSeeds),
plateVec,
plateIsOcean: Array.from(plateIsOcean),
originalPlateIsOcean: Array.from(plateIsOcean),
plateDensity: {}, plateDensityLand: {}, plateDensityOcean: {},
prePostElev,
r_elevation, t_elevation,
mountain_r: Array.from(mountain_r),
coastline_r: Array.from(coastline_r),
ocean_r: Array.from(ocean_r),
r_stress,
...buildClimateFields(windResult, oceanResult, precipResult, tempResult),
skipClimate: !!skipClimate,
seed, nMag,
debugLayers,
painted,
_timing: [],
_pipelineTiming: timing,
_postTiming: postTiming,
_workerTotal: tWorkerTotal,
_params: { N, P: 0, jitter, nMag, numContinents: 0, smoothing, terrainWarp, hydraulicErosion, thermalErosion, ridgeSharpening, glacialErosion, seed }
};
const transferList = [
r_xyz.buffer, t_xyz.buffer, r_plate.buffer,
prePostElev.buffer, r_elevation.buffer, t_elevation.buffer,
r_stress.buffer,
r_class.buffer, paintUplift.buffer, paintK.buffer, el.flowLog.buffer, el.slopeDeg.buffer, el.basin.buffer,
...(r_mark ? [r_mark.buffer] : []),
];
self.postMessage(result, transferList);
} catch (err) {
self.postMessage({ type: 'error', message: err.message, stack: err.stack });
}
}
self.onmessage = (e) => {
const { cmd } = e.data;
switch (cmd) {
case 'generate': handleGenerate(e.data); break;
case 'reapply': handleReapply(e.data); break;
case 'editRecompute': handleEditRecompute(e.data); break;
case 'computeClimate': handleComputeClimate(e.data); break;
case 'importHeightmap': handleImportHeightmap(e.data); break;
case 'importPainted': handleImportPainted(e.data); break;
default: self.postMessage({ type: 'error', message: `Unknown command: ${cmd}` });
}
};