363 lines
16 KiB
JavaScript
363 lines
16 KiB
JavaScript
// Plate generation — round-robin weighted fill with directional bias.
|
||
// Each plate gets a random growth rate and preferred direction.
|
||
|
||
import { makeRng, makeRandInt } from './rng.js';
|
||
import {
|
||
PLATE_LOW_PLATE_T_HIGH, PLATE_LOW_PLATE_T_RANGE,
|
||
PLATE_RATE_MIN_BASE, PLATE_RATE_MIN_LOW_T,
|
||
PLATE_RATE_RANGE_BASE, PLATE_RATE_RANGE_LOW_T,
|
||
PLATE_DIR_BASE_BASE, PLATE_DIR_BASE_LOW_T,
|
||
PLATE_DIR_SCALE_BASE, PLATE_DIR_SCALE_LOW_T,
|
||
PLATE_DIR_STRENGTH_CAP,
|
||
PLATE_COMPACT_BASE, PLATE_COMPACT_LOW_T,
|
||
PLATE_AREA_GOVERNOR_BASE, PLATE_AREA_GOVERNOR_LOW_T,
|
||
PLATE_COMPACT_THRESHOLD_MULT, PLATE_COMPACT_PENALTY_MULT,
|
||
PLATE_OMEGA_MIN, PLATE_OMEGA_RANGE,
|
||
PLATE_SMOOTH_BASE, PLATE_SMOOTH_LOW_T,
|
||
PLATE_SMOOTH_FIRST_THRESH, PLATE_SMOOTH_LATER_THRESH,
|
||
} from './terrain-config.js';
|
||
|
||
export function generatePlates(mesh, r_xyz, numPlates, seed) {
|
||
const { numRegions } = mesh;
|
||
const r_plate = new Int32Array(numRegions).fill(-1);
|
||
const rng = makeRng(seed + 0.5);
|
||
const randInt = makeRandInt(seed);
|
||
|
||
// Farthest-point seed distribution with top-3 jitter
|
||
const plateSeeds = new Set();
|
||
const isSeed = new Uint8Array(numRegions);
|
||
const minDistToSeed = new Float32Array(numRegions).fill(Infinity);
|
||
|
||
const firstSeed = randInt(numRegions);
|
||
plateSeeds.add(firstSeed);
|
||
isSeed[firstSeed] = 1;
|
||
const fsx = r_xyz[3*firstSeed], fsy = r_xyz[3*firstSeed+1], fsz = r_xyz[3*firstSeed+2];
|
||
for (let r = 0; r < numRegions; r++) {
|
||
minDistToSeed[r] = 1 - (r_xyz[3*r]*fsx + r_xyz[3*r+1]*fsy + r_xyz[3*r+2]*fsz);
|
||
}
|
||
minDistToSeed[firstSeed] = 0;
|
||
|
||
while (plateSeeds.size < numPlates && plateSeeds.size < numRegions) {
|
||
// Find top-3 farthest regions (flat vars, no object allocation)
|
||
let t0r = -1, t0d = -1, t1r = -1, t1d = -1, t2r = -1, t2d = -1;
|
||
for (let r = 0; r < numRegions; r++) {
|
||
if (isSeed[r]) continue;
|
||
const d = minDistToSeed[r];
|
||
if (d > t2d) {
|
||
if (d > t0d) {
|
||
t2r = t1r; t2d = t1d; t1r = t0r; t1d = t0d; t0r = r; t0d = d;
|
||
} else if (d > t1d) {
|
||
t2r = t1r; t2d = t1d; t1r = r; t1d = d;
|
||
} else {
|
||
t2r = r; t2d = d;
|
||
}
|
||
}
|
||
}
|
||
let validCount = (t0r !== -1) + (t1r !== -1) + (t2r !== -1);
|
||
if (!validCount) break;
|
||
const pick = randInt(validCount);
|
||
const newSeed = pick === 0 ? t0r : pick === 1 ? t1r : t2r;
|
||
plateSeeds.add(newSeed);
|
||
isSeed[newSeed] = 1;
|
||
const nsx = r_xyz[3*newSeed], nsy = r_xyz[3*newSeed+1], nsz = r_xyz[3*newSeed+2];
|
||
|
||
// Fused pass: update minDistToSeed from new seed AND find top-3 for next iteration
|
||
if (plateSeeds.size < numPlates) {
|
||
t0r = -1; t0d = -1; t1r = -1; t1d = -1; t2r = -1; t2d = -1;
|
||
for (let r = 0; r < numRegions; r++) {
|
||
const d = 1 - (r_xyz[3*r]*nsx + r_xyz[3*r+1]*nsy + r_xyz[3*r+2]*nsz);
|
||
if (d < minDistToSeed[r]) minDistToSeed[r] = d;
|
||
if (isSeed[r]) continue;
|
||
const md = minDistToSeed[r];
|
||
if (md > t2d) {
|
||
if (md > t0d) {
|
||
t2r = t1r; t2d = t1d; t1r = t0r; t1d = t0d; t0r = r; t0d = md;
|
||
} else if (md > t1d) {
|
||
t2r = t1r; t2d = t1d; t1r = r; t1d = md;
|
||
} else {
|
||
t2r = r; t2d = md;
|
||
}
|
||
}
|
||
}
|
||
// Next iteration can skip the search pass — top-3 is already computed
|
||
validCount = (t0r !== -1) + (t1r !== -1) + (t2r !== -1);
|
||
if (!validCount) break;
|
||
const pick2 = randInt(validCount);
|
||
const newSeed2 = pick2 === 0 ? t0r : pick2 === 1 ? t1r : t2r;
|
||
plateSeeds.add(newSeed2);
|
||
isSeed[newSeed2] = 1;
|
||
const ns2x = r_xyz[3*newSeed2], ns2y = r_xyz[3*newSeed2+1], ns2z = r_xyz[3*newSeed2+2];
|
||
for (let r = 0; r < numRegions; r++) {
|
||
const d = 1 - (r_xyz[3*r]*ns2x + r_xyz[3*r+1]*ns2y + r_xyz[3*r+2]*ns2z);
|
||
if (d < minDistToSeed[r]) minDistToSeed[r] = d;
|
||
}
|
||
} else {
|
||
// Last seed — just update distances (needed for distance field, but loop will exit)
|
||
for (let r = 0; r < numRegions; r++) {
|
||
const d = 1 - (r_xyz[3*r]*nsx + r_xyz[3*r+1]*nsy + r_xyz[3*r+2]*nsz);
|
||
if (d < minDistToSeed[r]) minDistToSeed[r] = d;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Interpolation factor: more cragginess at low plate counts
|
||
const lowPlateT = Math.max(0, Math.min(1, (PLATE_LOW_PLATE_T_HIGH - numPlates) / PLATE_LOW_PLATE_T_RANGE));
|
||
|
||
// Per-plate growth properties
|
||
const plateGrowthRate = {};
|
||
const plateGrowthDir = {};
|
||
const plateDirStrength = {};
|
||
|
||
const rateMin = PLATE_RATE_MIN_BASE - PLATE_RATE_MIN_LOW_T * lowPlateT; // 0.7 → 0.3
|
||
const rateRange = PLATE_RATE_RANGE_BASE + PLATE_RATE_RANGE_LOW_T * lowPlateT; // 2.3 → 4.7
|
||
const dirBase = PLATE_DIR_BASE_BASE + PLATE_DIR_BASE_LOW_T * lowPlateT; // 0.15 → 0.4
|
||
const dirScale = PLATE_DIR_SCALE_BASE + PLATE_DIR_SCALE_LOW_T * lowPlateT; // 0.25 → 0.5
|
||
|
||
for (const center of plateSeeds) {
|
||
plateGrowthRate[center] = rateMin + rng() * rng() * rateRange;
|
||
|
||
const px = r_xyz[3*center], py = r_xyz[3*center+1], pz = r_xyz[3*center+2];
|
||
const pLen = Math.sqrt(px*px + py*py + pz*pz) || 1;
|
||
const nx = px/pLen, ny = py/pLen, nz = pz/pLen;
|
||
const rx = rng()-0.5, ry = rng()-0.5, rz = rng()-0.5;
|
||
const d = rx*nx + ry*ny + rz*nz;
|
||
let tx = rx - d*nx, ty = ry - d*ny, tz = rz - d*nz;
|
||
const tLen = Math.sqrt(tx*tx + ty*ty + tz*tz) || 1;
|
||
plateGrowthDir[center] = [tx/tLen, ty/tLen, tz/tLen];
|
||
|
||
plateDirStrength[center] = Math.min(PLATE_DIR_STRENGTH_CAP, rng() * (dirBase + dirScale / plateGrowthRate[center]));
|
||
}
|
||
|
||
// Per-plate frontiers — round-robin ensures every plate advances
|
||
const plateIds = Array.from(plateSeeds);
|
||
const frontiers = new Map();
|
||
const plateAreaCount = {};
|
||
for (const pid of plateIds) {
|
||
r_plate[pid] = pid;
|
||
frontiers.set(pid, [pid]);
|
||
plateAreaCount[pid] = 1;
|
||
}
|
||
|
||
const { adjOffset, adjList } = mesh;
|
||
let remaining = numRegions - plateIds.length;
|
||
const COMPACT_WEIGHT = PLATE_COMPACT_BASE - PLATE_COMPACT_LOW_T * lowPlateT; // 0.3 → 0.08
|
||
const expectedArea = Math.max(1, (numRegions - plateIds.length) / numPlates);
|
||
const areaGovernorMult = PLATE_AREA_GOVERNOR_BASE + PLATE_AREA_GOVERNOR_LOW_T * lowPlateT; // 2.0 → 4.0
|
||
const invNumRegions = 1 / numRegions;
|
||
|
||
while (remaining > 0) {
|
||
let anyProgress = false;
|
||
for (const pid of plateIds) {
|
||
const frontier = frontiers.get(pid);
|
||
if (frontier.length === 0) continue;
|
||
|
||
const rate = plateGrowthRate[pid];
|
||
const dir = plateGrowthDir[pid];
|
||
const d0 = dir[0], d1 = dir[1], d2 = dir[2];
|
||
const dirStr = plateDirStrength[pid];
|
||
const dirStrHalf = dirStr * 0.5;
|
||
let steps = Math.max(1, Math.ceil(rate * (0.5 + rng())));
|
||
|
||
// Governor: halve steps for plates exceeding threshold
|
||
if (plateAreaCount[pid] > expectedArea * areaGovernorMult) {
|
||
steps = Math.max(1, Math.ceil(steps * 0.5));
|
||
}
|
||
|
||
// Compactness: expected chord distance for a circular plate of current area
|
||
const expectedChordDist = Math.sqrt((plateAreaCount[pid] || 1) * invNumRegions / Math.PI) * 2;
|
||
const compactThreshold = expectedChordDist * PLATE_COMPACT_THRESHOLD_MULT;
|
||
|
||
// Precompute seed coordinates
|
||
const sx = r_xyz[3*pid], sy = r_xyz[3*pid+1], sz = r_xyz[3*pid+2];
|
||
|
||
for (let s = 0; s < steps && frontier.length > 0; s++) {
|
||
let bestIdx = 0, bestScore = -Infinity;
|
||
const samples = Math.min(frontier.length, 3 + Math.floor(dirStr * 5));
|
||
for (let i = 0; i < samples; i++) {
|
||
const idx = randInt(frontier.length);
|
||
const cell = frontier[idx];
|
||
const ci = 3*cell;
|
||
const dx = r_xyz[ci] - sx, dy = r_xyz[ci+1] - sy, dz = r_xyz[ci+2] - sz;
|
||
const dLenSq = dx*dx + dy*dy + dz*dz;
|
||
const dLen = Math.sqrt(dLenSq) || 1;
|
||
const alignment = (dx*d0 + dy*d1 + dz*d2) / dLen;
|
||
|
||
// Compactness: seedDist = dLenSq/2 for unit-sphere points
|
||
const excess = Math.max(0, dLenSq * 0.5 - compactThreshold);
|
||
const compactPenalty = excess * (COMPACT_WEIGHT * PLATE_COMPACT_PENALTY_MULT);
|
||
|
||
const score = alignment * dirStr + rng() * (1 - dirStrHalf) - compactPenalty;
|
||
if (score > bestScore) { bestScore = score; bestIdx = idx; }
|
||
}
|
||
|
||
const current = frontier[bestIdx];
|
||
frontier[bestIdx] = frontier[frontier.length - 1];
|
||
frontier.pop();
|
||
|
||
for (let j = adjOffset[current], jEnd = adjOffset[current + 1]; j < jEnd; j++) {
|
||
const nb = adjList[j];
|
||
if (r_plate[nb] === -1) {
|
||
r_plate[nb] = pid;
|
||
frontier.push(nb);
|
||
plateAreaCount[pid]++;
|
||
remaining--;
|
||
anyProgress = true;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if (!anyProgress) break;
|
||
}
|
||
|
||
// Cleanup: assign orphaned regions to nearest claimed neighbor
|
||
let orphans = true;
|
||
while (orphans) {
|
||
orphans = false;
|
||
for (let r = 0; r < numRegions; r++) {
|
||
if (r_plate[r] === -1) {
|
||
for (let j = adjOffset[r], jEnd = adjOffset[r + 1]; j < jEnd; j++) {
|
||
const nb = adjList[j];
|
||
if (r_plate[nb] !== -1) {
|
||
r_plate[r] = r_plate[nb];
|
||
orphans = true;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
smoothAndReconnectPlates(mesh, r_plate, plateSeeds, Math.round(PLATE_SMOOTH_BASE - PLATE_SMOOTH_LOW_T * lowPlateT));
|
||
|
||
// Assign an Euler pole + angular velocity per plate
|
||
const plateVec = {};
|
||
for (const center of plateSeeds) {
|
||
// Random Euler pole uniformly distributed on the sphere
|
||
const theta = rng() * 2 * Math.PI;
|
||
const cosP = 2 * rng() - 1;
|
||
const sinP = Math.sqrt(1 - cosP * cosP);
|
||
const pole = [sinP * Math.cos(theta), sinP * Math.sin(theta), cosP];
|
||
// Angular velocity: magnitude 0.5–2.0, random sign
|
||
const omega = (PLATE_OMEGA_MIN + rng() * PLATE_OMEGA_RANGE) * (rng() < 0.5 ? -1 : 1);
|
||
plateVec[center] = { pole, omega };
|
||
}
|
||
|
||
return { r_plate, plateSeeds, plateVec };
|
||
}
|
||
|
||
/**
|
||
* Smooth plate boundaries via majority-vote, then reconnect severed plates.
|
||
* @param {SphereMesh} mesh
|
||
* @param {Int32Array} r_plate — mutated in place
|
||
* @param {Set|Array} plateSeeds — seed region IDs (used for connectivity roots & protection)
|
||
* @param {number} numPasses — number of majority-vote smoothing passes
|
||
*/
|
||
export function smoothAndReconnectPlates(mesh, r_plate, plateSeeds, numPasses) {
|
||
const { numRegions, adjOffset, adjList } = mesh;
|
||
const plateIds = Array.from(plateSeeds);
|
||
|
||
// Build seed lookup for protection during smoothing.
|
||
// Protects plate seed regions from being reassigned by majority-vote.
|
||
// After coarse→hi-res projection the seed IDs are coarse-mesh indices
|
||
// that won't satisfy r_plate[pid] === pid on the hi-res mesh, so the
|
||
// array stays all-zeros and protection is effectively skipped — this is
|
||
// intentional since projected boundaries don't need seed anchoring.
|
||
const isSeed = new Uint8Array(numRegions);
|
||
for (const pid of plateIds) {
|
||
if (pid < numRegions && r_plate[pid] === pid) isSeed[pid] = 1;
|
||
}
|
||
|
||
// Smooth boundaries: majority-vote removes thin tendrils
|
||
let maxDeg = 0;
|
||
for (let r = 0; r < numRegions; r++) {
|
||
const deg = adjOffset[r + 1] - adjOffset[r];
|
||
if (deg > maxDeg) maxDeg = deg;
|
||
}
|
||
const cntPlates = new Int32Array(maxDeg);
|
||
const cntValues = new Uint8Array(maxDeg);
|
||
for (let pass = 0; pass < numPasses; pass++) {
|
||
const threshold = pass === 0 ? PLATE_SMOOTH_FIRST_THRESH : PLATE_SMOOTH_LATER_THRESH;
|
||
for (let r = 0; r < numRegions; r++) {
|
||
const rStart = adjOffset[r], rEnd = adjOffset[r + 1];
|
||
const deg = rEnd - rStart;
|
||
let nDistinct = 0;
|
||
for (let j = rStart; j < rEnd; j++) {
|
||
const p = r_plate[adjList[j]];
|
||
let found = false;
|
||
for (let k = 0; k < nDistinct; k++) {
|
||
if (cntPlates[k] === p) { cntValues[k]++; found = true; break; }
|
||
}
|
||
if (!found) { cntPlates[nDistinct] = p; cntValues[nDistinct] = 1; nDistinct++; }
|
||
}
|
||
let bestPlate = r_plate[r], bestCount = 0;
|
||
for (let k = 0; k < nDistinct; k++) {
|
||
if (cntValues[k] > bestCount) { bestCount = cntValues[k]; bestPlate = cntPlates[k]; }
|
||
}
|
||
if (bestCount > deg * threshold && !isSeed[r]) {
|
||
r_plate[r] = bestPlate;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Reconnect: smoothing or projection may create disconnected plate fragments.
|
||
// For each plate, keep the LARGEST connected component and mark the rest
|
||
// for reassignment. This is stable across resolutions (unlike first-found).
|
||
{
|
||
const visited = new Uint8Array(numRegions);
|
||
// Per-plate: track the largest component's BFS list
|
||
const bestComponent = {}; // pid → [region indices]
|
||
|
||
for (let r = 0; r < numRegions; r++) {
|
||
if (visited[r]) continue;
|
||
const pid = r_plate[r];
|
||
const bfs = [r];
|
||
visited[r] = 1;
|
||
for (let qi = 0; qi < bfs.length; qi++) {
|
||
for (let ni = adjOffset[bfs[qi]], niEnd = adjOffset[bfs[qi] + 1]; ni < niEnd; ni++) {
|
||
const nb = adjList[ni];
|
||
if (!visited[nb] && r_plate[nb] === pid) {
|
||
visited[nb] = 1;
|
||
bfs.push(nb);
|
||
}
|
||
}
|
||
}
|
||
if (!bestComponent[pid] || bfs.length > bestComponent[pid].length) {
|
||
bestComponent[pid] = bfs;
|
||
}
|
||
}
|
||
|
||
// Mark regions in the largest component per plate
|
||
const inMain = new Uint8Array(numRegions);
|
||
for (const pid of Object.keys(bestComponent)) {
|
||
for (const r of bestComponent[pid]) inMain[r] = 1;
|
||
}
|
||
|
||
// Reassign orphaned regions (not in their plate's largest component)
|
||
// via BFS from the main-component boundary
|
||
const queue = [];
|
||
for (let r = 0; r < numRegions; r++) {
|
||
if (!inMain[r]) {
|
||
for (let ni = adjOffset[r], niEnd = adjOffset[r + 1]; ni < niEnd; ni++) {
|
||
if (inMain[adjList[ni]]) {
|
||
r_plate[r] = r_plate[adjList[ni]];
|
||
inMain[r] = 1;
|
||
queue.push(r);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
for (let qi = 0; qi < queue.length; qi++) {
|
||
const r = queue[qi];
|
||
for (let ni = adjOffset[r], niEnd = adjOffset[r + 1]; ni < niEnd; ni++) {
|
||
const nb = adjList[ni];
|
||
if (!inMain[nb]) {
|
||
r_plate[nb] = r_plate[r];
|
||
inMain[nb] = 1;
|
||
queue.push(nb);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|