This commit is contained in:
Rainer Leit
2026-09-25 17:02:24 +03:00
parent cc43ed8dc8
commit 9597629951
2149 changed files with 460234 additions and 1770 deletions
+104
View File
@@ -0,0 +1,104 @@
package plates
import "math"
// Belt is how faulted a margin's surroundings are: the deformation zone around a boundary, and how densely
// it is broken.
//
// It lives beside the plates rather than beside the faults because it describes a *boundary*, not a fault.
// How wide the ground is that a margin deforms is a property of what that margin is doing - a continental
// collision takes up its convergence across a belt a thousand kilometres wide and a mid-ocean ridge across an
// axis a few tens wide - and the traces are a consequence of that width, not the other way round. What
// internal/uplift's belt_faults.go does with these numbers, and the fault map they were read off, is
// documented there.
type Belt struct {
// ZoneKm is the deformation half-width of a *collision* margin closing at ReferenceCmYr, in kilometres.
// Every other kind of margin is a fraction of it, and every margin scales with its own rate.
ZoneKm float64 `json:"zone_km"`
// ReferenceCmYr is the rate ZoneKm is quoted at. Earth's big collisions run 2 to 5 cm/yr.
ReferenceCmYr float64 `json:"reference_cm_yr"`
// Per1000Km2 is the trace density over the *zone*, not over the planet: a belt is as faulted as a belt is
// wherever it happens to run, and the ground away from one is not lightly faulted, it is unfaulted.
Per1000Km2 float64 `json:"per_1000km2"`
// ThrowM is the total displacement over the whole run, low to high, before the closing rate scales it -
// the height of the scarp the fault would build if nothing eroded it.
ThrowM [2]float64 `json:"throw_m"`
// LengthKm is how long a trace is, low to high, before the local zone width scales it. A wide belt
// carries long faults and a narrow one cannot.
LengthKm [2]float64 `json:"length_km"`
// StrikeSpreadDeg is how far a trace may wander off the belt's local tangent. Small on purpose: a swarm
// being sub-parallel is the thing that makes it read as a swarm.
//
// ConjugateFraction is the share of traces drawn on the second, crossing direction, and ConjugateDeg is
// the angle between the two sets. One direction alone reads as corduroy.
//
// The first two are pointers for the reason Config.SpinFraction is: JSON cannot tell an absent number
// from a zero one, and both of these have a real meaning at zero - perfectly parallel traces, and no
// second set. Read as the same thing, a block that simply did not mention them silently turned them off,
// which is how the first painted planet came out with no conjugate set at all. Absent takes the default;
// an explicit 0 means none.
StrikeSpreadDeg *float64 `json:"strike_spread_deg"`
ConjugateFraction *float64 `json:"conjugate_fraction"`
ConjugateDeg float64 `json:"conjugate_deg"`
}
// DefaultStrikeSpreadDeg and DefaultConjugateFraction are what a belt that does not mention them gets.
const (
DefaultStrikeSpreadDeg = 11.0
DefaultConjugateFraction = 0.22
)
// Spread is the configured strike spread, or the default when the block said nothing.
func (b Belt) Spread() float64 {
if b.StrikeSpreadDeg == nil {
return DefaultStrikeSpreadDeg
}
return math.Max(0, *b.StrikeSpreadDeg)
}
// Conjugate is the configured share of crossing traces, or the default when the block said nothing.
func (b Belt) Conjugate() float64 {
if b.ConjugateFraction == nil {
return DefaultConjugateFraction
}
return math.Min(1, math.Max(0, *b.ConjugateFraction))
}
// DefaultBelt is what a planet that asks for belt faults but says nothing else gets.
func DefaultBelt() Belt {
return Belt{
ZoneKm: 6,
ReferenceCmYr: 4,
Per1000Km2: 90,
ThrowM: [2]float64{80, 420},
LengthKm: [2]float64{4, 16},
ConjugateDeg: 32,
// StrikeSpreadDeg and ConjugateFraction stay nil: their defaults live in Spread and Conjugate, so
// that an explicit zero can mean none.
}
}
// Wanted reports whether this asks for anything. A zero Belt is a planet whose margins are not faulted, which
// is what every painted planet had before this existed.
func (b Belt) Wanted() bool {
return b.ZoneKm > 0 && b.Per1000Km2 > 0 && b.LengthKm[1] > 0 && b.ThrowM[1] > 0
}
// WithDefaults fills in the fields that have a sensible value when left out. ZoneKm, Per1000Km2, ThrowM and
// LengthKm are deliberately not among them: those four are the feature, and defaulting them would turn
// leaving the block out into switching the feature on.
func (b Belt) WithDefaults() Belt {
d := DefaultBelt()
if b.ReferenceCmYr <= 0 {
b.ReferenceCmYr = d.ReferenceCmYr
}
if b.ConjugateDeg <= 0 {
b.ConjugateDeg = d.ConjugateDeg
}
return b
}