# -*- coding: utf-8 -*- # Copyright (C) 2003-2006 by Igor E. Novikov # Copyright (C) 1997, 1998, 1999, 2000, 2002 by Bernhard Herzog # # This library is covered by GNU Library General Public License. # For more info see COPYRIGHTS file in sK1 root directory. from math import pi, floor, atan2, ceil from traceback import print_stack from app.conf.const import SelectSet, SelectAdd, SelectSubtract, SelectDrag, \ Button1Mask, ConstraintMask,\ SCRIPT_GET, SCRIPT_OBJECT, SCRIPT_OBJECTLIST, SCRIPT_UNDO from app.events.warn import pdebug, warn, INTERNAL from app import Point, Polar, Rect, EmptyRect, UnionRects, PointsToRect from app import _, _sketch, CreatePath, config, RegisterCommands, \ CreateMultiUndo, NullUndo, Undo #from app.UI.command import AddCmd #import app.UI.skpixmaps #pixmaps = app.UI.skpixmaps.PixmapTk from app._sketch import ContAngle, ContSmooth, ContSymmetrical, \ SelNone, SelNodes, SelSegmentFirst, SelSegmentLast, Bezier, Line import handle from base import Primitive, Creator, Editor from blend import Blend, BlendPaths, MismatchError from properties import DefaultGraphicsProperties # # PolyBezier # # The most important primitive, since it can be used to draw lines, # curves and arbitrary contours (to some degree; circles for instance # can only be approximated, but that works quite well). # def undo_path(undo, path): if undo is not None: return apply(getattr(path, undo[0]), undo[1:]) else: return undo class PolyBezier(Primitive): is_Bezier = 1 has_edit_mode = 1 is_curve = 1 is_clip = 1 script_access = Primitive.script_access.copy() def __init__(self, paths = None, properties = None, duplicate = None): if duplicate is not None: if paths is None: paths = [] for path in duplicate.paths: paths.append(path.Duplicate()) self.paths = tuple(paths) else: # This special case uses the properties kwarg now, I # hope. warn(INTERNAL, 'Bezier object created with paths and duplicte') print_stack() if type(paths) != type(()): paths = (paths,) self.paths = paths elif paths is not None: if type(paths) != type(()): paths = (paths,) self.paths = paths else: self.paths = (CreatePath(),) Primitive.__init__(self, properties = properties, duplicate=duplicate) def Hit(self, p, rect, device, clip = 0): for path in self.paths: if path.hit_point(rect): return 1 return device.MultiBezierHit(self.paths, p, self.properties, clip or self.Filled(), ignore_outline_mode = clip) def do_undo(self, undo_list): undo = map(undo_path, undo_list, self.paths) self._changed() return (self.do_undo, undo) def Transform(self, trafo): undo = [] undostyle = NullUndo try: rect = self.bounding_rect for path in self.paths: undo.append(path.Transform(trafo)) self._changed() undo = (self.do_undo, undo) self.update_rects() # calling update_rects directly is a bit faster undostyle = Primitive.Transform(self, trafo, rects = (rect, self.bounding_rect)) return CreateMultiUndo(undostyle, undo) except: Undo(undostyle) if type(undo) != type(()): undo = (self.do_undo, undo) Undo(undo) raise def Translate(self, offset): for path in self.paths: path.Translate(offset) self._changed() return self.Translate, -offset def DrawShape(self, device, rect = None, clip = 0): Primitive.DrawShape(self, device) device.MultiBezier(self.paths, rect, clip) def GetObjectHandle(self, multiple): if self.paths: return self.paths[0].Node(0) return Point(0, 0) def GetSnapPoints(self): points = [] for path in self.paths: points = points + path.NodeList() return points def Snap(self, p): found = [(1e100, p)] for path in self.paths: t = path.nearest_point(p.x, p.y) if t is not None: #print p, t p2 = path.point_at(t) found.append((abs(p - p2), p2)) return min(found) def update_rects(self): if not self.paths: # this should never happen... self.coord_rect = self.bounding_rect = Rect(0, 0, 0, 0) return rect = self.paths[0].accurate_rect() for path in self.paths[1:]: rect = UnionRects(rect, path.accurate_rect()) self.coord_rect = rect if self.properties.HasLine(): rect = self.add_arrow_rects(rect) self.bounding_rect = rect.grown(self.properties.GrowAmount()) else: self.bounding_rect = rect def add_arrow_rects(self, rect): if self.properties.HasLine(): arrow1 = self.properties.line_arrow1 arrow2 = self.properties.line_arrow2 if arrow1 or arrow2: width = self.properties.line_width for path in self.paths: if not path.closed and path.len > 1: if arrow1 is not None: type, controls, p3, cont = path.Segment(1) p = path.Node(0) if type == Bezier: dir = p - controls[0] else: dir = p - p3 rect = UnionRects(rect, arrow1.BoundingRect(p,dir,width)) if arrow2 is not None: type, controls, p, cont = path.Segment(-1) if type == Bezier: dir = p - controls[1] else: dir = p - path.Node(-2) rect = UnionRects(rect, arrow2.BoundingRect(p,dir,width)) return rect def Info(self): nodes = 0 for path in self.paths: nodes = nodes + path.len if path.closed: nodes = nodes - 1 return _("PolyBezier (%(nodes)d nodes in %(paths)d paths)") \ % {'nodes':nodes, 'paths':len(self.paths)} def set_paths(self, paths): undo = (self.set_paths, self.paths) self.paths = tuple(paths) self._changed() return undo SetPaths = set_paths def Paths(self): return self.paths def PathsAsObjects(self): result = [] for path in self.paths: object = self.__class__(paths = (path.Duplicate(),), properties = self.properties.Duplicate()) result.append(object) return result script_access['PathsAsObjects'] = SCRIPT_OBJECTLIST def AsBezier(self): # is `return self' enough ? return self.Duplicate() # # # def SaveToFile(self, file): Primitive.SaveToFile(self, file) file.PolyBezier(self.paths) def load_straight(self, *args): apply(self.paths[-1].AppendLine, args) def load_curve(self, *args): apply(self.paths[-1].AppendBezier, args) def load_close(self, copy_cont_from_last = 0): self.paths[-1].load_close(copy_cont_from_last) def guess_continuity(self): for path in self.paths: path.guess_continuity() def load_IsComplete(self): if self.paths[0].len < 2: # we need at least two nodes return 0 return 1 def Blend(self, other, frac1, frac2): if self.__class__ != other.__class__: try: other = other.AsBezier() if not other: raise MismatchError except AttributeError, value: if value == 'AsBezier': raise MismatchError else: raise paths = BlendPaths(self.paths, other.paths, frac1, frac2) blended = PolyBezier(paths = paths) self.set_blended_properties(blended, other, frac1, frac2) return blended def Editor(self): return PolyBezierEditor(self) class PolyBezierCreator(Creator): creation_text = _("Create Curve") def __init__(self, start): self.path = CreatePath() Creator.__init__(self, start) def apply_constraints(self, p, state): if self.path.len > 0: node = self.path.Node(-1) elif self.dragging: node = self.drag_start else: return p if state & ConstraintMask: radius, angle = (p - node).polar() pi12 = pi / 12 angle = pi12 * floor(angle / pi12 + 0.5) p = node + Polar(radius, angle) return p def ButtonDown(self, p, button, state): p = self.apply_constraints(p, state) if self.path.len == 0: self.path.AppendLine(p) else: self.path.AppendBezier(self.drag_cur, p, p) return self.DragStart(p) def MouseMove(self, p, state): if not (state & Button1Mask): return self.DragMove(self.apply_constraints(p, state)) def ButtonUp(self, p, button, state): if not (state & Button1Mask): return p = self.apply_constraints(p, state) self.DragStop(p) if self.path.len > 1: type, (p1, p2), p, cont = self.path.Segment(-1) p2 = adjust_control_point(p2, p, self.drag_cur, ContSymmetrical) self.path.SetBezier(-1, p1, p2, p, ContSymmetrical) def EndCreation(self): return self.path.len > 1 def AppendInteractive(self, p): return self def ContinueCreation(self): return self.AppendInteractive def DrawDragged(self, device, partially): if not partially: self.path.draw_not_last(device.Bezier, device.Line) device.DrawHandleLine(self.path.Node(-1), self.drag_cur) device.DrawSmallRectHandle(self.drag_cur) if self.path.len > 1: type, (p1, p2), p, cont = self.path.Segment(-1) p2 = adjust_control_point(p2, p, self.drag_cur, ContSymmetrical) device.Bezier(self.path.Node(-2), p1, p2, p) device.DrawHandleLine(p, p2) device.DrawSmallRectHandle(p2) def CreatedObject(self): return PolyBezier(paths = (self.path,), properties = DefaultGraphicsProperties()) class PolyLineCreator(Creator): creation_text = _("Create Poly-Line") def __init__(self, start): self.path = CreatePath() self.was_dragged = 0 Creator.__init__(self, start) def apply_constraints(self, p, state): if self.path.len > 0: node = self.path.Node(-1) elif self.dragging: node = self.drag_start else: return p if state & ConstraintMask: radius, angle = (p - node).polar() pi12 = pi / 12 angle = pi12 * floor(angle / pi12 + 0.5) p = node + Polar(radius, angle) return p def ButtonDown(self, p, button, state): return self.DragStart(self.apply_constraints(p, state)) def MouseMove(self, p, state): if not (state & Button1Mask): return self.was_dragged = 1 self.DragMove(self.apply_constraints(p, state)) def ButtonUp(self, p, button, state): if not (state & Button1Mask): return self.DragStop(self.apply_constraints(p, state)) if self.was_dragged and self.path.len == 0: self.path.AppendLine(self.drag_start) self.path.AppendLine(self.drag_cur) def EndCreation(self): return self.path.len > 1 def AppendInteractive(self, p): return self def ContinueCreation(self): return self.AppendInteractive def DrawDragged(self, device, partially): if self.path.len > 1: if not partially: device.DrawBezierPath(self.path) if self.path.len >= 1: device.Line(self.path.Node(-1), self.drag_cur) else: if self.was_dragged: device.Line(self.drag_start, self.drag_cur) def CreatedObject(self): return PolyBezier(paths = (self.path,), properties = DefaultGraphicsProperties()) SelCurvePoint = -1 class PolyBezierEditor(Editor): EditedClass = PolyBezier commands = [] def __init__(self, object): self.selected_path = -1 self.selected_idx = -1 self.selection_type = SelNone self.other_segment = -1 Editor.__init__(self, object) self.Deselect() def SelectPoint(self, p, rect, device, mode): self.deselect() found = [] for i in range(len(self.paths)): path = self.paths[i] t = path.nearest_point(p.x, p.y) if t is not None: p2 = path.point_at(t) found.append((abs(p - p2), i, t, p2)) dist, i, t, p2 = min(found) self.selected_path = i self.selected_idx = t self.selection_type = SelCurvePoint return 1 def SelectHandle(self, handle, mode = SelectSet): path_idx, segment = handle.code if segment < 0: segment = -segment if segment % 2: self.selection_type = SelSegmentFirst else: self.selection_type = SelSegmentLast self.selected_idx = (segment + 1) / 2 segment = segment / 2 else: self.selection_type = SelNodes self.selected_idx = segment self.selected_path = path_idx path = self.paths[path_idx] if mode == SelectSet or mode == SelectDrag: if not path.SegmentSelected(segment): self.deselect() path.SelectSegment(segment) elif mode == SelectAdd: path.SelectSegment(segment) elif mode == SelectSubtract: path.SelectSegment(segment, 0) def SelectRect(self, rect, mode = SelectSet): selected = 0 for path in self.paths: selected = path.select_rect(rect, mode) or selected self.selection_type = SelNodes return selected def SelectAllNodes(self): for path in self.paths: for i in range(path.len): path.SelectSegment(i, 1) #AddCmd(commands, SelectAllNodes, _("Select All Nodes")) def deselect(self): for path in self.paths: path.deselect() def Deselect(self): self.deselect() self.selected_path = -1 self.selected_idx = -1 self.selection_type = SelNone def ButtonDown(self, p, button, state): if self.selected_path >= 0: path = self.paths[self.selected_path] if self.selection_type == SelNodes: start = path.Node(self.selected_idx) self.DragStart(start) return p - start elif self.selection_type == SelSegmentFirst: segment = self.selected_idx if segment > 1 \ and path.SegmentType(segment - 1) == Bezier\ and path.Continuity(segment - 1): self.other_segment = segment - 1 elif path.closed and segment == 1 \ and path.SegmentType(-1) == Bezier\ and path.Continuity(-1): self.other_segment = path.len - 1 else: self.other_segment = -1 p1 = path.Segment(segment)[1][0] self.DragStart(p1) return p - p1 elif self.selection_type == SelSegmentLast: segment = self.selected_idx self.other_segment = -1 if path.Continuity(segment): if segment < path.len - 1 \ and path.SegmentType(segment + 1) == Bezier: self.other_segment = segment + 1 elif path.closed and segment == path.len - 1 \ and path.SegmentType(1) == Bezier: self.other_segment = 1 p2 = path.Segment(segment)[1][1] self.DragStart(p2) return p - p2 elif self.selection_type == SelCurvePoint: start = path.point_at(self.selected_idx) segment = ceil(self.selected_idx) prev = next = -1 type = path.SegmentType(segment) if type == Bezier: if path.Continuity(segment): if segment < path.len - 1 \ and path.SegmentType(segment + 1) == Bezier: next = segment + 1 elif path.closed and segment == path.len - 1 \ and path.SegmentType(1) == Bezier: next = 1 if segment > 1 \ and path.SegmentType(segment - 1) == Bezier\ and path.Continuity(segment - 1): prev = segment - 1 elif path.closed and segment == 1 \ and path.SegmentType(-1) == Bezier\ and path.Continuity(-1): prev = path.len - 1 else: if segment < path.len - 1: next = segment + 1 elif path.closed and segment == path.len - 1: next = 1 if segment >= 1: prev = segment - 1 elif path.closed and segment == 1: prev = path.len - 1 self.other_segment = (prev, next) self.DragStart(start) return p - start def apply_constraints(self, p, state): if state & ConstraintMask: if self.selection_type == SelNodes: pi4 = pi / 4 off = p - self.drag_start d = Polar(pi4 * round(atan2(off.y, off.x) / pi4)) p = self.drag_start + (off * d) * d elif self.selection_type in (SelSegmentFirst, SelSegmentLast): path = self.paths[self.selected_path] if self.selection_type == SelSegmentFirst: node = path.Node(self.selected_idx - 1) else: node = path.Node(self.selected_idx) radius, angle = (p - node).polar() pi12 = pi / 12 angle = pi12 * floor(angle / pi12 + 0.5) p = node + Polar(radius, angle) return p def MouseMove(self, p, state): self.DragMove(self.apply_constraints(p, state)) def ButtonUp(self, p, button, state): p = self.apply_constraints(p, state) self.DragStop(p) type = self.selection_type if type == SelNodes: undo = [] for path in self.paths: if path.selection_count() > 0: undo.append(path.move_selected_nodes(self.off)) else: undo.append(None) if undo: self._changed() return (self.do_undo, undo) elif type in (SelSegmentFirst, SelSegmentLast): idx = self.selected_path segment = self.selected_idx path = self.paths[idx].Duplicate() paths = self.paths[:idx] + (path,) + self.paths[idx + 1:] if type == SelSegmentFirst: type, (p1, p2), node, cont = path.Segment(segment) path.SetBezier(segment, self.drag_cur, p2, node, cont) if self.other_segment >= 0: other = self.other_segment type, (p1, p2), node, cont = path.Segment(other) p2 = adjust_control_point(p2, node, self.drag_cur, cont) path.SetBezier(other, p1, p2, node, cont) path.SelectSegment(segment - 1) elif type == SelSegmentLast: type, (p1, p2), node, cont = path.Segment(segment) path.SetBezier(segment, p1, self.drag_cur, node, cont) if self.other_segment >= 0: other = self.other_segment type, (p1, p2), node2, cont2 = path.Segment(other) p1 = adjust_control_point(p1, node, self.drag_cur, cont) path.SetBezier(other, p1, p2, node2, cont2) path.SelectSegment(segment) return self.set_paths(paths) # set_paths calls _changed() elif self.selection_type == SelCurvePoint: idx = self.selected_path path = self.paths[idx].Duplicate() paths = self.paths[:idx] + (path,) + self.paths[idx + 1:] segment = int(self.selected_idx) t = self.selected_idx - segment type, control, node, cont = path.Segment(segment + 1) if type == Bezier: p1, p2 = control if t <= 0.5: alpha = ((t * 2) ** 3) / 2 else: alpha = 1 - (((1 - t) * 2) ** 3) / 2 p1 = p1 + (self.off / (3 * t * (1 - t)**2)) * (1 - alpha) p2 = p2 + (self.off / (3 * t**2 * (1 - t))) * alpha path.SetBezier(segment + 1, p1, p2, node, cont) else: path.SetLine(segment + 1, node + self.off, cont) prev, next = self.other_segment if prev >= 0: _type, _control, _node, _cont = path.Segment(prev) if _type == Bezier: _p1, _p2 = _control if type == Bezier: _p2 = adjust_control_point(_p2, _node, p1, _cont) else: _p2 = _p2 + self.off _node = _node + self.off path.SetBezier(prev, _p1, _p2, _node, _cont) else: path.SetLine(prev, _node + self.off, _cont) if next >= 0: _type, _control, _node, _cont = path.Segment(next) if _type == Bezier: _p1, _p2 = _control if type == Bezier: _p1 = adjust_control_point(_p1, node, p2, cont) else: _p1 = _p1 + self.off path.SetBezier(next, _p1, _p2, _node, _cont) return self.set_paths(paths) # set_paths calls _changed() def DrawDragged(self, device, partially): if self.selection_type == SelNodes: for path in self.paths: path.draw_dragged_nodes(self.off, partially, device.Bezier, device.Line) elif self.selection_type == SelSegmentFirst: if not partially: for path in self.paths: path.draw_unselected(device.Bezier, device.Line) path = self.paths[self.selected_path] segment = self.selected_idx node = path.Node(segment - 1) type, (p1, p2), node2, cont = path.Segment(segment) device.Bezier(node, self.drag_cur, p2, node2) device.DrawSmallRectHandle(self.drag_cur) device.DrawHandleLine(node, self.drag_cur) if self.other_segment >= 0: other = self.other_segment type, (p1, p2), node, cont = path.Segment(other) p2 = adjust_control_point(p2, node, self.drag_cur, cont) device.Bezier(path.Node(other - 1), p1, p2, node) device.DrawSmallRectHandle(p2) device.DrawHandleLine(node, p2) elif self.selection_type == SelSegmentLast: if not partially: for path in self.paths: path.draw_unselected(device.Bezier, device.Line) path = self.paths[self.selected_path] segment = self.selected_idx type, (p1, p2), node, cont = path.Segment(segment) device.Bezier(path.Node(segment - 1), p1, self.drag_cur, node) device.DrawSmallRectHandle(self.drag_cur) device.DrawHandleLine(node, self.drag_cur) if self.other_segment >= 0: other = self.other_segment type, (p1, p2), node2, cont2 = path.Segment(other) p1 = adjust_control_point(p1, node, self.drag_cur, cont) device.Bezier(node, p1, p2, node2) device.DrawSmallRectHandle(p1) device.DrawHandleLine(node, p1) elif self.selection_type == SelCurvePoint: path = self.paths[self.selected_path] segment = int(self.selected_idx) t = self.selected_idx - segment prevnode = path.Node(segment) type, control, node, cont = path.Segment(segment + 1) if type == Bezier: p1, p2 = control if t <= 0.5: alpha = ((t * 2) ** 3) / 2 else: alpha = 1 - (((1 - t) * 2) ** 3) / 2 p1 = p1 + (self.off / (3 * t * (1 - t)**2)) * (1 - alpha) p2 = p2 + (self.off / (3 * t**2 * (1 - t))) * alpha device.Bezier(prevnode, p1, p2, node) device.DrawSmallRectHandle(p1) device.DrawHandleLine(prevnode, p1) device.DrawSmallRectHandle(p2) device.DrawHandleLine(node, p2) else: device.DrawLine(prevnode + self.off, node + self.off) prev, next = self.other_segment if prev > 0: _type, _control, _node, _cont = path.Segment(prev) if _type == Bezier: _p1, _p2 = _control if type == Bezier: _p2 = adjust_control_point(_p2, _node, p1, _cont) device.Bezier(path.Node(prev - 1), _p1, _p2, _node) device.DrawSmallRectHandle(_p2) device.DrawHandleLine(_node, _p2) else: device.Bezier(path.Node(prev - 1), _p1, _p2 + self.off, _node + self.off) else: device.DrawLine(path.Node(prev - 1), _node + self.off) if next >= 0: _type, _control, _node, _cont = path.Segment(next) if _type == Bezier: _p1, _p2 = _control if type == Bezier: _p1 = adjust_control_point(_p1, node, p2, cont) device.Bezier(node, _p1, _p2, _node) device.DrawSmallRectHandle(_p1) device.DrawHandleLine(node, _p1) else: device.Bezier(node + self.off, _p1 + self.off, _p2, _node) else: device.DrawLine(node + self.off, _node) def GetHandles(self): NodeHandle = handle.MakeNodeHandle ControlHandle = handle.MakeControlHandle LineHandle = handle.MakeLineHandle handles = [] node_handles = [] append = handles.append for path_idx in range(len(self.paths)): path = self.paths[path_idx] if path.len > 0: if not path.closed: node_handles.append(NodeHandle(path.Node(0), path.SegmentSelected(0), (path_idx, 0))) for i in range(1, path.len): selected = path.SegmentSelected(i) node_handles.append(NodeHandle(path.Node(i), selected, (path_idx, i))) if (path.SegmentType(i) == Bezier and (selected or path.SegmentSelected(i - 1))): type, (p1, p2), node, cont = path.Segment(i) append(ControlHandle(p1, (path_idx, -(2 * i - 1)))) append(ControlHandle(p2, (path_idx, -(2 * i)))) append(LineHandle(path.Node(i - 1), p1)) append(LineHandle(p2, node)) if self.selection_type == SelCurvePoint: p = self.paths[self.selected_path].point_at(self.selected_idx) handles.append(handle.MakeCurveHandle(p)) return handles + node_handles def Info(self): selected = 0 idx = None paths = self.paths for i in range(len(paths)): path = paths[i] count = path.selection_count() if count > 0: selected = selected + count idx = i if selected > 1: return _("%d nodes in PolyBezier") % selected else: if idx is not None: path = paths[idx] for i in range(path.len): if path.SegmentSelected(i): break else: warn(INTERNAL, 'Strange selection count') return _("PolyBezier") if i == 0: return _("First node of PolyBezier") elif i == path.len - 1: return _("Last node of PolyBezier") else: return _("1 node of PolyBezier") else: if self.selection_type == SelCurvePoint: return _("Point on curve at position %.2f") \ % self.selected_idx else: return _("No Node of PolyBezier") # # Special poly bezier protocol: closing, continuity # def OpenNodes(self): if self.selection_type == SelCurvePoint: index = self.selected_path paths = list(self.paths) path = paths[index] paths[index:index + 1] = split_path_at(path, self.selected_idx) self.selected_idx = int(self.selected_idx) + 1 self.selection_type = SelNodes else: paths = [] for path in self.paths: if path.selection_count() >= 1: if path.closed: for i in range(path.len - 1): if path.SegmentSelected(i): start_idx = i break else: start_idx = 0 newpath = CreatePath() paths.append(newpath) p = path.Node(start_idx) newpath.AppendLine(p, ContAngle) for i in range(start_idx + 1, path.len): type, control, p, cont = path.Segment(i) if path.SegmentSelected(i): # XXX remove this ? cont = ContAngle newpath.AppendSegment(type, control, p, cont) if path.SegmentSelected(i) and i < path.len - 1: newpath = CreatePath() newpath.AppendLine(p, ContAngle) paths.append(newpath) if start_idx != 0: # the path was closed and the first node was not # selected for i in range(1, start_idx + 1): type, control, p, cont = path.Segment(i) newpath.AppendSegment(type, control, p, cont) else: paths.append(path) return self.set_paths(paths) #AddCmd(commands, OpenNodes, _("Cut Curve"), key_stroke = 'c', #bitmap = pixmaps.BezierOpenNodes) def CloseNodes(self): # find out if close is possible two = 0 one = 0 for i in range(len(self.paths)): path = self.paths[i] selected = path.selection_count() if not selected: continue if (path.closed and selected) or selected not in (1, 2): return if selected == 1: if path.SegmentSelected(0) or path.SegmentSelected(-1): one = one + 1 continue return else: if path.SegmentSelected(0) and path.SegmentSelected(-1): two = two + 1 continue return # now, close the nodes if one == 2 and two == 0: paths = [] append_to = None for path in self.paths: if path.selection_count(): if append_to: # path is the second of the paths involved end_node = append_to.Node(-1) if path.SegmentSelected(0): for i in range(1, path.len): type, p12, p, cont = path.Segment(i) if end_node is not None and type == Bezier: p12 = (p12[0] + end_node - path.Node(0), p12[1]) end_node = None append_to.AppendSegment(type, p12, p, cont) else: for i in range(path.len - 1, 0, -1): type, p12, p3, cont = path.Segment(i) if end_node is not None and type == Bezier: p12 = (p12[0], p12[1] + end_node - path.Node(-1)) end_node = None p = path.Node(i - 1) if type == Bezier: p12 = (p12[1], p12[0]) append_to.AppendSegment(type, p12, p, path.Continuity(i - 1)) continue else: # path is the first of the paths involved if path.SegmentSelected(0): # reverse the path append_to = CreatePath() p = path.Node(-1) append_to.AppendLine(p, ContAngle) for i in range(path.len - 1, 0, -1): type, p12, p3, cont = path.Segment(i) p = path.Node(i - 1) if type == Bezier: p12 = (p12[1], p12[0]) append_to.AppendSegment(type, p12, p, path.Continuity(i - 1)) path = append_to else: path = append_to = path.Duplicate() append_to.SetContinuity(-1, ContAngle) paths.append(path) undo = self.set_paths(paths) elif one == 0 and two == 1: undo_list = [] for path in self.paths: if path.selection_count(): undo_list.append(path.ClosePath()) else: undo_list.append(None) undo = (self.object.do_undo, undo_list) self._changed() else: return return undo #AddCmd(commands, CloseNodes, _("Close Nodes"), #bitmap = pixmaps.BezierCloseNodes) def SetContinuity(self, cont): new_paths = [] for path in self.paths: if path.selection_count(): new_paths.append(set_continuity(path, cont)) else: new_paths.append(path) return self.set_paths(new_paths) #AddCmd(commands, 'ContAngle', _("Angle"), SetContinuity, #args = ContAngle, bitmap = pixmaps.BezierAngle, key_stroke = 'a') #AddCmd(commands, 'ContSmooth', _("Smooth"), SetContinuity, #args = ContSmooth, bitmap = pixmaps.BezierSmooth, key_stroke = 's') #AddCmd(commands, 'ContSymmetrical', _("Symmetrical"), SetContinuity, #args = ContSymmetrical, bitmap = pixmaps.BezierSymm, key_stroke='y') def SegmentsToLines(self): if self.selection_type == SelCurvePoint: new_paths = list(self.paths) path = new_paths[self.selected_path] new_paths[self.selected_path] = segment_to_line(path, self.selected_idx) else: new_paths = [] for path in self.paths: if path.selection_count() > 1: new_paths.append(segments_to_lines(path)) else: new_paths.append(path) return self.set_paths(new_paths) #AddCmd(commands, SegmentsToLines, _("Curve->Line"), key_stroke = 'l', #bitmap = pixmaps.BezierCurveLine) def SegmentsToCurve(self): if self.selection_type == SelCurvePoint: new_paths = list(self.paths) path = new_paths[self.selected_path] new_paths[self.selected_path] = segment_to_curve(path, self.selected_idx) else: new_paths = [] for path in self.paths: if path.selection_count() > 1: new_paths.append(segments_to_beziers(path)) else: new_paths.append(path) return self.set_paths(new_paths) #AddCmd(commands, SegmentsToCurve, _("Line->Curve"), key_stroke = 'b', #bitmap = pixmaps.BezierLineCurve) def DeleteNodes(self): new_paths = [] for path in self.paths: if path.selection_count() > 0: newpath = delete_segments(path) else: newpath = path if newpath.len > 1: new_paths.append(newpath) else: # all nodes of path have been deleted if __debug__: pdebug('bezier', 'path removed') if new_paths: return self.set_paths(new_paths) else: if __debug__: pdebug('bezier', 'PolyBezier removed') self.document.DeselectObject(self.object) return self.parent.Remove(self.object) #AddCmd(commands, DeleteNodes, _("Delete Nodes"), #bitmap = pixmaps.BezierDeleteNode, key_stroke = ('-', 'Delete')) def InsertNodes(self): if self.selection_type == SelCurvePoint: new_paths = list(self.paths) path = new_paths[self.selected_path] new_paths[self.selected_path] = insert_node_at(path, self.selected_idx) self.selected_idx = int(self.selected_idx) + 1 self.selection_type = SelNodes else: new_paths = [] for path in self.paths: if path.selection_count() > 1: new_paths.append(insert_segments(path)) else: new_paths.append(path) return self.set_paths(new_paths) #AddCmd(commands, InsertNodes, _("Insert Nodes"), key_stroke = '+', #bitmap = pixmaps.BezierInsertNode) def ChangeRect(self): prop = self.properties if prop.IsAlgorithmicFill() or prop.IsAlgorithmicLine() \ or prop.line_arrow1 is not None or prop.line_arrow2 is not None \ or self.selection_type == SelCurvePoint: return self.bounding_rect filled = self.Filled() pts = [] for path in self.paths: if path.selection_count(): for i in range(1, path.len): if path.SegmentSelected(i - 1) or path.SegmentSelected(i): pts.append(path.Node(i - 1)) type, p12, p, cont = path.Segment(i) if type == Bezier: p1, p2 = p12 pts.append(p1) pts.append(p2) pts.append(p) if filled and not path.closed: if path.SegmentSelected(-1): pts.append(path.Node(0)) if path.SegmentSelected(0): pts.append(path.Node(-1)) if pts: return PointsToRect(pts).grown(prop.GrowAmount()) else: return EmptyRect context_commands = ('SelectAllNodes',) RegisterCommands(PolyBezierEditor) def adjust_control_point(p, node, control, continuity): if continuity == ContSymmetrical: return 2 * node - control elif continuity == ContSmooth: try: d = (control - node).normalized() length = abs(p - node) return node - length * d except ZeroDivisionError: # control == node return p else: return p def subdivide(p0, p1, p2, p3, t = 0.5): t2 = 1 - t r = t2 * p1 + t * p2 q1 = t2 * p0 + t * p1 q2 = t2 * q1 + t * r q5 = t2 * p2 + t * p3 q4 = t2 * r + t * q5 q3 = t2 * q2 + t * q4 return q1, q2, q3, q4, q5 def delete_segments(path): newpath = CreatePath() selected = path.selection_count() if (path.closed and selected == path.len - 1) or selected == path.len: return newpath f13 = 1.0 / 3.0; f23 = 2.0 / 3.0 i = 0 while path.SegmentSelected(i): i = i + 1 if path.closed and i > 0: if path.SegmentType(i) == Bezier: last_p2 = path.Segment(i)[1][1] last_type = Bezier else: last_p2 = f23 * path.Node(i - 1) + f13 * path.Node(i) last_type = Line else: last_p2 = None newpath.AppendLine(path.Node(i), path.Continuity(i)) seg_p1 = None; seg_type = None for i in range(i + 1, path.len): type, p12, p, cont = path.Segment(i) if type == Bezier: p1, p2 = p12 if path.SegmentSelected(i): if seg_type is None: seg_type = type if type == Bezier: seg_p1 = p1 else: seg_p1 = f23 * path.Node(i - 1) + f13 * p else: if seg_type is not None: if type == Bezier or seg_type == Bezier: if type == Line: p2 = f13 * path.Node(i - 1) + f23 * p newpath.AppendBezier(seg_p1, p2, p, cont) else: newpath.AppendLine(p, cont) seg_type = None else: newpath.AppendSegment(type, p12, p, cont) if path.closed: if last_p2 is not None: if last_type == Bezier or seg_type == Bezier: newpath.AppendBezier(seg_p1, last_p2, newpath.Node(0), newpath.Continuity(0)) else: newpath.AppendLine(newpath.Node(0), newpath.Continuity(0)) newpath.ClosePath() return newpath def insert_segments(path): newpath = CreatePath() newpath.AppendLine(path.Node(0), path.Continuity(0)) newpath.select_segment(0, path.SegmentSelected(0)) for i in range(1, path.len): type, p12, p, cont = path.Segment(i) if path.SegmentSelected(i) and path.SegmentSelected(i - 1): if type == Line: node = 0.5 * path.Node(i - 1) + 0.5 * path.Node(i) newpath.AppendLine(node) newpath.select_segment(-1) newpath.AppendLine(path.Node(i)) newpath.select_segment(-1) else: if newpath.Continuity(-1) == ContSymmetrical: newpath.SetContinuity(-1, ContSmooth) p1, p2 = p12 p1, p2, node, p3, p4 = subdivide(path.Node(i - 1), p1, p2, p) newpath.AppendBezier(p1, p2, node, ContSymmetrical) newpath.select_segment(-1) if cont == ContSymmetrical: cont = ContSmooth newpath.AppendBezier(p3, p4, p, cont) newpath.select_segment(-1) else: newpath.AppendSegment(type, p12, p, cont) newpath.select_segment(-1, path.SegmentSelected(i)) if path.closed: newpath.ClosePath() newpath.SetContinuity(-1, path.Continuity(-1)) return newpath def copy_selection(path, newpath): for i in range(path.len): newpath.select_segment(i, path.SegmentSelected(i)) def segments_to_lines(path): newpath = CreatePath() newpath.AppendLine(path.Node(0)) for i in range(1, path.len): if path.SegmentSelected(i) and path.SegmentSelected(i - 1): if path.SegmentType(i) == Bezier: cont = ContAngle newpath.SetContinuity(-1, ContAngle) else: cont = path.Continuity(i) newpath.AppendLine(path.Node(i), cont) else: apply(newpath.AppendSegment, path.Segment(i)) if path.closed: cont = newpath.Continuity(-1) newpath.ClosePath() newpath.SetContinuity(-1, cont) copy_selection(path, newpath) return newpath def segments_to_beziers(path): f13 = 1.0 / 3.0; f23 = 2.0 / 3.0 newpath = CreatePath() newpath.AppendLine(path.Node(0)) for i in range(1, path.len): type, p12, p, cont = path.Segment(i) if path.SegmentSelected(i) and path.SegmentSelected(i - 1): cont = path.Continuity(i) if type == Line: node1 = path.Node(i - 1); node2 = path.Node(i) p1 = f23 * node1 + f13 * node2 p2 = f13 * node1 + f23 * node2 cont = ContAngle else: p1, p2 = p12 newpath.AppendBezier(p1, p2, p, cont) else: newpath.AppendSegment(type, p12, p, cont) if path.closed: cont = newpath.Continuity(-1) newpath.ClosePath() newpath.SetContinuity(-1, cont) copy_selection(path, newpath) return newpath def set_continuity(path, cont): f13 = 1.0 / 3.0; f23 = 2.0 / 3.0 newpath = path.Duplicate() for i in range(1, path.len): if path.SegmentSelected(i): newpath.SetContinuity(i, cont) if cont == ContAngle: continue if newpath.SegmentType(i) != Bezier: continue if i == path.len - 1: if newpath.closed: other = 1 else: continue else: other = i + 1 if newpath.SegmentType(other) != Bezier: continue type, (p1, p2), node, oldcont = newpath.Segment(i) type, (p3, p4), other_node, other_cont = newpath.Segment(other) d = p3 - p2 if cont == ContSymmetrical: d = 0.5 * d p2 = adjust_control_point(p2, node, node + d, cont) p3 = adjust_control_point(p3, node, node - d, cont) newpath.SetBezier(i, p1, p2, node, cont) newpath.SetBezier(other, p3, p4, other_node, other_cont) return newpath def copy_path(dest, src, start = 0, end = -1, copy_selection = 1): if start < 0: start = src.len + start if end < 0: end = src.len + end for i in range(start, end + 1): type, control, node, cont = src.Segment(i) dest.AppendSegment(type, control, node, cont) if copy_selection: dest.select_segment(-1, src.SegmentSelected(i)) def insert_node_at(path, at): index = int(at) t = at - index newpath = CreatePath() copy_path(newpath, path, 0, index) type, control, node, cont = path.Segment(index + 1) if type == Line: newpath.AppendLine((1 - t) * path.Node(index) + t * node) newpath.select_segment(-1) newpath.AppendLine(node) else: if newpath.Continuity(-1) == ContSymmetrical: newpath.SetContinuity(-1, ContSmooth) p1, p2 = control p1, p2, q, p3, p4 = subdivide(newpath.Node(-1), p1, p2, node, t) newpath.AppendBezier(p1, p2, q, ContSmooth) newpath.select_segment(-1) if cont == ContSymmetrical: cont = ContSmooth newpath.AppendBezier(p3, p4, node, cont) copy_path(newpath, path, index + 2) if path.closed: newpath.ClosePath() newpath.SetContinuity(-1, path.Continuity(-1)) return newpath def split_path_at(path, at): index = int(at) t = at - index if path.closed: path1 = path2 = CreatePath() result = [path1] else: path1 = CreatePath() path2 = CreatePath() result = [path1, path2] copy_path(path1, path, 0, 0, copy_selection = 0) type, control, node, cont = path.Segment(index + 1) if type == Line: q = (1 - t) * path.Node(index) + t * node path2.AppendLine(q) path2.AppendLine(node) path2.select_segment(0) function = path1.AppendLine args = (q,) else: p1, p2 = control p1, p2, q, p3, p4 = subdivide(path.Node(index), p1, p2, node, t) path2.AppendLine(q) path2.AppendBezier(p3, p4, node, cont) path2.select_segment(0) function = path1.AppendBezier args = (p1, p2, q, ContSymmetrical) copy_path(path2, path, index + 2, copy_selection = 0) copy_path(path1, path, 1, index, copy_selection = 0) apply(function, args) return result def segment_to_line(path, at): index = int(at) if path.SegmentType(index + 1) == Bezier: newpath = CreatePath() copy_path(newpath, path, 0, index) newpath.SetContinuity(-1, ContAngle) newpath.AppendLine(path.Node(index + 1), ContAngle) copy_path(newpath, path, index + 2) if path.closed: cont = newpath.Continuity(-1) newpath.ClosePath() newpath.SetContinuity(-1, cont) else: newpath = path return newpath def segment_to_curve(path, at): index = int(at) if path.SegmentType(index + 1) == Line: newpath = CreatePath() copy_path(newpath, path, 0, index) f13 = 1.0 / 3.0; f23 = 2.0 / 3.0 node1 = path.Node(index); node2 = path.Node(index + 1) p1 = f23 * node1 + f13 * node2 p2 = f13 * node1 + f23 * node2 newpath.AppendBezier(p1, p2, node2, path.Continuity(index + 1)) copy_path(newpath, path, index + 2) if path.closed: cont = newpath.Continuity(-1) newpath.ClosePath() newpath.SetContinuity(-1, cont) else: newpath = path return newpath def CombineBeziers(beziers): combined = beziers[0].Duplicate() paths = combined.paths for bezier in beziers[1:]: paths = paths + bezier.paths combined.paths = paths return combined