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