package overlay import ( "encoding/json" "image/png" "os" "path/filepath" "salty/terrain/internal/field" ) // What the overlay hands to whatever builds the level. // // Two shapes, because two different things want it. A *raster* answers "what is under this square metre" and // is what a per-tile importer wants: one 8-bit image beside each height tile, an index per detail cell, zero // for nothing. A *feature list* answers "where do I put the village" and "what curve does the road follow", // and lives once at the planet root in world metres because a point is not a pixel and a spline crossing a // tile boundary is still one spline. // // Nothing in the generator reads either of them back. That is the point of the layer. // CoastScale is the per-pixel multiplier on the waterline roughening, or nil when no mark asks for one. // // A pixel with no instruction comes back negative rather than 1, which is the contract template.Coast.Scale // documents: an unmarked cell takes its instruction from the far side of the waterline instead of overriding // what the marked side said. func (l *Legend) CoastScale(r *Raster) []float32 { if !l.TouchesCoast() { return nil } per := make([]float32, len(l.Marks)+1) per[Blank] = -1 for i := range l.Marks { if j, set := l.Marks[i].Jitter(); set { per[i+1] = float32(j) } else { per[i+1] = -1 } } out := make([]float32, len(r.Mark)) field.Rows(r.H, func(y0, y1 int) { for i := y0 * r.W; i < y1*r.W; i++ { out[i] = per[r.Mark[i]] } }) return out } // Document is overlay.json: everything an importer needs to place what the author painted. type Document struct { Image string `json:"image"` Legend string `json:"legend"` // The frame the coordinates are in: metres east from the seam and metres south from the top painted row, // which is the same frame world.Planet uses for a painted cell once the polar pad is taken off. CircumferenceM float64 `json:"circumference_m"` HeightM float64 `json:"height_m"` PaintW int `json:"paint_w"` PaintH int `json:"paint_h"` MetresPerPxX float64 `json:"metres_per_px_x"` MetresPerPxY float64 `json:"metres_per_px_y"` Marks []MarkShare `json:"marks"` Features []Feature `json:"features"` } // MarkShare is one mark and how much of the world carries it. type MarkShare struct { Index int `json:"index"` Name string `json:"name"` Kind string `json:"kind"` RGB [3]int `json:"rgb"` Cells int `json:"cells_px"` AreaKm2 float64 `json:"area_km2"` Pieces int `json:"pieces"` WidthM float64 `json:"width_m,omitempty"` // Jitter and HasJitter are a pair and neither is omitempty, because the interesting value of the first // is **zero** - a pinned coastline - and omitting it would leave every consumer of this file unable to // tell "pin it" from "said nothing", which is the one distinction the key exists to make. Jitter float64 `json:"coast_jitter"` HasJitter bool `json:"has_coast_jitter"` Note string `json:"note,omitempty"` } // Describe builds the document from a classified overlay. func (l *Legend) Describe(r *Raster, m Match, s Scale, image, legend string) *Document { feats := l.Features(r, s) pieces := make([]int, len(l.Marks)+1) for _, f := range feats { pieces[f.Index]++ } doc := &Document{ Image: image, Legend: legend, CircumferenceM: s.CircumferenceM, HeightM: float64(r.H) * s.MetresPerPxY, PaintW: r.W, PaintH: r.H, MetresPerPxX: s.MetresPerPxX, MetresPerPxY: s.MetresPerPxY, Features: feats, } for i := range l.Marks { mk := &l.Marks[i] cells := 0 if i+1 < len(m.Counts) { cells = m.Counts[i+1] } share := MarkShare{ Index: i + 1, Name: mk.Name, Kind: mk.Kind, RGB: mk.RGB, Cells: cells, AreaKm2: float64(cells) * s.MetresPerPxX * s.MetresPerPxY / 1e6, Pieces: pieces[i+1], WidthM: mk.WidthM, Note: mk.Note, } if j, set := mk.Jitter(); set { share.Jitter, share.HasJitter = j, true } doc.Marks = append(doc.Marks, share) } return doc } // WriteJSON writes overlay.json. func (d *Document) WriteJSON(dir string) error { data, err := json.MarshalIndent(d, "", " ") if err != nil { return err } return os.WriteFile(filepath.Join(dir, "overlay.json"), append(data, '\n'), 0o644) } // WriteMask writes an 8-bit indexed PNG: one mark index a pixel, zero for nothing. It is the per-tile // output, and it is indexed rather than one image a mark because marks cannot overlap - the overlay is one // painting and a pixel is one colour - so 254 masks fit in the file one would have taken. func WriteMask(path string, w, h int, marks []uint8) error { return field.WriteGray8(path, w, h, marks, png.BestCompression) } // SampleWorld reads the overlay over a rectangle of some other grid, described in world metres. // // World metres rather than cell indices, because the caller is a *detail* tile: it is 2 m where the overlay // is 12.9 and the geology is 8, and it sits at an origin that is only expressible in metres. Going through // the common frame is the only way the three agree, and it is rule 1 of the tiling plan applied to a raster // instead of to a noise - a cell gets the same mark whichever tile reaches it. // // Nearest neighbour, for the same reason the class raster is: an index is a name, and the average of // "forest" and "road" is neither. func (r *Raster) SampleWorld(originXM, originYM, cellM float64, w, h int, s Scale) []uint8 { out := make([]uint8, w*h) field.Rows(h, func(b0, b1 int) { for y := b0; y < b1; y++ { py := int((originYM + (float64(y)+0.5)*cellM) / s.MetresPerPxY) if py < 0 { py = 0 } else if py >= r.H { py = r.H - 1 } row := py * r.W for x := 0; x < w; x++ { px := int((originXM + (float64(x)+0.5)*cellM) / s.MetresPerPxX) px = ((px % r.W) + r.W) % r.W out[y*w+x] = r.Mark[row+px] } } }) return out }