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