Tooling
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
// What a legend's numbers make, before anything is solved: the two angles per class that `terrain plan`
|
||||
// prints, ported from Tools/Terrain internal/planet/plan.go so the class table here says the same thing.
|
||||
//
|
||||
// Steady state for the stream-power law is S = U/(K·A^m). With channels allowed down to a single cell, A at a
|
||||
// drainage divide is one cell squared, so for n = 1 the uplift rate alone fixes the hillslope angle at the
|
||||
// divide - the most useful number in the whole legend, because it decides whether the ground is shaped by
|
||||
// rivers or by landsliding. But almost none of a map is divide: slope falls away downstream, and the median
|
||||
// over a class comes out at about a third of the divide angle in tangent. That ratio was measured on the Go
|
||||
// tool's 8 m grid over a factor of twenty in rate (0.34, 0.33, 0.33, 0.32), and it is the reason there are
|
||||
// two columns. An author who reads the divide angle as the landscape sets every rate two or three times too
|
||||
// hot.
|
||||
//
|
||||
// Pure functions of the legend and the bake's constants; no mesh, no DOM. The constants are the geology
|
||||
// grid's (cell size, K, m, angle of repose), which is what the numbers are *about*: the ground the bake makes,
|
||||
// not the globe here, whose relief is a scale.
|
||||
|
||||
export const SOLVE_DEFAULTS = { k: 5e-5, m: 0.5, cellM: 8, talusDeg: 35 };
|
||||
|
||||
const RAD = Math.PI / 180;
|
||||
|
||||
/** Hillslope angle at a divide, in degrees, for a rate in mm/yr and an erodibility multiplier on K. */
|
||||
export function divideAngleDeg(rateMmYr, kMult, solve) {
|
||||
const s = solve || SOLVE_DEFAULTS;
|
||||
const k = (s.k || SOLVE_DEFAULTS.k) * (kMult > 0 ? kMult : 1);
|
||||
const cellM = s.cellM || SOLVE_DEFAULTS.cellM;
|
||||
const m = (s.m === undefined || s.m === null) ? SOLVE_DEFAULTS.m : s.m;
|
||||
if (k <= 0 || cellM <= 0) return 0;
|
||||
const slope = (rateMmYr / 1000) / (k * Math.pow(cellM * cellM, m));
|
||||
return Math.atan(slope) / RAD;
|
||||
}
|
||||
|
||||
// Fractions of the *tangent*, not of the angle, because the law is about slope.
|
||||
const TYPICAL_MEDIAN_FRAC = 0.33;
|
||||
const TYPICAL_P90_FRAC = 0.45;
|
||||
|
||||
/** The median and 90th-percentile slope over a class whose divide angle is `deg`. */
|
||||
export function typicalFromDivide(deg) {
|
||||
const t = Math.tan(deg * RAD);
|
||||
return { median: Math.atan(t * TYPICAL_MEDIAN_FRAC) / RAD, p90: Math.atan(t * TYPICAL_P90_FRAC) / RAD };
|
||||
}
|
||||
|
||||
/** The ground a median slope reads as. Boundaries are angles, not rates, deliberately. */
|
||||
export function readsAs(deg) {
|
||||
if (deg < 3) return 'plain';
|
||||
if (deg < 8) return 'rolling';
|
||||
if (deg < 16) return 'hill country';
|
||||
if (deg < 28) return 'mountain';
|
||||
return 'alpine';
|
||||
}
|
||||
|
||||
/** The rate, in mm/yr, at which a divide at k = 1 reaches the angle of repose; above it the clamp shapes the ground. */
|
||||
export function clampCeilingMmYr(solve) {
|
||||
const s = solve || SOLVE_DEFAULTS;
|
||||
const k = s.k || SOLVE_DEFAULTS.k;
|
||||
const cellM = s.cellM || SOLVE_DEFAULTS.cellM;
|
||||
const m = (s.m === undefined || s.m === null) ? SOLVE_DEFAULTS.m : s.m;
|
||||
const talus = s.talusDeg || SOLVE_DEFAULTS.talusDeg;
|
||||
return Math.tan(talus * RAD) * k * Math.pow(cellM * cellM, m) * 1000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Every angle the plan prints for one parsed class (see painted.js parseLegend): the divide, the typical
|
||||
* median and P90, what it reads as, whether the divide is past the angle of repose, and the same for the
|
||||
* massif floor when the class has one. Null for a class that is not land.
|
||||
*/
|
||||
export function classAngles(c, solve) {
|
||||
if (!c || c.sea || c.stroke || c.derived) return null;
|
||||
const s = solve || SOLVE_DEFAULTS;
|
||||
const talus = s.talusDeg || SOLVE_DEFAULTS.talusDeg;
|
||||
const divide = divideAngleDeg(c.upliftMmYr, c.kMult, s);
|
||||
const typ = typicalFromDivide(divide);
|
||||
const out = {
|
||||
divide, median: typ.median, p90: typ.p90,
|
||||
readsAs: readsAs(typ.median),
|
||||
clamped: divide >= talus,
|
||||
floor: null,
|
||||
};
|
||||
if (c.massif && c.massif.fraction > 0) {
|
||||
const fd = divideAngleDeg(c.massif.floorMmYr, c.kMult, s);
|
||||
const ft = typicalFromDivide(fd);
|
||||
out.floor = { rateMmYr: c.massif.floorMmYr, divide: fd, median: ft.median, readsAs: readsAs(ft.median), fraction: c.massif.fraction };
|
||||
}
|
||||
return out;
|
||||
}
|
||||
Reference in New Issue
Block a user