change the coefficients order in realpolyroots
[PyX/mjg.git] / pyx / deco.py
blob3f2f0e7594b94d088d362db5a3974883a4b9ab9d
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, helper, 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
41 # Decorated path
44 class decoratedpath(canvas.canvasitem):
45 """Decorated path
47 The main purpose of this class is during the drawing
48 (stroking/filling) of a path. It collects attributes for the
49 stroke and/or fill operations.
50 """
52 def __init__(self, path, strokepath=None, fillpath=None,
53 styles=None, strokestyles=None, fillstyles=None,
54 ornaments=None):
56 self.path = path
58 # global style for stroking and filling and subdps
59 self.styles = styles
61 # styles which apply only for stroking and filling
62 self.strokestyles = strokestyles
63 self.fillstyles = fillstyles
65 # the decoratedpath can contain additional elements of the
66 # path (ornaments), e.g., arrowheads.
67 if ornaments is None:
68 self.ornaments = canvas.canvas()
69 else:
70 self.ornaments = ornaments
72 self.nostrokeranges = None
74 def ensurenormpath(self):
75 """convert self.path into a normpath"""
76 assert self.nostrokeranges is None or isinstance(self.path, path.normpath), "you don't understand what you are doing"
77 self.path = self.path.normpath()
79 def excluderange(self, begin, end):
80 assert isinstance(self.path, path.normpath), "you don't understand what this is about"
81 if self.nostrokeranges is None:
82 self.nostrokeranges = [(begin, end)]
83 else:
84 ibegin = 0
85 while ibegin < len(self.nostrokeranges) and self.nostrokeranges[ibegin][1] < begin:
86 ibegin += 1
88 if ibegin == len(self.nostrokeranges):
89 self.nostrokeranges.append((begin, end))
90 return
92 iend = len(self.nostrokeranges) - 1
93 while 0 <= iend and end < self.nostrokeranges[iend][0]:
94 iend -= 1
96 if iend == -1:
97 self.nostrokeranges.insert(0, (begin, end))
98 return
100 if self.nostrokeranges[ibegin][0] < begin:
101 begin = self.nostrokeranges[ibegin][0]
102 if end < self.nostrokeranges[iend][1]:
103 end = self.nostrokeranges[iend][1]
105 self.nostrokeranges[ibegin:iend+1] = [(begin, end)]
107 def bbox(self):
108 pathbbox = self.path.bbox()
109 ornamentsbbox = self.ornaments.bbox()
110 if ornamentsbbox is not None:
111 return ornamentsbbox + pathbbox
112 else:
113 return pathbbox
115 def registerPS(self, registry):
116 if self.styles:
117 for style in self.styles:
118 style.registerPS(registry)
119 if self.fillstyles:
120 for style in self.fillstyles:
121 style.registerPS(registry)
122 if self.strokestyles:
123 for style in self.strokestyles:
124 style.registerPS(registry)
125 self.ornaments.registerPS(registry)
127 def registerPDF(self, registry):
128 if self.styles:
129 for style in self.styles:
130 style.registerPDF(registry)
131 if self.fillstyles:
132 for style in self.fillstyles:
133 style.registerPDF(registry)
134 if self.strokestyles:
135 for style in self.strokestyles:
136 style.registerPDF(registry)
137 self.ornaments.registerPDF(registry)
139 def strokepath(self):
140 if self.nostrokeranges:
141 splitlist = []
142 for begin, end in self.nostrokeranges:
143 splitlist.append(begin)
144 splitlist.append(end)
145 split = self.path.split(splitlist)
146 # XXX properly handle closed paths?
147 result = split[0]
148 for i in range(2, len(split), 2):
149 result += split[i]
150 return result
151 else:
152 return self.path
154 def outputPS(self, file, writer, context):
155 # draw (stroke and/or fill) the decoratedpath on the canvas
156 # while trying to produce an efficient output, e.g., by
157 # not writing one path two times
159 # small helper
160 def _writestyles(styles, context):
161 for style in styles:
162 style.outputPS(file, writer, context)
164 if self.strokestyles is None and self.fillstyles is None:
165 raise RuntimeError("Path neither to be stroked nor filled")
167 strokepath = self.strokepath()
168 fillpath = self.path
170 # apply global styles
171 if self.styles:
172 file.write("gsave\n")
173 context = context()
174 _writestyles(self.styles, context)
176 if self.fillstyles is not None:
177 file.write("newpath\n")
178 fillpath.outputPS(file, writer, context)
180 if self.strokestyles is not None and strokepath is fillpath:
181 # do efficient stroking + filling if respective paths are identical
182 file.write("gsave\n")
184 if self.fillstyles:
185 _writestyles(self.fillstyles, context())
187 file.write("fill\n")
188 file.write("grestore\n")
190 if self.strokestyles:
191 file.write("gsave\n")
192 _writestyles(self.strokestyles, context())
194 file.write("stroke\n")
196 if self.strokestyles:
197 file.write("grestore\n")
198 else:
199 # only fill fillpath - for the moment
200 if self.fillstyles:
201 file.write("gsave\n")
202 _writestyles(self.fillstyles, context())
204 file.write("fill\n")
206 if self.fillstyles:
207 file.write("grestore\n")
209 if self.strokestyles is not None and (strokepath is not fillpath or self.fillstyles is None):
210 # this is the only relevant case still left
211 # Note that a possible stroking has already been done.
212 oldcontext = context
213 context = context()
215 if self.strokestyles:
216 file.write("gsave\n")
217 _writestyles(self.strokestyles, context)
219 file.write("newpath\n")
220 strokepath.outputPS(file, writer, context)
221 file.write("stroke\n")
223 if self.strokestyles:
224 file.write("grestore\n")
226 context = oldcontext
228 # now, draw additional elements of decoratedpath
229 self.ornaments.outputPS(file, writer, context)
231 # restore global styles
232 if self.styles:
233 file.write("grestore\n")
235 def outputPDF(self, file, writer, context):
236 # draw (stroke and/or fill) the decoratedpath on the canvas
238 def _writestyles(styles, context):
239 for style in styles:
240 style.outputPDF(file, writer, context)
242 def _writestrokestyles(strokestyles, context):
243 for style in strokestyles:
244 style.outputPDF(file, writer, context(fillattr=0))
246 def _writefillstyles(fillstyles, context):
247 for style in fillstyles:
248 style.outputPDF(file, writer, context(strokeattr=0))
250 if self.strokestyles is None and self.fillstyles is None:
251 raise RuntimeError("Path neither to be stroked nor filled")
253 strokepath = self.strokepath()
254 fillpath = self.path
256 # apply global styles
257 if self.styles:
258 file.write("q\n") # gsave
259 context = context()
260 _writestyles(self.styles, context)
262 if self.fillstyles is not None:
263 fillpath.outputPDF(file, writer, context)
265 if self.strokestyles is not None and strokepath is fillpath:
266 # do efficient stroking + filling
267 file.write("q\n") # gsave
268 oldcontext = context
269 context = context()
271 if self.fillstyles:
272 _writefillstyles(self.fillstyles, context)
273 if self.strokestyles:
274 _writestrokestyles(self.strokestyles, context)
276 file.write("B\n") # both stroke and fill
277 file.write("Q\n") # grestore
278 context = oldcontext
279 else:
280 # only fill fillpath - for the moment
281 if self.fillstyles:
282 file.write("q\n") # gsave
283 _writefillstyles(self.fillstyles, context())
285 file.write("f\n") # fill
287 if self.fillstyles:
288 file.write("Q\n") # grestore
290 if self.strokestyles is not None and (strokepath is not fillpath or self.fillstyles is None):
291 # this is the only relevant case still left
292 # Note that a possible stroking has already been done.
293 oldcontext = context
294 context = context()
296 if self.strokestyles:
297 file.write("q\n") # gsave
298 _writestrokestyles(self.strokestyles, context)
300 strokepath.outputPDF(file, writer, context)
301 file.write("S\n") # stroke
303 if self.strokestyles:
304 file.write("Q\n") # grestore
305 context = oldcontext
307 # now, draw additional elements of decoratedpath
308 self.ornaments.outputPDF(file, writer, context)
310 # restore global styles
311 if self.styles:
312 file.write("Q\n") # grestore
315 # Path decorators
318 class deco:
320 """decorators
322 In contrast to path styles, path decorators depend on the concrete
323 path to which they are applied. In particular, they don't make
324 sense without any path and can thus not be used in canvas.set!
328 def decorate(self, dp):
329 """apply a style to a given decoratedpath object dp
331 decorate accepts a decoratedpath object dp, applies PathStyle
332 by modifying dp in place and returning the new dp.
335 pass
338 # stroked and filled: basic decos which stroked and fill,
339 # respectively the path
342 class _stroked(deco, attr.exclusiveattr):
344 """stroked is a decorator, which draws the outline of the path"""
346 def __init__(self, styles=[]):
347 attr.exclusiveattr.__init__(self, _stroked)
348 self.styles = attr.mergeattrs(styles)
349 attr.checkattrs(self.styles, [style.strokestyle])
351 def __call__(self, styles=[]):
352 # XXX or should we also merge self.styles
353 return _stroked(styles)
355 def decorate(self, dp):
356 if dp.strokestyles is not None:
357 raise RuntimeError("Cannot stroke an already stroked path")
358 dp.strokestyles = self.styles
359 return dp
361 stroked = _stroked()
362 stroked.clear = attr.clearclass(_stroked)
365 class _filled(deco, attr.exclusiveattr):
367 """filled is a decorator, which fills the interior of the path"""
369 def __init__(self, styles=[]):
370 attr.exclusiveattr.__init__(self, _filled)
371 self.styles = attr.mergeattrs(styles)
372 attr.checkattrs(self.styles, [style.fillstyle])
374 def __call__(self, styles=[]):
375 # XXX or should we also merge self.styles
376 return _filled(styles)
378 def decorate(self, dp):
379 if dp.fillstyles is not None:
380 raise RuntimeError("Cannot fill an already filled path")
381 dp.fillstyles = self.styles
382 return dp
384 filled = _filled()
385 filled.clear = attr.clearclass(_filled)
388 # Arrows
391 # helper function which constructs the arrowhead
393 def _arrowhead(anormpath, size, angle, constrictionlen, reversed):
395 """helper routine, which returns an arrowhead from a given anormpath
397 returns arrowhead at begin of anormpath with size,
398 opening angle and constriction length constrictionlen. If constrictionlen is None, we
399 do not add a constriction.
402 if reversed:
403 anormpath = anormpath.reversed()
404 alen = anormpath.arclentoparam(size)
405 tx, ty = anormpath.atbegin()
407 # now we construct the template for our arrow but cutting
408 # the path a the corresponding length
409 arrowtemplate = anormpath.split(alen)[0]
411 # from this template, we construct the two outer curves
412 # of the arrow
413 arrowl = arrowtemplate.transformed(trafo.rotate(-angle/2.0, tx, ty))
414 arrowr = arrowtemplate.transformed(trafo.rotate( angle/2.0, tx, ty))
416 # now come the joining backward parts
418 if constrictionlen is not None:
419 # constriction point (cx, cy) lies on path
420 cx, cy = anormpath.at(anormpath.arclentoparam(constrictionlen))
421 arrowcr= path.line(*(arrowr.atend() + (cx,cy)))
422 arrow = arrowl.reversed() << arrowr << arrowcr
423 else:
424 arrow = arrowl.reversed() << arrowr
426 arrow[-1].close()
428 return arrow
431 _base = 6 * unit.v_pt
433 class arrow(deco, attr.attr):
435 """arrow is a decorator which adds an arrow to either side of the path"""
437 def __init__(self, attrs=[], position=0, size=_base, angle=45, constriction=0.8):
438 self.attrs = attr.mergeattrs([style.linestyle.solid, filled] + attrs)
439 attr.checkattrs(self.attrs, [deco, style.fillstyle, style.strokestyle])
440 self.position = position
441 self.size = size
442 self.angle = angle
443 self.constriction = constriction
445 def __call__(self, attrs=None, position=None, size=None, angle=None, constriction=helper.nodefault):
446 if attrs is None:
447 attrs = self.attrs
448 if position is None:
449 position = self.position
450 if size is None:
451 size = self.size
452 if angle is None:
453 angle = self.angle
454 if constriction is helper.nodefault:
455 constriction = self.constriction
456 return arrow(attrs=attrs, position=position, size=size, angle=angle, constriction=constriction)
458 def decorate(self, dp):
459 dp.ensurenormpath()
460 anormpath = dp.path
462 # calculate absolute arc length of constricition
463 # Note that we have to correct this length because the arrowtemplates are rotated
464 # by self.angle/2 to the left and right. Hence, if we want no constriction, i.e., for
465 # self.constriction = 1, we actually have a length which is approximately shorter
466 # by the given geometrical factor.
467 if self.constriction is not None:
468 constrictionlen = arrowheadconstrictionlen = self.size * self.constriction * math.cos(radians(self.angle/2.0))
469 else:
470 # if we do not want a constriction, i.e. constriction is None, we still
471 # need constrictionlen for cutting the path
472 constrictionlen = self.size * 1 * math.cos(radians(self.angle/2.0))
473 arrowheadconstrictionlen = None
475 if self.position == 0:
476 arrowhead = _arrowhead(anormpath, self.size, self.angle, arrowheadconstrictionlen, reversed=0)
477 else:
478 arrowhead = _arrowhead(anormpath, self.size, self.angle, arrowheadconstrictionlen, reversed=1)
480 # add arrowhead to decoratedpath
481 dp.ornaments.draw(arrowhead, self.attrs)
483 if self.position == 0:
484 # exclude first part of the first normsubpath from stroking
485 dp.excluderange(0, min(self.size, constrictionlen))
486 else:
487 dp.excluderange(anormpath.end() - min(self.size, constrictionlen), anormpath.end())
489 return dp
491 arrow.clear = attr.clearclass(arrow)
493 # arrows at begin of path
494 barrow = arrow(position=0)
495 barrow.SMALL = barrow(size=_base/math.sqrt(64))
496 barrow.SMALl = barrow(size=_base/math.sqrt(32))
497 barrow.SMAll = barrow(size=_base/math.sqrt(16))
498 barrow.SMall = barrow(size=_base/math.sqrt(8))
499 barrow.Small = barrow(size=_base/math.sqrt(4))
500 barrow.small = barrow(size=_base/math.sqrt(2))
501 barrow.normal = barrow(size=_base)
502 barrow.large = barrow(size=_base*math.sqrt(2))
503 barrow.Large = barrow(size=_base*math.sqrt(4))
504 barrow.LArge = barrow(size=_base*math.sqrt(8))
505 barrow.LARge = barrow(size=_base*math.sqrt(16))
506 barrow.LARGe = barrow(size=_base*math.sqrt(32))
507 barrow.LARGE = barrow(size=_base*math.sqrt(64))
509 # arrows at end of path
510 earrow = arrow(position=1)
511 earrow.SMALL = earrow(size=_base/math.sqrt(64))
512 earrow.SMALl = earrow(size=_base/math.sqrt(32))
513 earrow.SMAll = earrow(size=_base/math.sqrt(16))
514 earrow.SMall = earrow(size=_base/math.sqrt(8))
515 earrow.Small = earrow(size=_base/math.sqrt(4))
516 earrow.small = earrow(size=_base/math.sqrt(2))
517 earrow.normal = earrow(size=_base)
518 earrow.large = earrow(size=_base*math.sqrt(2))
519 earrow.Large = earrow(size=_base*math.sqrt(4))
520 earrow.LArge = earrow(size=_base*math.sqrt(8))
521 earrow.LARge = earrow(size=_base*math.sqrt(16))
522 earrow.LARGe = earrow(size=_base*math.sqrt(32))
523 earrow.LARGE = earrow(size=_base*math.sqrt(64))