doodle/pkg/drawtool/history.go
Noah Petherbridge c8620f871e Drawing Strokes and Undo/Redo Functionality
* Add new pkg/drawtool with utilities to abstract away drawing actions
  into Strokes and track undo/redo History for them.
* The freehand Pencil tool in EditorMode has been refactored to create a
  Stroke of Shape=Freehand and queue up its world pixels there instead
  of directly modifying the level chunker in real time. When the mouse
  button is released, the freehand Stroke is committed to the level
  chunker and added to the UndoHistory.
* UndoHistory is (temporarily) stored with the level.Level so it can
  survive trips to PlayScene and back, but is not stored as JSON on
  disk.
* Ctrl-Z and Ctrl-Y in EditorMode for undo and redo, respectively.
2019-07-03 16:25:23 -07:00

132 lines
3.0 KiB
Go

package drawtool
// History manages a history of Strokes added to a drawing.
type History struct {
limit int
head *HistoryElement // oldest history element, top of linked list
tail *HistoryElement // newest element added to history
}
// HistoryElement is a doubly linked list of stroke history.
type HistoryElement struct {
stroke *Stroke
next *HistoryElement
previous *HistoryElement
}
// NewHistory initializes a History list.
func NewHistory(limit int) *History {
return &History{
limit: limit,
}
}
// Reset clears the history.
func (h *History) Reset() {
h.head = nil
h.tail = nil
}
// Size returns the current size of the history list.
func (h *History) Size() int {
var (
size int
node = h.head
)
for node != nil {
size++
node = node.next
}
return size
}
// Latest returns the tail of the history (the most recent stroke). If you had
// recently called Undo, the latest stroke may still have a 'next' stroke.
// Returns nil if there was no stroke in history.
func (h *History) Latest() *Stroke {
if h.tail == nil {
return nil
}
return h.tail.stroke
}
// Oldest returns the head of the history (the earliest stroke added). If the
// history size limit had been reached, the oldest stroke will creep along
// forward and not necessarily be the FIRST EVER stroke added.
func (h *History) Oldest() *Stroke {
if h.head == nil {
return nil
}
return h.head.stroke
}
// AddStroke adds a stroke to the history, becoming the new tail at the end
// of the history data.
func (h *History) AddStroke(s *Stroke) {
var (
elem = &HistoryElement{
stroke: s,
}
tail = h.tail
)
// Make the current tail point to this one.
if tail != nil {
tail.next = elem
elem.previous = tail
}
// First stroke of the history? Make it the head of the linked list.
if h.head == nil {
h.head = elem
}
h.tail = elem
// Have we reached the history storage limit?
var size = h.Size()
if size > h.limit {
var node = h.tail
for i := 0; i < h.limit-1; i++ {
if node.previous == nil {
break
}
node = node.previous
}
h.head = node
h.head.previous = nil
}
}
// Undo steps back a step in the history. This sets the current tail to point
// to the "tail - 1" element, but doesn't change the link of that element to
// its future value yet; so that you can Redo it. But if you add a new stroke
// from this state, it will overwrite the tail.next and invalidate the old
// history that came after, starting a new branch of history from that point on.
//
// Returns false if the undo failed (no earlier node to move to).
func (h *History) Undo() bool {
if h.tail == nil {
return false
}
// if h.tail.previous == nil {
// return false
// }
h.tail = h.tail.previous
return true
}
// Redo advances forwards after a recent Undo. Note that if you added new strokes
// after an Undo, the new tail has no next node to move to and Redo returns false.
func (h *History) Redo() bool {
if h.tail == nil || h.tail.next == nil {
return false
}
h.tail = h.tail.next
return true
}