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