From 311320d30f3c36ffe68eb2d61171fd3effa0ef9b Mon Sep 17 00:00:00 2001 From: Ilia Denisov Date: Tue, 9 Sep 2025 00:13:34 +0300 Subject: [PATCH] refactor: bitmap package --- pkg/bitmap/assets_test/circle_case_00.txt | 0 pkg/bitmap/bitmap.go | 121 ++++++++++++++ pkg/bitmap/bitmap_test.go | 184 ++++++++++++++++++++++ pkg/bitmap/export_test.go | 9 ++ pkg/bitmap/go.mod | 3 + 5 files changed, 317 insertions(+) create mode 100644 pkg/bitmap/assets_test/circle_case_00.txt create mode 100644 pkg/bitmap/bitmap.go create mode 100644 pkg/bitmap/bitmap_test.go create mode 100644 pkg/bitmap/export_test.go create mode 100644 pkg/bitmap/go.mod diff --git a/pkg/bitmap/assets_test/circle_case_00.txt b/pkg/bitmap/assets_test/circle_case_00.txt new file mode 100644 index 0000000..e69de29 diff --git a/pkg/bitmap/bitmap.go b/pkg/bitmap/bitmap.go new file mode 100644 index 0000000..16913a8 --- /dev/null +++ b/pkg/bitmap/bitmap.go @@ -0,0 +1,121 @@ +package bitmap + +import ( + "fmt" + "math" +) + +const intSize = 32 + +type bitmap struct { + width uint32 + height uint32 + bitVector []uint32 +} + +func NewBitmap(width uint32, height uint32) bitmap { + return bitmap{width: width, height: height, bitVector: make([]uint32, int(math.Ceil(float64(width*height)/intSize)))} +} + +func (p bitmap) Set(x, y int) { + boundX := (p.width + uint32(x)) % p.width + boundY := (p.height + uint32(y)) % p.height + p.set(boundX + boundY*p.width) +} + +func (p bitmap) set(number uint32) { + p.bitVector[number/intSize] |= (0b1 << (number % intSize)) +} + +func (p bitmap) IsSet(x, y int) bool { + return p.isSet(uint32(x) + uint32(y)*p.width) +} + +func (p bitmap) isSet(number uint32) bool { + return p.bitVector[number/intSize]&(0b1<<(number%intSize)) > 0 +} + +func (p bitmap) Circle(x, y int, r float64) { + plotX := 0 + plotY := int(math.Ceil(r)) + delta := 3 - 2*plotY + lastY := plotY + for plotX <= plotY { + p.octant(x, y, plotX, plotY) + if plotY < lastY { + for lineX := 0; lineX < plotX; lineX++ { + p.octant(x, y, lineX, plotY) + } + lastY = plotY + } + if delta < 0 { + delta += 4*plotX + 6 + } else { + delta += 4*(plotX-plotY) + 10 + plotY -= 1 + } + plotX += 1 + } + for fillX := 0; fillX < plotX; fillX++ { + for fillY := 0; fillY <= fillX; fillY++ { + p.octant(x, y, fillX, fillY) + } + } +} + +func (p bitmap) CircleAdjacent(x, y int, r float64) { + plotX := 0 + plotY := int(math.Ceil(r)) + delta := 1 - 2*plotY + err := 0 + for plotX <= plotY { + p.octant(x, y, plotX, plotY) + err = 2*(delta+plotY) - 1 + if delta < 0 && err <= 0 { + plotX += 1 + delta += 2*plotX + 1 + continue + } + if delta > 0 && err > 0 { + plotY -= 1 + delta -= 2*plotY + 1 + continue + } + plotX += 1 + plotY -= 1 + delta += 2 * (plotX - plotY) + } +} + +func (p bitmap) octant(x, y int, plotX, plotY int) { + p.Set(x+plotX, y+plotY) + p.Set(x+plotX, y-plotY) + p.Set(x-plotX, y+plotY) + p.Set(x-plotX, y-plotY) + p.Set(x+plotY, y+plotX) + p.Set(x+plotY, y-plotX) + p.Set(x-plotY, y+plotX) + p.Set(x-plotY, y-plotX) +} + +func (p bitmap) Clear() { + for i := range p.bitVector { + p.bitVector[i] &= 0 + } +} + +func (p bitmap) String() string { + px := map[bool]string{true: "██", false: "░░"} + var result string + cnt := 0 + for i := 0; i < len(p.bitVector); i++ { + for bit := 0; bit < intSize && cnt < int(p.width*p.height); bit++ { + result += fmt.Sprintf("%s", px[p.bitVector[i]&(0b1< 0]) + cnt++ + if cnt%int(p.width) == 0 { + result += "\n" + } + } + } + return result +} diff --git a/pkg/bitmap/bitmap_test.go b/pkg/bitmap/bitmap_test.go new file mode 100644 index 0000000..5ea2dbd --- /dev/null +++ b/pkg/bitmap/bitmap_test.go @@ -0,0 +1,184 @@ +package bitmap_test + +import ( + "fmt" + "math/rand" + "os" + "strings" + "testing" + + "bitmap" +) + +func TestBitVectorSize(t *testing.T) { + type testCase struct { + width, height uint32 + expectSize int + } + for _, tc := range []testCase{ + { + width: 10, + height: 10, + expectSize: 4, + }, + { + width: 1, + height: 1, + expectSize: 1, + }, + { + width: 32, + height: 32, + expectSize: 32, + }, + } { + t.Run(fmt.Sprintf("w=%d h=%d s=%d", tc.width, tc.height, tc.expectSize), func(t *testing.T) { + bm := bitmap.NewBitmap(tc.width, tc.height) + l := len(bitmap.Value(bm)) + if tc.expectSize != l { + t.Errorf("expected bitmap size: %d, got: %d", tc.expectSize, l) + } + }) + } +} + +func TestSetPixel(t *testing.T) { + type coord struct { + x, y int + } + type testCase struct { + width, height uint32 + pixels []coord + bits []int + } + asMap := func(bits []int) map[int]bool { + result := make(map[int]bool) + for i := range bits { + if _, ok := result[bits[i]]; ok { + t.Fatalf("source bits duplicate at idx=%d", i) + } else { + result[bits[i]] = true + } + } + return result + } + asUint32 := func(v bool) uint32 { return map[bool]uint32{true: 1, false: 0}[v] } + for i, tc := range []testCase{ + { + width: 5, + height: 5, + pixels: []coord{{0, 0}}, + bits: []int{0}, + }, + { + width: 5, + height: 5, + pixels: []coord{{1, 0}}, + bits: []int{1}, + }, + { + width: 5, + height: 5, + pixels: []coord{{2, 0}}, + bits: []int{2}, + }, + { + width: 5, + height: 5, + pixels: []coord{{0, 1}}, + bits: []int{5}, + }, + { + width: 5, + height: 5, + pixels: []coord{{4, 4}}, + bits: []int{24}, + }, + { + width: 8, + height: 8, + pixels: []coord{{7, 7}}, + bits: []int{63}, + }, + } { + t.Run(fmt.Sprintf("tc#%d", i), func(t *testing.T) { + bm := bitmap.NewBitmap(tc.width, tc.height) + for _, c := range tc.pixels { + if bm.IsSet(c.x, c.y) { + t.Errorf("expected pixel to be clear at x=%d y=%d", c.x, c.y) + } + bm.Set(c.x, c.y) + if !bm.IsSet(c.x, c.y) { + t.Errorf("expected pixel to be set at x=%d y=%d", c.x, c.y) + } + } + bitVector := bitmap.Value(bm) + bitNum := 0 + expected := asMap(tc.bits) + for bi := range bitVector { + for ; bitNum < (bi+1)*32; bitNum++ { + if (bitVector[bi]>>(bitNum%32))&1 != asUint32(expected[bitNum]) { + t.Errorf("expected: bit #%d to be %t, got %v", bitNum, expected[bitNum], uint32(1<<(bitNum%32))) + } + } + } + }) + } +} + +func TestClear(t *testing.T) { + var bm = bitmap.NewBitmap(10, 10) + for range 50 { + bm.Set(rand.Intn(10), rand.Intn(10)) + } + var acc uint32 + for _, holder := range bitmap.Value(bm) { + acc |= holder + } + if acc == 0 { + t.Errorf("some pixels should be set") + } + bm.Clear() + acc = 0 + for _, holder := range bitmap.Value(bm) { + acc |= holder + } + if acc != 0 { + t.Errorf("all pixels should be clear") + } +} + +func TestCircle5x5(t *testing.T) { + type testCase struct { + x, y int + r float64 + filled bool + } + bm := bitmap.NewBitmap(80, 80) + for i, tc := range []testCase{ + {3, 3, 0.9, false}, + } { + file := fmt.Sprintf("assets_test/circle_case_%02d.txt", i) + _ = file + t.Run(file, func(t *testing.T) { + bm.CircleAdjacent(tc.x, tc.y, tc.r) + b, err := os.ReadFile(file) + if err != nil { + t.Fatal(err) + } + expect := strings.TrimSpace(string(b)) + if expect != bm.String() { + t.Errorf("expect:\n%s\ngot:\n%s\n", expect, bm.String()) + } + }) + } +} + +func TestCircle(t *testing.T) { + var size int = 40 + var bm1 = bitmap.NewBitmap(uint32(size), uint32(size)) + bm1.Circle(size/2+5, size/2-5, float64(size/2)-3) + fmt.Println(bm1) + var bm2 = bitmap.NewBitmap(uint32(size), uint32(size)) + bm2.CircleAdjacent(size/2+20, size/2, float64(size/2)-5) +} diff --git a/pkg/bitmap/export_test.go b/pkg/bitmap/export_test.go new file mode 100644 index 0000000..166adfd --- /dev/null +++ b/pkg/bitmap/export_test.go @@ -0,0 +1,9 @@ +package bitmap + +import "slices" + +func (p bitmap) value() []uint32 { + return slices.Clone(p.bitVector) +} + +var Value = (bitmap).value diff --git a/pkg/bitmap/go.mod b/pkg/bitmap/go.mod new file mode 100644 index 0000000..2ea27db --- /dev/null +++ b/pkg/bitmap/go.mod @@ -0,0 +1,3 @@ +module bitmap + +go 1.24.5