7ac4d83d73e3151d3d25071ac5f3bd259e9d03ff
[vrm.git] / vrm.py
1 #!BPY
2 """
3 Name: 'VRM'
4 Blender: 245
5 Group: 'Render'
6 Tooltip: 'Vector Rendering Method script'
7 """
8
9 __author__ = "Antonio Ospite"
10 __url__ = ["http://projects.blender.org/projects/vrm"]
11 __version__ = "0.3.beta"
12
13 __bpydoc__ = """\
14     Render the scene and save the result in vector format.
15 """
16
17 # ---------------------------------------------------------------------
18 #    Copyright (c) 2006, 2007, 2008 Antonio Ospite
19 #
20 #    This program is free software; you can redistribute it and/or modify
21 #    it under the terms of the GNU General Public License as published by
22 #    the Free Software Foundation; either version 2 of the License, or
23 #    (at your option) any later version.
24 #
25 #    This program is distributed in the hope that it will be useful,
26 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
27 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
28 #    GNU General Public License for more details.
29 #
30 #    You should have received a copy of the GNU General Public License
31 #    along with this program; if not, write to the Free Software
32 #    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
33 #
34 # ---------------------------------------------------------------------
35 #
36 # Additional credits:
37 #   Thanks to Emilio Aguirre for S2flender from which I took inspirations :)
38 #   Thanks to Nikola Radovanovic, the author of the original VRM script,
39 #       the code you read here has been rewritten _almost_ entirely
40 #       from scratch but Nikola gave me the idea, so I thank him publicly.
41 #
42 # ---------------------------------------------------------------------
43 #
44 # Things TODO for a next release:
45 #   - Shadeless shader
46 #   - FIX the issue with negative scales in object tranformations!
47 #   - Use a better depth sorting algorithm
48 #   - Review how selections are made (this script uses selection states of
49 #     primitives to represent visibility infos)
50 #   - Use a data structure other than Mesh to represent the 2D image?
51 #     Think to a way to merge (adjacent) polygons that have the same color.
52 #     Or a way to use paths for silhouettes and contours.
53 #   - Consider SMIL for animation handling instead of ECMA Script? (Firefox do
54 #     not support SMIL for animations)
55 #   - Switch to the Mesh structure, should be considerably faster
56 #     (partially done, but with Mesh we cannot sort faces, yet)
57 #   - Implement Edge Styles (silhouettes, contours, etc.) (partially done).
58 #   - Implement Shading Styles? (partially done, to make more flexible).
59 #   - Add Vector Writers other than SVG.
60 #   - set the background color!
61 #   - Check memory use!!
62 #
63 # ---------------------------------------------------------------------
64 #
65 # Changelog:
66 #
67 #   vrm-0.3.py  - ...
68 #     * Adapted to blender API 2.45
69 #     * First release after code restucturing.
70 #       Now the script offers a useful set of functionalities
71 #       and it can render animations, too.
72 #     * Optimization in Renderer.doEdgeStyle(), build a topology cache
73 #       so to speed up the lookup of adjacent faces of an edge.
74 #       Thanks ideasman42.
75 #     * The SVG output is now SVG 1.0 valid.
76 #       Checked with: http://jiggles.w3.org/svgvalidator/ValidatorURI.html
77 #     * Progress indicator during HSR.
78 #     * Initial SWF output support (using ming)
79 #     * Fixed a bug in the animation code, now the projection matrix is
80 #       recalculated at each frame!
81 #     * PDF output (using reportlab)
82 #     * Fixed another problem in the animation code the current frame was off
83 #       by one in the case of camera movement.
84 #     * Use fps as specified in blender when VectorWriter handles animation
85 #     * Remove the real file opening in the abstract VectorWriter
86 #     * View frustum clipping
87 #     * Scene clipping done using bounding box instead of object center
88 #     * Fix camera type selection for blender>2.43 (Thanks to Thomas Lachmann)
89 #     * Compatibility with python 2.3
90 #     * Process only object that are on visible layers.
91 #     * Saving config to registry (Thanks to Thomas Lachmann for a draft
92 #       implementation)
93 #
94 # ---------------------------------------------------------------------
95
96 import Blender
97 from Blender import Scene, Object, Mesh, NMesh, Material, Lamp, Camera, Window
98 from Blender.Mathutils import *
99 from math import *
100 import sys, time
101
102 def uniq(alist):
103     tmpdict = dict()
104     return [tmpdict.setdefault(e,e) for e in alist if e not in tmpdict]
105     # in python > 2.4 we ca use the following
106     #return [ u for u in alist if u not in locals()['_[1]'] ]
107
108
109 # Constants
110 EPS = 10e-5
111
112 # We use a global progress Indicator Object
113 progress = None
114
115
116 # Config class for global settings
117
118 class config:
119     polygons = dict()
120     polygons['SHOW'] = True
121     polygons['SHADING'] = 'FLAT' # FLAT or TOON
122     polygons['HSR'] = 'PAINTER' # PAINTER or NEWELL
123     # Hidden to the user for now
124     polygons['EXPANSION_TRICK'] = True
125
126     polygons['TOON_LEVELS'] = 2
127
128     edges = dict()
129     edges['SHOW'] = False
130     edges['SHOW_HIDDEN'] = False
131     edges['STYLE'] = 'MESH' # MESH or SILHOUETTE
132     edges['WIDTH'] = 2
133     edges['COLOR'] = [0, 0, 0]
134
135     output = dict()
136     output['FORMAT'] = 'SVG'
137     output['ANIMATION'] = False
138     output['JOIN_OBJECTS'] = True
139
140     def saveToRegistry():
141         registry = {}
142
143         for k,v in config.__dict__.iteritems():
144
145             # config class store settings in dictionaries
146             if v.__class__ == dict().__class__:
147
148                 regkey_prefix = k.upper()+"_"
149
150                 for opt_k,opt_v in v.iteritems():
151                     regkey = regkey_prefix + opt_k
152
153                     registry[regkey] = opt_v
154
155         Blender.Registry.SetKey('VRM', registry, True)
156
157     saveToRegistry = staticmethod(saveToRegistry)
158
159     def loadFromRegistry():
160         registry = Blender.Registry.GetKey('VRM', True)
161         if not registry:
162                return
163
164         for k,v in registry.iteritems():
165             k_tmp = k.split('_')
166             conf_attr = k_tmp[0].lower()
167             conf_key = str.join("_",k_tmp[1:])
168             conf_val = v
169
170             if config.__dict__.has_key(conf_attr):
171                 config.__dict__[conf_attr][conf_key] = conf_val
172
173     loadFromRegistry = staticmethod(loadFromRegistry)
174
175
176 # Utility functions
177 print_debug = False
178
179 def dumpfaces(flist, filename):
180     """Dump a single face to a file.
181     """
182     if not print_debug:
183         return
184
185     class tmpmesh:
186         pass
187
188     m = tmpmesh()
189     m.faces = flist
190
191     writerobj = SVGVectorWriter(filename)
192
193     writerobj.open()
194     writerobj._printPolygons(m)
195
196     writerobj.close()
197
198 def debug(msg):
199     if print_debug:
200         sys.stderr.write(msg)
201
202 def EQ(v1, v2):
203     return (abs(v1[0]-v2[0]) < EPS and
204             abs(v1[1]-v2[1]) < EPS )
205 by_furthest_z = (lambda f1, f2:
206     cmp(max([v.co[2] for v in f1]), max([v.co[2] for v in f2])+EPS)
207     )
208
209 def sign(x):
210
211     if x < -EPS:
212     #if x < 0:
213         return -1
214     elif x > EPS:
215     #elif x > 0:
216         return 1
217     else:
218         return 0
219
220
221 # ---------------------------------------------------------------------
222 #
223 ## HSR Utility class
224 #
225 # ---------------------------------------------------------------------
226
227 EPS = 10e-5
228 INF = 10e5
229
230 class HSR:
231     """A utility class for HSR processing.
232     """
233
234     def is_nonplanar_quad(face):
235         """Determine if a quad is non-planar.
236
237         From: http://mathworld.wolfram.com/Coplanar.html
238
239         Geometric objects lying in a common plane are said to be coplanar.
240         Three noncollinear points determine a plane and so are trivially coplanar.
241         Four points are coplanar iff the volume of the tetrahedron defined by them is
242         0,
243
244             | x_1 y_1 z_1 1 |
245             | x_2 y_2 z_2 1 |
246             | x_3 y_3 z_3 1 |
247             | x_4 y_4 z_4 1 | == 0
248
249         Coplanarity is equivalent to the statement that the pair of lines
250         determined by the four points are not skew, and can be equivalently stated
251         in vector form as (x_3-x_1).[(x_2-x_1)x(x_4-x_3)]==0.
252
253         An arbitrary number of n points x_1, ..., x_n can be tested for
254         coplanarity by finding the point-plane distances of the points
255         x_4, ..., x_n from the plane determined by (x_1,x_2,x_3)
256         and checking if they are all zero.
257         If so, the points are all coplanar.
258
259         We here check only for 4-point complanarity.
260         """
261         n = len(face)
262
263         # assert(n>4)
264         if n < 3 or n > 4:
265             print "ERROR a mesh in Blender can't have more than 4 vertices or less than 3"
266             raise AssertionError
267
268         elif n == 3:
269             # three points must be complanar
270             return False
271         else: # n == 4
272             x1 = Vector(face[0].co)
273             x2 = Vector(face[1].co)
274             x3 = Vector(face[2].co)
275             x4 = Vector(face[3].co)
276
277             v = (x3-x1) * CrossVecs((x2-x1), (x4-x3))
278             if v != 0:
279                 return True
280
281         return False
282
283     is_nonplanar_quad = staticmethod(is_nonplanar_quad)
284
285     def pointInPolygon(poly, v):
286         return False
287
288     pointInPolygon = staticmethod(pointInPolygon)
289
290     def edgeIntersection(s1, s2, do_perturbate=False):
291
292         (x1, y1) = s1[0].co[0], s1[0].co[1]
293         (x2, y2) = s1[1].co[0], s1[1].co[1]
294
295         (x3, y3) = s2[0].co[0], s2[0].co[1]
296         (x4, y4) = s2[1].co[0], s2[1].co[1]
297
298         #z1 = s1[0].co[2]
299         #z2 = s1[1].co[2]
300         #z3 = s2[0].co[2]
301         #z4 = s2[1].co[2]
302
303
304         # calculate delta values (vector components)
305         dx1 = x2 - x1;
306         dx2 = x4 - x3;
307         dy1 = y2 - y1;
308         dy2 = y4 - y3;
309
310         #dz1 = z2 - z1;
311         #dz2 = z4 - z3;
312
313         C = dy2 * dx1 - dx2 * dy1 #  /* cross product */
314         if C == 0:  #/* parallel */
315             return None
316
317         dx3 = x1 - x3 # /* combined origin offset vector */
318         dy3 = y1 - y3
319
320         a1 = (dy3 * dx2 - dx3 * dy2) / C;
321         a2 = (dy3 * dx1 - dx3 * dy1) / C;
322
323         # check for degeneracies
324         #print_debug("\n")
325         #print_debug(str(a1)+"\n")
326         #print_debug(str(a2)+"\n\n")
327
328         if (a1 == 0 or a1 == 1 or a2 == 0 or a2 == 1):
329             # Intersection on boundaries, we consider the point external?
330             return None
331
332         elif (a1>0.0 and a1<1.0 and a2>0.0 and a2<1.0): #  /* lines cross */
333             x = x1 + a1*dx1
334             y = y1 + a1*dy1
335
336             #z = z1 + a1*dz1
337             z = 0
338             return (NMesh.Vert(x, y, z), a1, a2)
339
340         else:
341             # lines have intersections but not those segments
342             return None
343
344     edgeIntersection = staticmethod(edgeIntersection)
345
346     def isVertInside(self, v):
347         winding_number = 0
348         coincidence = False
349
350         # Create point at infinity
351         point_at_infinity = NMesh.Vert(-INF, v.co[1], -INF)
352
353         for i in range(len(self.v)):
354             s1 = (point_at_infinity, v)
355             s2 = (self.v[i-1], self.v[i])
356
357             if EQ(v.co, s2[0].co) or EQ(v.co, s2[1].co):
358                 coincidence = True
359
360             if HSR.edgeIntersection(s1, s2, do_perturbate=False):
361                 winding_number += 1
362
363         # Check even or odd
364         if winding_number % 2 == 0 :
365             return False
366         else:
367             if coincidence:
368                 return False
369             return True
370
371     isVertInside = staticmethod(isVertInside)
372
373
374     def det(a, b, c):
375         return ((b[0] - a[0]) * (c[1] - a[1]) -
376                 (b[1] - a[1]) * (c[0] - a[0]) )
377
378     det = staticmethod(det)
379
380     def pointInPolygon(q, P):
381         is_in = False
382
383         point_at_infinity = NMesh.Vert(-INF, q.co[1], -INF)
384
385         det = HSR.det
386
387         for i in range(len(P.v)):
388             p0 = P.v[i-1]
389             p1 = P.v[i]
390             if (det(q.co, point_at_infinity.co, p0.co)<0) != (det(q.co, point_at_infinity.co, p1.co)<0):
391                 if det(p0.co, p1.co, q.co) == 0 :
392                     #print "On Boundary"
393                     return False
394                 elif (det(p0.co, p1.co, q.co)<0) != (det(p0.co, p1.co, point_at_infinity.co)<0):
395                     is_in = not is_in
396
397         return is_in
398
399     pointInPolygon = staticmethod(pointInPolygon)
400
401     def projectionsOverlap(f1, f2):
402         """ If you have nonconvex, but still simple polygons, an acceptable method
403         is to iterate over all vertices and perform the Point-in-polygon test[1].
404         The advantage of this method is that you can compute the exact
405         intersection point and collision normal that you will need to simulate
406         collision. When you have the point that lies inside the other polygon, you
407         just iterate over all edges of the second polygon again and look for edge
408         intersections. Note that this method detects collsion when it already
409         happens. This algorithm is fast enough to perform it hundreds of times per
410         sec.  """
411
412         for i in range(len(f1.v)):
413
414
415             # If a point of f1 in inside f2, there is an overlap!
416             v1 = f1.v[i]
417             #if HSR.isVertInside(f2, v1):
418             if HSR.pointInPolygon(v1, f2):
419                 return True
420
421             # If not the polygon can be ovelap as well, so we check for
422             # intersection between an edge of f1 and all the edges of f2
423
424             v0 = f1.v[i-1]
425
426             for j in range(len(f2.v)):
427                 v2 = f2.v[j-1]
428                 v3 = f2.v[j]
429
430                 e1 = v0, v1
431                 e2 = v2, v3
432
433                 intrs = HSR.edgeIntersection(e1, e2)
434                 if intrs:
435                     #print_debug(str(v0.co) + " " + str(v1.co) + " " +
436                     #        str(v2.co) + " " + str(v3.co) )
437                     #print_debug("\nIntersection\n")
438
439                     return True
440
441         return False
442
443     projectionsOverlap = staticmethod(projectionsOverlap)
444
445     def midpoint(p1, p2):
446         """Return the midpoint of two vertices.
447         """
448         m = MidpointVecs(Vector(p1), Vector(p2))
449         mv = NMesh.Vert(m[0], m[1], m[2])
450
451         return mv
452
453     midpoint = staticmethod(midpoint)
454
455     def facesplit(P, Q, facelist, nmesh):
456         """Split P or Q according to the strategy illustrated in the Newell's
457         paper.
458         """
459
460         by_furthest_z = (lambda f1, f2:
461                 cmp(max([v.co[2] for v in f1]), max([v.co[2] for v in f2])+EPS)
462                 )
463
464         # Choose if split P on Q plane or vice-versa
465
466         n = 0
467         for Pi in P:
468             d = HSR.Distance(Vector(Pi), Q)
469             if d <= EPS:
470                 n += 1
471         pIntersectQ = (n != len(P))
472
473         n = 0
474         for Qi in Q:
475             d = HSR.Distance(Vector(Qi), P)
476             if d >= -EPS:
477                 n += 1
478         qIntersectP = (n != len(Q))
479
480         newfaces = []
481
482         # 1. If parts of P lie in both half-spaces of Q
483         # then splice P in two with the plane of Q
484         if pIntersectQ:
485             #print "We split P"
486             f = P
487             plane = Q
488
489             newfaces = HSR.splitOn(plane, f)
490
491         # 2. Else if parts of Q lie in both half-space of P
492         # then splice Q in two with the plane of P
493         if qIntersectP and newfaces == None:
494             #print "We split Q"
495             f = Q
496             plane = P
497
498             newfaces = HSR.splitOn(plane, f)
499             #print "After"
500
501         # 3. Else slice P in half through the mid-point of
502         # the longest pair of opposite sides
503         if newfaces == None:
504
505             print "We ignore P..."
506             facelist.remove(P)
507             return facelist
508
509             #f = P
510
511             #if len(P)==3:
512             #    v1 = midpoint(f[0], f[1])
513             #    v2 = midpoint(f[1], f[2])
514             #if len(P)==4:
515             #    v1 = midpoint(f[0], f[1])
516             #    v2 = midpoint(f[2], f[3])
517             #vec3 = (Vector(v2)+10*Vector(f.normal))
518             #
519             #v3 = NMesh.Vert(vec3[0], vec3[1], vec3[2])
520
521             #plane = NMesh.Face([v1, v2, v3])
522             #
523             #newfaces = splitOn(plane, f)
524
525
526         if newfaces == None:
527             print "Big FAT problem, we weren't able to split POLYGONS!"
528             raise AssertionError
529
530         #print newfaces
531         if newfaces:
532             #for v in f:
533             #    if v not in plane and v in nmesh.verts:
534             #        nmesh.verts.remove(v)
535             for nf in newfaces:
536
537                 nf.mat = f.mat
538                 nf.sel = f.sel
539                 nf.col = [f.col[0]] * len(nf.v)
540
541                 nf.smooth = 0
542
543                 for v in nf:
544                     nmesh.verts.append(v)
545                 # insert pieces in the list
546                 facelist.append(nf)
547
548             facelist.remove(f)
549
550         # and resort the faces
551         facelist.sort(by_furthest_z)
552         facelist.sort(lambda f1, f2: cmp(f1.smooth, f2.smooth))
553         facelist.reverse()
554
555         #print [ f.smooth for f in facelist ]
556
557         return facelist
558
559     facesplit = staticmethod(facesplit)
560
561     def isOnSegment(v1, v2, p, extremes_internal=False):
562         """Check if point p is in segment v1v2.
563         """
564
565         l1 = (v1-p).length
566         l2 = (v2-p).length
567
568         # Should we consider extreme points as internal ?
569         # The test:
570         # if p == v1 or p == v2:
571         if l1 < EPS or l2 < EPS:
572             return extremes_internal
573
574         l = (v1-v2).length
575
576         # if the sum of l1 and l2 is circa l, then the point is on segment,
577         if abs(l - (l1+l2)) < EPS:
578             return True
579         else:
580             return False
581
582     isOnSegment = staticmethod(isOnSegment)
583
584     def Distance(point, face):
585         """ Calculate the distance between a point and a face.
586
587         An alternative but more expensive method can be:
588
589             ip = Intersect(Vector(face[0]), Vector(face[1]), Vector(face[2]),
590                     Vector(face.no), Vector(point), 0)
591
592             d = Vector(ip - point).length
593
594         See: http://mathworld.wolfram.com/Point-PlaneDistance.html
595         """
596
597         p = Vector(point)
598         plNormal = Vector(face.no)
599         plVert0 = Vector(face.v[0])
600
601         d = (plVert0 * plNormal) - (p * plNormal)
602
603         #d = plNormal * (plVert0 - p)
604
605         #print "\nd: %.10f - sel: %d, %s\n" % (d, face.sel, str(point))
606
607         return d
608
609     Distance = staticmethod(Distance)
610
611     def makeFaces(vl):
612         #
613         # make one or two new faces based on a list of vertex-indices
614         #
615         newfaces = []
616
617         if len(vl) <= 4:
618             nf = NMesh.Face()
619
620             for v in vl:
621                 nf.v.append(v)
622
623             newfaces.append(nf)
624
625         else:
626             nf = NMesh.Face()
627
628             nf.v.append(vl[0])
629             nf.v.append(vl[1])
630             nf.v.append(vl[2])
631             nf.v.append(vl[3])
632             newfaces.append(nf)
633
634             nf = NMesh.Face()
635             nf.v.append(vl[3])
636             nf.v.append(vl[4])
637             nf.v.append(vl[0])
638             newfaces.append(nf)
639
640         return newfaces
641
642     makeFaces = staticmethod(makeFaces)
643
644     def splitOn(Q, P, return_positive_faces=True, return_negative_faces=True):
645         """Split P using the plane of Q.
646         Logic taken from the knife.py python script
647         """
648
649         # Check if P and Q are parallel
650         u = CrossVecs(Vector(Q.no),Vector(P.no))
651         ax = abs(u[0])
652         ay = abs(u[1])
653         az = abs(u[2])
654
655         if (ax+ay+az) < EPS:
656             print "PARALLEL planes!!"
657             return
658
659
660         # The final aim is to find the intersection line between P
661         # and the plane of Q, and split P along this line
662
663         nP = len(P.v)
664
665         # Calculate point-plane Distance between vertices of P and plane Q
666         d = []
667         for i in range(0, nP):
668             d.append(HSR.Distance(P.v[i], Q))
669
670         newVertList = []
671
672         posVertList = []
673         negVertList = []
674         for i in range(nP):
675             d0 = d[i-1]
676             V0 = P.v[i-1]
677
678             d1 = d[i]
679             V1 = P.v[i]
680
681             #print "d0:", d0, "d1:", d1
682
683             # if the vertex lies in the cutplane
684             if abs(d1) < EPS:
685                 #print "d1 On cutplane"
686                 posVertList.append(V1)
687                 negVertList.append(V1)
688             else:
689                 # if the previous vertex lies in cutplane
690                 if abs(d0) < EPS:
691                     #print "d0 on Cutplane"
692                     if d1 > 0:
693                         #print "d1 on positive Halfspace"
694                         posVertList.append(V1)
695                     else:
696                         #print "d1 on negative Halfspace"
697                         negVertList.append(V1)
698                 else:
699                     # if they are on the same side of the plane
700                     if d1*d0 > 0:
701                         #print "On the same half-space"
702                         if d1 > 0:
703                             #print "d1 on positive Halfspace"
704                             posVertList.append(V1)
705                         else:
706                             #print "d1 on negative Halfspace"
707                             negVertList.append(V1)
708
709                     # the vertices are not on the same side of the plane, so we have an intersection
710                     else:
711                         #print "Intersection"
712
713                         e = Vector(V0), Vector(V1)
714                         tri = Vector(Q[0]), Vector(Q[1]), Vector(Q[2])
715
716                         inters = Intersect(tri[0], tri[1], tri[2], e[1]-e[0], e[0], 0)
717                         if inters == None:
718                             print "Split Break"
719                             break
720
721                         #print "Intersection", inters
722
723                         nv = NMesh.Vert(inters[0], inters[1], inters[2])
724                         newVertList.append(nv)
725
726                         posVertList.append(nv)
727                         negVertList.append(nv)
728
729                         if d1 > 0:
730                             posVertList.append(V1)
731                         else:
732                             negVertList.append(V1)
733
734
735         # uniq for python > 2.4
736         #posVertList = [ u for u in posVertList if u not in locals()['_[1]'] ]
737         #negVertList = [ u for u in negVertList if u not in locals()['_[1]'] ]
738
739         # a more portable way
740         posVertList = uniq(posVertList)
741         negVertList = uniq(negVertList)
742
743
744         # If vertex are all on the same half-space, return
745         #if len(posVertList) < 3:
746         #    print "Problem, we created a face with less that 3 vertices??"
747         #    posVertList = []
748         #if len(negVertList) < 3:
749         #    print "Problem, we created a face with less that 3 vertices??"
750         #    negVertList = []
751
752         if len(posVertList) < 3 or len(negVertList) < 3:
753             #print "RETURN NONE, SURE???"
754             return None
755
756         if not return_positive_faces:
757             posVertList = []
758         if not return_negative_faces:
759             negVertList = []
760
761         newfaces = HSR.addNewFaces(posVertList, negVertList)
762
763         return newfaces
764
765     splitOn = staticmethod(splitOn)
766
767     def addNewFaces(posVertList, negVertList):
768         # Create new faces resulting from the split
769         outfaces = []
770         if len(posVertList) or len(negVertList):
771
772             #newfaces = [posVertList] + [negVertList]
773             newfaces = ( [[ NMesh.Vert(v[0], v[1], v[2]) for v in posVertList]] +
774                     [[ NMesh.Vert(v[0], v[1], v[2]) for v in negVertList]] )
775
776             for nf in newfaces:
777                 if nf and len(nf)>2:
778                     outfaces += HSR.makeFaces(nf)
779
780         return outfaces
781
782
783     addNewFaces = staticmethod(addNewFaces)
784
785
786 # ---------------------------------------------------------------------
787 #
788 ## Mesh Utility class
789 #
790 # ---------------------------------------------------------------------
791
792 class MeshUtils:
793
794     def buildEdgeFaceUsersCache(me):
795         '''
796         Takes a mesh and returns a list aligned with the meshes edges.
797         Each item is a list of the faces that use the edge
798         would be the equiv for having ed.face_users as a property
799
800         Taken from .blender/scripts/bpymodules/BPyMesh.py,
801         thanks to ideasman_42.
802         '''
803
804         def sorted_edge_indicies(ed):
805             i1= ed.v1.index
806             i2= ed.v2.index
807             if i1>i2:
808                 i1,i2= i2,i1
809             return i1, i2
810
811
812         face_edges_dict= dict([(sorted_edge_indicies(ed), (ed.index, [])) for ed in me.edges])
813         for f in me.faces:
814             fvi= [v.index for v in f.v]# face vert idx's
815             for i in xrange(len(f)):
816                 i1= fvi[i]
817                 i2= fvi[i-1]
818
819                 if i1>i2:
820                     i1,i2= i2,i1
821
822                 face_edges_dict[i1,i2][1].append(f)
823
824         face_edges= [None] * len(me.edges)
825         for ed_index, ed_faces in face_edges_dict.itervalues():
826             face_edges[ed_index]= ed_faces
827
828         return face_edges
829
830     def isMeshEdge(adjacent_faces):
831         """Mesh edge rule.
832
833         A mesh edge is visible if _at_least_one_ of its adjacent faces is selected.
834         Note: if the edge has no adjacent faces we want to show it as well,
835         useful for "edge only" portion of objects.
836         """
837
838         if len(adjacent_faces) == 0:
839             return True
840
841         selected_faces = [f for f in adjacent_faces if f.sel]
842
843         if len(selected_faces) != 0:
844             return True
845         else:
846             return False
847
848     def isSilhouetteEdge(adjacent_faces):
849         """Silhuette selection rule.
850
851         An edge is a silhuette edge if it is shared by two faces with
852         different selection status or if it is a boundary edge of a selected
853         face.
854         """
855
856         if ((len(adjacent_faces) == 1 and adjacent_faces[0].sel == 1) or
857             (len(adjacent_faces) == 2 and
858                 adjacent_faces[0].sel != adjacent_faces[1].sel)
859             ):
860             return True
861         else:
862             return False
863
864     buildEdgeFaceUsersCache = staticmethod(buildEdgeFaceUsersCache)
865     isMeshEdge = staticmethod(isMeshEdge)
866     isSilhouetteEdge = staticmethod(isSilhouetteEdge)
867
868
869 # ---------------------------------------------------------------------
870 #
871 ## Shading Utility class
872 #
873 # ---------------------------------------------------------------------
874
875 class ShadingUtils:
876
877     shademap = None
878
879     def toonShadingMapSetup():
880         levels = config.polygons['TOON_LEVELS']
881
882         texels = 2*levels - 1
883         tmp_shademap = [0.0] + [(i)/float(texels-1) for i in xrange(1, texels-1) ] + [1.0]
884
885         return tmp_shademap
886
887     def toonShading(u):
888
889         shademap = ShadingUtils.shademap
890
891         if not shademap:
892             shademap = ShadingUtils.toonShadingMapSetup()
893
894         v = 1.0
895         for i in xrange(0, len(shademap)-1):
896             pivot = (shademap[i]+shademap[i+1])/2.0
897             j = int(u>pivot)
898
899             v = shademap[i+j]
900
901             if v < shademap[i+1]:
902                 return v
903
904         return v
905
906     toonShadingMapSetup = staticmethod(toonShadingMapSetup)
907     toonShading = staticmethod(toonShading)
908
909
910 # ---------------------------------------------------------------------
911 #
912 ## Projections classes
913 #
914 # ---------------------------------------------------------------------
915
916 class Projector:
917     """Calculate the projection of an object given the camera.
918
919     A projector is useful to so some per-object transformation to obtain the
920     projection of an object given the camera.
921
922     The main method is #doProjection# see the method description for the
923     parameter list.
924     """
925
926     def __init__(self, cameraObj, canvasRatio):
927         """Calculate the projection matrix.
928
929         The projection matrix depends, in this case, on the camera settings.
930         TAKE CARE: This projector expects vertices in World Coordinates!
931         """
932
933         camera = cameraObj.getData()
934
935         aspect = float(canvasRatio[0])/float(canvasRatio[1])
936         near = camera.clipStart
937         far = camera.clipEnd
938
939         scale = float(camera.scale)
940
941         fovy = atan(0.5/aspect/(camera.lens/32))
942         fovy = fovy * 360.0/pi
943
944
945         if Blender.Get('version') < 243:
946             camPersp = 0
947             camOrtho = 1
948         else:
949             camPersp = 'persp'
950             camOrtho = 'ortho'
951
952         # What projection do we want?
953         if camera.type == camPersp:
954             mP = self._calcPerspectiveMatrix(fovy, aspect, near, far)
955         elif camera.type == camOrtho:
956             mP = self._calcOrthoMatrix(fovy, aspect, near, far, scale)
957
958
959         # View transformation
960         cam = Matrix(cameraObj.getInverseMatrix())
961         cam.transpose()
962
963         mP = mP * cam
964
965         self.projectionMatrix = mP
966
967     ##
968     # Public methods
969     #
970
971     def doProjection(self, v):
972         """Project the point on the view plane.
973
974         Given a vertex calculate the projection using the current projection
975         matrix.
976         """
977
978         # Note that we have to work on the vertex using homogeneous coordinates
979         # From blender 2.42+ we don't need to resize the vector to be 4d
980         # when applying a 4x4 matrix, but we do that anyway since we need the
981         # 4th coordinate later
982         p = self.projectionMatrix * Vector(v).resize4D()
983
984         # Perspective division
985         if p[3] != 0:
986             p[0] = p[0]/p[3]
987             p[1] = p[1]/p[3]
988             p[2] = p[2]/p[3]
989
990         # restore the size
991         p[3] = 1.0
992         p.resize3D()
993
994         return p
995
996
997     ##
998     # Private methods
999     #
1000
1001     def _calcPerspectiveMatrix(self, fovy, aspect, near, far):
1002         """Return a perspective projection matrix.
1003         """
1004
1005         top = near * tan(fovy * pi / 360.0)
1006         bottom = -top
1007         left = bottom*aspect
1008         right= top*aspect
1009         x = (2.0 * near) / (right-left)
1010         y = (2.0 * near) / (top-bottom)
1011         a = (right+left) / (right-left)
1012         b = (top+bottom) / (top - bottom)
1013         c = - ((far+near) / (far-near))
1014         d = - ((2*far*near)/(far-near))
1015
1016         m = Matrix(
1017                 [x,   0.0,    a,    0.0],
1018                 [0.0,   y,    b,    0.0],
1019                 [0.0, 0.0,    c,      d],
1020                 [0.0, 0.0, -1.0,    0.0])
1021
1022         return m
1023
1024     def _calcOrthoMatrix(self, fovy, aspect , near, far, scale):
1025         """Return an orthogonal projection matrix.
1026         """
1027
1028         # The 11 in the formula was found emiprically
1029         top = near * tan(fovy * pi / 360.0) * (scale * 11)
1030         bottom = -top
1031         left = bottom * aspect
1032         right= top * aspect
1033         rl = right-left
1034         tb = top-bottom
1035         fn = near-far
1036         tx = -((right+left)/rl)
1037         ty = -((top+bottom)/tb)
1038         tz = ((far+near)/fn)
1039
1040         m = Matrix(
1041                 [2.0/rl, 0.0,    0.0,     tx],
1042                 [0.0,    2.0/tb, 0.0,     ty],
1043                 [0.0,    0.0,    2.0/fn,  tz],
1044                 [0.0,    0.0,    0.0,    1.0])
1045
1046         return m
1047
1048
1049 # ---------------------------------------------------------------------
1050 #
1051 ## Progress Indicator
1052 #
1053 # ---------------------------------------------------------------------
1054
1055 class Progress:
1056     """A model for a progress indicator.
1057
1058     Do the progress calculation calculation and
1059     the view independent stuff of a progress indicator.
1060     """
1061     def __init__(self, steps=0):
1062         self.name = ""
1063         self.steps = steps
1064         self.completed = 0
1065         self.progress = 0
1066
1067     def setSteps(self, steps):
1068         """Set the number of steps of the activity wich we want to track.
1069         """
1070         self.steps = steps
1071
1072     def getSteps(self):
1073         return self.steps
1074
1075     def setName(self, name):
1076         """Set the name of the activity wich we want to track.
1077         """
1078         self.name = name
1079
1080     def getName(self):
1081         return self.name
1082
1083     def getProgress(self):
1084         return self.progress
1085
1086     def reset(self):
1087         self.completed = 0
1088         self.progress = 0
1089
1090     def update(self):
1091         """Update the model, call this method when one step is completed.
1092         """
1093         if self.progress == 100:
1094             return False
1095
1096         self.completed += 1
1097         self.progress = ( float(self.completed) / float(self.steps) ) * 100
1098         self.progress = int(self.progress)
1099
1100         return True
1101
1102
1103 class ProgressIndicator:
1104     """An abstraction of a View for the Progress Model
1105     """
1106     def __init__(self):
1107
1108         # Use a refresh rate so we do not show the progress at
1109         # every update, but every 'self.refresh_rate' times.
1110         self.refresh_rate = 10
1111         self.shows_counter = 0
1112
1113         self.quiet = False
1114
1115         self.progressModel = None
1116
1117     def setQuiet(self, value):
1118         self.quiet = value
1119
1120     def setActivity(self, name, steps):
1121         """Initialize the Model.
1122
1123         In a future version (with subactivities-progress support) this method
1124         could only set the current activity.
1125         """
1126         self.progressModel = Progress()
1127         self.progressModel.setName(name)
1128         self.progressModel.setSteps(steps)
1129
1130     def getActivity(self):
1131         return self.progressModel
1132
1133     def update(self):
1134         """Update the model and show the actual progress.
1135         """
1136         assert(self.progressModel)
1137
1138         if self.progressModel.update():
1139             if self.quiet:
1140                 return
1141
1142             self.show(self.progressModel.getProgress(),
1143                     self.progressModel.getName())
1144
1145         # We return always True here so we can call the update() method also
1146         # from lambda funcs (putting the call in logical AND with other ops)
1147         return True
1148
1149     def show(self, progress, name=""):
1150         self.shows_counter = (self.shows_counter + 1) % self.refresh_rate
1151         if self.shows_counter != 0:
1152             return
1153
1154         if progress == 100:
1155             self.shows_counter = -1
1156
1157
1158 class ConsoleProgressIndicator(ProgressIndicator):
1159     """Show a progress bar on stderr, a la wget.
1160     """
1161     def __init__(self):
1162         ProgressIndicator.__init__(self)
1163
1164         self.swirl_chars = ["-", "\\", "|", "/"]
1165         self.swirl_count = -1
1166
1167     def show(self, progress, name):
1168         ProgressIndicator.show(self, progress, name)
1169
1170         bar_length = 70
1171         bar_progress = int( (progress/100.0) * bar_length )
1172         bar = ("=" * bar_progress).ljust(bar_length)
1173
1174         self.swirl_count = (self.swirl_count+1)%len(self.swirl_chars)
1175         swirl_char = self.swirl_chars[self.swirl_count]
1176
1177         progress_bar = "%s |%s| %c %3d%%" % (name, bar, swirl_char, progress)
1178
1179         sys.stderr.write(progress_bar+"\r")
1180         if progress == 100:
1181             sys.stderr.write("\n")
1182
1183
1184 class GraphicalProgressIndicator(ProgressIndicator):
1185     """Interface to the Blender.Window.DrawProgressBar() method.
1186     """
1187     def __init__(self):
1188         ProgressIndicator.__init__(self)
1189
1190         #self.swirl_chars = ["-", "\\", "|", "/"]
1191         # We have to use letters with the same width, for now!
1192         # Blender progress bar considers the font widths when
1193         # calculating the progress bar width.
1194         self.swirl_chars = ["\\", "/"]
1195         self.swirl_count = -1
1196
1197     def show(self, progress, name):
1198         ProgressIndicator.show(self, progress)
1199
1200         self.swirl_count = (self.swirl_count+1)%len(self.swirl_chars)
1201         swirl_char = self.swirl_chars[self.swirl_count]
1202
1203         progress_text = "%s - %c %3d%%" % (name, swirl_char, progress)
1204
1205         # Finally draw  the Progress Bar
1206         Window.WaitCursor(1) # Maybe we can move that call in the constructor?
1207         Window.DrawProgressBar(progress/100.0, progress_text)
1208
1209         if progress == 100:
1210             Window.DrawProgressBar(1, progress_text)
1211             Window.WaitCursor(0)
1212
1213
1214
1215 # ---------------------------------------------------------------------
1216 #
1217 ## 2D Object representation class
1218 #
1219 # ---------------------------------------------------------------------
1220
1221 # TODO: a class to represent the needed properties of a 2D vector image
1222 # For now just using a [N]Mesh structure.
1223
1224
1225 # ---------------------------------------------------------------------
1226 #
1227 ## Vector Drawing Classes
1228 #
1229 # ---------------------------------------------------------------------
1230
1231 ## A generic Writer
1232
1233 class VectorWriter:
1234     """
1235     A class for printing output in a vectorial format.
1236
1237     Given a 2D representation of the 3D scene the class is responsible to
1238     write it is a vector format.
1239
1240     Every subclasses of VectorWriter must have at last the following public
1241     methods:
1242         - open(self)
1243         - close(self)
1244         - printCanvas(self, scene,
1245             doPrintPolygons=True, doPrintEdges=False, showHiddenEdges=False):
1246     """
1247
1248     def __init__(self, fileName):
1249         """Set the output file name and other properties"""
1250
1251         try:
1252             config.writer
1253         except:
1254             config.writer = dict()
1255             config.writer['SETTING'] = True
1256
1257         self.outputFileName = fileName
1258
1259         context = Scene.GetCurrent().getRenderingContext()
1260         self.canvasSize = ( context.imageSizeX(), context.imageSizeY() )
1261
1262         self.fps = context.fps
1263
1264         self.startFrame = 1
1265         self.endFrame = 1
1266         self.animation = False
1267
1268
1269     ##
1270     # Public Methods
1271     #
1272
1273     def open(self, startFrame=1, endFrame=1):
1274         if startFrame != endFrame:
1275             self.startFrame = startFrame
1276             self.endFrame = endFrame
1277             self.animation = True
1278
1279         print "Outputting to: ", self.outputFileName
1280
1281         return
1282
1283     def close(self):
1284         return
1285
1286     def printCanvas(self, scene, doPrintPolygons=True, doPrintEdges=False,
1287             showHiddenEdges=False):
1288         """This is the interface for the needed printing routine.
1289         """
1290         return
1291
1292
1293 ## SVG Writer
1294
1295 class SVGVectorWriter(VectorWriter):
1296     """A concrete class for writing SVG output.
1297     """
1298
1299     def __init__(self, fileName):
1300         """Simply call the parent Contructor.
1301         """
1302         VectorWriter.__init__(self, fileName)
1303
1304         self.file = None
1305
1306
1307     ##
1308     # Public Methods
1309     #
1310
1311     def open(self, startFrame=1, endFrame=1):
1312         """Do some initialization operations.
1313         """
1314         VectorWriter.open(self, startFrame, endFrame)
1315
1316         self.file = open(self.outputFileName, "w")
1317
1318         self._printHeader()
1319
1320     def close(self):
1321         """Do some finalization operation.
1322         """
1323         self._printFooter()
1324
1325         if self.file:
1326             self.file.close()
1327
1328         # remember to call the close method of the parent as last
1329         VectorWriter.close(self)
1330
1331
1332     def printCanvas(self, scene, doPrintPolygons=True, doPrintEdges=False,
1333             showHiddenEdges=False):
1334         """Convert the scene representation to SVG.
1335         """
1336
1337         Objects = scene.objects
1338
1339         context = scene.getRenderingContext()
1340         framenumber = context.currentFrame()
1341
1342         if self.animation:
1343             framestyle = "display:none"
1344         else:
1345             framestyle = "display:block"
1346
1347         # Assign an id to this group so we can set properties on it using DOM
1348         self.file.write("<g id=\"frame%d\" style=\"%s\">\n" %
1349                 (framenumber, framestyle) )
1350
1351
1352         for obj in Objects:
1353
1354             if(obj.getType() != 'Mesh'):
1355                 continue
1356
1357             self.file.write("<g id=\"%s\">\n" % obj.getName())
1358
1359             mesh = obj.getData(mesh=1)
1360
1361             if doPrintPolygons:
1362                 self._printPolygons(mesh)
1363
1364             if doPrintEdges:
1365                 self._printEdges(mesh, showHiddenEdges)
1366
1367             self.file.write("</g>\n")
1368
1369         self.file.write("</g>\n")
1370
1371
1372     ##
1373     # Private Methods
1374     #
1375
1376     def _calcCanvasCoord(self, v):
1377         """Convert vertex in scene coordinates to canvas coordinates.
1378         """
1379
1380         pt = Vector([0, 0, 0])
1381
1382         mW = float(self.canvasSize[0])/2.0
1383         mH = float(self.canvasSize[1])/2.0
1384
1385         # rescale to canvas size
1386         pt[0] = v.co[0]*mW + mW
1387         pt[1] = v.co[1]*mH + mH
1388         pt[2] = v.co[2]
1389
1390         # For now we want (0,0) in the top-left corner of the canvas.
1391         # Mirror and translate along y
1392         pt[1] *= -1
1393         pt[1] += self.canvasSize[1]
1394
1395         return pt
1396
1397     def _printHeader(self):
1398         """Print SVG header."""
1399
1400         self.file.write("<?xml version=\"1.0\"?>\n")
1401         self.file.write("<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.0//EN\"\n")
1402         self.file.write("\t\"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd\">\n")
1403         self.file.write("<svg version=\"1.0\"\n")
1404         self.file.write("\txmlns=\"http://www.w3.org/2000/svg\"\n")
1405         self.file.write("\twidth=\"%d\" height=\"%d\">\n\n" %
1406                 self.canvasSize)
1407
1408         if self.animation:
1409             delay = 1000/self.fps
1410
1411             self.file.write("""\n<script type="text/javascript"><![CDATA[
1412             globalStartFrame=%d;
1413             globalEndFrame=%d;
1414
1415             timerID = setInterval("NextFrame()", %d);
1416             globalFrameCounter=%d;
1417             \n""" % (self.startFrame, self.endFrame, delay, self.startFrame) )
1418
1419             self.file.write("""\n
1420             function NextFrame()
1421             {
1422               currentElement  = document.getElementById('frame'+globalFrameCounter)
1423               previousElement = document.getElementById('frame'+(globalFrameCounter-1))
1424
1425               if (!currentElement)
1426               {
1427                 return;
1428               }
1429
1430               if (globalFrameCounter > globalEndFrame)
1431               {
1432                 clearInterval(timerID)
1433               }
1434               else
1435               {
1436                 if(previousElement)
1437                 {
1438                     previousElement.style.display="none";
1439                 }
1440                 currentElement.style.display="block";
1441                 globalFrameCounter++;
1442               }
1443             }
1444             \n]]></script>\n
1445             \n""")
1446
1447     def _printFooter(self):
1448         """Print the SVG footer."""
1449
1450         self.file.write("\n</svg>\n")
1451
1452     def _printPolygons(self, mesh):
1453         """Print the selected (visible) polygons.
1454         """
1455
1456         if len(mesh.faces) == 0:
1457             return
1458
1459         self.file.write("<g>\n")
1460
1461         for face in mesh.faces:
1462             if not face.sel:
1463                continue
1464
1465             self.file.write("<path d=\"")
1466
1467             #p = self._calcCanvasCoord(face.verts[0])
1468             p = self._calcCanvasCoord(face.v[0])
1469             self.file.write("M %g,%g L " % (p[0], p[1]))
1470
1471             for v in face.v[1:]:
1472                 p = self._calcCanvasCoord(v)
1473                 self.file.write("%g,%g " % (p[0], p[1]))
1474
1475             # get rid of the last blank space, just cosmetics here.
1476             self.file.seek(-1, 1)
1477             self.file.write(" z\"\n")
1478
1479             # take as face color the first vertex color
1480             if face.col:
1481                 fcol = face.col[0]
1482                 color = [fcol.r, fcol.g, fcol.b, fcol.a]
1483             else:
1484                 color = [255, 255, 255, 255]
1485
1486             # Convert the color to the #RRGGBB form
1487             str_col = "#%02X%02X%02X" % (color[0], color[1], color[2])
1488
1489             # Handle transparent polygons
1490             opacity_string = ""
1491             if color[3] != 255:
1492                 opacity = float(color[3])/255.0
1493                 opacity_string = " fill-opacity: %g; stroke-opacity: %g; opacity: 1;" % (opacity, opacity)
1494                 #opacity_string = "opacity: %g;" % (opacity)
1495
1496             self.file.write("\tstyle=\"fill:" + str_col + ";")
1497             self.file.write(opacity_string)
1498
1499             # use the stroke property to alleviate the "adjacent edges" problem,
1500             # we simulate polygon expansion using borders,
1501             # see http://www.antigrain.com/svg/index.html for more info
1502             stroke_width = 1.0
1503
1504             # EXPANSION TRICK is not that useful where there is transparency
1505             if config.polygons['EXPANSION_TRICK'] and color[3] == 255:
1506                 # str_col = "#000000" # For debug
1507                 self.file.write(" stroke:%s;\n" % str_col)
1508                 self.file.write(" stroke-width:" + str(stroke_width) + ";\n")
1509                 self.file.write(" stroke-linecap:round;stroke-linejoin:round")
1510
1511             self.file.write("\"/>\n")
1512
1513         self.file.write("</g>\n")
1514
1515     def _printEdges(self, mesh, showHiddenEdges=False):
1516         """Print the wireframe using mesh edges.
1517         """
1518
1519         stroke_width = config.edges['WIDTH']
1520         stroke_col = config.edges['COLOR']
1521
1522         self.file.write("<g>\n")
1523
1524         for e in mesh.edges:
1525
1526             hidden_stroke_style = ""
1527
1528             if e.sel == 0:
1529                 if showHiddenEdges == False:
1530                     continue
1531                 else:
1532                     hidden_stroke_style = ";\n stroke-dasharray:3, 3"
1533
1534             p1 = self._calcCanvasCoord(e.v1)
1535             p2 = self._calcCanvasCoord(e.v2)
1536
1537             self.file.write("<line x1=\"%g\" y1=\"%g\" x2=\"%g\" y2=\"%g\"\n"
1538                     % ( p1[0], p1[1], p2[0], p2[1] ) )
1539             self.file.write(" style=\"stroke:rgb("+str(stroke_col[0])+","+str(stroke_col[1])+","+str(stroke_col[2])+");")
1540             self.file.write(" stroke-width:"+str(stroke_width)+";\n")
1541             self.file.write(" stroke-linecap:round;stroke-linejoin:round")
1542             self.file.write(hidden_stroke_style)
1543             self.file.write("\"/>\n")
1544
1545         self.file.write("</g>\n")
1546
1547
1548 ## SWF Writer
1549
1550 try:
1551     from ming import *
1552     SWFSupported = True
1553 except:
1554     SWFSupported = False
1555
1556 class SWFVectorWriter(VectorWriter):
1557     """A concrete class for writing SWF output.
1558     """
1559
1560     def __init__(self, fileName):
1561         """Simply call the parent Contructor.
1562         """
1563         VectorWriter.__init__(self, fileName)
1564
1565         self.movie = None
1566         self.sprite = None
1567
1568
1569     ##
1570     # Public Methods
1571     #
1572
1573     def open(self, startFrame=1, endFrame=1):
1574         """Do some initialization operations.
1575         """
1576         VectorWriter.open(self, startFrame, endFrame)
1577         self.movie = SWFMovie()
1578         self.movie.setDimension(self.canvasSize[0], self.canvasSize[1])
1579         if self.animation:
1580             self.movie.setRate(self.fps)
1581             numframes = endFrame - startFrame + 1
1582             self.movie.setFrames(numframes)
1583
1584     def close(self):
1585         """Do some finalization operation.
1586         """
1587         self.movie.save(self.outputFileName)
1588
1589         # remember to call the close method of the parent
1590         VectorWriter.close(self)
1591
1592     def printCanvas(self, scene, doPrintPolygons=True, doPrintEdges=False,
1593             showHiddenEdges=False):
1594         """Convert the scene representation to SVG.
1595         """
1596         context = scene.getRenderingContext()
1597         framenumber = context.currentFrame()
1598
1599         Objects = scene.objects
1600
1601         if self.sprite:
1602             self.movie.remove(self.sprite)
1603
1604         sprite = SWFSprite()
1605
1606         for obj in Objects:
1607
1608             if(obj.getType() != 'Mesh'):
1609                 continue
1610
1611             mesh = obj.getData(mesh=1)
1612
1613             if doPrintPolygons:
1614                 self._printPolygons(mesh, sprite)
1615
1616             if doPrintEdges:
1617                 self._printEdges(mesh, sprite, showHiddenEdges)
1618
1619         sprite.nextFrame()
1620         i = self.movie.add(sprite)
1621         # Remove the instance the next time
1622         self.sprite = i
1623         if self.animation:
1624             self.movie.nextFrame()
1625
1626
1627     ##
1628     # Private Methods
1629     #
1630
1631     def _calcCanvasCoord(self, v):
1632         """Convert vertex in scene coordinates to canvas coordinates.
1633         """
1634
1635         pt = Vector([0, 0, 0])
1636
1637         mW = float(self.canvasSize[0])/2.0
1638         mH = float(self.canvasSize[1])/2.0
1639
1640         # rescale to canvas size
1641         pt[0] = v.co[0]*mW + mW
1642         pt[1] = v.co[1]*mH + mH
1643         pt[2] = v.co[2]
1644
1645         # For now we want (0,0) in the top-left corner of the canvas.
1646         # Mirror and translate along y
1647         pt[1] *= -1
1648         pt[1] += self.canvasSize[1]
1649
1650         return pt
1651
1652     def _printPolygons(self, mesh, sprite):
1653         """Print the selected (visible) polygons.
1654         """
1655
1656         if len(mesh.faces) == 0:
1657             return
1658
1659         for face in mesh.faces:
1660             if not face.sel:
1661                continue
1662
1663             if face.col:
1664                 fcol = face.col[0]
1665                 color = [fcol.r, fcol.g, fcol.b, fcol.a]
1666             else:
1667                 color = [255, 255, 255, 255]
1668
1669             s = SWFShape()
1670             f = s.addFill(color[0], color[1], color[2], color[3])
1671             s.setRightFill(f)
1672
1673             # The starting point of the shape
1674             p0 = self._calcCanvasCoord(face.verts[0])
1675             s.movePenTo(p0[0], p0[1])
1676
1677             for v in face.verts[1:]:
1678                 p = self._calcCanvasCoord(v)
1679                 s.drawLineTo(p[0], p[1])
1680
1681             # Closing the shape
1682             s.drawLineTo(p0[0], p0[1])
1683
1684             s.end()
1685             sprite.add(s)
1686
1687
1688     def _printEdges(self, mesh, sprite, showHiddenEdges=False):
1689         """Print the wireframe using mesh edges.
1690         """
1691
1692         stroke_width = config.edges['WIDTH']
1693         stroke_col = config.edges['COLOR']
1694
1695         s = SWFShape()
1696
1697         for e in mesh.edges:
1698
1699             # Next, we set the line width and color for our shape.
1700             s.setLine(stroke_width, stroke_col[0], stroke_col[1], stroke_col[2],
1701             255)
1702
1703             if e.sel == 0:
1704                 if showHiddenEdges == False:
1705                     continue
1706                 else:
1707                     # SWF does not support dashed lines natively, so -for now-
1708                     # draw hidden lines thinner and half-trasparent
1709                     s.setLine(stroke_width/2, stroke_col[0], stroke_col[1],
1710                             stroke_col[2], 128)
1711
1712             p1 = self._calcCanvasCoord(e.v1)
1713             p2 = self._calcCanvasCoord(e.v2)
1714
1715             s.movePenTo(p1[0], p1[1])
1716             s.drawLineTo(p2[0], p2[1])
1717
1718         s.end()
1719         sprite.add(s)
1720
1721
1722 ## PDF Writer
1723
1724 try:
1725     from reportlab.pdfgen import canvas
1726     PDFSupported = True
1727 except:
1728     PDFSupported = False
1729
1730 class PDFVectorWriter(VectorWriter):
1731     """A concrete class for writing PDF output.
1732     """
1733
1734     def __init__(self, fileName):
1735         """Simply call the parent Contructor.
1736         """
1737         VectorWriter.__init__(self, fileName)
1738
1739         self.canvas = None
1740
1741
1742     ##
1743     # Public Methods
1744     #
1745
1746     def open(self, startFrame=1, endFrame=1):
1747         """Do some initialization operations.
1748         """
1749         VectorWriter.open(self, startFrame, endFrame)
1750         size = (self.canvasSize[0], self.canvasSize[1])
1751         self.canvas = canvas.Canvas(self.outputFileName, pagesize=size, bottomup=0)
1752
1753     def close(self):
1754         """Do some finalization operation.
1755         """
1756         self.canvas.save()
1757
1758         # remember to call the close method of the parent
1759         VectorWriter.close(self)
1760
1761     def printCanvas(self, scene, doPrintPolygons=True, doPrintEdges=False,
1762             showHiddenEdges=False):
1763         """Convert the scene representation to SVG.
1764         """
1765         context = scene.getRenderingContext()
1766         framenumber = context.currentFrame()
1767
1768         Objects = scene.objects
1769
1770         for obj in Objects:
1771
1772             if(obj.getType() != 'Mesh'):
1773                 continue
1774
1775             mesh = obj.getData(mesh=1)
1776
1777             if doPrintPolygons:
1778                 self._printPolygons(mesh)
1779
1780             if doPrintEdges:
1781                 self._printEdges(mesh, showHiddenEdges)
1782
1783         self.canvas.showPage()
1784
1785     ##
1786     # Private Methods
1787     #
1788
1789     def _calcCanvasCoord(self, v):
1790         """Convert vertex in scene coordinates to canvas coordinates.
1791         """
1792
1793         pt = Vector([0, 0, 0])
1794
1795         mW = float(self.canvasSize[0])/2.0
1796         mH = float(self.canvasSize[1])/2.0
1797
1798         # rescale to canvas size
1799         pt[0] = v.co[0]*mW + mW
1800         pt[1] = v.co[1]*mH + mH
1801         pt[2] = v.co[2]
1802
1803         # For now we want (0,0) in the top-left corner of the canvas.
1804         # Mirror and translate along y
1805         pt[1] *= -1
1806         pt[1] += self.canvasSize[1]
1807
1808         return pt
1809
1810     def _printPolygons(self, mesh):
1811         """Print the selected (visible) polygons.
1812         """
1813
1814         if len(mesh.faces) == 0:
1815             return
1816
1817         for face in mesh.faces:
1818             if not face.sel:
1819                continue
1820
1821             if face.col:
1822                 fcol = face.col[0]
1823                 color = [fcol.r/255.0, fcol.g/255.0, fcol.b/255.0,
1824                         fcol.a/255.0]
1825             else:
1826                 color = [1, 1, 1, 1]
1827
1828             self.canvas.setFillColorRGB(color[0], color[1], color[2])
1829             # For debug
1830             self.canvas.setStrokeColorRGB(0, 0, 0)
1831
1832             path = self.canvas.beginPath()
1833
1834             # The starting point of the path
1835             p0 = self._calcCanvasCoord(face.verts[0])
1836             path.moveTo(p0[0], p0[1])
1837
1838             for v in face.verts[1:]:
1839                 p = self._calcCanvasCoord(v)
1840                 path.lineTo(p[0], p[1])
1841
1842             # Closing the shape
1843             path.close()
1844
1845             self.canvas.drawPath(path, stroke=0, fill=1)
1846
1847     def _printEdges(self, mesh, showHiddenEdges=False):
1848         """Print the wireframe using mesh edges.
1849         """
1850
1851         stroke_width = config.edges['WIDTH']
1852         stroke_col = config.edges['COLOR']
1853
1854         self.canvas.setLineCap(1)
1855         self.canvas.setLineJoin(1)
1856         self.canvas.setLineWidth(stroke_width)
1857         self.canvas.setStrokeColorRGB(stroke_col[0]/255.0, stroke_col[1]/255.0,
1858             stroke_col[2]/255)
1859
1860         for e in mesh.edges:
1861
1862             self.canvas.setLineWidth(stroke_width)
1863
1864             if e.sel == 0:
1865                 if showHiddenEdges == False:
1866                     continue
1867                 else:
1868                     # PDF does not support dashed lines natively, so -for now-
1869                     # draw hidden lines thinner
1870                     self.canvas.setLineWidth(stroke_width/2.0)
1871
1872             p1 = self._calcCanvasCoord(e.v1)
1873             p2 = self._calcCanvasCoord(e.v2)
1874
1875             self.canvas.line(p1[0], p1[1], p2[0], p2[1])
1876
1877
1878
1879 # ---------------------------------------------------------------------
1880 #
1881 ## Rendering Classes
1882 #
1883 # ---------------------------------------------------------------------
1884
1885 # A dictionary to collect different shading style methods
1886 shadingStyles = dict()
1887 shadingStyles['FLAT'] = None
1888 shadingStyles['TOON'] = None
1889
1890 # A dictionary to collect different edge style methods
1891 edgeStyles = dict()
1892 edgeStyles['MESH'] = MeshUtils.isMeshEdge
1893 edgeStyles['SILHOUETTE'] = MeshUtils.isSilhouetteEdge
1894
1895 # A dictionary to collect the supported output formats
1896 outputWriters = dict()
1897 outputWriters['SVG'] = SVGVectorWriter
1898 if SWFSupported:
1899     outputWriters['SWF'] = SWFVectorWriter
1900 if PDFSupported:
1901     outputWriters['PDF'] = PDFVectorWriter
1902
1903
1904 class Renderer:
1905     """Render a scene viewed from the active camera.
1906
1907     This class is responsible of the rendering process, transformation and
1908     projection of the objects in the scene are invoked by the renderer.
1909
1910     The rendering is done using the active camera for the current scene.
1911     """
1912
1913     def __init__(self):
1914         """Make the rendering process only for the current scene by default.
1915
1916         We will work on a copy of the scene, to be sure that the current scene do
1917         not get modified in any way.
1918         """
1919
1920         # Render the current Scene, this should be a READ-ONLY property
1921         self._SCENE = Scene.GetCurrent()
1922
1923         # Use the aspect ratio of the scene rendering context
1924         context = self._SCENE.getRenderingContext()
1925
1926         aspect_ratio = float(context.imageSizeX())/float(context.imageSizeY())
1927         self.canvasRatio = (float(context.aspectRatioX())*aspect_ratio,
1928                             float(context.aspectRatioY())
1929                             )
1930
1931         # Render from the currently active camera
1932         #self.cameraObj = self._SCENE.objects.camera
1933
1934         self.lights = []
1935
1936
1937     ##
1938     # Public Methods
1939     #
1940
1941     def doRendering(self, outputWriter, animation=False):
1942         """Render picture or animation and write it out.
1943
1944         The parameters are:
1945             - a Vector writer object that will be used to output the result.
1946             - a flag to tell if we want to render an animation or only the
1947               current frame.
1948         """
1949
1950         context = self._SCENE.getRenderingContext()
1951         origCurrentFrame = context.currentFrame()
1952
1953         # Handle the animation case
1954         if not animation:
1955             startFrame = origCurrentFrame
1956             endFrame = startFrame
1957             outputWriter.open()
1958         else:
1959             startFrame = context.startFrame()
1960             endFrame = context.endFrame()
1961             outputWriter.open(startFrame, endFrame)
1962
1963         # Do the rendering process frame by frame
1964         print "Start Rendering of %d frames" % (endFrame-startFrame+1)
1965         for f in xrange(startFrame, endFrame+1):
1966             print "\n\nFrame: %d" % f
1967
1968             # FIXME To get the correct camera position we have to use +1 here.
1969             # Is there a bug somewhere in the Scene module?
1970             context.currentFrame(f+1)
1971             self.cameraObj = self._SCENE.objects.camera
1972
1973             # Use some temporary workspace, a full copy of the scene
1974             inputScene = self._SCENE.copy(2)
1975
1976             # To get the objects at this frame remove the +1 ...
1977             ctx = inputScene.getRenderingContext()
1978             ctx.currentFrame(f)
1979
1980
1981             # Get a projector for this camera.
1982             # NOTE: the projector wants object in world coordinates,
1983             # so we should remember to apply modelview transformations
1984             # _before_ we do projection transformations.
1985             self.proj = Projector(self.cameraObj, self.canvasRatio)
1986
1987             try:
1988                 renderedScene = self.doRenderScene(inputScene)
1989             except :
1990                 print "There was an error! Aborting."
1991                 import traceback
1992                 print traceback.print_exc()
1993
1994                 self._SCENE.makeCurrent()
1995                 Scene.Unlink(inputScene)
1996                 del inputScene
1997                 return
1998
1999             outputWriter.printCanvas(renderedScene,
2000                     doPrintPolygons = config.polygons['SHOW'],
2001                     doPrintEdges    = config.edges['SHOW'],
2002                     showHiddenEdges = config.edges['SHOW_HIDDEN'])
2003
2004             # delete the rendered scene
2005             self._SCENE.makeCurrent()
2006             Scene.Unlink(renderedScene)
2007             del renderedScene
2008
2009         outputWriter.close()
2010         print "Done!"
2011         context.currentFrame(origCurrentFrame)
2012
2013
2014     def doRenderScene(self, workScene):
2015         """Control the rendering process.
2016
2017         Here we control the entire rendering process invoking the operation
2018         needed to transform and project the 3D scene in two dimensions.
2019         """
2020
2021         # global processing of the scene
2022
2023         self._filterHiddenObjects(workScene)
2024
2025         self._buildLightSetup(workScene)
2026
2027         self._doSceneClipping(workScene)
2028
2029         self._doConvertGeometricObjsToMesh(workScene)
2030
2031         if config.output['JOIN_OBJECTS']:
2032             self._joinMeshObjectsInScene(workScene)
2033
2034         self._doSceneDepthSorting(workScene)
2035
2036         # Per object activities
2037
2038         Objects = workScene.objects
2039
2040         print "Total Objects: %d" % len(Objects)
2041         for i,obj in enumerate(Objects):
2042             print "\n\n-------"
2043             print "Rendering Object: %d" % i
2044
2045             if obj.getType() != 'Mesh':
2046                 print "Only Mesh supported! - Skipping type:", obj.getType()
2047                 continue
2048
2049             print "Rendering: ", obj.getName()
2050
2051             mesh = obj.getData(mesh=1)
2052
2053             self._doModelingTransformation(mesh, obj.matrix)
2054
2055             self._doBackFaceCulling(mesh)
2056
2057
2058             # When doing HSR with NEWELL we may want to flip all normals
2059             # toward the viewer
2060             if config.polygons['HSR'] == "NEWELL":
2061                 for f in mesh.faces:
2062                     f.sel = 1-f.sel
2063                 mesh.flipNormals()
2064                 for f in mesh.faces:
2065                     f.sel = 1
2066
2067             self._doLighting(mesh)
2068
2069             # Do "projection" now so we perform further processing
2070             # in Normalized View Coordinates
2071             self._doProjection(mesh, self.proj)
2072
2073             self._doViewFrustumClipping(mesh)
2074
2075             self._doHiddenSurfaceRemoval(mesh)
2076
2077             self._doEdgesStyle(mesh, edgeStyles[config.edges['STYLE']])
2078
2079             # Update the object data, important! :)
2080             mesh.update()
2081
2082         return workScene
2083
2084
2085     ##
2086     # Private Methods
2087     #
2088
2089     # Utility methods
2090
2091     def _getObjPosition(self, obj):
2092         """Return the obj position in World coordinates.
2093         """
2094         return obj.matrix.translationPart()
2095
2096     def _cameraViewVector(self):
2097         """Get the View Direction form the camera matrix.
2098         """
2099         return Vector(self.cameraObj.matrix[2]).resize3D()
2100
2101
2102     # Faces methods
2103
2104     def _isFaceVisible(self, face):
2105         """Determine if a face of an object is visible from the current camera.
2106
2107         The view vector is calculated from the camera location and one of the
2108         vertices of the face (expressed in World coordinates, after applying
2109         modelview transformations).
2110
2111         After those transformations we determine if a face is visible by
2112         computing the angle between the face normal and the view vector, this
2113         angle has to be between -90 and 90 degrees for the face to be visible.
2114         This corresponds somehow to the dot product between the two, if it
2115         results > 0 then the face is visible.
2116
2117         There is no need to normalize those vectors since we are only interested in
2118         the sign of the cross product and not in the product value.
2119
2120         NOTE: here we assume the face vertices are in WorldCoordinates, so
2121         please transform the object _before_ doing the test.
2122         """
2123
2124         normal = Vector(face.no)
2125         camPos = self._getObjPosition(self.cameraObj)
2126         view_vect = None
2127
2128         # View Vector in orthographics projections is the view Direction of
2129         # the camera
2130         if self.cameraObj.data.getType() == 1:
2131             view_vect = self._cameraViewVector()
2132
2133         # View vector in perspective projections can be considered as
2134         # the difference between the camera position and one point of
2135         # the face, we choose the farthest point from the camera.
2136         if self.cameraObj.data.getType() == 0:
2137             vv = max( [ ((camPos - Vector(v.co)).length, (camPos - Vector(v.co))) for v in face] )
2138             view_vect = vv[1]
2139
2140
2141         # if d > 0 the face is visible from the camera
2142         d = view_vect * normal
2143
2144         if d > 0:
2145             return True
2146         else:
2147             return False
2148
2149
2150     # Scene methods
2151
2152     def _filterHiddenObjects(self, scene):
2153         """Discard object that are on hidden layers in the scene.
2154         """
2155
2156         Objects = scene.objects
2157
2158         visible_obj_list = [ obj for obj in Objects if
2159                 set(obj.layers).intersection(set(scene.getLayers())) ]
2160
2161         for o in Objects:
2162             if o not in visible_obj_list:
2163                 scene.objects.unlink(o)
2164
2165         scene.update()
2166
2167
2168
2169     def _buildLightSetup(self, scene):
2170         # Get the list of lighting sources
2171         obj_lst = scene.objects
2172         self.lights = [ o for o in obj_lst if o.getType() == 'Lamp' ]
2173
2174         # When there are no lights we use a default lighting source
2175         # that have the same position of the camera
2176         if len(self.lights) == 0:
2177             l = Lamp.New('Lamp')
2178             lobj = Object.New('Lamp')
2179             lobj.loc = self.cameraObj.loc
2180             lobj.link(l)
2181             self.lights.append(lobj)
2182
2183
2184     def _doSceneClipping(self, scene):
2185         """Clip whole objects against the View Frustum.
2186
2187         For now clip away only objects according to their center position.
2188         """
2189
2190         cam_pos = self._getObjPosition(self.cameraObj)
2191         view_vect = self._cameraViewVector()
2192
2193         near = self.cameraObj.data.clipStart
2194         far  = self.cameraObj.data.clipEnd
2195
2196         aspect = float(self.canvasRatio[0])/float(self.canvasRatio[1])
2197         fovy = atan(0.5/aspect/(self.cameraObj.data.lens/32))
2198         fovy = fovy * 360.0/pi
2199
2200         Objects = scene.objects
2201
2202         for o in Objects:
2203             if o.getType() != 'Mesh': continue;
2204
2205             """
2206             obj_vect = Vector(cam_pos) - self._getObjPosition(o)
2207
2208             d = obj_vect*view_vect
2209             theta = AngleBetweenVecs(obj_vect, view_vect)
2210
2211             # if the object is outside the view frustum, clip it away
2212             if (d < near) or (d > far) or (theta > fovy):
2213                 scene.objects.unlink(o)
2214             """
2215
2216             # Use the object bounding box
2217             # (whose points are already in WorldSpace Coordinate)
2218
2219             bb = o.getBoundBox()
2220
2221             points_outside = 0
2222             for p in bb:
2223                 p_vect = Vector(cam_pos) - Vector(p)
2224
2225                 d = p_vect * view_vect
2226                 theta = AngleBetweenVecs(p_vect, view_vect)
2227
2228                 # Is this point outside the view frustum?
2229                 if (d < near) or (d > far) or (theta > fovy):
2230                     points_outside += 1
2231
2232             # If the bb is all outside the view frustum we clip the whole
2233             # object away
2234             if points_outside == len(bb):
2235                 scene.objects.unlink(o)
2236
2237
2238
2239     def _doConvertGeometricObjsToMesh(self, scene):
2240         """Convert all "geometric" objects to mesh ones.
2241         """
2242         geometricObjTypes = ['Mesh', 'Surf', 'Curve', 'Text']
2243         #geometricObjTypes = ['Mesh', 'Surf', 'Curve']
2244
2245         Objects = scene.objects
2246
2247         objList = [ o for o in Objects if o.getType() in geometricObjTypes ]
2248         for obj in objList:
2249             old_obj = obj
2250             obj = self._convertToRawMeshObj(obj)
2251             scene.objects.link(obj)
2252             scene.objects.unlink(old_obj)
2253
2254
2255             # XXX Workaround for Text and Curve which have some normals
2256             # inverted when they are converted to Mesh, REMOVE that when
2257             # blender will fix that!!
2258             if old_obj.getType() in ['Curve', 'Text']:
2259                 me = obj.getData(mesh=1)
2260                 for f in me.faces: f.sel = 1;
2261                 for v in me.verts: v.sel = 1;
2262                 me.remDoubles(0)
2263                 me.triangleToQuad()
2264                 me.recalcNormals()
2265                 me.update()
2266
2267
2268     def _doSceneDepthSorting(self, scene):
2269         """Sort objects in the scene.
2270
2271         The object sorting is done accordingly to the object centers.
2272         """
2273
2274         c = self._getObjPosition(self.cameraObj)
2275
2276         by_obj_center_pos = (lambda o1, o2:
2277                 (o1.getType() == 'Mesh' and o2.getType() == 'Mesh') and
2278                 cmp((self._getObjPosition(o1) - Vector(c)).length,
2279                     (self._getObjPosition(o2) - Vector(c)).length)
2280             )
2281
2282         # Implement sorting by bounding box, the object with the bb
2283         # nearest to the camera should be drawn as last.
2284         by_nearest_bbox_point = (lambda o1, o2:
2285                 (o1.getType() == 'Mesh' and o2.getType() == 'Mesh') and
2286                 cmp( min( [(Vector(p) - Vector(c)).length for p in o1.getBoundBox()] ),
2287                      min( [(Vector(p) - Vector(c)).length for p in o2.getBoundBox()] )
2288                 )
2289             )
2290
2291
2292         Objects = list(scene.objects)
2293
2294         #Objects.sort(by_obj_center_pos)
2295         Objects.sort(by_nearest_bbox_point)
2296
2297         # update the scene
2298         for o in Objects:
2299             scene.objects.unlink(o)
2300             scene.objects.link(o)
2301
2302     def _joinMeshObjectsInScene(self, scene):
2303         """Merge all the Mesh Objects in a scene into a single Mesh Object.
2304         """
2305
2306         oList = [o for o in scene.objects if o.getType()=='Mesh']
2307
2308         # FIXME: Object.join() do not work if the list contains 1 object
2309         if len(oList) == 1:
2310             return
2311
2312         mesh = Mesh.New('BigOne')
2313         bigObj = Object.New('Mesh', 'BigOne')
2314         bigObj.link(mesh)
2315
2316         scene.objects.link(bigObj)
2317
2318         try:
2319             bigObj.join(oList)
2320         except RuntimeError:
2321             print "\nWarning! - Can't Join Objects\n"
2322             scene.objects.unlink(bigObj)
2323             return
2324         except TypeError:
2325             print "Objects Type error?"
2326
2327         for o in oList:
2328             scene.objects.unlink(o)
2329
2330         scene.update()
2331
2332
2333     # Per object/mesh methods
2334
2335     def _convertToRawMeshObj(self, object):
2336         """Convert geometry based object to a mesh object.
2337         """
2338         me = Mesh.New('RawMesh_'+object.name)
2339         me.getFromObject(object.name)
2340
2341         newObject = Object.New('Mesh', 'RawMesh_'+object.name)
2342         newObject.link(me)
2343
2344         # If the object has no materials set a default material
2345         if not me.materials:
2346             me.materials = [Material.New()]
2347             #for f in me.faces: f.mat = 0
2348
2349         newObject.setMatrix(object.getMatrix())
2350
2351         return newObject
2352
2353     def _doModelingTransformation(self, mesh, matrix):
2354         """Transform object coordinates to world coordinates.
2355
2356         This step is done simply applying to the object its tranformation
2357         matrix and recalculating its normals.
2358         """
2359         # XXX FIXME: blender do not transform normals in the right way when
2360         # there are negative scale values
2361         if matrix[0][0] < 0 or matrix[1][1] < 0 or matrix[2][2] < 0:
2362             print "WARNING: Negative scales, expect incorrect results!"
2363
2364         mesh.transform(matrix, True)
2365
2366     def _doBackFaceCulling(self, mesh):
2367         """Simple Backface Culling routine.
2368
2369         At this level we simply do a visibility test face by face and then
2370         select the vertices belonging to visible faces.
2371         """
2372
2373         # Select all vertices, so edges can be displayed even if there are no
2374         # faces
2375         for v in mesh.verts:
2376             v.sel = 1
2377
2378         Mesh.Mode(Mesh.SelectModes['FACE'])
2379         # Loop on faces
2380         for f in mesh.faces:
2381             f.sel = 0
2382             if self._isFaceVisible(f):
2383                 f.sel = 1
2384
2385     def _doLighting(self, mesh):
2386         """Apply an Illumination and shading model to the object.
2387
2388         The model used is the Phong one, it may be inefficient,
2389         but I'm just learning about rendering and starting from Phong seemed
2390         the most natural way.
2391         """
2392
2393         # If the mesh has vertex colors already, use them,
2394         # otherwise turn them on and do some calculations
2395         if mesh.vertexColors:
2396             return
2397         mesh.vertexColors = 1
2398
2399         materials = mesh.materials
2400
2401         camPos = self._getObjPosition(self.cameraObj)
2402
2403         # We do per-face color calculation (FLAT Shading), we can easily turn
2404         # to a per-vertex calculation if we want to implement some shading
2405         # technique. For an example see:
2406         # http://www.miralab.unige.ch/papers/368.pdf
2407         for f in mesh.faces:
2408             if not f.sel:
2409                 continue
2410
2411             mat = None
2412             if materials:
2413                 mat = materials[f.mat]
2414
2415             # A new default material
2416             if mat == None:
2417                 mat = Material.New('defMat')
2418
2419             # Check if it is a shadeless material
2420             elif mat.getMode() & Material.Modes['SHADELESS']:
2421                 I = mat.getRGBCol()
2422                 # Convert to a value between 0 and 255
2423                 tmp_col = [ int(c * 255.0) for c in I]
2424
2425                 for c in f.col:
2426                     c.r = tmp_col[0]
2427                     c.g = tmp_col[1]
2428                     c.b = tmp_col[2]
2429                     #c.a = tmp_col[3]
2430
2431                 continue
2432
2433
2434             # do vertex color calculation
2435
2436             TotDiffSpec = Vector([0.0, 0.0, 0.0])
2437
2438             for l in self.lights:
2439                 light_obj = l
2440                 light_pos = self._getObjPosition(l)
2441                 light = light_obj.getData()
2442
2443                 L = Vector(light_pos).normalize()
2444
2445                 V = (Vector(camPos) - Vector(f.cent)).normalize()
2446
2447                 N = Vector(f.no).normalize()
2448
2449                 if config.polygons['SHADING'] == 'TOON':
2450                     NL = ShadingUtils.toonShading(N*L)
2451                 else:
2452                     NL = (N*L)
2453
2454                 # Should we use NL instead of (N*L) here?
2455                 R = 2 * (N*L) * N - L
2456
2457                 Ip = light.getEnergy()
2458
2459                 # Diffuse co-efficient
2460                 kd = mat.getRef() * Vector(mat.getRGBCol())
2461                 for i in [0, 1, 2]:
2462                     kd[i] *= light.col[i]
2463
2464                 Idiff = Ip * kd * max(0, NL)
2465
2466
2467                 # Specular component
2468                 ks = mat.getSpec() * Vector(mat.getSpecCol())
2469                 ns = mat.getHardness()
2470                 Ispec = Ip * ks * pow(max(0, (V*R)), ns)
2471
2472                 TotDiffSpec += (Idiff+Ispec)
2473
2474
2475             # Ambient component
2476             Iamb = Vector(Blender.World.Get()[0].getAmb())
2477             ka = mat.getAmb()
2478
2479             # Emissive component (convert to a triplet)
2480             ki = Vector([mat.getEmit()]*3)
2481
2482             #I = ki + Iamb + (Idiff + Ispec)
2483             I = ki + (ka * Iamb) + TotDiffSpec
2484
2485
2486             # Set Alpha component
2487             I = list(I)
2488             I.append(mat.getAlpha())
2489
2490             # Clamp I values between 0 and 1
2491             I = [ min(c, 1) for c in I]
2492             I = [ max(0, c) for c in I]
2493
2494             # Convert to a value between 0 and 255
2495             tmp_col = [ int(c * 255.0) for c in I]
2496
2497             for c in f.col:
2498                 c.r = tmp_col[0]
2499                 c.g = tmp_col[1]
2500                 c.b = tmp_col[2]
2501                 c.a = tmp_col[3]
2502
2503     def _doProjection(self, mesh, projector):
2504         """Apply Viewing and Projection tranformations.
2505         """
2506
2507         for v in mesh.verts:
2508             p = projector.doProjection(v.co[:])
2509             v.co[0] = p[0]
2510             v.co[1] = p[1]
2511             v.co[2] = p[2]
2512
2513         #mesh.recalcNormals()
2514         #mesh.update()
2515
2516         # We could reeset Camera matrix, since now
2517         # we are in Normalized Viewing Coordinates,
2518         # but doung that would affect World Coordinate
2519         # processing for other objects
2520
2521         #self.cameraObj.data.type = 1
2522         #self.cameraObj.data.scale = 2.0
2523         #m = Matrix().identity()
2524         #self.cameraObj.setMatrix(m)
2525
2526     def _doViewFrustumClipping(self, mesh):
2527         """Clip faces against the View Frustum.
2528         """
2529
2530         # The Canonical View Volume, 8 vertices, and 6 faces,
2531         # We consider its face normals pointing outside
2532
2533         v1 = NMesh.Vert(1, 1, -1)
2534         v2 = NMesh.Vert(1, -1, -1)
2535         v3 = NMesh.Vert(-1, -1, -1)
2536         v4 = NMesh.Vert(-1, 1, -1)
2537         v5 = NMesh.Vert(1, 1, 1)
2538         v6 = NMesh.Vert(1, -1, 1)
2539         v7 = NMesh.Vert(-1, -1, 1)
2540         v8 = NMesh.Vert(-1, 1, 1)
2541
2542         cvv = []
2543         f1 = NMesh.Face([v1, v4, v3, v2])
2544         cvv.append(f1)
2545         f2 = NMesh.Face([v5, v6, v7, v8])
2546         cvv.append(f2)
2547         f3 = NMesh.Face([v1, v2, v6, v5])
2548         cvv.append(f3)
2549         f4 = NMesh.Face([v2, v3, v7, v6])
2550         cvv.append(f4)
2551         f5 = NMesh.Face([v3, v4, v8, v7])
2552         cvv.append(f5)
2553         f6 = NMesh.Face([v4, v1, v5, v8])
2554         cvv.append(f6)
2555
2556         nmesh = NMesh.GetRaw(mesh.name)
2557         clippedfaces = nmesh.faces[:]
2558         facelist = clippedfaces[:]
2559
2560         for clipface in cvv:
2561
2562             clippedfaces = []
2563
2564             for f in facelist:
2565
2566                 #newfaces = HSR.splitOn(clipface, f, return_positive_faces=False)
2567                 newfaces = None
2568
2569                 if not newfaces:
2570                     # Check if the face is all outside the view frustum
2571                     # TODO: Do this test before, it is more efficient
2572                     points_outside = 0
2573                     for v in f:
2574                         if abs(v[0]) > 1-EPS or abs(v[1]) > 1-EPS or abs(v[2]) > 1-EPS:
2575                             points_outside += 1
2576
2577                     if points_outside != len(f):
2578                         clippedfaces.append(f)
2579                 else:
2580                     for nf in newfaces:
2581                         for v in nf:
2582                             nmesh.verts.append(v)
2583
2584                         nf.mat = f.mat
2585                         nf.sel = f.sel
2586                         nf.col = [f.col[0]] * len(nf.v)
2587
2588                         clippedfaces.append(nf)
2589             facelist = clippedfaces[:]
2590
2591
2592         nmesh.faces = facelist
2593         nmesh.update()
2594
2595
2596     # HSR routines
2597     def __simpleDepthSort(self, mesh):
2598         """Sort faces by the furthest vertex.
2599
2600         This simple mesthod is known also as the painter algorithm, and it
2601         solves HSR correctly only for convex meshes.
2602         """
2603
2604         #global progress
2605
2606         # The sorting requires circa n*log(n) steps
2607         n = len(mesh.faces)
2608         progress.setActivity("HSR: Painter", n*log(n))
2609
2610         by_furthest_z = (lambda f1, f2: progress.update() and
2611                 cmp(max([v.co[2] for v in f1]), max([v.co[2] for v in f2])+EPS)
2612                 )
2613
2614         # FIXME: using NMesh to sort faces. We should avoid that!
2615         nmesh = NMesh.GetRaw(mesh.name)
2616
2617         # remember that _higher_ z values mean further points
2618         nmesh.faces.sort(by_furthest_z)
2619         nmesh.faces.reverse()
2620
2621         nmesh.update()
2622
2623
2624     def __newellDepthSort(self, mesh):
2625         """Newell's depth sorting.
2626
2627         """
2628
2629         #global progress
2630
2631         # Find non planar quads and convert them to triangle
2632         #for f in mesh.faces:
2633         #    f.sel = 0
2634         #    if is_nonplanar_quad(f.v):
2635         #        print "NON QUAD??"
2636         #        f.sel = 1
2637
2638
2639         # Now reselect all faces
2640         for f in mesh.faces:
2641             f.sel = 1
2642         mesh.quadToTriangle()
2643
2644         # FIXME: using NMesh to sort faces. We should avoid that!
2645         nmesh = NMesh.GetRaw(mesh.name)
2646
2647         # remember that _higher_ z values mean further points
2648         nmesh.faces.sort(by_furthest_z)
2649         nmesh.faces.reverse()
2650
2651         # Begin depth sort tests
2652
2653         # use the smooth flag to set marked faces
2654         for f in nmesh.faces:
2655             f.smooth = 0
2656
2657         facelist = nmesh.faces[:]
2658         maplist = []
2659
2660
2661         # The steps are _at_least_ equal to len(facelist), we do not count the
2662         # feces coming out from splitting!!
2663         progress.setActivity("HSR: Newell", len(facelist))
2664         #progress.setQuiet(True)
2665
2666
2667         while len(facelist):
2668             debug("\n----------------------\n")
2669             debug("len(facelits): %d\n" % len(facelist))
2670             P = facelist[0]
2671
2672             pSign = sign(P.normal[2])
2673
2674             # We can discard faces parallel to the view vector
2675             #if P.normal[2] == 0:
2676             #    facelist.remove(P)
2677             #    continue
2678
2679             split_done = 0
2680             face_marked = 0
2681
2682             for Q in facelist[1:]:
2683
2684                 debug("P.smooth: " + str(P.smooth) + "\n")
2685                 debug("Q.smooth: " + str(Q.smooth) + "\n")
2686                 debug("\n")
2687
2688                 qSign = sign(Q.normal[2])
2689                 # TODO: check also if Q is parallel??
2690
2691                 # Test 0: We need to test only those Qs whose furthest vertex
2692                 # is closer to the observer than the closest vertex of P.
2693
2694                 zP = [v.co[2] for v in P.v]
2695                 zQ = [v.co[2] for v in Q.v]
2696                 notZOverlap = min(zP) > max(zQ) + EPS
2697
2698                 if notZOverlap:
2699                     debug("\nTest 0\n")
2700                     debug("NOT Z OVERLAP!\n")
2701                     if Q.smooth == 0:
2702                         # If Q is not marked then we can safely print P
2703                         break
2704                     else:
2705                         debug("met a marked face\n")
2706                         continue
2707
2708
2709                 # Test 1: X extent overlapping
2710                 xP = [v.co[0] for v in P.v]
2711                 xQ = [v.co[0] for v in Q.v]
2712                 #notXOverlap = (max(xP) <= min(xQ)) or (max(xQ) <= min(xP))
2713                 notXOverlap = (min(xQ) >= max(xP)-EPS) or (min(xP) >= max(xQ)-EPS)
2714
2715                 if notXOverlap:
2716                     debug("\nTest 1\n")
2717                     debug("NOT X OVERLAP!\n")
2718                     continue
2719
2720
2721                 # Test 2: Y extent Overlapping
2722                 yP = [v.co[1] for v in P.v]
2723                 yQ = [v.co[1] for v in Q.v]
2724                 #notYOverlap = (max(yP) <= min(yQ)) or (max(yQ) <= min(yP))
2725                 notYOverlap = (min(yQ) >= max(yP)-EPS) or (min(yP) >= max(yQ)-EPS)
2726
2727                 if notYOverlap:
2728                     debug("\nTest 2\n")
2729                     debug("NOT Y OVERLAP!\n")
2730                     continue
2731
2732
2733                 # Test 3: P vertices are all behind the plane of Q
2734                 n = 0
2735                 for Pi in P:
2736                     d = qSign * HSR.Distance(Vector(Pi), Q)
2737                     if d <= EPS:
2738                         n += 1
2739                 pVerticesBehindPlaneQ = (n == len(P))
2740
2741                 if pVerticesBehindPlaneQ:
2742                     debug("\nTest 3\n")
2743                     debug("P BEHIND Q!\n")
2744                     continue
2745
2746
2747                 # Test 4: Q vertices in front of the plane of P
2748                 n = 0
2749                 for Qi in Q:
2750                     d = pSign * HSR.Distance(Vector(Qi), P)
2751                     if d >= -EPS:
2752                         n += 1
2753                 qVerticesInFrontPlaneP = (n == len(Q))
2754
2755                 if qVerticesInFrontPlaneP:
2756                     debug("\nTest 4\n")
2757                     debug("Q IN FRONT OF P!\n")
2758                     continue
2759
2760
2761                 # Test 5: Check if projections of polygons effectively overlap,
2762                 # in previous tests we checked only bounding boxes.
2763
2764                 #if not projectionsOverlap(P, Q):
2765                 if not ( HSR.projectionsOverlap(P, Q) or HSR.projectionsOverlap(Q, P)):
2766                     debug("\nTest 5\n")
2767                     debug("Projections do not overlap!\n")
2768                     continue
2769
2770                 # We still can't say if P obscures Q.
2771
2772                 # But if Q is marked we do a face-split trying to resolve a
2773                 # difficulty (maybe a visibility cycle).
2774                 if Q.smooth == 1:
2775                     # Split P or Q
2776                     debug("Possibly a cycle detected!\n")
2777                     debug("Split here!!\n")
2778
2779                     facelist = HSR.facesplit(P, Q, facelist, nmesh)
2780                     split_done = 1
2781                     break
2782
2783                 # The question now is: Does Q obscure P?
2784
2785
2786                 # Test 3bis: Q vertices are all behind the plane of P
2787                 n = 0
2788                 for Qi in Q:
2789                     d = pSign * HSR.Distance(Vector(Qi), P)
2790                     if d <= EPS:
2791                         n += 1
2792                 qVerticesBehindPlaneP = (n == len(Q))
2793
2794                 if qVerticesBehindPlaneP:
2795                     debug("\nTest 3bis\n")
2796                     debug("Q BEHIND P!\n")
2797
2798
2799                 # Test 4bis: P vertices in front of the plane of Q
2800                 n = 0
2801                 for Pi in P:
2802                     d = qSign * HSR.Distance(Vector(Pi), Q)
2803                     if d >= -EPS:
2804                         n += 1
2805                 pVerticesInFrontPlaneQ = (n == len(P))
2806
2807                 if pVerticesInFrontPlaneQ:
2808                     debug("\nTest 4bis\n")
2809                     debug("P IN FRONT OF Q!\n")
2810
2811
2812                 # We don't even know if Q does obscure P, so they should
2813                 # intersect each other, split one of them in two parts.
2814                 if not qVerticesBehindPlaneP and not pVerticesInFrontPlaneQ:
2815                     debug("\nSimple Intersection?\n")
2816                     debug("Test 3bis or 4bis failed\n")
2817                     debug("Split here!!2\n")
2818
2819                     facelist = HSR.facesplit(P, Q, facelist, nmesh)
2820                     split_done = 1
2821                     break
2822
2823                 facelist.remove(Q)
2824                 facelist.insert(0, Q)
2825                 Q.smooth = 1
2826                 face_marked = 1
2827                 debug("Q marked!\n")
2828                 break
2829
2830             # Write P!
2831             if split_done == 0 and face_marked == 0:
2832                 facelist.remove(P)
2833                 maplist.append(P)
2834                 dumpfaces(maplist, "dump"+str(len(maplist)).zfill(4)+".svg")
2835
2836                 progress.update()
2837
2838             if len(facelist) == 870:
2839                 dumpfaces([P, Q], "loopdebug.svg")
2840
2841
2842             #if facelist == None:
2843             #    maplist = [P, Q]
2844             #    print [v.co for v in P]
2845             #    print [v.co for v in Q]
2846             #    break
2847
2848             # end of while len(facelist)
2849
2850
2851         nmesh.faces = maplist
2852         #for f in nmesh.faces:
2853         #    f.sel = 1
2854
2855         nmesh.update()
2856
2857
2858     def _doHiddenSurfaceRemoval(self, mesh):
2859         """Do HSR for the given mesh.
2860         """
2861         if len(mesh.faces) == 0:
2862             return
2863
2864         if config.polygons['HSR'] == 'PAINTER':
2865             print "\nUsing the Painter algorithm for HSR."
2866             self.__simpleDepthSort(mesh)
2867
2868         elif config.polygons['HSR'] == 'NEWELL':
2869             print "\nUsing the Newell's algorithm for HSR."
2870             self.__newellDepthSort(mesh)
2871
2872
2873     def _doEdgesStyle(self, mesh, edgestyleSelect):
2874         """Process Mesh Edges accroding to a given selection style.
2875
2876         Examples of algorithms:
2877
2878         Contours:
2879             given an edge if its adjacent faces have the same normal (that is
2880             they are complanar), than deselect it.
2881
2882         Silhouettes:
2883             given an edge if one its adjacent faces is frontfacing and the
2884             other is backfacing, than select it, else deselect.
2885         """
2886
2887         Mesh.Mode(Mesh.SelectModes['EDGE'])
2888
2889         edge_cache = MeshUtils.buildEdgeFaceUsersCache(mesh)
2890
2891         for i,edge_faces in enumerate(edge_cache):
2892             mesh.edges[i].sel = 0
2893             if edgestyleSelect(edge_faces):
2894                 mesh.edges[i].sel = 1
2895
2896         """
2897         for e in mesh.edges:
2898
2899             e.sel = 0
2900             if edgestyleSelect(e, mesh):
2901                 e.sel = 1
2902         """
2903         #
2904
2905
2906 # ---------------------------------------------------------------------
2907 #
2908 ## GUI Class and Main Program
2909 #
2910 # ---------------------------------------------------------------------
2911
2912
2913 from Blender import BGL, Draw
2914 from Blender.BGL import *
2915
2916 class GUI:
2917
2918     def _init():
2919
2920         # Output Format menu
2921         output_format = config.output['FORMAT']
2922         default_value = outputWriters.keys().index(output_format)+1
2923         GUI.outFormatMenu = Draw.Create(default_value)
2924         GUI.evtOutFormatMenu = 0
2925
2926         # Animation toggle button
2927         GUI.animToggle = Draw.Create(config.output['ANIMATION'])
2928         GUI.evtAnimToggle = 1
2929
2930         # Join Objects toggle button
2931         GUI.joinObjsToggle = Draw.Create(config.output['JOIN_OBJECTS'])
2932         GUI.evtJoinObjsToggle = 2
2933
2934         # Render filled polygons
2935         GUI.polygonsToggle = Draw.Create(config.polygons['SHOW'])
2936
2937         # Shading Style menu
2938         shading_style = config.polygons['SHADING']
2939         default_value = shadingStyles.keys().index(shading_style)+1
2940         GUI.shadingStyleMenu = Draw.Create(default_value)
2941         GUI.evtShadingStyleMenu = 21
2942
2943         GUI.evtPolygonsToggle = 3
2944         # We hide the config.polygons['EXPANSION_TRICK'], for now
2945
2946         # Render polygon edges
2947         GUI.showEdgesToggle = Draw.Create(config.edges['SHOW'])
2948         GUI.evtShowEdgesToggle = 4
2949
2950         # Render hidden edges
2951         GUI.showHiddenEdgesToggle = Draw.Create(config.edges['SHOW_HIDDEN'])
2952         GUI.evtShowHiddenEdgesToggle = 5
2953
2954         # Edge Style menu
2955         edge_style = config.edges['STYLE']
2956         default_value = edgeStyles.keys().index(edge_style)+1
2957         GUI.edgeStyleMenu = Draw.Create(default_value)
2958         GUI.evtEdgeStyleMenu = 6
2959
2960         # Edge Width slider
2961         GUI.edgeWidthSlider = Draw.Create(config.edges['WIDTH'])
2962         GUI.evtEdgeWidthSlider = 7
2963
2964         # Edge Color Picker
2965         c = config.edges['COLOR']
2966         GUI.edgeColorPicker = Draw.Create(c[0]/255.0, c[1]/255.0, c[2]/255.0)
2967         GUI.evtEdgeColorPicker = 71
2968
2969         # Render Button
2970         GUI.evtRenderButton = 8
2971
2972         # Exit Button
2973         GUI.evtExitButton = 9
2974
2975         # Save default button
2976         GUI.evtSaveDefaultButton = 99
2977
2978     def draw():
2979
2980         # initialize static members
2981         GUI._init()
2982
2983         glClear(GL_COLOR_BUFFER_BIT)
2984         glColor3f(0.0, 0.0, 0.0)
2985         glRasterPos2i(10, 380)
2986         Draw.Text("VRM: Vector Rendering Method script. Version %s." %
2987                 __version__)
2988         glRasterPos2i(10, 365)
2989         Draw.Text("%s (c) 2006, 2007" % __author__)
2990
2991         glRasterPos2i(10, 335)
2992         Draw.Text("Press Q or ESC to quit.")
2993
2994         # Build the output format menu
2995         glRasterPos2i(10, 310)
2996         Draw.Text("Select the output Format:")
2997         outMenuStruct = "Output Format %t"
2998         for t in outputWriters.keys():
2999            outMenuStruct = outMenuStruct + "|%s" % t
3000         GUI.outFormatMenu = Draw.Menu(outMenuStruct, GUI.evtOutFormatMenu,
3001                 10, 285, 160, 18, GUI.outFormatMenu.val, "Choose the Output Format")
3002
3003         # Animation toggle
3004         GUI.animToggle = Draw.Toggle("Animation", GUI.evtAnimToggle,
3005                 10, 260, 160, 18, GUI.animToggle.val,
3006                 "Toggle rendering of animations")
3007
3008         # Join Objects toggle
3009         GUI.joinObjsToggle = Draw.Toggle("Join objects", GUI.evtJoinObjsToggle,
3010                 10, 235, 160, 18, GUI.joinObjsToggle.val,
3011                 "Join objects in the rendered file")
3012
3013         # Render Button
3014         Draw.Button("Render", GUI.evtRenderButton, 10, 210-25, 75, 25+18,
3015                 "Start Rendering")
3016         Draw.Button("Exit", GUI.evtExitButton, 95, 210-25, 75, 25+18, "Exit!")
3017
3018         Draw.Button("Save settings as default", GUI.evtSaveDefaultButton, 10, 210-50, 160, 18,
3019                 "Save settings as default")
3020
3021         # Rendering Styles
3022         glRasterPos2i(200, 310)
3023         Draw.Text("Rendering Style:")
3024
3025         # Render Polygons
3026         GUI.polygonsToggle = Draw.Toggle("Filled Polygons", GUI.evtPolygonsToggle,
3027                 200, 285, 160, 18, GUI.polygonsToggle.val,
3028                 "Render filled polygons")
3029
3030         if GUI.polygonsToggle.val == 1:
3031
3032             # Polygon Shading Style
3033             shadingStyleMenuStruct = "Shading Style %t"
3034             for t in shadingStyles.keys():
3035                 shadingStyleMenuStruct = shadingStyleMenuStruct + "|%s" % t.lower()
3036             GUI.shadingStyleMenu = Draw.Menu(shadingStyleMenuStruct, GUI.evtShadingStyleMenu,
3037                     200, 260, 160, 18, GUI.shadingStyleMenu.val,
3038                     "Choose the shading style")
3039
3040
3041         # Render Edges
3042         GUI.showEdgesToggle = Draw.Toggle("Show Edges", GUI.evtShowEdgesToggle,
3043                 200, 235, 160, 18, GUI.showEdgesToggle.val,
3044                 "Render polygon edges")
3045
3046         if GUI.showEdgesToggle.val == 1:
3047
3048             # Edge Style
3049             edgeStyleMenuStruct = "Edge Style %t"
3050             for t in edgeStyles.keys():
3051                 edgeStyleMenuStruct = edgeStyleMenuStruct + "|%s" % t.lower()
3052             GUI.edgeStyleMenu = Draw.Menu(edgeStyleMenuStruct, GUI.evtEdgeStyleMenu,
3053                     200, 210, 160, 18, GUI.edgeStyleMenu.val,
3054                     "Choose the edge style")
3055
3056             # Edge size
3057             GUI.edgeWidthSlider = Draw.Slider("Width: ", GUI.evtEdgeWidthSlider,
3058                     200, 185, 140, 18, GUI.edgeWidthSlider.val,
3059                     0.0, 10.0, 0, "Change Edge Width")
3060
3061             # Edge Color
3062             GUI.edgeColorPicker = Draw.ColorPicker(GUI.evtEdgeColorPicker,
3063                     342, 185, 18, 18, GUI.edgeColorPicker.val, "Choose Edge Color")
3064
3065             # Show Hidden Edges
3066             GUI.showHiddenEdgesToggle = Draw.Toggle("Show Hidden Edges",
3067                     GUI.evtShowHiddenEdgesToggle,
3068                     200, 160, 160, 18, GUI.showHiddenEdgesToggle.val,
3069                     "Render hidden edges as dashed lines")
3070
3071
3072     def event(evt, val):
3073
3074         if evt == Draw.ESCKEY or evt == Draw.QKEY:
3075             Draw.Exit()
3076         else:
3077             return
3078
3079         Draw.Redraw(1)
3080
3081     def button_event(evt):
3082
3083         if evt == GUI.evtExitButton:
3084             Draw.Exit()
3085
3086         elif evt == GUI.evtOutFormatMenu:
3087             i = GUI.outFormatMenu.val - 1
3088             config.output['FORMAT']= outputWriters.keys()[i]
3089             # Set the new output file
3090             global outputfile
3091             outputfile = Blender.sys.splitext(basename)[0] + "." + str(config.output['FORMAT']).lower()
3092
3093         elif evt == GUI.evtAnimToggle:
3094             config.output['ANIMATION'] = bool(GUI.animToggle.val)
3095
3096         elif evt == GUI.evtJoinObjsToggle:
3097             config.output['JOIN_OBJECTS'] = bool(GUI.joinObjsToggle.val)
3098
3099         elif evt == GUI.evtPolygonsToggle:
3100             config.polygons['SHOW'] = bool(GUI.polygonsToggle.val)
3101
3102         elif evt == GUI.evtShadingStyleMenu:
3103             i = GUI.shadingStyleMenu.val - 1
3104             config.polygons['SHADING'] = shadingStyles.keys()[i]
3105
3106         elif evt == GUI.evtShowEdgesToggle:
3107             config.edges['SHOW'] = bool(GUI.showEdgesToggle.val)
3108
3109         elif evt == GUI.evtShowHiddenEdgesToggle:
3110             config.edges['SHOW_HIDDEN'] = bool(GUI.showHiddenEdgesToggle.val)
3111
3112         elif evt == GUI.evtEdgeStyleMenu:
3113             i = GUI.edgeStyleMenu.val - 1
3114             config.edges['STYLE'] = edgeStyles.keys()[i]
3115
3116         elif evt == GUI.evtEdgeWidthSlider:
3117             config.edges['WIDTH'] = float(GUI.edgeWidthSlider.val)
3118
3119         elif evt == GUI.evtEdgeColorPicker:
3120             config.edges['COLOR'] = [int(c*255.0) for c in GUI.edgeColorPicker.val]
3121
3122         elif evt == GUI.evtRenderButton:
3123             label = "Save %s" % config.output['FORMAT']
3124             # Show the File Selector
3125             global outputfile
3126             Blender.Window.FileSelector(vectorize, label, outputfile)
3127
3128         elif evt == GUI.evtSaveDefaultButton:
3129             config.saveToRegistry()
3130
3131         else:
3132             print "Event: %d not handled!" % evt
3133
3134         if evt:
3135             Draw.Redraw(1)
3136             #GUI.conf_debug()
3137
3138     def conf_debug():
3139         from pprint import pprint
3140         print "\nConfig"
3141         pprint(config.output)
3142         pprint(config.polygons)
3143         pprint(config.edges)
3144
3145     _init = staticmethod(_init)
3146     draw = staticmethod(draw)
3147     event = staticmethod(event)
3148     button_event = staticmethod(button_event)
3149     conf_debug = staticmethod(conf_debug)
3150
3151 # A wrapper function for the vectorizing process
3152 def vectorize(filename):
3153     """The vectorizing process is as follows:
3154
3155      - Instanciate the writer and the renderer
3156      - Render!
3157      """
3158
3159     if filename == "":
3160         print "\nERROR: invalid file name!"
3161         return
3162
3163     from Blender import Window
3164     editmode = Window.EditMode()
3165     if editmode: Window.EditMode(0)
3166
3167     actualWriter = outputWriters[config.output['FORMAT']]
3168     writer = actualWriter(filename)
3169
3170     renderer = Renderer()
3171     renderer.doRendering(writer, config.output['ANIMATION'])
3172
3173     if editmode: Window.EditMode(1)
3174
3175
3176
3177 # Here the main
3178 if __name__ == "__main__":
3179
3180     global progress
3181
3182     config.loadFromRegistry()
3183
3184     # initialize writer setting also here to configure writer specific
3185     # settings on startup
3186     actualWriter = outputWriters[config.output['FORMAT']]
3187     writer = actualWriter("")
3188
3189     outputfile = ""
3190     basename = Blender.sys.basename(Blender.Get('filename'))
3191     if basename != "":
3192         outputfile = Blender.sys.splitext(basename)[0] + "." + str(config.output['FORMAT']).lower()
3193
3194     if Blender.mode == 'background':
3195         progress = ConsoleProgressIndicator()
3196         vectorize(outputfile)
3197     else:
3198         progress = GraphicalProgressIndicator()
3199         Draw.Register(GUI.draw, GUI.event, GUI.button_event)