Code

5391af3e6c8bd397236fb36a69c7a665f1803021
[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                 'stroke-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                     if long_angle > pi:
151                         long_angle -= 2*pi
152                     width      = so.RADIUS * cos(long_angle)
153                     height     = so.RADIUS * sin(long_angle) * sin(so.TILT)       #the rise is scaled by the sine of the tilt
154                     # length     = sqrt(width*width+height*height)  #by pythagorean theorem
155                     # inverse    = sin(acos(length/so.RADIUS))
156                     inverse    = abs(sin(long_angle)) * cos(so.TILT)
157                     
158                     minorRad   = so.RADIUS * inverse
159                     minorRad=minorRad + EPSILON
160                     
161                     #calculate the rotation of the ellipse to get it to pass through the pole (in degrees)
162                     rotation = atan(height/width)*(180.0/pi)
163                     transform = "rotate("+str(rotation)+')' #generate the transform string
164                     #the rotation will be applied about the group centre (the centre of the sphere)
165                     
166                     # remove the hidden side of the ellipses if required
167                     # this is always exactly half the ellipse, but we need to find out which half
168                     start_end = (0, 2*pi)   #Default start and end angles -> full ellipse
169                     if so.HIDE_BACK:
170                         if long_angle <= pi/2:           #cut out the half ellispse that is hidden
171                             start_end = (pi/2, 3*pi/2)
172                         else:
173                             start_end = (3*pi/2, pi/2)
174                     
175                     #finally, draw the line of longitude
176                     #the centre is always at the centre of the sphere
177                     draw_SVG_ellipse( ( minorRad, so.RADIUS ), (0,0), grp_long , start_end,transform)
178                 
179             # LINES OF LATITUDE
180             if so.NUM_LAT > 0:
181             
182                 #GROUP FOR THE LINES OF LATITUDE
183                 grp_name = 'Lines of Latitude'
184                 grp_attribs = {inkex.addNS('label','inkscape'):grp_name}
185                 grp_lat = inkex.etree.SubElement(grp, 'g', grp_attribs)
186                 
187                 
188                 so.NUM_LAT = so.NUM_LAT + 1     #Account for the fact that we loop over N-1 elements
189                 delta_lat = 180.0/so.NUM_LAT    #Angle between the line of latitude (subtended at the centre)
190                 
191                 for i in range(1,so.NUM_LAT):
192                     lat_angle=((delta_lat*i)*(pi/180))            #The angle of this line of latitude (from a pole)
193                     
194                     majorRad=so.RADIUS*sin(lat_angle)                 #The width of the LoLat (no change due to projection)
195                     minorRad=so.RADIUS*sin(lat_angle) * sin(so.TILT)     #The projected height of the line of latitude
196                     minorRad=minorRad + EPSILON
197                     
198                     cy=so.RADIUS*cos(lat_angle) * cos(so.TILT) #The projected y position of the LoLat
199                     cx=0                                    #The x position is just the center of the sphere
200                     
201                     if so.HIDE_BACK:
202                         if lat_angle > so.TILT:                     #this LoLat is partially or fully visible
203                             if lat_angle > pi-so.TILT:               #this LoLat is fully visible
204                                 draw_SVG_ellipse((majorRad, minorRad), (cx,cy), grp_lat)
205                             else: #this LoLat is partially visible
206                                 proportion = -(acos( tan(lat_angle - pi/2)/tan(pi/2 - so.TILT)) )/pi + 1
207                                 start_end = ( pi/2 - proportion*pi, pi/2 + proportion*pi ) #make the start and end angles (mirror image around pi/2)
208                                 draw_SVG_ellipse((majorRad, minorRad), (cx,cy), grp_lat, start_end)
209                             
210                     else: #just draw the full lines of latitude
211                         draw_SVG_ellipse((majorRad, minorRad), (cx,cy), grp_lat)
212             
213         
214             #THE HORIZON CIRCLE
215             draw_SVG_ellipse((so.RADIUS, so.RADIUS), (0,0), grp) #circle, centred on the sphere centre
216             
217 if __name__ == '__main__':
218     e = Wireframe_Sphere()
219     e.affect()
221 # vim: expandtab shiftwidth=4 tabstop=8 softtabstop=4 encoding=utf-8 textwidth=99