Tooling
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user