From f4ffae089e7aefedeb6c0d588bdc9d3261fa59df Mon Sep 17 00:00:00 2001 From: Nahshon Unna-Tsameret Date: Mon, 26 Apr 2021 11:19:52 +0300 Subject: [PATCH] implement bucket tool Signed-off-by: Nahshon Unna-Tsameret --- state/state.go | 119 ++++++++++++++++++++++++++++++++++++-------- state/state_test.go | 111 +++++++++++++++++++++++++++++++++++++++++ webapp/index.gohtml | 26 ++++++++-- 3 files changed, 231 insertions(+), 25 deletions(-) diff --git a/state/state.go b/state/state.go index f90bd9b..67d88e5 100644 --- a/state/state.go +++ b/state/state.go @@ -11,6 +11,7 @@ import ( const ( penName = "pen" eraserName = "eraser" + bucketName = "bucket" ) type Canvas [][]common.Color @@ -61,6 +62,7 @@ func NewState(canvasWidth, canvasHeight uint8) *State { s.tools = map[string]tool{ penName: s.pen, eraserName: s.eraser, + bucketName: s.bucket, } _ = s.Reset() @@ -149,50 +151,123 @@ func (s *State) Paint() *Change { return s.tools[s.toolName]() } -func (s *State) pen() *Change { - px := s.paintPixel(s.color) - if px == nil { +func (s *State) setSinglePixelTool(color common.Color) *Change { + after, before := s.paintPixel(color, s.cursor.X, s.cursor.Y) + if after == nil { return nil } + if before != nil { + change := &Change{ + Pixels: []Pixel{*before}, + } + undoList.push(change) + } + return &Change{ - Pixels: []Pixel{*px}, + Pixels: []Pixel{*after}, } } +func (s *State) pen() *Change { + return s.setSinglePixelTool(s.color) +} + func (s *State) eraser() *Change { - px := s.paintPixel(0) - if px == nil { - return nil + return s.setSinglePixelTool(0) +} + +func (s State) getNeighbors(center Pixel) []Pixel { + color := center.Color + res := make([]Pixel, 0, 4) + if center.Y > 0 && color == s.canvas[center.Y-1][center.X] { + res = append(res, Pixel{X: center.X, Y: center.Y - 1, Color: color}) } - return &Change{ - Pixels: []Pixel{*px}, + if center.X > 0 && color == s.canvas[center.Y][center.X-1] { + res = append(res, Pixel{X: center.X - 1, Y: center.Y, Color: color}) } + + if center.Y < s.canvasHeight-1 && color == s.canvas[center.Y+1][center.X] { + res = append(res, Pixel{X: center.X, Y: center.Y + 1, Color: color}) + } + + if center.X < s.canvasWidth-1 && color == s.canvas[center.Y][center.X+1] { + res = append(res, Pixel{X: center.X + 1, Y: center.Y, Color: color}) + } + + return res } -func (s *State) paintPixel(color common.Color) *Pixel { - if s.cursor.Y >= s.canvasHeight || s.cursor.X >= s.canvasWidth { - log.Printf("Error: Cursor (%d, %d) is out of canvas\n", s.cursor.X, s.cursor.Y) - return nil +func (s *State) paintNeighbors(px Pixel, after *[]Pixel, before *[]Pixel) { + nbs := s.getNeighbors(px) + + afterPx, beforePx := s.paintPixel(s.color, px.X, px.Y) + if afterPx != nil { + *after = append(*after, *afterPx) + + if beforePx != nil { + *before = append(*before, *beforePx) + } + + for _, n := range nbs { + s.paintNeighbors(n, after, before) + } } +} - if s.canvas[s.cursor.Y][s.cursor.X] != color { - chng := &Change{ - Pixels: []Pixel{{X: s.cursor.X, Y: s.cursor.Y, Color: s.canvas[s.cursor.Y][s.cursor.X]}}, +func (s *State) bucket() *Change { + cursorPx := Pixel{ + X: s.cursor.X, + Y: s.cursor.Y, + Color: s.canvas[s.cursor.Y][s.cursor.X], + } + + after := make([]Pixel, 0, 10) + before := make([]Pixel, 0, 10) + + s.paintNeighbors(cursorPx, &after, &before) + if len(after) > 0 { + undoChange := &Change{ + Pixels: before, } - undoList.push(chng) - s.canvas[s.cursor.Y][s.cursor.X] = color - return &Pixel{ - X: s.cursor.X, - Y: s.cursor.Y, - Color: color, + + undoList.push(undoChange) + + return &Change{ + Pixels: after, } } return nil } +func (s *State) paintPixel(color common.Color, x, y uint8) (*Pixel, *Pixel) { + if y >= s.canvasHeight || x >= s.canvasWidth { + log.Printf("Error: Cursor (%d, %d) is out of canvas\n", x, y) + return nil, nil + } + + if s.canvas[y][x] != color { + before := &Pixel{ + X: x, + Y: y, + Color: s.canvas[y][x], + } + + s.canvas[y][x] = color + after := &Pixel{ + X: x, + Y: y, + Color: color, + } + + return after, before + } + + return nil, nil +} + func (s State) CreateDisplayMessage() hat.DisplayMessage { c := make([][]common.Color, common.WindowSize) for y := uint8(0); y < common.WindowSize; y++ { diff --git a/state/state_test.go b/state/state_test.go index daee453..f112172 100644 --- a/state/state_test.go +++ b/state/state_test.go @@ -1,6 +1,7 @@ package state import ( + "reflect" "testing" "github.com/nunnatsa/piHatDraw/common" @@ -364,3 +365,113 @@ func TestState_Undo(t *testing.T) { } } + +func TestBucket(t *testing.T) { + s := NewState(8, 8) + s.canvas = [][]common.Color{ + {0, 0, 0, 0, 0, 0, 0, 0}, + {0, 0, 0, 0, 0, 0, 0, 0}, + {0, 0, 1, 1, 1, 1, 1, 0}, + {0, 0, 1, 0, 0, 0, 1, 0}, + {0, 0, 1, 0, 0, 0, 1, 0}, + {0, 0, 1, 0, 0, 0, 1, 0}, + {0, 0, 1, 1, 1, 1, 1, 0}, + {0, 0, 0, 0, 0, 0, 0, 0}, + } + + s.cursor.Y = 4 + s.cursor.X = 4 + s.toolName = bucketName + s.color = 2 + s.Paint() + expected := Canvas{ + {0, 0, 0, 0, 0, 0, 0, 0}, + {0, 0, 0, 0, 0, 0, 0, 0}, + {0, 0, 1, 1, 1, 1, 1, 0}, + {0, 0, 1, 2, 2, 2, 1, 0}, + {0, 0, 1, 2, 2, 2, 1, 0}, + {0, 0, 1, 2, 2, 2, 1, 0}, + {0, 0, 1, 1, 1, 1, 1, 0}, + {0, 0, 0, 0, 0, 0, 0, 0}, + } + + if !reflect.DeepEqual(expected, s.canvas) { + t.Errorf("should fill the square") + } + + s.cursor.X = 2 + s.cursor.Y = 2 + s.color = 3 + s.Paint() + expected = Canvas{ + {0, 0, 0, 0, 0, 0, 0, 0}, + {0, 0, 0, 0, 0, 0, 0, 0}, + {0, 0, 3, 3, 3, 3, 3, 0}, + {0, 0, 3, 2, 2, 2, 3, 0}, + {0, 0, 3, 2, 2, 2, 3, 0}, + {0, 0, 3, 2, 2, 2, 3, 0}, + {0, 0, 3, 3, 3, 3, 3, 0}, + {0, 0, 0, 0, 0, 0, 0, 0}, + } + + if !reflect.DeepEqual(expected, s.canvas) { + t.Errorf("should fill the square") + } + + s.cursor.X = 0 + s.cursor.Y = 0 + s.color = 4 + s.Paint() + + expected = Canvas{ + {4, 4, 4, 4, 4, 4, 4, 4}, + {4, 4, 4, 4, 4, 4, 4, 4}, + {4, 4, 3, 3, 3, 3, 3, 4}, + {4, 4, 3, 2, 2, 2, 3, 4}, + {4, 4, 3, 2, 2, 2, 3, 4}, + {4, 4, 3, 2, 2, 2, 3, 4}, + {4, 4, 3, 3, 3, 3, 3, 4}, + {4, 4, 4, 4, 4, 4, 4, 4}, + } + + if !reflect.DeepEqual(expected, s.canvas) { + t.Errorf("should fill the square") + } + + s.cursor.X = 7 + s.cursor.Y = 7 + s.color = 5 + s.Paint() + + expected = Canvas{ + {5, 5, 5, 5, 5, 5, 5, 5}, + {5, 5, 5, 5, 5, 5, 5, 5}, + {5, 5, 3, 3, 3, 3, 3, 5}, + {5, 5, 3, 2, 2, 2, 3, 5}, + {5, 5, 3, 2, 2, 2, 3, 5}, + {5, 5, 3, 2, 2, 2, 3, 5}, + {5, 5, 3, 3, 3, 3, 3, 5}, + {5, 5, 5, 5, 5, 5, 5, 5}, + } + + if !reflect.DeepEqual(expected, s.canvas) { + t.Errorf("should fill the square") + } + + s.Undo() + + expected = Canvas{ + {4, 4, 4, 4, 4, 4, 4, 4}, + {4, 4, 4, 4, 4, 4, 4, 4}, + {4, 4, 3, 3, 3, 3, 3, 4}, + {4, 4, 3, 2, 2, 2, 3, 4}, + {4, 4, 3, 2, 2, 2, 3, 4}, + {4, 4, 3, 2, 2, 2, 3, 4}, + {4, 4, 3, 3, 3, 3, 3, 4}, + {4, 4, 4, 4, 4, 4, 4, 4}, + } + + if !reflect.DeepEqual(expected, s.canvas) { + t.Errorf("should fill the square") + } +} diff --git a/webapp/index.gohtml b/webapp/index.gohtml index 26a39e7..69cf4b9 100644 --- a/webapp/index.gohtml +++ b/webapp/index.gohtml @@ -69,6 +69,8 @@ + +

@@ -148,8 +150,8 @@ win = data.window } - if (data.pen) { - color = data.pen.color + if (data.color) { + color = data.color } const mt = document.getElementById("matrix") @@ -183,12 +185,26 @@ const cursorElement = document.getElementById(cursorID) cursorElement.style.color = reverseColor(cursorElement.style.backgroundColor) - cursorElement.innerText = tool === "pen" ? '+' : tool === "eraser" ? 'x' : "?" + switch (tool) { + case "pen": + cursorElement.innerText = '+' + break + case "eraser": + cursorElement.innerText = 'x' + break + case "bucket": + cursorElement.innerText = 'o' + break + default: + cursorElement.innerText = '?' + } if (tool === "pen") { toolTypePen.checked = "checked" } else if (tool === "eraser") { toolTypeEraser.checked = "checked" + } else if (tool === "bucket") { + toolTypeBucket.checked = "checked" } colorPicker.value = color @@ -211,6 +227,10 @@ toolTypeEraser.onchange = function (e) { setTool("eraser") } + const toolTypeBucket = document.getElementById("toolTypeBucket") + toolTypeBucket.onchange = function (e) { + setTool("bucket") + } pickColorFromPixel = document.getElementById("pickColorFromPixel") pickColorFromPixel.onclick = function (e) {