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