1#!/usr/local/bin/python3.8 2# coding=utf-8 3# 4# Copyright (C) 2007 Joel Holdsworth joel@airwebreathe.org.uk 5# 6# This program is free software; you can redistribute it and/or modify 7# it under the terms of the GNU General Public License as published by 8# the Free Software Foundation; either version 2 of the License, or 9# (at your option) any later version. 10# 11# This program is distributed in the hope that it will be useful, 12# but WITHOUT ANY WARRANTY; without even the implied warranty of 13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14# GNU General Public License for more details. 15# 16# You should have received a copy of the GNU General Public License 17# along with this program; if not, write to the Free Software 18# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19# 20 21import math 22import inkex 23 24class Spirograph(inkex.EffectExtension): 25 def add_arguments(self, pars): 26 pars.add_argument("--primaryr", type=float, default=60.0, 27 help="The radius of the outer gear") 28 pars.add_argument("--secondaryr", type=float, default=100.0, 29 help="The radius of the inner gear") 30 pars.add_argument("--penr", type=float, default=50.0, 31 help="The distance of the pen from the inner gear") 32 pars.add_argument("--gearplacement", default="inside", 33 help="Selects whether the gear is inside or outside the ring") 34 pars.add_argument("--rotation", type=float, default=0.0, 35 help="The number of degrees to rotate the image by") 36 pars.add_argument("--quality", type=int, default=16, 37 help="The quality of the calculated output") 38 39 def effect(self): 40 self.options.primaryr = self.svg.unittouu(str(self.options.primaryr) + 'px') 41 self.options.secondaryr = self.svg.unittouu(str(self.options.secondaryr) + 'px') 42 self.options.penr = self.svg.unittouu(str(self.options.penr) + 'px') 43 44 if self.options.secondaryr == 0: 45 return 46 if self.options.quality == 0: 47 return 48 49 if self.options.gearplacement.strip(' ').lower().startswith('outside'): 50 a = self.options.primaryr + self.options.secondaryr 51 flip = -1 52 else: 53 a = self.options.primaryr - self.options.secondaryr 54 flip = 1 55 56 ratio = a / self.options.secondaryr 57 if ratio == 0: 58 return 59 scale = 2 * math.pi / (ratio * self.options.quality) 60 61 rotation = - math.pi * self.options.rotation / 180 62 63 new = inkex.PathElement() 64 new.style = inkex.Style(stroke='#000000', fill='none', stroke_width='1.0') 65 66 path_string = '' 67 maxPointCount = 1000 68 69 for i in range(maxPointCount): 70 71 theta = i * scale 72 73 view_center = self.svg.namedview.center 74 x = a * math.cos(theta + rotation) + \ 75 self.options.penr * math.cos(ratio * theta + rotation) * flip + \ 76 view_center[0] 77 y = a * math.sin(theta + rotation) - \ 78 self.options.penr * math.sin(ratio * theta + rotation) + \ 79 view_center[1] 80 81 dx = (-a * math.sin(theta + rotation) - ratio * self.options.penr * math.sin(ratio * theta + rotation) * flip) * scale / 3 82 dy = (a * math.cos(theta + rotation) - ratio * self.options.penr * math.cos(ratio * theta + rotation)) * scale / 3 83 84 if i <= 0: 85 path_string += 'M {},{} C {},{} '.format(str(x), str(y), str(x + dx), str(y + dy)) 86 else: 87 path_string += '{},{} {},{}'.format(str(x - dx), str(y - dy), str(x), str(y)) 88 89 if math.fmod(i / ratio, self.options.quality) == 0 and i % self.options.quality == 0: 90 path_string += 'Z' 91 break 92 else: 93 if i == maxPointCount - 1: 94 pass # we reached the allowed maximum of points, stop here 95 else: 96 path_string += ' C {},{} '.format(str(x + dx), str(y + dy)) 97 98 new.path = path_string 99 self.svg.get_current_layer().append(new) 100 101if __name__ == '__main__': 102 Spirograph().run() 103