diff --git a/.gitignore b/.gitignore index 3b735ec..2948c0a 100644 --- a/.gitignore +++ b/.gitignore @@ -15,7 +15,9 @@ *.out # Dependency directories (remove the comment below to include it) -# vendor/ +vendor/ # Go workspace file go.work + +.idea/ \ No newline at end of file diff --git a/bench_test.go b/bench_test.go new file mode 100644 index 0000000..f8630e8 --- /dev/null +++ b/bench_test.go @@ -0,0 +1,39 @@ +package boxpacker3_test + +import ( + rand2 "crypto/rand" + "math/big" + "testing" + + "github.com/google/uuid" + + "github.com/bavix/boxpacker3" +) + +func BenchmarkPacker(b *testing.B) { + items := make(boxpacker3.ItemSlice, 0, 100) + + for x := 0; x < 100; x++ { + w, _ := rand2.Int(rand2.Reader, big.NewInt(150)) + l, _ := rand2.Int(rand2.Reader, big.NewInt(150)) + h, _ := rand2.Int(rand2.Reader, big.NewInt(150)) + w2, _ := rand2.Int(rand2.Reader, big.NewInt(100)) + + items = append(items, boxpacker3.NewItem( + uuid.New().String(), + float64(w.Int64()), + float64(l.Int64()), + float64(h.Int64()), + float64(w2.Int64()), + )) + } + + boxes := NewDefaultBoxList() + packer := boxpacker3.NewPacker() + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + _, _ = packer.Pack(boxes, items) + } +} diff --git a/box.go b/box.go new file mode 100644 index 0000000..cbd706b --- /dev/null +++ b/box.go @@ -0,0 +1,114 @@ +package boxpacker3 + +type Box struct { + ID string + Width float64 + Height float64 + Depth float64 + MaxWeight float64 + Volume float64 + Items []*Item + + itemsVolume float64 + itemsWeight float64 +} + +type BoxSlice []*Box + +func (bs BoxSlice) Len() int { + return len(bs) +} + +func (bs BoxSlice) Less(i, j int) bool { + return bs[i].GetVolume() < bs[j].GetVolume() +} + +func (bs BoxSlice) Swap(i, j int) { + bs[i], bs[j] = bs[j], bs[i] +} + +func NewBox(id string, w, h, d, mw float64) *Box { + return &Box{ + ID: id, + Width: w, + Height: h, + Depth: d, + MaxWeight: mw, + Volume: w * h * d, + Items: make([]*Item, 0), + } +} + +func (b *Box) GetID() string { + return b.ID +} + +func (b *Box) GetWidth() float64 { + return b.Width +} + +func (b *Box) GetHeight() float64 { + return b.Height +} + +func (b *Box) GetDepth() float64 { + return b.Depth +} + +func (b *Box) GetVolume() float64 { + return b.Volume +} + +func (b *Box) GetMaxWeight() float64 { + return b.MaxWeight +} + +// PutItem Пытается поместить элемент в опорную точку p коробки b. +func (b *Box) PutItem(item *Item, p Pivot) bool { + fit := false + + if b.itemsVolume+item.GetVolume() > b.GetVolume() { + return false + } + + if b.itemsWeight+item.GetWeight() > b.GetMaxWeight() { + return false + } + + item.Position = p + for rt := RotationTypeWhd; rt <= RotationTypeWdh; rt++ { + item.RotationType = rt + d := item.GetDimension() + + if b.GetWidth() < p[WidthAxis]+d[WidthAxis] || b.GetHeight() < p[HeightAxis]+d[HeightAxis] || b.GetDepth() < p[DepthAxis]+d[DepthAxis] { + continue + } + + fit = true + + for _, ib := range b.Items { + if ib.Intersect(item) { + fit = false + + break + } + } + + if fit { + b.insert(item) + + return fit + } + + break + } + + return fit +} + +func (b *Box) insert(item *Item) { + b.Items = append(b.Items, item) + + b.itemsVolume += item.GetVolume() + b.itemsWeight += item.GetWeight() +} diff --git a/clone.go b/clone.go new file mode 100644 index 0000000..3c3408a --- /dev/null +++ b/clone.go @@ -0,0 +1,16 @@ +package boxpacker3 + +func copyPtr[T any](original *T) *T { + copyOfValue := *original + + return ©OfValue +} + +func copySlicePtr[T any](data []*T) []*T { + result := make([]*T, len(data)) + for i := range data { + result[i] = copyPtr(data[i]) + } + + return result +} diff --git a/clone_test.go b/clone_test.go new file mode 100644 index 0000000..d5686ea --- /dev/null +++ b/clone_test.go @@ -0,0 +1,33 @@ +package boxpacker3 + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestCopyPtr(t *testing.T) { + a := 1 + b := struct{ a int }{} + + require.Same(t, &a, &a) + require.Same(t, &b, &b) + require.NotSame(t, &a, copyPtr(&a)) + require.NotSame(t, &b, copyPtr(&b)) +} + +func TestCopySlicePtr(t *testing.T) { + a := struct{ a int }{} + b := struct{ a int }{} + c := []*struct{ a int }{&a, &b} + + d := make([]*struct{ a int }, len(c)) + copy(d, c) + + e := copySlicePtr(c) + + for i := range d { + require.Same(t, c[i], d[i]) + require.NotSame(t, c[i], e[i]) + } +} diff --git a/costs.go b/costs.go new file mode 100644 index 0000000..de5fed1 --- /dev/null +++ b/costs.go @@ -0,0 +1,25 @@ +package boxpacker3 + +type RotationType int + +const ( + RotationTypeWhd RotationType = iota + RotationTypeHwd + RotationTypeHdw + RotationTypeDhw + RotationTypeDwh + RotationTypeWdh +) + +type Axis int + +const ( + WidthAxis Axis = iota + HeightAxis + DepthAxis +) + +type ( + Pivot [3]float64 + Dimension [3]float64 +) diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..85c0993 --- /dev/null +++ b/go.mod @@ -0,0 +1,14 @@ +module github.com/bavix/boxpacker3 + +go 1.20 + +require ( + github.com/google/uuid v1.3.0 + github.com/stretchr/testify v1.8.4 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..8d8b455 --- /dev/null +++ b/go.sum @@ -0,0 +1,12 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/item.go b/item.go new file mode 100644 index 0000000..22883cf --- /dev/null +++ b/item.go @@ -0,0 +1,109 @@ +package boxpacker3 + +import ( + "math" +) + +type Item struct { + ID string + Width float64 + Height float64 + Depth float64 + Weight float64 + Volume float64 + + RotationType RotationType + Position Pivot +} + +type ItemSlice []*Item + +func (is ItemSlice) Len() int { + return len(is) +} + +func (is ItemSlice) Less(i, j int) bool { + return is[i].GetVolume() < is[j].GetVolume() +} + +func (is ItemSlice) Swap(i, j int) { + is[i], is[j] = is[j], is[i] +} + +func NewItem(id string, w, h, d, wg float64) *Item { + return &Item{ + ID: id, + Width: w, + Height: h, + Depth: d, + Weight: wg, + Volume: w * h * d, + } +} + +func (i *Item) GetID() string { + return i.ID +} + +func (i *Item) GetWidth() float64 { + return i.Width +} + +func (i *Item) GetHeight() float64 { + return i.Height +} + +func (i *Item) GetDepth() float64 { + return i.Depth +} + +func (i *Item) GetVolume() float64 { + return i.Volume +} + +func (i *Item) GetWeight() float64 { + return i.Weight +} + +//nolint:nonamedreturns +func (i *Item) GetDimension() (d Dimension) { + switch i.RotationType { + case RotationTypeWhd: + d = Dimension{i.GetWidth(), i.GetHeight(), i.GetDepth()} + case RotationTypeHwd: + d = Dimension{i.GetHeight(), i.GetWidth(), i.GetDepth()} + case RotationTypeHdw: + d = Dimension{i.GetHeight(), i.GetDepth(), i.GetWidth()} + case RotationTypeDhw: + d = Dimension{i.GetDepth(), i.GetHeight(), i.GetWidth()} + case RotationTypeDwh: + d = Dimension{i.GetDepth(), i.GetWidth(), i.GetHeight()} + case RotationTypeWdh: + d = Dimension{i.GetWidth(), i.GetDepth(), i.GetHeight()} + } + + return +} + +// Intersect Проверяет пересечения между элементом i и элементом it. +func (i *Item) Intersect(it *Item) bool { + d1 := i.GetDimension() + d2 := it.GetDimension() + + return rectIntersect(d1, d2, i, it, WidthAxis, HeightAxis) && + rectIntersect(d1, d2, i, it, HeightAxis, DepthAxis) && + rectIntersect(d1, d2, i, it, WidthAxis, DepthAxis) +} + +// rectIntersect Проверяет пересекаются ли два прямоугольника от осей x и y элементов i1 и i2. +func rectIntersect(d1, d2 Dimension, i1, i2 *Item, x, y Axis) bool { + cx1 := i1.Position[x] + d1[x]/2 //nolint:gomnd + cy1 := i1.Position[y] + d1[y]/2 //nolint:gomnd + cx2 := i2.Position[x] + d2[x]/2 //nolint:gomnd + cy2 := i2.Position[y] + d2[y]/2 //nolint:gomnd + + ix := math.Max(cx1, cx2) - math.Min(cx1, cx2) + iy := math.Max(cy1, cy2) - math.Min(cy1, cy2) + + return ix < (d1[x]+d2[x])/2 && iy < (d1[y]+d2[y])/2 +} diff --git a/packer.go b/packer.go new file mode 100644 index 0000000..9928074 --- /dev/null +++ b/packer.go @@ -0,0 +1,208 @@ +package boxpacker3 + +import ( + "sort" +) + +type Packer struct{} + +type Result struct { + UnfitItems ItemSlice + Boxes BoxSlice +} + +func NewPacker() *Packer { + return &Packer{} +} + +func (p *Packer) Pack(inputBoxes []*Box, inputItems []*Item) (*Result, error) { + boxes := BoxSlice(copySlicePtr(inputBoxes)) + items := ItemSlice(copySlicePtr(inputItems)) + + sort.Sort(boxes) + sort.Sort(items) + + result := &Result{ + UnfitItems: make(ItemSlice, 0, len(items)), + Boxes: p.preferredSort(boxes, items), + } + + maxIter := len(items) + for i := 0; len(items) > 0 && i < maxIter; i++ { + box := p.FindFittedBox(result.Boxes, items[0]) + if box == nil { + result.UnfitItems = append(result.UnfitItems, items[0]) + items = items[1:] + + continue + } + + items = p.packToBox(result.Boxes, box, items) + } + + return result, nil +} + +func (p *Packer) preferredSort(boxes BoxSlice, items ItemSlice) BoxSlice { + volume := 0. + weight := 0. + + for _, item := range items { + volume += item.GetVolume() + weight += item.GetWeight() + } + + preferredBoxes := make(BoxSlice, 0, len(boxes)) + otherBoxes := make(BoxSlice, 0, len(boxes)) + + for _, b := range boxes { + if b.GetVolume() >= volume && b.GetMaxWeight() >= weight { + preferredBoxes = append(preferredBoxes, b) + } else { + otherBoxes = append(otherBoxes, b) + } + } + + return append(preferredBoxes, otherBoxes...) +} + +// packToBox Упаковывает товары в коробку b. Возвращает не упакованные товары. +// +//nolint:cyclop,nonamedreturns,gocognit,funlen +func (p *Packer) packToBox(boxes []*Box, b *Box, items []*Item) (unpacked []*Item) { + if !b.PutItem(items[0], Pivot{0, 0, 0}) { + if b2 := p.getBiggerBoxThan(boxes, b); b2 != nil { + return p.packToBox(boxes, b2, items) + } + + return items + } + + // Упаковываем неупакованные товары. + for _, i := range items[1:] { + var fitted bool + + // Пробуем опорные точки для коробки, которые не пересекаются с существующими товарами в коробке. + for pt := WidthAxis; pt <= DepthAxis && !fitted; pt++ { + for _, ib := range b.Items { + var pv Pivot + + switch pt { + case WidthAxis: + pv = Pivot{ib.Position[WidthAxis] + ib.GetWidth(), ib.Position[HeightAxis], ib.Position[DepthAxis]} + case HeightAxis: + pv = Pivot{ib.Position[WidthAxis], ib.Position[HeightAxis] + ib.GetHeight(), ib.Position[DepthAxis]} + case DepthAxis: + pv = Pivot{ib.Position[WidthAxis], ib.Position[HeightAxis], ib.Position[DepthAxis] + ib.GetDepth()} + } + + if b.PutItem(i, pv) { + fitted = true + + break + } + } + } + + //nolint:nestif + if !fitted { + forRevert := copySlicePtr(b.Items) + itemSlice := copySlicePtr(b.Items) + bkitemsWeight := b.itemsWeight + bkitemsVolume := b.itemsVolume + b.Items = []*Item{} + b.itemsWeight = 0 + b.itemsVolume = 0 + + if b.PutItem(i, Pivot{0, 0, 0}) { + cnt := len(itemSlice) + for k := 0; k < 100 && cnt > 0; k++ { + j := itemSlice[0] + + // Пробуем опорные точки для коробки, которые не пересекаются с существующими товарами в коробке. + for pt := WidthAxis; pt <= DepthAxis && cnt > 0; pt++ { + for _, ib := range b.Items { + if j == ib { + continue + } + + var pv Pivot + + switch pt { + case WidthAxis: + pv = Pivot{ib.Position[WidthAxis] + ib.GetWidth(), ib.Position[HeightAxis], ib.Position[DepthAxis]} + case HeightAxis: + pv = Pivot{ib.Position[WidthAxis], ib.Position[HeightAxis] + ib.GetHeight(), ib.Position[DepthAxis]} + case DepthAxis: + pv = Pivot{ib.Position[WidthAxis], ib.Position[HeightAxis], ib.Position[DepthAxis] + ib.GetDepth()} + } + + if b.PutItem(j, pv) { + itemSlice = itemSlice[1:] + cnt-- + + break + } + } + } + } + + fitted = len(itemSlice) == 0 + + if !fitted { + b.Items = forRevert + b.itemsVolume = bkitemsVolume + b.itemsWeight = bkitemsWeight + } + } else { + b.Items = forRevert + b.itemsVolume = bkitemsVolume + b.itemsWeight = bkitemsWeight + } + + for b2 := p.getBiggerBoxThan(boxes, b); b2 != nil && !fitted; b2 = p.getBiggerBoxThan(boxes, b) { + if left := p.packToBox(boxes, b2, append(b2.Items, i)); len(left) == 0 { + b = b2 + fitted = true + } + } + + if !fitted { + unpacked = append(unpacked, i) + } + } + } + + return //nolint:nakedret +} + +func (p *Packer) getBiggerBoxThan(boxes []*Box, b *Box) *Box { + v := b.GetVolume() + for _, b2 := range boxes { + if b2.GetVolume() > v { + return b2 + } + } + + return nil +} + +// FindFittedBox находит коробку для товара. +func (p *Packer) FindFittedBox(boxes []*Box, i *Item) *Box { + for _, b := range boxes { + if !b.PutItem(i, Pivot{0, 0, 0}) { + continue + } + + if len(b.Items) == 1 && b.Items[0] == i { + b.itemsVolume -= i.GetVolume() + b.itemsWeight -= i.GetWeight() + + b.Items = []*Item{} + } + + return b + } + + return nil +} diff --git a/packer_test.go b/packer_test.go new file mode 100644 index 0000000..cbb9e91 --- /dev/null +++ b/packer_test.go @@ -0,0 +1,209 @@ +package boxpacker3_test + +import ( + "testing" + + "github.com/bavix/boxpacker3" + "github.com/google/uuid" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" +) + +const ( + // BoxTypeF "Тип Е", 220, 185, 50, 20000. + BoxTypeF = "8ec81501-11a4-4b3f-9a52-7cd2f9c8370c" + + // BoxTypeE "Тип Д", 165, 215, 100, 20000. + BoxTypeE = "9c69baf8-1ca3-46a0-9fc2-6f15ad9fef9a" + + // BoxTypeG "Тип Г", 265, 165, 190, 20000. + BoxTypeG = "2c5279d3-48ad-451b-b673-f6d9be7fc6f6" + + // BoxTypeC "Тип В", 425, 165, 190, 20000. + BoxTypeC = "7f1cc68f-d554-4094-8734-c68df5c13154" + + // BoxTypeB "Тип Б", 425, 265, 190, 20000. + BoxTypeB = "76cede41-86bb-4487-bfb0-9513f032d53e" + + // BoxTypeA "Тип А", 425, 265, 380, 20000. + BoxTypeA = "8e10cebf-cee6-4136-b060-1587b993d083" + + // BoxTypeStd "Стандартная", 530, 380, 265, 20000. + BoxTypeStd = "ba973206-aa64-493b-b37a-c53192cde8fd" + + // BoxTypeNotStd1 "Не Стандартная 1", 1000, 500, 500, 20000. + BoxTypeNotStd1 = "cb1ed5b8-7405-48c5-bfd0-d86f75c99261" + + // BoxTypeNotStd2 "Не Стандартная 2", 1000, 1000, 1000, 20000. + BoxTypeNotStd2 = "d91e2661-aebb-4a55-bfb5-4ff9c6e3c008" + + // BoxTypeNotStd3 "Не Стандартная 3", 2000, 500, 500, 20000. + BoxTypeNotStd3 = "a0ecd730-375a-4313-bbe8-820710606b3d" + + // BoxTypeNotStd4 "Не Стандартная 4", 2000, 2000, 2000, 20000. + BoxTypeNotStd4 = "6dff37f0-4dd1-4143-abdc-c19ab94f2e68" + + // BoxTypeNotStd5 "Не Стандартная 5", 2500, 2500, 2500, 20000. + BoxTypeNotStd5 = "abac6d59-b51f-4d62-a338-42aca7afe1cc" + + // BoxTypeNotStd6 "Огромные размеры 6", 3000, 3000, 3000, 20000. + BoxTypeNotStd6 = "981ffb30-a7b9-4d9e-820e-04de2145763e" +) + +//nolint:gomnd +func NewDefaultBoxList() []*boxpacker3.Box { + return []*boxpacker3.Box{ + boxpacker3.NewBox(BoxTypeF, 220, 185, 50, 20000), // 0 + boxpacker3.NewBox(BoxTypeE, 165, 215, 100, 20000), // 1 + boxpacker3.NewBox(BoxTypeG, 265, 165, 190, 20000), // 2 + boxpacker3.NewBox(BoxTypeC, 425, 165, 190, 20000), // 3 + boxpacker3.NewBox(BoxTypeB, 425, 265, 190, 20000), // 4 + boxpacker3.NewBox(BoxTypeA, 425, 265, 380, 20000), // 5 + boxpacker3.NewBox(BoxTypeStd, 530, 380, 265, 20000), // 6 + boxpacker3.NewBox(BoxTypeNotStd1, 1000, 500, 500, 20000), // 7 + boxpacker3.NewBox(BoxTypeNotStd2, 1000, 1000, 1000, 20000), // 8 + boxpacker3.NewBox(BoxTypeNotStd3, 2000, 500, 500, 20000), // 9 + boxpacker3.NewBox(BoxTypeNotStd4, 2000, 2000, 2000, 20000), // 10 + boxpacker3.NewBox(BoxTypeNotStd5, 2500, 2500, 2500, 20000), // 11 + boxpacker3.NewBox(BoxTypeNotStd6, 3000, 3000, 3000, 20000), // 12 + } +} + +type PackerSuit struct { + suite.Suite +} + +func TestBoxPackerSuite(t *testing.T) { + t.Parallel() + + suite.Run(t, new(PackerSuit)) +} + +func (s *PackerSuit) TestMinBox() { + t := s.T() + t.Parallel() + + packer := boxpacker3.NewPacker() + boxes := NewDefaultBoxList() + item := boxpacker3.NewItem( + uuid.New().String(), + 8, + 17, + 5, + 384) + + packResult, err := packer.Pack(boxes, []*boxpacker3.Item{item}) + require.NoError(t, err) + require.NotNil(t, packResult) + + checks := map[string]int{ + BoxTypeF: 1, + } + + require.Len(t, packResult.UnfitItems, 0) + + for i := 0; i < len(packResult.Boxes); i++ { + require.Len(t, packResult.Boxes[i].Items, checks[packResult.Boxes[i].ID]) + } +} + +func (s *PackerSuit) TestRotate() { + t := s.T() + t.Parallel() + + packer := boxpacker3.NewPacker() + boxes := NewDefaultBoxList() + + items := []*boxpacker3.Item{ + boxpacker3.NewItem(uuid.New().String(), 100, 380, 250, 2690), + boxpacker3.NewItem(uuid.New().String(), 380, 100, 250, 2690), + boxpacker3.NewItem(uuid.New().String(), 250, 380, 100, 2690), + boxpacker3.NewItem(uuid.New().String(), 100, 380, 250, 2690), + boxpacker3.NewItem(uuid.New().String(), 100, 380, 250, 2690), + } + + packResult, err := packer.Pack(boxes, items) + require.NoError(t, err) + require.NotNil(t, packResult) + + checks := map[string]int{ + BoxTypeStd: 5, + } + + require.Len(t, packResult.UnfitItems, 0) + + for i := 0; i < len(packResult.Boxes); i++ { + require.Len(t, packResult.Boxes[i].Items, checks[packResult.Boxes[i].ID], packResult.Boxes[i].ID) + } +} + +func (s *PackerSuit) TestStd() { + t := s.T() + t.Parallel() + + packer := boxpacker3.NewPacker() + boxes := NewDefaultBoxList() + + items := []*boxpacker3.Item{ + boxpacker3.NewItem(uuid.New().String(), 100, 380, 250, 2690), + boxpacker3.NewItem(uuid.New().String(), 100, 380, 250, 2690), + boxpacker3.NewItem(uuid.New().String(), 100, 380, 250, 2690), + boxpacker3.NewItem(uuid.New().String(), 100, 380, 250, 2690), + boxpacker3.NewItem(uuid.New().String(), 100, 380, 250, 2690), + } + + packResult, err := packer.Pack(boxes, items) + require.NoError(t, err) + require.NotNil(t, packResult) + + checks := map[string]int{ + BoxTypeStd: 5, + } + + require.Len(t, packResult.UnfitItems, 0) + + for i := 0; i < len(packResult.Boxes); i++ { + require.Len(t, packResult.Boxes[i].Items, checks[packResult.Boxes[i].ID], packResult.Boxes[i].ID) + } +} + +func (s *PackerSuit) TestPacker_AllBoxes() { + t := s.T() + t.Parallel() + + packer := boxpacker3.NewPacker() + + boxes := NewDefaultBoxList() + reverse := make([]*boxpacker3.Box, len(boxes)) + + for i := range boxes { + reverse[i] = boxes[len(boxes)-1-i] + } + + items := []*boxpacker3.Item{ + boxpacker3.NewItem(uuid.New().String(), 1000, 1000, 1000, 20000), + boxpacker3.NewItem(uuid.New().String(), 2000, 500, 500, 20000), + boxpacker3.NewItem(uuid.New().String(), 2000, 2000, 2000, 20000), + boxpacker3.NewItem(uuid.New().String(), 2500, 2500, 2500, 20000), + boxpacker3.NewItem(uuid.New().String(), 3000, 3000, 3000, 20000), + + boxpacker3.NewItem(uuid.New().String(), 220, 185, 50, 20000), + boxpacker3.NewItem(uuid.New().String(), 165, 215, 100, 20000), + boxpacker3.NewItem(uuid.New().String(), 265, 165, 190, 20000), + boxpacker3.NewItem(uuid.New().String(), 425, 165, 190, 20000), + boxpacker3.NewItem(uuid.New().String(), 425, 265, 190, 20000), + boxpacker3.NewItem(uuid.New().String(), 425, 265, 380, 20000), + boxpacker3.NewItem(uuid.New().String(), 530, 380, 265, 20000), + boxpacker3.NewItem(uuid.New().String(), 1000, 500, 500, 20000), + } + + packResult, err := packer.Pack(reverse, items) + require.NoError(t, err) + require.NotNil(t, packResult) + + require.Len(t, packResult.UnfitItems, 0) + + for i := 0; i < len(packResult.Boxes); i++ { + require.Len(t, packResult.Boxes[i].Items, 1) + } +}