Files
UnrealPrototyping/Tools/Terrain/internal/plates/paint_test.go
T
2026-09-25 17:02:24 +03:00

222 lines
7.8 KiB
Go

package plates
import (
"math"
"testing"
)
// A painted layer and its legend, built by hand: two plates split at a quarter and three quarters of the way
// round, driven into each other along X with no spin.
func paintedStripes(w, h int) ([]uint8, *PaintLegend) {
lg := &PaintLegend{
WarnDistance: 60,
Plates: []PaintPlate{
{Name: "west", RGB: [3]int{200, 60, 60}, SpeedCmYr: 2, HeadingDeg: 90}, // due east
{Name: "east", RGB: [3]int{60, 60, 200}, SpeedCmYr: 2, HeadingDeg: 270}, // due west
},
}
px := make([]uint8, w*h*3)
for y := range h {
for x := range w {
id := 0
if x >= w/4 && x < 3*w/4 {
id = 1
}
o := (y*w + x) * 3
c := lg.Plates[id].RGB
px[o], px[o+1], px[o+2] = uint8(c[0]), uint8(c[1]), uint8(c[2])
}
}
return px, lg
}
func TestAPaintedLayerBecomesAPlanet(t *testing.T) {
p := testPlanet(t)
px, lg := paintedStripes(400, 200)
m, match, err := FromPainting(p, Default(), lg, px, 400, 200, allLandAt)
if err != nil {
t.Fatalf("from painting: %v", err)
}
if match.Far != 0 {
t.Errorf("%d of %d sampled cells did not match a plate colour", match.Far, match.Cells)
}
if len(m.Plates) != 2 {
t.Fatalf("%d plates from a two-colour legend", len(m.Plates))
}
if len(m.Boundaries) != 2 {
t.Fatalf("%d boundaries; two stripes on a cylinder make two contacts", len(m.Boundaries))
}
// The same invariant the generated path has: with a pure translation one margin closes and the other
// opens, by the same amount. Two plates at 2 cm/yr closing head-on give 4 cm/yr.
means := sortedMeans(m.Boundaries)
if means[0] >= 0 || means[1] <= 0 {
t.Fatalf("closing rates %.4g and %.4g; one of each is the only arrangement possible", means[0], means[1])
}
if got := math.Abs(means[1]); math.Abs(got-0.04) > 1e-3 {
t.Errorf("painted plates at 2 cm/yr each close at %.4g m/yr, want 0.04", got)
}
// Both painted as land, so both are continental and the closing margin is a collision.
if got, want := kinds(m.Boundaries), []string{"collision", "rift"}; !sameStrings(got, want) {
t.Errorf("got %v, want %v", got, want)
}
}
func allLandAt(xM, yM float64) bool { return true }
func TestAHeadingIsACompassBearing(t *testing.T) {
p := testPlanet(t)
px, lg := paintedStripes(400, 200)
// 0 points at the top of the image, which is -Y; 90 to the right, which is +X.
lg.Plates[0].HeadingDeg = 0
lg.Plates[1].HeadingDeg = 90
m, _, err := FromPainting(p, Default(), lg, px, 400, 200, allLandAt)
if err != nil {
t.Fatalf("from painting: %v", err)
}
north, east := m.Plates[0], m.Plates[1]
if math.Abs(north.TransXM) > 1e-9 || north.TransYM >= 0 {
t.Errorf("heading 0 gives (%.4g, %.4g); it should point at the top of the map",
north.TransXM, north.TransYM)
}
if math.Abs(east.TransYM) > 1e-9 || east.TransXM <= 0 {
t.Errorf("heading 90 gives (%.4g, %.4g); it should point to the right of the map",
east.TransXM, east.TransYM)
}
if got := math.Hypot(east.TransXM, east.TransYM); math.Abs(got-0.02) > 1e-9 {
t.Errorf("2 cm/yr came out as %.4g m/yr", got)
}
}
func TestThePaintingCanOverruleTheLandMask(t *testing.T) {
p := testPlanet(t)
px, lg := paintedStripes(400, 200)
// Every cell is land, so both plates are continental and the closing margin is a collision.
m, _, err := FromPainting(p, Default(), lg, px, 400, 200, allLandAt)
if err != nil {
t.Fatalf("from painting: %v", err)
}
if !m.Plates[0].Continental || !m.Plates[1].Continental {
t.Fatal("a planet of land has an oceanic plate on it")
}
// The legend says otherwise about one of them, and a legend that bothers to say so wins.
oceanic := false
lg.Plates[1].Continental = &oceanic
m, _, err = FromPainting(p, Default(), lg, px, 400, 200, allLandAt)
if err != nil {
t.Fatalf("from painting: %v", err)
}
if m.Plates[1].Continental {
t.Error("the legend called plate 1 oceanic and the land mask overruled it")
}
// And the consequence is the point of the override: the same margin is now a subduction zone.
if got, want := kinds(m.Boundaries), []string{"ridge", "subduction"}; !sameStrings(got, want) {
t.Errorf("got %v, want %v", got, want)
}
}
func TestACentroidIsMeasuredTheShortWayRound(t *testing.T) {
p := testPlanet(t)
const w, h = 400, 200
lg := &PaintLegend{
WarnDistance: 60,
Plates: []PaintPlate{
{Name: "seam", RGB: [3]int{200, 60, 60}, SpeedCmYr: 2, HeadingDeg: 90},
{Name: "rest", RGB: [3]int{60, 60, 200}, SpeedCmYr: 2, HeadingDeg: 270},
},
}
// Plate 0 is painted across the meridian: the left eighth and the right eighth of the image. Its centre
// is the seam, and an arithmetic mean of those columns would put it on the far side of the planet - and
// with it the pole it spins about.
px := make([]uint8, w*h*3)
for y := range h {
for x := range w {
id := 1
if x < w/8 || x >= 7*w/8 {
id = 0
}
o := (y*w + x) * 3
c := lg.Plates[id].RGB
px[o], px[o+1], px[o+2] = uint8(c[0]), uint8(c[1]), uint8(c[2])
}
}
m, _, err := FromPainting(p, Default(), lg, px, w, h, allLandAt)
if err != nil {
t.Fatalf("from painting: %v", err)
}
circ := p.CircumferenceM()
got := m.Plates[0].SiteXM
// Near the meridian, measured the short way round: either just above 0 or just below the circumference.
if d := math.Abs(wrapDelta(got, circ)); d > circ/16 {
t.Errorf("the seam-straddling plate's centre is at %.0f m of %.0f; it should be near the meridian, "+
"and the arithmetic mean would have put it near %.0f", got, circ, circ/2)
}
}
func TestAProposalReadsBackAsTheSamePlanet(t *testing.T) {
p := testPlanet(t)
cfg := Default()
cfg.Count = 6
land := func(xM, yM float64) bool { return yM > 4000 && yM < 14000 }
made, err := Build(p, 3630, cfg, land)
if err != nil {
t.Fatalf("build: %v", err)
}
// The round trip is what makes Propose worth having: what it writes has to be a layer that comes back as
// the planet it was written from, or an author's first edit starts from something that was never true.
px, w, h, lg := made.Propose(1600)
read, match, err := FromPainting(p, cfg, lg, px, w, h, land)
if err != nil {
t.Fatalf("read back: %v", err)
}
if match.Far != 0 {
t.Errorf("%d of %d cells of its own proposal did not match its own legend", match.Far, match.Cells)
}
if len(read.Plates) != len(made.Plates) {
t.Fatalf("%d plates written, %d read back", len(made.Plates), len(read.Plates))
}
// The motions survive the trip through the legend's cm/yr and degrees. Rounded when written - a tenth of
// a cm/yr and a whole degree - so the tolerance is the rounding, not a fudge.
for i := range made.Plates {
a, b := made.Plates[i], read.Plates[i]
if d := math.Hypot(a.TransXM-b.TransXM, a.TransYM-b.TransYM); d > 0.0006 {
t.Errorf("plate %d drifts %.5g m/yr differently after the round trip", i, d)
}
if a.Continental != b.Continental {
t.Errorf("plate %d was %v continental and reads back %v", i, a.Continental, b.Continental)
}
}
// And the tectonics: the same margins doing the same things. Not vertex-for-vertex - the proposal is a
// raster at 1600 px and the model was traced at the tectonic grid - but the same boundaries by count and
// by what each one is.
if len(read.Boundaries) != len(made.Boundaries) {
t.Errorf("%d boundaries written, %d read back", len(made.Boundaries), len(read.Boundaries))
}
if got, want := kinds(read.Boundaries), kinds(made.Boundaries); !sameStrings(got, want) {
t.Errorf("margins read back as %v, were %v", got, want)
}
}
func TestALegendThatCannotBeAPlanetIsRefused(t *testing.T) {
one := &PaintLegend{Plates: []PaintPlate{{Name: "only", RGB: [3]int{1, 2, 3}}}}
if err := one.validate("test"); err == nil {
t.Error("a planet in one plate was accepted; it has no boundaries")
}
dup := &PaintLegend{Plates: []PaintPlate{
{Name: "a", RGB: [3]int{1, 2, 3}},
{Name: "b", RGB: [3]int{1, 2, 3}},
}}
if err := dup.validate("test"); err == nil {
t.Error("two plates sharing a colour were accepted; a colour is one plate")
}
}