Add test 5 of Newell algorithm
[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 #   - Check memory use!!
62 #   - Support Indexed palettes!! (Useful for ILDA FILES, for example,
63 #     see http://www.linux-laser.org/download/autotrace/ilda-output.patch)
64 #
65 # ---------------------------------------------------------------------
66 #
67 # Changelog:
68 #
69 #   vrm-0.3.py  - ...
70 #     * First release after code restucturing.
71 #       Now the script offers a useful set of functionalities
72 #       and it can render animations, too.
73 #     * Optimization in Renderer.doEdgeStyle(), build a topology cache
74 #       so to speed up the lookup of adjacent faces of an edge.
75 #       Thanks ideasman42.
76 #     * The SVG output is now SVG 1.0 valid.
77 #       Checked with: http://jiggles.w3.org/svgvalidator/ValidatorURI.html
78 #     * Progress indicator during HSR.
79 #
80 # ---------------------------------------------------------------------
81
82 import Blender
83 from Blender import Scene, Object, Mesh, NMesh, Material, Lamp, Camera, Window
84 from Blender.Mathutils import *
85 from math import *
86 import sys, time
87
88
89 # Some global settings
90
91 class config:
92     polygons = dict()
93     polygons['SHOW'] = True
94     polygons['SHADING'] = 'FLAT'
95     polygons['HSR'] = 'PAINTER' # 'PAINTER' or 'NEWELL'
96     polygons['HSR'] = 'NEWELL'
97     # Hidden to the user for now
98     polygons['EXPANSION_TRICK'] = True
99
100     polygons['TOON_LEVELS'] = 2
101
102     edges = dict()
103     edges['SHOW'] = False
104     edges['SHOW_HIDDEN'] = False
105     edges['STYLE'] = 'MESH'
106     edges['WIDTH'] = 2
107     edges['COLOR'] = [0, 0, 0]
108
109     output = dict()
110     output['FORMAT'] = 'SVG'
111     output['ANIMATION'] = False
112     output['JOIN_OBJECTS'] = True
113
114
115
116 # Debug utility function
117 print_debug = True
118 def debug(msg):
119     if print_debug:
120         sys.stderr.write(msg)
121
122
123 # ---------------------------------------------------------------------
124 #
125 ## Mesh Utility class
126 #
127 # ---------------------------------------------------------------------
128 class MeshUtils:
129
130     def buildEdgeFaceUsersCache(me):
131         ''' 
132         Takes a mesh and returns a list aligned with the meshes edges.
133         Each item is a list of the faces that use the edge
134         would be the equiv for having ed.face_users as a property
135
136         Taken from .blender/scripts/bpymodules/BPyMesh.py,
137         thanks to ideasman_42.
138         '''
139
140         def sorted_edge_indicies(ed):
141             i1= ed.v1.index
142             i2= ed.v2.index
143             if i1>i2:
144                 i1,i2= i2,i1
145             return i1, i2
146
147         
148         face_edges_dict= dict([(sorted_edge_indicies(ed), (ed.index, [])) for ed in me.edges])
149         for f in me.faces:
150             fvi= [v.index for v in f.v]# face vert idx's
151             for i in xrange(len(f)):
152                 i1= fvi[i]
153                 i2= fvi[i-1]
154                 
155                 if i1>i2:
156                     i1,i2= i2,i1
157                 
158                 face_edges_dict[i1,i2][1].append(f)
159         
160         face_edges= [None] * len(me.edges)
161         for ed_index, ed_faces in face_edges_dict.itervalues():
162             face_edges[ed_index]= ed_faces
163         
164         return face_edges
165
166     def isMeshEdge(adjacent_faces):
167         """Mesh edge rule.
168
169         A mesh edge is visible if _at_least_one_ of its adjacent faces is selected.
170         Note: if the edge has no adjacent faces we want to show it as well,
171         useful for "edge only" portion of objects.
172         """
173
174         if len(adjacent_faces) == 0:
175             return True
176
177         selected_faces = [f for f in adjacent_faces if f.sel]
178
179         if len(selected_faces) != 0:
180             return True
181         else:
182             return False
183
184     def isSilhouetteEdge(adjacent_faces):
185         """Silhuette selection rule.
186
187         An edge is a silhuette edge if it is shared by two faces with
188         different selection status or if it is a boundary edge of a selected
189         face.
190         """
191
192         if ((len(adjacent_faces) == 1 and adjacent_faces[0].sel == 1) or
193             (len(adjacent_faces) == 2 and
194                 adjacent_faces[0].sel != adjacent_faces[1].sel)
195             ):
196             return True
197         else:
198             return False
199
200     buildEdgeFaceUsersCache = staticmethod(buildEdgeFaceUsersCache)
201     isMeshEdge = staticmethod(isMeshEdge)
202     isSilhouetteEdge = staticmethod(isSilhouetteEdge)
203
204
205 # ---------------------------------------------------------------------
206 #
207 ## Shading Utility class
208 #
209 # ---------------------------------------------------------------------
210 class ShadingUtils:
211
212     shademap = None
213
214     def toonShadingMapSetup():
215         levels = config.polygons['TOON_LEVELS']
216
217         texels = 2*levels - 1
218         tmp_shademap = [0.0] + [(i)/float(texels-1) for i in xrange(1, texels-1) ] + [1.0]
219
220         return tmp_shademap
221
222     def toonShading(u):
223
224         shademap = ShadingUtils.shademap
225
226         if not shademap:
227             shademap = ShadingUtils.toonShadingMapSetup()
228
229         v = 1.0
230         for i in xrange(0, len(shademap)-1):
231             pivot = (shademap[i]+shademap[i+1])/2.0
232             j = int(u>pivot)
233
234             v = shademap[i+j]
235
236             if v < shademap[i+1]:
237                 return v
238
239         return v
240
241     toonShadingMapSetup = staticmethod(toonShadingMapSetup)
242     toonShading = staticmethod(toonShading)
243
244
245 # ---------------------------------------------------------------------
246 #
247 ## Projections classes
248 #
249 # ---------------------------------------------------------------------
250
251 class Projector:
252     """Calculate the projection of an object given the camera.
253     
254     A projector is useful to so some per-object transformation to obtain the
255     projection of an object given the camera.
256     
257     The main method is #doProjection# see the method description for the
258     parameter list.
259     """
260
261     def __init__(self, cameraObj, canvasRatio):
262         """Calculate the projection matrix.
263
264         The projection matrix depends, in this case, on the camera settings.
265         TAKE CARE: This projector expects vertices in World Coordinates!
266         """
267
268         camera = cameraObj.getData()
269
270         aspect = float(canvasRatio[0])/float(canvasRatio[1])
271         near = camera.clipStart
272         far = camera.clipEnd
273
274         scale = float(camera.scale)
275
276         fovy = atan(0.5/aspect/(camera.lens/32))
277         fovy = fovy * 360.0/pi
278         
279         # What projection do we want?
280         if camera.type == 0:
281             mP = self._calcPerspectiveMatrix(fovy, aspect, near, far) 
282         elif camera.type == 1:
283             mP = self._calcOrthoMatrix(fovy, aspect, near, far, scale) 
284         
285         # View transformation
286         cam = Matrix(cameraObj.getInverseMatrix())
287         cam.transpose() 
288         
289         mP = mP * cam
290
291         self.projectionMatrix = mP
292
293     ##
294     # Public methods
295     #
296
297     def doProjection(self, v):
298         """Project the point on the view plane.
299
300         Given a vertex calculate the projection using the current projection
301         matrix.
302         """
303         
304         # Note that we have to work on the vertex using homogeneous coordinates
305         # From blender 2.42+ we don't need to resize the vector to be 4d
306         # when applying a 4x4 matrix, but we do that anyway since we need the
307         # 4th coordinate later
308         p = self.projectionMatrix * Vector(v).resize4D()
309         
310         # Perspective division
311         if p[3] != 0:
312             p[0] = p[0]/p[3]
313             p[1] = p[1]/p[3]
314             p[2] = p[2]/p[3]
315
316         # restore the size
317         p[3] = 1.0
318         p.resize3D()
319
320         return p
321
322
323     ##
324     # Private methods
325     #
326     
327     def _calcPerspectiveMatrix(self, fovy, aspect, near, far):
328         """Return a perspective projection matrix.
329         """
330         
331         top = near * tan(fovy * pi / 360.0)
332         bottom = -top
333         left = bottom*aspect
334         right= top*aspect
335         x = (2.0 * near) / (right-left)
336         y = (2.0 * near) / (top-bottom)
337         a = (right+left) / (right-left)
338         b = (top+bottom) / (top - bottom)
339         c = - ((far+near) / (far-near))
340         d = - ((2*far*near)/(far-near))
341         
342         m = Matrix(
343                 [x,   0.0,    a,    0.0],
344                 [0.0,   y,    b,    0.0],
345                 [0.0, 0.0,    c,      d],
346                 [0.0, 0.0, -1.0,    0.0])
347
348         return m
349
350     def _calcOrthoMatrix(self, fovy, aspect , near, far, scale):
351         """Return an orthogonal projection matrix.
352         """
353         
354         # The 11 in the formula was found emiprically
355         top = near * tan(fovy * pi / 360.0) * (scale * 11)
356         bottom = -top 
357         left = bottom * aspect
358         right= top * aspect
359         rl = right-left
360         tb = top-bottom
361         fn = near-far 
362         tx = -((right+left)/rl)
363         ty = -((top+bottom)/tb)
364         tz = ((far+near)/fn)
365
366         m = Matrix(
367                 [2.0/rl, 0.0,    0.0,     tx],
368                 [0.0,    2.0/tb, 0.0,     ty],
369                 [0.0,    0.0,    2.0/fn,  tz],
370                 [0.0,    0.0,    0.0,    1.0])
371         
372         return m
373
374
375 # ---------------------------------------------------------------------
376 #
377 ## Progress Indicator
378 #
379 # ---------------------------------------------------------------------
380
381 class Progress:
382     """A model for a progress indicator.
383     
384     Do the progress calculation calculation and
385     the view independent stuff of a progress indicator.
386     """
387     def __init__(self, steps=0):
388         self.name = ""
389         self.steps = steps
390         self.completed = 0
391         self.progress = 0
392
393     def setSteps(self, steps):
394         """Set the number of steps of the activity wich we want to track.
395         """
396         self.steps = steps
397
398     def getSteps(self):
399         return self.steps
400
401     def setName(self, name):
402         """Set the name of the activity wich we want to track.
403         """
404         self.name = name
405
406     def getName(self):
407         return self.name
408
409     def getProgress(self):
410         return self.progress
411
412     def reset(self):
413         self.completed = 0
414         self.progress = 0
415
416     def update(self):
417         """Update the model, call this method when one step is completed.
418         """
419         if self.progress == 100:
420             return False
421
422         self.completed += 1
423         self.progress = ( float(self.completed) / float(self.steps) ) * 100
424         self.progress = int(self.progress)
425
426         return True
427
428
429 class ProgressIndicator:
430     """An abstraction of a View for the Progress Model
431     """
432     def __init__(self):
433
434         # Use a refresh rate so we do not show the progress at
435         # every update, but every 'self.refresh_rate' times.
436         self.refresh_rate = 10
437         self.shows_counter = 0
438
439         self.quiet = False
440
441         self.progressModel = None
442
443     def setQuiet(self, value):
444         self.quiet = value
445
446     def setActivity(self, name, steps):
447         """Initialize the Model.
448
449         In a future version (with subactivities-progress support) this method
450         could only set the current activity.
451         """
452         self.progressModel = Progress()
453         self.progressModel.setName(name)
454         self.progressModel.setSteps(steps)
455
456     def getActivity(self):
457         return self.progressModel
458
459     def update(self):
460         """Update the model and show the actual progress.
461         """
462         assert(self.progressModel)
463
464         if self.progressModel.update():
465             if self.quiet:
466                 return
467
468             self.show(self.progressModel.getProgress(),
469                     self.progressModel.getName())
470
471         # We return always True here so we can call the update() method also
472         # from lambda funcs (putting the call in logical AND with other ops)
473         return True
474
475     def show(self, progress, name=""):
476         self.shows_counter = (self.shows_counter + 1) % self.refresh_rate
477         if self.shows_counter != 0:
478             return
479
480         if progress == 100:
481             self.shows_counter = -1
482
483
484 class ConsoleProgressIndicator(ProgressIndicator):
485     """Show a progress bar on stderr, a la wget.
486     """
487     def __init__(self):
488         ProgressIndicator.__init__(self)
489
490         self.swirl_chars = ["-", "\\", "|", "/"]
491         self.swirl_count = -1
492
493     def show(self, progress, name):
494         ProgressIndicator.show(self, progress, name)
495         
496         bar_length = 70
497         bar_progress = int( (progress/100.0) * bar_length )
498         bar = ("=" * bar_progress).ljust(bar_length)
499
500         self.swirl_count = (self.swirl_count+1)%len(self.swirl_chars)
501         swirl_char = self.swirl_chars[self.swirl_count]
502
503         progress_bar = "%s |%s| %c %3d%%" % (name, bar, swirl_char, progress)
504
505         sys.stderr.write(progress_bar+"\r")
506         if progress == 100:
507             sys.stderr.write("\n")
508
509
510 class GraphicalProgressIndicator(ProgressIndicator):
511     """Interface to the Blender.Window.DrawProgressBar() method.
512     """
513     def __init__(self):
514         ProgressIndicator.__init__(self)
515
516         #self.swirl_chars = ["-", "\\", "|", "/"]
517         # We have to use letters with the same width, for now!
518         # Blender progress bar considers the font widths when
519         # calculating the progress bar width.
520         self.swirl_chars = ["\\", "/"]
521         self.swirl_count = -1
522
523     def show(self, progress, name):
524         ProgressIndicator.show(self, progress)
525
526         self.swirl_count = (self.swirl_count+1)%len(self.swirl_chars)
527         swirl_char = self.swirl_chars[self.swirl_count]
528
529         progress_text = "%s - %c %3d%%" % (name, swirl_char, progress)
530
531         # Finally draw  the Progress Bar
532         Window.WaitCursor(1) # Maybe we can move that call in the constructor?
533         Window.DrawProgressBar(progress/100.0, progress_text)
534
535         if progress == 100:
536             Window.DrawProgressBar(1, progress_text)
537             Window.WaitCursor(0)
538
539
540
541 # ---------------------------------------------------------------------
542 #
543 ## 2D Object representation class
544 #
545 # ---------------------------------------------------------------------
546
547 # TODO: a class to represent the needed properties of a 2D vector image
548 # For now just using a [N]Mesh structure.
549
550
551 # ---------------------------------------------------------------------
552 #
553 ## Vector Drawing Classes
554 #
555 # ---------------------------------------------------------------------
556
557 ## A generic Writer
558
559 class VectorWriter:
560     """
561     A class for printing output in a vectorial format.
562
563     Given a 2D representation of the 3D scene the class is responsible to
564     write it is a vector format.
565
566     Every subclasses of VectorWriter must have at last the following public
567     methods:
568         - open(self)
569         - close(self)
570         - printCanvas(self, scene,
571             doPrintPolygons=True, doPrintEdges=False, showHiddenEdges=False):
572     """
573     
574     def __init__(self, fileName):
575         """Set the output file name and other properties"""
576
577         self.outputFileName = fileName
578         self.file = None
579         
580         context = Scene.GetCurrent().getRenderingContext()
581         self.canvasSize = ( context.imageSizeX(), context.imageSizeY() )
582
583         self.startFrame = 1
584         self.endFrame = 1
585         self.animation = False
586
587
588     ##
589     # Public Methods
590     #
591     
592     def open(self, startFrame=1, endFrame=1):
593         if startFrame != endFrame:
594             self.startFrame = startFrame
595             self.endFrame = endFrame
596             self.animation = True
597
598         self.file = open(self.outputFileName, "w")
599         print "Outputting to: ", self.outputFileName
600
601         return
602
603     def close(self):
604         self.file.close()
605         return
606
607     def printCanvas(self, scene, doPrintPolygons=True, doPrintEdges=False,
608             showHiddenEdges=False):
609         """This is the interface for the needed printing routine.
610         """
611         return
612         
613
614 ## SVG Writer
615
616 class SVGVectorWriter(VectorWriter):
617     """A concrete class for writing SVG output.
618     """
619
620     def __init__(self, fileName):
621         """Simply call the parent Contructor.
622         """
623         VectorWriter.__init__(self, fileName)
624
625
626     ##
627     # Public Methods
628     #
629
630     def open(self, startFrame=1, endFrame=1):
631         """Do some initialization operations.
632         """
633         VectorWriter.open(self, startFrame, endFrame)
634         self._printHeader()
635
636     def close(self):
637         """Do some finalization operation.
638         """
639         self._printFooter()
640
641         # remember to call the close method of the parent
642         VectorWriter.close(self)
643
644         
645     def printCanvas(self, scene, doPrintPolygons=True, doPrintEdges=False,
646             showHiddenEdges=False):
647         """Convert the scene representation to SVG.
648         """
649
650         Objects = scene.getChildren()
651
652         context = scene.getRenderingContext()
653         framenumber = context.currentFrame()
654
655         if self.animation:
656             framestyle = "display:none"
657         else:
658             framestyle = "display:block"
659         
660         # Assign an id to this group so we can set properties on it using DOM
661         self.file.write("<g id=\"frame%d\" style=\"%s\">\n" %
662                 (framenumber, framestyle) )
663
664
665         for obj in Objects:
666
667             if(obj.getType() != 'Mesh'):
668                 continue
669
670             self.file.write("<g id=\"%s\">\n" % obj.getName())
671
672             mesh = obj.getData(mesh=1)
673
674             if doPrintPolygons:
675                 self._printPolygons(mesh)
676
677             if doPrintEdges:
678                 self._printEdges(mesh, showHiddenEdges)
679             
680             self.file.write("</g>\n")
681
682         self.file.write("</g>\n")
683
684     
685     ##  
686     # Private Methods
687     #
688     
689     def _calcCanvasCoord(self, v):
690         """Convert vertex in scene coordinates to canvas coordinates.
691         """
692
693         pt = Vector([0, 0, 0])
694         
695         mW = float(self.canvasSize[0])/2.0
696         mH = float(self.canvasSize[1])/2.0
697
698         # rescale to canvas size
699         pt[0] = v.co[0]*mW + mW
700         pt[1] = v.co[1]*mH + mH
701         pt[2] = v.co[2]
702          
703         # For now we want (0,0) in the top-left corner of the canvas.
704         # Mirror and translate along y
705         pt[1] *= -1
706         pt[1] += self.canvasSize[1]
707         
708         return pt
709
710     def _printHeader(self):
711         """Print SVG header."""
712
713         self.file.write("<?xml version=\"1.0\"?>\n")
714         self.file.write("<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.0//EN\"\n")
715         self.file.write("\t\"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd\">\n")
716         self.file.write("<svg version=\"1.0\"\n")
717         self.file.write("\txmlns=\"http://www.w3.org/2000/svg\"\n")
718         self.file.write("\twidth=\"%d\" height=\"%d\">\n\n" %
719                 self.canvasSize)
720
721         if self.animation:
722
723             self.file.write("""\n<script type="text/javascript"><![CDATA[
724             globalStartFrame=%d;
725             globalEndFrame=%d;
726
727             /* FIXME: Use 1000 as interval as lower values gives problems */
728             timerID = setInterval("NextFrame()", 1000);
729             globalFrameCounter=%d;
730
731             function NextFrame()
732             {
733               currentElement  = document.getElementById('frame'+globalFrameCounter)
734               previousElement = document.getElementById('frame'+(globalFrameCounter-1))
735
736               if (!currentElement)
737               {
738                 return;
739               }
740
741               if (globalFrameCounter > globalEndFrame)
742               {
743                 clearInterval(timerID)
744               }
745               else
746               {
747                 if(previousElement)
748                 {
749                     previousElement.style.display="none";
750                 }
751                 currentElement.style.display="block";
752                 globalFrameCounter++;
753               }
754             }
755             \n]]></script>\n
756             \n""" % (self.startFrame, self.endFrame, self.startFrame) )
757                 
758     def _printFooter(self):
759         """Print the SVG footer."""
760
761         self.file.write("\n</svg>\n")
762
763     def _printPolygons(self, mesh): 
764         """Print the selected (visible) polygons.
765         """
766
767         if len(mesh.faces) == 0:
768             return
769
770         self.file.write("<g>\n")
771
772         for face in mesh.faces:
773             if not face.sel:
774                continue
775
776             self.file.write("<path d=\"")
777
778             p = self._calcCanvasCoord(face.verts[0])
779             self.file.write("M %g,%g L " % (p[0], p[1]))
780
781             for v in face.verts[1:]:
782                 p = self._calcCanvasCoord(v)
783                 self.file.write("%g,%g " % (p[0], p[1]))
784             
785             # get rid of the last blank space, just cosmetics here.
786             self.file.seek(-1, 1) 
787             self.file.write(" z\"\n")
788             
789             # take as face color the first vertex color
790             if face.col:
791                 fcol = face.col[0]
792                 color = [fcol.r, fcol.g, fcol.b, fcol.a]
793             else:
794                 color = [255, 255, 255, 255]
795
796             # Convert the color to the #RRGGBB form
797             str_col = "#%02X%02X%02X" % (color[0], color[1], color[2])
798
799             # Handle transparent polygons
800             opacity_string = ""
801             if color[3] != 255:
802                 opacity = float(color[3])/255.0
803                 opacity_string = " fill-opacity: %g; stroke-opacity: %g; opacity: 1;" % (opacity, opacity)
804
805             self.file.write("\tstyle=\"fill:" + str_col + ";")
806             self.file.write(opacity_string)
807
808             # use the stroke property to alleviate the "adjacent edges" problem,
809             # we simulate polygon expansion using borders,
810             # see http://www.antigrain.com/svg/index.html for more info
811             stroke_width = 1.0
812
813             if config.polygons['EXPANSION_TRICK']:
814                 str_col = "#000000" # For debug
815                 self.file.write(" stroke:%s;\n" % str_col)
816                 self.file.write(" stroke-width:" + str(stroke_width) + ";\n")
817                 self.file.write(" stroke-linecap:round;stroke-linejoin:round")
818
819             self.file.write("\"/>\n")
820
821         self.file.write("</g>\n")
822
823     def _printEdges(self, mesh, showHiddenEdges=False):
824         """Print the wireframe using mesh edges.
825         """
826
827         stroke_width = config.edges['WIDTH']
828         stroke_col = config.edges['COLOR']
829         
830         self.file.write("<g>\n")
831
832         for e in mesh.edges:
833             
834             hidden_stroke_style = ""
835             
836             if e.sel == 0:
837                 if showHiddenEdges == False:
838                     continue
839                 else:
840                     hidden_stroke_style = ";\n stroke-dasharray:3, 3"
841
842             p1 = self._calcCanvasCoord(e.v1)
843             p2 = self._calcCanvasCoord(e.v2)
844             
845             self.file.write("<line x1=\"%g\" y1=\"%g\" x2=\"%g\" y2=\"%g\"\n"
846                     % ( p1[0], p1[1], p2[0], p2[1] ) )
847             self.file.write(" style=\"stroke:rgb("+str(stroke_col[0])+","+str(stroke_col[1])+","+str(stroke_col[2])+");")
848             self.file.write(" stroke-width:"+str(stroke_width)+";\n")
849             self.file.write(" stroke-linecap:round;stroke-linejoin:round")
850             self.file.write(hidden_stroke_style)
851             self.file.write("\"/>\n")
852
853         self.file.write("</g>\n")
854
855
856 # ---------------------------------------------------------------------
857 #
858 ## Rendering Classes
859 #
860 # ---------------------------------------------------------------------
861
862 # A dictionary to collect different shading style methods
863 shadingStyles = dict()
864 shadingStyles['FLAT'] = None
865 shadingStyles['TOON'] = None
866
867 # A dictionary to collect different edge style methods
868 edgeStyles = dict()
869 edgeStyles['MESH'] = MeshUtils.isMeshEdge
870 edgeStyles['SILHOUETTE'] = MeshUtils.isSilhouetteEdge
871
872 # A dictionary to collect the supported output formats
873 outputWriters = dict()
874 outputWriters['SVG'] = SVGVectorWriter
875
876
877 class Renderer:
878     """Render a scene viewed from the active camera.
879     
880     This class is responsible of the rendering process, transformation and
881     projection of the objects in the scene are invoked by the renderer.
882
883     The rendering is done using the active camera for the current scene.
884     """
885
886     def __init__(self):
887         """Make the rendering process only for the current scene by default.
888
889         We will work on a copy of the scene, to be sure that the current scene do
890         not get modified in any way.
891         """
892
893         # Render the current Scene, this should be a READ-ONLY property
894         self._SCENE = Scene.GetCurrent()
895         
896         # Use the aspect ratio of the scene rendering context
897         context = self._SCENE.getRenderingContext()
898
899         aspect_ratio = float(context.imageSizeX())/float(context.imageSizeY())
900         self.canvasRatio = (float(context.aspectRatioX())*aspect_ratio,
901                             float(context.aspectRatioY())
902                             )
903
904         # Render from the currently active camera 
905         self.cameraObj = self._SCENE.getCurrentCamera()
906
907         # Get a projector for this camera.
908         # NOTE: the projector wants object in world coordinates,
909         # so we should remember to apply modelview transformations
910         # _before_ we do projection transformations.
911         self.proj = Projector(self.cameraObj, self.canvasRatio)
912
913         # Get the list of lighting sources
914         obj_lst = self._SCENE.getChildren()
915         self.lights = [ o for o in obj_lst if o.getType() == 'Lamp']
916
917         # When there are no lights we use a default lighting source
918         # that have the same position of the camera
919         if len(self.lights) == 0:
920             l = Lamp.New('Lamp')
921             lobj = Object.New('Lamp')
922             lobj.loc = self.cameraObj.loc
923             lobj.link(l) 
924             self.lights.append(lobj)
925
926
927     ##
928     # Public Methods
929     #
930
931     def doRendering(self, outputWriter, animation=False):
932         """Render picture or animation and write it out.
933         
934         The parameters are:
935             - a Vector writer object that will be used to output the result.
936             - a flag to tell if we want to render an animation or only the
937               current frame.
938         """
939         
940         context = self._SCENE.getRenderingContext()
941         origCurrentFrame = context.currentFrame()
942
943         # Handle the animation case
944         if not animation:
945             startFrame = origCurrentFrame
946             endFrame = startFrame
947             outputWriter.open()
948         else:
949             startFrame = context.startFrame()
950             endFrame = context.endFrame()
951             outputWriter.open(startFrame, endFrame)
952         
953         # Do the rendering process frame by frame
954         print "Start Rendering of %d frames" % (endFrame-startFrame)
955         for f in xrange(startFrame, endFrame+1):
956             print "\n\nFrame: %d" % f
957             context.currentFrame(f)
958
959             # Use some temporary workspace, a full copy of the scene
960             inputScene = self._SCENE.copy(2)
961             # And Set our camera accordingly
962             self.cameraObj = inputScene.getCurrentCamera()
963
964             try:
965                 renderedScene = self.doRenderScene(inputScene)
966             except :
967                 print "There was an error! Aborting."
968                 import traceback
969                 print traceback.print_exc()
970
971                 self._SCENE.makeCurrent()
972                 Scene.unlink(inputScene)
973                 del inputScene
974                 return
975
976             outputWriter.printCanvas(renderedScene,
977                     doPrintPolygons = config.polygons['SHOW'],
978                     doPrintEdges    = config.edges['SHOW'],
979                     showHiddenEdges = config.edges['SHOW_HIDDEN'])
980             
981             # delete the rendered scene
982             self._SCENE.makeCurrent()
983             Scene.unlink(renderedScene)
984             del renderedScene
985
986         outputWriter.close()
987         print "Done!"
988         context.currentFrame(origCurrentFrame)
989
990
991     def doRenderScene(self, workScene):
992         """Control the rendering process.
993         
994         Here we control the entire rendering process invoking the operation
995         needed to transform and project the 3D scene in two dimensions.
996         """
997         
998         # global processing of the scene
999
1000         self._doSceneClipping(workScene)
1001
1002         self._doConvertGeometricObjsToMesh(workScene)
1003
1004         if config.output['JOIN_OBJECTS']:
1005             self._joinMeshObjectsInScene(workScene)
1006
1007         self._doSceneDepthSorting(workScene)
1008         
1009         # Per object activities
1010
1011         Objects = workScene.getChildren()
1012         print "Total Objects: %d" % len(Objects)
1013         for i,obj in enumerate(Objects):
1014             print "\n\n-------"
1015             print "Rendering Object: %d" % i
1016
1017             if obj.getType() != 'Mesh':
1018                 print "Only Mesh supported! - Skipping type:", obj.getType()
1019                 continue
1020
1021             print "Rendering: ", obj.getName()
1022
1023             mesh = obj.getData(mesh=1)
1024
1025             self._doModelingTransformation(mesh, obj.matrix)
1026
1027             self._doBackFaceCulling(mesh)
1028
1029             self._doLighting(mesh)
1030
1031             # Do "projection" now so we perform further processing
1032             # in Normalized View Coordinates
1033             self._doProjection(mesh, self.proj)
1034
1035             self._doViewFrustumClipping(mesh)
1036
1037             self._doHiddenSurfaceRemoval(mesh)
1038
1039             self._doEdgesStyle(mesh, edgeStyles[config.edges['STYLE']])
1040
1041             
1042             # Update the object data, important! :)
1043             mesh.update()
1044
1045         return workScene
1046
1047
1048     ##
1049     # Private Methods
1050     #
1051
1052     # Utility methods
1053
1054     def _getObjPosition(self, obj):
1055         """Return the obj position in World coordinates.
1056         """
1057         return obj.matrix.translationPart()
1058
1059     def _cameraViewVector(self):
1060         """Get the View Direction form the camera matrix.
1061         """
1062         return Vector(self.cameraObj.matrix[2]).resize3D()
1063
1064
1065     # Faces methods
1066
1067     def _isFaceVisible(self, face):
1068         """Determine if a face of an object is visible from the current camera.
1069         
1070         The view vector is calculated from the camera location and one of the
1071         vertices of the face (expressed in World coordinates, after applying
1072         modelview transformations).
1073
1074         After those transformations we determine if a face is visible by
1075         computing the angle between the face normal and the view vector, this
1076         angle has to be between -90 and 90 degrees for the face to be visible.
1077         This corresponds somehow to the dot product between the two, if it
1078         results > 0 then the face is visible.
1079
1080         There is no need to normalize those vectors since we are only interested in
1081         the sign of the cross product and not in the product value.
1082
1083         NOTE: here we assume the face vertices are in WorldCoordinates, so
1084         please transform the object _before_ doing the test.
1085         """
1086
1087         normal = Vector(face.no)
1088         camPos = self._getObjPosition(self.cameraObj)
1089         view_vect = None
1090
1091         # View Vector in orthographics projections is the view Direction of
1092         # the camera
1093         if self.cameraObj.data.getType() == 1:
1094             view_vect = self._cameraViewVector()
1095
1096         # View vector in perspective projections can be considered as
1097         # the difference between the camera position and one point of
1098         # the face, we choose the farthest point from the camera.
1099         if self.cameraObj.data.getType() == 0:
1100             vv = max( [ ((camPos - Vector(v.co)).length, (camPos - Vector(v.co))) for v in face] )
1101             view_vect = vv[1]
1102
1103
1104         # if d > 0 the face is visible from the camera
1105         d = view_vect * normal
1106         
1107         if d > 0:
1108             return True
1109         else:
1110             return False
1111
1112
1113     # Scene methods
1114
1115     def _doSceneClipping(self, scene):
1116         """Clip whole objects against the View Frustum.
1117
1118         For now clip away only objects according to their center position.
1119         """
1120
1121         cpos = self._getObjPosition(self.cameraObj)
1122         view_vect = self._cameraViewVector()
1123
1124         near = self.cameraObj.data.clipStart
1125         far  = self.cameraObj.data.clipEnd
1126
1127         aspect = float(self.canvasRatio[0])/float(self.canvasRatio[1])
1128         fovy = atan(0.5/aspect/(self.cameraObj.data.lens/32))
1129         fovy = fovy * 360.0/pi
1130
1131         Objects = scene.getChildren()
1132         for o in Objects:
1133             if o.getType() != 'Mesh': continue;
1134
1135             obj_vect = Vector(cpos) - self._getObjPosition(o)
1136
1137             d = obj_vect*view_vect
1138             theta = AngleBetweenVecs(obj_vect, view_vect)
1139             
1140             # if the object is outside the view frustum, clip it away
1141             if (d < near) or (d > far) or (theta > fovy):
1142                 scene.unlink(o)
1143
1144     def _doConvertGeometricObjsToMesh(self, scene):
1145         """Convert all "geometric" objects to mesh ones.
1146         """
1147         #geometricObjTypes = ['Mesh', 'Surf', 'Curve', 'Text']
1148         geometricObjTypes = ['Mesh', 'Surf', 'Curve']
1149
1150         Objects = scene.getChildren()
1151         objList = [ o for o in Objects if o.getType() in geometricObjTypes ]
1152         for obj in objList:
1153             old_obj = obj
1154             obj = self._convertToRawMeshObj(obj)
1155             scene.link(obj)
1156             scene.unlink(old_obj)
1157
1158
1159             # XXX Workaround for Text and Curve which have some normals
1160             # inverted when they are converted to Mesh, REMOVE that when
1161             # blender will fix that!!
1162             if old_obj.getType() in ['Curve', 'Text']:
1163                 me = obj.getData(mesh=1)
1164                 for f in me.faces: f.sel = 1;
1165                 for v in me.verts: v.sel = 1;
1166                 me.remDoubles(0)
1167                 me.triangleToQuad()
1168                 me.recalcNormals()
1169                 me.update()
1170
1171
1172     def _doSceneDepthSorting(self, scene):
1173         """Sort objects in the scene.
1174
1175         The object sorting is done accordingly to the object centers.
1176         """
1177
1178         c = self._getObjPosition(self.cameraObj)
1179
1180         by_center_pos = (lambda o1, o2:
1181                 (o1.getType() == 'Mesh' and o2.getType() == 'Mesh') and
1182                 cmp((self._getObjPosition(o1) - Vector(c)).length,
1183                     (self._getObjPosition(o2) - Vector(c)).length)
1184             )
1185
1186         # TODO: implement sorting by bounding box, if obj1.bb is inside obj2.bb,
1187         # then ob1 goes farther than obj2, useful when obj2 has holes
1188         by_bbox = None
1189         
1190         Objects = scene.getChildren()
1191         Objects.sort(by_center_pos)
1192         
1193         # update the scene
1194         for o in Objects:
1195             scene.unlink(o)
1196             scene.link(o)
1197
1198     def _joinMeshObjectsInScene(self, scene):
1199         """Merge all the Mesh Objects in a scene into a single Mesh Object.
1200         """
1201
1202         oList = [o for o in scene.getChildren() if o.getType()=='Mesh']
1203
1204         # FIXME: Object.join() do not work if the list contains 1 object
1205         if len(oList) == 1:
1206             return
1207
1208         mesh = Mesh.New('BigOne')
1209         bigObj = Object.New('Mesh', 'BigOne')
1210         bigObj.link(mesh)
1211
1212         scene.link(bigObj)
1213
1214         try:
1215             bigObj.join(oList)
1216         except RuntimeError:
1217             print "\nWarning! - Can't Join Objects\n"
1218             scene.unlink(bigObj)
1219             return
1220         except TypeError:
1221             print "Objects Type error?"
1222         
1223         for o in oList:
1224             scene.unlink(o)
1225
1226         scene.update()
1227
1228  
1229     # Per object/mesh methods
1230
1231     def _convertToRawMeshObj(self, object):
1232         """Convert geometry based object to a mesh object.
1233         """
1234         me = Mesh.New('RawMesh_'+object.name)
1235         me.getFromObject(object.name)
1236
1237         newObject = Object.New('Mesh', 'RawMesh_'+object.name)
1238         newObject.link(me)
1239
1240         # If the object has no materials set a default material
1241         if not me.materials:
1242             me.materials = [Material.New()]
1243             #for f in me.faces: f.mat = 0
1244
1245         newObject.setMatrix(object.getMatrix())
1246
1247         return newObject
1248
1249     def _doModelingTransformation(self, mesh, matrix):
1250         """Transform object coordinates to world coordinates.
1251
1252         This step is done simply applying to the object its tranformation
1253         matrix and recalculating its normals.
1254         """
1255         # XXX FIXME: blender do not transform normals in the right way when
1256         # there are negative scale values
1257         if matrix[0][0] < 0 or matrix[1][1] < 0 or matrix[2][2] < 0:
1258             print "WARNING: Negative scales, expect incorrect results!"
1259
1260         mesh.transform(matrix, True)
1261
1262     def _doBackFaceCulling(self, mesh):
1263         """Simple Backface Culling routine.
1264         
1265         At this level we simply do a visibility test face by face and then
1266         select the vertices belonging to visible faces.
1267         """
1268         
1269         # Select all vertices, so edges can be displayed even if there are no
1270         # faces
1271         for v in mesh.verts:
1272             v.sel = 1
1273         
1274         Mesh.Mode(Mesh.SelectModes['FACE'])
1275         # Loop on faces
1276         for f in mesh.faces:
1277             f.sel = 0
1278             if self._isFaceVisible(f):
1279                 f.sel = 1
1280
1281     def _doLighting(self, mesh):
1282         """Apply an Illumination and shading model to the object.
1283
1284         The model used is the Phong one, it may be inefficient,
1285         but I'm just learning about rendering and starting from Phong seemed
1286         the most natural way.
1287         """
1288
1289         # If the mesh has vertex colors already, use them,
1290         # otherwise turn them on and do some calculations
1291         if mesh.vertexColors:
1292             return
1293         mesh.vertexColors = 1
1294
1295         materials = mesh.materials
1296
1297         camPos = self._getObjPosition(self.cameraObj)
1298
1299         # We do per-face color calculation (FLAT Shading), we can easily turn
1300         # to a per-vertex calculation if we want to implement some shading
1301         # technique. For an example see:
1302         # http://www.miralab.unige.ch/papers/368.pdf
1303         for f in mesh.faces:
1304             if not f.sel:
1305                 continue
1306
1307             mat = None
1308             if materials:
1309                 mat = materials[f.mat]
1310
1311             # A new default material
1312             if mat == None:
1313                 mat = Material.New('defMat')
1314
1315             # Check if it is a shadeless material
1316             elif mat.getMode() & Material.Modes['SHADELESS']:
1317                 I = mat.getRGBCol()
1318                 # Convert to a value between 0 and 255
1319                 tmp_col = [ int(c * 255.0) for c in I]
1320
1321                 for c in f.col:
1322                     c.r = tmp_col[0]
1323                     c.g = tmp_col[1]
1324                     c.b = tmp_col[2]
1325                     #c.a = tmp_col[3]
1326
1327                 continue
1328
1329
1330             # do vertex color calculation
1331
1332             TotDiffSpec = Vector([0.0, 0.0, 0.0])
1333
1334             for l in self.lights:
1335                 light_obj = l
1336                 light_pos = self._getObjPosition(l)
1337                 light = light_obj.data
1338             
1339                 L = Vector(light_pos).normalize()
1340
1341                 V = (Vector(camPos) - Vector(f.cent)).normalize()
1342
1343                 N = Vector(f.no).normalize()
1344
1345                 if config.polygons['SHADING'] == 'TOON':
1346                     NL = ShadingUtils.toonShading(N*L)
1347                 else:
1348                     NL = (N*L)
1349
1350                 # Should we use NL instead of (N*L) here?
1351                 R = 2 * (N*L) * N - L
1352
1353                 Ip = light.getEnergy()
1354
1355                 # Diffuse co-efficient
1356                 kd = mat.getRef() * Vector(mat.getRGBCol())
1357                 for i in [0, 1, 2]:
1358                     kd[i] *= light.col[i]
1359
1360                 Idiff = Ip * kd * max(0, NL)
1361
1362
1363                 # Specular component
1364                 ks = mat.getSpec() * Vector(mat.getSpecCol())
1365                 ns = mat.getHardness()
1366                 Ispec = Ip * ks * pow(max(0, (V*R)), ns)
1367
1368                 TotDiffSpec += (Idiff+Ispec)
1369
1370
1371             # Ambient component
1372             Iamb = Vector(Blender.World.Get()[0].getAmb())
1373             ka = mat.getAmb()
1374
1375             # Emissive component (convert to a triplet)
1376             ki = Vector([mat.getEmit()]*3)
1377
1378             #I = ki + Iamb + (Idiff + Ispec)
1379             I = ki + (ka * Iamb) + TotDiffSpec
1380
1381
1382             # Set Alpha component
1383             I = list(I)
1384             I.append(mat.getAlpha())
1385
1386             # Clamp I values between 0 and 1
1387             I = [ min(c, 1) for c in I]
1388             I = [ max(0, c) for c in I]
1389
1390             # Convert to a value between 0 and 255
1391             tmp_col = [ int(c * 255.0) for c in I]
1392
1393             for c in f.col:
1394                 c.r = tmp_col[0]
1395                 c.g = tmp_col[1]
1396                 c.b = tmp_col[2]
1397                 c.a = tmp_col[3]
1398
1399     def _doProjection(self, mesh, projector):
1400         """Apply Viewing and Projection tranformations.
1401         """
1402
1403         for v in mesh.verts:
1404             p = projector.doProjection(v.co[:])
1405             v.co[0] = p[0]
1406             v.co[1] = p[1]
1407             v.co[2] = p[2]
1408
1409         #mesh.recalcNormals()
1410         #mesh.update()
1411
1412         # We could reeset Camera matrix, since now
1413         # we are in Normalized Viewing Coordinates,
1414         # but doung that would affect World Coordinate
1415         # processing for other objects
1416
1417         #self.cameraObj.data.type = 1
1418         #self.cameraObj.data.scale = 2.0
1419         #m = Matrix().identity()
1420         #self.cameraObj.setMatrix(m)
1421
1422     def _doViewFrustumClipping(self, mesh):
1423         """Clip faces against the View Frustum.
1424         """
1425
1426     # HSR routines
1427     def __simpleDepthSort(self, mesh):
1428         """Sort faces by the furthest vertex.
1429
1430         This simple mesthod is known also as the painter algorithm, and it
1431         solves HSR correctly only for convex meshes.
1432         """
1433
1434         global progress
1435         # The sorting requires circa n*log(n) steps
1436         n = len(mesh.faces)
1437         progress.setActivity("HSR: Painter", n*log(n))
1438         
1439
1440         by_furthest_z = (lambda f1, f2: progress.update() and
1441                 cmp(max([v.co[2] for v in f1]), max([v.co[2] for v in f2]))
1442                 )
1443
1444         # FIXME: using NMesh to sort faces. We should avoid that!
1445         nmesh = NMesh.GetRaw(mesh.name)
1446
1447         # remember that _higher_ z values mean further points
1448         nmesh.faces.sort(by_furthest_z)
1449         nmesh.faces.reverse()
1450
1451         nmesh.update()
1452
1453     def __topologicalDepthSort(self, mesh):
1454         """Occlusion based on topological occlusion.
1455         
1456         Build the occlusion graph of the mesh,
1457         and then do topological sort on that graph
1458         """
1459         return
1460
1461     def __newellDepthSort(self, mesh):
1462         """Newell's depth sorting.
1463
1464         """
1465         by_furthest_z = (lambda f1, f2:
1466                 cmp(max([v.co[2] for v in f1]), max([v.co[2] for v in f2]))
1467                 )
1468
1469
1470         def isOnSegment(v1, v2, p):
1471
1472             # when p is at extreme points
1473             if p == v1 or p == v2:
1474                 return False
1475
1476
1477             EPS = 10e-7
1478
1479             l1 = (v1-p).length
1480             l2 = (v2-p).length
1481             l = (v1-v2).length
1482
1483             print "l: ", l, " l1: ", l1, " l2: ", l2, "diff: %.9f" % (l - (l1+l2) )
1484
1485             if abs(l - (l1+l2)) < EPS:
1486                 return True
1487             else:
1488                 return False
1489
1490
1491
1492         def Distance(point, face):
1493             """ Calculate the distance between a point and a face.
1494
1495             An alternative but more expensive method can be:
1496
1497                 ip = Intersect(Vector(face[0]), Vector(face[1]), Vector(face[2]),
1498                         Vector(face.no), Vector(point), 0)
1499
1500                 d = Vector(ip - point).length
1501             """
1502
1503             plNormal = Vector(face.no)
1504             plVert0 = Vector(face[0])
1505
1506             #d = abs( (point * plNormal ) - (plVert0 * plNormal) )
1507             d = (point * plNormal ) - (plVert0 * plNormal)
1508             debug("d: %.10f - sel: %d, %s\n" % (d, face.sel, str(point)) )
1509
1510             return d
1511
1512
1513         # FIXME: using NMesh to sort faces. We should avoid that!
1514         nmesh = NMesh.GetRaw(mesh.name)
1515
1516         # remember that _higher_ z values mean further points
1517         nmesh.faces.sort(by_furthest_z)
1518         nmesh.faces.reverse()
1519
1520         
1521         # Begin depth sort tests
1522
1523         # use the smooth flag to set marked faces
1524         for f in nmesh.faces:
1525             f.smooth = 0
1526
1527         facelist = nmesh.faces[:]
1528         maplist = []
1529
1530         #EPS = 10e-7
1531         EPS = 0
1532
1533         global progress
1534         progress.setActivity("HSR: Newell", len(facelist))
1535         progress.setQuiet(True)
1536
1537         #while len(facelist)-1:
1538         while len(facelist):
1539             P = facelist[0]
1540
1541             pSign = 1
1542             if P.sel == 0:
1543                 pSign = -1
1544
1545             #while False:
1546             for Q in facelist[1:]:
1547
1548                 debug("P.smooth: " + str(P.smooth) + "\n")
1549                 debug("Q.smooth: " + str(Q.smooth) + "\n")
1550                 debug("\n")
1551
1552                 qSign = 1
1553                 if Q.sel == 0:
1554                     qSign = -1
1555  
1556                 # We need to test only those Qs whose furthest vertex
1557                 # is closer to the observer than the closest vertex of P.
1558
1559                 zP = [v.co[2] for v in P.v]
1560                 zQ = [v.co[2] for v in Q.v]
1561                 ZOverlap = min(zP) < max(zQ)
1562
1563                 if not ZOverlap:
1564                     debug("\nTest 0\n")
1565                     debug("NOT Z OVERLAP!\n")
1566                     if not Q.smooth:
1567                         # We can safely print P
1568                         break
1569                     else:
1570                         continue
1571                 
1572                 # Test 1: X extent overlapping
1573                 xP = [v.co[0] for v in P.v]
1574                 xQ = [v.co[0] for v in Q.v]
1575                 notXOverlap = (max(xP) < min(xQ)) or (max(xQ) < min(xP))
1576
1577                 if notXOverlap:
1578                     debug("\nTest 1\n")
1579                     debug("NOT X OVERLAP!\n")
1580                     continue
1581
1582                 # Test 2: Y extent Overlapping
1583                 yP = [v.co[1] for v in P.v]
1584                 yQ = [v.co[1] for v in Q.v]
1585                 notYOverlap = (max(yP) < min(yQ)) or (max(yQ) < min(yP))
1586
1587                 if notYOverlap:
1588                     debug("\nTest 2\n")
1589                     debug("NOT Y OVERLAP!\n")
1590                     continue
1591                 
1592
1593                 # Test 3: P vertices are all behind the plane of Q
1594                 n = 0
1595                 for Pi in P:
1596                     print P.col[0]
1597                     d = qSign * Distance(Vector(Pi), Q)
1598                     if d > EPS:
1599                         n += 1
1600                 pVerticesBehindPlaneQ = (n == len(P))
1601
1602                 if pVerticesBehindPlaneQ:
1603                     debug("\nTest 3\n")
1604                     debug("P BEHIND Q!\n")
1605                     continue
1606
1607
1608                 # Test 4: Q vertices in front of the plane of P
1609                 n = 0
1610                 for Qi in Q:
1611                     print Q.col[0]
1612                     d = pSign * Distance(Vector(Qi), P)
1613                     if d <= EPS:
1614                         n += 1
1615                 qVerticesInFrontPlaneP = (n == len(Q))
1616
1617                 if qVerticesInFrontPlaneP:
1618                     debug("\nTest 4\n")
1619                     debug("Q IN FRONT OF P!\n")
1620                     continue
1621
1622                 # Test 5: Line Intersections... TODO
1623                 # Check if polygons effectively overlap each other, not only
1624                 # boundig boxes as dome before.
1625                 # Since we We are working in normalized projection coordinates
1626                 # we kust check if polygons intersect.
1627
1628                 def projectionsOverlap(P, Q):
1629
1630                     for i in range(0, len(P.v)):
1631
1632                         v1 = Vector(P.v[i-1])
1633                         v1[2] = 0
1634                         v2 = Vector(P.v[i])
1635                         v2[2] = 0
1636
1637                         for j in range(0, len(Q.v)):
1638                             v3 = Vector(Q.v[j-1])
1639                             v3[2] = 0
1640                             v4 = Vector(Q.v[j])
1641                             v4[2] = 0
1642                             
1643                             ret = LineIntersect(v1, v2, v3, v4)
1644                             # if line v1-v2 and v3-v4 intersect both return
1645                             # values are the same.
1646                             if ret and ret[0] == ret[1]  and isOnSegment(v1, v2,
1647                                     ret[0]) and isOnSegment(v3, v4, ret[1]):
1648                                 debug("Projections OVERLAP!!\n")
1649                                 debug("line1:"+
1650                                         " M "+ str(v1[0])+','+str(v1[1]) + ' L ' + str(v2[0])+','+str(v2[1]) + '\n' +
1651                                         " M "+ str(v3[0])+','+str(v3[1]) + ' L ' + str(v4[0])+','+str(v4[1]) + '\n' +
1652                                         "\n")
1653                                 debug("return: "+ str(ret)+"\n")
1654                                 return True
1655
1656                     return False
1657
1658                 if not projectionsOverlap(P, Q):
1659                     debug("\nTest 5\n")
1660                     debug("Projections do not overlap!\n")
1661                     continue
1662
1663
1664                 # We do not know if P obscures Q.
1665                 if Q.smooth == 1:
1666                     # Split P or Q, TODO
1667                     debug("Cycle detected!\n")
1668                     debug("Split here!!\n")
1669                     continue
1670
1671
1672                 # The question now is: Does Q obscure P?
1673
1674                 # Test 3bis: Q vertices are all behind the plane of P
1675                 n = 0
1676                 for Qi in Q:
1677                     print Q.col[0]
1678                     d = pSign * Distance(Vector(Qi), P)
1679                     if d > EPS:
1680                         n += 1
1681                 qVerticesBehindPlaneP = (n == len(Q))
1682
1683                 if qVerticesBehindPlaneP:
1684                     debug("\nTest 3bis\n")
1685                     debug("Q BEHIND P!\n")
1686
1687
1688                 # Test 4bis: P vertices in front of the plane of Q
1689                 n = 0
1690                 for Pi in P:
1691                     print P.col[0]
1692                     d = qSign * Distance(Vector(Pi), Q)
1693                     if d <= EPS:
1694                         n += 1
1695                 pVerticesInFrontPlaneQ = (n == len(P))
1696
1697                 if pVerticesInFrontPlaneQ:
1698                     debug("\nTest 4bis\n")
1699                     debug("P IN FRONT OF Q!\n")
1700
1701
1702                 import intersection
1703
1704                 if not qVerticesBehindPlaneP and not pVerticesInFrontPlaneQ:
1705                     debug("\nSimple Intersection?\n")
1706                     # Split P or Q, TODO
1707                     print "Test 3bis or 4bis failed"
1708                     print "Split here!!2\n"
1709
1710                     """newfaces = intersection.splitOn(P, Q, 0)
1711                     print newfaces
1712                     facelist.remove(Q)
1713                     for nf in newfaces:
1714                         if nf:
1715                             nf.col = Q.col
1716                             facelist.append(nf)
1717                     """
1718
1719                     break
1720
1721                 # We do not know
1722                 if Q.smooth:
1723                     # split P or Q
1724                     print "Split here!!\n"
1725                     """
1726                     newfaces = intersection.splitOn(P, Q, 0)
1727                     facelist.remove(Q)
1728                     for nf in newfaces:
1729                         if nf:
1730                             nf.col = Q.col
1731                             facelist.append(nf)
1732
1733                     """ 
1734                     break
1735
1736                 Q.smooth = 1
1737                 facelist.remove(Q)
1738                 facelist.insert(0, Q)
1739            
1740             # Write P!                     
1741             P = facelist[0]
1742             facelist.remove(P)
1743             maplist.append(P)
1744
1745             progress .update()
1746
1747         
1748         nmesh.faces = maplist
1749
1750         for f in nmesh.faces:
1751             f.sel = 1
1752         nmesh.update()
1753
1754     def _doHiddenSurfaceRemoval(self, mesh):
1755         """Do HSR for the given mesh.
1756         """
1757         if len(mesh.faces) == 0:
1758             return
1759
1760         if config.polygons['HSR'] == 'PAINTER':
1761             print "\nUsing the Painter algorithm for HSR."
1762             self.__simpleDepthSort(mesh)
1763
1764         elif config.polygons['HSR'] == 'NEWELL':
1765             print "\nUsing the Newell's algorithm for HSR."
1766             self.__newellDepthSort(mesh)
1767
1768
1769     def _doEdgesStyle(self, mesh, edgestyleSelect):
1770         """Process Mesh Edges accroding to a given selection style.
1771
1772         Examples of algorithms:
1773
1774         Contours:
1775             given an edge if its adjacent faces have the same normal (that is
1776             they are complanar), than deselect it.
1777
1778         Silhouettes:
1779             given an edge if one its adjacent faces is frontfacing and the
1780             other is backfacing, than select it, else deselect.
1781         """
1782
1783         Mesh.Mode(Mesh.SelectModes['EDGE'])
1784
1785         edge_cache = MeshUtils.buildEdgeFaceUsersCache(mesh)
1786
1787         for i,edge_faces in enumerate(edge_cache):
1788             mesh.edges[i].sel = 0
1789             if edgestyleSelect(edge_faces):
1790                 mesh.edges[i].sel = 1
1791
1792         """
1793         for e in mesh.edges:
1794
1795             e.sel = 0
1796             if edgestyleSelect(e, mesh):
1797                 e.sel = 1
1798         """
1799                 
1800
1801
1802 # ---------------------------------------------------------------------
1803 #
1804 ## GUI Class and Main Program
1805 #
1806 # ---------------------------------------------------------------------
1807
1808
1809 from Blender import BGL, Draw
1810 from Blender.BGL import *
1811
1812 class GUI:
1813     
1814     def _init():
1815
1816         # Output Format menu 
1817         output_format = config.output['FORMAT']
1818         default_value = outputWriters.keys().index(output_format)+1
1819         GUI.outFormatMenu = Draw.Create(default_value)
1820         GUI.evtOutFormatMenu = 0
1821
1822         # Animation toggle button
1823         GUI.animToggle = Draw.Create(config.output['ANIMATION'])
1824         GUI.evtAnimToggle = 1
1825
1826         # Join Objects toggle button
1827         GUI.joinObjsToggle = Draw.Create(config.output['JOIN_OBJECTS'])
1828         GUI.evtJoinObjsToggle = 2
1829
1830         # Render filled polygons
1831         GUI.polygonsToggle = Draw.Create(config.polygons['SHOW'])
1832
1833         # Shading Style menu 
1834         shading_style = config.polygons['SHADING']
1835         default_value = shadingStyles.keys().index(shading_style)+1
1836         GUI.shadingStyleMenu = Draw.Create(default_value)
1837         GUI.evtShadingStyleMenu = 21
1838
1839         GUI.evtPolygonsToggle = 3
1840         # We hide the config.polygons['EXPANSION_TRICK'], for now
1841
1842         # Render polygon edges
1843         GUI.showEdgesToggle = Draw.Create(config.edges['SHOW'])
1844         GUI.evtShowEdgesToggle = 4
1845
1846         # Render hidden edges
1847         GUI.showHiddenEdgesToggle = Draw.Create(config.edges['SHOW_HIDDEN'])
1848         GUI.evtShowHiddenEdgesToggle = 5
1849
1850         # Edge Style menu 
1851         edge_style = config.edges['STYLE']
1852         default_value = edgeStyles.keys().index(edge_style)+1
1853         GUI.edgeStyleMenu = Draw.Create(default_value)
1854         GUI.evtEdgeStyleMenu = 6
1855
1856         # Edge Width slider
1857         GUI.edgeWidthSlider = Draw.Create(config.edges['WIDTH'])
1858         GUI.evtEdgeWidthSlider = 7
1859
1860         # Edge Color Picker
1861         c = config.edges['COLOR']
1862         GUI.edgeColorPicker = Draw.Create(c[0]/255.0, c[1]/255.0, c[2]/255.0)
1863         GUI.evtEdgeColorPicker = 71
1864
1865         # Render Button
1866         GUI.evtRenderButton = 8
1867
1868         # Exit Button
1869         GUI.evtExitButton = 9
1870
1871     def draw():
1872
1873         # initialize static members
1874         GUI._init()
1875
1876         glClear(GL_COLOR_BUFFER_BIT)
1877         glColor3f(0.0, 0.0, 0.0)
1878         glRasterPos2i(10, 350)
1879         Draw.Text("VRM: Vector Rendering Method script. Version %s." %
1880                 __version__)
1881         glRasterPos2i(10, 335)
1882         Draw.Text("Press Q or ESC to quit.")
1883
1884         # Build the output format menu
1885         glRasterPos2i(10, 310)
1886         Draw.Text("Select the output Format:")
1887         outMenuStruct = "Output Format %t"
1888         for t in outputWriters.keys():
1889            outMenuStruct = outMenuStruct + "|%s" % t
1890         GUI.outFormatMenu = Draw.Menu(outMenuStruct, GUI.evtOutFormatMenu,
1891                 10, 285, 160, 18, GUI.outFormatMenu.val, "Choose the Output Format")
1892
1893         # Animation toggle
1894         GUI.animToggle = Draw.Toggle("Animation", GUI.evtAnimToggle,
1895                 10, 260, 160, 18, GUI.animToggle.val,
1896                 "Toggle rendering of animations")
1897
1898         # Join Objects toggle
1899         GUI.joinObjsToggle = Draw.Toggle("Join objects", GUI.evtJoinObjsToggle,
1900                 10, 235, 160, 18, GUI.joinObjsToggle.val,
1901                 "Join objects in the rendered file")
1902
1903         # Render Button
1904         Draw.Button("Render", GUI.evtRenderButton, 10, 210-25, 75, 25+18,
1905                 "Start Rendering")
1906         Draw.Button("Exit", GUI.evtExitButton, 95, 210-25, 75, 25+18, "Exit!")
1907
1908         # Rendering Styles
1909         glRasterPos2i(200, 310)
1910         Draw.Text("Rendering Style:")
1911
1912         # Render Polygons
1913         GUI.polygonsToggle = Draw.Toggle("Filled Polygons", GUI.evtPolygonsToggle,
1914                 200, 285, 160, 18, GUI.polygonsToggle.val,
1915                 "Render filled polygons")
1916
1917         if GUI.polygonsToggle.val == 1:
1918
1919             # Polygon Shading Style
1920             shadingStyleMenuStruct = "Shading Style %t"
1921             for t in shadingStyles.keys():
1922                 shadingStyleMenuStruct = shadingStyleMenuStruct + "|%s" % t.lower()
1923             GUI.shadingStyleMenu = Draw.Menu(shadingStyleMenuStruct, GUI.evtShadingStyleMenu,
1924                     200, 260, 160, 18, GUI.shadingStyleMenu.val,
1925                     "Choose the shading style")
1926
1927
1928         # Render Edges
1929         GUI.showEdgesToggle = Draw.Toggle("Show Edges", GUI.evtShowEdgesToggle,
1930                 200, 235, 160, 18, GUI.showEdgesToggle.val,
1931                 "Render polygon edges")
1932
1933         if GUI.showEdgesToggle.val == 1:
1934             
1935             # Edge Style
1936             edgeStyleMenuStruct = "Edge Style %t"
1937             for t in edgeStyles.keys():
1938                 edgeStyleMenuStruct = edgeStyleMenuStruct + "|%s" % t.lower()
1939             GUI.edgeStyleMenu = Draw.Menu(edgeStyleMenuStruct, GUI.evtEdgeStyleMenu,
1940                     200, 210, 160, 18, GUI.edgeStyleMenu.val,
1941                     "Choose the edge style")
1942
1943             # Edge size
1944             GUI.edgeWidthSlider = Draw.Slider("Width: ", GUI.evtEdgeWidthSlider,
1945                     200, 185, 140, 18, GUI.edgeWidthSlider.val,
1946                     0.0, 10.0, 0, "Change Edge Width")
1947
1948             # Edge Color
1949             GUI.edgeColorPicker = Draw.ColorPicker(GUI.evtEdgeColorPicker,
1950                     342, 185, 18, 18, GUI.edgeColorPicker.val, "Choose Edge Color")
1951
1952             # Show Hidden Edges
1953             GUI.showHiddenEdgesToggle = Draw.Toggle("Show Hidden Edges",
1954                     GUI.evtShowHiddenEdgesToggle,
1955                     200, 160, 160, 18, GUI.showHiddenEdgesToggle.val,
1956                     "Render hidden edges as dashed lines")
1957
1958         glRasterPos2i(10, 160)
1959         Draw.Text("%s (c) 2006" % __author__)
1960
1961     def event(evt, val):
1962
1963         if evt == Draw.ESCKEY or evt == Draw.QKEY:
1964             Draw.Exit()
1965         else:
1966             return
1967
1968         Draw.Redraw(1)
1969
1970     def button_event(evt):
1971
1972         if evt == GUI.evtExitButton:
1973             Draw.Exit()
1974
1975         elif evt == GUI.evtOutFormatMenu:
1976             i = GUI.outFormatMenu.val - 1
1977             config.output['FORMAT']= outputWriters.keys()[i]
1978
1979         elif evt == GUI.evtAnimToggle:
1980             config.output['ANIMATION'] = bool(GUI.animToggle.val)
1981
1982         elif evt == GUI.evtJoinObjsToggle:
1983             config.output['JOIN_OBJECTS'] = bool(GUI.joinObjsToggle.val)
1984
1985         elif evt == GUI.evtPolygonsToggle:
1986             config.polygons['SHOW'] = bool(GUI.polygonsToggle.val)
1987
1988         elif evt == GUI.evtShadingStyleMenu:
1989             i = GUI.shadingStyleMenu.val - 1
1990             config.polygons['SHADING'] = shadingStyles.keys()[i]
1991
1992         elif evt == GUI.evtShowEdgesToggle:
1993             config.edges['SHOW'] = bool(GUI.showEdgesToggle.val)
1994
1995         elif evt == GUI.evtShowHiddenEdgesToggle:
1996             config.edges['SHOW_HIDDEN'] = bool(GUI.showHiddenEdgesToggle.val)
1997
1998         elif evt == GUI.evtEdgeStyleMenu:
1999             i = GUI.edgeStyleMenu.val - 1
2000             config.edges['STYLE'] = edgeStyles.keys()[i]
2001
2002         elif evt == GUI.evtEdgeWidthSlider:
2003             config.edges['WIDTH'] = float(GUI.edgeWidthSlider.val)
2004
2005         elif evt == GUI.evtEdgeColorPicker:
2006             config.edges['COLOR'] = [int(c*255.0) for c in GUI.edgeColorPicker.val]
2007
2008         elif evt == GUI.evtRenderButton:
2009             label = "Save %s" % config.output['FORMAT']
2010             # Show the File Selector
2011             global outputfile
2012             Blender.Window.FileSelector(vectorize, label, outputfile)
2013
2014         else:
2015             print "Event: %d not handled!" % evt
2016
2017         if evt:
2018             Draw.Redraw(1)
2019             #GUI.conf_debug()
2020
2021     def conf_debug():
2022         from pprint import pprint
2023         print "\nConfig"
2024         pprint(config.output)
2025         pprint(config.polygons)
2026         pprint(config.edges)
2027
2028     _init = staticmethod(_init)
2029     draw = staticmethod(draw)
2030     event = staticmethod(event)
2031     button_event = staticmethod(button_event)
2032     conf_debug = staticmethod(conf_debug)
2033
2034 # A wrapper function for the vectorizing process
2035 def vectorize(filename):
2036     """The vectorizing process is as follows:
2037      
2038      - Instanciate the writer and the renderer
2039      - Render!
2040      """
2041
2042     if filename == "":
2043         print "\nERROR: invalid file name!"
2044         return
2045
2046     from Blender import Window
2047     editmode = Window.EditMode()
2048     if editmode: Window.EditMode(0)
2049
2050     actualWriter = outputWriters[config.output['FORMAT']]
2051     writer = actualWriter(filename)
2052     
2053     renderer = Renderer()
2054     renderer.doRendering(writer, config.output['ANIMATION'])
2055
2056     if editmode: Window.EditMode(1) 
2057
2058 # We use a global progress Indicator Object
2059 progress = None
2060
2061 # Here the main
2062 if __name__ == "__main__":
2063
2064     global progress
2065
2066     outputfile = ""
2067     basename = Blender.sys.basename(Blender.Get('filename'))
2068     if basename != "":
2069         outputfile = Blender.sys.splitext(basename)[0] + "." + str(config.output['FORMAT']).lower()
2070
2071     if Blender.mode == 'background':
2072         progress = ConsoleProgressIndicator()
2073         vectorize(outputfile)
2074     else:
2075         progress = GraphicalProgressIndicator()
2076         Draw.Register(GUI.draw, GUI.event, GUI.button_event)