Skip to content

Commit

Permalink
Finally figured out how I want the packer to operate
Browse files Browse the repository at this point in the history
  • Loading branch information
Allen Ray committed Jul 30, 2021
1 parent 6a33149 commit 98421a5
Show file tree
Hide file tree
Showing 3 changed files with 352 additions and 4 deletions.
205 changes: 205 additions & 0 deletions packer/base.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
package packer

import (
"errors"
"sort"

"github.com/faiface/pixel"
)

type CreateFlags uint8
type InsertFlags uint8

var (
ErrorNoEmptySpace = errors.New("Couldn't find an empty space")
ErrorNoSpaceAfterGrowth = errors.New("Failed to find space after growth")
ErrorSplitFailed = errors.New("Split failed")
)

// NewPacker-time flags
const (
BaseAllowGrowth CreateFlags = 1 << iota // Should the packer space try to grow larger to fit oversized images
BaseDebugDraw // Show the lines of the empty spaces when drawing the texture as an entire sprite
)

// Insert-time flags
const (
BaseInsertFlipped InsertFlags = 1 << iota // Flip the sprite upside-down on insert
)

type Base struct {
bounds pixel.Rect
emptySpaces spaceList
pic *pixel.PictureData
flags CreateFlags
images map[int]pixel.Rect
dirty bool
batch *pixel.Batch
}

func NewBase(width, height int, flags CreateFlags) (base *Base) {
bounds := pixel.R(0, 0, float64(width), float64(height))
base = &Base{
bounds: bounds,
flags: flags,
pic: pixel.MakePictureData(bounds),
emptySpaces: spaceList{bounds},
images: make(map[int]pixel.Rect),
}
base.batch = pixel.NewBatch(&pixel.TrianglesData{}, base.pic)

return
}

func (pack Base) find(bounds pixel.Rect) (int, bool) {
for i, space := range pack.emptySpaces {
if bounds.W() <= space.W() && bounds.H() <= space.H() {
return i, true
}
}
return -1, false
}

func (pack Base) spriteFrom(id int) *pixel.Sprite {
image := pack.images[id]
return pixel.NewSprite(pack.pic, image)
}

func (pack *Base) grow(size pixel.Vec) (err error) {
sprites := pack.extract()
pack.bounds = rect(0, 0, pack.bounds.W()+size.X, pack.bounds.H()+size.Y)
return pack.fill(sprites)
}

func (pack *Base) remove(i int) (removed pixel.Rect) {
removed = pack.emptySpaces[i]
pack.emptySpaces = append(pack.emptySpaces[:i], pack.emptySpaces[i+1:]...)
return
}

func (pack *Base) insert(bounds pixel.Rect, id int) (r pixel.Rect, err error) {
var (
s *createdSplits
)
candidateIndex, found := pack.find(bounds)

if !found {
if pack.flags&BaseAllowGrowth == 0 {
return pixel.ZR, ErrorNoEmptySpace
}

if err = pack.grow(bounds.Size()); err != nil {
return
}

candidateIndex, found = pack.find(bounds)
if !found {
return pixel.ZR, ErrorNoSpaceAfterGrowth
}
}

space := pack.remove(candidateIndex)
if s, err = split(bounds, space); err != nil {
return pixel.ZR, err
}

if s.count == 2 {
pack.emptySpaces = append(pack.emptySpaces, s.bigger)
}
pack.emptySpaces = append(pack.emptySpaces, s.smaller)

sort.Sort(pack.emptySpaces)

pack.images[id] = rect(space.Min.X, space.Min.Y, bounds.W(), bounds.H())

return pack.images[id], nil
}

func (pack *Base) makeDirty() {
pack.dirty = true
}

func (pack *Base) Insert(id int, image *pixel.Sprite) error {
return pack.InsertV(id, image, 0)
}

func (pack *Base) InsertV(id int, image *pixel.Sprite, flags InsertFlags) error {
pic := image.Picture().(*pixel.PictureData)

return pack.InsertPictureDataV(id, pic, flags)
}

func (pack *Base) InsertPictureData(id int, pic *pixel.PictureData) error {
return pack.InsertPictureDataV(id, pic, 0)
}

func (pack *Base) InsertPictureDataV(id int, pic *pixel.PictureData, flags InsertFlags) (err error) {
var (
bounds = pic.Bounds()
space pixel.Rect
)

if space, err = pack.insert(bounds, id); err != nil {
return
}

for y := 0; y < int(bounds.H()); y++ {
for x := 0; x < int(bounds.W()); x++ {
i := pack.pic.Index(pixel.V(space.Min.X+float64(x), space.Min.Y+float64(y)))
var ii int
if flags&BaseInsertFlipped != 0 {
ii = pic.Index(pixel.V(float64(x), (bounds.H()-1)-float64(y)))
} else {
ii = pic.Index(pixel.V(float64(x), float64(y)))
}
pack.pic.Pix[i] = pic.Pix[ii]
}
}

pack.makeDirty()

return
}

func (pack Base) extract() (sprites spriteList) {
sprites = make(spriteList, 0)
for id := range pack.images {
sprite := pack.spriteFrom(id)
sprites = append(sprites, idSprite{id: id, sprite: sprite})
}
sort.Sort(sprites)
return
}

