Code

Mnemonics in "Fill and stroke", "Align and distribute", and "Transform" dialogs ...
[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 automatically.
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 between max, min and average z-value of the vertices
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, 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 it 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 #FILE IO ROUTINES
64 def get_filename(self_options):
65         if self_options.obj == 'from_file':
66             file = self_options.spec_file
67         else:
68             file = self_options.obj + '.obj'
69             
70         return file
72 def objfile(name):
73     import os.path
74     if __name__ == '__main__':
75         filename = sys.argv[0]
76     else:
77         filename = __file__
78     path = os.path.abspath(os.path.dirname(filename))
79     path = os.path.join(path, 'Poly3DObjects', name)
80     return path
81     
82 def get_obj_data(obj, name):
83     infile = open(objfile(name))
84     
85     #regular expressions
86     getname = '(.[nN]ame:\\s*)(.*)'
87     floating = '([\-\+\\d*\.e]*)'   #a possibly non-integer number, with +/- and exponent.
88     getvertex = '(v\\s+)'+floating+'\\s+'+floating+'\\s+'+floating
89     getedgeline = '(l\\s+)(.*)'
90     getfaceline = '(f\\s+)(.*)'
91     getnextint = '(\\d+)([/\\d]*)(.*)'#we need to deal with 123\343\123 or 123\\456 as equivalent to 123 (we are ignoring the other options in the obj file)
92     
93     for line in infile:
94         if line[0]=='#':                    #we have a comment line
95             m = re.search(getname, line)        #check to see if this line contains a name
96             if m:
97                 obj.name = m.group(2)           #if it does, set the property
98         elif line[0] == 'v':                #we have a vertex (maybe)
99             m = re.search(getvertex, line)      #check to see if this line contains a valid vertex
100             if m:                               #we have a valid vertex
101                 obj.vtx.append( [float(m.group(2)), float(m.group(3)), float(m.group(4)) ] )
102         elif line[0] == 'l':                #we have a line (maybe)
103             m = re.search(getedgeline, line)    #check to see if this line begins 'l '
104             if m:                               #we have a line beginning 'l '
105                 vtxlist = []    #buffer
106                 while line:
107                     m2 = re.search(getnextint, line)
108                     if m2:
109                         vtxlist.append( int(m2.group(1)) )
110                         line = m2.group(3)#remainder
111                     else:
112                         line = None
113                 if len(vtxlist) > 1:#we need at least 2 vertices to make an edge
114                     for i in range (len(vtxlist)-1):#we can have more than one vertex per line - get adjacent pairs
115                         obj.edg.append( ( vtxlist[i], vtxlist[i+1] ) )#get the vertex pair between that vertex and the next
116         elif line[0] == 'f':                #we have a face (maybe)
117             m = re.search(getfaceline, line)
118             if m:                               #we have a line beginning 'f '
119                 vtxlist = []#buffer
120                 while line:
121                     m2 = re.search(getnextint, line)
122                     if m2:
123                         vtxlist.append( int(m2.group(1)) )
124                         line = m2.group(3)#remainder
125                     else:
126                         line = None
127                 if len(vtxlist) > 2:            #we need at least 3 vertices to make an edge
128                     obj.fce.append(vtxlist)
129     
130     if obj.name == '':#no name was found, use filename, without extension (.obj)
131         obj.name = name[0:-4]
133 #RENDERING AND SVG OUTPUT FUNCTIONS
135 def draw_SVG_dot((cx, cy), st, name, parent):
136     style = { 'stroke': '#000000', 'stroke-width':str(st.th), 'fill': st.fill, 'stroke-opacity':st.s_opac, 'fill-opacity':st.f_opac}
137     circ_attribs = {'style':simplestyle.formatStyle(style),
138                     inkex.addNS('label','inkscape'):name,
139                     'r':str(st.r),
140                     'cx':str(cx), 'cy':str(-cy)}
141     inkex.etree.SubElement(parent, inkex.addNS('circle','svg'), circ_attribs )
142     
143 def draw_SVG_line((x1, y1),(x2, y2), st, name, parent):
144     style = { 'stroke': '#000000', 'stroke-width':str(st.th), 'stroke-linecap':st.linecap}
145     line_attribs = {'style':simplestyle.formatStyle(style),
146                     inkex.addNS('label','inkscape'):name,
147                     'd':'M '+str(x1)+','+str(-y1)+' L '+str(x2)+','+str(-y2)}
148     inkex.etree.SubElement(parent, inkex.addNS('path','svg'), line_attribs )
149     
150 def draw_SVG_poly(pts, face, st, name, parent):
151     style = { 'stroke': '#000000', 'stroke-width':str(st.th), 'stroke-linejoin':st.linejoin, \
152               'stroke-opacity':st.s_opac, 'fill': st.fill, 'fill-opacity':st.f_opac}   
153     for i in range(len(face)):
154         if i == 0:#for first point
155             d = 'M'#move to
156         else:
157             d = d + 'L'#line to
158         d = d+ str(pts[face[i]-1][0]) + ',' + str(-pts[face[i]-1][1])#add point
159     d = d + 'z' #close the polygon
160     
161     line_attribs = {'style':simplestyle.formatStyle(style),
162                     inkex.addNS('label','inkscape'):name,'d': d}
163     inkex.etree.SubElement(parent, inkex.addNS('path','svg'), line_attribs )
164     
165 def draw_edges( edge_list, pts, st, parent ):
166     for edge in edge_list:#for every edge
167         pt_1 = pts[ edge[0]-1 ][0:2] #the point at the start
168         pt_2 = pts[ edge[1]-1 ][0:2] #the point at the end
169         name = 'Edge'+str(edge[0])+'-'+str(edge[1])
170         draw_SVG_line(pt_1,pt_2,st, name, parent)#plot edges
171                               
172 def draw_faces( faces_data, pts, obj, shading, fill_col,st, parent):          
173     for face in faces_data:#for every polygon that has been sorted
174         if shading:
175             st.fill = get_darkened_colour(fill_col, face[1]/pi)#darken proportionally to angle to lighting vector
176         else:
177             st.fill = get_darkened_colour(fill_col, 1)#do not darken colour
178                           
179         face_no = face[3]#the number of the face to draw
180         draw_SVG_poly(pts, obj.fce[ face_no ], st, 'Face:'+str(face_no), parent)
182 def get_darkened_colour( (r,g,b), factor):
183 #return a hex triplet of colour, reduced in lightness proportionally to a value between 0 and 1
184     return  '#' + "%02X" % floor( factor*r ) \
185                 + "%02X" % floor( factor*g ) \
186                 + "%02X" % floor( factor*b ) #make the colour string
188 def make_rotation_log(options):
189 #makes a string recording the axes and angles of each roation, so an object can be repeated
190     return   options.r1_ax+str('%.2f'%options.r1_ang)+':'+\
191              options.r2_ax+str('%.2f'%options.r2_ang)+':'+\
192              options.r3_ax+str('%.2f'%options.r3_ang)+':'+\
193              options.r1_ax+str('%.2f'%options.r4_ang)+':'+\
194              options.r2_ax+str('%.2f'%options.r5_ang)+':'+\
195              options.r3_ax+str('%.2f'%options.r6_ang)
197 #MATHEMATICAL FUNCTIONS
198 def get_angle( vector1, vector2 ): #returns the angle between two vectors
199     return acos( dot(vector1, vector2) )
201 def length(vector):#return the pythagorean length of a vector
202     return sqrt(dot(vector,vector))
204 def normalise(vector):#return the unit vector pointing in the same direction as the argument
205     return vector / length(vector)
207 def get_normal( pts, face): #returns the normal vector for the plane passing though the first three elements of face of pts
208     #n = pt[0]->pt[1] x pt[0]->pt[3]
209     a = (array(pts[ face[0]-1 ]) - array(pts[ face[1]-1 ]))
210     b = (array(pts[ face[0]-1 ]) - array(pts[ face[2]-1 ]))
211     return cross(a,b).flatten()
213 def get_unit_normal(pts, face, cw_wound): #returns the unit normal for the plane passing through the first three points of face, taking account of winding
214     if cw_wound:
215         winding = -1 #if it is clockwise wound, reverse the vecotr direction
216     else:
217         winding = 1 #else leave alone
218     
219     return winding*normalise(get_normal(pts, face))
221 def rotate( matrix, angle, axis ):#choose the correct rotation matrix to use
222     if   axis == 'x':
223         matrix = rot_x(matrix, angle)
224     elif axis == 'y':
225         matrix = rot_y(matrix, angle)
226     elif axis == 'z':
227         matrix = rot_z(matrix, angle)
228     return matrix
229     
230 def rot_z( matrix , a):#rotate around the z-axis by a radians
231     trans_mat = mat(array( [[ cos(a) , -sin(a) ,    0   ],
232                             [ sin(a) ,  cos(a) ,    0   ],
233                             [   0    ,    0    ,    1   ]]))
234     return trans_mat*matrix
236 def rot_y( matrix , a):#rotate around the y-axis by a radians
237     trans_mat = mat(array( [[ cos(a) ,    0    , sin(a) ],
238                             [   0    ,    1    ,    0   ],
239                             [-sin(a) ,    0    , cos(a) ]]))
240     return trans_mat*matrix
241     
242 def rot_x( matrix , a):#rotate around the x-axis by a radians
243     trans_mat = mat(array( [[   1    ,    0    ,    0   ],
244                             [   0    ,  cos(a) ,-sin(a) ],
245                             [   0    ,  sin(a) , cos(a) ]]))
246     return trans_mat*matrix
248 def get_transformed_pts( vtx_list, trans_mat):#translate the points according to the matrix
249     transformed_pts = []
250     for vtx in vtx_list:
251         transformed_pts.append((trans_mat * mat(vtx).T).T.tolist()[0] )#transform the points at add to the list
252     return transformed_pts
254 def get_max_z(pts, face): #returns the largest z_value of any point in the face
255     max_z = pts[ face[0]-1 ][2]
256     for i in range(1, len(face)):
257         if pts[ face[0]-1 ][2] >= max_z:
258             max_z = pts[ face[0]-1 ][2]
259     return max_z
260     
261 def get_min_z(pts, face): #returns the smallest z_value of any point in the face
262     min_z = pts[ face[0]-1 ][2]
263     for i in range(1, len(face)):
264         if pts[ face[i]-1 ][2] <= min_z:
265             min_z = pts[ face[i]-1 ][2]
266     return min_z
267     
268 def get_cent_z(pts, face): #returns the centroid z_value of any point in the face
269     sum = 0
270     for i in range(len(face)):
271             sum += pts[ face[i]-1 ][2]
272     return sum/len(face)
273     
274 def get_z_sort_param(pts, face, method): #returns the z-sorting parameter specified by 'method' ('max', 'min', 'cent')  
275     z_sort_param = ''
276     if  method == 'max':
277         z_sort_param  = get_max_z(pts, face)
278     elif method == 'min':
279         z_sort_param  = get_min_z(pts, face)
280     else:
281         z_sort_param  = get_cent_z(pts, face)
282     return z_sort_param
284 #OBJ DATA MANIPULATION
285 def remove_duplicates(list):#removes the duplicates from a list
286     list.sort()#sort the list
287  
288     last = list[-1]
289     for i in range(len(list)-2, -1, -1):
290         if last==list[i]:
291             del list[i]
292         else:
293             last = list[i]
294     return list
296 def make_edge_list(face_list):#make an edge vertex list from an existing face vertex list
297     edge_list = []
298     for i in range(len(face_list)):#for every face
299         edges = len(face_list[i]) #number of edges around that face
300         for j in range(edges):#for every vertex in that face
301             new_edge = [face_list[i][j], face_list[i][(j+1)%edges] ]
302             new_edge.sort() #put in ascending order of vertices (to ensure we spot duplicates)
303             edge_list.append( new_edge )#get the vertex pair between that vertex and the next
304     
305     return remove_duplicates(edge_list)
306     
307 class Style(object): #container for style information
308     def __init__(self,options):
309         self.th = options.th
310         self.fill= '#ff0000'
311         self.col = '#000000'
312         self.r = 2
313         self.f_opac = str(options.f_opac/100.0)
314         self.s_opac = str(options.s_opac/100.0)
315         self.linecap = 'round'
316         self.linejoin = 'round'
318 class Obj(object): #a 3d object defined by the vertices and the faces (eg a polyhedron)
319 #edges can be generated from this information
320     def __init__(self):
321         self.vtx = []
322         self.edg = []
323         self.fce = []
324         self.name=''
325         
326     def set_type(self, options):
327         if options.type == 'face':
328             if self.fce != []:
329                 self.type = 'face'
330             else:
331                 inkex.errormsg(_('No face data found in specified file.'))
332                 inkex.errormsg(_('Try selecting "Edge Specified" in the Model File tab.\n'))
333                 self.type = 'error'
334         else:
335             if self.edg != []:
336                 self.type = 'edge'
337             else:
338                 inkex.errormsg(_('No edge data found in specified file.'))
339                 inkex.errormsg(_('Try selecting "Face Specified" in the Model File tab.\n'))
340                 self.type = 'error'
342 class Poly_3D(inkex.Effect):
343     def __init__(self):
344         inkex.Effect.__init__(self)
345         self.OptionParser.add_option("--tab",
346             action="store", type="string", 
347             dest="tab", default="object") 
349 #MODEL FILE SETTINGS
350         self.OptionParser.add_option("--obj",
351             action="store", type="string", 
352             dest="obj", default='cube')
353         self.OptionParser.add_option("--spec_file",
354             action="store", type="string", 
355             dest="spec_file", default='great_rhombicuboct.obj')
356         self.OptionParser.add_option("--cw_wound",
357             action="store", type="inkbool", 
358             dest="cw_wound", default='true')
359         self.OptionParser.add_option("--type",
360             action="store", type="string", 
361             dest="type", default='face')
362 #VEIW SETTINGS
363         self.OptionParser.add_option("--r1_ax",
364             action="store", type="string", 
365             dest="r1_ax", default=0)
366         self.OptionParser.add_option("--r2_ax",
367             action="store", type="string", 
368             dest="r2_ax", default=0)
369         self.OptionParser.add_option("--r3_ax",
370             action="store", type="string", 
371             dest="r3_ax", default=0)
372         self.OptionParser.add_option("--r4_ax",
373             action="store", type="string", 
374             dest="r4_ax", default=0)
375         self.OptionParser.add_option("--r5_ax",
376             action="store", type="string", 
377             dest="r5_ax", default=0)
378         self.OptionParser.add_option("--r6_ax",
379             action="store", type="string", 
380             dest="r6_ax", default=0)
381         self.OptionParser.add_option("--r1_ang",
382             action="store", type="float", 
383             dest="r1_ang", default=0)
384         self.OptionParser.add_option("--r2_ang",
385             action="store", type="float", 
386             dest="r2_ang", default=0)
387         self.OptionParser.add_option("--r3_ang",
388             action="store", type="float", 
389             dest="r3_ang", default=0)
390         self.OptionParser.add_option("--r4_ang",
391             action="store", type="float", 
392             dest="r4_ang", default=0)
393         self.OptionParser.add_option("--r5_ang",
394             action="store", type="float", 
395             dest="r5_ang", default=0)
396         self.OptionParser.add_option("--r6_ang",
397             action="store", type="float", 
398             dest="r6_ang", default=0)
399         self.OptionParser.add_option("--scl",
400             action="store", type="float", 
401             dest="scl", default=100.0)
402 #STYLE SETTINGS
403         self.OptionParser.add_option("--show",
404             action="store", type="string", 
405             dest="show", default='faces')
406         self.OptionParser.add_option("--shade",
407             action="store", type="inkbool", 
408             dest="shade", default='true')
409         self.OptionParser.add_option("--f_r",
410             action="store", type="int", 
411             dest="f_r", default=255)
412         self.OptionParser.add_option("--f_g",
413             action="store", type="int", 
414             dest="f_g", default=0)
415         self.OptionParser.add_option("--f_b",
416             action="store", type="int", 
417             dest="f_b", default=0)
418         self.OptionParser.add_option("--f_opac",
419             action="store", type="int", 
420             dest="f_opac", default=100)
421         self.OptionParser.add_option("--s_opac",
422             action="store", type="int", 
423             dest="s_opac", default=100)
424         self.OptionParser.add_option("--th",
425             action="store", type="float", 
426             dest="th", default=2)
427         self.OptionParser.add_option("--lv_x",
428             action="store", type="float", 
429             dest="lv_x", default=1)
430         self.OptionParser.add_option("--lv_y",
431             action="store", type="float", 
432             dest="lv_y", default=1)
433         self.OptionParser.add_option("--lv_z",
434             action="store", type="float", 
435             dest="lv_z", default=-2)
436         self.OptionParser.add_option("--back",
437             action="store", type="inkbool", 
438             dest="back", default='false')
439         self.OptionParser.add_option("--norm",
440             action="store", type="inkbool", 
441             dest="norm", default='true')
442         self.OptionParser.add_option("--z_sort",
443             action="store", type="string", 
444             dest="z_sort", default='min')
445             
446             
447     def effect(self):
448         so = self.options#shorthand
449         
450         #INITIALISE AND LOAD DATA
451         
452         obj = Obj() #create the object
453         file = get_filename(so)#get the file to load data from
454         get_obj_data(obj, file)#load data from the obj file
455         obj.set_type(so)#set the type (face or edge) as per the settings
456         
457         st = Style(so) #initialise style
458         fill_col = (so.f_r, so.f_g, so.f_b) #colour tuple for the face fill
459         lighting = normalise( (so.lv_x,-so.lv_y,so.lv_z) ) #unit light vector
460         
461         #INKSCAPE GROUP TO CONTAIN THE POLYHEDRON
462         
463         #Put in in the centre of the current view
464         poly_transform = 'translate(' + str( self.view_center[0]) + ',' + str( self.view_center[1]) + ')'
465         #we will put all the rotations in the object name, so it can be repeated in 
466         poly_name = obj.name+':'+make_rotation_log(so)
467         poly_attribs = {inkex.addNS('label','inkscape'):poly_name,
468                         'transform':poly_transform }
469         poly = inkex.etree.SubElement(self.current_layer, 'g', poly_attribs)#the group to put everything in
470         
471         #TRANFORMATION OF THE OBJECT (ROTATION, SCALE, ETC)
472         
473         trans_mat = mat(identity(3, float)) #init. trans matrix as identity matrix
474         for i in range(1, 7):#for each rotation
475             axis  = eval('so.r'+str(i)+'_ax')
476             angle = eval('so.r'+str(i)+'_ang') *pi/180
477             trans_mat = rotate(trans_mat, angle, axis)
478         trans_mat = trans_mat*so.scl #scale by linear factor (do this only after the transforms to reduce round-off)
479         
480         transformed_pts = get_transformed_pts(obj.vtx, trans_mat) #the points as projected in the z-axis onto the viewplane
481         
482         #RENDERING OF THE OBJECT
483         
484         if so.show == 'vtx':
485             for i in range(len(transformed_pts)):
486                 draw_SVG_dot([transformed_pts[i][0],transformed_pts[i][1]], st, 'Point'+str(i), poly)#plot points using transformed_pts x and y coords
487         
488         elif so.show == 'edg':
489             if obj.type == 'face':#we must generate the edge list from the faces
490                 edge_list = make_edge_list(obj.fce)
491             else:#we already have an edge list
492                 edge_list = obj.edg
493                         
494             draw_edges( edge_list, transformed_pts, st, poly)
495                               
496         elif so.show == 'fce':
497             if obj.type == 'face':#we have a face list
498                
499                 z_list = []
500                 
501                 for i in range(len(obj.fce)):
502                     face = obj.fce[i] #the face we are dealing with
503                     norm = get_unit_normal(transformed_pts, face, so.cw_wound) #get the normal vector to the face
504                     angle = get_angle( norm, lighting )#get the angle between the normal and the lighting vector
505                     z_sort_param = get_z_sort_param(transformed_pts, face, so.z_sort)
506                     
507                     if so.back or norm[2] > 0: # include all polygons or just the front-facing ones as needed
508                         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
509                 
510                 z_list.sort(lambda x, y: cmp(x[0],y[0])) #sort by ascending sort parameter of the face
511                 draw_faces( z_list, transformed_pts, obj, so.shade, fill_col, st, poly)
513             else:#we cannot generate a list of faces from the edges without a lot of computation
514                 inkex.errormsg(_('Face Data Not Found. Ensure file contains face data, and check the file is imported as "Face-Specified" under the "Model File" tab.\n'))
515         else:
516             inkex.errormsg(_('Internal Error. No view type selected\n'))
517         
518 if __name__ == '__main__':
519     e = Poly_3D()
520     e.affect()
523 # vim: expandtab shiftwidth=4 tabstop=8 softtabstop=4 fileencoding=utf-8 textwidth=99