[PythonCAD] Update on undo/redo work and other goings on

Art Haas ahaas at airmail.net
Thu Feb 5 13:42:18 EST 2004


Hi.

So if you're getting the code with Subversion and update your repo,
you'll find that a huge number of changes have just appeared affecting
the entities like points, segments, circles, etc. These changes are
part of my means of adding undo/redo abilities to PythonCAD, and they
also reflect a large design change to the core of the program itself.

Here's the problem the old design faced when doing undo/redo stuff -
there was no good means of know just when an entity changed without
trying to keep track of this information at every place that made a
change. This shortcoming could have been dealt with in the code now by
doing something like the following 

info = get_entity_to_be_changed_info()
save_this_info_somewhere(info)
change_the_entity(val1, val2, ...)

This approach just wouldn't work, and it would make scripting in
PythonCAD unbearable. A better approach is needed, and I think what I've
added is a good start.

Anyone doing programming with QT deals with the Signals and Slots
approach Trolltech built that toolkit around. An object can send a
message (signal) to another object by invoking a method (slot) in that
object once the two have been connected with a connect() call. GTK
has a similar approach, but I think the QT example is better as it
maps more easily onto Python's object oriented features.

A new base class called Messenger was added to the PythonCAD code, and
this class provides connect() and sendMessage() method that do the same
thing as the QT signals/slots code. One object can send a message to
some other object if the two have been connected. Let me illustrate
how I've changed things with a bit of code.

Here's the old code for setting the 'x' value of a Point.  In class '_Point' 
there is this ...

    def setx(self, val):
        _v = val
        if not isinstance(_v, float):
            _v = float(val)
        self.__x = _v

... and in the class 'Point' - a subclass of '_Point' - there is the
following ...

    def setx(self, x):
        _Point.setx(self, x)
        self.modified()

So, the best we could get from the old class is testing if the instance
was modified, but there is no way to know just what the old value was
before it was changed without getting that value before the change was
made.

Here's the code from the new 'Point' class. First, there is no longer a
'_Point' parent class, as I've tried to simplify the class hierarchy and
not have classes with multiple base classes.

    def setx(self, val):
        if self.isLocked():
            raise RuntimeError, "Coordinate change not allowed - object locked."
        _v = val
        if not isinstance(_v, float):
            _v = float(val)
        _x = self.__x
        if abs(_x - _v) > 1e-10:
            self.__x = _v
            self.sendMessage('moved', _x, self.__y)
            self.modified()

First, notice that there is a new self.isLocked() call. When simpilfying
the base classes of the drawing entities, I created a new class called
Entity that will allow you to set an entity as being locked - no
changing it until you unlock it - or hidden. I've had people suggest to
me that both features would be good to have, and now they are in there.
So, back in the code, we check that the new value is a float value, then
we compare it to the existing value. If the two values differ, then
the point gets the new value assigned to it's 'x' value, and the point
sends out a message called 'moved' and passes the old 'x' value and the
current 'y' value as arguments. Now, lets suppose that there is some
other object out there that is interested in keeping an eye on what a
Point instance is doing. By using a connect() call, this object would
then be notified by the point that it has been moved and here are the
old coordinates of the point. So, we've now got some means of finding
out when an entity changes and what value(s) it posssed before the
change was made.

It just so happens that there _is_ an entity now in PythonCAD that does
track when a point moves. One of the goals I had for this year was to
improve the entity storage data structures used in the program. The old
approach was based on a sorted list, and needed entities that would be
stored in these lists to implement a __cmp__ method so the list could be
kept sorted. The code now uses quadtrees for storing most of the
entities in a drawing, and this approach should make for faster
searches of entities.

The quadtree that stores Points is a PointQuadtree, and when a Point
instance is added into a PointQuadtree instance, the Point instance
makes this call:

    obj.connect('moved', self, PointQuadtree.movePoint)

The Point instance is the variable 'obj', and the PointQuadtree is
'self', So, when the Point sends out a 'moved' message, the
PointQuadtree receives it and invokes the movePoint() method.

    def movePoint(self, obj, *args):
        ... nifty code here ...

When an object creates a connection with connect(), the first argument
to connect is a string giving the message, the second argument is the
object that will receive the message, and the third argument is a method
that the receiving object can call. In this case, the PointQuadtree
instance will call its 'movePoint' method. The method itself will
recieve as its first argument the object making the call, in our case
the point that moved, then there are a variable number of arguments.
Again, in our 'moved' message case, there will be two arguments.
See the code in 'Generic/point.py' for the details about the nifty code.

The code changes in the last couple of weeks are really major, and I am
sure that there glitches in what I've commited, so if you are accessing
the Subversion repo things will be a little unstable for a while. The
changes themself are working fairly well here, and I really like the way
this messaging stuff is working, as adding undo/redo stuff can now be
done by creating objects that utilize this message passing to save and
store info about an object. In the case of a Point, an object would need
to connect to the 'moved' message, and then it could store the old
position of the point, and as the point itself is an argument, it can
retrieve the points current position and store that as well.

I'll be working on robustifying the new code over the next few weeks, as
well as adding some to the message handling code. I think the ability to
ignore a message may be useful. Also I'll start cooking up some classes
that keep track of entity history for the undo/redo stuff.

If you pull the code from Subversion, please test it and let me know of
any problems you encounter. Having more eyes go over the code would
certainly be good. I'll be adding some stuff to the website regarding
the messaging approach the code is now using over the next few weeks.

Art

-- 
Man once surrendering his reason, has no remaining guard against absurdities
the most monstrous, and like a ship without rudder, is the sport of every wind.

-Thomas Jefferson to James Smith, 1822



More information about the PythonCAD mailing list