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