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