6

I'm writing a painter program, in which I want to implement the undo and redo of the basic operations like drawing lines, drawing ellipse, and so on.
Is there any good methods to do that? What attributes should be included in one operation?
EDIT:
Actually I know the Command pattern, but what I want to figure out is that what data should be recorded in a Command object, in order to complete the undo.

Philip Zhang
  • 627
  • 8
  • 17
  • 11
    FWIW, Sean Parent discussed Photoshop's in [his talk](http://channel9.msdn.com/Events/GoingNative/2013/Inheritance-Is-The-Base-Class-of-Evil). – chris Jul 21 '14 at 16:14
  • You could create inverse of the basic operations (ex: draw line -> erase line), and maintain and stack of last operations (you could if you want discard old operations. Or you could storage a diff of the drawing canvas as snapshot (try to minimize the storage size needed by this) – NetVipeC Jul 21 '14 at 16:17
  • Memento pattern mentioned isn't that bad addition to Command working on a set of changed objects. It's just the answer is too narrow as it is. – πάντα ῥεῖ Jul 21 '14 at 16:47
  • @Chris that is a pretty deep talk to just cover "undo redo" :) – Yakk - Adam Nevraumont Jul 21 '14 at 17:49
  • @Yakk, Personally, I really like the technique to allow value semantics with a container of a polymorphic class. – chris Jul 21 '14 at 17:51
  • @chris true, type erased pImpl polymorphic value semantics is really neat. But it can blow people's minds a touch. – Yakk - Adam Nevraumont Jul 21 '14 at 18:10
  • I have already implemented the functionality according to @Yakk, thanks for all your guys. – Philip Zhang Aug 04 '14 at 12:02

4 Answers4

10

So undoing/redoing in a raster application is a bit tricky.

First, spend 25 minutes watching this talk (via @chris). It may blow your mind, and it is rather quick. Then keep reading.

The easiest method is to simply make a copy of the raster canvas before each operation. Store a list of these in the undo/redo buffer, and you can happily go forward and back. (You may also want to store tool state).

The downside to this is that it is large and expensive. There are a number of ways to mitigate this.

First, you can tile the image (which you should anyhow), and use copy-on-write. Now tiles that are not modified are shared over the undo list.

Second, instead of a full raster, record the parameters required to recreate the effect. So long as you have a reasonably nearby (in terms of time to rebuild from) "before" image you can rebuild from, this can be pretty fast.

Third, record both that and information required to "erase" the effect. This can sometimes be cheaper than a copy of all of the tiles you have touched (and a copy of the all of the tiles you have touched is one way to implement this).

You can make your undo/redo stack be a hybrid of this. For a stage to be reconstructable, you either need a raster copy, or a previous raster copy and a set of forward operations you can repeat to regenerate it, or a future raster copy and a set of backward operations to you can repeat to regenerate it.

On top of this, you have the possibility to shove said undo information out of memory and save it to disk.

In some cases, you also want to be able to collapse multiple steps into one undo/redo step. This can let you keep fine-grained undo/redo for operations "recently", while still being able to undo all the way back to "you opened the file".

Generally, you care about reaching an undo/redo step from the current step (which means you only need forward difference information for redo steps, and backwards for undo), but that can add complications to managing the undo/redo stack (sometimes it is easier to just store both).

There is also the question of "do you want to save the undo/redo operation data when you save the file" or not (should it be serializable?). Do you want to support undo/redo trees or just a linear history? (Ie, if you undo 3 times, then draw a pixel, should you be able to "get back" to the original history, or is it destroyed by the pixel draw?)

However, practically you start with simple raster copies of the canvas. This is the standard against which any other implementation has to be measured, so for unit testing purposes "full raster" undo/redo capabilities needs to be available anyhow. Add in "total memory spent on undo/redo" settings and to-disk serialization (and "total disk space on undo/redo") and collapsing rules next (because these are easy steps). Then tile your raster image and implement copy-on-write tiles, and you'll be at 90% effectiveness.

After that, start worrying about making optimized undo/redo for some tools for which tile-based raster information can be massively beaten.

Now, go back to the video at the start of the post. Watch it again. Use pause to stop the video at each of the code points. Type the code in yourself. Try to understand what you wrote. If you don't understand what you wrote, go off and learn about it. You'll be a better programmer, and if you make it to the end of the talk you will write a better raster painting application with much more powerful undo/redo.

The basics of that implementation is a copy on write type erasure system for document data, using type erasure to allow for value semantics. It does not immediately generalize to a document model, but even if the only thing you did was use something like it for your tile system (with use_count()==1 resulting in you breaking const for copy-on-write) you'll be ahead of the game.


Things to watch out for.

Unused code is buggy code. So you don't want your undo/redo system to use complicated code paths that are off of the main operation of your program.

If you are saving your undo/redo operations into the file, how often do you take a command, convert it to an undo/redo command, serialize that, deserialize that, execute the command, and determine the result is solid?

You really, really need unit tests. And the most of the code that does this is going to be unique to exactly that workflow.

On the other hand, if undo/redo is just swapping which tiles are active, and every draw operation does a copy-on-write (hence swapping tiles), the complicated part of the code for undo/redo is run all of the time.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
2

You can use the Memento pattern for implementing undo/redo:

The memento pattern is a software design pattern that provides the ability to restore an object to its previous state (undo via rollback).

The memento pattern is implemented with three objects: the originator, a caretaker and a memento. The originator is some object that has an internal state. The caretaker is going to do something to the originator, but wants to be able to undo the change. The caretaker first asks the originator for a memento object. Then it does whatever operation (or sequence of operations) it was going to do. To roll back to the state before the operations, it returns the memento object to the originator. The memento object itself is an opaque object (one which the caretaker cannot, or should not, change). When using this pattern, care should be taken if the originator may change other objects or resources - the memento pattern operates on a single object.

JBentley
  • 6,099
  • 5
  • 37
  • 72
Paul Evans
  • 27,315
  • 3
  • 37
  • 54
1

The way I've done this is to have each operation call the undo manager at the start and end of the operation (for bookkeeping) and then as it paints it "checks out" or asks for write access to rectangular ranges of pixels (typically tiles of 128x128 or 256x256 pixels) as it goes. The undo manager makes copies of the original pixels and replaces the active copy with the modified ones. Then on an undo it replaces the original back, and on a redo it replaces the modified back. Using tiles is a good middle ground between tracking individual pixels and backing up the entire image per operation.

Dithermaster
  • 6,223
  • 1
  • 12
  • 20
0

You should read about Command design pattern http://en.wikipedia.org/wiki/Command_pattern - this is exactly what you need to implement multi-level undo. Basically you do not execute modifications directly, but you create objects implementing Do() and Undo() methods. Then it is easy to implement the code making and undoing changes in one place. Instead of changing something, you create an object, call its Do() method, and add it to undo queue. Then for undo you call Undo() methods in the objects in the queue.

Wojtek Surowka
  • 20,535
  • 4
  • 44
  • 51