# Sketch - A Python-based interactive drawing program
# Copyright (C) 1997, 1998, 1999, 2000, 2002, 2005 by Bernhard Herzog
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Library General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library 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
# Library General Public License for more details.
#
# You should have received a copy of the GNU Library General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
from math import pi,floor,atan2,ceil
from traceback import print_stack
from Sketch.const import SelectSet,SelectAdd,SelectSubtract,SelectDrag,\
Button1Mask, ConstraintMask,\
SCRIPT_GET, SCRIPT_OBJECT, SCRIPT_OBJECTLIST, SCRIPT_UNDO
from Sketch.warn import pdebug,warn,INTERNAL
from Sketch import Point,Polar,Rect,EmptyRect,UnionRects,PointsToRect
from Sketch import _,_sketch,CreatePath,config,RegisterCommands,\
CreateMultiUndo, NullUndo, Undo
from Sketch.UI.command import AddCmd
import Sketch.UI.skpixmaps
pixmaps = Sketch.UI.skpixmaps.PixmapTk
from Sketch._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 = int(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
|