From 6571d0197803e2e820015a30cd65fb92236b8116 Mon Sep 17 00:00:00 2001 From: Nahshon Unna-Tsameret Date: Thu, 15 Apr 2021 18:08:31 +0300 Subject: [PATCH] support undo Signed-off-by: Nahshon Unna-Tsameret --- controller/controller.go | 41 ++-- controller/controller_test.go | 239 ++++++++++++++++----- state/change.go | 51 +++++ state/change_test.go | 49 +++++ state/state.go | 140 ++++++++++--- state/state_test.go | 148 +++++++++++-- webapp/index.gohtml | 82 ++++++-- webapp/websocket.go | 381 ++++++++++++++++++---------------- webapp/websocket_test.go | 56 +++++ 9 files changed, 863 insertions(+), 324 deletions(-) create mode 100644 state/change.go create mode 100644 state/change_test.go diff --git a/controller/controller.go b/controller/controller.go index 6c6679a..65cbeb4 100644 --- a/controller/controller.go +++ b/controller/controller.go @@ -8,12 +8,10 @@ import ( "syscall" "github.com/nunnatsa/piHatDraw/common" - - "github.com/nunnatsa/piHatDraw/notifier" - "github.com/nunnatsa/piHatDraw/webapp" - "github.com/nunnatsa/piHatDraw/hat" + "github.com/nunnatsa/piHatDraw/notifier" "github.com/nunnatsa/piHatDraw/state" + "github.com/nunnatsa/piHatDraw/webapp" ) type Controller struct { @@ -59,7 +57,7 @@ func (c *Controller) do() { c.screenEvents <- msg for { - changed := false + var change *state.Change = nil select { case <-signals: @@ -68,19 +66,19 @@ func (c *Controller) do() { case je := <-c.joystickEvents: switch je { case hat.MoveUp: - changed = c.state.GoUp() + change = c.state.GoUp() case hat.MoveLeft: - changed = c.state.GoLeft() + change = c.state.GoLeft() case hat.MoveDown: - changed = c.state.GoDown() + change = c.state.GoDown() case hat.MoveRight: - changed = c.state.GoRight() + change = c.state.GoRight() case hat.Pressed: - changed = c.state.PaintPixel() + change = c.state.PaintPixel() } case e := <-c.clientEvents: @@ -91,20 +89,19 @@ func (c *Controller) do() { case webapp.ClientEventReset: if data { - c.state.Reset() - changed = true + change = c.state.Reset() } case webapp.ClientEventSetColor: color := common.Color(data) - changed = c.state.SetColor(color) + change = c.state.SetColor(color) case webapp.ClientEventSetTool: switch string(data) { case "pen": - changed = c.state.SetPen() + change = c.state.SetPen() case "eraser": - changed = c.state.SetEraser() + change = c.state.SetEraser() default: log.Printf(`unknown tool "%s"`, data) } @@ -112,11 +109,14 @@ func (c *Controller) do() { case webapp.ClientEventDownload: ch := chan [][]common.Color(data) ch <- c.state.Canvas.Clone() + + case webapp.ClientEventUndo: + change = c.state.Undo() } } - if changed { - c.Update() + if change != nil { + c.Update(change) } } } @@ -128,13 +128,13 @@ func (c *Controller) stop(signals chan os.Signal) { close(c.done) } -func (c *Controller) Update() { +func (c *Controller) Update(change *state.Change) { msg := c.state.CreateDisplayMessage() go func() { c.screenEvents <- msg }() - js, err := json.Marshal(c.state) + js, err := json.Marshal(change) if err != nil { log.Println(err) } else { @@ -143,7 +143,8 @@ func (c *Controller) Update() { } func (c *Controller) registered(id uint64) { - js, err := json.Marshal(c.state) + change := c.state.GetFullChange() + js, err := json.Marshal(change) if err != nil { log.Println(err) } else { diff --git a/controller/controller_test.go b/controller/controller_test.go index 1bd7cf7..22d48ff 100644 --- a/controller/controller_test.go +++ b/controller/controller_test.go @@ -2,15 +2,20 @@ package controller import ( "encoding/json" - "github.com/nunnatsa/piHatDraw/common" + "fmt" + "reflect" "testing" + "github.com/nunnatsa/piHatDraw/common" + "github.com/nunnatsa/piHatDraw/hat" "github.com/nunnatsa/piHatDraw/notifier" + "github.com/nunnatsa/piHatDraw/state" "github.com/nunnatsa/piHatDraw/webapp" +) - "github.com/nunnatsa/piHatDraw/hat" - - "github.com/nunnatsa/piHatDraw/state" +const ( + eraserToolName = "eraser" + penToolName = "pen" ) func TestControllerStart(t *testing.T) { @@ -55,130 +60,260 @@ func TestControllerStart(t *testing.T) { } ce <- webapp.ClientEventRegistered(client1) - <-checkNotifications(t, reg1, x, y) + err := <-checkMoveNotifications(reg1, x, y) + if err != nil { + t.Fatal(err) + } ce <- webapp.ClientEventRegistered(client2) - <-checkNotifications(t, reg2, x, y) + err = <-checkMoveNotifications(reg2, x, y) + if err != nil { + t.Fatal(err) + } if msg.CursorY != y-s.Window.Y { t.Errorf("msg.CursorY should be %d but it's %d", y-s.Window.Y, msg.CursorY) } + ce <- webapp.ClientEventUndo(true) + if len(se) != 0 || len(reg1) != 0 || len(reg2) != 0 { + t.Error("should not initiate a chenage") + } + hatMock.MoveDown() msg = <-se if msg.CursorY != y+1-s.Window.Y { t.Errorf("msg.CursorY should be %d but it's %d", y+1-s.Window.Y, msg.CursorY) } - <-checkNotifications(t, reg1, x, y+1) - <-checkNotifications(t, reg2, x, y+1) + err = <-checkMoveNotifications(reg1, x, y+1) + if err != nil { + t.Fatal(err) + } + err = <-checkMoveNotifications(reg2, x, y+1) + if err != nil { + t.Fatal(err) + } + + ce <- webapp.ClientEventUndo(true) + if len(se) != 0 || len(reg1) != 0 || len(reg2) != 0 { + t.Error("should not initiate a chenage") + } hatMock.MoveUp() msg = <-se if msg.CursorY != y-s.Window.Y { t.Errorf("msg.CursorY should be %d but it's %d", y-s.Window.Y, msg.CursorY) } - <-checkNotifications(t, reg1, x, y) - <-checkNotifications(t, reg2, x, y) + err = <-checkMoveNotifications(reg1, x, y) + if err != nil { + t.Fatal(err) + } + err = <-checkMoveNotifications(reg2, x, y) + if err != nil { + t.Fatal(err) + } + + ce <- webapp.ClientEventUndo(true) + if len(se) != 0 || len(reg1) != 0 || len(reg2) != 0 { + t.Error("should not initiate a chenage") + } hatMock.MoveRight() msg = <-se if msg.CursorX != x+1-s.Window.X { t.Errorf("msg.CursorX should be %d but it's %d", x+1-s.Window.X, msg.CursorY) } - <-checkNotifications(t, reg1, x+1, y) - <-checkNotifications(t, reg2, x+1, y) + err = <-checkMoveNotifications(reg1, x+1, y) + if err != nil { + t.Fatal(err) + } + + err = <-checkMoveNotifications(reg2, x+1, y) + if err != nil { + t.Fatal(err) + } + + ce <- webapp.ClientEventUndo(true) + if len(se) != 0 || len(reg1) != 0 || len(reg2) != 0 { + t.Error("should not initiate a change") + } hatMock.MoveLeft() msg = <-se if msg.CursorX != x-s.Window.X { t.Errorf("msg.CursorX should be %d but it's %d", x-s.Window.X, msg.CursorX) } - <-checkNotifications(t, reg1, x, y) - <-checkNotifications(t, reg2, x, y) + err = <-checkMoveNotifications(reg1, x, y) + if err != nil { + t.Fatal(err) + } + + err = <-checkMoveNotifications(reg2, x, y) + if err != nil { + t.Fatal(err) + } + + ce <- webapp.ClientEventUndo(true) + if len(se) != 0 || len(reg1) != 0 || len(reg2) != 0 { + t.Error("should not initiate a change") + } hatMock.Press() msg = <-se if msg.Screen[y-s.Window.Y][x-s.Window.X] != 0xFFFFFF { t.Errorf("msg.Screen[%d][%d] should be set", y, x) } - <-checkNotifications(t, reg1, x, y, x, y) - <-checkNotifications(t, reg2, x, y, x, y) + <-checkPaintNotifications(t, reg1, state.Pixel{X: x, Y: y, Color: 0xFFFFFF}) + <-checkPaintNotifications(t, reg2, state.Pixel{X: x, Y: y, Color: 0xFFFFFF}) - ce <- webapp.ClientEventSetColor(0x123456) - <-checkNotificationsColor(t, reg1, 0x123456, "pen") - <-checkNotificationsColor(t, reg2, 0x123456, "pen") + clr := common.Color(0x123456) + ce <- webapp.ClientEventSetColor(clr) + <-checkNotificationsColor(t, reg1, &clr, "") + <-checkNotificationsColor(t, reg2, &clr, "") - ce <- webapp.ClientEventSetTool("eraser") - <-checkNotificationsColor(t, reg1, 0x123456, "eraser") - <-checkNotificationsColor(t, reg2, 0x123456, "eraser") + ce <- webapp.ClientEventSetTool(eraserToolName) + <-checkNotificationsColor(t, reg1, nil, eraserToolName) + <-checkNotificationsColor(t, reg2, nil, eraserToolName) - ce <- webapp.ClientEventSetColor(0x654321) - <-checkNotificationsColor(t, reg1, 0x654321, "eraser") - <-checkNotificationsColor(t, reg2, 0x654321, "eraser") + clr = common.Color(0x654321) + ce <- webapp.ClientEventSetColor(clr) + <-checkNotificationsColor(t, reg1, &clr, "") + <-checkNotificationsColor(t, reg2, &clr, "") + + ce <- webapp.ClientEventSetTool(penToolName) + <-checkNotificationsColor(t, reg1, nil, penToolName) + <-checkNotificationsColor(t, reg2, nil, penToolName) + + ce <- webapp.ClientEventUndo(true) + <-checkPaintNotifications(t, reg1, state.Pixel{X: x, Y: y, Color: 0}) + <-checkPaintNotifications(t, reg2, state.Pixel{X: x, Y: y, Color: 0}) + + hatMock.Press() + msg = <-se + if msg.Screen[y-s.Window.Y][x-s.Window.X] != 0xFFFFFF { + t.Errorf("msg.Screen[%d][%d] should be set", y, x) + } + <-checkPaintNotifications(t, reg1, state.Pixel{X: x, Y: y, Color: 0x654321}) + <-checkPaintNotifications(t, reg2, state.Pixel{X: x, Y: y, Color: 0x654321}) - ce <- webapp.ClientEventSetTool("pen") - <-checkNotificationsColor(t, reg1, 0x654321, "pen") - <-checkNotificationsColor(t, reg2, 0x654321, "pen") ce <- webapp.ClientEventReset(true) - <-checkNotificationsColor(t, reg1, 0xFFFFFF, "pen") - <-checkNotifications(t, reg2, x, y) + initColor := common.Color(0xFFFFFF) + <-checkNotificationsColor(t, reg1, &initColor, penToolName) + err = <-checkMoveNotifications(reg2, x, y) + if err != nil { + t.Fatal(err) + } + ce <- webapp.ClientEventUndo(true) + ns := state.NewState(40, 24) + ns.Canvas[12][20] = 0x654321 + err = <-checkResetNotifications(reg1, ns.Canvas) + if err != nil { + t.Fatal(err) + } + err = <-checkResetNotifications(reg2, ns.Canvas) + if err != nil { + t.Fatal(err) + } } -func checkNotifications(t *testing.T, reg chan []byte, x uint8, y uint8, points ...uint8) chan bool { - doneCheckingNotifier := make(chan bool) +func checkMoveNotifications(reg chan []byte, x uint8, y uint8) chan error { + doneCheckingNotifier := make(chan error) go func() { defer close(doneCheckingNotifier) msg := <-reg - webMsg, err := getCanvasFromMsg(msg) + webMsg, err := getChangeFromMsg(msg) if err != nil { - t.Fatal("getCanvasFromMsg", err) + doneCheckingNotifier <- fmt.Errorf("getChangeFromMsg %v", err) + return } if webMsg.Cursor.X != x { - t.Errorf("webMsg.Cursor.X should be %d but it's %d", x, webMsg.Cursor.X) + doneCheckingNotifier <- fmt.Errorf("webMsg.Cursor.X should be %d but it's %d", x, webMsg.Cursor.X) + return } if webMsg.Cursor.Y != y { - t.Errorf("webMsg.Cursor.y should be %d but it's %d", y+1, webMsg.Cursor.Y) + doneCheckingNotifier <- fmt.Errorf("webMsg.Cursor.y should be %d but it's %d", y+1, webMsg.Cursor.Y) + return + } + }() + return doneCheckingNotifier +} + +func checkPaintNotifications(t *testing.T, reg chan []byte, pixels ...state.Pixel) chan bool { + doneCheckingNotifier := make(chan bool) + go func() { + defer close(doneCheckingNotifier) + + msg := <-reg + webMsg, err := getChangeFromMsg(msg) + if err != nil { + t.Fatal("getChangeFromMsg", err) } - if len(points) > 0 && len(points)%2 == 0 { - for i := 0; i < len(points); i += 2 { - px := points[i] - py := points[i+1] + if len(pixels) != len(webMsg.Pixels) { + t.Fatalf("wrong length of webMsg.Pixels; should be %d but it's %d", len(pixels), len(webMsg.Pixels)) + } - if webMsg.Canvas[py][px] != 0xFFFFFF { - t.Error("webMsg.Canvas[py][px] should be set") - } + for i, p := range pixels { + mp := webMsg.Pixels[i] + if mp.X != p.X || mp.Y != p.Y || mp.Color != p.Color { + t.Errorf("wrong pixel. Expected: %#v; Actual: %#v", p, mp) } } + }() return doneCheckingNotifier } -func checkNotificationsColor(t *testing.T, reg chan []byte, color common.Color, tool string) chan bool { +func checkNotificationsColor(t *testing.T, reg chan []byte, color *common.Color, tool string) chan bool { doneCheckingNotifier := make(chan bool) go func() { defer close(doneCheckingNotifier) msg := <-reg - webMsg, err := getCanvasFromMsg(msg) + webMsg, err := getChangeFromMsg(msg) if err != nil { - t.Fatal("getCanvasFromMsg", err) + t.Fatal("getChangeFromMsg", err) } - if webMsg.Pen.Color != color { - t.Errorf("webMsg.Cursor.Pen.Color should be %x but it's %x", color, webMsg.Pen.Color) + if color != nil { + if webMsg.Pen == nil { + t.Fatalf("webMsg.Cursor.Pen.Color should be #%06x but pen is nil", *color) + } else if webMsg.Pen.Color != *color { + t.Fatalf("webMsg.Cursor.Pen.Color should be #%06x but it's %x", *color, webMsg.Pen.Color) + } } - if webMsg.ToolName != tool { - t.Errorf("webMsg.Cursor.ToolName should be %x but it's %x", tool, webMsg.ToolName) + if tool != webMsg.ToolName { + t.Errorf(`webMsg.Cursor.ToolName should be "%s" but it's "%s"`, tool, webMsg.ToolName) } }() return doneCheckingNotifier } -func getCanvasFromMsg(msg []byte) (*state.State, error) { - s := &state.State{} +func getChangeFromMsg(msg []byte) (*state.Change, error) { + s := &state.Change{} if err := json.Unmarshal(msg, s); err != nil { return nil, err } return s, nil } + +func checkResetNotifications(reg chan []byte, canvas [][]common.Color) chan error { + doneCheckingNotifier := make(chan error) + go func() { + defer close(doneCheckingNotifier) + + msg := <-reg + webMsg, err := getChangeFromMsg(msg) + if err != nil { + doneCheckingNotifier <- fmt.Errorf("getChangeFromMsg %v", err) + return + } + + webMsgCanvas := [][]common.Color(*webMsg.Canvas) + if !reflect.DeepEqual(webMsgCanvas, canvas) { + doneCheckingNotifier <- fmt.Errorf("canvas should contain the point before the reset") + return + } + }() + return doneCheckingNotifier +} diff --git a/state/change.go b/state/change.go new file mode 100644 index 0000000..e0d232f --- /dev/null +++ b/state/change.go @@ -0,0 +1,51 @@ +package state + +import ( + "github.com/nunnatsa/piHatDraw/common" +) + +type Pixel struct { + X uint8 `json:"x"` + Y uint8 `json:"y"` + Color common.Color `json:"color"` +} + +type Change struct { + Canvas *canvas `json:"canvas,omitempty"` + Cursor *cursor `json:"cursor,omitempty"` + Window *window `json:"window,omitempty"` + ToolName string `json:"toolName,omitempty"` + Pen *pen `json:"pen,omitempty"` + + Pixels []Pixel `json:"pixels,omitempty"` +} + +type changeNode struct { + data *Change + next *changeNode +} + +type changeStack struct { + head *changeNode +} + +func (s *changeStack) push(change *Change) { + s.head = &changeNode{ + data: change, + next: s.head, + } +} + +func (s *changeStack) pop() *Change { + if s == nil || s.head == nil { + return nil + } + res := s.head + s.head = s.head.next + + return res.data +} + +var undoList = &changeStack{ + head: nil, +} diff --git a/state/change_test.go b/state/change_test.go new file mode 100644 index 0000000..5688d5b --- /dev/null +++ b/state/change_test.go @@ -0,0 +1,49 @@ +package state + +import "testing" + +func (s *changeStack) len() int { + if s == nil { + return 0 + } + + res := 0 + for p := s.head; p != nil; p = p.next { + res++ + } + + return res +} + +func TestChangeStack(t *testing.T) { + s := changeStack{} + if s.len() != 0 { + t.Errorf("should be empty") + } + + s.push(&Change{ToolName: "first"}) + if s.len() != 1 { + t.Errorf("should be with len of 1") + } + s.push(&Change{ToolName: "second"}) + if s.len() != 2 { + t.Errorf("should be with len of 2") + } + + // check LIFO: + chng := s.pop() + if chng == nil { + t.Fatal("should no be bil") + } + if chng.ToolName != "second" { + t.Errorf("chng.ToolName should be 'second'") + } + chng = s.pop() + if chng.ToolName != "first" { + t.Errorf("chng.ToolName should be 'first'") + } + chng = s.pop() + if chng != nil { + t.Error("chng should be nil") + } +} diff --git a/state/state.go b/state/state.go index 9c186be..abe073e 100644 --- a/state/state.go +++ b/state/state.go @@ -3,9 +3,8 @@ package state import ( "log" - "github.com/nunnatsa/piHatDraw/hat" - "github.com/nunnatsa/piHatDraw/common" + "github.com/nunnatsa/piHatDraw/hat" ) type canvas [][]common.Color @@ -62,7 +61,7 @@ type State struct { Window window `json:"window,omitempty"` canvasWidth uint8 canvasHeight uint8 - ToolName string `json:"tool"` + ToolName string `json:"toolName"` Pen *pen `json:"pen"` tool tool } @@ -73,12 +72,21 @@ func NewState(canvasWidth, canvasHeight uint8) *State { canvasHeight: canvasHeight, } - s.Reset() + _ = s.Reset() return s } -func (s *State) Reset() { +func (s *State) Reset() *Change { + if len(s.Canvas) > 0 { + cv := s.Canvas.Clone() + chng := &Change{ + Canvas: &cv, + } + + undoList.push(chng) + } + c := make([][]common.Color, s.canvasHeight) for y := uint8(0); y < s.canvasHeight; y++ { c[y] = make([]common.Color, s.canvasWidth) @@ -93,67 +101,79 @@ func (s *State) Reset() { s.Window = win s.Pen = &pen{Color: 0xFFFFFF} s.SetPen() + + return s.GetFullChange() } -func (s *State) GoUp() bool { +func (s *State) GoUp() *Change { if s.Cursor.Y > 0 { s.Cursor.Y-- if s.Cursor.Y < s.Window.Y { s.Window.Y = s.Cursor.Y } - return true + return s.getPositionChange() } - return false + return nil } -func (s *State) GoLeft() bool { +func (s *State) GoLeft() *Change { if s.Cursor.X > 0 { s.Cursor.X-- if s.Cursor.X < s.Window.X { s.Window.X = s.Cursor.X } - return true + return s.getPositionChange() } - return false + return nil } -func (s *State) GoDown() bool { +func (s *State) GoDown() *Change { if s.Cursor.Y < s.canvasHeight-1 { s.Cursor.Y++ if s.Cursor.Y > s.Window.Y+common.WindowSize-1 { s.Window.Y++ } - return true + return s.getPositionChange() } - return false + return nil } -func (s *State) GoRight() bool { +func (s *State) GoRight() *Change { if s.Cursor.X < s.canvasWidth-1 { s.Cursor.X++ if s.Cursor.X > s.Window.X+common.WindowSize-1 { s.Window.X++ } - return true + return s.getPositionChange() } - return false + return nil } -func (s *State) PaintPixel() bool { +func (s *State) PaintPixel() *Change { 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 false + return nil } c := s.tool.GetColor() if s.Canvas[s.Cursor.Y][s.Cursor.X] != c { + chng := &Change{ + Pixels: []Pixel{{X: s.Cursor.X, Y: s.Cursor.Y, Color: s.Canvas[s.Cursor.Y][s.Cursor.X]}}, + } + undoList.push(chng) s.Canvas[s.Cursor.Y][s.Cursor.X] = c - return true + return &Change{ + Pixels: []Pixel{{ + X: s.Cursor.X, + Y: s.Cursor.Y, + Color: c, + }}, + } } - return false + return nil } func (s State) CreateDisplayMessage() hat.DisplayMessage { @@ -166,30 +186,82 @@ func (s State) CreateDisplayMessage() hat.DisplayMessage { return hat.NewDisplayMessage(c, s.Cursor.X-s.Window.X, s.Cursor.Y-s.Window.Y) } -func (s *State) SetColor(cl common.Color) bool { +func (s *State) SetColor(cl common.Color) *Change { if s.Pen.GetColor() != cl { s.Pen.SetColor(cl) - return true + return &Change{ + Pen: &pen{ + Color: cl, + }, + } } - return false + return nil } -func (s *State) SetPen() bool { +const ( + penName = "pen" + eraserName = "eraser" +) + +func (s *State) SetPen() *Change { s.tool = s.Pen - if s.ToolName != "pen" { - s.ToolName = "pen" - return true + if s.ToolName != penName { + s.ToolName = penName + return &Change{ + ToolName: penName, + } } - return false + return nil } var eraser Eraser -func (s *State) SetEraser() bool { - if s.ToolName != "eraser" { - s.ToolName = "eraser" +func (s *State) SetEraser() *Change { + if s.ToolName != eraserName { + s.ToolName = eraserName s.tool = eraser - return true + return &Change{ + ToolName: eraserName, + } } - return false + return nil +} + +func (s State) getPositionChange() *Change { + return &Change{ + Cursor: &cursor{ + X: s.Cursor.X, + Y: s.Cursor.Y, + }, + Window: &window{ + X: s.Window.X, + Y: s.Window.Y, + }, + } +} + +func (s State) GetFullChange() *Change { + cv := s.Canvas.Clone() + return &Change{ + Canvas: &cv, + Cursor: &s.Cursor, + Window: &s.Window, + ToolName: s.ToolName, + Pen: s.Pen, + } +} + +func (s *State) Undo() *Change { + chng := undoList.pop() + if chng != nil { + if chng.Canvas != nil { + s.Canvas = *chng.Canvas + } else if len(chng.Pixels) > 0 { + for _, pixel := range chng.Pixels { + s.Canvas[pixel.Y][pixel.X] = pixel.Color + } + } + } + + return chng } diff --git a/state/state_test.go b/state/state_test.go index 8cf8363..370acd5 100644 --- a/state/state_test.go +++ b/state/state_test.go @@ -65,18 +65,32 @@ func TestStateGoUp(t *testing.T) { x := s.Cursor.X y := s.Cursor.Y - s.GoUp() + change := s.GoUp() if s.Cursor.Y != y-1 { t.Errorf("s.Cursor.Y should be %d but it's %d", y-1, s.Cursor.Y) } if s.Cursor.X != x { t.Errorf("s.Cursor.X should be %d but it's %d", x, s.Cursor.X) } + if change.Cursor.Y != y-1 { + t.Errorf("change.Cursor.Y should be %d but it's %d", y-1, change.Cursor.Y) + } + + if change.Cursor.X != x { + t.Errorf("change.Cursor.X should be %d but it's %d", x, change.Cursor.X) + } + + if l := undoList.len(); l > 0 { + t.Errorf("undo list should be empty, but it's with length of %d", l) + } s.Cursor.Y = 0 - s.GoUp() + change = s.GoUp() + if change != nil { + t.Errorf("change should be nil") + } if s.Cursor.Y != 0 { - t.Errorf("s.Cursor.Y should be 0 but it's %d", s.Cursor.Y) + t.Errorf("s.Cursor.Y should be %d but it's %d", 0, s.Cursor.Y) } if s.Cursor.X != x { t.Errorf("s.Cursor.X should be %d but it's %d", x, s.Cursor.X) @@ -89,16 +103,29 @@ func TestStateGoDown(t *testing.T) { x := s.Cursor.X y := s.Cursor.Y - s.GoDown() + change := s.GoDown() if s.Cursor.Y != y+1 { t.Errorf("s.Cursor.Y should be %d but it's %d", y+1, s.Cursor.Y) } if s.Cursor.X != x { t.Errorf("s.Cursor.X should be %d but it's %d", x, s.Cursor.X) } + if change.Cursor.Y != y+1 { + t.Errorf("change.Cursor.Y should be %d but it's %d", y+1, change.Cursor.Y) + } + if change.Cursor.X != x { + t.Errorf("change.Cursor.X should be %d but it's %d", x, change.Cursor.X) + } + + if l := undoList.len(); l > 0 { + t.Errorf("undo list should be empty, but it's with length of %d", l) + } s.Cursor.Y = canvasHeight - 1 - s.GoDown() + change = s.GoDown() + if change != nil { + t.Errorf("change should be nil") + } if s.Cursor.Y != canvasHeight-1 { t.Errorf("s.Cursor.Y should be %d but it's %d", canvasHeight-1, s.Cursor.Y) } @@ -113,18 +140,32 @@ func TestStateGoLeft(t *testing.T) { x := s.Cursor.X y := s.Cursor.Y - s.GoLeft() + change := s.GoLeft() if s.Cursor.X != x-1 { - t.Errorf("s.Cursor.Y should be %d but it's %d", x-1, s.Cursor.X) + t.Errorf("s.Cursor.X should be %d but it's %d", x-1, s.Cursor.X) } if s.Cursor.Y != y { - t.Errorf("s.Cursor.X should be %d but it's %d", y, s.Cursor.Y) + t.Errorf("s.Cursor.Y should be %d but it's %d", y, s.Cursor.Y) + } + if change.Cursor.X != x-1 { + t.Errorf("change.Cursor.X should be %d but it's %d", x-1, change.Cursor.X) + } + + if change.Cursor.Y != y { + t.Errorf("change.Cursor.Y should be %d but it's %d", y, change.Cursor.Y) + } + + if l := undoList.len(); l > 0 { + t.Errorf("undo list should be empty, but it's with length of %d", l) } s.Cursor.X = 0 - s.GoLeft() + change = s.GoLeft() + if change != nil { + t.Errorf("change should be nil") + } if s.Cursor.X != 0 { - t.Errorf("s.Cursor.X should be 0 but it's %d", s.Cursor.X) + t.Errorf("s.Cursor.X should be %d but it's %d", 0, s.Cursor.X) } if s.Cursor.Y != y { t.Errorf("s.Cursor.Y should be %d but it's %d", y, s.Cursor.Y) @@ -137,16 +178,30 @@ func TestStateGoRight(t *testing.T) { x := s.Cursor.X y := s.Cursor.Y - s.GoRight() + change := s.GoRight() if s.Cursor.X != x+1 { t.Errorf("s.Cursor.X should be %d but it's %d", x+1, s.Cursor.X) } if s.Cursor.Y != y { t.Errorf("s.Cursor.Y should be %d but it's %d", y, s.Cursor.Y) } + if change.Cursor.X != x+1 { + t.Errorf("change.Cursor.X should be %d but it's %d", x+1, change.Cursor.X) + } + + if change.Cursor.Y != y { + t.Errorf("change.Cursor.Y should be %d but it's %d", y, change.Cursor.Y) + } + + if l := undoList.len(); l > 0 { + t.Errorf("undo list should be empty, but it's with length of %d", l) + } s.Cursor.X = canvasWidth - 1 - s.GoRight() + change = s.GoRight() + if change != nil { + t.Errorf("change should be nil") + } if s.Cursor.X != canvasWidth-1 { t.Errorf("s.Cursor.X should be %d but it's %d", canvasWidth-1, s.Cursor.X) } @@ -163,16 +218,35 @@ func TestStatePaintPixel(t *testing.T) { } res := s.PaintPixel() - if !res { - t.Error("should return true") + if res == nil { + t.Fatal("should return a change") } if s.Canvas[s.Cursor.Y][s.Cursor.X] != 0xFFFFFF { - t.Errorf("s.Canvas[%d][%d] should be set, but it's not", s.Cursor.Y, s.Cursor.X) + t.Fatalf("s.Canvas[%d][%d] should be set, but it's not", s.Cursor.Y, s.Cursor.X) + } + + if len(res.Pixels) != 1 { + t.Errorf("res.Pixels should be with len of 1, but it is %d.", len(res.Pixels)) + } + pixel := res.Pixels[0] + if pixel.X != s.Cursor.X || pixel.Y != s.Cursor.Y || pixel.Color != s.Pen.Color { + t.Errorf("x should be %d, y should be %d and color should be #%06x; but pixel is %#v", s.Cursor.X, s.Cursor.Y, s.Pen.Color, pixel) + } + if l := undoList.len(); l != 1 { + t.Errorf("undo list should be with len of 1, but it's with length of %d", l) + } + + change := undoList.pop() + if l := len(change.Pixels); l != 1 { + t.Fatalf("pixel list should be with len of 1, but it's with length of %d", l) + } + if change.Pixels[0].X != res.Pixels[0].X || change.Pixels[0].Y != res.Pixels[0].Y || change.Pixels[0].Color != 0 { + t.Errorf("x should be %d, y should be %d and color should be #%06x; but pixel is %#v", s.Cursor.X, s.Cursor.Y, s.Pen.Color, change) } res = s.PaintPixel() - if res { - t.Error("should return false") + if res != nil { + t.Fatal("should not return a change") } if s.Canvas[s.Cursor.Y][s.Cursor.X] != 0xFFFFFF { t.Errorf("s.Canvas[%d][%d] should be set, but it's not", s.Cursor.Y, s.Cursor.X) @@ -180,16 +254,19 @@ func TestStatePaintPixel(t *testing.T) { s.Cursor.X = canvasWidth res = s.PaintPixel() - if res { - t.Error("should return false") + if res != nil { + t.Fatal("should not return a change") + } + if undoList.len() != 0 { + t.Error("undo list should be empty") } s.Cursor.X = canvasWidth / 2 s.Cursor.Y = canvasHeight res = s.PaintPixel() - if res { - t.Error("should return false") + if res != nil { + t.Fatal("should not return a change") } } @@ -235,3 +312,32 @@ func TestState_getColor(t *testing.T) { t.Errorf("color should be 0x12346 but it's 0x%x", c) } } + +func TestState_Undo(t *testing.T) { + s := NewState(canvasWidth, canvasHeight) + if s.Canvas[3][4] != 0 { + t.Error("s.Canvas[3][4] should be 0") + } + if s.Canvas[10][20] != 0 { + t.Error("s.Canvas[3][4] should be 0") + } + + c := &Change{ + Pixels: []Pixel{{ + X: 4, Y: 3, Color: 0x112233, + }, { + X: 20, Y: 10, Color: 0x112233, + }}, + } + + undoList.push(c) + + s.Undo() + if s.Canvas[3][4] != 0x112233 { + t.Error("s.Canvas[3][4] should be 0x112233") + } + if s.Canvas[10][20] != 0x112233 { + t.Error("s.Canvas[3][4] should be 0x112233") + } + +} diff --git a/webapp/index.gohtml b/webapp/index.gohtml index 0fe8201..26a39e7 100644 --- a/webapp/index.gohtml +++ b/webapp/index.gohtml @@ -14,8 +14,8 @@ } td { - text-align:center; - vertical-align:middle + text-align: center; + vertical-align: middle } table { @@ -65,9 +65,9 @@

Tool: - + - +

@@ -84,6 +84,9 @@

+

+ +

@@ -98,10 +101,14 @@