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