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.
If “UNDO” string is encountered, pop the top element from Undo stack and push it to Redo stack. If “REDO” string is encountered, pop the top element of Redo stack and push it into the Undo stack. If “READ” string is encountered, print all the elements of the Undo stack in reverse order.
Click the pencil button and draw over the canvas. Use undo and redo buttons to navigate through states.
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.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With