1#!/usr/local/bin/python3.8
2# coding=utf-8
3#
4# Copyright (C) 2007 John Beard john.j.beard@gmail.com
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"""
21This extension allows you to draw various triangle constructions
22It requires a path to be selected
23It will use the first three nodes of this path
24
25Dimensions of a triangle__
26
27       /`__
28      / a_c``--__
29     /           ``--__ s_a
30s_b /                  ``--__
31   /a_a                    a_b`--__
32  /--------------------------------``B
33 A              s_b
34"""
35
36from math import acos, cos, pi, sin, sqrt, tan
37
38import inkex
39from inkex import PathElement, Circle
40
41(X, Y) = range(2)
42
43# DRAWING ROUTINES
44
45# draw an SVG triangle given in trilinar coords
46def draw_SVG_circle(rad, centre, params, style, name, parent):  # draw an SVG circle with a given radius as trilinear coordinates
47    if rad == 0:  # we want a dot
48        r = style.d_rad  # get the dot width from the style
49        circ_style = {'stroke': style.d_col, 'stroke-width': str(style.d_th), 'fill': style.d_fill}
50    else:
51        r = rad  # use given value
52        circ_style = {'stroke': style.c_col, 'stroke-width': str(style.c_th), 'fill': style.c_fill}
53
54    cx, cy = get_cartesian_pt(centre, params)
55    circ_attribs = {'cx': str(cx), 'cy': str(cy), 'r': str(r)}
56    elem = parent.add(Circle(**circ_attribs))
57    elem.style = circ_style
58    elem.label = name
59
60
61# draw an SVG triangle given in trilinar coords
62def draw_SVG_tri(vert_mat, params, style, name, parent):
63    p1, p2, p3 = get_cartesian_tri(vert_mat, params)  # get the vertex matrix in cartesian points
64    elem = parent.add(PathElement())
65    elem.path = 'M ' + str(p1[0]) + ',' + str(p1[1]) +\
66                ' L ' + str(p2[0]) + ',' + str(p2[1]) +\
67                ' L ' + str(p3[0]) + ',' + str(p3[1]) +\
68                ' L ' + str(p1[0]) + ',' + str(p1[1]) + ' z'
69    elem.style = {'stroke': style.l_col, 'stroke-width': str(style.l_th), 'fill': style.l_fill}
70    elem.label = name
71
72
73# draw an SVG line segment between the given (raw) points
74def draw_SVG_line(a, b, style, name, parent):
75    (x1, y1) = a
76    (x2, y2) = b
77    line = parent.add(PathElement())
78    line.style = {'stroke': style.l_col, 'stroke-width': str(style.l_th), 'fill': style.l_fill}
79    line.path = 'M ' + str(x1) + ',' + str(y1) + ' L ' + str(x2) + ',' + str(y2)
80    line.lavel = name
81
82
83# lines from each vertex to a corresponding point in trilinears
84def draw_vertex_lines(vert_mat, params, width, name, parent):
85    for i in range(3):
86        oppositepoint = get_cartesian_pt(vert_mat[i], params)
87        draw_SVG_line(params[3][-i % 3], oppositepoint, width, name + ':' + str(i), parent)
88
89
90# MATHEMATICAL ROUTINES
91
92def distance(a, b):
93    """find the pythagorean distance"""
94    (x0, y0) = a
95    (x1, y1) = b
96    return sqrt((x0 - x1) * (x0 - x1) + (y0 - y1) * (y0 - y1))
97
98
99def vector_from_to(a, b):
100    """get the vector from (x0,y0) to (x1,y1)"""
101    return b[X] - a[X], b[Y], a[Y]
102
103
104def get_cartesian_pt(t, p):  # get the cartesian coordinates from a trilinear set
105    denom = p[0][0] * t[0] + p[0][1] * t[1] + p[0][2] * t[2]
106    c1 = p[0][1] * t[1] / denom
107    c2 = p[0][2] * t[2] / denom
108    return c1 * p[2][1][0] + c2 * p[2][0][0], c1 * p[2][1][1] + c2 * p[2][0][1]
109
110
111def get_cartesian_tri(arg, params):
112    """get the cartesian points from a trilinear vertex matrix"""
113    (t11, t12, t13), (t21, t22, t23), (t31, t32, t33) = arg
114    p1 = get_cartesian_pt((t11, t12, t13), params)
115    p2 = get_cartesian_pt((t21, t22, t23), params)
116    p3 = get_cartesian_pt((t31, t32, t33), params)
117    return p1, p2, p3
118
119
120def angle_from_3_sides(a, b, c):  # return the angle opposite side c
121    cosx = (a * a + b * b - c * c) / (2 * a * b)  # use the cosine rule
122    return acos(cosx)
123
124
125def translate_string(string, os):  # translates s_a, a_a, etc to params[x][y], with cyclic offset
126    string = string.replace('s_a', 'params[0][' + str((os + 0) % 3) + ']')  # replace with ref. to the relvant values,
127    string = string.replace('s_b', 'params[0][' + str((os + 1) % 3) + ']')  # cycled by i
128    string = string.replace('s_c', 'params[0][' + str((os + 2) % 3) + ']')
129    string = string.replace('a_a', 'params[1][' + str((os + 0) % 3) + ']')
130    string = string.replace('a_b', 'params[1][' + str((os + 1) % 3) + ']')
131    string = string.replace('a_c', 'params[1][' + str((os + 2) % 3) + ']')
132    string = string.replace('area', 'params[4][0]')
133    string = string.replace('semiperim', 'params[4][1]')
134    return string
135
136
137def pt_from_tcf(tcf, params):  # returns a trilinear triplet from a triangle centre function
138    trilin_pts = []  # will hold the final points
139    for i in range(3):
140        temp = tcf  # read in the tcf
141        temp = translate_string(temp, i)
142        func = eval('lambda params: ' + temp.strip('"'))  # the function leading to the trilinar element
143        trilin_pts.append(func(params))  # evaluate the function for the first trilinear element
144    return trilin_pts
145
146
147# SVG DATA PROCESSING
148
149def get_n_points_from_path(node, n):
150    """returns a list of first n points (x,y) in an SVG path-representing node"""
151    points = list(node.path.control_points)
152    if len(points) < 3:
153        return []
154    return points[:3]
155
156# EXTRA MATHS FUNCTIONS
157def sec(x):  # secant(x)
158    if x == pi / 2 or x == -pi / 2 or x == 3 * pi / 2 or x == -3 * pi / 2:  # sec(x) is undefined
159        return 100000000000
160    else:
161        return 1 / cos(x)
162
163
164def csc(x):  # cosecant(x)
165    if x == 0 or x == pi or x == 2 * pi or x == -2 * pi:  # csc(x) is undefined
166        return 100000000000
167    else:
168        return 1 / sin(x)
169
170
171def cot(x):  # cotangent(x)
172    if x == 0 or x == pi or x == 2 * pi or x == -2 * pi:  # cot(x) is undefined
173        return 100000000000
174    else:
175        return 1 / tan(x)
176
177
178class Style(object):  # container for style information
179    def __init__(self, svg):
180        # dot markers
181        self.d_rad = svg.unittouu('4px')  # dot marker radius
182        self.d_th = svg.unittouu('2px')  # stroke width
183        self.d_fill = '#aaaaaa'  # fill colour
184        self.d_col = '#000000'  # stroke colour
185
186        # lines
187        self.l_th = svg.unittouu('2px')
188        self.l_fill = 'none'
189        self.l_col = '#000000'
190
191        # circles
192        self.c_th = svg.unittouu('2px')
193        self.c_fill = 'none'
194        self.c_col = '#000000'
195
196
197class DrawFromTriangle(inkex.EffectExtension):
198    def add_arguments(self, pars):
199        pars.add_argument("--tab")
200        # PRESET POINT OPTIONS
201        pars.add_argument("--circumcircle", type=inkex.Boolean, default=False)
202        pars.add_argument("--circumcentre", type=inkex.Boolean, default=False)
203        pars.add_argument("--incircle", type=inkex.Boolean, default=False)
204        pars.add_argument("--incentre", type=inkex.Boolean, default=False)
205        pars.add_argument("--contact_tri", type=inkex.Boolean, default=False)
206        pars.add_argument("--excircles", type=inkex.Boolean, default=False)
207        pars.add_argument("--excentres", type=inkex.Boolean, default=False)
208        pars.add_argument("--extouch_tri", type=inkex.Boolean, default=False)
209        pars.add_argument("--excentral_tri", type=inkex.Boolean, default=False)
210        pars.add_argument("--orthocentre", type=inkex.Boolean, default=False)
211        pars.add_argument("--orthic_tri", type=inkex.Boolean, default=False)
212        pars.add_argument("--altitudes", type=inkex.Boolean, default=False)
213        pars.add_argument("--anglebisectors", type=inkex.Boolean, default=False)
214        pars.add_argument("--centroid", type=inkex.Boolean, default=False)
215        pars.add_argument("--ninepointcentre", type=inkex.Boolean, default=False)
216        pars.add_argument("--ninepointcircle", type=inkex.Boolean, default=False)
217        pars.add_argument("--symmedians", type=inkex.Boolean, default=False)
218        pars.add_argument("--sym_point", type=inkex.Boolean, default=False)
219        pars.add_argument("--sym_tri", type=inkex.Boolean, default=False)
220        pars.add_argument("--gergonne_pt", type=inkex.Boolean, default=False)
221        pars.add_argument("--nagel_pt", type=inkex.Boolean, default=False)
222        # CUSTOM POINT OPTIONS
223        pars.add_argument("--mode", default='trilin')
224        pars.add_argument("--cust_str", default='s_a')
225        pars.add_argument("--cust_pt", type=inkex.Boolean, default=False)
226        pars.add_argument("--cust_radius", type=inkex.Boolean, default=False)
227        pars.add_argument("--radius", default='s_a')
228        pars.add_argument("--isogonal_conj", type=inkex.Boolean, default=False)
229        pars.add_argument("--isotomic_conj", type=inkex.Boolean, default=False)
230
231    def effect(self):
232        so = self.options  # shorthand
233
234        pts = []  # initialise in case nothing is selected and following loop is not executed
235        for node in self.svg.selection.filter(inkex.PathElement):
236            # find the (x,y) coordinates of the first 3 points of the path
237            pts = get_n_points_from_path(node, 3)
238
239        if len(pts) == 3:  # if we have right number of nodes, else skip and end program
240            st = Style(self.svg)  # style for dots, lines and circles
241
242            # CREATE A GROUP TO HOLD ALL GENERATED ELEMENTS IN
243            # Hold relative to point A (pt[0])
244            layer = self.svg.get_current_layer().add(inkex.Group.new('TriangleElements'))
245            layer.transform = 'translate(' + str(pts[0][0]) + ',' + str(pts[0][1]) + ')'
246
247            # GET METRICS OF THE TRIANGLE
248            # vertices in the local coordinates (set pt[0] to be the origin)
249            vtx = [[0, 0],
250                   [pts[1][0] - pts[0][0], pts[1][1] - pts[0][1]],
251                   [pts[2][0] - pts[0][0], pts[2][1] - pts[0][1]]]
252
253            s_a = distance(vtx[1], vtx[2])  # get the scalar side lengths
254            s_b = distance(vtx[0], vtx[1])
255            s_c = distance(vtx[0], vtx[2])
256            sides = (s_a, s_b, s_c)  # side list for passing to functions easily and for indexing
257
258            a_a = angle_from_3_sides(s_b, s_c, s_a)  # angles in radians
259            a_b = angle_from_3_sides(s_a, s_c, s_b)
260            a_c = angle_from_3_sides(s_a, s_b, s_c)
261            angles = (a_a, a_b, a_c)
262
263            ab = vector_from_to(vtx[0], vtx[1])  # vector from a to b
264            ac = vector_from_to(vtx[0], vtx[2])  # vector from a to c
265            bc = vector_from_to(vtx[1], vtx[2])  # vector from b to c
266            vecs = (ab, ac)  # vectors for finding cartesian point from trilinears
267
268            semiperim = (s_a + s_b + s_c) / 2.0  # semiperimeter
269            area = sqrt(semiperim * (semiperim - s_a) * (semiperim - s_b) * (semiperim - s_c))  # area of the triangle by heron's formula
270            uvals = (area, semiperim)  # useful values
271
272            params = (sides, angles, vecs, vtx, uvals)  # all useful triangle parameters in one object
273
274            # BEGIN DRAWING
275            if so.circumcentre or so.circumcircle:
276                r = s_a * s_b * s_c / (4 * area)
277                pt = (cos(a_a), cos(a_b), cos(a_c))
278                if so.circumcentre:
279                    draw_SVG_circle(0, pt, params, st, 'Circumcentre', layer)
280                if so.circumcircle:
281                    draw_SVG_circle(r, pt, params, st, 'Circumcircle', layer)
282
283            if so.incentre or so.incircle:
284                pt = [1, 1, 1]
285                if so.incentre:
286                    draw_SVG_circle(0, pt, params, st, 'Incentre', layer)
287                if so.incircle:
288                    r = area / semiperim
289                    draw_SVG_circle(r, pt, params, st, 'Incircle', layer)
290
291            if so.contact_tri:
292                t1 = s_b * s_c / (-s_a + s_b + s_c)
293                t2 = s_a * s_c / (s_a - s_b + s_c)
294                t3 = s_a * s_b / (s_a + s_b - s_c)
295                v_mat = ((0, t2, t3), (t1, 0, t3), (t1, t2, 0))
296                draw_SVG_tri(v_mat, params, st, 'ContactTriangle', layer)
297
298            if so.extouch_tri:
299                t1 = (-s_a + s_b + s_c) / s_a
300                t2 = (s_a - s_b + s_c) / s_b
301                t3 = (s_a + s_b - s_c) / s_c
302                v_mat = ((0, t2, t3), (t1, 0, t3), (t1, t2, 0))
303                draw_SVG_tri(v_mat, params, st, 'ExtouchTriangle', layer)
304
305            if so.orthocentre:
306                pt = pt_from_tcf('cos(a_b)*cos(a_c)', params)
307                draw_SVG_circle(0, pt, params, st, 'Orthocentre', layer)
308
309            if so.orthic_tri:
310                v_mat = [[0, sec(a_b), sec(a_c)], [sec(a_a), 0, sec(a_c)], [sec(a_a), sec(a_b), 0]]
311                draw_SVG_tri(v_mat, params, st, 'OrthicTriangle', layer)
312
313            if so.centroid:
314                pt = [1 / s_a, 1 / s_b, 1 / s_c]
315                draw_SVG_circle(0, pt, params, st, 'Centroid', layer)
316
317            if so.ninepointcentre or so.ninepointcircle:
318                pt = [cos(a_b - a_c), cos(a_c - a_a), cos(a_a - a_b)]
319                if so.ninepointcentre:
320                    draw_SVG_circle(0, pt, params, st, 'NinePointCentre', layer)
321                if so.ninepointcircle:
322                    r = s_a * s_b * s_c / (8 * area)
323                    draw_SVG_circle(r, pt, params, st, 'NinePointCircle', layer)
324
325            if so.altitudes:
326                v_mat = [[0, sec(a_b), sec(a_c)], [sec(a_a), 0, sec(a_c)], [sec(a_a), sec(a_b), 0]]
327                draw_vertex_lines(v_mat, params, st, 'Altitude', layer)
328
329            if so.anglebisectors:
330                v_mat = ((0, 1, 1), (1, 0, 1), (1, 1, 0))
331                draw_vertex_lines(v_mat, params, st, 'AngleBisectors', layer)
332
333            if so.excircles or so.excentres or so.excentral_tri:
334                v_mat = ((-1, 1, 1), (1, -1, 1), (1, 1, -1))
335                if so.excentral_tri:
336                    draw_SVG_tri(v_mat, params, st, 'ExcentralTriangle', layer)
337                for i in range(3):
338                    if so.excircles:
339                        r = area / (semiperim - sides[i])
340                        draw_SVG_circle(r, v_mat[i], params, st, 'Excircle:' + str(i), layer)
341                    if so.excentres:
342                        draw_SVG_circle(0, v_mat[i], params, st, 'Excentre:' + str(i), layer)
343
344            if so.sym_tri or so.symmedians:
345                v_mat = ((0, s_b, s_c), (s_a, 0, s_c), (s_a, s_b, 0))
346                if so.sym_tri:
347                    draw_SVG_tri(v_mat, params, st, 'SymmedialTriangle', layer)
348                if so.symmedians:
349                    draw_vertex_lines(v_mat, params, st, 'Symmedian', layer)
350
351            if so.sym_point:
352                pt = (s_a, s_b, s_c)
353                draw_SVG_circle(0, pt, params, st, 'SymmmedianPoint', layer)
354
355            if so.gergonne_pt:
356                pt = pt_from_tcf('1/(s_a*(s_b+s_c-s_a))', params)
357                draw_SVG_circle(0, pt, params, st, 'GergonnePoint', layer)
358
359            if so.nagel_pt:
360                pt = pt_from_tcf('(s_b+s_c-s_a)/s_a', params)
361                draw_SVG_circle(0, pt, params, st, 'NagelPoint', layer)
362
363            if so.cust_pt or so.cust_radius or so.isogonal_conj or so.isotomic_conj:
364                pt = []  # where we will store the point in trilinears
365                if so.mode == 'trilin':  # if we are receiving from trilinears
366                    for i in range(3):
367                        strings = so.cust_str.split(':')  # get split string
368                        strings[i] = translate_string(strings[i], 0)
369                        func = eval('lambda params: ' + strings[i].strip('"'))  # the function leading to the trilinar element
370                        pt.append(func(params))  # evaluate the function for the trilinear element
371                else:  # we need a triangle function
372                    string = so.cust_str  # don't need to translate, as the pt_from_tcf function does that for us
373                    pt = pt_from_tcf(string, params)  # get the point from the tcf directly
374
375                if so.cust_pt:  # draw the point
376                    draw_SVG_circle(0, pt, params, st, 'CustomTrilinearPoint', layer)
377                if so.cust_radius:  # draw the circle with given radius
378                    strings = translate_string(so.radius, 0)
379                    func = eval('lambda params: ' + strings.strip('"'))  # the function leading to the radius
380                    r = func(params)
381                    draw_SVG_circle(r, pt, params, st, 'CustomTrilinearCircle', layer)
382                if so.isogonal_conj:
383                    isogonal = [0, 0, 0]
384                    for i in range(3):
385                        isogonal[i] = 1 / pt[i]
386                    draw_SVG_circle(0, isogonal, params, st, 'CustomIsogonalConjugate', layer)
387                if so.isotomic_conj:
388                    isotomic = [0, 0, 0]
389                    for i in range(3):
390                        isotomic[i] = 1 / (params[0][i] * params[0][i] * pt[i])
391                    draw_SVG_circle(0, isotomic, params, st, 'CustomIsotomicConjugate', layer)
392
393
394if __name__ == '__main__':
395    DrawFromTriangle().run()
396