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