remove helper.nodefault in favour of module-local _marker classes
[PyX.git] / pyx / deco.py
blobc17d48407b639fe3ac60a5b7c32a4170d496db44
1 #!/usr/bin/env python
2 # -*- coding: ISO-8859-1 -*-
5 # Copyright (C) 2002-2005 Jörg Lehmann <joergl@users.sourceforge.net>
6 # Copyright (C) 2003-2004 Michael Schindler <m-schindler@users.sourceforge.net>
7 # Copyright (C) 2002-2005 André Wobst <wobsta@users.sourceforge.net>
9 # This file is part of PyX (http://pyx.sourceforge.net/).
11 # PyX is free software; you can redistribute it and/or modify
12 # it under the terms of the GNU General Public License as published by
13 # the Free Software Foundation; either version 2 of the License, or
14 # (at your option) any later version.
16 # PyX is distributed in the hope that it will be useful,
17 # but WITHOUT ANY WARRANTY; without even the implied warranty of
18 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 # GNU General Public License for more details.
21 # You should have received a copy of the GNU General Public License
22 # along with PyX; if not, write to the Free Software
23 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
25 # TODO:
26 # - should we improve on the arc length -> arg parametrization routine or
27 # should we at least factor it out?
29 from __future__ import nested_scopes
31 import sys, math
32 import attr, canvas, color, path, style, trafo, unit
34 try:
35 from math import radians
36 except ImportError:
37 # fallback implementation for Python 2.1 and below
38 def radians(x): return x*math.pi/180
40 class _marker: pass
43 # Decorated path
46 class decoratedpath(canvas.canvasitem):
47 """Decorated path
49 The main purpose of this class is during the drawing
50 (stroking/filling) of a path. It collects attributes for the
51 stroke and/or fill operations.
52 """
54 def __init__(self, path, strokepath=None, fillpath=None,
55 styles=None, strokestyles=None, fillstyles=None,
56 ornaments=None):
58 self.path = path
60 # global style for stroking and filling and subdps
61 self.styles = styles
63 # styles which apply only for stroking and filling
64 self.strokestyles = strokestyles
65 self.fillstyles = fillstyles
67 # the decoratedpath can contain additional elements of the
68 # path (ornaments), e.g., arrowheads.
69 if ornaments is None:
70 self.ornaments = canvas.canvas()
71 else:
72 self.ornaments = ornaments
74 self.nostrokeranges = None
76 def ensurenormpath(self):
77 """convert self.path into a normpath"""
78 assert self.nostrokeranges is None or isinstance(self.path, path.normpath), "you don't understand what you are doing"
79 self.path = self.path.normpath()
81 def excluderange(self, begin, end):
82 assert isinstance(self.path, path.normpath), "you don't understand what this is about"
83 if self.nostrokeranges is None:
84 self.nostrokeranges = [(begin, end)]
85 else:
86 ibegin = 0
87 while ibegin < len(self.nostrokeranges) and self.nostrokeranges[ibegin][1] < begin:
88 ibegin += 1
90 if ibegin == len(self.nostrokeranges):
91 self.nostrokeranges.append((begin, end))
92 return
94 iend = len(self.nostrokeranges) - 1
95 while 0 <= iend and end < self.nostrokeranges[iend][0]:
96 iend -= 1
98 if iend == -1:
99 self.nostrokeranges.insert(0, (begin, end))
100 return
102 if self.nostrokeranges[ibegin][0] < begin:
103 begin = self.nostrokeranges[ibegin][0]
104 if end < self.nostrokeranges[iend][1]:
105 end = self.nostrokeranges[iend][1]
107 self.nostrokeranges[ibegin:iend+1] = [(begin, end)]
109 def bbox(self):
110 pathbbox = self.path.bbox()
111 ornamentsbbox = self.ornaments.bbox()
112 if ornamentsbbox is not None:
113 return ornamentsbbox + pathbbox
114 else:
115 return pathbbox
117 def registerPS(self, registry):
118 if self.styles:
119 for style in self.styles:
120 style.registerPS(registry)
121 if self.fillstyles:
122 for style in self.fillstyles:
123 style.registerPS(registry)
124 if self.strokestyles:
125 for style in self.strokestyles:
126 style.registerPS(registry)
127 self.ornaments.registerPS(registry)
129 def registerPDF(self, registry):
130 if self.styles:
131 for style in self.styles:
132 style.registerPDF(registry)
133 if self.fillstyles:
134 for style in self.fillstyles:
135 style.registerPDF(registry)
136 if self.strokestyles:
137 for style in self.strokestyles:
138 style.registerPDF(registry)
139 self.ornaments.registerPDF(registry)
141 def strokepath(self):
142 if self.nostrokeranges:
143 splitlist = []
144 for begin, end in self.nostrokeranges:
145 splitlist.append(begin)
146 splitlist.append(end)
147 split = self.path.split(splitlist)
148 # XXX properly handle closed paths?
149 result = split[0]
150 for i in range(2, len(split), 2):
151 result += split[i]
152 return result
153 else:
154 return self.path
156 def outputPS(self, file, writer, context):
157 # draw (stroke and/or fill) the decoratedpath on the canvas
158 # while trying to produce an efficient output, e.g., by
159 # not writing one path two times
161 # small helper
162 def _writestyles(styles, context):
163 for style in styles:
164 style.outputPS(file, writer, context)
166 if self.strokestyles is None and self.fillstyles is None:
167 raise RuntimeError("Path neither to be stroked nor filled")
169 strokepath = self.strokepath()
170 fillpath = self.path
172 # apply global styles
173 if self.styles:
174 file.write("gsave\n")
175 context = context()
176 _writestyles(self.styles, context)
178 if self.fillstyles is not None:
179 file.write("newpath\n")
180 fillpath.outputPS(file, writer, context)
182 if self.strokestyles is not None and strokepath is fillpath:
183 # do efficient stroking + filling if respective paths are identical
184 file.write("gsave\n")
186 if self.fillstyles:
187 _writestyles(self.fillstyles, context())
189 file.write("fill\n")
190 file.write("grestore\n")
192 if self.strokestyles:
193 file.write("gsave\n")
194 _writestyles(self.strokestyles, context())
196 file.write("stroke\n")
198 if self.strokestyles:
199 file.write("grestore\n")
200 else:
201 # only fill fillpath - for the moment
202 if self.fillstyles:
203 file.write("gsave\n")
204 _writestyles(self.fillstyles, context())
206 file.write("fill\n")
208 if self.fillstyles:
209 file.write("grestore\n")
211 if self.strokestyles is not None and (strokepath is not fillpath or self.fillstyles is None):
212 # this is the only relevant case still left
213 # Note that a possible stroking has already been done.
214 oldcontext = context
215 context = context()
217 if self.strokestyles:
218 file.write("gsave\n")
219 _writestyles(self.strokestyles, context)
221 file.write("newpath\n")
222 strokepath.outputPS(file, writer, context)
223 file.write("stroke\n")
225 if self.strokestyles:
226 file.write("grestore\n")
228 context = oldcontext
230 # now, draw additional elements of decoratedpath
231 self.ornaments.outputPS(file, writer, context)
233 # restore global styles
234 if self.styles:
235 file.write("grestore\n")
237 def outputPDF(self, file, writer, context):
238 # draw (stroke and/or fill) the decoratedpath on the canvas
240 def _writestyles(styles, context):
241 for style in styles:
242 style.outputPDF(file, writer, context)
244 def _writestrokestyles(strokestyles, context):
245 for style in strokestyles:
246 style.outputPDF(file, writer, context(fillattr=0))
248 def _writefillstyles(fillstyles, context):
249 for style in fillstyles:
250 style.outputPDF(file, writer, context(strokeattr=0))
252 if self.strokestyles is None and self.fillstyles is None:
253 raise RuntimeError("Path neither to be stroked nor filled")
255 strokepath = self.strokepath()
256 fillpath = self.path
258 # apply global styles
259 if self.styles:
260 file.write("q\n") # gsave
261 context = context()
262 _writestyles(self.styles, context)
264 if self.fillstyles is not None:
265 fillpath.outputPDF(file, writer, context)
267 if self.strokestyles is not None and strokepath is fillpath:
268 # do efficient stroking + filling
269 file.write("q\n") # gsave
270 oldcontext = context
271 context = context()
273 if self.fillstyles:
274 _writefillstyles(self.fillstyles, context)
275 if self.strokestyles:
276 _writestrokestyles(self.strokestyles, context)
278 file.write("B\n") # both stroke and fill
279 file.write("Q\n") # grestore
280 context = oldcontext
281 else:
282 # only fill fillpath - for the moment
283 if self.fillstyles:
284 file.write("q\n") # gsave
285 _writefillstyles(self.fillstyles, context())
287 file.write("f\n") # fill
289 if self.fillstyles:
290 file.write("Q\n") # grestore
292 if self.strokestyles is not None and (strokepath is not fillpath or self.fillstyles is None):
293 # this is the only relevant case still left
294 # Note that a possible stroking has already been done.
295 oldcontext = context
296 context = context()
298 if self.strokestyles:
299 file.write("q\n") # gsave
300 _writestrokestyles(self.strokestyles, context)
302 strokepath.outputPDF(file, writer, context)
303 file.write("S\n") # stroke
305 if self.strokestyles:
306 file.write("Q\n") # grestore
307 context = oldcontext
309 # now, draw additional elements of decoratedpath
310 self.ornaments.outputPDF(file, writer, context)
312 # restore global styles
313 if self.styles:
314 file.write("Q\n") # grestore
317 # Path decorators
320 class deco:
322 """decorators
324 In contrast to path styles, path decorators depend on the concrete
325 path to which they are applied. In particular, they don't make
326 sense without any path and can thus not be used in canvas.set!
330 def decorate(self, dp):
331 """apply a style to a given decoratedpath object dp
333 decorate accepts a decoratedpath object dp, applies PathStyle
334 by modifying dp in place and returning the new dp.
337 pass
340 # stroked and filled: basic decos which stroked and fill,
341 # respectively the path
344 class _stroked(deco, attr.exclusiveattr):
346 """stroked is a decorator, which draws the outline of the path"""
348 def __init__(self, styles=[]):
349 attr.exclusiveattr.__init__(self, _stroked)
350 self.styles = attr.mergeattrs(styles)
351 attr.checkattrs(self.styles, [style.strokestyle])
353 def __call__(self, styles=[]):
354 # XXX or should we also merge self.styles
355 return _stroked(styles)
357 def decorate(self, dp):
358 if dp.strokestyles is not None:
359 raise RuntimeError("Cannot stroke an already stroked path")
360 dp.strokestyles = self.styles
361 return dp
363 stroked = _stroked()
364 stroked.clear = attr.clearclass(_stroked)
367 class _filled(deco, attr.exclusiveattr):
369 """filled is a decorator, which fills the interior of the path"""
371 def __init__(self, styles=[]):
372 attr.exclusiveattr.__init__(self, _filled)
373 self.styles = attr.mergeattrs(styles)
374 attr.checkattrs(self.styles, [style.fillstyle])
376 def __call__(self, styles=[]):
377 # XXX or should we also merge self.styles
378 return _filled(styles)
380 def decorate(self, dp):
381 if dp.fillstyles is not None:
382 raise RuntimeError("Cannot fill an already filled path")
383 dp.fillstyles = self.styles
384 return dp
386 filled = _filled()
387 filled.clear = attr.clearclass(_filled)
390 # Arrows
393 # helper function which constructs the arrowhead
395 def _arrowhead(anormpath, size, angle, constrictionlen, reversed):
397 """helper routine, which returns an arrowhead from a given anormpath
399 returns arrowhead at begin of anormpath with size,
400 opening angle and constriction length constrictionlen. If constrictionlen is None, we
401 do not add a constriction.
404 if reversed:
405 anormpath = anormpath.reversed()
406 alen = anormpath.arclentoparam(size)
407 tx, ty = anormpath.atbegin()
409 # now we construct the template for our arrow but cutting
410 # the path a the corresponding length
411 arrowtemplate = anormpath.split(alen)[0]
413 # from this template, we construct the two outer curves
414 # of the arrow
415 arrowl = arrowtemplate.transformed(trafo.rotate(-angle/2.0, tx, ty))
416 arrowr = arrowtemplate.transformed(trafo.rotate( angle/2.0, tx, ty))
418 # now come the joining backward parts
420 if constrictionlen is not None:
421 # constriction point (cx, cy) lies on path
422 cx, cy = anormpath.at(anormpath.arclentoparam(constrictionlen))
423 arrowcr= path.line(*(arrowr.atend() + (cx,cy)))
424 arrow = arrowl.reversed() << arrowr << arrowcr
425 else:
426 arrow = arrowl.reversed() << arrowr
428 arrow[-1].close()
430 return arrow
433 _base = 6 * unit.v_pt
435 class arrow(deco, attr.attr):
437 """arrow is a decorator which adds an arrow to either side of the path"""
439 def __init__(self, attrs=[], position=0, size=_base, angle=45, constriction=0.8):
440 self.attrs = attr.mergeattrs([style.linestyle.solid, filled] + attrs)
441 attr.checkattrs(self.attrs, [deco, style.fillstyle, style.strokestyle])
442 self.position = position
443 self.size = size
444 self.angle = angle
445 self.constriction = constriction
447 def __call__(self, attrs=None, position=None, size=None, angle=None, constriction=_marker):
448 if attrs is None:
449 attrs = self.attrs
450 if position is None:
451 position = self.position
452 if size is None:
453 size = self.size
454 if angle is None:
455 angle = self.angle
456 if constriction is _marker:
457 constriction = self.constriction
458 return arrow(attrs=attrs, position=position, size=size, angle=angle, constriction=constriction)
460 def decorate(self, dp):
461 dp.ensurenormpath()
462 anormpath = dp.path
464 # calculate absolute arc length of constricition
465 # Note that we have to correct this length because the arrowtemplates are rotated
466 # by self.angle/2 to the left and right. Hence, if we want no constriction, i.e., for
467 # self.constriction = 1, we actually have a length which is approximately shorter
468 # by the given geometrical factor.
469 if self.constriction is not None:
470 constrictionlen = arrowheadconstrictionlen = self.size * self.constriction * math.cos(radians(self.angle/2.0))
471 else:
472 # if we do not want a constriction, i.e. constriction is None, we still
473 # need constrictionlen for cutting the path
474 constrictionlen = self.size * 1 * math.cos(radians(self.angle/2.0))
475 arrowheadconstrictionlen = None
477 if self.position == 0:
478 arrowhead = _arrowhead(anormpath, self.size, self.angle, arrowheadconstrictionlen, reversed=0)
479 else:
480 arrowhead = _arrowhead(anormpath, self.size, self.angle, arrowheadconstrictionlen, reversed=1)
482 # add arrowhead to decoratedpath
483 dp.ornaments.draw(arrowhead, self.attrs)
485 if self.position == 0:
486 # exclude first part of the first normsubpath from stroking
487 dp.excluderange(0, min(self.size, constrictionlen))
488 else:
489 dp.excluderange(anormpath.end() - min(self.size, constrictionlen), anormpath.end())
491 return dp
493 arrow.clear = attr.clearclass(arrow)
495 # arrows at begin of path
496 barrow = arrow(position=0)
497 barrow.SMALL = barrow(size=_base/math.sqrt(64))
498 barrow.SMALl = barrow(size=_base/math.sqrt(32))
499 barrow.SMAll = barrow(size=_base/math.sqrt(16))
500 barrow.SMall = barrow(size=_base/math.sqrt(8))
501 barrow.Small = barrow(size=_base/math.sqrt(4))
502 barrow.small = barrow(size=_base/math.sqrt(2))
503 barrow.normal = barrow(size=_base)
504 barrow.large = barrow(size=_base*math.sqrt(2))
505 barrow.Large = barrow(size=_base*math.sqrt(4))
506 barrow.LArge = barrow(size=_base*math.sqrt(8))
507 barrow.LARge = barrow(size=_base*math.sqrt(16))
508 barrow.LARGe = barrow(size=_base*math.sqrt(32))
509 barrow.LARGE = barrow(size=_base*math.sqrt(64))
511 # arrows at end of path
512 earrow = arrow(position=1)
513 earrow.SMALL = earrow(size=_base/math.sqrt(64))
514 earrow.SMALl = earrow(size=_base/math.sqrt(32))
515 earrow.SMAll = earrow(size=_base/math.sqrt(16))
516 earrow.SMall = earrow(size=_base/math.sqrt(8))
517 earrow.Small = earrow(size=_base/math.sqrt(4))
518 earrow.small = earrow(size=_base/math.sqrt(2))
519 earrow.normal = earrow(size=_base)
520 earrow.large = earrow(size=_base*math.sqrt(2))
521 earrow.Large = earrow(size=_base*math.sqrt(4))
522 earrow.LArge = earrow(size=_base*math.sqrt(8))
523 earrow.LARge = earrow(size=_base*math.sqrt(16))
524 earrow.LARGe = earrow(size=_base*math.sqrt(32))
525 earrow.LARGE = earrow(size=_base*math.sqrt(64))