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