package coast import ( "math" "salty/terrain/internal/field" ) // The coast is a *distance*, not a line. Every coastal process is written in terms of how far a cell is from // the waterline and which stretch of waterline it belongs to: the shelf deepens with distance offshore, the // surf planes the land within a reach of it, sediment settles in the shallows behind it, and shelter is a // property of a stretch of shore that every cell near it inherits. So the first thing the pass builds is an // exact signed distance field with a feature index, and everything after it is a lookup. // // Exact, not a chamfer approximation: Felzenszwalb & Huttenlocher's transform is two 1-D passes and O(n) // whatever the radius, so there is nothing to buy by approximating, and a chamfer's 2 % anisotropy would show // up directly as a shelf that is wider along the grid axes than across them. // edt returns, for every cell, the squared distance in cells to the nearest seed cell and the index of that // seed. A column pass finds the nearest seed in each column; a row pass takes the lower envelope of the // parabolas those distances define. // // Cells in a column with no seed at all are given a cost above any real distance rather than an infinity, so // the envelope arithmetic never sees a NaN; they are then never chosen unless the map has no seeds anywhere, // which the caller checks for. func edt(seed []bool, w, h int) (d2 []float32, near []int32) { d2 = make([]float32, w*h) near = make([]int32, w*h) bigF := float64(w*w+h*h) * 4 // above any achievable dx² + dy² bigD := float32(math.Sqrt(bigF)) colD := make([]float32, w*h) // distance in cells to the nearest seed in this column colN := make([]int32, w*h) // that seed's row, or -1 field.Rows(w, func(x0, x1 int) { for x := x0; x < x1; x++ { best := -1 for y := 0; y < h; y++ { i := y*w + x if seed[i] { best = y } if best < 0 { colD[i], colN[i] = bigD, -1 } else { colD[i], colN[i] = float32(y-best), int32(best) } } best = -1 for y := h - 1; y >= 0; y-- { i := y*w + x if seed[i] { best = y } if best >= 0 { if d := float32(best - y); d < colD[i] { colD[i], colN[i] = d, int32(best) } } } } }) field.Rows(h, func(y0, y1 int) { f := make([]float64, w) v := make([]int, w) z := make([]float64, w+1) for y := y0; y < y1; y++ { row := y * w for x := 0; x < w; x++ { d := float64(colD[row+x]) f[x] = d * d } k := 0 v[0] = 0 z[0] = math.Inf(-1) z[1] = math.Inf(1) for q := 1; q < w; q++ { s := intersect(f, v[k], q) for s <= z[k] { k-- s = intersect(f, v[k], q) } k++ v[k] = q z[k] = s z[k+1] = math.Inf(1) } k = 0 for q := 0; q < w; q++ { for z[k+1] < float64(q) { k++ } dx := float64(q - v[k]) d2[row+q] = float32(dx*dx + f[v[k]]) if n := colN[row+v[k]]; n < 0 { near[row+q] = -1 } else { near[row+q] = n*int32(w) + int32(v[k]) } } } }) return d2, near } // intersect is where the parabolas rooted at p and q cross. func intersect(f []float64, p, q int) float64 { return ((f[q] + float64(q*q)) - (f[p] + float64(p*p))) / float64(2*q-2*p) } // Geometry is the coastline as the rest of the pass sees it. type Geometry struct { W, H int CellM float64 // Dist is metres to the waterline: positive inland, negative offshore. Dist *field.Field // Ref is, for every cell, the waterline cell whose stretch of shore it belongs to. A land cell takes the // sea cell nearest to it, which is on the waterline by construction; a sea cell takes the waterline cell // nearest to the land cell nearest to it, which is the stretch of shore facing it. Every per-shore // quantity — shelter, shelf width, the backshore relief — is computed once on the waterline and read // everywhere else through this. Ref []int32 // Waterline is the sea cells that touch land, in row-major order so anything iterating them is // deterministic. Waterline []int32 // ShoreM is the length of the land/sea boundary in metres, counted as boundary edges. It overestimates a // diagonal coast by about 4/pi, as any edge-counted perimeter does. ShoreM float64 } // Measure builds the signed distance field and the shore reference from a land/sea mask. func Measure(sea []bool, w, h int, cellM float64) *Geometry { anySea, anyLand := false, false land := make([]bool, len(sea)) for i, s := range sea { land[i] = !s if s { anySea = true } else { anyLand = true } } g := &Geometry{W: w, H: h, CellM: cellM, Dist: field.New(w, h, cellM), Ref: make([]int32, w*h)} for i := range g.Ref { g.Ref[i] = -1 } if !anySea || !anyLand { return g // an all-land or all-sea map has no coast; every pass below is a no-op on it } d2Sea, nearSea := edt(sea, w, h) // for a land cell: how far to water, and where d2Land, nearLand := edt(land, w, h) // for a sea cell: how far to land, and where for i := range sea { if sea[i] { g.Dist.Data[i] = float32(-math.Sqrt(float64(d2Land[i])) * cellM) } else { g.Dist.Data[i] = float32(math.Sqrt(float64(d2Sea[i])) * cellM) } } // The waterline: sea cells with land in the eight-neighbourhood, which is d2Land of 1 or 2. for i := range sea { if sea[i] && d2Land[i] <= 2.001 { g.Waterline = append(g.Waterline, int32(i)) } } for i := range sea { if sea[i] { if l := nearLand[i]; l >= 0 { g.Ref[i] = nearSea[l] } } else { g.Ref[i] = nearSea[i] } } // Perimeter by boundary edges, which is what a shoreline length means on a grid. edges := 0 for y := 0; y < h; y++ { for x := 0; x < w; x++ { i := y*w + x if x+1 < w && sea[i] != sea[i+1] { edges++ } if y+1 < h && sea[i] != sea[i+w] { edges++ } } } g.ShoreM = float64(edges) * cellM return g }