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
73 def get_obj_data(obj, name):
74 infile = open(objfile(name))
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
84 obj.vtx = []
85 obj.edg = []
86 obj.fce = []
87 obj.name=''
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)
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 )
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 )
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
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 )
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()
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
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
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)
186 def length(vector):#return the pythagorean length of a vector
187 return sqrt(dot(vector,vector))
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
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
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
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
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
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')
340 def effect(self):
341 so = self.options
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'
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
389 obj = Obj() #create the object
390 get_obj_data(obj, file)
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'
406 trans_mat = mat(identity(3, float)) #init. trans matrix as identity matrix
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
432 vp_pts=[] #the points as projected in the z-axis onto the viewplane
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
437 lighting = [so.lv_x,-so.lv_y,so.lv_z] #direction of light vector
438 lighting = lighting/length(lighting) #normalise
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
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
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
454 draw_SVG_line((pt_1[0], pt_1[1]),
455 (pt_2[0], pt_2[1]),
456 st, 'Edge', proj)#plot edges
458 elif so.show == 'fce':
459 if obj.type == 'face':#we have a face list
461 if so.cw_wound: rev = -1 #if cw wound, reverse normals
462 else: rev = 1
464 z_list = []
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
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])
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')
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
489 z_list.sort(lambda x, y: cmp(x[0],y[0])) #sort by ascending sort parameter of the face
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
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')
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