// Ocean / land assignment. // Targets ~30% land by surface area. numContinents controls how many // separate landmasses to create. Small trapped interior seas are absorbed. import { makeRng } from './rng.js'; export function assignOceanLand(mesh, r_plate, plateSeeds, r_xyz, seed, numContinents, continentSizeVariety = 0, landCoverage = 0.3) { const rng = makeRng(seed + 42); const numRegions = mesh.numRegions; const plateIds = Array.from(plateSeeds); const numPlates = plateIds.length; const { adjOffset, adjList } = mesh; // 1. Plate areas and centroids const plateArea = {}; const plateCentroid = {}; for (const pid of plateIds) { plateArea[pid] = 0; plateCentroid[pid] = [0, 0, 0]; } for (let r = 0; r < numRegions; r++) { const p = r_plate[r]; if (!plateCentroid[p]) { plateArea[p] = 0; plateCentroid[p] = [0, 0, 0]; } plateArea[p]++; plateCentroid[p][0] += r_xyz[3*r]; plateCentroid[p][1] += r_xyz[3*r+1]; plateCentroid[p][2] += r_xyz[3*r+2]; } for (const pid of plateIds) { const a = plateArea[pid] || 1; plateCentroid[pid][0] /= a; plateCentroid[pid][1] /= a; plateCentroid[pid][2] /= a; } // 2. Plate adjacency graph + perimeter const plateAdj = {}; const platePerim = {}; for (const pid of plateIds) { plateAdj[pid] = new Set(); platePerim[pid] = 0; } for (let r = 0; r < numRegions; r++) { const myPlate = r_plate[r]; let isBoundary = false; for (let ni = adjOffset[r], niEnd = adjOffset[r + 1]; ni < niEnd; ni++) { const nbPlate = r_plate[adjList[ni]]; if (myPlate !== nbPlate) { plateAdj[myPlate].add(nbPlate); isBoundary = true; } } if (isBoundary) platePerim[myPlate]++; } // Plate compactness const plateCompact = {}; let maxCompact = 0; for (const pid of plateIds) { const c = Math.sqrt(plateArea[pid] || 1) / (platePerim[pid] || 1); plateCompact[pid] = c; if (c > maxCompact) maxCompact = c; } if (maxCompact > 0) { for (const pid of plateIds) plateCompact[pid] /= maxCompact; } const targetLandArea = landCoverage * numRegions; // 3. Pick continent seeds via farthest-point sampling const effectiveNum = Math.min(numContinents, numPlates); const continentSeeds = []; const chosen = new Set(); const first = plateIds[Math.floor(rng() * numPlates)]; continentSeeds.push(first); chosen.add(first); for (let s = 1; s < effectiveNum; s++) { const candidates = []; for (const pid of plateIds) { if (chosen.has(pid)) continue; const cx = plateCentroid[pid]; let minDist = Infinity; for (const existing of continentSeeds) { const ex = plateCentroid[existing]; const dx = cx[0]-ex[0], dy = cx[1]-ex[1], dz = cx[2]-ex[2]; const d = dx*dx + dy*dy + dz*dz; if (d < minDist) minDist = d; } const rawAreaFactor = Math.sqrt(numRegions / numPlates) / Math.sqrt(plateArea[pid] || 1); const areaFactor = 1 + (rawAreaFactor - 1) * (1 - continentSizeVariety * 0.5); const compact = 0.3 + 0.7 * plateCompact[pid]; candidates.push({ pid, score: minDist * areaFactor * compact }); } if (candidates.length === 0) break; candidates.sort((a, b) => b.score - a.score); const topK = Math.min(candidates.length, 3); const pick = candidates[Math.floor(rng() * topK)]; continentSeeds.push(pick.pid); chosen.add(pick.pid); } // If seeds alone exceed the land budget, trim the largest seeds let seedArea = 0; for (const pid of continentSeeds) seedArea += plateArea[pid]; while (continentSeeds.length > 1 && seedArea > targetLandArea) { let maxIdx = 0; for (let i = 1; i < continentSeeds.length; i++) { if (plateArea[continentSeeds[i]] > plateArea[continentSeeds[maxIdx]]) maxIdx = i; } seedArea -= plateArea[continentSeeds[maxIdx]]; chosen.delete(continentSeeds[maxIdx]); continentSeeds.splice(maxIdx, 1); } // 4. Initialize continent assignment const plateContinent = {}; for (let c = 0; c < continentSeeds.length; c++) { plateContinent[continentSeeds[c]] = c; } let landArea = seedArea; // 5. Round-robin growth with per-continent targets const growTarget = targetLandArea * 0.9; const numC = continentSeeds.length; // Per-continent growth targets: at variety=0 all equal, at variety=1 highly skewed const continentTarget = new Float64Array(numC); const continentArea = new Float64Array(numC); for (let c = 0; c < numC; c++) { continentArea[c] = plateArea[continentSeeds[c]]; } if (continentSizeVariety > 0 && numC > 1) { const weights = []; for (let c = 0; c < numC; c++) { // Log-normal-ish: at variety=1, weights span ~0.3x to ~3.5x (12:1 ratio) const logWeight = (rng() - 0.5) * continentSizeVariety * 2.5; weights.push(Math.exp(logWeight)); } const totalWeight = weights.reduce((a, b) => a + b, 0); for (let c = 0; c < numC; c++) { continentTarget[c] = growTarget * weights[c] / totalWeight; } } else { const equal = growTarget / Math.max(numC, 1); for (let c = 0; c < numC; c++) continentTarget[c] = equal; } let progress = true; while (progress && landArea < growTarget) { progress = false; for (let c = 0; c < numC && landArea < growTarget; c++) { // Skip continents that have reached their individual target if (continentArea[c] >= continentTarget[c]) continue; const candidates = []; for (const pid of plateIds) { if (plateContinent[pid] !== undefined) continue; let touchesSelf = false, touchesOther = false; let sameCount = 0; for (const adj of plateAdj[pid]) { const ac = plateContinent[adj]; if (ac === c) { touchesSelf = true; sameCount++; } else if (ac !== undefined) { touchesOther = true; break; } } if (touchesSelf && !touchesOther) { candidates.push({ pid, score: sameCount + plateCompact[pid] * 3 + rng() * 0.5 }); } } if (candidates.length === 0) continue; candidates.sort((a, b) => b.score - a.score); const topK = Math.min(candidates.length, 3); const pick = candidates[Math.floor(rng() * topK)]; plateContinent[pick.pid] = c; continentArea[c] += plateArea[pick.pid]; landArea += plateArea[pick.pid]; progress = true; } } // 6. Absorb trapped interior seas const oceanComponents = []; const visited = new Set(); for (const pid of plateIds) { if (plateContinent[pid] !== undefined || visited.has(pid)) continue; const component = [pid]; visited.add(pid); for (let qi = 0; qi < component.length; qi++) { for (const adj of plateAdj[component[qi]]) { if (plateContinent[adj] === undefined && !visited.has(adj)) { visited.add(adj); component.push(adj); } } } oceanComponents.push(component); } let mainIdx = 0; for (let i = 1; i < oceanComponents.length; i++) { let areaI = 0, areaM = 0; for (const p of oceanComponents[i]) areaI += plateArea[p]; for (const p of oceanComponents[mainIdx]) areaM += plateArea[p]; if (areaI > areaM) mainIdx = i; } const absorbCap = targetLandArea * 1.1; for (let i = 0; i < oceanComponents.length; i++) { if (i === mainIdx) continue; const component = oceanComponents[i]; const bordering = new Set(); for (const op of component) { for (const adj of plateAdj[op]) { if (plateContinent[adj] !== undefined) bordering.add(plateContinent[adj]); } if (bordering.size > 1) break; } if (bordering.size === 1) { let compArea = 0; for (const op of component) compArea += plateArea[op]; if (landArea + compArea <= absorbCap) { const c = bordering.values().next().value; for (const op of component) plateContinent[op] = c; landArea += compArea; } } } // 7. Build plateIsOcean set const plateIsOcean = new Set(); for (const pid of plateIds) { if (plateContinent[pid] === undefined) plateIsOcean.add(pid); } return plateIsOcean; }