doodle/pkg/level/chunk_map.go

193 lines
4.5 KiB
Go
Raw Normal View History

Implement Chunk System for Pixel Data Starts the implementation of the chunk-based pixel storage system for levels and drawings. Previously the levels had a Pixels structure which was just an array of X,Y and palette index triplets. The new chunk system divides the map up into square chunks, and lets each chunk manage its own memory layout. The "MapAccessor" layout is implemented first which is a map of X,Y coordinates to their Swatches (pointer to an index of the palette). When serialized the MapAccessor maps the "X,Y": "index" similarly to the old Pixels array. The object hierarchy for the chunk system is like: * Chunker: the manager of the chunks who keeps track of the ChunkSize and a map of "chunk coordinates" to the chunk in charge of it. * Chunk: a part of the drawing ChunkSize length square. A chunk has a Type (of how it stores its data, 0 being a map[Point]Swatch and 1 being a [][]Swatch 2D array), and the chunk has an Accessor which implements the underlying type. * Accessor: an interface for a Chunk to provide access to its pixels. * MapAccessor: a "sparse map" of coordinates to their Swatches. * GridAccessor: TBD, will be a "dense" 2D grid of Swatches. The JSON files are loaded in two passes: 1. The chunks only load their swatch indexes from disk. 2. With the palette also loaded, the chunks are "inflated" and linked to their swatch pointers. Misc changes: * The `level.Canvas` UI widget switches from the old Grid data type to being able to directly use a `level.Chunker` * The Chunker is a shared data type between the on-disk level format and the actual renderer (level.Canvas), so saving the level is easy because you can just pull the Chunker out from the canvas. * ChunkSize is stored inside the level file and the default value is at balance/numbers.go: 1000
2018-09-23 22:20:45 +00:00
package level
import (
"encoding/json"
"errors"
"fmt"
"git.kirsle.net/apps/doodle/lib/render"
"github.com/vmihailenco/msgpack"
Implement Chunk System for Pixel Data Starts the implementation of the chunk-based pixel storage system for levels and drawings. Previously the levels had a Pixels structure which was just an array of X,Y and palette index triplets. The new chunk system divides the map up into square chunks, and lets each chunk manage its own memory layout. The "MapAccessor" layout is implemented first which is a map of X,Y coordinates to their Swatches (pointer to an index of the palette). When serialized the MapAccessor maps the "X,Y": "index" similarly to the old Pixels array. The object hierarchy for the chunk system is like: * Chunker: the manager of the chunks who keeps track of the ChunkSize and a map of "chunk coordinates" to the chunk in charge of it. * Chunk: a part of the drawing ChunkSize length square. A chunk has a Type (of how it stores its data, 0 being a map[Point]Swatch and 1 being a [][]Swatch 2D array), and the chunk has an Accessor which implements the underlying type. * Accessor: an interface for a Chunk to provide access to its pixels. * MapAccessor: a "sparse map" of coordinates to their Swatches. * GridAccessor: TBD, will be a "dense" 2D grid of Swatches. The JSON files are loaded in two passes: 1. The chunks only load their swatch indexes from disk. 2. With the palette also loaded, the chunks are "inflated" and linked to their swatch pointers. Misc changes: * The `level.Canvas` UI widget switches from the old Grid data type to being able to directly use a `level.Chunker` * The Chunker is a shared data type between the on-disk level format and the actual renderer (level.Canvas), so saving the level is easy because you can just pull the Chunker out from the canvas. * ChunkSize is stored inside the level file and the default value is at balance/numbers.go: 1000
2018-09-23 22:20:45 +00:00
)
// MapAccessor implements a chunk accessor by using a map of points to their
// palette indexes. This is the simplest accessor and is best for sparse chunks.
type MapAccessor map[render.Point]*Swatch
// NewMapAccessor initializes a MapAccessor.
func NewMapAccessor() MapAccessor {
return MapAccessor{}
}
// Inflate the sparse swatches from their palette indexes.
func (a MapAccessor) Inflate(pal *Palette) error {
for point, swatch := range a {
if swatch.IsSparse() {
// Replace this with the correct swatch from the palette.
if swatch.paletteIndex >= len(pal.Swatches) {
Implement Chunk System for Pixel Data Starts the implementation of the chunk-based pixel storage system for levels and drawings. Previously the levels had a Pixels structure which was just an array of X,Y and palette index triplets. The new chunk system divides the map up into square chunks, and lets each chunk manage its own memory layout. The "MapAccessor" layout is implemented first which is a map of X,Y coordinates to their Swatches (pointer to an index of the palette). When serialized the MapAccessor maps the "X,Y": "index" similarly to the old Pixels array. The object hierarchy for the chunk system is like: * Chunker: the manager of the chunks who keeps track of the ChunkSize and a map of "chunk coordinates" to the chunk in charge of it. * Chunk: a part of the drawing ChunkSize length square. A chunk has a Type (of how it stores its data, 0 being a map[Point]Swatch and 1 being a [][]Swatch 2D array), and the chunk has an Accessor which implements the underlying type. * Accessor: an interface for a Chunk to provide access to its pixels. * MapAccessor: a "sparse map" of coordinates to their Swatches. * GridAccessor: TBD, will be a "dense" 2D grid of Swatches. The JSON files are loaded in two passes: 1. The chunks only load their swatch indexes from disk. 2. With the palette also loaded, the chunks are "inflated" and linked to their swatch pointers. Misc changes: * The `level.Canvas` UI widget switches from the old Grid data type to being able to directly use a `level.Chunker` * The Chunker is a shared data type between the on-disk level format and the actual renderer (level.Canvas), so saving the level is easy because you can just pull the Chunker out from the canvas. * ChunkSize is stored inside the level file and the default value is at balance/numbers.go: 1000
2018-09-23 22:20:45 +00:00
return fmt.Errorf("MapAccessor.Inflate: swatch for point %s has paletteIndex %d but palette has only %d colors",
point,
swatch.paletteIndex,
len(pal.Swatches),
)
}
a[point] = pal.Swatches[swatch.paletteIndex]
}
}
return nil
}
// Len returns the current size of the map, or number of pixels registered.
func (a MapAccessor) Len() int {
return len(a)
}
// IterViewport returns a channel to loop over pixels in the viewport.
func (a MapAccessor) IterViewport(viewport render.Rect) <-chan Pixel {
pipe := make(chan Pixel)
go func() {
for px := range a.Iter() {
if px.Point().Inside(viewport) {
pipe <- px
}
}
close(pipe)
}()
return pipe
}
// Iter returns a channel to loop over all points in this chunk.
func (a MapAccessor) Iter() <-chan Pixel {
pipe := make(chan Pixel)
go func() {
for point, swatch := range a {
pipe <- Pixel{
X: point.X,
Y: point.Y,
Swatch: swatch,
}
}
close(pipe)
}()
return pipe
}
// Get a pixel from the map.
func (a MapAccessor) Get(p render.Point) (*Swatch, error) {
pixel, ok := a[p]
if !ok {
return nil, errors.New("no pixel")
}
return pixel, nil
}
// Set a pixel on the map.
func (a MapAccessor) Set(p render.Point, sw *Swatch) error {
a[p] = sw
return nil
}
// Delete a pixel from the map.
func (a MapAccessor) Delete(p render.Point) error {
if _, ok := a[p]; ok {
delete(a, p)
return nil
}
return errors.New("pixel was not there")
}
// MarshalJSON to convert the chunk map to JSON.
//
// When serialized, the key is the "X,Y" coordinate and the value is the
// swatch index of the Palette, rather than redundantly serializing out the
// Swatch object for every pixel.
func (a MapAccessor) MarshalJSON() ([]byte, error) {
dict := map[string]int{}
for point, sw := range a {
dict[point.String()] = sw.Index()
}
out, err := json.Marshal(dict)
return out, err
}
// UnmarshalJSON to convert the chunk map back from JSON.
func (a MapAccessor) UnmarshalJSON(b []byte) error {
var dict map[string]int
err := json.Unmarshal(b, &dict)
if err != nil {
return err
}
for coord, index := range dict {
point, err := render.ParsePoint(coord)
if err != nil {
return fmt.Errorf("MapAccessor.UnmarshalJSON: %s", err)
}
a[point] = NewSparseSwatch(index)
}
return nil
}
// // MarshalMsgpack serializes for msgpack.
// func (a MapAccessor) MarshalMsgpack() ([]byte, error) {
// dict := map[string]int{}
// for point, sw := range a {
// dict[point.String()] = sw.Index()
// }
// return msgpack.Marshal(dict)
// }
//
// // Serialize converts the chunk accessor to a map for serialization.
// func (a MapAccessor) Serialize() interface{} {
// dict := map[string]int{}
// for point, sw := range a {
// dict[point.String()] = sw.Index()
// }
// return dict
// }
//
// // UnmarshalMsgpack decodes from msgpack format.
// func (a MapAccessor) UnmarshalMsgpack(b []byte) error {
// var dict map[string]int
// err := msgpack.Unmarshal(b, &dict)
// if err != nil {
// return err
// }
//
// for coord, index := range dict {
// point, err := render.ParsePoint(coord)
// if err != nil {
// return fmt.Errorf("MapAccessor.UnmarshalJSON: %s", err)
// }
// a[point] = NewSparseSwatch(index)
// }
//
// return nil
// }
func (a MapAccessor) EncodeMsgpack(enc *msgpack.Encoder) error {
dict := map[string]int{}
for point, sw := range a {
dict[point.String()] = sw.Index()
}
return enc.Encode(dict)
}
func (a MapAccessor) DecodeMsgpack(dec *msgpack.Decoder) error {
v, err := dec.DecodeMap()
if err != nil {
return fmt.Errorf("MapAccessor.DecodeMsgpack: %s", err)
}
dict := v.(map[string]int)
for coord, index := range dict {
point, err := render.ParsePoint(coord)
if err != nil {
return fmt.Errorf("MapAccessor.UnmarshalJSON: %s", err)
}
a[point] = NewSparseSwatch(index)
}
return nil
}