Code

Merging from trunk
[inkscape.git] / share / extensions / polyhedron_3d.py
1 #!/usr/bin/env python 
2 '''
3 Copyright (C) 2007 John Beard john.j.beard@gmail.com
5 ##This extension draws 3d objects from a Wavefront .obj 3D file stored in a local folder
6 ##Many settings for appearance, lighting, rotation, etc are available.
8 #                              ^y
9 #                              |
10 #        __--``|               |_--``|     __--
11 #  __--``      |         __--``|     |_--``
12 # |       z    |        |      |_--``|
13 # |       <----|--------|-----_0-----|----------------
14 # |            |        |_--`` |     |
15 # |      __--``     <-``|      |_--``
16 # |__--``           x   |__--``|
17 #   IMAGE PLANE           SCENE|
18 #                              |
20 #Vertices are given as "v" followed by three numbers (x,y,z).
21 #All files need a vertex list
22 #v  x.xxx   y.yyy   z.zzz
24 #Faces are given by a list of vertices
25 #(vertex 1 is the first in the list above, 2 the second, etc):
26 #f  1   2   3
28 #Edges are given by a list of vertices. These will be broken down
29 #into adjacent pairs. Each edge can connect only two vertices
30 #l  1   2   3
32 #Faces are rendered according to the painter's algorithm and perhaps
33 #back-face culling, if selected. The parameter to sort the faces by
34 #is user-selectable
36 ######LICENCE#######
37 This program is free software; you can redistribute it and/or modify
38 it under the terms of the GNU General Public License as published by
39 the Free Software Foundation; either version 2 of the License, or
40 (at your option) any later version.
42 This program is distributed in the hope that it will be useful,
43 but WITHOUT ANY WARRANTY; without even the implied warranty of
44 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
45 GNU General Public License for more details.
47 You should have received a copy of the GNU General Public License
48 along with this program; if not, write to the Free Software
49 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
50 '''
52 import inkex
53 import simplestyle, sys, simplepath, re
54 from math import *
55 import gettext
56 _ = gettext.gettext
57 try:
58     from numpy import *
59 except:
60     inkex.errormsg(_("Failed to import the numpy module. This module is required by this extension. Please install them and try again.  On a Debian-like system this can be done with the command, sudo apt-get install python-numpy."))
61     sys.exit()
63 def objfile(name):
64     import os.path
65     if __name__ == '__main__':
66         filename = sys.argv[0]
67     else:
68         filename = __file__
69     path = os.path.abspath(os.path.dirname(filename))
70     path = os.path.join(path, 'Poly3DObjects', name)
71     return path
72     
73 def get_obj_data(obj, name):
74     infile = open(objfile(name))
75     
76     #regular expressions
77     getname = '(.[nN]ame:\\s*)(.*)'
78     floating = '([\-\+\\d*\.e]*)'
79     getvertex = '(v\\s+)'+floating+'\\s+'+floating+'\\s+'+floating
80     getedgeline = '(l\\s+)(.*)'
81     getfaceline = '(f\\s+)(.*)'
82     getnextint = '(\\d+)([/\\d]*)(.*)'#we need to deal with 133\343\123 or 123\\456 as one item
83     
84     obj.vtx = []
85     obj.edg = []
86     obj.fce = []
87     obj.name=''
88     
89     for line in infile:
90         if line[0]=='#': #we have a comment line
91             m = re.search(getname, line)
92             if m:
93                 obj.name = m.group(2)
94         elif line[0:1] == 'v': #we have a vertex (maybe)
95             m = re.search(getvertex, line)
96             if m: #we have a valid vertex
97                 obj.vtx.append( [float(m.group(2)), float(m.group(3)), float(m.group(4)) ] )
98         elif line[0:1] == 'l':#we have a line (maybe)
99             m = re.search(getedgeline, line)
100             if m:#we have a line beginning 'l '
101                 vtxlist = []#buffer
102                 while line:
103                     m2 = re.search(getnextint, line)
104                     if m2:
105                         vtxlist.append( int(m2.group(1)) )
106                         line = m2.group(3)#remainder
107                     else:
108                         line = None
109                 if len(vtxlist) > 1:#we need at least 2 vertices to make an edge
110                     for i in range (len(vtxlist)-1):#we can have more than one vertex per line - get adjacent pairs
111                         obj.edg.append( ( vtxlist[i], vtxlist[i+1] ) )#get the vertex pair between that vertex and the next
112         elif line[0:1] == 'f':#we have a face (maybe)
113             m = re.search(getfaceline, line)
114             if m:#we have a line beginning 'l '
115                 vtxlist = []#buffer
116                 while line:
117                     m2 = re.search(getnextint, line)
118                     if m2:
119                         vtxlist.append( int(m2.group(1)) )
120                         line = m2.group(3)#remainder
121                     else:
122                         line = None
123                 if len(vtxlist) > 2:#we need at least 3 vertices to make an edge
124                     obj.fce.append(vtxlist)
125     
126     if obj.name == '':#no name was found, use filename, without extension
127         obj.name = name[0:-4]
129 def draw_SVG_dot((cx, cy), st, name, parent):
130     style = { 'stroke': '#000000', 'stroke-width':str(st.th), 'fill': st.fill, 'stroke-opacity':st.s_opac, 'fill-opacity':st.f_opac}
131     circ_attribs = {'style':simplestyle.formatStyle(style),
132                     inkex.addNS('label','inkscape'):name,
133                     'r':str(st.r),
134                     'cx':str(cx), 'cy':str(-cy)}
135     inkex.etree.SubElement(parent, inkex.addNS('circle','svg'), circ_attribs )
136     
137 def draw_SVG_line((x1, y1),(x2, y2), st, name, parent):
138     #sys.stderr.write(str(p1))
139     style = { 'stroke': '#000000', 'stroke-width':str(st.th)}
140     line_attribs = {'style':simplestyle.formatStyle(style),
141                     inkex.addNS('label','inkscape'):name,
142                     'd':'M '+str(x1)+','+str(-y1)+' L '+str(x2)+','+str(-y2)}
143     inkex.etree.SubElement(parent, inkex.addNS('path','svg'), line_attribs )
144     
145 def draw_SVG_poly(pts, face, st, name, parent):
146     style = { 'stroke': '#000000', 'stroke-width':str(st.th), 'stroke-linejoin':st.linejoin, \
147               'stroke-opacity':st.s_opac, 'fill': st.fill, 'fill-opacity':st.f_opac}   
148     for i in range(len(face)):
149         if i == 0:#for first point
150             d = 'M'#move to
151         else:
152             d = d + 'L'#line to
153         d = d+ str(pts[face[i]-1][0]) + ',' + str(-pts[face[i]-1][1])#add point
154     d = d + 'z' #close the polygon
155     
156     line_attribs = {'style':simplestyle.formatStyle(style),
157                     inkex.addNS('label','inkscape'):name,'d': d}
158     inkex.etree.SubElement(parent, inkex.addNS('path','svg'), line_attribs )
159     
160 def get_normal( pts, face): #returns the normal vector for the plane passing though the first three elements of face of pts
161     #n = pt[0]->pt[1] x pt[0]->pt[3]
162     a = (array(pts[ face[0]-1 ]) - array(pts[ face[1]-1 ]))
163     b = (array(pts[ face[0]-1 ]) - array(pts[ face[2]-1 ]))
164     return cross(a,b).flatten()
165     
166 def get_max_z(pts, face): #returns the largest z_value of any point in the face
167     max_z = pts[ face[0]-1 ][2]
168     for i in range(1, len(face)):
169         if pts[ face[0]-1 ][2] >= max_z:
170             max_z = pts[ face[0]-1 ][2]
171     return max_z
172     
173 def get_min_z(pts, face): #returns the smallest z_value of any point in the face
174     min_z = pts[ face[0]-1 ][2]
175     for i in range(1, len(face)):
176         if pts[ face[i]-1 ][2] <= min_z:
177             min_z = pts[ face[i]-1 ][2]
178     return min_z
179     
180 def get_cent_z(pts, face): #returns the centroid z_value of any point in the face
181     sum = 0
182     for i in range(len(face)):
183             sum += pts[ face[i]-1 ][2]
184     return sum/len(face)
185     
186 def length(vector):#return the pythagorean length of a vector
187     return sqrt(dot(vector,vector))
188     
189 def rot_z( matrix , a):
190     trans_mat = mat(array( [[ cos(a) , -sin(a) ,    0   ],
191                             [ sin(a) ,  cos(a) ,    0   ],
192                             [   0    ,    0    ,    1   ]]))
193     return trans_mat*matrix
195 def rot_y( matrix , a):
196     trans_mat = mat(array( [[ cos(a) ,    0    , sin(a) ],
197                             [   0    ,    1    ,    0   ],
198                             [-sin(a) ,    0    , cos(a) ]]))
199     return trans_mat*matrix
200     
201 def rot_x( matrix , a):
202     trans_mat = mat(array( [[   1    ,    0    ,    0   ],
203                             [   0    ,  cos(a) ,-sin(a) ],
204                             [   0    ,  sin(a) , cos(a) ]]))
205     return trans_mat*matrix
206     
207 def make_edge_list(face_list):#make an edge vertex list from an existing face vertex list
208     edge_list = []
209     for i in range(len(face_list)):#for every face
210         edges = len(face_list[i]) #number of edges around that face
211         for j in range(edges):#for every vertex in that face
212             edge_list.append( [face_list[i][j], face_list[i][(j+1)%edges] ] )#get the vertex pair between that vertex and the next
213  
214     for i in range(len(edge_list)):
215         edge_list[i].sort()#sort the entries of the entries
216     edge_list.sort()#sort the list
217  
218     last = edge_list[-1] #delete duplicate entries
219     for i in range(len(edge_list)-2, -1, -1):
220         if last==edge_list[i]:
221             del edge_list[i]
222         else:
223             last=edge_list[i]
224     return edge_list
225     
226 class Style(object): #container for style information
227     def __init__(self):
228         None
230 class Obj(object): #a 3d object defined by the vertices and the faces (eg a polyhedron)
231 #edges can be generated from this information
232     def __init__(self):
233         None
235 class Poly_3D(inkex.Effect):
236     def __init__(self):
237         inkex.Effect.__init__(self)
238         self.OptionParser.add_option("--tab",
239             action="store", type="string", 
240             dest="tab", default="object") 
242 #MODEL FILE SETTINGS
243         self.OptionParser.add_option("--obj",
244             action="store", type="string", 
245             dest="obj", default='cube')
246         self.OptionParser.add_option("--spec_file",
247             action="store", type="string", 
248             dest="spec_file", default='great_rhombicuboct.obj')
249         self.OptionParser.add_option("--cw_wound",
250             action="store", type="inkbool", 
251             dest="cw_wound", default='true')
252         self.OptionParser.add_option("--type",
253             action="store", type="string", 
254             dest="type", default='face')
255 #VEIW SETTINGS
256         self.OptionParser.add_option("--r1_ax",
257             action="store", type="string", 
258             dest="r1_ax", default=0)
259         self.OptionParser.add_option("--r2_ax",
260             action="store", type="string", 
261             dest="r2_ax", default=0)
262         self.OptionParser.add_option("--r3_ax",
263             action="store", type="string", 
264             dest="r3_ax", default=0)
265         self.OptionParser.add_option("--r4_ax",
266             action="store", type="string", 
267             dest="r4_ax", default=0)
268         self.OptionParser.add_option("--r5_ax",
269             action="store", type="string", 
270             dest="r5_ax", default=0)
271         self.OptionParser.add_option("--r6_ax",
272             action="store", type="string", 
273             dest="r6_ax", default=0)
274         self.OptionParser.add_option("--r1_ang",
275             action="store", type="float", 
276             dest="r1_ang", default=0)
277         self.OptionParser.add_option("--r2_ang",
278             action="store", type="float", 
279             dest="r2_ang", default=0)
280         self.OptionParser.add_option("--r3_ang",
281             action="store", type="float", 
282             dest="r3_ang", default=0)
283         self.OptionParser.add_option("--r4_ang",
284             action="store", type="float", 
285             dest="r4_ang", default=0)
286         self.OptionParser.add_option("--r5_ang",
287             action="store", type="float", 
288             dest="r5_ang", default=0)
289         self.OptionParser.add_option("--r6_ang",
290             action="store", type="float", 
291             dest="r6_ang", default=0)
292         self.OptionParser.add_option("--scl",
293             action="store", type="float", 
294             dest="scl", default=100.0)
295 #STYLE SETTINGS
296         self.OptionParser.add_option("--show",
297             action="store", type="string", 
298             dest="show", default='faces')
299         self.OptionParser.add_option("--shade",
300             action="store", type="inkbool", 
301             dest="shade", default='true')
302         self.OptionParser.add_option("--f_r",
303             action="store", type="int", 
304             dest="f_r", default=255)
305         self.OptionParser.add_option("--f_g",
306             action="store", type="int", 
307             dest="f_g", default=0)
308         self.OptionParser.add_option("--f_b",
309             action="store", type="int", 
310             dest="f_b", default=0)
311         self.OptionParser.add_option("--f_opac",
312             action="store", type="int", 
313             dest="f_opac", default=100)
314         self.OptionParser.add_option("--s_opac",
315             action="store", type="int", 
316             dest="s_opac", default=100)
317         self.OptionParser.add_option("--th",
318             action="store", type="float", 
319             dest="th", default=2)
320         self.OptionParser.add_option("--lv_x",
321             action="store", type="float", 
322             dest="lv_x", default=1)
323         self.OptionParser.add_option("--lv_y",
324             action="store", type="float", 
325             dest="lv_y", default=1)
326         self.OptionParser.add_option("--lv_z",
327             action="store", type="float", 
328             dest="lv_z", default=-2)
329         self.OptionParser.add_option("--back",
330             action="store", type="inkbool", 
331             dest="back", default='false')
332         self.OptionParser.add_option("--norm",
333             action="store", type="inkbool", 
334             dest="norm", default='true')
335         self.OptionParser.add_option("--z_sort",
336             action="store", type="string", 
337             dest="z_sort", default='min')
338             
339             
340     def effect(self):
341         so = self.options
342         
343         st = Style()
344         st.th = so.th
345         st.fill= '#ff0000'
346         st.col = '#000000'
347         st.r = 2
348         st.f_opac = str(so.f_opac/100.0)
349         st.s_opac = str(so.s_opac/100.0)
350         st.linecap = 'round'
351         st.linejoin = 'round'
352         
353         file = ''
354         if   so.obj == 'cube':
355             file = 'cube.obj'
356         elif so.obj == 't_cube':
357             file = 'trunc_cube.obj'
358         elif so.obj == 'sn_cube':
359             file = 'snub_cube.obj'
360         elif so.obj == 'cuboct':
361             file = 'cuboct.obj'
362         elif so.obj == 'tet':
363             file = 'tet.obj'
364         elif so.obj == 't_tet':
365             file = 'trunc_tet.obj'
366         elif so.obj == 'oct':
367             file = 'oct.obj'
368         elif so.obj == 't_oct':
369             file = 'trunc_oct.obj'
370         elif so.obj == 'icos':
371             file = 'icos.obj'
372         elif so.obj == 't_icos':
373             file = 'trunc_icos.obj'
374         elif so.obj == 's_t_icos':
375             file = 'small_triam_icos.obj'
376         elif so.obj == 'g_s_dodec':
377             file = 'great_stel_dodec.obj'
378         elif so.obj == 'dodec':
379             file = 'dodec.obj'
380         elif so.obj == 'sn_dodec':
381             file = 'snub_dodec.obj'
382         elif so.obj == 'g_dodec':
383             file = 'great_dodec.obj'
384         elif so.obj == 't_dodec':
385             file = 'trunc_dodec.obj'
386         elif so.obj == 'from_file':
387             file = so.spec_file
388             
389         obj = Obj() #create the object
390         get_obj_data(obj, file)
391         
392         obj.type=''
393         if so.type == 'face':
394             if len(obj.fce) > 0:
395                 obj.type = 'face'
396             else:
397                 sys.stderr.write('No face data found in specified file\n')
398                 obj.type = 'error'
399         else:
400             if len(obj.edg) > 0:
401                 obj.type = 'edge'
402             else:
403                 sys.stderr.write('No edge data found in specified file\n')
404                 obj.type = 'error'
405         
406         trans_mat = mat(identity(3, float)) #init. trans matrix as identity matrix
407         
408         #perform rotations
409         for i in range(1, 7):#for each rotation
410             axis  = eval('so.r'+str(i)+'_ax')
411             angle = eval('so.r'+str(i)+'_ang') *pi/180
412             if   axis == 'x':
413                 trans_mat = rot_x(trans_mat, angle)
414             elif axis == 'y':
415                 trans_mat = rot_y(trans_mat, angle)
416             elif axis == 'z':
417                 trans_mat = rot_z(trans_mat, angle)
419         # Embed points in group
420         #Put in in the centre of the current view
421         t = 'translate(' + str( self.view_center[0]) + ',' + str( self.view_center[1]) + ')'
422         #we will put all the rotations in the object name, so it can be repeated in future
423         proj_attribs = {inkex.addNS('label','inkscape'):obj.name+':'+so.r1_ax+str('%.2f'%so.r1_ang)+':'+
424                                                                      so.r2_ax+str('%.2f'%so.r2_ang)+':'+
425                                                                      so.r3_ax+str('%.2f'%so.r3_ang)+':'+
426                                                                      so.r1_ax+str('%.2f'%so.r4_ang)+':'+
427                                                                      so.r2_ax+str('%.2f'%so.r5_ang)+':'+
428                                                                      so.r3_ax+str('%.2f'%so.r6_ang),
429                         'transform':t }
430         proj = inkex.etree.SubElement(self.current_layer, 'g', proj_attribs)#the group to put everything in
431         
432         vp_pts=[] #the points as projected in the z-axis onto the viewplane
433         
434         for i in range(len(obj.vtx)):
435             vp_pts.append((so.scl* (trans_mat * mat(obj.vtx[i]).T)).T.tolist()[0] )#transform the points at add to vp_pts
436         
437         lighting = [so.lv_x,-so.lv_y,so.lv_z] #direction of light vector
438         lighting = lighting/length(lighting) #normalise
439         
440         if so.show == 'vtx':
441             for i in range(len(vp_pts)):
442                 draw_SVG_dot([vp_pts[i][0],vp_pts[i][1]], st, 'Point'+str(i), proj)#plot points
443         
444         elif so.show == 'edg':
445             if obj.type == 'face':#we must generate the edge list
446                 edge_list = make_edge_list(obj.fce)
447             else:#we already have an edge list
448                 edge_list = obj.edg
449                         
450             for i in range(len(edge_list)):#for every edge
451                 pt_1 = vp_pts[ edge_list[i][0]-1 ] #the point at the start
452                 pt_2 = vp_pts[ edge_list[i][1]-1 ] #the point at the end
453                 
454                 draw_SVG_line((pt_1[0], pt_1[1]),
455                               (pt_2[0], pt_2[1]),
456                               st, 'Edge', proj)#plot edges
457                               
458         elif so.show == 'fce':
459             if obj.type == 'face':#we have a face list
460                 
461                 if so.cw_wound: rev = -1 #if cw wound, reverse normals
462                 else: rev = 1
463                 
464                 z_list = []
465                 
466                 for i in range(len(obj.fce)):
467                     norm = get_normal(vp_pts, obj.fce[i])#get the normal to the face
468                     norm = rev*norm / length(norm)#normalise and reverse if needed
469                     angle = acos( dot(norm, lighting) )#get the angle between the normal and the lighting vector
470                     
471                     
472                     if   so.z_sort =='max':
473                         z_sort_param  = get_max_z(vp_pts, obj.fce[i])
474                     elif so.z_sort == 'min':
475                         z_sort_param  = get_min_z(vp_pts, obj.fce[i])
476                     else:
477                         z_sort_param  = get_cent_z(vp_pts, obj.fce[i])
478                     
479                     if so.norm:#if a log of normals is required
480                         if i == 0:
481                             sys.stderr.write('Normal Vectors for each face are: \n\n')
482                         sys.stderr.write('Face '+str(i)+': ' + str(norm) + '\n')
483                     
484                     if so.back: # draw all polygons
485                         z_list.append((z_sort_param, angle, norm, i) )
486                     elif norm[2] > 0:#ignore backwards-facing faces (back face cull)
487                         z_list.append((z_sort_param, angle, norm, i) ) #record the maximum z-value of the face and angle to light, along with the face ID and normal
488                 
489                 z_list.sort(lambda x, y: cmp(x[0],y[0])) #sort by ascending sort parameter of the face
490                 
491                 for i in range(len(z_list)):#for every polygon that has been sorted
492                     if so.shade:
493                         st.fill = '#' + "%02X" % floor( z_list[i][1]*so.f_r/pi ) \
494                                       + "%02X" % floor( z_list[i][1]*so.f_g/pi ) \
495                                       + "%02X" % floor( z_list[i][1]*so.f_b/pi ) #make the colour string
496                     else:
497                         st.fill = '#' + '%02X' % so.f_r + '%02X' % so.f_g + '%02X' % so.f_b #opaque
498                                       
499                     face_no = z_list[i][3]#the number of the face to draw
500                     draw_SVG_poly(vp_pts, obj.fce[ face_no ], st, 'Face:'+str(face_no), proj)
501             else:
502                 sys.stderr.write('Face Data Not Found. Ensure file contains face data, and check the file is imported as "Face-Specifed" under the "Model File" tab.\n')
503         else:
504             sys.stderr.write('Internal Error. No view type selected\n')
505         
506 if __name__ == '__main__':
507     e = Poly_3D()
508     e.affect()
511 # vim: expandtab shiftwidth=4 tabstop=8 softtabstop=4 encoding=utf-8 textwidth=99