mistake in handling column numbers > 9 (thanks to Thomas Lueck for reporting that...
[PyX/mjg.git] / pyx / canvas.py
blobc57616487a58b4670c31c0f89dedd628b9858493
1 #!/usr/bin/env python
4 # Copyright (C) 2002 Jörg Lehmann <joergl@users.sourceforge.net>
5 # Copyright (C) 2002 André Wobst <wobsta@users.sourceforge.net>
7 # This file is part of PyX (http://pyx.sourceforge.net/).
9 # PyX is free software; you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation; either version 2 of the License, or
12 # (at your option) any later version.
14 # PyX is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 # GNU General Public License for more details.
19 # You should have received a copy of the GNU General Public License
20 # along with PyX; if not, write to the Free Software
21 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
23 # TODO:
24 # - should we improve on the arc length -> arg parametrization routine or
25 # should we at least factor it out?
26 # - Should we really set linewidth in canvas.writetofile. Why don't we
27 # rely on the PS default (like for all other PathStyles)
28 # - How should we handle the passing of stroke and fill styles to
29 # arrows? Calls, new instances, ...?
32 """The canvas module provides a PostScript canvas class and related classes
34 A PostScript canvas is the pivotal object for the creation of (E)PS-Files.
35 It collects all the elements that should be displayed (PSCmds) together
36 with attributes if applicable. Furthermore, a canvas can be globally
37 transformed (i.e. translated, rotated, etc.) and clipped.
39 """
41 import types, math, time
42 import pyx
43 import base
44 import bbox, unit, trafo
45 import path
47 # PostScript-procedure definitions
48 # cf. file: 5002.EPSF_Spec_v3.0.pdf
49 # with important correction in EndEPSF:
50 # end operator is missing in the spec!
52 _PSProlog = """/BeginEPSF {
53 /b4_Inc_state save def
54 /dict_count countdictstack def
55 /op_count count 1 sub def
56 userdict begin
57 /showpage { } def
58 0 setgray 0 setlinecap
59 1 setlinewidth 0 setlinejoin
60 10 setmiterlimit [ ] 0 setdash newpath
61 /languagelevel where
62 {pop languagelevel
63 1 ne
64 {false setstrokeadjust false setoverprint
65 } if
66 } if
67 } bind def
68 /EndEPSF {
69 end
70 count op_count sub {pop} repeat
71 countdictstack dict_count sub {end} repeat
72 b4_Inc_state restore
73 } bind def"""
75 # known paperformats as tuple(width, height)
77 _paperformats = { "a4" : ("210 t mm", "297 t mm"),
78 "a3" : ("297 t mm", "420 t mm"),
79 "a2" : ("420 t mm", "594 t mm"),
80 "a1" : ("594 t mm", "840 t mm"),
81 "a0" : ("840 t mm", "1188 t mm"),
82 "a0b" : ("910 t mm", "1370 t mm"),
83 "letter" : ("8.5 t in", "11 t in"),
84 "legal" : ("8.5 t in", "14 t in")}
88 # Exceptions
91 class CanvasException(Exception): pass
94 class linecap(base.PathStyle):
96 """linecap of paths"""
98 def __init__(self, value=0):
99 self.value=value
101 def write(self, file):
102 file.write("%d setlinecap\n" % self.value)
104 linecap.butt = linecap(0)
105 linecap.round = linecap(1)
106 linecap.square = linecap(2)
109 class linejoin(base.PathStyle):
111 """linejoin of paths"""
113 def __init__(self, value=0):
114 self.value=value
116 def write(self, file):
117 file.write("%d setlinejoin\n" % self.value)
119 linejoin.miter = linejoin(0)
120 linejoin.round = linejoin(1)
121 linejoin.bevel = linejoin(2)
124 class miterlimit(base.PathStyle):
126 """miterlimit of paths"""
128 def __init__(self, value=10.0):
129 self.value=value
131 def write(self, file):
132 file.write("%f setmiterlimit\n" % self.value)
135 miterlimit.lessthan180deg = miterlimit(1/math.sin(math.pi*180/360))
136 miterlimit.lessthan90deg = miterlimit(1/math.sin(math.pi*90/360))
137 miterlimit.lessthan60deg = miterlimit(1/math.sin(math.pi*60/360))
138 miterlimit.lessthan45deg = miterlimit(1/math.sin(math.pi*45/360))
139 miterlimit.lessthan11deg = miterlimit(10) # the default, approximately 11.4783 degress
141 class dash(base.PathStyle):
143 """dash of paths"""
145 def __init__(self, pattern=[], offset=0):
146 self.pattern=pattern
147 self.offset=offset
149 def write(self, file):
150 patternstring=""
151 for element in self.pattern:
152 patternstring=patternstring + `element` + " "
154 file.write("[%s] %d setdash\n" % (patternstring, self.offset))
157 class linestyle(base.PathStyle):
159 """linestyle (linecap together with dash) of paths"""
161 def __init__(self, c=linecap.butt, d=dash([])):
162 self.c=c
163 self.d=d
165 def write(self, file):
166 self.c.write(file)
167 self.d.write(file)
169 linestyle.solid = linestyle(linecap.butt, dash([]))
170 linestyle.dashed = linestyle(linecap.butt, dash([2]))
171 linestyle.dotted = linestyle(linecap.round, dash([0, 3]))
172 linestyle.dashdotted = linestyle(linecap.round, dash([0, 3, 3, 3]))
175 class linewidth(base.PathStyle, unit.length):
177 """linewidth of paths"""
179 def __init__(self, l="0 cm"):
180 unit.length.__init__(self, l=l, default_type="w")
182 def write(self, file):
183 file.write("%f setlinewidth\n" % unit.topt(self))
185 _base=0.02
187 linewidth.THIN = linewidth("%f cm" % (_base/math.sqrt(32)))
188 linewidth.THIn = linewidth("%f cm" % (_base/math.sqrt(16)))
189 linewidth.THin = linewidth("%f cm" % (_base/math.sqrt(8)))
190 linewidth.Thin = linewidth("%f cm" % (_base/math.sqrt(4)))
191 linewidth.thin = linewidth("%f cm" % (_base/math.sqrt(2)))
192 linewidth.normal = linewidth("%f cm" % _base)
193 linewidth.thick = linewidth("%f cm" % (_base*math.sqrt(2)))
194 linewidth.Thick = linewidth("%f cm" % (_base*math.sqrt(4)))
195 linewidth.THick = linewidth("%f cm" % (_base*math.sqrt(8)))
196 linewidth.THIck = linewidth("%f cm" % (_base*math.sqrt(16)))
197 linewidth.THICk = linewidth("%f cm" % (_base*math.sqrt(32)))
198 linewidth.THICK = linewidth("%f cm" % (_base*math.sqrt(64)))
202 # Decorated path
205 class DecoratedPath(base.PSCmd):
206 """Decorated path
208 The main purpose of this class is during the drawing
209 (stroking/filling) of a path. It collects attributes for the
210 stroke and/or fill operations.
213 def __init__(self,
214 path, strokepath=None, fillpath=None,
215 styles=None, strokestyles=None, fillstyles=None,
216 subdps=None):
218 self.path = path
220 # path to be stroked or filled (or None)
221 self.strokepath = strokepath
222 self.fillpath = fillpath
224 # global style for stroking and filling and subdps
225 self.styles = styles or []
227 # styles which apply only for stroking and filling
228 self.strokestyles = strokestyles or []
229 self.fillstyles = fillstyles or []
231 # additional elements of the path, e.g., arrowheads,
232 # which are by themselves DecoratedPaths
233 self.subdps = subdps or []
235 def addsubdp(self, subdp):
236 """add a further decorated path to the list of subdps"""
238 self.subdps.append(subdp)
240 def bbox(self):
241 return reduce(lambda x,y: x+y.bbox(),
242 self.subdps,
243 self.path.bbox())
246 def write(self, file):
247 # draw (stroke and/or fill) the DecoratedPath on the canvas
248 # while trying to produce an efficient output, e.g., by
249 # not writing one path two times
251 # small helper
252 def _writestyles(styles, file=file):
253 for style in styles:
254 style.write(file)
257 # apply global styles
258 if self.styles:
259 _gsave().write(file)
260 _writestyles(self.styles)
262 if self.fillpath:
263 _newpath().write(file)
264 self.fillpath.write(file)
266 if self.strokepath==self.fillpath:
267 # do efficient stroking + filling
268 _gsave().write(file)
270 if self.fillstyles:
271 _writestyles(self.fillstyles)
273 _fill().write(file)
274 _grestore().write(file)
276 if self.strokestyles:
277 _gsave().write(file)
278 _writestyles(self.strokestyles)
280 _stroke().write(file)
282 if self.strokestyles:
283 _grestore().write(file)
284 else:
285 # only fill fillpath - for the moment
286 if self.fillstyles:
287 _gsave().write(file)
288 _writestyles(self.fillstyles)
290 _fill().write(file)
292 if self.fillstyles:
293 _grestore().write(file)
295 if self.strokepath and self.strokepath!=self.fillpath:
296 # this is the only relevant case still left
297 # Note that a possible stroking has already been done.
299 if self.strokestyles:
300 _gsave().write(file)
301 _writestyles(self.strokestyles)
303 _newpath().write(file)
304 self.strokepath.write(file)
305 _stroke().write(file)
307 if self.strokestyles:
308 _grestore().write(file)
310 # now, draw additional subdps
311 for subdp in self.subdps:
312 subdp.write(file)
314 # restore global styles
315 if self.styles:
316 _grestore().write(file)
319 # Path decorators
322 class PathDeco:
324 """Path decorators
326 In contrast to path styles, path decorators depend on the concrete
327 path to which they are applied. In particular, they don't make
328 sense without any path and can thus not be used in canvas.set!
332 def decorate(self, dp):
333 """apply a style to a given DecoratedPath object dp
335 decorate accepts a DecoratedPath object dp, applies PathStyle
336 by modifying dp in place and returning the new dp.
339 pass
343 # stroked and filled: basic PathDecos which stroked and fill,
344 # respectively the path
347 class stroked(PathDeco):
349 """stroked is a PathDecorator, which draws the outline of the path"""
351 def __init__(self, *styles):
352 self.styles=list(styles)
354 def decorate(self, dp):
355 dp.strokepath=dp.path
356 dp.strokestyles=self.styles
358 return dp
361 class filled(PathDeco):
363 """filled is a PathDecorator, which fills the interior of the path"""
365 def __init__(self, *styles):
366 self.styles=list(styles)
368 def decorate(self, dp):
369 dp.fillpath=dp.path
370 dp.fillstyles=self.styles
372 return dp
376 # _arrowhead: helper routine
379 def _arrowhead(anormpath, size, angle, constriction):
381 """helper routine, which returns an arrowhead for a normpath
383 returns arrowhead at pos (0: begin, !=0: end) of anormpath with size,
384 opening angle and relative constriction
387 # first order conversion from pts to the bezier curve's
388 # parametrization
390 tlen = unit.topt(anormpath.tangent(0).arclength())
392 alen = unit.topt(size)/tlen
394 if alen>anormpath.range(): alen=anormpath().range()
396 # get tip (tx, ty)
397 tx, ty = anormpath.begin()
399 # now we construct the template for our arrow but cutting
400 # the path a the corresponding length
401 arrowtemplate = anormpath.split(alen)[0]
403 # from this template, we construct the two outer curves
404 # of the arrow
405 arrowl = arrowtemplate.transformed(trafo.rotation(-angle/2.0, tx, ty))
406 arrowr = arrowtemplate.transformed(trafo.rotation( angle/2.0, tx, ty))
408 # now come the joining backward parts
409 if constriction:
410 # arrow with constriction
412 # constriction point (cx, cy) lies on path
413 cx, cy = anormpath.at(constriction*alen)
415 arrowcr= path.line(*(arrowr.end()+(cx,cy)))
417 arrow = arrowl.reversed() << arrowr << arrowcr
418 arrow.append(path.closepath())
419 else:
420 # arrow without constriction
421 arrow = arrowl.reversed() << arrowr
422 arrow.append(path.closepath())
424 return arrow
426 class arrow(PathDeco):
428 """A general arrow"""
430 def __init__(self,
431 position, size, angle=45, constriction=0.8,
432 styles=None, strokestyles=None, fillstyles=None):
433 self.position = position
434 self.size = size
435 self.angle = angle
436 self.constriction = constriction
437 self.styles = styles or []
438 self.strokestyles = strokestyles or []
439 self.fillstyles = fillstyles or []
441 def __call__(self, *styles):
442 fillstyles = reduce(lambda x, y:list(x)+(y.styles or []),
443 filter(lambda x: isinstance(x, filled),
444 styles),
447 strokestyles = reduce(lambda x, y:list(x)+(y.styles or []),
448 filter(lambda x: isinstance(x, stroked),
449 styles),
452 styles = filter(lambda x:
453 not (isinstance(x,filled) or
454 isinstance(x,stroked)),
455 styles)
458 return arrow(position=self.position,
459 size=self.size,
460 angle=self.angle,
461 constriction=self.constriction,
462 styles=styles,
463 strokestyles=strokestyles,
464 fillstyles=fillstyles)
466 def decorate(self, dp):
468 # TODO: error, when strokepath is not defined
470 # convert to normpath if necessary
471 if isinstance(dp.strokepath, path.normpath):
472 anormpath=dp.strokepath
473 else:
474 anormpath=path.normpath(dp.path)
476 if self.position:
477 anormpath=anormpath.reversed()
479 ahead = _arrowhead(anormpath, self.size, self.angle, self.constriction)
481 dp.addsubdp(DecoratedPath(ahead,
482 strokepath=ahead, fillpath=ahead,
483 styles=self.styles,
484 strokestyles=self.strokestyles,
485 fillstyles=self.fillstyles))
487 # the following lines are copied from arrowhead.init()
488 # TODO: can this be done better?
490 # first order conversion from pts to the bezier curve's
491 # parametrization
493 tlen = unit.topt(anormpath.tangent(0).arclength())
495 alen = unit.topt(self.size)/tlen
496 if alen>anormpath.range(): alen=anormpath().range()
498 if self.constriction:
499 ilen = alen*self.constriction
500 else:
501 ilen = alen
503 # correct somewhat for rotation of arrow segments
504 ilen = ilen*math.cos(math.pi*self.angle/360.0)
506 # this is the rest of the path, we have to draw
507 anormpath = anormpath.split(ilen)[1]
509 # go back to original orientation, if necessary
510 if self.position:
511 anormpath=anormpath.reversed()
513 # set the new (shortened) strokepath
514 dp.strokepath=anormpath
516 return dp
519 class barrow(arrow):
521 """arrow at begin of path"""
523 def __init__(self, size, angle=45, constriction=0.8,
524 styles=None, strokestyles=None, fillstyles=None):
525 arrow.__init__(self,
526 position=0,
527 size=size,
528 angle=angle,
529 constriction=constriction,
530 styles=styles,
531 strokestyles=strokestyles,
532 fillstyles=fillstyles)
534 _base = 4
536 barrow.SMALL = barrow("%f v pt" % (_base/math.sqrt(64)))
537 barrow.SMALl = barrow("%f v pt" % (_base/math.sqrt(32)))
538 barrow.SMAll = barrow("%f v pt" % (_base/math.sqrt(16)))
539 barrow.SMall = barrow("%f v pt" % (_base/math.sqrt(8)))
540 barrow.Small = barrow("%f v pt" % (_base/math.sqrt(4)))
541 barrow.small = barrow("%f v pt" % (_base/math.sqrt(2)))
542 barrow.normal = barrow("%f v pt" % _base)
543 barrow.large = barrow("%f v pt" % (_base*math.sqrt(2)))
544 barrow.Large = barrow("%f v pt" % (_base*math.sqrt(4)))
545 barrow.LArge = barrow("%f v pt" % (_base*math.sqrt(8)))
546 barrow.LARge = barrow("%f v pt" % (_base*math.sqrt(16)))
547 barrow.LARGe = barrow("%f v pt" % (_base*math.sqrt(32)))
548 barrow.LARGE = barrow("%f v pt" % (_base*math.sqrt(64)))
551 class earrow(arrow):
553 """arrow at end of path"""
555 def __init__(self, size, angle=45, constriction=0.8,
556 styles=[], strokestyles=[], fillstyles=[]):
557 arrow.__init__(self,
558 position=1,
559 size=size,
560 angle=angle,
561 constriction=constriction,
562 styles=styles,
563 strokestyles=strokestyles,
564 fillstyles=fillstyles)
567 earrow.SMALL = earrow("%f v pt" % (_base/math.sqrt(64)))
568 earrow.SMALl = earrow("%f v pt" % (_base/math.sqrt(32)))
569 earrow.SMAll = earrow("%f v pt" % (_base/math.sqrt(16)))
570 earrow.SMall = earrow("%f v pt" % (_base/math.sqrt(8)))
571 earrow.Small = earrow("%f v pt" % (_base/math.sqrt(4)))
572 earrow.small = earrow("%f v pt" % (_base/math.sqrt(2)))
573 earrow.normal = earrow("%f v pt" % _base)
574 earrow.large = earrow("%f v pt" % (_base*math.sqrt(2)))
575 earrow.Large = earrow("%f v pt" % (_base*math.sqrt(4)))
576 earrow.LArge = earrow("%f v pt" % (_base*math.sqrt(8)))
577 earrow.LARge = earrow("%f v pt" % (_base*math.sqrt(16)))
578 earrow.LARGe = earrow("%f v pt" % (_base*math.sqrt(32)))
579 earrow.LARGE = earrow("%f v pt" % (_base*math.sqrt(64)))
582 # clipping class
585 class clip(base.PSCmd):
587 """class for use in canvas constructor which clips to a path"""
589 def __init__(self, path):
590 """construct a clip instance for a given path"""
591 self.path = path
593 def bbox(self):
594 # as a PSCmd a clipping path has NO influence on the bbox...
595 return bbox.bbox()
597 def clipbbox(self):
598 # ... but for clipping, we nevertheless need the bbox
599 return self.path.bbox()
601 def write(self, file):
602 _newpath().write(file)
603 self.path.write(file)
604 _clip().write(file)
607 # some very primitive Postscript operators
610 class _newpath(base.PSOp):
611 def write(self, file):
612 file.write("newpath\n")
615 class _stroke(base.PSOp):
616 def write(self, file):
617 file.write("stroke\n")
620 class _fill(base.PSOp):
621 def write(self, file):
622 file.write("fill\n")
625 class _clip(base.PSOp):
626 def write(self, file):
627 file.write("clip\n")
630 class _gsave(base.PSOp):
631 def write(self, file):
632 file.write("gsave\n")
635 class _grestore(base.PSOp):
636 def write(self, file):
637 file.write("grestore\n")
640 # The main canvas class
643 class canvas(base.PSCmd):
645 """a canvas is a collection of PSCmds together with PSAttrs"""
647 def __init__(self, *args):
649 """construct a canvas
651 The canvas can be modfied by supplying args, which have
652 to be instances of one of the following classes:
653 - trafo.trafo (leading to a global transformation of the canvas)
654 - canvas.clip (clips the canvas)
655 - base.PathStyle (sets some global attributes of the canvas)
657 Note that, while the first two properties are fixed for the
658 whole canvas, the last one can be changed via canvas.set()
662 self.PSOps = []
663 self.trafo = trafo.trafo()
664 self.clipbbox = bbox.bbox()
666 for arg in args:
667 if isinstance(arg, trafo._trafo):
668 self.trafo = self.trafo*arg
669 self.PSOps.append(arg)
670 elif isinstance(arg, clip):
671 self.clipbbox=(self.clipbbox*
672 arg.clipbbox().transform(self.trafo))
673 self.PSOps.append(arg)
674 else:
675 self.set(arg)
677 def bbox(self):
678 """returns bounding box of canvas"""
679 obbox = reduce(lambda x,y:
680 isinstance(y, base.PSCmd) and x+y.bbox() or x,
681 self.PSOps,
682 bbox.bbox())
684 # transform according to our global transformation and
685 # intersect with clipping bounding box (which have already been
686 # transformed in canvas.__init__())
687 return obbox.transform(self.trafo)*self.clipbbox
689 def write(self, file):
690 for cmd in self.PSOps:
691 cmd.write(file)
693 def writetofile(self, filename, paperformat=None, rotated=0, fittosize=0, margin="1 t cm", bboxenhance="1 t pt"):
694 """write canvas to EPS file
696 If paperformat is set to a known paperformat, the output will be centered on
697 the page.
699 If rotated is set, the output will first be rotated by 90 degrees.
701 If fittosize is set, then the output is scaled to the size of the
702 page (minus margin). In that case, the paperformat the specification
703 of the paperformat is obligatory.
705 returns the canvas
709 if filename[-4:]!=".eps":
710 filename = filename + ".eps"
712 try:
713 file = open(filename, "w")
714 except IOError:
715 assert 0, "cannot open output file" # TODO: Fehlerbehandlung...
717 abbox=self.bbox().enhance(bboxenhance)
718 ctrafo=None # global transformation of canvas
720 if rotated:
721 ctrafo = trafo._rotation(90,
722 0.5*(abbox.llx+abbox.urx),
723 0.5*(abbox.lly+abbox.ury))
725 if paperformat:
726 # center (optionally rotated) output on page
727 try:
728 width, height = _paperformats[paperformat]
729 width = unit.topt(width)
730 height = unit.topt(height)
731 except KeyError:
732 raise KeyError, "unknown paperformat '%s'" % paperformat
734 if not ctrafo: ctrafo=trafo.trafo()
736 ctrafo = ctrafo._translate(0.5*(width -(abbox.urx-abbox.llx))-
737 abbox.llx,
738 0.5*(height-(abbox.ury-abbox.lly))-
739 abbox.lly)
741 if fittosize:
742 # scale output to pagesize - margins
743 margin=unit.topt(margin)
745 if rotated:
746 sfactor = min((height-2*margin)/(abbox.urx-abbox.llx),
747 (width-2*margin)/(abbox.ury-abbox.lly))
748 else:
749 sfactor = min((width-2*margin)/(abbox.urx-abbox.llx),
750 (height-2*margin)/(abbox.ury-abbox.lly))
752 ctrafo = ctrafo._scale(sfactor, sfactor, 0.5*width, 0.5*height)
755 elif fittosize:
756 assert 0, "must specify paper size for fittosize" # TODO: exception...
758 # if there has been a global transformation, adjust the bounding box
759 # accordingly
760 if ctrafo: abbox = abbox.transform(ctrafo)
762 file.write("%!PS-Adobe-3.0 EPSF 3.0\n")
763 abbox.write(file)
764 file.write("%%%%Creator: PyX %s\n" % pyx.__version__)
765 file.write("%%%%Title: %s\n" % filename)
766 file.write("%%%%CreationDate: %s\n" %
767 time.asctime(time.localtime(time.time())))
768 file.write("%%EndComments\n")
770 file.write("%%BeginProlog\n")
771 file.write(_PSProlog)
772 file.write("\n%%EndProlog\n")
774 # again, if there has occured global transformation, apply it now
775 if ctrafo: ctrafo.write(file)
777 file.write("%f setlinewidth\n" % unit.topt(linewidth.normal))
779 # here comes the actual content
780 self.write(file)
782 file.write("showpage\n")
783 file.write("%%Trailer\n")
784 file.write("%%EOF\n")
786 return self
788 def insert(self, *PSOps):
789 """insert one or more PSOps in the canvas
791 All canvases will be encapsulated in a gsave/grestore pair.
793 returns the (last) PSOp
797 for PSOp in PSOps:
798 if isinstance(PSOp, canvas):
799 self.PSOps.append(_gsave())
801 self.PSOps.append(PSOp)
803 if isinstance(PSOp, canvas):
804 self.PSOps.append(_grestore())
806 # save last command for return value
807 lastop = PSOp
809 return lastop
811 def set(self, *styles):
812 """sets styles args globally for the rest of the canvas
814 returns canvas
818 for style in styles:
819 if not isinstance(style, base.PathStyle):
820 raise NotImplementedError, "can only set PathStyle"
822 self.insert(style)
824 return self
826 def draw(self, path, *args):
827 """draw path on canvas using the style given by args
829 The argument list args consists of PathStyles, which modify
830 the appearance of the path, or PathDecos,
831 which add some new visual elements to the path.
833 returns the canvas
837 dp = DecoratedPath(path)
839 # XXX: use attrlist
840 if [x for x in args if not (isinstance(x, base.PathStyle) or
841 isinstance(x, PathDeco))]:
842 raise ValueError("Only instances of base.PathStyle or canvas.PathDeco are allowed")
844 # set global styles
845 dp.styles = filter(lambda x: isinstance(x, base.PathStyle), args)
847 # add path decorations and modify path accordingly
848 for deco in filter(lambda x: isinstance(x, PathDeco), args):
849 dp = deco.decorate(dp)
851 self.insert(dp)
853 return self
855 def stroke(self, path, *args):
856 """stroke path on canvas using the style given by args
858 The argument list args consists of PathStyles, which modify
859 the appearance of the path, or PathDecos,
860 which add some new visual elements to the path.
862 returns the canvas
866 return self.draw(path, stroked(), *args)
868 def fill(self, path, *args):
869 """fill path on canvas using the style given by args
871 The argument list args consists of PathStyles, which modify
872 the appearance of the path, or PathDecos,
873 which add some new visual elements to the path.
875 returns the canvas
879 return self.draw(path, filled(), *args)