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 @@