Code

share/extension/*.py: noop: minor whitespace regularizations ahead of adding vim...
[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 try:
56     from numpy import *
57 except:
58     inkex.debug("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.")
59     sys.exit()
61 def objfile(name):
62     import os.path
63     if __name__ == '__main__':
64         filename = sys.argv[0]
65     else:
66         filename = __file__
67     path = os.path.abspath(os.path.dirname(filename))
68     path = os.path.join(path, 'Poly3DObjects', name)
69     return path
70     
71 def get_obj_data(obj, name):
72     infile = open(objfile(name))
73     
74     #regular expressions
75     getname = '(.[nN]ame:\\s*)(.*)'
76     floating = '([\-\+\\d*\.e]*)'
77     getvertex = '(v\\s+)'+floating+'\\s+'+floating+'\\s+'+floating
78     getedgeline = '(l\\s+)(.*)'
79     getfaceline = '(f\\s+)(.*)'
80     getnextint = '(\\d+)([/\\d]*)(.*)'#we need to deal with 133\343\123 or 123\\456 as one item
81     
82     obj.vtx = []
83     obj.edg = []
84     obj.fce = []
85     obj.name=''
86     
87     for line in infile:
88         if line[0]=='#': #we have a comment line
89             m = re.search(getname, line)
90             if m:
91                 obj.name = m.group(2)
92         elif line[0:1] == 'v': #we have a vertex (maybe)
93             m = re.search(getvertex, line)
94             if m: #we have a valid vertex
95                 obj.vtx.append( [float(m.group(2)), float(m.group(3)), float(m.group(4)) ] )
96         elif line[0:1] == 'l':#we have a line (maybe)
97             m = re.search(getedgeline, line)
98             if m:#we have a line beginning 'l '
99                 vtxlist = []#buffer
100                 while line:
101                     m2 = re.search(getnextint, line)
102                     if m2:
103                         vtxlist.append( int(m2.group(1)) )
104                         line = m2.group(3)#remainder
105                     else:
106                         line = None
107                 if len(vtxlist) > 1:#we need at least 2 vertices to make an edge
108                     for i in range (len(vtxlist)-1):#we can have more than one vertex per line - get adjacent pairs
109                         obj.edg.append( ( vtxlist[i], vtxlist[i+1] ) )#get the vertex pair between that vertex and the next
110         elif line[0:1] == 'f':#we have a face (maybe)
111             m = re.search(getfaceline, line)
112             if m:#we have a line beginning 'l '
113                 vtxlist = []#buffer
114                 while line:
115                     m2 = re.search(getnextint, line)
116                     if m2:
117                         vtxlist.append( int(m2.group(1)) )
118                         line = m2.group(3)#remainder
119                     else:
120                         line = None
121                 if len(vtxlist) > 2:#we need at least 3 vertices to make an edge
122                     obj.fce.append(vtxlist)
123     
124     if obj.name == '':#no name was found, use filename, without extension
125         obj.name = name[0:-4]
127 def draw_SVG_dot((cx, cy), st, name, parent):
128     style = { 'stroke': '#000000', 'stroke-width':str(st.th), 'fill': st.fill, 'stroke-opacity':st.s_opac, 'fill-opacity':st.f_opac}
129     circ_attribs = {'style':simplestyle.formatStyle(style),
130                     inkex.addNS('label','inkscape'):name,
131                     'r':str(st.r),
132                     'cx':str(cx), 'cy':str(-cy)}
133     inkex.etree.SubElement(parent, inkex.addNS('circle','svg'), circ_attribs )
134     
135 def draw_SVG_line((x1, y1),(x2, y2), st, name, parent):
136     #sys.stderr.write(str(p1))
137     style = { 'stroke': '#000000', 'stroke-width':str(st.th)}
138     line_attribs = {'style':simplestyle.formatStyle(style),
139                     inkex.addNS('label','inkscape'):name,
140                     'd':'M '+str(x1)+','+str(-y1)+' L '+str(x2)+','+str(-y2)}
141     inkex.etree.SubElement(parent, inkex.addNS('path','svg'), line_attribs )
142     
143 def draw_SVG_poly(pts, face, st, name, parent):
144     style = { 'stroke': '#000000', 'stroke-width':str(st.th), 'stroke-linejoin':st.linejoin, \
145               'stroke-opacity':st.s_opac, 'fill': st.fill, 'fill-opacity':st.f_opac}   
146     for i in range(len(face)):
147         if i == 0:#for first point
148             d = 'M'#move to
149         else:
150             d = d + 'L'#line to
151         d = d+ str(pts[face[i]-1][0]) + ',' + str(-pts[face[i]-1][1])#add point
152     d = d + 'z' #close the polygon
153     
154     line_attribs = {'style':simplestyle.formatStyle(style),
155                     inkex.addNS('label','inkscape'):name,'d': d}
156     inkex.etree.SubElement(parent, inkex.addNS('path','svg'), line_attribs )
157     
158 def get_normal( pts, face): #returns the normal vector for the plane passing though the first three elements of face of pts
159     #n = pt[0]->pt[1] x pt[0]->pt[3]
160     a = (array(pts[ face[0]-1 ]) - array(pts[ face[1]-1 ]))
161     b = (array(pts[ face[0]-1 ]) - array(pts[ face[2]-1 ]))
162     return cross(a,b).flatten()
163     
164 def get_max_z(pts, face): #returns the largest z_value of any point in the face
165     max_z = pts[ face[0]-1 ][2]
166     for i in range(1, len(face)):
167         if pts[ face[0]-1 ][2] >= max_z:
168             max_z = pts[ face[0]-1 ][2]
169     return max_z
170     
171 def get_min_z(pts, face): #returns the smallest z_value of any point in the face
172     min_z = pts[ face[0]-1 ][2]
173     for i in range(1, len(face)):
174         if pts[ face[i]-1 ][2] <= min_z:
175             min_z = pts[ face[i]-1 ][2]
176     return min_z
177     
178 def get_cent_z(pts, face): #returns the centroid z_value of any point in the face
179     sum = 0
180     for i in range(len(face)):
181             sum += pts[ face[i]-1 ][2]
182     return sum/len(face)
183     
184 def length(vector):#return the pythagorean length of a vector
185     return sqrt(dot(vector,vector))
186     
187 def rot_z( matrix , a):
188     trans_mat = mat(array( [[ cos(a) , -sin(a) ,    0   ],
189                             [ sin(a) ,  cos(a) ,    0   ],
190                             [   0    ,    0    ,    1   ]]))
191     return trans_mat*matrix
193 def rot_y( matrix , a):
194     trans_mat = mat(array( [[ cos(a) ,    0    , sin(a) ],
195                             [   0    ,    1    ,    0   ],
196                             [-sin(a) ,    0    , cos(a) ]]))
197     return trans_mat*matrix
198     
199 def rot_x( matrix , a):
200     trans_mat = mat(array( [[   1    ,    0    ,    0   ],
201                             [   0    ,  cos(a) ,-sin(a) ],
202                             [   0    ,  sin(a) , cos(a) ]]))
203     return trans_mat*matrix
204     
205 def make_edge_list(face_list):#make an edge vertex list from an existing face vertex list
206     edge_list = []
207     for i in range(len(face_list)):#for every face
208         edges = len(face_list[i]) #number of edges around that face
209         for j in range(edges):#for every vertex in that face
210             edge_list.append( [face_list[i][j], face_list[i][(j+1)%edges] ] )#get the vertex pair between that vertex and the next
211  
212     for i in range(len(edge_list)):
213         edge_list[i].sort()#sort the entries of the entries
214     edge_list.sort()#sort the list
215  
216     last = edge_list[-1] #delete duplicate entries
217     for i in range(len(edge_list)-2, -1, -1):
218         if last==edge_list[i]:
219             del edge_list[i]
220         else:
221             last=edge_list[i]
222     return edge_list
223     
224 class Style(object): #container for style information
225     def __init__(self):
226         None
228 class Obj(object): #a 3d object defined by the vertices and the faces (eg a polyhedron)
229 #edges can be generated from this information
230     def __init__(self):
231         None
233 class Poly_3D(inkex.Effect):
234     def __init__(self):
235         inkex.Effect.__init__(self)
236         self.OptionParser.add_option("--tab",
237             action="store", type="string", 
238             dest="tab", default="object") 
240 #MODEL FILE SETTINGS
241         self.OptionParser.add_option("--obj",
242             action="store", type="string", 
243             dest="obj", default='cube')
244         self.OptionParser.add_option("--spec_file",
245             action="store", type="string", 
246             dest="spec_file", default='great_rhombicuboct.obj')
247         self.OptionParser.add_option("--cw_wound",
248             action="store", type="inkbool", 
249             dest="cw_wound", default='true')
250         self.OptionParser.add_option("--type",
251             action="store", type="string", 
252             dest="type", default='face')
253 #VEIW SETTINGS
254         self.OptionParser.add_option("--r1_ax",
255             action="store", type="string", 
256             dest="r1_ax", default=0)
257         self.OptionParser.add_option("--r2_ax",
258             action="store", type="string", 
259             dest="r2_ax", default=0)
260         self.OptionParser.add_option("--r3_ax",
261             action="store", type="string", 
262             dest="r3_ax", default=0)
263         self.OptionParser.add_option("--r4_ax",
264             action="store", type="string", 
265             dest="r4_ax", default=0)
266         self.OptionParser.add_option("--r5_ax",
267             action="store", type="string", 
268             dest="r5_ax", default=0)
269         self.OptionParser.add_option("--r6_ax",
270             action="store", type="string", 
271             dest="r6_ax", default=0)
272         self.OptionParser.add_option("--r1_ang",
273             action="store", type="float", 
274             dest="r1_ang", default=0)
275         self.OptionParser.add_option("--r2_ang",
276             action="store", type="float", 
277             dest="r2_ang", default=0)
278         self.OptionParser.add_option("--r3_ang",
279             action="store", type="float", 
280             dest="r3_ang", default=0)
281         self.OptionParser.add_option("--r4_ang",
282             action="store", type="float", 
283             dest="r4_ang", default=0)
284         self.OptionParser.add_option("--r5_ang",
285             action="store", type="float", 
286             dest="r5_ang", default=0)
287         self.OptionParser.add_option("--r6_ang",
288             action="store", type="float", 
289             dest="r6_ang", default=0)
290         self.OptionParser.add_option("--scl",
291             action="store", type="float", 
292             dest="scl", default=100.0)
293 #STYLE SETTINGS
294         self.OptionParser.add_option("--show",
295             action="store", type="string", 
296             dest="show", default='faces')
297         self.OptionParser.add_option("--shade",
298             action="store", type="inkbool", 
299             dest="shade", default='true')
300         self.OptionParser.add_option("--f_r",
301             action="store", type="int", 
302             dest="f_r", default=255)
303         self.OptionParser.add_option("--f_g",
304             action="store", type="int", 
305             dest="f_g", default=0)
306         self.OptionParser.add_option("--f_b",
307             action="store", type="int", 
308             dest="f_b", default=0)
309         self.OptionParser.add_option("--f_opac",
310             action="store", type="int", 
311             dest="f_opac", default=100)
312         self.OptionParser.add_option("--s_opac",
313             action="store", type="int", 
314             dest="s_opac", default=100)
315         self.OptionParser.add_option("--th",
316             action="store", type="float", 
317             dest="th", default=2)
318         self.OptionParser.add_option("--lv_x",
319             action="store", type="float", 
320             dest="lv_x", default=1)
321         self.OptionParser.add_option("--lv_y",
322             action="store", type="float", 
323             dest="lv_y", default=1)
324         self.OptionParser.add_option("--lv_z",
325             action="store", type="float", 
326             dest="lv_z", default=-2)
327         self.OptionParser.add_option("--back",
328             action="store", type="inkbool", 
329             dest="back", default='false')
330         self.OptionParser.add_option("--norm",
331             action="store", type="inkbool", 
332             dest="norm", default='true')
333         self.OptionParser.add_option("--z_sort",
334             action="store", type="string", 
335             dest="z_sort", default='min')
336             
337             
338     def effect(self):
339         so = self.options
340         
341         st = Style()
342         st.th = so.th
343         st.fill= '#ff0000'
344         st.col = '#000000'
345         st.r = 2
346         st.f_opac = str(so.f_opac/100.0)
347         st.s_opac = str(so.s_opac/100.0)
348         st.linecap = 'round'
349         st.linejoin = 'round'
350         
351         file = ''
352         if   so.obj == 'cube':
353             file = 'cube.obj'
354         elif so.obj == 't_cube':
355             file = 'trunc_cube.obj'
356         elif so.obj == 'sn_cube':
357             file = 'snub_cube.obj'
358         elif so.obj == 'cuboct':
359             file = 'cuboct.obj'
360         elif so.obj == 'tet':
361             file = 'tet.obj'
362         elif so.obj == 't_tet':
363             file = 'trunc_tet.obj'
364         elif so.obj == 'oct':
365             file = 'oct.obj'
366         elif so.obj == 't_oct':
367             file = 'trunc_oct.obj'
368         elif so.obj == 'icos':
369             file = 'icos.obj'
370         elif so.obj == 't_icos':
371             file = 'trunc_icos.obj'
372         elif so.obj == 's_t_icos':
373             file = 'small_triam_icos.obj'
374         elif so.obj == 'g_s_dodec':
375             file = 'great_stel_dodec.obj'
376         elif so.obj == 'dodec':
377             file = 'dodec.obj'
378         elif so.obj == 'sn_dodec':
379             file = 'snub_dodec.obj'
380         elif so.obj == 'g_dodec':
381             file = 'great_dodec.obj'
382         elif so.obj == 't_dodec':
383             file = 'trunc_dodec.obj'
384         elif so.obj == 'from_file':
385             file = so.spec_file
386             
387         obj = Obj() #create the object
388         get_obj_data(obj, file)
389         
390         obj.type=''
391         if so.type == 'face':
392             if len(obj.fce) > 0:
393                 obj.type = 'face'
394             else:
395                 sys.stderr.write('No face data found in specified file\n')
396                 obj.type = 'error'
397         else:
398             if len(obj.edg) > 0:
399                 obj.type = 'edge'
400             else:
401                 sys.stderr.write('No edge data found in specified file\n')
402                 obj.type = 'error'
403         
404         trans_mat = mat(identity(3, float)) #init. trans matrix as identity matrix
405         
406         #perform rotations
407         for i in range(1, 7):#for each rotation
408             axis  = eval('so.r'+str(i)+'_ax')
409             angle = eval('so.r'+str(i)+'_ang') *pi/180
410             if   axis == 'x':
411                 trans_mat = rot_x(trans_mat, angle)
412             elif axis == 'y':
413                 trans_mat = rot_y(trans_mat, angle)
414             elif axis == 'z':
415                 trans_mat = rot_z(trans_mat, angle)
417         # Embed points in group
418         #Put in in the centre of the current view
419         t = 'translate(' + str( self.view_center[0]) + ',' + str( self.view_center[1]) + ')'
420         #we will put all the rotations in the object name, so it can be repeated in future
421         proj_attribs = {inkex.addNS('label','inkscape'):obj.name+':'+so.r1_ax+str('%.2f'%so.r1_ang)+':'+
422                                                                      so.r2_ax+str('%.2f'%so.r2_ang)+':'+
423                                                                      so.r3_ax+str('%.2f'%so.r3_ang)+':'+
424                                                                      so.r1_ax+str('%.2f'%so.r4_ang)+':'+
425                                                                      so.r2_ax+str('%.2f'%so.r5_ang)+':'+
426                                                                      so.r3_ax+str('%.2f'%so.r6_ang),
427                         'transform':t }
428         proj = inkex.etree.SubElement(self.current_layer, 'g', proj_attribs)#the group to put everything in
429         
430         vp_pts=[] #the points as projected in the z-axis onto the viewplane
431         
432         for i in range(len(obj.vtx)):
433             vp_pts.append((so.scl* (trans_mat * mat(obj.vtx[i]).T)).T.tolist()[0] )#transform the points at add to vp_pts
434         
435         lighting = [so.lv_x,-so.lv_y,so.lv_z] #direction of light vector
436         lighting = lighting/length(lighting) #normalise
437         
438         if so.show == 'vtx':
439             for i in range(len(vp_pts)):
440                 draw_SVG_dot([vp_pts[i][0],vp_pts[i][1]], st, 'Point'+str(i), proj)#plot points
441         
442         elif so.show == 'edg':
443             if obj.type == 'face':#we must generate the edge list
444                 edge_list = make_edge_list(obj.fce)
445             else:#we already have an edge list
446                 edge_list = obj.edg
447                         
448             for i in range(len(edge_list)):#for every edge
449                 pt_1 = vp_pts[ edge_list[i][0]-1 ] #the point at the start
450                 pt_2 = vp_pts[ edge_list[i][1]-1 ] #the point at the end
451                 
452                 draw_SVG_line((pt_1[0], pt_1[1]),
453                               (pt_2[0], pt_2[1]),
454                               st, 'Edge', proj)#plot edges
455                               
456         elif so.show == 'fce':
457             if obj.type == 'face':#we have a face list
458                 
459                 if so.cw_wound: rev = -1 #if cw wound, reverse normals
460                 else: rev = 1
461                 
462                 z_list = []
463                 
464                 for i in range(len(obj.fce)):
465                     norm = get_normal(vp_pts, obj.fce[i])#get the normal to the face
466                     norm = rev*norm / length(norm)#normalise and reverse if needed
467                     angle = acos( dot(norm, lighting) )#get the angle between the normal and the lighting vector
468                     
469                     
470                     if   so.z_sort =='max':
471                         z_sort_param  = get_max_z(vp_pts, obj.fce[i])
472                     elif so.z_sort == 'min':
473                         z_sort_param  = get_min_z(vp_pts, obj.fce[i])
474                     else:
475                         z_sort_param  = get_cent_z(vp_pts, obj.fce[i])
476                     
477                     if so.norm:#if a log of normals is required
478                         if i == 0:
479                             sys.stderr.write('Normal Vectors for each face are: \n\n')
480                         sys.stderr.write('Face '+str(i)+': ' + str(norm) + '\n')
481                     
482                     if so.back: # draw all polygons
483                         z_list.append((z_sort_param, angle, norm, i) )
484                     elif norm[2] > 0:#ignore backwards-facing faces (back face cull)
485                         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
486                 
487                 z_list.sort(lambda x, y: cmp(x[0],y[0])) #sort by ascending sort parameter of the face
488                 
489                 for i in range(len(z_list)):#for every polygon that has been sorted
490                     if so.shade:
491                         st.fill = '#' + "%02X" % floor( z_list[i][1]*so.f_r/pi ) \
492                                       + "%02X" % floor( z_list[i][1]*so.f_g/pi ) \
493                                       + "%02X" % floor( z_list[i][1]*so.f_b/pi ) #make the colour string
494                     else:
495                         st.fill = '#' + '%02X' % so.f_r + '%02X' % so.f_g + '%02X' % so.f_b #opaque
496                                       
497                     face_no = z_list[i][3]#the number of the face to draw
498                     draw_SVG_poly(vp_pts, obj.fce[ face_no ], st, 'Face:'+str(face_no), proj)
499             else:
500                 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')
501         else:
502             sys.stderr.write('Internal Error. No view type selected\n')
503         
504 e = Poly_3D()
505 e.affect()