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