# # Copyright (c) 2002, 2003, 2004, 2005, 2006 Art Haas # # This file is part of PythonCAD. # # PythonCAD is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # PythonCAD is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with PythonCAD; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # # classes for line segments # from __future__ import generators import math from PythonCAD.Generic import graphicobject from PythonCAD.Generic import tolerance from PythonCAD.Generic import style from PythonCAD.Generic import linetype from PythonCAD.Generic import color from PythonCAD.Generic import point from PythonCAD.Generic import util from PythonCAD.Generic import quadtree class Segment(graphicobject.GraphicObject): """A class representing a line segment. A Segment has two attributes. p1: A Point object representing the first end point p2: A Point object representing the second end point A Segment has the following methods: getEndpoints(): Return the two endpoints of the Segment. {get/set}P1: Get/Set the Segment first endpoint. {get/set}P2: Get/Set the Segment second endpoint. move(): Move the Segment. length(): Get the Segment length. getCoefficients(): Return the segment as ax + by + c = 0 getProjection(): Project a coordinate on to the Segment mapCoords(): Test if a coordinate pair is within some distance to a Segment. inRegion(): Test if the Segment is visible in some area. clone(): Make an identical copy of a Segment. """ __messages = { 'moved' : True, 'endpoint_changed' : True } __defstyle = None def __init__(self, p1, p2, st=None, lt=None, col=None, th=None, **kw): """Initialize a Segment object. Segment(p1, p2[, st, lt, col, th]) p1: Segment first endpoint - may be a Point or a two-item tuple of floats. p2: Segment second endpoint - may be a Point or a two-item tuple of floats. The following arguments are optional: st: A Style object lt: A Linetype object that overrides the linetype in the Style. col: A Color object that overrides the color in the Style. th: A float that overrides the line thickness in the Style. """ _p1 = p1 if not isinstance(_p1, point.Point): _p1 = point.Point(p1) _p2 = p2 if not isinstance(_p2, point.Point): _p2 = point.Point(p2) if _p1 is _p2: raise ValueError, "Segments cannot have identical endpoints." _st = st if _st is None: _st = self.getDefaultStyle() super(Segment, self).__init__(_st, lt, col, th, **kw) self.__p1 = _p1 _p1.connect('moved', self.__movePoint) _p1.connect('change_pending', self.__pointChangePending) _p1.connect('change_complete', self.__pointChangeComplete) _p1.storeUser(self) self.__p2 = _p2 _p2.connect('moved', self.__movePoint) _p2.connect('change_pending', self.__pointChangePending) _p2.connect('change_complete', self.__pointChangeComplete) _p2.storeUser(self) def __str__(self): return "Segment: %s to %s" % (self.__p1, self.__p2) def __eq__(self, obj): """Compare a Segment to another for equality. """ if not isinstance(obj, Segment): return False if obj is self: return True _sp1 = self.__p1 _sp2 = self.__p2 _op1, _op2 = obj.getEndpoints() return (((_sp1 == _op1) and (_sp2 == _op2)) or ((_sp1 == _op2) and (_sp2 == _op1))) def __ne__(self, obj): """Compare a Segment to another for inequality. """ if not isinstance(obj, Segment): return True if obj is self: return False _sp1 = self.__p1 _sp2 = self.__p2 _op1, _op2 = obj.getEndpoints() return (((_sp1 != _op1) or (_sp2 != _op2)) and ((_sp1 != _op2) or (_sp2 != _op1))) def getDefaultStyle(cls): if cls.__defstyle is None: _s = style.Style(u'Default Segment Style', linetype.Linetype(u'Solid', None), color.Color(0xffffff), 1.0) cls.__defstyle = _s return cls.__defstyle getDefaultStyle = classmethod(getDefaultStyle) def setDefaultStyle(cls, s): if not isinstance(s, style.Style): raise TypeError, "Invalid style: " + `type(s)` cls.__defstyle = s setDefaultStyle = classmethod(setDefaultStyle) def finish(self): self.__p1.disconnect(self) self.__p1.freeUser(self) self.__p2.disconnect(self) self.__p2.freeUser(self) self.__p1 = self.__p2 = None super(Segment, self).finish() def setStyle(self, s): """Set the Style of the Segment setStyle(s) This method extends GraphicObject::setStyle(). """ _s = s if _s is None: _s = self.getDefaultStyle() super(Segment, self).setStyle(_s) def getValues(self): """Return values comprising the Segment. getValues() This method extends the GraphicObject::getValues() method. """ _data = super(Segment, self).getValues() _data.setValue('type', 'segment') _data.setValue('p1', self.__p1.getID()) _data.setValue('p2', self.__p2.getID()) return _data def getEndpoints(self): """Get the endpoints of the Segment. getEndpoints() This function returns a tuple containing the two Point objects that are the endpoints of the segment. """ return self.__p1, self.__p2 def getP1(self): """Return the first endpoint Point of the Segment. getP1() """ return self.__p1 def setP1(self, p): """Set the first endpoint Point of the Segment. setP1(p) """ if self.isLocked(): raise RuntimeError, "Setting endpoint not allowed - object locked." if not isinstance(p, point.Point): raise TypeError, "Invalid P1 endpoint type: " + `type(p)` if p is self.__p2: raise ValueError, "Segments cannot have identical endpoints." _pt = self.__p1 if _pt is not p: _pt.disconnect(self) _pt.freeUser(self) self.startChange('endpoint_changed') self.__p1 = p self.endChange('endpoint_changed') self.sendMessage('endpoint_changed', _pt, p) p.storeUser(self) p.connect('moved', self.__movePoint) p.connect('change_pending', self.__pointChangePending) p.connect('change_complete', self.__pointChangeComplete) if abs(_pt.x - p.x) > 1e-10 or abs(_pt.y - p.y) > 1e-10: _x, _y = self.__p2.getCoords() self.sendMessage('moved', _pt.x, _pt.y, _x, _y) self.modified() p1 = property(getP1, setP1, None, "First endpoint of the Segment.") def getP2(self): """Return the second endpoint Point of the Segment. getP2() """ return self.__p2 def setP2(self, p): """Set the second endpoint Point of the Segment. setP2(p) """ if self.isLocked(): raise RuntimeError, "Setting endpoint not allowed - object locked." if not isinstance(p, point.Point): raise TypeError, "Invalid P2 endpoint type: " + `type(p)` if p is self.__p1: raise ValueError, "Segments cannot have identical endpoints." _pt = self.__p2 if _pt is not p: _pt.disconnect(self) _pt.freeUser(self) self.startChange('endpoint_changed') self.__p2 = p self.endChange('endpoint_changed') self.sendMessage('endpoint_changed', _pt, p) p.storeUser(self) p.connect('moved', self.__movePoint) p.connect('change_pending', self.__pointChangePending) p.connect('change_complete', self.__pointChangeComplete) if abs(_pt.x - p.x) > 1e-10 or abs(_pt.y - p.y) > 1e-10: _x, _y = self.__p1.getCoords() self.sendMessage('moved', _x, _y, _pt.x, _pt.y) self.modified() p2 = property(getP2, setP2, None, "Second endpoint of the Segment.") def move(self, dx, dy): """Move a Segment. move(dx, dy) The first argument gives the x-coordinate displacement, and the second gives the y-coordinate displacement. Both values should be floats. """ if self.isLocked() or self.__p1.isLocked() or self.__p2.isLocked(): raise RuntimeError, "Moving Segment not allowed - object locked." _dx = util.get_float(dx) _dy = util.get_float(dy) if abs(_dx) > 1e-10 or abs(_dy) > 1e-10: _x1, _y1 = self.__p1.getCoords() _x2, _y2 = self.__p2.getCoords() self.ignore('moved') try: self.__p1.move(_dx, _dy) self.__p2.move(_dx, _dy) finally: self.receive('moved') self.sendMessage('moved', _x1, _y1, _x2, _y2) def length(self): """Return the length of the Segment. length() """ return self.__p1 - self.__p2 def getCoefficients(self): """Express the line segment as a function ax + by + c = 0 getCoefficients() This method returns a tuple of three floats: (a, b, c) """ _x1, _y1 = self.__p1.getCoords() _x2, _y2 = self.__p2.getCoords() _a = _y2 - _y1 _b = _x1 - _x2 _c = (_x2 * _y1) - (_x1 * _y2) return _a, _b, _c def getProjection(self, x, y): """Find the projection point of some coordinates on the Segment. getProjection(x, y) Arguments 'x' and 'y' should be float values. """ _x = util.get_float(x) _y = util.get_float(y) _x1, _y1 = self.__p1.getCoords() _x2, _y2 = self.__p2.getCoords() _sqlen = pow((_x2 - _x1), 2) + pow((_y2 - _y1), 2) if _sqlen < 1e-10: # coincident points return None _rn = ((_x - _x1) * (_x2 - _x1)) + ((_y - _y1) * (_y2 - _y1)) _r = _rn/_sqlen if _r < 0.0 or _r > 1.0: return None _px = _x1 + _r * (_x2 - _x1) _py = _y1 + _r * (_y2 - _y1) return _px, _py def mapCoords(self, x, y, tol=tolerance.TOL): """Return the nearest Point on the Segment to a coordinate pair. mapCoords(x, y[, tol]) The function has two required arguments: x: A Float value giving the 'x' coordinate y: A Float value giving the 'y' coordinate There is a single optional argument: tol: A float value equal or greater than 0. This function is used to map a possibly near-by coordinate pair to an actual Point on the Segment. If the distance between the actual Point and the coordinates used as an argument is less than the tolerance, the actual Point is returned. Otherwise, this function returns None. """ _x = util.get_float(x) _y = util.get_float(y) _t = tolerance.toltest(tol) _x1, _y1 = self.__p1.getCoords() _x2, _y2 = self.__p2.getCoords() return util.map_coords(_x, _y, _x1, _y1, _x2, _y2, _t) def inRegion(self, xmin, ymin, xmax, ymax, fully=False): """Return whether or not a Segment exists within a region. inRegion(xmin, ymin, xmax, ymax[, fully]) The four arguments define the boundary of an area, and the method returns True if the Segment lies within that area. If the optional argument fully is used and is True, then both endpoints of the Segment must lie within the boundary. Otherwise, the method returns False. """ _xmin = util.get_float(xmin) _ymin = util.get_float(ymin) _xmax = util.get_float(xmax) if _xmax < _xmin: raise ValueError, "Illegal values: xmax < xmin" _ymax = util.get_float(ymax) if _ymax < _ymin: raise ValueError, "Illegal values: ymax < ymin" util.test_boolean(fully) _x1, _y1 = self.__p1.getCoords() _x2, _y2 = self.__p2.getCoords() _pxmin = min(_x1, _x2) _pymin = min(_y1, _y2) _pxmax = max(_x1, _x2) _pymax = max(_y1, _y2) if ((_pxmax < _xmin) or (_pymax < _ymin) or (_pxmin > _xmax) or (_pymin > _ymax)): return False if fully: if ((_pxmin > _xmin) and (_pymin > _ymin) and (_pxmax < _xmax) and (_pymax < _ymax)): return True return False return util.in_region(_x1, _y1, _x2, _y2, _xmin, _ymin, _xmax, _ymax) def clipToRegion(self, xmin, ymin, xmax, ymax): """Clip the Segment using the Liang-Barsky Algorithm. clipToRegion(xmin, ymin, xmax, ymax) """ _xmin = util.get_float(xmin) _ymin = util.get_float(ymin) _xmax = util.get_float(xmax) if _xmax < _xmin: raise ValueError, "Illegal values: xmax < xmin" _ymax = util.get_float(ymax) if _ymax < _ymin: raise ValueError, "Illegal values: ymax < ymin" _x1, _y1 = self.__p1.getCoords() _x2, _y2 = self.__p2.getCoords() # # simple tests to reject line # if ((max(_x1, _x2) < _xmin) or (max(_y1, _y2) < _ymin) or (min(_x1, _x2) > _xmax) or (min(_y1, _y2) > _ymax)): return None # # simple tests to accept line # _coords = None if (_xmin < _x1 < _xmax and _xmin < _x2 < _xmax and _ymin < _y1 < _ymax and _ymin < _y2 < _ymax): _coords = (_x1, _y1, _x2, _y2) else: # # the Segment can be parameterized as # # x = u * (x2 - x1) + x1 # y = u * (y2 - y1) + y1 # # for u = 0, x => x1, y => y1 # for u = 1, x => x2, y => y2 # # The following is the Liang-Barsky Algorithm # for segment clipping # _dx = _x2 - _x1 _dy = _y2 - _y1 _P = [-_dx, _dx, -_dy, _dy] _q = [(_x1 - _xmin), (_xmax - _x1), (_y1 - _ymin), (_ymax - _y1)] _u1 = 0.0 _u2 = 1.0 _valid = True for _i in range(4): _pi = _P[_i] _qi = _q[_i] if abs(_pi) < 1e-10: if _qi < 0.0: _valid = False break else: _r = _qi/_pi if _pi < 0.0: if _r > _u2: _valid = False break if _r > _u1: _u1 = _r else: if _r < _u1: _valid = False break if _r < _u2: _u2 = _r if _valid: _coords = (((_u1 * _dx) + _x1), ((_u1 * _dy) + _y1), ((_u2 * _dx) + _x1), ((_u2 * _dy) + _y1)) return _coords def __pointChangePending(self, p, *args): _alen = len(args) if _alen < 1: raise ValueError, "Invalid argument count: %d" % _alen if args[0] == 'moved': self.startChange('moved') def __pointChangeComplete(self, p, *args): _alen = len(args) if _alen < 1: raise ValueError, "Invalid argument count: %d" % _alen if args[0] == 'moved': self.endChange('moved') def __movePoint(self, p, *args): _alen = len(args) if _alen < 2: raise ValueError, "Invalid argument count: %d" % _alen _x = util.get_float(args[0]) _y = util.get_float(args[1]) if p is self.__p1: _x1 = _x _y1 = _y _x2, _y2 = self.__p2.getCoords() elif p is self.__p2: _x1, _y1 = self.__p1.getCoords() _x2 = _x _y2 = _y else: raise ValueError, "Unexpected Segment endpoint: " + `p` self.sendMessage('moved', _x1, _y1, _x2, _y2) def clone(self): """Create an identical copy of a Segment. clone() """ _cp1 = self.__p1.clone() _cp2 = self.__p2.clone() _st = self.getStyle() _lt = self.getLinetype() _col = self.getColor() _th = self.getThickness() return Segment(_cp1, _cp2, _st, _lt, _col, _th) def sendsMessage(self, m): if m in Segment.__messages: return True return super(Segment, self).sendsMessage(m) # # Quadtree Segment storage # class SegmentQuadtree(quadtree.Quadtree): def __init__(self): super(SegmentQuadtree, self).__init__() def getNodes(self, *args): _alen = len(args) if _alen != 4: raise ValueError, "Expected 4 arguments, got %d" % _alen _x1 = util.get_float(args[0]) _y1 = util.get_float(args[1]) _x2 = util.get_float(args[2]) _y2 = util.get_float(args[3]) _sxmin = min(_x1, _x2) _sxmax = max(_x1, _x2) _symin = min(_y1, _y2) _symax = max(_y1, _y2) _nodes = [self.getTreeRoot()] while len(_nodes): _node = _nodes.pop() _xmin, _ymin, _xmax, _ymax = _node.getBoundary() if ((_sxmin > _xmax) or (_sxmax < _xmin) or (_symin > _ymax) or (_symax < _ymin)): continue if _node.hasSubnodes(): _xmid = (_xmin + _xmax)/2.0 _ymid = (_ymin + _ymax)/2.0 _ne = _nw = _sw = _se = True if _sxmax < _xmid: # seg on left side _ne = _se = False if _sxmin > _xmid: # seg on right side _nw = _sw = False if _symax < _ymid: # seg below _nw = _ne = False if _symin > _ymid: # seg above _sw = _se = False if _ne: _nodes.append(_node.getSubnode(quadtree.QTreeNode.NENODE)) if _nw: _nodes.append(_node.getSubnode(quadtree.QTreeNode.NWNODE)) if _sw: _nodes.append(_node.getSubnode(quadtree.QTreeNode.SWNODE)) if _se: _nodes.append(_node.getSubnode(quadtree.QTreeNode.SENODE)) else: yield _node def addObject(self, obj): if not isinstance(obj, Segment): raise TypeError, "Invalid Segment object: " + `obj` if obj in self: return _p1, _p2 = obj.getEndpoints() _x1, _y1 = _p1.getCoords() _x2, _y2 = _p2.getCoords() _bounds = self.getTreeRoot().getBoundary() _xmin = _ymin = _xmax = _ymax = None _sxmin = min(_x1, _x2) _sxmax = max(_x1, _x2) _symin = min(_y1, _y2) _symax = max(_y1, _y2) _resize = False if _bounds is None: # first node in tree _resize = True _xmin = _sxmin - 1.0 _ymin = _symin - 1.0 _xmax = _sxmax + 1.0 _ymax = _symax + 1.0 else: _xmin, _ymin, _xmax, _ymax = _bounds if _sxmin < _xmin: _xmin = _sxmin - 1.0 _resize = True if _sxmax > _xmax: _xmax = _sxmax + 1.0 _resize = True if _symin < _ymin: _ymin = _symin - 1.0 _resize = True if _symax > _ymax: _ymax = _symax + 1.0 _resize = True if _resize: self.resize(_xmin, _ymin, _xmax, _ymax) for _node in self.getNodes(_x1, _y1, _x2, _y2): _xmin, _ymin, _xmax, _ymax = _node.getBoundary() if obj.inRegion(_xmin, _ymin, _xmax, _ymax): _node.addObject(obj) super(SegmentQuadtree, self).addObject(obj) obj.connect('moved', self._moveSegment) def delObject(self, obj): if obj not in self: return _p1, _p2 = obj.getEndpoints() _x1, _y1 = _p1.getCoords() _x2, _y2 = _p2.getCoords() _pdict = {} for _node in self.getNodes(_x1, _y1, _x2, _y2): _node.delObject(obj) _parent = _node.getParent() if _parent is not None: _pid = id(_parent) if _pid not in _pdict: _pdict[_pid] = _parent super(SegmentQuadtree, self).delObject(obj) obj.disconnect(self) for _parent in _pdict.values(): self.purgeSubnodes(_parent) # # test # _nodes = [self.getTreeRoot()] while len(_nodes): _node = _nodes.pop() if _node.hasSubnodes(): _nodes.extend(_node.getSubnodes()) else: for _obj in _node.getObjects(): if _obj is obj: raise ValueError, "object still in tree" + `obj` def find(self, *args): _alen = len(args) if _alen < 4: raise ValueError, "Invalid argument count: %d" % _alen _x1 = args[0] if not isinstance(_x1, float): _x1 = float(args[0]) _y1 = args[1] if not isinstance(_y1, float): _y1 = float(args[1]) _x2 = args[2] if not isinstance(_x2, float): _x2 = float(args[2]) _y2 = args[3] if not isinstance(_y2, float): _y2 = float(args[3]) _t = tolerance.TOL if _alen > 4: _t = tolerance.toltest(args[4]) _xmin = min(_x1, _x2) - _t _ymin = min(_y1, _y2) - _t _xmax = max(_x1, _x2) + _t _ymax = max(_y1, _y2) + _t _segs = [] for _seg in self.getInRegion(_xmin, _ymin, _xmax, _ymax): _p1, _p2 = _seg.getEndpoints() if ((abs(_x1 - _p1.x) < _t) and (abs(_y1 - _p1.y) < _t) and (abs(_x2 - _p2.x) < _t) and (abs(_y2 - _p2.y) < _t)): _segs.append(_seg) elif ((abs(_x2 - _p1.x) < _t) and (abs(_y2 - _p1.y) < _t) and (abs(_x1 - _p2.x) < _t) and (abs(_y1 - _p2.y) < _t)): _segs.append(_seg) else: pass return _segs def _moveSegment(self, obj, *args): if obj not in self: raise ValueError, "Segment not stored in Quadtree: " + `obj` _alen = len(args) if _alen < 4: raise ValueError, "Invalid argument count: %d" % _alen _x1 = util.get_float(args[0]) _y1 = util.get_float(args[1]) _x2 = util.get_float(args[2]) _y2 = util.get_float(args[3]) for _node in self.getNodes(_x1, _y1, _x2, _y2): _node.delObject(obj) # segment may not be in node ... super(SegmentQuadtree, self).delObject(obj) obj.disconnect(self) self.addObject(obj) def getClosest(self, x, y, tol=tolerance.TOL): _x = util.get_float(x) _y = util.get_float(y) _t = tolerance.toltest(tol) _seg = _tsep = None _bailout = False _sdict = {} _nodes = [self.getTreeRoot()] while len(_nodes): _node = _nodes.pop() _xmin, _ymin, _xmax, _ymax = _node.getBoundary() if ((_x < (_xmin - _t)) or (_x > (_xmax + _t)) or (_y < (_ymin - _t)) or (_y > (_ymax + _t))): continue if _node.hasSubnodes(): _nodes.extend(_node.getSubnodes()) else: for _s in _node.getObjects(): _sid = id(_s) if _sid not in _sdict: _p1, _p2 = _s.getEndpoints() _px, _py = _p1.getCoords() if ((abs(_px - _x) < 1e-10) and (abs(_py - _y) < 1e-10)): _seg = _s _bailout = True break _px, _py = _p2.getCoords() if ((abs(_px - _x) < 1e-10) and (abs(_py - _y) < 1e-10)): _seg = _s _bailout = True break _sdict[_sid] = True _pt = _s.mapCoords(_x, _y, _t) if _pt is not None: _px, _py = _pt _sep = math.hypot((_px - _x), (_py - _y)) if _tsep is None: _tsep = _sep _seg = _s else: if _sep < _tsep: _tsep = _sep _seg = _s if _bailout: break return _seg def getInRegion(self, xmin, ymin, xmax, ymax): _xmin = util.get_float(xmin) _ymin = util.get_float(ymin) _xmax = util.get_float(xmax) if _xmax < _xmin: raise ValueError, "Illegal values: xmax < xmin" _ymax = util.get_float(ymax) if _ymax < _ymin: raise ValueError, "Illegal values: ymax < ymin" _segs = [] if not len(self): return _segs _nodes = [self.getTreeRoot()] _sdict = {} while len(_nodes): _node = _nodes.pop() if _node.hasSubnodes(): for _subnode in _node.getSubnodes(): _sxmin, _symin, _sxmax, _symax = _subnode.getBoundary() if ((_sxmin > _xmax) or (_symin > _ymax) or (_sxmax < _xmin) or (_symax < _ymin)): continue _nodes.append(_subnode) else: for _seg in _node.getObjects(): _sid = id(_seg) if _sid not in _sdict: if _seg.inRegion(_xmin, _ymin, _xmax, _ymax): _segs.append(_seg) _sdict[_sid] = True return _segs # # Segment history class # class SegmentLog(graphicobject.GraphicObjectLog): def __init__(self, s): if not isinstance(s, Segment): raise TypeError, "Invalid segment: " + `s` super(SegmentLog, self).__init__(s) s.connect('endpoint_changed', self.__endpointChanged) def __endpointChanged(self, s, *args): _alen = len(args) if _alen < 2: raise ValueError, "Invalid argument count: %d" % _alen _old = args[0] if not isinstance(_old, point.Point): raise TypeError, "Invalid old endpoint: " + `_old` _new = args[1] if not isinstance(_new, point.Point): raise TypeError, "Invalid new endpoint: " + `_new` self.saveUndoData('endpoint_changed', _old.getID(), _new.getID()) def execute(self, undo, *args): util.test_boolean(undo) _alen = len(args) if len(args) == 0: raise ValueError, "No arguments to execute()" _s = self.getObject() _p1, _p2 = _s.getEndpoints() _op = args[0] if _op == 'endpoint_changed': if _alen < 3: raise ValueError, "Invalid argument count: %d" % _alen _oid = args[1] _nid = args[2] _parent = _s.getParent() if _parent is None: raise ValueError, "Segment has no parent - cannot undo" self.ignore(_op) try: if undo: _pt = _parent.getObject(_oid) if _pt is None or not isinstance(_pt, point.Point): raise ValueError, "Old endpoint missing: id=%d" % _oid _s.startUndo() try: if _p1.getID() == _nid: _s.setP1(_pt) elif _p2.getID() == _nid: _s.setP2(_pt) else: raise ValueError, "Unexpected endpoint ID: %d" % _nid finally: _s.endUndo() else: _pt = _parent.getObject(_nid) if _pt is None or not isinstance(_pt, point.Point): raise ValueError, "New endpoint missing: id=%d" % _nid _s.startRedo() try: if _p1.getID() == _oid: _s.setP1(_pt) elif _p2.getID() == _oid: _s.setP2(_pt) else: raise ValueError, "Unexpected endpoint ID: %d" % _oid finally: _s.endRedo() finally: self.receive(_op) self.saveData(undo, _op, _oid, _nid) else: super(SegmentLog, self).execute(undo, *args)