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
71 def get_obj_data(obj, name):
72 infile = open(objfile(name))
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
82 obj.vtx = []
83 obj.edg = []
84 obj.fce = []
85 obj.name=''
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)
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 )
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 )
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
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 )
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()
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
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
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)
184 def length(vector):#return the pythagorean length of a vector
185 return sqrt(dot(vector,vector))
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
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
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
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
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
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')
338 def effect(self):
339 so = self.options
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'
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
387 obj = Obj() #create the object
388 get_obj_data(obj, file)
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'
404 trans_mat = mat(identity(3, float)) #init. trans matrix as identity matrix
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
430 vp_pts=[] #the points as projected in the z-axis onto the viewplane
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
435 lighting = [so.lv_x,-so.lv_y,so.lv_z] #direction of light vector
436 lighting = lighting/length(lighting) #normalise
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
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
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
452 draw_SVG_line((pt_1[0], pt_1[1]),
453 (pt_2[0], pt_2[1]),
454 st, 'Edge', proj)#plot edges
456 elif so.show == 'fce':
457 if obj.type == 'face':#we have a face list
459 if so.cw_wound: rev = -1 #if cw wound, reverse normals
460 else: rev = 1
462 z_list = []
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
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])
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')
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
487 z_list.sort(lambda x, y: cmp(x[0],y[0])) #sort by ascending sort parameter of the face
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
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')
504 e = Poly_3D()
505 e.affect()