// 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}` }); } };