func (pack *Base) fill(sprites spriteList) (err error) {
pack.emptySpaces = pack.emptySpaces[:0]
pack.emptySpaces = append(pack.emptySpaces, pack.bounds)
pack.pic = pixel.MakePictureData(pack.bounds)
pack.batch = pixel.NewBatch(&pixel.TrianglesData{}, pack.pic)
pack.makeDirty()

for _, cont := range sprites {
if err = pack.Insert(cont.id, cont.sprite); err != nil {
return
}
}
return
}

func (pack *Base) Optimize() (err error) {
return pack.fill(pack.extract())
}

func (pack *Base) Draw(id int, m pixel.Matrix) {
sprite := pack.spriteFrom(id)

sprite.Draw(pack.batch, m)
}

func (pack *Base) DrawTo(target pixel.Target) {
pack.batch.Draw(target)
}

func (pack *Base) Clear() {
pack.batch.Clear()
}
45 changes: 41 additions & 4 deletions packer/data.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,16 @@ func (s spriteList) Less(i, j int) bool {
return s[i].sprite.Picture().Bounds().Area() < s[j].sprite.Picture().Bounds().Area()
}

type picDataList []struct {
id int
pic *pixel.PictureData
}

// container for the leftover space after split
type createdSplits struct {
count int
smaller, bigger pixel.Rect
hasSmall, hasBig bool
count int
smaller, bigger pixel.Rect
}

// helper if the split was invalid
Expand All @@ -40,13 +46,44 @@ func splitsFailed() *createdSplits {
// adds the given leftover spaces to this container
func splits(rects ...pixel.Rect) (s *createdSplits) {
s = &createdSplits{
count: len(rects),
smaller: rects[0],
count: len(rects),
hasSmall: true,
smaller: rects[0],
}

if s.count == 2 {
s.hasBig = true
s.bigger = rects[1]
}

return
}

func split(image, space pixel.Rect) (s *createdSplits, err error) {
w := space.W() - image.W()
h := space.H() - image.H()

if w < 0 || h < 0 {
return nil, ErrorSplitFailed
} else if w == 0 && h == 0 {
// perfectly fit case
return &createdSplits{}, nil
} else if w > 0 && h == 0 {
r := rect(space.Min.X+image.W(), space.Min.Y, w, image.H())
return splits(r), nil
} else if w == 0 && h > 0 {
r := rect(space.Min.X, space.Min.Y+image.H(), image.W(), h)
return splits(r), nil
}

var smaller, larger pixel.Rect
if w > h {
smaller = rect(space.Min.X, space.Min.Y+image.H(), image.W(), h)
larger = rect(space.Min.X+image.W(), space.Min.Y, w, space.H())
} else {
smaller = rect(space.Min.X+image.W(), space.Min.Y, w, image.H())
larger = rect(space.Min.X, space.Min.Y+image.H(), space.W(), h)
}

return splits(smaller, larger), nil
}
106 changes: 106 additions & 0 deletions packer/new.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package packer

import (
"sort"

"github.com/dusk125/goutil/logger"
"github.com/faiface/pixel"
)

type PackFlags uint8

const (
PackInsertFlipped PackFlags = 1 << iota
)

type New struct {
bounds pixel.Rect
emptySpaces spaceList
queued picDataList
images map[int]pixel.Rect
flags CreateFlags
batch *pixel.Batch
}

func NewNew(width, height int, flags CreateFlags) (pack *New) {
bounds := pixel.R(0, 0, float64(width), float64(height))
pack = &New{
bounds: bounds,
flags: flags,
emptySpaces: spaceList{bounds},
images: make(map[int]pixel.Rect),
queued: make(picDataList, 0),
}
return
}

func (pack *New) InsertPictureData(id int, pic *pixel.PictureData) (err error) {
pack.queued = append(pack.queued, struct {
id int
pic *pixel.PictureData
}{id: id, pic: pic})
return
}

func (pack New) find(bounds pixel.Rect) (index int, found bool) {
for i, space := range pack.emptySpaces {
if bounds.W() <= space.W() && bounds.H() <= space.H() {
return i, true
}
}
return
}

func (pack *New) remove(i int) (removed pixel.Rect) {
removed = pack.emptySpaces[i]
pack.emptySpaces = append(pack.emptySpaces[:i], pack.emptySpaces[i+1:]...)
return
}

func (pack *New) Pack() (err error) {
sort.Slice(pack.queued, func(i, j int) bool {
return pack.queued[i].pic.Bounds().Area() > pack.queued[j].pic.Bounds().Area()
})

for _, data := range pack.queued {
var (
s *createdSplits
bounds = data.pic.Bounds()
index, found = pack.find(bounds)
)

if !found {
if pack.flags&BaseAllowGrowth == 0 {
return ErrorNoEmptySpace
}

pack.bounds = rect(0, 0, pack.bounds.W()+bounds.Size().X, pack.bounds.H()+bounds.Size().Y)
index, found = pack.find(bounds)
if !found {
return ErrorNoSpaceAfterGrowth
}
}

space := pack.remove(index)
if s, err = split(bounds, space); err != nil {
return err
}

if s.hasBig {
pack.emptySpaces = append(pack.emptySpaces, s.bigger)
}
if s.hasSmall {
pack.emptySpaces = append(pack.emptySpaces, s.smaller)
}

sort.Slice(pack.emptySpaces, func(i, j int) bool {
return pack.emptySpaces[i].Area() < pack.emptySpaces[j].Area()
})

pack.images[data.id] = rect(space.Min.X, space.Min.Y, bounds.W(), bounds.H())
}

logger.Debug("Final bounds:", pack.bounds)

return
}

0 comments on commit 98421a5

Please sign in to comment.