# Sketch - A Python-based interactive drawing program # Copyright (C) 1997, 1998, 1999, 2000, 2003 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 import math from math import atan2, hypot, pi, sin, cos from app import Point, Rotation, Translation, Trafo, NullPoint, NullUndo from blend import Blend, MismatchError, BlendTrafo import color class Pattern: is_procedural = 1 is_Empty = 0 is_Solid = 0 is_Gradient = 0 is_RadialGradient = 0 is_AxialGradient = 0 is_ConicalGradient = 0 is_Hatching = 0 is_Tiled = 0 is_Image = 0 name = '' def __init__(self, duplicate = None): pass def SetName(self, name): self.name = name def Name(self): return self.name def Execute(self, device, rect = None): pass def Transform(self, trafo, rects = None): # This method is usually called by a primitives Transform method. return NullUndo def Duplicate(self): return self.__class__(duplicate = self) Copy = Duplicate class EmptyPattern_(Pattern): is_procedural = 0 is_Empty = 1 def Duplicate(self): return self Copy = Duplicate def Blend(self, *args): return self def SaveToFile(self, file): file.EmptyPattern() def __str__(self): return 'EmptyPattern' EmptyPattern = EmptyPattern_() class SolidPattern(Pattern): is_procedural = 0 is_Solid = 1 def __init__(self, color = None, duplicate = None): if duplicate is not None: self.color = duplicate.color elif color is not None: self.color = color else: raise ValueError,'SolidPattern be must created with color argument' def __cmp__(self, other): if self.__class__ == other.__class__: return cmp(self.color, other.color) else: return cmp(id(self), id(other)) def __str__(self): return 'SolidPattern(%s)' % `self.color` def Execute(self, device, rect = None): device.SetFillColor(self.color) def Blend(self, other, frac1, frac2): if other.__class__ == self.__class__: return SolidPattern(Blend(self.color.RGB(), other.color.RGB(), frac1, frac2)) else: raise MismatchError def Color(self): return self.color def SaveToFile(self, file): file.SolidPattern(self.color) class GradientPattern(Pattern): is_Gradient = 1 def __init__(self, gradient, duplicate = None): if duplicate is not None: Pattern.__init__(self, duplicate = duplicate) self.gradient = duplicate.gradient.Duplicate() elif gradient: self.gradient = gradient else: raise ValueError,\ 'GradientPattern must be created with gradient argument' def Gradient(self): return self.gradient def SetGradient(self, gradient): undo = (self.SetGradient, self.gradient) self.gradient = gradient return undo class LinearGradient(GradientPattern): is_AxialGradient = 1 def __init__(self, gradient = None, direction = Point(0, -1), border = 0, duplicate = None): GradientPattern.__init__(self, gradient, duplicate = duplicate) self.direction = direction self.border = border if duplicate is not None: if duplicate.__class__ == self.__class__: self.direction = duplicate.direction self.border = duplicate.border elif duplicate.__class__ == ConicalGradient: self.direction = duplicate.direction elif duplicate.__class__ == RadialGradient: self.border = duplicate.border def SetDirection(self, dir): undo = (self.SetDirection, self.direction) self.direction = dir return undo def Direction(self): return self.direction def Border(self): return self.border def SetBorder(self, border): undo = (self.SetBorder, self.border) self.border = border return undo def Transform(self, trafo, rects = None): dx, dy = self.direction dx, dy = trafo.DTransform(dy, -dx) dir = Point(dy, -dx).normalized() if dir * trafo.DTransform(self.direction) < 0: dir = -dir return self.SetDirection(dir) def Execute(self, device, rect): if device.has_axial_gradient: self.execute_axial_gradient(device, rect) return SetFillColor = device.SetFillColor FillRectangle = device.FillRectangle steps = device.gradient_steps colors = self.gradient.Sample(steps) SetFillColor(colors[0]) apply(device.FillRectangle, tuple(rect)) device.PushTrafo() vx, vy = self.direction angle = atan2(vy, vx) - pi / 2 center = rect.center() rot = Rotation(angle, center) left, bottom, right, top = rot(rect) device.Concat(rot) device.Translate(center) height = top - bottom miny = -height / 2 height = height * (1.0 - self.border) width = right - left dy = height / steps y = height / 2 x = width / 2 for i in range(steps): SetFillColor(colors[i]) FillRectangle(-x, y, +x, miny) y = y - dy device.PopTrafo() def execute_axial_gradient(self, device, rect): vx, vy = self.direction angle = atan2(vy, vx) - pi / 2 center = rect.center() rot = Rotation(angle, center) left, bottom, right, top = rot(rect) height = (top - bottom) * (1.0 - self.border) trafo = rot(Translation(center)) device.AxialGradient(self.gradient, trafo(0, height / 2), trafo(0, -height / 2)) def Blend(self, other, frac1, frac2): if other.__class__ == self.__class__: gradient = other.gradient dir = other.direction border = other.border elif other.__class__ == SolidPattern: gradient = other.Color() dir = self.direction border = self.border else: raise MismatchError return LinearGradient(Blend(self.gradient, gradient, frac1, frac2), frac1 * self.direction + frac2 * dir, frac1 * self.border + frac2 * border) def SaveToFile(self, file): file.LinearGradientPattern(self.gradient, self.direction, self.border) class RadialGradient(GradientPattern): is_RadialGradient = 1 def __init__(self, gradient = None, center = Point(0.5, 0.5), border = 0, duplicate = None): GradientPattern.__init__(self, gradient, duplicate = duplicate) self.center = center self.border = border if duplicate is not None: if duplicate.__class__ == self.__class__: self.center = duplicate.center self.border = duplicate.border elif duplicate.__class__ == ConicalGradient: self.center = duplicate.center elif duplicate.__class__ == LinearGradient: self.border = duplicate.border def SetCenter(self, center): undo = (self.SetCenter, self.center) self.center = center return undo def Center(self): return self.center def Border(self): return self.border def SetBorder(self, border): undo = (self.SetBorder, self.border) self.border = border return undo def Transform(self, trafo, rects = None): if rects: r1, r2 = rects left, bottom, right, top = r1 cx, cy = self.center cx = cx * right + (1 - cx) * left cy = cy * top + (1 - cy) * bottom cx, cy = trafo(cx, cy) left, bottom, right, top = r2 len = right - left if len: cx = (cx - left) / len else: cx = 0 len = top - bottom if len: cy = (cy - bottom) / len else: cy = 0 center = Point(cx, cy) else: center = self.center return self.SetCenter(center) def Execute(self, device, rect): if device.has_radial_gradient: self.execute_radial(device, rect) return steps = device.gradient_steps cx, cy = self.center cx = cx * rect.right + (1 - cx) * rect.left cy = cy * rect.top + (1 - cy) * rect.bottom radius = max(hypot(rect.left - cx, rect.top - cy), hypot(rect.right - cx, rect.top - cy), hypot(rect.right - cx, rect.bottom - cy), hypot(rect.left - cx, rect.bottom - cy)) color = self.gradient.ColorAt SetFillColor = device.SetFillColor FillCircle = device.FillCircle SetFillColor(color(0)) apply(device.FillRectangle, tuple(rect)) radius = radius * (1.0 - self.border) dr = radius / steps device.PushTrafo() device.Translate(cx, cy) center = NullPoint for i in range(steps): SetFillColor(color(float(i) / (steps - 1))) FillCircle(center, radius) radius = radius - dr device.PopTrafo() def execute_radial(self, device, rect): cx, cy = self.center cx = cx * rect.right + (1 - cx) * rect.left cy = cy * rect.top + (1 - cy) * rect.bottom radius = max(hypot(rect.left - cx, rect.top - cy), hypot(rect.right - cx, rect.top - cy), hypot(rect.right - cx, rect.bottom - cy), hypot(rect.left - cx, rect.bottom - cy)) radius = radius * (1.0 - self.border) device.RadialGradient(self.gradient, (cx, cy), radius, 0) def Blend(self, other, frac1, frac2): if other.__class__ == self.__class__: gradient = other.gradient center = other.center border = other.border elif other.__class__ == SolidPattern: gradient = other.Color() center = self.center border = self.border else: raise MismatchError return RadialGradient(Blend(self.gradient, gradient, frac1, frac2), frac1 * self.center + frac2 * center, frac1 * self.border + frac2 * border) def SaveToFile(self, file): file.RadialGradientPattern(self.gradient, self.center, self.border) class ConicalGradient(GradientPattern): is_ConicalGradient = 1 def __init__(self, gradient = None, center = Point(0.5, 0.5), direction = Point(1, 0), duplicate = None): GradientPattern.__init__(self, gradient, duplicate = duplicate) self.center = center self.direction = direction if duplicate is not None: if duplicate.__class__ == self.__class__: self.center = duplicate.center self.direction = duplicate.direction elif duplicate.__class__ == LinearGradient: self.direction = duplicate.direction elif duplicate.__class__ == RadialGradient: self.center = duplicate.center def __set_center_and_dir(self, center, dir): undo = (self.__set_center_and_dir, self.center, self.direction) self.center = center self.direction = dir return undo def Transform(self, trafo, rects = None): dir = trafo.DTransform(self.direction).normalized() if rects: r1, r2 = rects left, bottom, right, top = r1 cx, cy = self.center cx = cx * right + (1 - cx) * left cy = cy * top + (1 - cy) * bottom cx, cy = trafo(cx, cy) left, bottom, right, top = r2 len = right - left if len: cx = (cx - left) / len else: cx = 0 len = top - bottom if len: cy = (cy - bottom) / len else: cy = 0 center = Point(cx, cy) else: center = self.center return self.__set_center_and_dir(center, dir) def SetCenter(self, center): undo = (self.SetCenter, self.center) self.center = center return undo def Center(self): return self.center def SetDirection(self, dir): undo = (self.SetDirection, self.direction) self.direction = dir return undo def Direction(self): return self.direction def Execute(self, device, rect): if device.has_conical_gradient: self.execute_conical(device, rect) return steps = device.gradient_steps cx, cy = self.center left, bottom, right, top = rect cx = cx * right + (1 - cx) * left cy = cy * top + (1 - cy) * bottom vx, vy = self.direction angle = atan2(vy, vx) rot = Rotation(angle, cx, cy) radius = max(hypot(left - cx, top - cy), hypot(right - cx, top - cy), hypot(right - cx, bottom - cy), hypot(left-cx,bottom-cy)) + 10 device.PushTrafo() device.Concat(rot) device.Translate(cx, cy) device.Scale(radius) colors = self.gradient.Sample(steps) SetFillColor = device.SetFillColor FillPolygon = device.FillPolygon da = pi / steps points = [(1, 0)] for i in range(steps): a = da * (i + 1) x = cos(a); y = sin(a) points.insert(0, (x, y)) points.append((x, -y)) colors.reverse() SetFillColor(colors[0]) FillPolygon(points) points.insert(0, (0, 0)) for i in range(steps): SetFillColor(colors[i]) del points[1] del points[-1] FillPolygon(points) device.PopTrafo() def execute_conical(self, device, rect): cx, cy = self.center left, bottom, right, top = rect cx = cx * right + (1 - cx) * left cy = cy * top + (1 - cy) * bottom angle = self.direction.polar()[1] device.ConicalGradient(self.gradient, (cx, cy), angle) def Blend(self, other, frac1, frac2): if other.__class__ == self.__class__: gradient = other.gradient dir = other.direction center = other.center elif other.__class__ == SolidPattern: gradient = other.Color() dir = self.direction center = self.center else: raise MismatchError return ConicalGradient(Blend(self.gradient, gradient, frac1, frac2), frac1 * self.center + frac2 * center, frac1 * self.direction + frac2 * dir) def SaveToFile(self, file): file.ConicalGradientPattern(self.gradient, self.center, self.direction) class HatchingPattern(Pattern): is_Hatching = 1 def __init__(self, foreground = None, background = None, direction = Point(1, 0), spacing = 5.0, width = 0.5, duplicate = None): if duplicate is not None: self.foreground = duplicate.foreground self.background = duplicate.background self.spacing = duplicate.spacing self.width = duplicate.width self.direction = duplicate.direction elif foreground: self.foreground = foreground if not background: background = color.StandardColors.white self.background = background self.spacing = spacing self.width = width self.direction = direction else: raise ValueError,\ 'HatchingPattern must be created with color argument' def SetDirection(self, dir): undo = (self.SetDirection, self.direction) self.direction = dir return undo def Direction(self): return self.direction def SetSpacing(self, spacing): undo = (self.SetSpacing, self.spacing) self.spacing = spacing return undo def Spacing(self): return self.spacing def Width(self): return self.width def Transform(self, trafo, rects = None): # XXX: should spacing be transformed as well? Should the pattern be # transformed at all? dir = trafo.DTransform(self.direction).normalized() return self.SetDirection(dir) def SetForeground(self, foreground): undo = (self.SetForeground, self.foreground) self.foreground = foreground return undo def Foreground(self): return self.foreground def SetBackground(self, color): undo = (self.SetBackground, self.background) self.background = color return undo def Background(self): return self.background def Execute(self, device, rect): left, bottom, right, top = rect dy = self.spacing if dy > 0: device.SetFillColor(self.background) device.FillRectangle(left, top, right, bottom) device.PushTrafo() vx, vy = self.direction angle = atan2(vy, vx) center = rect.center() rot = Rotation(angle, center) left, bottom, right, top = rot(rect) device.Concat(rot) device.Translate(center) height = top - bottom width = right - left steps = int(height / dy + 1) y = height / 2 x = width / 2 device.SetLineColor(self.foreground) device.SetLineAttributes(self.width) drawline = device.DrawLineXY for i in range(steps): drawline(-x, y, +x, y) y = y - dy device.PopTrafo() else: device.SetFillColor(self.foreground) device.FillRectangle(left, bottom, right, top) def Blend(self, other, frac1, frac2): if other.__class__ == self.__class__: fg = other.foreground bg = other.background dir = other.direction spacing = other.spacing width = other.width elif other.__class__ == SolidPattern: fg = bg = other.Color() dir = self.direction spacing = self.spacing width = self.width else: raise MismatchError return HatchingPattern(Blend(self.foreground, fg, frac1, frac2), Blend(self.background, bg, frac1, frac2), frac1 * self.direction + frac2 * dir, frac1 * self.spacing + frac2 * spacing, frac1 * self.width + frac2 * width) def SaveToFile(self, file): file.HatchingPattern(self.foreground, self.background, self.direction, self.spacing, self.width) class ImageTilePattern(Pattern): is_Tiled = 1 is_Image = 1 data = None def __init__(self, data = None, trafo = None, duplicate = None): if duplicate is not None: data = duplicate.data self.trafo = duplicate.trafo else: if trafo is None: #width, height = data.size trafo = Trafo(1, 0, 0, -1, 0, 0) self.trafo = trafo self.data = data def set_transformation(self, trafo): undo = (self.set_transformation, self.trafo) self.trafo = trafo return undo def Transform(self, trafo, rects = None): if rects: r1, r2 = rects trafo = trafo(Translation(r1.left, r1.top)) trafo = Translation(-r2.left, -r2.top)(trafo) return self.set_transformation(trafo(self.trafo)) def Execute(self, device, rect): device.TileImage(self.data, Translation(rect.left, rect.top)(self.trafo)) def Blend(self, other, frac1, frac2): if self.__class__ == other.__class__: if self.data is other.data: return self.__class__(self.data, BlendTrafo(self.trafo, other.trafo, frac1, frac2)) raise MismatchError def SaveToFile(self, file): file.ImageTilePattern(self.data, self.trafo)