Code

Wireframe Spheres extension by inductiveload
[inkscape.git] / share / extensions / wireframe_sphere.py
1 #!/usr/bin/env python 
2 # -*- coding: UTF-8 -*-
3 '''
4 Copyright (C) 2009 John Beard john.j.beard@gmail.com
6 ######DESCRIPTION######
8 This extension renders a wireframe sphere constructed from lines of latitude
9 and lines of longitude.
11 The number of lines of latitude and longitude is independently variable. Lines 
12 of latitude and longtude are in separate subgroups. The whole figure is also in
13 its own group.
15 The whole sphere can be tilted towards or away from the veiwer by a given 
16 number of degrees. If the whole sphere is then rotated normally in Inkscape,
17 any position can be acheived.
19 There is an option to hide the lines at the back of the sphere, as if the 
20 sphere were opaque.
21     #FIXME: Lines of latitude only have an approximation of the function needed
22             to hide the back portion. If you can derive the proper equation,
23             please add it in.
24             Line of longitude have the exact method already.
25             Workaround: Use the Inkscape ellipse tool to edit the start and end
26             points of the lines of latitude to end at the horizon circle.
27             
28            
29 #TODO:  Add support for odd numbers of lines of longitude. This means breaking
30         the line at the poles, and having two half ellipses for each line.
31         The angles at which the ellipse arcs pass the poles are not constant and
32         need to be derived before this can be implemented.
33 #TODO:  Add support for prolate and oblate spheroids
35 ######LICENCE#######
36 This program is free software; you can redistribute it and/or modify
37 it under the terms of the GNU General Public License as published by
38 the Free Software Foundation; either version 2 of the License, or
39 (at your option) any later version.
41 This program is distributed in the hope that it will be useful,
42 but WITHOUT ANY WARRANTY; without even the implied warranty of
43 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
44 GNU General Public License for more details.
46 You should have received a copy of the GNU General Public License
47 along with this program; if not, write to the Free Software
48 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
50 ######VERSION HISTORY#####
51     Ver.       Date                       Notes
52     
53     0.10    2009-10-25  First version. Basic spheres supported.
54                         Hidden lines of latitude still not properly calculated.
55                         Prolate and oblate spheroids not considered.
56 '''
58 import inkex, simplestyle
60 import gettext
61 _ = gettext.gettext
63 from math import *
65 #SVG OUTPUT FUNCTIONS ================================================
66 def draw_SVG_ellipse((rx, ry), (cx, cy), parent, start_end=(0,2*pi),transform='' ):
68     style = {   'stroke'        : '#000000',
69                 'width'         : '1',
70                 'fill'          : 'none'            }
71     circ_attribs = {'style':simplestyle.formatStyle(style),
72         inkex.addNS('cx','sodipodi')        :str(cx),
73         inkex.addNS('cy','sodipodi')        :str(cy),
74         inkex.addNS('rx','sodipodi')        :str(rx),
75         inkex.addNS('ry','sodipodi')        :str(ry),
76         inkex.addNS('start','sodipodi')     :str(start_end[0]),
77         inkex.addNS('end','sodipodi')       :str(start_end[1]),
78         inkex.addNS('open','sodipodi')      :'true',    #all ellipse sectors we will draw are open
79         inkex.addNS('type','sodipodi')      :'arc',
80         'transform'                         :transform
81         
82             }
83     circ = inkex.etree.SubElement(parent, inkex.addNS('path','svg'), circ_attribs )
84     
85 class Wireframe_Sphere(inkex.Effect):
86     def __init__(self):
87         inkex.Effect.__init__(self)
88         
89         #PARSE OPTIONS
90         self.OptionParser.add_option("--num_lat",
91             action="store", type="int",
92             dest="NUM_LAT", default=19)
93         self.OptionParser.add_option("--num_long",
94             action="store", type="int",
95             dest="NUM_LONG", default=24)
96         self.OptionParser.add_option("--radius",
97             action="store", type="float", 
98             dest="RADIUS", default=100.0)
99         self.OptionParser.add_option("--tilt",
100             action="store", type="float",
101             dest="TILT", default=35.0)
102         self.OptionParser.add_option("--rotation",
103             action="store", type="float",
104             dest="ROT_OFFSET", default=4)
105         self.OptionParser.add_option("--hide_back",
106             action="store", type="inkbool", 
107             dest="HIDE_BACK", default=False)
108             
109     def effect(self):
110         
111         so = self.options
112         
113         #PARAMETER PROCESSING
114         
115         if so.NUM_LONG % 2 != 0: #lines of longitude are odd : abort
116             inkex.errormsg(_('Please enter an even number of lines of longitude.'))
117         else:
118             if so.TILT < 0:            # if the tilt is backwards
119                 flip = ' scale(1, -1)' # apply a vertical flip to the whole sphere
120             else:
121                 flip = '' #no flip
123             so.TILT       =  abs(so.TILT)*(pi/180)  #Convert to radians
124             so.ROT_OFFSET = so.ROT_OFFSET*(pi/180)  #Convert to radians
125             
126             EPSILON = 0.001 #add a tiny value to the ellipse radii, so that if we get a zero radius, the ellipse still shows up as a line
128             #INKSCAPE GROUP TO CONTAIN EVERYTHING
129             
130             centre = self.view_center   #Put in in the centre of the current view
131             grp_transform = 'translate' + str( centre ) + flip
132             grp_name = 'WireframeSphere'
133             grp_attribs = {inkex.addNS('label','inkscape'):grp_name,
134                            'transform':grp_transform }
135             grp = inkex.etree.SubElement(self.current_layer, 'g', grp_attribs)#the group to put everything in
136             
137             #LINES OF LONGITUDE
138             
139             if so.NUM_LONG > 0:      #only process longitudes if we actually want some
140                 
141                 #GROUP FOR THE LINES OF LONGITUDE
142                 grp_name = 'Lines of Longitude'
143                 grp_attribs = {inkex.addNS('label','inkscape'):grp_name}
144                 grp_long = inkex.etree.SubElement(grp, 'g', grp_attribs)
145                 
146                 delta_long = 360.0/so.NUM_LONG      #angle between neighbouring lines of longitude in degrees
147                 
148                 for i in range(0,so.NUM_LONG/2):
149                     long_angle = so.ROT_OFFSET + (i*delta_long)*(pi/180.0); #The longitude of this particular line in radians
150                     width      = so.RADIUS * cos(long_angle)
151                     height     = so.RADIUS * sin(long_angle) * sin(so.TILT)       #the rise is scaled by the sine of the tilt
152                     length     = sqrt(width*width+height*height)  #by pythagorean theorem
153                     inverse    = sin(acos(length/so.RADIUS))
154                     
155                     minorRad   = so.RADIUS * inverse
156                     minorRad=minorRad + EPSILON
157                     
158                     #calculate the rotation of the ellipse to get it to pass through the pole (in degrees)
159                     rotation = atan(height/width)*(180.0/pi)
160                     transform = "rotate("+str(rotation)+')' #generate the transform string
161                     #the rotation will be applied about the group centre (the centre of the sphere)
162                     
163                     # remove the hidden side of the ellipses if required
164                     # this is always exactly half the ellipse, but we need to find out which half
165                     start_end = (0, 2*pi)   #Default start and end angles -> full ellipse
166                     if so.HIDE_BACK:
167                         if long_angle <= pi/2:           #cut out the half ellispse that is hidden
168                             start_end = (pi/2, 3*pi/2)
169                         else:
170                             start_end = (3*pi/2, pi/2)
171                     
172                     #finally, draw the line of longitude
173                     #the centre is always at the centre of the sphere
174                     draw_SVG_ellipse( ( minorRad, so.RADIUS ), (0,0), grp_long , start_end,transform)
175                 
176             # LINES OF LATITUDE
177             if so.NUM_LAT > 0:
178             
179                 #GROUP FOR THE LINES OF LATITUDE
180                 grp_name = 'Lines of Latitude'
181                 grp_attribs = {inkex.addNS('label','inkscape'):grp_name}
182                 grp_lat = inkex.etree.SubElement(grp, 'g', grp_attribs)
183                 
184                 
185                 so.NUM_LAT = so.NUM_LAT + 1     #Account for the fact that we loop over N-1 elements
186                 delta_lat = 180.0/so.NUM_LAT    #Angle between the line of latitude (subtended at the centre)
187                 
188                 for i in range(1,so.NUM_LAT):
189                     lat_angle=((delta_lat*i)*(pi/180))            #The angle of this line of latitude (from a pole)
190                     
191                     majorRad=so.RADIUS*sin(lat_angle)                 #The width of the LoLat (no change due to projection)
192                     minorRad=so.RADIUS*sin(lat_angle) * sin(so.TILT)     #The projected height of the line of latitude
193                     minorRad=minorRad + EPSILON
194                     
195                     cy=so.RADIUS*cos(lat_angle) * cos(so.TILT) #The projected y position of the LoLat
196                     cx=0                                    #The x position is just the center of the sphere
197                     
198                     if so.HIDE_BACK:
199                         if lat_angle > so.TILT:                     #this LoLat is partially or fully visible
200                             if lat_angle > pi-so.TILT:               #this LoLat is fully visible
201                                 draw_SVG_ellipse((majorRad, minorRad), (cx,cy), grp_lat)
202                             else: #this LoLat is partially visible
203                                 
204                                 proportion = -(acos( (lat_angle - pi/2)/(pi/2 - so.TILT)) )/pi + 1 #this is a dirty hacky approximation
205                                 #FIXME: if you can work out the right way to do this, please do it
206                                 start_end = ( pi/2 - proportion*pi, pi/2 + proportion*pi ) #make the start and end angles (mirror image around pi/2)
207                                 draw_SVG_ellipse((majorRad, minorRad), (cx,cy), grp_lat, start_end)
208                             
209                     else: #just draw the full lines of latitude
210                         draw_SVG_ellipse((majorRad, minorRad), (cx,cy), grp_lat)
211             
212         
213             #THE HORIZON CIRCLE
214             draw_SVG_ellipse((so.RADIUS, so.RADIUS), (0,0), grp) #circle, centred on the sphere centre
215             
216 if __name__ == '__main__':
217     e = Wireframe_Sphere()
218     e.affect()
220 # vim: expandtab shiftwidth=4 tabstop=8 softtabstop=4 encoding=utf-8 textwidth=99