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
This commit is contained in:
parent
90a4859326
commit
3c185528f9
|
@ -4,4 +4,7 @@ package balance
|
||||||
var (
|
var (
|
||||||
// Speed to scroll a canvas with arrow keys in Edit Mode.
|
// Speed to scroll a canvas with arrow keys in Edit Mode.
|
||||||
CanvasScrollSpeed int32 = 8
|
CanvasScrollSpeed int32 = 8
|
||||||
|
|
||||||
|
// Default chunk size for canvases.
|
||||||
|
ChunkSize = 1000
|
||||||
)
|
)
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"git.kirsle.net/apps/doodle/balance"
|
||||||
"git.kirsle.net/apps/doodle/events"
|
"git.kirsle.net/apps/doodle/events"
|
||||||
"git.kirsle.net/apps/doodle/level"
|
"git.kirsle.net/apps/doodle/level"
|
||||||
"git.kirsle.net/apps/doodle/render"
|
"git.kirsle.net/apps/doodle/render"
|
||||||
|
@ -14,7 +15,7 @@ type EditorScene struct {
|
||||||
// Configuration for the scene initializer.
|
// Configuration for the scene initializer.
|
||||||
OpenFile bool
|
OpenFile bool
|
||||||
Filename string
|
Filename string
|
||||||
Canvas *level.Grid
|
Canvas *level.Chunker
|
||||||
|
|
||||||
UI *EditorUI
|
UI *EditorUI
|
||||||
|
|
||||||
|
@ -37,7 +38,7 @@ func (s *EditorScene) Name() string {
|
||||||
|
|
||||||
// Setup the editor scene.
|
// Setup the editor scene.
|
||||||
func (s *EditorScene) Setup(d *Doodle) error {
|
func (s *EditorScene) Setup(d *Doodle) error {
|
||||||
s.drawing = level.NewCanvas(true)
|
s.drawing = level.NewCanvas(balance.ChunkSize, true)
|
||||||
s.drawing.Palette = level.DefaultPalette()
|
s.drawing.Palette = level.DefaultPalette()
|
||||||
if len(s.drawing.Palette.Swatches) > 0 {
|
if len(s.drawing.Palette.Swatches) > 0 {
|
||||||
s.drawing.SetSwatch(s.drawing.Palette.Swatches[0])
|
s.drawing.SetSwatch(s.drawing.Palette.Swatches[0])
|
||||||
|
@ -80,7 +81,7 @@ func (s *EditorScene) Loop(d *Doodle, ev *events.State) error {
|
||||||
if ev.KeyName.Read() == "p" {
|
if ev.KeyName.Read() == "p" {
|
||||||
log.Info("Play Mode, Go!")
|
log.Info("Play Mode, Go!")
|
||||||
d.Goto(&PlayScene{
|
d.Goto(&PlayScene{
|
||||||
Canvas: s.drawing.Grid(),
|
// Canvas: s.drawing.Grid(), XXX
|
||||||
})
|
})
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -122,14 +123,7 @@ func (s *EditorScene) SaveLevel(filename string) {
|
||||||
m.Width = s.width
|
m.Width = s.width
|
||||||
m.Height = s.height
|
m.Height = s.height
|
||||||
m.Palette = s.drawing.Palette
|
m.Palette = s.drawing.Palette
|
||||||
|
m.Chunker = s.drawing.Chunker()
|
||||||
for pixel := range *s.drawing.Grid() {
|
|
||||||
m.Pixels = append(m.Pixels, &level.Pixel{
|
|
||||||
X: pixel.X,
|
|
||||||
Y: pixel.Y,
|
|
||||||
PaletteIndex: int32(pixel.Swatch.Index()),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
json, err := m.ToJSON()
|
json, err := m.ToJSON()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
2
fps.go
2
fps.go
|
@ -13,7 +13,7 @@ const maxSamples = 100
|
||||||
// Debug mode options, these can be enabled in the dev console
|
// Debug mode options, these can be enabled in the dev console
|
||||||
// like: boolProp DebugOverlay true
|
// like: boolProp DebugOverlay true
|
||||||
var (
|
var (
|
||||||
DebugOverlay = false
|
DebugOverlay = true
|
||||||
DebugCollision = true
|
DebugCollision = true
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,7 @@ type Canvas struct {
|
||||||
// Set to true to allow clicking to edit this canvas.
|
// Set to true to allow clicking to edit this canvas.
|
||||||
Editable bool
|
Editable bool
|
||||||
|
|
||||||
grid Grid
|
chunks *Chunker
|
||||||
pixelHistory []*Pixel
|
pixelHistory []*Pixel
|
||||||
lastPixel *Pixel
|
lastPixel *Pixel
|
||||||
|
|
||||||
|
@ -24,35 +24,31 @@ type Canvas struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCanvas initializes a Canvas widget.
|
// NewCanvas initializes a Canvas widget.
|
||||||
func NewCanvas(editable bool) *Canvas {
|
func NewCanvas(size int, editable bool) *Canvas {
|
||||||
w := &Canvas{
|
w := &Canvas{
|
||||||
Editable: editable,
|
Editable: editable,
|
||||||
Palette: NewPalette(),
|
Palette: NewPalette(),
|
||||||
grid: Grid{},
|
chunks: NewChunker(size),
|
||||||
}
|
}
|
||||||
w.setup()
|
w.setup()
|
||||||
return w
|
return w
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load initializes the Canvas using an existing Palette and Grid.
|
// Load initializes the Canvas using an existing Palette and Grid.
|
||||||
func (w *Canvas) Load(p *Palette, g *Grid) {
|
func (w *Canvas) Load(p *Palette, g *Chunker) {
|
||||||
w.Palette = p
|
w.Palette = p
|
||||||
w.grid = *g
|
w.chunks = g
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadFilename initializes the Canvas using a file on disk.
|
// LoadFilename initializes the Canvas using a file on disk.
|
||||||
func (w *Canvas) LoadFilename(filename string) error {
|
func (w *Canvas) LoadFilename(filename string) error {
|
||||||
w.grid = Grid{}
|
|
||||||
|
|
||||||
m, err := LoadJSON(filename)
|
m, err := LoadJSON(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, pixel := range m.Pixels {
|
|
||||||
w.grid[pixel] = nil
|
|
||||||
}
|
|
||||||
w.Palette = m.Palette
|
w.Palette = m.Palette
|
||||||
|
w.chunks = m.Chunker
|
||||||
|
|
||||||
if len(w.Palette.Swatches) > 0 {
|
if len(w.Palette.Swatches) > 0 {
|
||||||
w.SetSwatch(w.Palette.Swatches[0])
|
w.SetSwatch(w.Palette.Swatches[0])
|
||||||
|
@ -80,7 +76,6 @@ func (w *Canvas) setup() {
|
||||||
// Loop is called on the scene's event loop to handle mouse interaction with
|
// Loop is called on the scene's event loop to handle mouse interaction with
|
||||||
// the canvas, i.e. to edit it.
|
// the canvas, i.e. to edit it.
|
||||||
func (w *Canvas) Loop(ev *events.State) error {
|
func (w *Canvas) Loop(ev *events.State) error {
|
||||||
log.Info("my territory")
|
|
||||||
var (
|
var (
|
||||||
P = w.Point()
|
P = w.Point()
|
||||||
_ = P
|
_ = P
|
||||||
|
@ -117,9 +112,13 @@ func (w *Canvas) Loop(ev *events.State) error {
|
||||||
if ev.Button1.Now {
|
if ev.Button1.Now {
|
||||||
// log.Warn("Button1: %+v", ev.Button1)
|
// log.Warn("Button1: %+v", ev.Button1)
|
||||||
lastPixel := w.lastPixel
|
lastPixel := w.lastPixel
|
||||||
pixel := &Pixel{
|
cursor := render.Point{
|
||||||
X: ev.CursorX.Now - P.X + w.Scroll.X,
|
X: ev.CursorX.Now - P.X + w.Scroll.X,
|
||||||
Y: ev.CursorY.Now - P.Y + w.Scroll.Y,
|
Y: ev.CursorY.Now - P.Y + w.Scroll.Y,
|
||||||
|
}
|
||||||
|
pixel := &Pixel{
|
||||||
|
X: cursor.X,
|
||||||
|
Y: cursor.Y,
|
||||||
Palette: w.Palette,
|
Palette: w.Palette,
|
||||||
Swatch: w.Palette.ActiveSwatch,
|
Swatch: w.Palette.ActiveSwatch,
|
||||||
}
|
}
|
||||||
|
@ -130,13 +129,7 @@ func (w *Canvas) Loop(ev *events.State) error {
|
||||||
// Draw the pixels in between.
|
// Draw the pixels in between.
|
||||||
if lastPixel != pixel {
|
if lastPixel != pixel {
|
||||||
for point := range render.IterLine(lastPixel.X, lastPixel.Y, pixel.X, pixel.Y) {
|
for point := range render.IterLine(lastPixel.X, lastPixel.Y, pixel.X, pixel.Y) {
|
||||||
dot := &Pixel{
|
w.chunks.Set(point, lastPixel.Swatch)
|
||||||
X: point.X,
|
|
||||||
Y: point.Y,
|
|
||||||
Palette: lastPixel.Palette,
|
|
||||||
Swatch: lastPixel.Swatch,
|
|
||||||
}
|
|
||||||
w.grid[dot] = nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -145,7 +138,7 @@ func (w *Canvas) Loop(ev *events.State) error {
|
||||||
w.pixelHistory = append(w.pixelHistory, pixel)
|
w.pixelHistory = append(w.pixelHistory, pixel)
|
||||||
|
|
||||||
// Save in the pixel canvas map.
|
// Save in the pixel canvas map.
|
||||||
w.grid[pixel] = nil
|
w.chunks.Set(cursor, pixel.Swatch)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
w.lastPixel = nil
|
w.lastPixel = nil
|
||||||
|
@ -167,9 +160,9 @@ func (w *Canvas) Viewport() render.Rect {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Grid returns the underlying grid object.
|
// Chunker returns the underlying Chunker object.
|
||||||
func (w *Canvas) Grid() *Grid {
|
func (w *Canvas) Chunker() *Chunker {
|
||||||
return &w.grid
|
return w.chunks
|
||||||
}
|
}
|
||||||
|
|
||||||
// ScrollBy adjusts the viewport scroll position.
|
// ScrollBy adjusts the viewport scroll position.
|
||||||
|
@ -197,20 +190,15 @@ func (w *Canvas) Present(e render.Engine, p render.Point) {
|
||||||
H: S.H - w.BoxThickness(2),
|
H: S.H - w.BoxThickness(2),
|
||||||
})
|
})
|
||||||
|
|
||||||
for pixel := range w.grid {
|
for px := range w.chunks.IterViewport(Viewport) {
|
||||||
point := render.NewPoint(pixel.X, pixel.Y)
|
|
||||||
if point.Inside(Viewport) {
|
|
||||||
// This pixel is visible in the canvas, but offset it by the
|
// This pixel is visible in the canvas, but offset it by the
|
||||||
// scroll height.
|
// scroll height.
|
||||||
point.Add(render.Point{
|
px.X -= Viewport.X
|
||||||
X: -Viewport.X,
|
px.Y -= Viewport.Y
|
||||||
Y: -Viewport.Y,
|
color := px.Swatch.Color
|
||||||
})
|
|
||||||
color := pixel.Swatch.Color
|
|
||||||
e.DrawPoint(color, render.Point{
|
e.DrawPoint(color, render.Point{
|
||||||
X: p.X + w.BoxThickness(1) + point.X,
|
X: p.X + w.BoxThickness(1) + px.X,
|
||||||
Y: p.Y + w.BoxThickness(1) + point.Y,
|
Y: p.Y + w.BoxThickness(1) + px.Y,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
89
level/chunk.go
Normal file
89
level/chunk.go
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
package level
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"git.kirsle.net/apps/doodle/render"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Types of chunks.
|
||||||
|
const (
|
||||||
|
MapType int = iota
|
||||||
|
GridType
|
||||||
|
)
|
||||||
|
|
||||||
|
// Chunk holds a single portion of the pixel canvas.
|
||||||
|
type Chunk struct {
|
||||||
|
Type int // map vs. 2D array.
|
||||||
|
Accessor
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSONChunk holds a lightweight (interface-free) copy of the Chunk for
|
||||||
|
// unmarshalling JSON files from disk.
|
||||||
|
type JSONChunk struct {
|
||||||
|
Type int `json:"type"`
|
||||||
|
Data json.RawMessage `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Accessor provides a high-level API to interact with absolute pixel coordinates
|
||||||
|
// while abstracting away the details of how they're stored.
|
||||||
|
type Accessor interface {
|
||||||
|
Inflate(*Palette) error
|
||||||
|
Iter() <-chan Pixel
|
||||||
|
IterViewport(viewport render.Rect) <-chan Pixel
|
||||||
|
Get(render.Point) (*Swatch, error)
|
||||||
|
Set(render.Point, *Swatch) error
|
||||||
|
Delete(render.Point) error
|
||||||
|
Len() int
|
||||||
|
MarshalJSON() ([]byte, error)
|
||||||
|
UnmarshalJSON([]byte) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewChunk creates a new chunk.
|
||||||
|
func NewChunk() *Chunk {
|
||||||
|
return &Chunk{
|
||||||
|
Type: MapType,
|
||||||
|
Accessor: NewMapAccessor(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage returns the percent of free space vs. allocated pixels in the chunk.
|
||||||
|
func (c *Chunk) Usage(size int) float64 {
|
||||||
|
return float64(c.Len()) / float64(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON writes the chunk to JSON.
|
||||||
|
func (c *Chunk) MarshalJSON() ([]byte, error) {
|
||||||
|
data, err := c.Accessor.MarshalJSON()
|
||||||
|
if err != nil {
|
||||||
|
return []byte{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
generic := &JSONChunk{
|
||||||
|
Type: c.Type,
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
b, err := json.Marshal(generic)
|
||||||
|
return b, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON loads the chunk from JSON and uses the correct accessor to
|
||||||
|
// parse the inner details.
|
||||||
|
func (c *Chunk) UnmarshalJSON(b []byte) error {
|
||||||
|
// Parse it generically so we can hand off the inner "data" object to the
|
||||||
|
// right accessor for unmarshalling.
|
||||||
|
generic := &JSONChunk{}
|
||||||
|
err := json.Unmarshal(b, generic)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Chunk.UnmarshalJSON: failed to unmarshal into generic JSONChunk type: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch c.Type {
|
||||||
|
case MapType:
|
||||||
|
c.Accessor = NewMapAccessor()
|
||||||
|
return c.Accessor.UnmarshalJSON(generic.Data)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("Chunk.UnmarshalJSON: unsupported chunk type '%d'", c.Type)
|
||||||
|
}
|
||||||
|
}
|
129
level/chunk_map.go
Normal file
129
level/chunk_map.go
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
package level
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"git.kirsle.net/apps/doodle/render"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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 len(pal.Swatches) < swatch.paletteIndex {
|
||||||
|
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
|
||||||
|
}
|
306
level/chunk_test.go
Normal file
306
level/chunk_test.go
Normal file
|
@ -0,0 +1,306 @@
|
||||||
|
package level_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.kirsle.net/apps/doodle/level"
|
||||||
|
"git.kirsle.net/apps/doodle/render"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Test the high level Chunker.
|
||||||
|
func TestChunker(t *testing.T) {
|
||||||
|
c := level.NewChunker(1000)
|
||||||
|
|
||||||
|
// Test swatches.
|
||||||
|
var (
|
||||||
|
grey = &level.Swatch{
|
||||||
|
Name: "solid",
|
||||||
|
Color: render.Grey,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
type testCase struct {
|
||||||
|
name string
|
||||||
|
run func() error
|
||||||
|
}
|
||||||
|
tests := []testCase{
|
||||||
|
testCase{
|
||||||
|
name: "Access a pixel on the blank map and expect an error",
|
||||||
|
run: func() error {
|
||||||
|
p := render.NewPoint(65535, -214564545)
|
||||||
|
_, err := c.Get(p)
|
||||||
|
if err == nil {
|
||||||
|
return fmt.Errorf("unexpected success getting point %s", p)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
testCase{
|
||||||
|
name: "Set a pixel",
|
||||||
|
run: func() error {
|
||||||
|
// Set a point.
|
||||||
|
p := render.NewPoint(100, 200)
|
||||||
|
err := c.Set(p, grey)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unexpected error getting point %s: %s", p, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
testCase{
|
||||||
|
name: "Verify the set pixel",
|
||||||
|
run: func() error {
|
||||||
|
p := render.NewPoint(100, 200)
|
||||||
|
px, err := c.Get(p)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if px != grey {
|
||||||
|
return fmt.Errorf("pixel at %s not the expected color:\n"+
|
||||||
|
"Expected: %s\n"+
|
||||||
|
" Got: %s",
|
||||||
|
p,
|
||||||
|
grey,
|
||||||
|
px,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
testCase{
|
||||||
|
name: "Verify the neighboring pixel is unset",
|
||||||
|
run: func() error {
|
||||||
|
p := render.NewPoint(101, 200)
|
||||||
|
_, err := c.Get(p)
|
||||||
|
if err == nil {
|
||||||
|
return fmt.Errorf("unexpected success getting point %s", p)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
testCase{
|
||||||
|
name: "Delete the set pixel",
|
||||||
|
run: func() error {
|
||||||
|
p := render.NewPoint(100, 200)
|
||||||
|
err := c.Delete(p)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
testCase{
|
||||||
|
name: "Verify the deleted pixel is unset",
|
||||||
|
run: func() error {
|
||||||
|
p := render.NewPoint(100, 200)
|
||||||
|
_, err := c.Get(p)
|
||||||
|
if err == nil {
|
||||||
|
return fmt.Errorf("unexpected success getting point %s", p)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
testCase{
|
||||||
|
name: "Delete a pixel that didn't exist",
|
||||||
|
run: func() error {
|
||||||
|
p := render.NewPoint(-100, -100)
|
||||||
|
err := c.Delete(p)
|
||||||
|
if err == nil {
|
||||||
|
return fmt.Errorf("unexpected success deleting point %s", p)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
if err := test.run(); err != nil {
|
||||||
|
t.Errorf("Failed: %s\n%s", test.name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test the map chunk accessor.
|
||||||
|
func TestMapAccessor(t *testing.T) {
|
||||||
|
a := level.NewMapAccessor()
|
||||||
|
_ = a
|
||||||
|
|
||||||
|
// Test action types
|
||||||
|
var (
|
||||||
|
Get = "Get"
|
||||||
|
Set = "Set"
|
||||||
|
Delete = "Delete"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Test swatches.
|
||||||
|
var (
|
||||||
|
red = &level.Swatch{
|
||||||
|
Name: "fire",
|
||||||
|
Color: render.Red,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
type testCase struct {
|
||||||
|
Action string
|
||||||
|
P render.Point
|
||||||
|
S *level.Swatch
|
||||||
|
Expect *level.Swatch
|
||||||
|
Err bool // expect error
|
||||||
|
}
|
||||||
|
tests := []testCase{
|
||||||
|
// Get a random point and expect to fail.
|
||||||
|
testCase{
|
||||||
|
Action: Get,
|
||||||
|
P: render.NewPoint(128, 128),
|
||||||
|
Err: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Set a point.
|
||||||
|
testCase{
|
||||||
|
Action: Set,
|
||||||
|
S: red,
|
||||||
|
P: render.NewPoint(1024, 2048),
|
||||||
|
},
|
||||||
|
|
||||||
|
// Verify it exists.
|
||||||
|
testCase{
|
||||||
|
Action: Get,
|
||||||
|
P: render.NewPoint(1024, 2048),
|
||||||
|
Expect: red,
|
||||||
|
},
|
||||||
|
|
||||||
|
// A neighboring point does not exist.
|
||||||
|
testCase{
|
||||||
|
Action: Get,
|
||||||
|
P: render.NewPoint(1025, 2050),
|
||||||
|
Err: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Delete a pixel that doesn't exist.
|
||||||
|
testCase{
|
||||||
|
Action: Delete,
|
||||||
|
P: render.NewPoint(1987, 2006),
|
||||||
|
Err: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Delete one that does.
|
||||||
|
testCase{
|
||||||
|
Action: Delete,
|
||||||
|
P: render.NewPoint(1024, 2048),
|
||||||
|
},
|
||||||
|
|
||||||
|
// Verify gone
|
||||||
|
testCase{
|
||||||
|
Action: Get,
|
||||||
|
P: render.NewPoint(1024, 2048),
|
||||||
|
Err: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
var px *level.Swatch
|
||||||
|
var err error
|
||||||
|
switch test.Action {
|
||||||
|
case Get:
|
||||||
|
px, err = a.Get(test.P)
|
||||||
|
case Set:
|
||||||
|
err = a.Set(test.P, test.S)
|
||||||
|
case Delete:
|
||||||
|
err = a.Delete(test.P)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil && !test.Err {
|
||||||
|
t.Errorf("unexpected error from %s %s: %s", test.Action, test.P, err)
|
||||||
|
continue
|
||||||
|
} else if err == nil && test.Err {
|
||||||
|
t.Errorf("didn't get error when we expected from %s %s", test.Action, test.P)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if test.Action == Get {
|
||||||
|
if px != test.Expect {
|
||||||
|
t.Errorf("didn't get expected result\n"+
|
||||||
|
"Expected: %s\n"+
|
||||||
|
" Got: %s\n",
|
||||||
|
test.Expect,
|
||||||
|
px,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test the ChunkCoordinate function.
|
||||||
|
func TestChunkCoordinates(t *testing.T) {
|
||||||
|
c := level.NewChunker(1000)
|
||||||
|
|
||||||
|
type testCase struct {
|
||||||
|
In render.Point
|
||||||
|
Expect render.Point
|
||||||
|
}
|
||||||
|
tests := []testCase{
|
||||||
|
testCase{
|
||||||
|
In: render.NewPoint(0, 0),
|
||||||
|
Expect: render.NewPoint(0, 0),
|
||||||
|
},
|
||||||
|
testCase{
|
||||||
|
In: render.NewPoint(128, 128),
|
||||||
|
Expect: render.NewPoint(0, 0),
|
||||||
|
},
|
||||||
|
testCase{
|
||||||
|
In: render.NewPoint(1024, 128),
|
||||||
|
Expect: render.NewPoint(1, 0),
|
||||||
|
},
|
||||||
|
testCase{
|
||||||
|
In: render.NewPoint(3600, 1228),
|
||||||
|
Expect: render.NewPoint(3, 1),
|
||||||
|
},
|
||||||
|
testCase{
|
||||||
|
In: render.NewPoint(-100, -1),
|
||||||
|
Expect: render.NewPoint(-1, -1),
|
||||||
|
},
|
||||||
|
testCase{
|
||||||
|
In: render.NewPoint(-950, 100),
|
||||||
|
Expect: render.NewPoint(-1, 0),
|
||||||
|
},
|
||||||
|
testCase{
|
||||||
|
In: render.NewPoint(-1001, -856),
|
||||||
|
Expect: render.NewPoint(-2, -1),
|
||||||
|
},
|
||||||
|
testCase{
|
||||||
|
In: render.NewPoint(-3600, -4800),
|
||||||
|
Expect: render.NewPoint(-4, -5),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
actual := c.ChunkCoordinate(test.In)
|
||||||
|
if actual != test.Expect {
|
||||||
|
t.Errorf(
|
||||||
|
"Failed ChunkCoordinate conversion:\n"+
|
||||||
|
" Input: %s\n"+
|
||||||
|
"Expected: %s\n"+
|
||||||
|
" Got: %s",
|
||||||
|
test.In,
|
||||||
|
test.Expect,
|
||||||
|
actual,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestZeroChunkSize(t *testing.T) {
|
||||||
|
c := &level.Chunker{}
|
||||||
|
|
||||||
|
coord := c.ChunkCoordinate(render.NewPoint(1200, 3600))
|
||||||
|
if !coord.IsZero() {
|
||||||
|
t.Errorf("ChunkCoordinate didn't fail with a zero chunk size!")
|
||||||
|
}
|
||||||
|
}
|
145
level/chunker.go
Normal file
145
level/chunker.go
Normal file
|
@ -0,0 +1,145 @@
|
||||||
|
package level
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"git.kirsle.net/apps/doodle/render"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Chunker is the data structure that manages the chunks of a level, and
|
||||||
|
// provides the API to interact with the pixels using their absolute coordinates
|
||||||
|
// while abstracting away the underlying details.
|
||||||
|
type Chunker struct {
|
||||||
|
Size int `json:"size"`
|
||||||
|
Chunks ChunkMap `json:"chunks"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewChunker creates a new chunk manager with a given chunk size.
|
||||||
|
func NewChunker(size int) *Chunker {
|
||||||
|
return &Chunker{
|
||||||
|
Size: size,
|
||||||
|
Chunks: ChunkMap{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inflate iterates over the pixels in the (loaded) chunks and expands any
|
||||||
|
// Sparse Swatches (which have only their palette index, from the file format
|
||||||
|
// on disk) to connect references to the swatches in the palette.
|
||||||
|
func (c *Chunker) Inflate(pal *Palette) error {
|
||||||
|
for coord, chunk := range c.Chunks {
|
||||||
|
log.Debug("Chunker.Inflate: expanding chunk %s %+v", coord, chunk)
|
||||||
|
chunk.Inflate(pal)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IterViewport returns a channel to iterate every point that exists within
|
||||||
|
// the viewport rect.
|
||||||
|
func (c *Chunker) IterViewport(viewport render.Rect) <-chan Pixel {
|
||||||
|
pipe := make(chan Pixel)
|
||||||
|
go func() {
|
||||||
|
// Get the chunk box coordinates.
|
||||||
|
var (
|
||||||
|
topLeft = c.ChunkCoordinate(render.NewPoint(viewport.X, viewport.Y))
|
||||||
|
bottomRight = c.ChunkCoordinate(render.Point{
|
||||||
|
X: viewport.X + viewport.W,
|
||||||
|
Y: viewport.Y + viewport.H,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
for cx := topLeft.X; cx <= bottomRight.X; cx++ {
|
||||||
|
for cy := topLeft.Y; cy <= bottomRight.Y; cy++ {
|
||||||
|
if chunk, ok := c.GetChunk(render.NewPoint(cx, cy)); ok {
|
||||||
|
for px := range chunk.Iter() {
|
||||||
|
pipe <- px
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
close(pipe)
|
||||||
|
}()
|
||||||
|
return pipe
|
||||||
|
}
|
||||||
|
|
||||||
|
// IterPixels returns a channel to iterate over every pixel in the entire
|
||||||
|
// chunker.
|
||||||
|
func (c *Chunker) IterPixels() <-chan Pixel {
|
||||||
|
pipe := make(chan Pixel)
|
||||||
|
go func() {
|
||||||
|
for _, chunk := range c.Chunks {
|
||||||
|
for px := range chunk.Iter() {
|
||||||
|
pipe <- px
|
||||||
|
}
|
||||||
|
}
|
||||||
|
close(pipe)
|
||||||
|
}()
|
||||||
|
return pipe
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetChunk gets a chunk at a certain position. Returns false if not found.
|
||||||
|
func (c *Chunker) GetChunk(p render.Point) (*Chunk, bool) {
|
||||||
|
chunk, ok := c.Chunks[p]
|
||||||
|
return chunk, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a pixel at the given coordinate. Returns the Palette entry for that
|
||||||
|
// pixel or else returns an error if not found.
|
||||||
|
func (c *Chunker) Get(p render.Point) (*Swatch, error) {
|
||||||
|
// Compute the chunk coordinate.
|
||||||
|
coord := c.ChunkCoordinate(p)
|
||||||
|
if chunk, ok := c.Chunks[coord]; ok {
|
||||||
|
return chunk.Get(p)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("no chunk %s exists for point %s", coord, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set a pixel at the given coordinate.
|
||||||
|
func (c *Chunker) Set(p render.Point, sw *Swatch) error {
|
||||||
|
coord := c.ChunkCoordinate(p)
|
||||||
|
chunk, ok := c.Chunks[coord]
|
||||||
|
if !ok {
|
||||||
|
chunk = NewChunk()
|
||||||
|
c.Chunks[coord] = chunk
|
||||||
|
}
|
||||||
|
|
||||||
|
return chunk.Set(p, sw)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete a pixel at the given coordinate.
|
||||||
|
func (c *Chunker) Delete(p render.Point) error {
|
||||||
|
coord := c.ChunkCoordinate(p)
|
||||||
|
if chunk, ok := c.Chunks[coord]; ok {
|
||||||
|
return chunk.Delete(p)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("no chunk %s exists for point %s", coord, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChunkCoordinate computes a chunk coordinate from an absolute coordinate.
|
||||||
|
func (c *Chunker) ChunkCoordinate(abs render.Point) render.Point {
|
||||||
|
if c.Size == 0 {
|
||||||
|
return render.Point{}
|
||||||
|
}
|
||||||
|
|
||||||
|
size := float64(c.Size)
|
||||||
|
return render.NewPoint(
|
||||||
|
int32(math.Floor(float64(abs.X)/size)),
|
||||||
|
int32(math.Floor(float64(abs.Y)/size)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChunkMap maps a chunk coordinate to its chunk data.
|
||||||
|
type ChunkMap map[render.Point]*Chunk
|
||||||
|
|
||||||
|
// MarshalJSON to convert the chunk map to JSON. This is needed for writing so
|
||||||
|
// the JSON encoder knows how to serializes a `map[Point]*Chunk` but the inverse
|
||||||
|
// is not necessary to implement.
|
||||||
|
func (c ChunkMap) MarshalJSON() ([]byte, error) {
|
||||||
|
dict := map[string]*Chunk{}
|
||||||
|
for point, chunk := range c {
|
||||||
|
dict[point.String()] = chunk
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := json.Marshal(dict)
|
||||||
|
return out, err
|
||||||
|
}
|
|
@ -16,6 +16,19 @@ func (m *Level) ToJSON() ([]byte, error) {
|
||||||
return out.Bytes(), err
|
return out.Bytes(), err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WriteJSON writes a level to JSON on disk.
|
||||||
|
func (m *Level) WriteJSON(filename string) error {
|
||||||
|
fh, err := os.Create(filename)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Level.WriteJSON(%s): failed to create file: %s", filename, err)
|
||||||
|
}
|
||||||
|
defer fh.Close()
|
||||||
|
|
||||||
|
_ = fh
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// LoadJSON loads a map from JSON file.
|
// LoadJSON loads a map from JSON file.
|
||||||
func LoadJSON(filename string) (*Level, error) {
|
func LoadJSON(filename string) (*Level, error) {
|
||||||
fh, err := os.Open(filename)
|
fh, err := os.Open(filename)
|
||||||
|
@ -24,13 +37,17 @@ func LoadJSON(filename string) (*Level, error) {
|
||||||
}
|
}
|
||||||
defer fh.Close()
|
defer fh.Close()
|
||||||
|
|
||||||
|
// Decode the JSON file from disk.
|
||||||
m := New()
|
m := New()
|
||||||
decoder := json.NewDecoder(fh)
|
decoder := json.NewDecoder(fh)
|
||||||
err = decoder.Decode(&m)
|
err = decoder.Decode(&m)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return m, err
|
return m, fmt.Errorf("level.LoadJSON: JSON decode error: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Inflate the chunk metadata to map the pixels to their palette indexes.
|
||||||
|
m.Chunker.Inflate(m.Palette)
|
||||||
|
|
||||||
// Inflate the private instance values.
|
// Inflate the private instance values.
|
||||||
m.Palette.Inflate()
|
m.Palette.Inflate()
|
||||||
for _, px := range m.Pixels {
|
for _, px := range m.Pixels {
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
package level
|
package level
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"git.kirsle.net/apps/doodle/render"
|
"git.kirsle.net/apps/doodle/render"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -50,30 +48,6 @@ type Palette struct {
|
||||||
byName map[string]int // Cache map of swatches by name
|
byName map[string]int // Cache map of swatches by name
|
||||||
}
|
}
|
||||||
|
|
||||||
// Swatch holds details about a single value in the palette.
|
|
||||||
type Swatch struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Color render.Color `json:"color"`
|
|
||||||
|
|
||||||
// Optional attributes.
|
|
||||||
Solid bool `json:"solid,omitempty"`
|
|
||||||
Fire bool `json:"fire,omitempty"`
|
|
||||||
Water bool `json:"water,omitempty"`
|
|
||||||
|
|
||||||
// Private runtime attributes.
|
|
||||||
index int // position in the Palette, for reverse of `Palette.byName`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s Swatch) String() string {
|
|
||||||
return s.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
// Index returns the Swatch's position in the palette.
|
|
||||||
func (s Swatch) Index() int {
|
|
||||||
fmt.Printf("%+v index: %d", s, s.index)
|
|
||||||
return s.index
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inflate the palette swatch caches. Always call this method after you have
|
// Inflate the palette swatch caches. Always call this method after you have
|
||||||
// initialized the palette (i.e. loaded it from JSON); this will update the
|
// initialized the palette (i.e. loaded it from JSON); this will update the
|
||||||
// "color by name" cache and assign the index numbers to each swatch.
|
// "color by name" cache and assign the index numbers to each swatch.
|
||||||
|
|
56
level/swatch.go
Normal file
56
level/swatch.go
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
package level
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"git.kirsle.net/apps/doodle/render"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Swatch holds details about a single value in the palette.
|
||||||
|
type Swatch struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Color render.Color `json:"color"`
|
||||||
|
|
||||||
|
// Optional attributes.
|
||||||
|
Solid bool `json:"solid,omitempty"`
|
||||||
|
Fire bool `json:"fire,omitempty"`
|
||||||
|
Water bool `json:"water,omitempty"`
|
||||||
|
|
||||||
|
// Private runtime attributes.
|
||||||
|
index int // position in the Palette, for reverse of `Palette.byName`
|
||||||
|
|
||||||
|
// When the swatch is loaded from JSON we only get the index number, and
|
||||||
|
// need to expand out the swatch later when the palette is loaded.
|
||||||
|
paletteIndex int
|
||||||
|
isSparse bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSparseSwatch creates a sparse Swatch from a palette index that will need
|
||||||
|
// later expansion, when loading drawings from disk.
|
||||||
|
func NewSparseSwatch(paletteIndex int) *Swatch {
|
||||||
|
return &Swatch{
|
||||||
|
isSparse: true,
|
||||||
|
paletteIndex: paletteIndex,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Swatch) String() string {
|
||||||
|
if s.isSparse {
|
||||||
|
return fmt.Sprintf("Swatch<sparse:%d>", s.paletteIndex)
|
||||||
|
}
|
||||||
|
if s.Name == "" {
|
||||||
|
return s.Color.String()
|
||||||
|
}
|
||||||
|
return s.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsSparse returns whether this Swatch is sparse (has only a palette index) and
|
||||||
|
// requires inflation.
|
||||||
|
func (s *Swatch) IsSparse() bool {
|
||||||
|
return s.isSparse
|
||||||
|
}
|
||||||
|
|
||||||
|
// Index returns the Swatch's position in the palette.
|
||||||
|
func (s *Swatch) Index() int {
|
||||||
|
return s.index
|
||||||
|
}
|
|
@ -3,18 +3,24 @@ package level
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"git.kirsle.net/apps/doodle/balance"
|
||||||
|
"git.kirsle.net/apps/doodle/render"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Level is the container format for Doodle map drawings.
|
// Level is the container format for Doodle map drawings.
|
||||||
type Level struct {
|
type Level struct {
|
||||||
Version int32 `json:"version"` // File format version spec.
|
Version int `json:"version"` // File format version spec.
|
||||||
GameVersion string `json:"gameVersion"` // Game version that created the level.
|
GameVersion string `json:"gameVersion"` // Game version that created the level.
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Author string `json:"author"`
|
Author string `json:"author"`
|
||||||
Password string `json:"passwd"`
|
Password string `json:"passwd"`
|
||||||
Locked bool `json:"locked"`
|
Locked bool `json:"locked"`
|
||||||
|
|
||||||
// Level size.
|
// Chunked pixel data.
|
||||||
|
Chunker *Chunker `json:"chunks"`
|
||||||
|
|
||||||
|
// XXX: deprecated?
|
||||||
Width int32 `json:"w"`
|
Width int32 `json:"w"`
|
||||||
Height int32 `json:"h"`
|
Height int32 `json:"h"`
|
||||||
|
|
||||||
|
@ -31,6 +37,7 @@ type Level struct {
|
||||||
func New() *Level {
|
func New() *Level {
|
||||||
return &Level{
|
return &Level{
|
||||||
Version: 1,
|
Version: 1,
|
||||||
|
Chunker: NewChunker(balance.ChunkSize),
|
||||||
Pixels: []*Pixel{},
|
Pixels: []*Pixel{},
|
||||||
Palette: &Palette{},
|
Palette: &Palette{},
|
||||||
}
|
}
|
||||||
|
@ -51,6 +58,14 @@ func (p Pixel) String() string {
|
||||||
return fmt.Sprintf("Pixel<%s '%s' (%d,%d)>", p.Swatch.Color, p.Swatch.Name, p.X, p.Y)
|
return fmt.Sprintf("Pixel<%s '%s' (%d,%d)>", p.Swatch.Color, p.Swatch.Name, p.X, p.Y)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Point returns the pixel's point.
|
||||||
|
func (p Pixel) Point() render.Point {
|
||||||
|
return render.Point{
|
||||||
|
X: p.X,
|
||||||
|
Y: p.Y,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MarshalJSON serializes a Pixel compactly as a simple list.
|
// MarshalJSON serializes a Pixel compactly as a simple list.
|
||||||
func (p Pixel) MarshalJSON() ([]byte, error) {
|
func (p Pixel) MarshalJSON() ([]byte, error) {
|
||||||
return []byte(fmt.Sprintf(
|
return []byte(fmt.Sprintf(
|
||||||
|
|
1
log.go
1
log.go
|
@ -10,5 +10,6 @@ func init() {
|
||||||
Level: golog.DebugLevel,
|
Level: golog.DebugLevel,
|
||||||
Theme: golog.DarkTheme,
|
Theme: golog.DarkTheme,
|
||||||
Colors: golog.ExtendedColor,
|
Colors: golog.ExtendedColor,
|
||||||
|
TimeFormat: "2006-01-02 15:04:05.000000",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,7 +63,7 @@ func (s *PlayScene) Loop(d *Doodle, ev *events.State) error {
|
||||||
if ev.KeyName.Read() == "e" {
|
if ev.KeyName.Read() == "e" {
|
||||||
log.Info("Edit Mode, Go!")
|
log.Info("Edit Mode, Go!")
|
||||||
d.Goto(&EditorScene{
|
d.Goto(&EditorScene{
|
||||||
Canvas: s.canvas,
|
// Canvas: s.canvas,
|
||||||
})
|
})
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,46 +38,6 @@ type Engine interface {
|
||||||
Loop() error // maybe?
|
Loop() error // maybe?
|
||||||
}
|
}
|
||||||
|
|
||||||
// Point holds an X,Y coordinate value.
|
|
||||||
type Point struct {
|
|
||||||
X int32
|
|
||||||
Y int32
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewPoint makes a new Point at an X,Y coordinate.
|
|
||||||
func NewPoint(x, y int32) Point {
|
|
||||||
return Point{
|
|
||||||
X: x,
|
|
||||||
Y: y,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p Point) String() string {
|
|
||||||
return fmt.Sprintf("Point<%d,%d>", p.X, p.Y)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsZero returns if the point is the zero value.
|
|
||||||
func (p Point) IsZero() bool {
|
|
||||||
return p.X == 0 && p.Y == 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inside returns whether the Point falls inside the rect.
|
|
||||||
func (p Point) Inside(r Rect) bool {
|
|
||||||
var (
|
|
||||||
x1 = r.X
|
|
||||||
y1 = r.Y
|
|
||||||
x2 = r.X + r.W
|
|
||||||
y2 = r.Y + r.H
|
|
||||||
)
|
|
||||||
return p.X >= x1 && p.X <= x2 && p.Y >= y1 && p.Y <= y2
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add (or subtract) the other point to your current point.
|
|
||||||
func (p *Point) Add(other Point) {
|
|
||||||
p.X += other.X
|
|
||||||
p.Y += other.Y
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rect has a coordinate and a width and height.
|
// Rect has a coordinate and a width and height.
|
||||||
type Rect struct {
|
type Rect struct {
|
||||||
X int32
|
X int32
|
||||||
|
|
94
render/point.go
Normal file
94
render/point.go
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
package render
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Point holds an X,Y coordinate value.
|
||||||
|
type Point struct {
|
||||||
|
X int32
|
||||||
|
Y int32
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPoint makes a new Point at an X,Y coordinate.
|
||||||
|
func NewPoint(x, y int32) Point {
|
||||||
|
return Point{
|
||||||
|
X: x,
|
||||||
|
Y: y,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Point) String() string {
|
||||||
|
return fmt.Sprintf("%d,%d", p.X, p.Y)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParsePoint to parse a point from its string representation.
|
||||||
|
func ParsePoint(v string) (Point, error) {
|
||||||
|
halves := strings.Split(v, ",")
|
||||||
|
if len(halves) != 2 {
|
||||||
|
return Point{}, fmt.Errorf("'%s': not a valid coordinate string", v)
|
||||||
|
}
|
||||||
|
x, errX := strconv.Atoi(halves[0])
|
||||||
|
y, errY := strconv.Atoi(halves[1])
|
||||||
|
if errX != nil || errY != nil {
|
||||||
|
return Point{}, fmt.Errorf("invalid coordinate string (X: %v; Y: %v)",
|
||||||
|
errX,
|
||||||
|
errY,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return Point{
|
||||||
|
X: int32(x),
|
||||||
|
Y: int32(y),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsZero returns if the point is the zero value.
|
||||||
|
func (p Point) IsZero() bool {
|
||||||
|
return p.X == 0 && p.Y == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inside returns whether the Point falls inside the rect.
|
||||||
|
func (p Point) Inside(r Rect) bool {
|
||||||
|
var (
|
||||||
|
x1 = r.X
|
||||||
|
y1 = r.Y
|
||||||
|
x2 = r.X + r.W
|
||||||
|
y2 = r.Y + r.H
|
||||||
|
)
|
||||||
|
return p.X >= x1 && p.X <= x2 && p.Y >= y1 && p.Y <= y2
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add (or subtract) the other point to your current point.
|
||||||
|
func (p *Point) Add(other Point) {
|
||||||
|
p.X += other.X
|
||||||
|
p.Y += other.Y
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalText to convert the point into text so that a render.Point may be used
|
||||||
|
// as a map key and serialized to JSON.
|
||||||
|
func (p *Point) MarshalText() ([]byte, error) {
|
||||||
|
return []byte(fmt.Sprintf("%d,%d", p.X, p.Y)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalText to restore it from text.
|
||||||
|
func (p *Point) UnmarshalText(b []byte) error {
|
||||||
|
halves := strings.Split(strings.Trim(string(b), `"`), ",")
|
||||||
|
if len(halves) != 2 {
|
||||||
|
return fmt.Errorf("'%s': not a valid coordinate string", b)
|
||||||
|
}
|
||||||
|
|
||||||
|
x, errX := strconv.Atoi(halves[0])
|
||||||
|
y, errY := strconv.Atoi(halves[1])
|
||||||
|
if errX != nil || errY != nil {
|
||||||
|
return fmt.Errorf("Point.UnmarshalJSON: Atoi errors (X=%s Y=%s)",
|
||||||
|
errX,
|
||||||
|
errY,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
p.X = int32(x)
|
||||||
|
p.Y = int32(y)
|
||||||
|
return nil
|
||||||
|
}
|
1
shell.go
1
shell.go
|
@ -13,6 +13,7 @@ import (
|
||||||
|
|
||||||
// Flash a message to the user.
|
// Flash a message to the user.
|
||||||
func (d *Doodle) Flash(template string, v ...interface{}) {
|
func (d *Doodle) Flash(template string, v ...interface{}) {
|
||||||
|
log.Warn(template, v...)
|
||||||
d.shell.Write(fmt.Sprintf(template, v...))
|
d.shell.Write(fmt.Sprintf(template, v...))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user