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.
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
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
82 }
83 circ = inkex.etree.SubElement(parent, inkex.addNS('path','svg'), circ_attribs )
85 class Wireframe_Sphere(inkex.Effect):
86 def __init__(self):
87 inkex.Effect.__init__(self)
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)
109 def effect(self):
111 so = self.options
113 #PARAMETER PROCESSING
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
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
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
137 #LINES OF LONGITUDE
139 if so.NUM_LONG > 0: #only process longitudes if we actually want some
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)
146 delta_long = 360.0/so.NUM_LONG #angle between neighbouring lines of longitude in degrees
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))
155 minorRad = so.RADIUS * inverse
156 minorRad=minorRad + EPSILON
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)
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)
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)
176 # LINES OF LATITUDE
177 if so.NUM_LAT > 0:
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)
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)
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)
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
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
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
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)
209 else: #just draw the full lines of latitude
210 draw_SVG_ellipse((majorRad, minorRad), (cx,cy), grp_lat)
213 #THE HORIZON CIRCLE
214 draw_SVG_ellipse((so.RADIUS, so.RADIUS), (0,0), grp) #circle, centred on the sphere centre
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