1#!/usr/local/bin/python3.8
2# coding=utf-8
3#
4# Copyright (C) 2005 Aaron Spike, aaron@ekips.org (super paths et al)
5#               2007 hugomatic... (gcode.py)
6#               2009 Nick Drobchenko, nick@cnc-club.ru (main developer)
7#               2011 Chris Lusby Taylor, clusbytaylor@enterprise.net (engraving functions)
8#
9# This program is free software; you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation; either version 2 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program; if not, write to the Free Software
21# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
22#
23"""
24Comments starting "#LT" or "#CLT" are by Chris Lusby Taylor who rewrote the engraving function in 2011.
25History of CLT changes to engraving and other functions it uses:
269 May 2011 Changed test of tool diameter to square it
2710 May Note that there are many unused functions, including:
28      bound_to_bound_distance, csp_curvature_radius_at_t,
29        csp_special_points, csplength, rebuild_csp, csp_slope,
30        csp_simple_bound_to_point_distance, csp_bound_to_point_distance,
31        bez_at_t, bez_to_point_distance, bez_normalized_slope, matrix_mul, transpose
32       Fixed csp_point_inside_bound() to work if x outside bounds
3320 May Now encoding the bisectors of angles.
3423 May Using r/cos(a) instead of normalised normals for bisectors of angles.
3523 May Note that Z values generated for engraving are in pixels, not mm.
36       Removed the biarc curves - straight lines are better.
3724 May Changed Bezier slope calculation to be less sensitive to tiny differences in points.
38       Added use of self.options.engraving_newton_iterations to control accuracy
3925 May Big restructure and new recursive function.
40       Changed the way I treat corners - I now find if the centre of a proposed circle is
41                within the area bounded by the line being tested and the two angle bisectors at
42            its ends. See get_radius_to_line().
4329 May Eliminating redundant points. If A,B,C colinear, drop B
4430 May Eliminating redundant lines in divided Beziers. Changed subdivision of lines
45  7Jun Try to show engraving in 3D
46 8 Jun Displaying in stereo 3D.
47       Fixed a bug in bisect - it could go wrong due to rounding errors if
48            1+x1.x2+y1.y2<0 which should never happen. BTW, I spotted a non-normalised normal
49            returned by csp_normalized_normal. Need to check for that.
50 9 Jun Corrected spelling of 'definition' but still match previous 'defention' and       'defenition' if found in file
51     Changed get_tool to find 1.6.04 tools or new tools with corrected spelling
5210 Jun Put 3D into a separate layer called 3D, created unless it already exists
53       Changed csp_normalized_slope to reject lines shorter than 1e-9.
5410 Jun Changed all dimensions seen by user to be mm/inch, not pixels. This includes
55      tool diameter, maximum engraving distance, tool shape and all Z values.
5612 Jun ver 208 Now scales correctly if orientation points moved or stretched.
5712 Jun ver 209. Now detect if engraving toolshape not a function of radius
58                Graphics now indicate Gcode toolpath, limited by min(tool diameter/2,max-dist)
5924 Jan 2017 Removed hard-coded scale values from orientation point calculation
60TODO Change line division to be recursive, depending on what line is touched. See line_divide
61"""
62
63__version__ = '1.7'
64
65import cmath
66import copy
67import math
68import os
69import re
70import sys
71import time
72from functools import partial
73
74import numpy
75
76import inkex
77from inkex.bezier import bezierlength, bezierparameterize, beziertatlength
78from inkex import Transform, PathElement, TextElement, Tspan, Group, Layer, Marker, CubicSuperPath, Style
79
80if sys.version_info[0] > 2:
81    xrange = range
82    unicode = str
83
84def ireplace(self, old, new, count=0):
85    pattern = re.compile(re.escape(old), re.I)
86    return re.sub(pattern, new, self, count)
87
88
89################################################################################
90#
91# Styles and additional parameters
92#
93################################################################################
94
95TAU = math.pi * 2
96STRAIGHT_TOLERANCE = 0.0001
97STRAIGHT_DISTANCE_TOLERANCE = 0.0001
98ENGRAVING_TOLERANCE = 0.0001
99LOFT_LENGTHS_TOLERANCE = 0.0000001
100
101EMC_TOLERANCE_EQUAL = 0.00001
102
103options = {}
104defaults = {
105    'header': """%
106(Header)
107(Generated by gcodetools from Inkscape.)
108(Using default header. To add your own header create file "header" in the output dir.)
109M3
110(Header end.)
111""",
112    'footer': """
113(Footer)
114M5
115G00 X0.0000 Y0.0000
116M2
117(Using default footer. To add your own footer create file "footer" in the output dir.)
118(end)
119%"""
120}
121
122INTERSECTION_RECURSION_DEPTH = 10
123INTERSECTION_TOLERANCE = 0.00001
124
125def marker_style(stroke, marker='DrawCurveMarker', width=1):
126    """Set a marker style with some basic defaults"""
127    return Style(stroke=stroke, fill='none', stroke_width=width,
128                 marker_end='url(#{})'.format(marker))
129
130MARKER_STYLE = {
131    "in_out_path_style": marker_style('#0072a7', 'InOutPathMarker'),
132    "loft_style": {
133        'main curve': marker_style('#88f', 'Arrow2Mend'),
134    },
135    "biarc_style": {
136        'biarc0': marker_style('#88f'),
137        'biarc1': marker_style('#8f8'),
138        'line': marker_style('#f88'),
139        'area': marker_style('#777', width=0.1),
140    },
141    "biarc_style_dark": {
142        'biarc0': marker_style('#33a'),
143        'biarc1': marker_style('#3a3'),
144        'line': marker_style('#a33'),
145        'area': marker_style('#222', width=0.3),
146    },
147    "biarc_style_dark_area": {
148        'biarc0': marker_style('#33a', width=0.1),
149        'biarc1': marker_style('#3a3', width=0.1),
150        'line': marker_style('#a33', width=0.1),
151        'area': marker_style('#222', width=0.3),
152    },
153    "biarc_style_i": {
154        'biarc0': marker_style('#880'),
155        'biarc1': marker_style('#808'),
156        'line': marker_style('#088'),
157        'area': marker_style('#999', width=0.3),
158    },
159    "biarc_style_dark_i": {
160        'biarc0': marker_style('#dd5'),
161        'biarc1': marker_style('#d5d'),
162        'line': marker_style('#5dd'),
163        'area': marker_style('#aaa', width=0.3),
164    },
165    "biarc_style_lathe_feed": {
166        'biarc0': marker_style('#07f', width=0.4),
167        'biarc1': marker_style('#0f7', width=0.4),
168        'line': marker_style('#f44', width=0.4),
169        'area': marker_style('#aaa', width=0.3),
170    },
171    "biarc_style_lathe_passing feed": {
172        'biarc0': marker_style('#07f', width=0.4),
173        'biarc1': marker_style('#0f7', width=0.4),
174        'line': marker_style('#f44', width=0.4),
175        'area': marker_style('#aaa', width=0.3),
176    },
177    "biarc_style_lathe_fine feed": {
178        'biarc0': marker_style('#7f0', width=0.4),
179        'biarc1': marker_style('#f70', width=0.4),
180        'line': marker_style('#744', width=0.4),
181        'area': marker_style('#aaa', width=0.3),
182    },
183    "area artefact": Style(stroke='#ff0000', fill='#ffff00', stroke_width=1),
184    "area artefact arrow": Style(stroke='#ff0000', fill='#ffff00', stroke_width=1),
185    "dxf_points": Style(stroke="#ff0000", fill="#ff0000"),
186}
187
188
189################################################################################
190# Gcode additional functions
191################################################################################
192
193def gcode_comment_str(s, replace_new_line=False):
194    if replace_new_line:
195        s = re.sub(r"[\n\r]+", ".", s)
196    res = ""
197    if s[-1] == "\n":
198        s = s[:-1]
199    for a in s.split("\n"):
200        if a != "":
201            res += "(" + re.sub(r"[\(\)\\\n\r]", ".", a) + ")\n"
202        else:
203            res += "\n"
204    return res
205
206
207################################################################################
208# Cubic Super Path additional functions
209################################################################################
210
211
212def csp_from_polyline(line):
213    return [[[point[:] for _ in range(3)] for point in subline] for subline in line]
214
215
216def csp_remove_zero_segments(csp, tolerance=1e-7):
217    res = []
218    for subpath in csp:
219        if len(subpath) > 0:
220            res.append([subpath[0]])
221            for sp1, sp2 in zip(subpath, subpath[1:]):
222                if point_to_point_d2(sp1[1], sp2[1]) <= tolerance and point_to_point_d2(sp1[2], sp2[1]) <= tolerance and point_to_point_d2(sp1[1], sp2[0]) <= tolerance:
223                    res[-1][-1][2] = sp2[2]
224                else:
225                    res[-1].append(sp2)
226    return res
227
228
229def point_inside_csp(p, csp, on_the_path=True):
230    # we'll do the raytracing and see how many intersections are there on the ray's way.
231    # if number of intersections is even then point is outside.
232    # ray will be x=p.x and y=>p.y
233    # you can assign any value to on_the_path, by default if point is on the path
234    # function will return thai it's inside the path.
235    x, y = p
236    ray_intersections_count = 0
237    for subpath in csp:
238
239        for i in range(1, len(subpath)):
240            sp1 = subpath[i - 1]
241            sp2 = subpath[i]
242            ax, ay, bx, by, cx, cy, dx, dy = csp_parameterize(sp1, sp2)
243            if ax == 0 and bx == 0 and cx == 0 and dx == x:
244                # we've got a special case here
245                b = csp_true_bounds([[sp1, sp2]])
246                if b[1][1] <= y <= b[3][1]:
247                    # points is on the path
248                    return on_the_path
249                else:
250                    # we can skip this segment because it won't influence the answer.
251                    pass
252            else:
253                for t in csp_line_intersection([x, y], [x, y + 5], sp1, sp2):
254                    if t == 0 or t == 1:
255                        # we've got another special case here
256                        x1, y1 = csp_at_t(sp1, sp2, t)
257                        if y1 == y:
258                            # the point is on the path
259                            return on_the_path
260                        # if t == 0 we should have considered this case previously.
261                        if t == 1:
262                            # we have to check the next segment if it is on the same side of the ray
263                            st_d = csp_normalized_slope(sp1, sp2, 1)[0]
264                            if st_d == 0:
265                                st_d = csp_normalized_slope(sp1, sp2, 0.99)[0]
266
267                            for j in range(1, len(subpath) + 1):
268                                if (i + j) % len(subpath) == 0:
269                                    continue  # skip the closing segment
270                                sp11 = subpath[(i - 1 + j) % len(subpath)]
271                                sp22 = subpath[(i + j) % len(subpath)]
272                                ax1, ay1, bx1, by1, cx1, cy1, dx1, dy1 = csp_parameterize(sp1, sp2)
273                                if ax1 == 0 and bx1 == 0 and cx1 == 0 and dx1 == x:
274                                    continue  # this segment parallel to the ray, so skip it
275                                en_d = csp_normalized_slope(sp11, sp22, 0)[0]
276                                if en_d == 0:
277                                    en_d = csp_normalized_slope(sp11, sp22, 0.01)[0]
278                                if st_d * en_d <= 0:
279                                    ray_intersections_count += 1
280                                    break
281                    else:
282                        x1, y1 = csp_at_t(sp1, sp2, t)
283                        if y1 == y:
284                            # the point is on the path
285                            return on_the_path
286                        else:
287                            if y1 > y and 3 * ax * t ** 2 + 2 * bx * t + cx != 0:  # if it's 0 the path only touches the ray
288                                ray_intersections_count += 1
289    return ray_intersections_count % 2 == 1
290
291
292def csp_close_all_subpaths(csp, tolerance=0.000001):
293    for i in range(len(csp)):
294        if point_to_point_d2(csp[i][0][1], csp[i][-1][1]) > tolerance ** 2:
295            csp[i][-1][2] = csp[i][-1][1][:]
296            csp[i] += [[csp[i][0][1][:] for _ in range(3)]]
297        else:
298            if csp[i][0][1] != csp[i][-1][1]:
299                csp[i][-1][1] = csp[i][0][1][:]
300    return csp
301
302
303def csp_simple_bound(csp):
304    minx = None
305    miny = None
306    maxx = None
307    maxy = None
308
309    for subpath in csp:
310        for sp in subpath:
311            for p in sp:
312                minx = min(minx, p[0]) if minx is not None else p[0]
313                miny = min(miny, p[1]) if miny is not None else p[1]
314                maxx = max(maxx, p[0]) if maxx is not None else p[0]
315                maxy = max(maxy, p[1]) if maxy is not None else p[1]
316    return minx, miny, maxx, maxy
317
318
319def csp_segment_to_bez(sp1, sp2):
320    return sp1[1:] + sp2[:2]
321
322
323def csp_to_point_distance(csp, p, dist_bounds=(0, 1e100)):
324    min_dist = [1e100, 0, 0, 0]
325    for j in range(len(csp)):
326        for i in range(1, len(csp[j])):
327            d = csp_seg_to_point_distance(csp[j][i - 1], csp[j][i], p, sample_points=5)
328            if d[0] < dist_bounds[0]:
329                return [d[0], j, i, d[1]]
330            else:
331                if d[0] < min_dist[0]:
332                    min_dist = [d[0], j, i, d[1]]
333    return min_dist
334
335
336def csp_seg_to_point_distance(sp1, sp2, p, sample_points=5):
337    ax, ay, bx, by, cx, cy, dx, dy = csp_parameterize(sp1, sp2)
338    dx = dx - p[0]
339    dy = dy - p[1]
340    if sample_points < 2:
341        sample_points = 2
342    d = min([(p[0] - sp1[1][0]) ** 2 + (p[1] - sp1[1][1]) ** 2, 0.], [(p[0] - sp2[1][0]) ** 2 + (p[1] - sp2[1][1]) ** 2, 1.])
343    for k in range(sample_points):
344        t = float(k) / (sample_points - 1)
345        i = 0
346        while i == 0 or abs(f) > 0.000001 and i < 20:
347            t2 = t ** 2
348            t3 = t ** 3
349            f = (ax * t3 + bx * t2 + cx * t + dx) * (3 * ax * t2 + 2 * bx * t + cx) + (ay * t3 + by * t2 + cy * t + dy) * (3 * ay * t2 + 2 * by * t + cy)
350            df = (6 * ax * t + 2 * bx) * (ax * t3 + bx * t2 + cx * t + dx) + (3 * ax * t2 + 2 * bx * t + cx) ** 2 + (6 * ay * t + 2 * by) * (ay * t3 + by * t2 + cy * t + dy) + (3 * ay * t2 + 2 * by * t + cy) ** 2
351            if df != 0:
352                t = t - f / df
353            else:
354                break
355            i += 1
356        if 0 <= t <= 1:
357            p1 = csp_at_t(sp1, sp2, t)
358            d1 = (p1[0] - p[0]) ** 2 + (p1[1] - p[1]) ** 2
359            if d1 < d[0]:
360                d = [d1, t]
361    return d
362
363
364def csp_seg_to_csp_seg_distance(sp1, sp2, sp3, sp4, dist_bounds=(0, 1e100), sample_points=5, tolerance=.01):
365    # check the ending points first
366    dist = csp_seg_to_point_distance(sp1, sp2, sp3[1], sample_points)
367    dist += [0.]
368    if dist[0] <= dist_bounds[0]:
369        return dist
370    d = csp_seg_to_point_distance(sp1, sp2, sp4[1], sample_points)
371    if d[0] < dist[0]:
372        dist = d + [1.]
373        if dist[0] <= dist_bounds[0]:
374            return dist
375    d = csp_seg_to_point_distance(sp3, sp4, sp1[1], sample_points)
376    if d[0] < dist[0]:
377        dist = [d[0], 0., d[1]]
378        if dist[0] <= dist_bounds[0]:
379            return dist
380    d = csp_seg_to_point_distance(sp3, sp4, sp2[1], sample_points)
381    if d[0] < dist[0]:
382        dist = [d[0], 1., d[1]]
383        if dist[0] <= dist_bounds[0]:
384            return dist
385    sample_points -= 2
386    if sample_points < 1:
387        sample_points = 1
388    ax1, ay1, bx1, by1, cx1, cy1, dx1, dy1 = csp_parameterize(sp1, sp2)
389    ax2, ay2, bx2, by2, cx2, cy2, dx2, dy2 = csp_parameterize(sp3, sp4)
390    #    try to find closes points using Newtons method
391    for k in range(sample_points):
392        for j in range(sample_points):
393            t1 = float(k + 1) / (sample_points + 1)
394            t2 = float(j) / (sample_points + 1)
395
396            t12 = t1 * t1
397            t13 = t1 * t1 * t1
398            t22 = t2 * t2
399            t23 = t2 * t2 * t2
400            i = 0
401
402            F1 = [0, 0]
403            F2 = [[0, 0], [0, 0]]
404            F = 1e100
405            x = ax1 * t13 + bx1 * t12 + cx1 * t1 + dx1 - (ax2 * t23 + bx2 * t22 + cx2 * t2 + dx2)
406            y = ay1 * t13 + by1 * t12 + cy1 * t1 + dy1 - (ay2 * t23 + by2 * t22 + cy2 * t2 + dy2)
407            while i < 2 or abs(F - Flast) > tolerance and i < 30:
408                f1x = 3 * ax1 * t12 + 2 * bx1 * t1 + cx1
409                f1y = 3 * ay1 * t12 + 2 * by1 * t1 + cy1
410                f2x = 3 * ax2 * t22 + 2 * bx2 * t2 + cx2
411                f2y = 3 * ay2 * t22 + 2 * by2 * t2 + cy2
412                F1[0] = 2 * f1x * x + 2 * f1y * y
413                F1[1] = -2 * f2x * x - 2 * f2y * y
414                F2[0][0] = 2 * (6 * ax1 * t1 + 2 * bx1) * x + 2 * f1x * f1x + 2 * (6 * ay1 * t1 + 2 * by1) * y + 2 * f1y * f1y
415                F2[0][1] = -2 * f1x * f2x - 2 * f1y * f2y
416                F2[1][0] = -2 * f2x * f1x - 2 * f2y * f1y
417                F2[1][1] = -2 * (6 * ax2 * t2 + 2 * bx2) * x + 2 * f2x * f2x - 2 * (6 * ay2 * t2 + 2 * by2) * y + 2 * f2y * f2y
418                F2 = inv_2x2(F2)
419                if F2 is not None:
420                    t1 -= (F2[0][0] * F1[0] + F2[0][1] * F1[1])
421                    t2 -= (F2[1][0] * F1[0] + F2[1][1] * F1[1])
422                    t12 = t1 * t1
423                    t13 = t1 * t1 * t1
424                    t22 = t2 * t2
425                    t23 = t2 * t2 * t2
426                    x = ax1 * t13 + bx1 * t12 + cx1 * t1 + dx1 - (ax2 * t23 + bx2 * t22 + cx2 * t2 + dx2)
427                    y = ay1 * t13 + by1 * t12 + cy1 * t1 + dy1 - (ay2 * t23 + by2 * t22 + cy2 * t2 + dy2)
428                    Flast = F
429                    F = x * x + y * y
430                else:
431                    break
432                i += 1
433            if F < dist[0] and 0 <= t1 <= 1 and 0 <= t2 <= 1:
434                dist = [F, t1, t2]
435                if dist[0] <= dist_bounds[0]:
436                    return dist
437    return dist
438
439
440def csp_to_csp_distance(csp1, csp2, dist_bounds=(0, 1e100), tolerance=.01):
441    dist = [1e100, 0, 0, 0, 0, 0, 0]
442    for i1 in range(len(csp1)):
443        for j1 in range(1, len(csp1[i1])):
444            for i2 in range(len(csp2)):
445                for j2 in range(1, len(csp2[i2])):
446                    d = csp_seg_bound_to_csp_seg_bound_max_min_distance(csp1[i1][j1 - 1], csp1[i1][j1], csp2[i2][j2 - 1], csp2[i2][j2])
447                    if d[0] >= dist_bounds[1]:
448                        continue
449                    if d[1] < dist_bounds[0]:
450                        return [d[1], i1, j1, 1, i2, j2, 1]
451                    d = csp_seg_to_csp_seg_distance(csp1[i1][j1 - 1], csp1[i1][j1], csp2[i2][j2 - 1], csp2[i2][j2], dist_bounds, tolerance=tolerance)
452                    if d[0] < dist[0]:
453                        dist = [d[0], i1, j1, d[1], i2, j2, d[2]]
454                    if dist[0] <= dist_bounds[0]:
455                        return dist
456            if dist[0] >= dist_bounds[1]:
457                return dist
458    return dist
459
460
461def csp_split(sp1, sp2, t=.5):
462    [x1, y1] = sp1[1]
463    [x2, y2] = sp1[2]
464    [x3, y3] = sp2[0]
465    [x4, y4] = sp2[1]
466    x12 = x1 + (x2 - x1) * t
467    y12 = y1 + (y2 - y1) * t
468    x23 = x2 + (x3 - x2) * t
469    y23 = y2 + (y3 - y2) * t
470    x34 = x3 + (x4 - x3) * t
471    y34 = y3 + (y4 - y3) * t
472    x1223 = x12 + (x23 - x12) * t
473    y1223 = y12 + (y23 - y12) * t
474    x2334 = x23 + (x34 - x23) * t
475    y2334 = y23 + (y34 - y23) * t
476    x = x1223 + (x2334 - x1223) * t
477    y = y1223 + (y2334 - y1223) * t
478    return [sp1[0], sp1[1], [x12, y12]], [[x1223, y1223], [x, y], [x2334, y2334]], [[x34, y34], sp2[1], sp2[2]]
479
480
481def csp_true_bounds(csp):
482    # Finds minx,miny,maxx,maxy of the csp and return their (x,y,i,j,t)
483    minx = [float("inf"), 0, 0, 0]
484    maxx = [float("-inf"), 0, 0, 0]
485    miny = [float("inf"), 0, 0, 0]
486    maxy = [float("-inf"), 0, 0, 0]
487    for i in range(len(csp)):
488        for j in range(1, len(csp[i])):
489            ax, ay, bx, by, cx, cy, x0, y0 = bezierparameterize((csp[i][j - 1][1], csp[i][j - 1][2], csp[i][j][0], csp[i][j][1]))
490            roots = cubic_solver(0, 3 * ax, 2 * bx, cx) + [0, 1]
491            for root in roots:
492                if type(root) is complex and abs(root.imag) < 1e-10:
493                    root = root.real
494                if type(root) is not complex and 0 <= root <= 1:
495                    y = ay * (root ** 3) + by * (root ** 2) + cy * root + y0
496                    x = ax * (root ** 3) + bx * (root ** 2) + cx * root + x0
497                    maxx = max([x, y, i, j, root], maxx)
498                    minx = min([x, y, i, j, root], minx)
499
500            roots = cubic_solver(0, 3 * ay, 2 * by, cy) + [0, 1]
501            for root in roots:
502                if type(root) is complex and root.imag == 0:
503                    root = root.real
504                if type(root) is not complex and 0 <= root <= 1:
505                    y = ay * (root ** 3) + by * (root ** 2) + cy * root + y0
506                    x = ax * (root ** 3) + bx * (root ** 2) + cx * root + x0
507                    maxy = max([y, x, i, j, root], maxy)
508                    miny = min([y, x, i, j, root], miny)
509    maxy[0], maxy[1] = maxy[1], maxy[0]
510    miny[0], miny[1] = miny[1], miny[0]
511
512    return minx, miny, maxx, maxy
513
514
515############################################################################
516# csp_segments_intersection(sp1,sp2,sp3,sp4)
517#
518# Returns array containing all intersections between two segments of cubic
519# super path. Results are [ta,tb], or [ta0, ta1, tb0, tb1, "Overlap"]
520# where ta, tb are values of t for the intersection point.
521############################################################################
522def csp_segments_intersection(sp1, sp2, sp3, sp4):
523    a = csp_segment_to_bez(sp1, sp2)
524    b = csp_segment_to_bez(sp3, sp4)
525
526    def polish_intersection(a, b, ta, tb, tolerance=INTERSECTION_TOLERANCE):
527        ax, ay, bx, by, cx, cy, dx, dy = bezierparameterize(a)
528        ax1, ay1, bx1, by1, cx1, cy1, dx1, dy1 = bezierparameterize(b)
529        i = 0
530        F = [.0, .0]
531        F1 = [[.0, .0], [.0, .0]]
532        while i == 0 or (abs(F[0]) ** 2 + abs(F[1]) ** 2 > tolerance and i < 10):
533            ta3 = ta ** 3
534            ta2 = ta ** 2
535            tb3 = tb ** 3
536            tb2 = tb ** 2
537            F[0] = ax * ta3 + bx * ta2 + cx * ta + dx - ax1 * tb3 - bx1 * tb2 - cx1 * tb - dx1
538            F[1] = ay * ta3 + by * ta2 + cy * ta + dy - ay1 * tb3 - by1 * tb2 - cy1 * tb - dy1
539            F1[0][0] = 3 * ax * ta2 + 2 * bx * ta + cx
540            F1[0][1] = -3 * ax1 * tb2 - 2 * bx1 * tb - cx1
541            F1[1][0] = 3 * ay * ta2 + 2 * by * ta + cy
542            F1[1][1] = -3 * ay1 * tb2 - 2 * by1 * tb - cy1
543            det = F1[0][0] * F1[1][1] - F1[0][1] * F1[1][0]
544            if det != 0:
545                F1 = [[F1[1][1] / det, -F1[0][1] / det], [-F1[1][0] / det, F1[0][0] / det]]
546                ta = ta - (F1[0][0] * F[0] + F1[0][1] * F[1])
547                tb = tb - (F1[1][0] * F[0] + F1[1][1] * F[1])
548            else:
549                break
550            i += 1
551
552        return ta, tb
553
554    def recursion(a, b, ta0, ta1, tb0, tb1, depth_a, depth_b):
555        global bezier_intersection_recursive_result
556        if a == b:
557            bezier_intersection_recursive_result += [[ta0, tb0, ta1, tb1, "Overlap"]]
558            return
559        tam = (ta0 + ta1) / 2
560        tbm = (tb0 + tb1) / 2
561        if depth_a > 0 and depth_b > 0:
562            a1, a2 = bez_split(a, 0.5)
563            b1, b2 = bez_split(b, 0.5)
564            if bez_bounds_intersect(a1, b1):
565                recursion(a1, b1, ta0, tam, tb0, tbm, depth_a - 1, depth_b - 1)
566            if bez_bounds_intersect(a2, b1):
567                recursion(a2, b1, tam, ta1, tb0, tbm, depth_a - 1, depth_b - 1)
568            if bez_bounds_intersect(a1, b2):
569                recursion(a1, b2, ta0, tam, tbm, tb1, depth_a - 1, depth_b - 1)
570            if bez_bounds_intersect(a2, b2):
571                recursion(a2, b2, tam, ta1, tbm, tb1, depth_a - 1, depth_b - 1)
572        elif depth_a > 0:
573            a1, a2 = bez_split(a, 0.5)
574            if bez_bounds_intersect(a1, b):
575                recursion(a1, b, ta0, tam, tb0, tb1, depth_a - 1, depth_b)
576            if bez_bounds_intersect(a2, b):
577                recursion(a2, b, tam, ta1, tb0, tb1, depth_a - 1, depth_b)
578        elif depth_b > 0:
579            b1, b2 = bez_split(b, 0.5)
580            if bez_bounds_intersect(a, b1):
581                recursion(a, b1, ta0, ta1, tb0, tbm, depth_a, depth_b - 1)
582            if bez_bounds_intersect(a, b2):
583                recursion(a, b2, ta0, ta1, tbm, tb1, depth_a, depth_b - 1)
584        else:  # Both segments have been subdivided enough. Let's get some intersections :).
585            intersection, t1, t2 = straight_segments_intersection([a[0]] + [a[3]], [b[0]] + [b[3]])
586            if intersection:
587                if intersection == "Overlap":
588                    t1 = (max(0, min(1, t1[0])) + max(0, min(1, t1[1]))) / 2
589                    t2 = (max(0, min(1, t2[0])) + max(0, min(1, t2[1]))) / 2
590                bezier_intersection_recursive_result += [[ta0 + t1 * (ta1 - ta0), tb0 + t2 * (tb1 - tb0)]]
591
592    global bezier_intersection_recursive_result
593    bezier_intersection_recursive_result = []
594    recursion(a, b, 0., 1., 0., 1., INTERSECTION_RECURSION_DEPTH, INTERSECTION_RECURSION_DEPTH)
595    intersections = bezier_intersection_recursive_result
596    for i in range(len(intersections)):
597        if len(intersections[i]) < 5 or intersections[i][4] != "Overlap":
598            intersections[i] = polish_intersection(a, b, intersections[i][0], intersections[i][1])
599    return intersections
600
601
602def csp_segments_true_intersection(sp1, sp2, sp3, sp4):
603    intersections = csp_segments_intersection(sp1, sp2, sp3, sp4)
604    res = []
605    for intersection in intersections:
606        if (
607                (len(intersection) == 5 and intersection[4] == "Overlap" and (0 <= intersection[0] <= 1 or 0 <= intersection[1] <= 1) and (0 <= intersection[2] <= 1 or 0 <= intersection[3] <= 1))
608                or (0 <= intersection[0] <= 1 and 0 <= intersection[1] <= 1)
609        ):
610            res += [intersection]
611    return res
612
613
614def csp_get_t_at_curvature(sp1, sp2, c, sample_points=16):
615    # returns a list containing [t1,t2,t3,...,tn],  0<=ti<=1...
616    if sample_points < 2:
617        sample_points = 2
618    tolerance = .0000000001
619    res = []
620    ax, ay, bx, by, cx, cy, dx, dy = csp_parameterize(sp1, sp2)
621    for k in range(sample_points):
622        t = float(k) / (sample_points - 1)
623        i = 0
624        F = 1e100
625        while i < 2 or abs(F) > tolerance and i < 17:
626            try:  # some numerical calculation could exceed the limits
627                t2 = t * t
628                # slopes...
629                f1x = 3 * ax * t2 + 2 * bx * t + cx
630                f1y = 3 * ay * t2 + 2 * by * t + cy
631                f2x = 6 * ax * t + 2 * bx
632                f2y = 6 * ay * t + 2 * by
633                f3x = 6 * ax
634                f3y = 6 * ay
635                d = (f1x ** 2 + f1y ** 2) ** 1.5
636                F1 = (
637                        ((f1x * f3y - f3x * f1y) * d - (f1x * f2y - f2x * f1y) * 3. * (f2x * f1x + f2y * f1y) * ((f1x ** 2 + f1y ** 2) ** .5)) /
638                        ((f1x ** 2 + f1y ** 2) ** 3)
639                )
640                F = (f1x * f2y - f1y * f2x) / d - c
641                t -= F / F1
642            except:
643                break
644            i += 1
645        if 0 <= t <= 1 and F <= tolerance:
646            if len(res) == 0:
647                res.append(t)
648            for i in res:
649                if abs(t - i) <= 0.001:
650                    break
651            if not abs(t - i) <= 0.001:
652                res.append(t)
653    return res
654
655
656def csp_max_curvature(sp1, sp2):
657    ax, ay, bx, by, cx, cy, dx, dy = csp_parameterize(sp1, sp2)
658    tolerance = .0001
659    F = 0.
660    i = 0
661    while i < 2 or F - Flast < tolerance and i < 10:
662        t = .5
663        f1x = 3 * ax * t ** 2 + 2 * bx * t + cx
664        f1y = 3 * ay * t ** 2 + 2 * by * t + cy
665        f2x = 6 * ax * t + 2 * bx
666        f2y = 6 * ay * t + 2 * by
667        f3x = 6 * ax
668        f3y = 6 * ay
669        d = pow(f1x ** 2 + f1y ** 2, 1.5)
670        if d != 0:
671            Flast = F
672            F = (f1x * f2y - f1y * f2x) / d
673            F1 = (
674                    (d * (f1x * f3y - f3x * f1y) - (f1x * f2y - f2x * f1y) * 3. * (f2x * f1x + f2y * f1y) * pow(f1x ** 2 + f1y ** 2, .5)) /
675                    (f1x ** 2 + f1y ** 2) ** 3
676            )
677            i += 1
678            if F1 != 0:
679                t -= F / F1
680            else:
681                break
682        else:
683            break
684    return t
685
686
687def csp_curvature_at_t(sp1, sp2, t, depth=3):
688    ax, ay, bx, by, cx, cy, dx, dy = bezierparameterize(csp_segment_to_bez(sp1, sp2))
689
690    # curvature = (x'y''-y'x'') / (x'^2+y'^2)^1.5
691
692    f1x = 3 * ax * t ** 2 + 2 * bx * t + cx
693    f1y = 3 * ay * t ** 2 + 2 * by * t + cy
694    f2x = 6 * ax * t + 2 * bx
695    f2y = 6 * ay * t + 2 * by
696    d = (f1x ** 2 + f1y ** 2) ** 1.5
697    if d != 0:
698        return (f1x * f2y - f1y * f2x) / d
699    else:
700        t1 = f1x * f2y - f1y * f2x
701        if t1 > 0:
702            return 1e100
703        if t1 < 0:
704            return -1e100
705        # Use the Lapitals rule to solve 0/0 problem for 2 times...
706        t1 = 2 * (bx * ay - ax * by) * t + (ay * cx - ax * cy)
707        if t1 > 0:
708            return 1e100
709        if t1 < 0:
710            return -1e100
711        t1 = bx * ay - ax * by
712        if t1 > 0:
713            return 1e100
714        if t1 < 0:
715            return -1e100
716        if depth > 0:
717            # little hack ;^) hope it won't influence anything...
718            return csp_curvature_at_t(sp1, sp2, t * 1.004, depth - 1)
719        return 1e100
720
721
722def csp_subpath_ccw(subpath):
723    # Remove all zero length segments
724    s = 0
725    if (P(subpath[-1][1]) - P(subpath[0][1])).l2() > 1e-10:
726        subpath[-1][2] = subpath[-1][1]
727        subpath[0][0] = subpath[0][1]
728        subpath += [[subpath[0][1], subpath[0][1], subpath[0][1]]]
729    pl = subpath[-1][2]
730    for sp1 in subpath:
731        for p in sp1:
732            s += (p[0] - pl[0]) * (p[1] + pl[1])
733            pl = p
734    return s < 0
735
736
737def csp_at_t(sp1, sp2, t):
738    ax = sp1[1][0]
739    bx = sp1[2][0]
740    cx = sp2[0][0]
741    dx = sp2[1][0]
742
743    ay = sp1[1][1]
744    by = sp1[2][1]
745    cy = sp2[0][1]
746    dy = sp2[1][1]
747
748    x1 = ax + (bx - ax) * t
749    y1 = ay + (by - ay) * t
750
751    x2 = bx + (cx - bx) * t
752    y2 = by + (cy - by) * t
753
754    x3 = cx + (dx - cx) * t
755    y3 = cy + (dy - cy) * t
756
757    x4 = x1 + (x2 - x1) * t
758    y4 = y1 + (y2 - y1) * t
759
760    x5 = x2 + (x3 - x2) * t
761    y5 = y2 + (y3 - y2) * t
762
763    x = x4 + (x5 - x4) * t
764    y = y4 + (y5 - y4) * t
765
766    return [x, y]
767
768
769def csp_at_length(sp1, sp2, l=0.5, tolerance=0.01):
770    bez = (sp1[1][:], sp1[2][:], sp2[0][:], sp2[1][:])
771    t = beziertatlength(bez, l, tolerance)
772    return csp_at_t(sp1, sp2, t)
773
774
775def cspseglength(sp1, sp2, tolerance=0.01):
776    bez = (sp1[1][:], sp1[2][:], sp2[0][:], sp2[1][:])
777    return bezierlength(bez, tolerance)
778
779
780def csp_line_intersection(l1, l2, sp1, sp2):
781    dd = l1[0]
782    cc = l2[0] - l1[0]
783    bb = l1[1]
784    aa = l2[1] - l1[1]
785    if aa == cc == 0:
786        return []
787    if aa:
788        coef1 = cc / aa
789        coef2 = 1
790    else:
791        coef1 = 1
792        coef2 = aa / cc
793    bez = (sp1[1][:], sp1[2][:], sp2[0][:], sp2[1][:])
794    ax, ay, bx, by, cx, cy, x0, y0 = bezierparameterize(bez)
795    a = coef1 * ay - coef2 * ax
796    b = coef1 * by - coef2 * bx
797    c = coef1 * cy - coef2 * cx
798    d = coef1 * (y0 - bb) - coef2 * (x0 - dd)
799    roots = cubic_solver(a, b, c, d)
800    retval = []
801    for i in roots:
802        if type(i) is complex and abs(i.imag) < 1e-7:
803            i = i.real
804        if type(i) is not complex and -1e-10 <= i <= 1. + 1e-10:
805            retval.append(i)
806    return retval
807
808
809def csp_split_by_two_points(sp1, sp2, t1, t2):
810    if t1 > t2:
811        t1, t2 = t2, t1
812    if t1 == t2:
813        sp1, sp2, sp3 = csp_split(sp1, sp2, t1)
814        return [sp1, sp2, sp2, sp3]
815    elif t1 <= 1e-10 and t2 >= 1. - 1e-10:
816        return [sp1, sp1, sp2, sp2]
817    elif t1 <= 1e-10:
818        sp1, sp2, sp3 = csp_split(sp1, sp2, t2)
819        return [sp1, sp1, sp2, sp3]
820    elif t2 >= 1. - 1e-10:
821        sp1, sp2, sp3 = csp_split(sp1, sp2, t1)
822        return [sp1, sp2, sp3, sp3]
823    else:
824        sp1, sp2, sp3 = csp_split(sp1, sp2, t1)
825        sp2, sp3, sp4 = csp_split(sp2, sp3, (t2 - t1) / (1 - t1))
826        return [sp1, sp2, sp3, sp4]
827
828
829def csp_seg_split(sp1, sp2, points):
830    # points is float=t or list [t1, t2, ..., tn]
831    if type(points) is float:
832        points = [points]
833    points.sort()
834    res = [sp1, sp2]
835    last_t = 0
836    for t in points:
837        if 1e-10 < t < 1. - 1e-10:
838            sp3, sp4, sp5 = csp_split(res[-2], res[-1], (t - last_t) / (1 - last_t))
839            last_t = t
840            res[-2:] = [sp3, sp4, sp5]
841    return res
842
843
844def csp_subpath_split_by_points(subpath, points):
845    # points are [[i,t]...] where i-segment's number
846    points.sort()
847    points = [[1, 0.]] + points + [[len(subpath) - 1, 1.]]
848    parts = []
849    for int1, int2 in zip(points, points[1:]):
850        if int1 == int2:
851            continue
852        if int1[1] == 1.:
853            int1[0] += 1
854            int1[1] = 0.
855        if int1 == int2:
856            continue
857        if int2[1] == 0.:
858            int2[0] -= 1
859            int2[1] = 1.
860        if int1[0] == 0 and int2[0] == len(subpath) - 1:  # and small(int1[1]) and small(int2[1]-1) :
861            continue
862        if int1[0] == int2[0]:  # same segment
863            sp = csp_split_by_two_points(subpath[int1[0] - 1], subpath[int1[0]], int1[1], int2[1])
864            if sp[1] != sp[2]:
865                parts += [[sp[1], sp[2]]]
866        else:
867            sp5, sp1, sp2 = csp_split(subpath[int1[0] - 1], subpath[int1[0]], int1[1])
868            sp3, sp4, sp5 = csp_split(subpath[int2[0] - 1], subpath[int2[0]], int2[1])
869            if int1[0] == int2[0] - 1:
870                parts += [[sp1, [sp2[0], sp2[1], sp3[2]], sp4]]
871            else:
872                parts += [[sp1, sp2] + subpath[int1[0] + 1:int2[0] - 1] + [sp3, sp4]]
873    return parts
874
875
876def arc_from_s_r_n_l(s, r, n, l):
877    if abs(n[0] ** 2 + n[1] ** 2 - 1) > 1e-10:
878        n = normalize(n)
879    return arc_from_c_s_l([s[0] + n[0] * r, s[1] + n[1] * r], s, l)
880
881
882def arc_from_c_s_l(c, s, l):
883    r = point_to_point_d(c, s)
884    if r == 0:
885        return []
886    alpha = l / r
887    cos_ = math.cos(alpha)
888    sin_ = math.sin(alpha)
889    e = [c[0] + (s[0] - c[0]) * cos_ - (s[1] - c[1]) * sin_, c[1] + (s[0] - c[0]) * sin_ + (s[1] - c[1]) * cos_]
890    n = [c[0] - s[0], c[1] - s[1]]
891    slope = rotate_cw(n) if l > 0 else rotate_ccw(n)
892    return csp_from_arc(s, e, c, r, slope)
893
894
895def csp_from_arc(start, end, center, r, slope_st):
896    # Creates csp that approximise specified arc
897    r = abs(r)
898    alpha = (atan2(end[0] - center[0], end[1] - center[1]) - atan2(start[0] - center[0], start[1] - center[1])) % TAU
899
900    sectors = int(abs(alpha) * 2 / math.pi) + 1
901    alpha_start = atan2(start[0] - center[0], start[1] - center[1])
902    cos_ = math.cos(alpha_start)
903    sin_ = math.sin(alpha_start)
904    k = (4. * math.tan(alpha / sectors / 4.) / 3.)
905    if dot(slope_st, [- sin_ * k * r, cos_ * k * r]) < 0:
906        if alpha > 0:
907            alpha -= TAU
908        else:
909            alpha += TAU
910    if abs(alpha * r) < 0.001:
911        return []
912
913    sectors = int(abs(alpha) * 2 / math.pi) + 1
914    k = (4. * math.tan(alpha / sectors / 4.) / 3.)
915    result = []
916    for i in range(sectors + 1):
917        cos_ = math.cos(alpha_start + alpha * i / sectors)
918        sin_ = math.sin(alpha_start + alpha * i / sectors)
919        sp = [[], [center[0] + cos_ * r, center[1] + sin_ * r], []]
920        sp[0] = [sp[1][0] + sin_ * k * r, sp[1][1] - cos_ * k * r]
921        sp[2] = [sp[1][0] - sin_ * k * r, sp[1][1] + cos_ * k * r]
922        result += [sp]
923    result[0][0] = result[0][1][:]
924    result[-1][2] = result[-1][1]
925
926    return result
927
928
929def point_to_arc_distance(p, arc):
930    # Distance calculattion from point to arc
931    P0, P2, c, a = arc
932    p = P(p)
933    r = (P0 - c).mag()
934    if r > 0:
935        i = c + (p - c).unit() * r
936        alpha = ((i - c).angle() - (P0 - c).angle())
937        if a * alpha < 0:
938            if alpha > 0:
939                alpha = alpha - TAU
940            else:
941                alpha = TAU + alpha
942        if between(alpha, 0, a) or min(abs(alpha), abs(alpha - a)) < STRAIGHT_TOLERANCE:
943            return (p - i).mag(), [i.x, i.y]
944        else:
945            d1 = (p - P0).mag()
946            d2 = (p - P2).mag()
947            if d1 < d2:
948                return d1, [P0.x, P0.y]
949            else:
950                return d2, [P2.x, P2.y]
951
952
953def csp_to_arc_distance(sp1, sp2, arc1, arc2, tolerance=0.01):  # arc = [start,end,center,alpha]
954    n = 10
955    i = 0
956    d = (0, [0, 0])
957    d1 = (0, [0, 0])
958    dl = 0
959    while i < 1 or (abs(d1[0] - dl[0]) > tolerance and i < 4):
960        i += 1
961        dl = d1 * 1
962        for j in range(n + 1):
963            t = float(j) / n
964            p = csp_at_t(sp1, sp2, t)
965            d = min(point_to_arc_distance(p, arc1), point_to_arc_distance(p, arc2))
966            d1 = max(d1, d)
967        n = n * 2
968    return d1[0]
969
970
971def csp_point_inside_bound(sp1, sp2, p):
972    bez = [sp1[1], sp1[2], sp2[0], sp2[1]]
973    x, y = p
974    c = 0
975    # CLT added test of x in range
976    xmin = 1e100
977    xmax = -1e100
978    for i in range(4):
979        [x0, y0] = bez[i - 1]
980        [x1, y1] = bez[i]
981        xmin = min(xmin, x0)
982        xmax = max(xmax, x0)
983        if x0 - x1 != 0 and (y - y0) * (x1 - x0) >= (x - x0) * (y1 - y0) and x > min(x0, x1) and x <= max(x0, x1):
984            c += 1
985    return xmin <= x <= xmax and c % 2 == 0
986
987
988def line_line_intersect(p1, p2, p3, p4):  # Return only true intersection.
989    if (p1[0] == p2[0] and p1[1] == p2[1]) or (p3[0] == p4[0] and p3[1] == p4[1]):
990        return False
991    x = (p2[0] - p1[0]) * (p4[1] - p3[1]) - (p2[1] - p1[1]) * (p4[0] - p3[0])
992    if x == 0:  # Lines are parallel
993        if (p3[0] - p1[0]) * (p2[1] - p1[1]) == (p3[1] - p1[1]) * (p2[0] - p1[0]):
994            if p3[0] != p4[0]:
995                t11 = (p1[0] - p3[0]) / (p4[0] - p3[0])
996                t12 = (p2[0] - p3[0]) / (p4[0] - p3[0])
997                t21 = (p3[0] - p1[0]) / (p2[0] - p1[0])
998                t22 = (p4[0] - p1[0]) / (p2[0] - p1[0])
999            else:
1000                t11 = (p1[1] - p3[1]) / (p4[1] - p3[1])
1001                t12 = (p2[1] - p3[1]) / (p4[1] - p3[1])
1002                t21 = (p3[1] - p1[1]) / (p2[1] - p1[1])
1003                t22 = (p4[1] - p1[1]) / (p2[1] - p1[1])
1004            return "Overlap" if (0 <= t11 <= 1 or 0 <= t12 <= 1) and (0 <= t21 <= 1 or 0 <= t22 <= 1) else False
1005        else:
1006            return False
1007    else:
1008        return (
1009                0 <= ((p4[0] - p3[0]) * (p1[1] - p3[1]) - (p4[1] - p3[1]) * (p1[0] - p3[0])) / x <= 1 and
1010                0 <= ((p2[0] - p1[0]) * (p1[1] - p3[1]) - (p2[1] - p1[1]) * (p1[0] - p3[0])) / x <= 1)
1011
1012
1013def line_line_intersection_points(p1, p2, p3, p4):  # Return only points [ (x,y) ]
1014    if (p1[0] == p2[0] and p1[1] == p2[1]) or (p3[0] == p4[0] and p3[1] == p4[1]):
1015        return []
1016    x = (p2[0] - p1[0]) * (p4[1] - p3[1]) - (p2[1] - p1[1]) * (p4[0] - p3[0])
1017    if x == 0:  # Lines are parallel
1018        if (p3[0] - p1[0]) * (p2[1] - p1[1]) == (p3[1] - p1[1]) * (p2[0] - p1[0]):
1019            if p3[0] != p4[0]:
1020                t11 = (p1[0] - p3[0]) / (p4[0] - p3[0])
1021                t12 = (p2[0] - p3[0]) / (p4[0] - p3[0])
1022                t21 = (p3[0] - p1[0]) / (p2[0] - p1[0])
1023                t22 = (p4[0] - p1[0]) / (p2[0] - p1[0])
1024            else:
1025                t11 = (p1[1] - p3[1]) / (p4[1] - p3[1])
1026                t12 = (p2[1] - p3[1]) / (p4[1] - p3[1])
1027                t21 = (p3[1] - p1[1]) / (p2[1] - p1[1])
1028                t22 = (p4[1] - p1[1]) / (p2[1] - p1[1])
1029            res = []
1030            if (0 <= t11 <= 1 or 0 <= t12 <= 1) and (0 <= t21 <= 1 or 0 <= t22 <= 1):
1031                if 0 <= t11 <= 1:
1032                    res += [p1]
1033                if 0 <= t12 <= 1:
1034                    res += [p2]
1035                if 0 <= t21 <= 1:
1036                    res += [p3]
1037                if 0 <= t22 <= 1:
1038                    res += [p4]
1039            return res
1040        else:
1041            return []
1042    else:
1043        t1 = ((p4[0] - p3[0]) * (p1[1] - p3[1]) - (p4[1] - p3[1]) * (p1[0] - p3[0])) / x
1044        t2 = ((p2[0] - p1[0]) * (p1[1] - p3[1]) - (p2[1] - p1[1]) * (p1[0] - p3[0])) / x
1045        if 0 <= t1 <= 1 and 0 <= t2 <= 1:
1046            return [[p1[0] * (1 - t1) + p2[0] * t1, p1[1] * (1 - t1) + p2[1] * t1]]
1047        else:
1048            return []
1049
1050
1051def point_to_point_d2(a, b):
1052    return (a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2
1053
1054
1055def point_to_point_d(a, b):
1056    return math.sqrt((a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2)
1057
1058
1059def point_to_line_segment_distance_2(p1, p2, p3):
1060    # p1 - point, p2,p3 - line segment
1061    # draw_pointer(p1)
1062    w0 = [p1[0] - p2[0], p1[1] - p2[1]]
1063    v = [p3[0] - p2[0], p3[1] - p2[1]]
1064    c1 = w0[0] * v[0] + w0[1] * v[1]
1065    if c1 <= 0:
1066        return w0[0] * w0[0] + w0[1] * w0[1]
1067    c2 = v[0] * v[0] + v[1] * v[1]
1068    if c2 <= c1:
1069        return (p1[0] - p3[0]) ** 2 + (p1[1] - p3[1]) ** 2
1070    return (p1[0] - p2[0] - v[0] * c1 / c2) ** 2 + (p1[1] - p2[1] - v[1] * c1 / c2)
1071
1072
1073def line_to_line_distance_2(p1, p2, p3, p4):
1074    if line_line_intersect(p1, p2, p3, p4):
1075        return 0
1076    return min(
1077            point_to_line_segment_distance_2(p1, p3, p4),
1078            point_to_line_segment_distance_2(p2, p3, p4),
1079            point_to_line_segment_distance_2(p3, p1, p2),
1080            point_to_line_segment_distance_2(p4, p1, p2))
1081
1082
1083def csp_seg_bound_to_csp_seg_bound_max_min_distance(sp1, sp2, sp3, sp4):
1084    bez1 = csp_segment_to_bez(sp1, sp2)
1085    bez2 = csp_segment_to_bez(sp3, sp4)
1086    min_dist = 1e100
1087    max_dist = 0.
1088    for i in range(4):
1089        if csp_point_inside_bound(sp1, sp2, bez2[i]) or csp_point_inside_bound(sp3, sp4, bez1[i]):
1090            min_dist = 0.
1091            break
1092    for i in range(4):
1093        for j in range(4):
1094            d = line_to_line_distance_2(bez1[i - 1], bez1[i], bez2[j - 1], bez2[j])
1095            if d < min_dist:
1096                min_dist = d
1097            d = (bez2[j][0] - bez1[i][0]) ** 2 + (bez2[j][1] - bez1[i][1]) ** 2
1098            if max_dist < d:
1099                max_dist = d
1100    return min_dist, max_dist
1101
1102
1103def csp_reverse(csp):
1104    for i in range(len(csp)):
1105        n = []
1106        for j in csp[i]:
1107            n = [[j[2][:], j[1][:], j[0][:]]] + n
1108        csp[i] = n[:]
1109    return csp
1110
1111
1112def csp_normalized_slope(sp1, sp2, t):
1113    ax, ay, bx, by, cx, cy, dx, dy = bezierparameterize((sp1[1][:], sp1[2][:], sp2[0][:], sp2[1][:]))
1114    if sp1[1] == sp2[1] == sp1[2] == sp2[0]:
1115        return [1., 0.]
1116    f1x = 3 * ax * t * t + 2 * bx * t + cx
1117    f1y = 3 * ay * t * t + 2 * by * t + cy
1118    if abs(f1x * f1x + f1y * f1y) > 1e-9:  # LT changed this from 1e-20, which caused problems
1119        l = math.sqrt(f1x * f1x + f1y * f1y)
1120        return [f1x / l, f1y / l]
1121
1122    if t == 0:
1123        f1x = sp2[0][0] - sp1[1][0]
1124        f1y = sp2[0][1] - sp1[1][1]
1125        if abs(f1x * f1x + f1y * f1y) > 1e-9:  # LT changed this from 1e-20, which caused problems
1126            l = math.sqrt(f1x * f1x + f1y * f1y)
1127            return [f1x / l, f1y / l]
1128        else:
1129            f1x = sp2[1][0] - sp1[1][0]
1130            f1y = sp2[1][1] - sp1[1][1]
1131            if f1x * f1x + f1y * f1y != 0:
1132                l = math.sqrt(f1x * f1x + f1y * f1y)
1133                return [f1x / l, f1y / l]
1134    elif t == 1:
1135        f1x = sp2[1][0] - sp1[2][0]
1136        f1y = sp2[1][1] - sp1[2][1]
1137        if abs(f1x * f1x + f1y * f1y) > 1e-9:
1138            l = math.sqrt(f1x * f1x + f1y * f1y)
1139            return [f1x / l, f1y / l]
1140        else:
1141            f1x = sp2[1][0] - sp1[1][0]
1142            f1y = sp2[1][1] - sp1[1][1]
1143            if f1x * f1x + f1y * f1y != 0:
1144                l = math.sqrt(f1x * f1x + f1y * f1y)
1145                return [f1x / l, f1y / l]
1146    else:
1147        return [1., 0.]
1148
1149
1150def csp_normalized_normal(sp1, sp2, t):
1151    nx, ny = csp_normalized_slope(sp1, sp2, t)
1152    return [-ny, nx]
1153
1154
1155def csp_parameterize(sp1, sp2):
1156    return bezierparameterize(csp_segment_to_bez(sp1, sp2))
1157
1158
1159def csp_concat_subpaths(*s):
1160    def concat(s1, s2):
1161        if not s1:
1162            return s2
1163        if not s2:
1164            return s1
1165        if (s1[-1][1][0] - s2[0][1][0]) ** 2 + (s1[-1][1][1] - s2[0][1][1]) ** 2 > 0.00001:
1166            return s1[:-1] + [[s1[-1][0], s1[-1][1], s1[-1][1]], [s2[0][1], s2[0][1], s2[0][2]]] + s2[1:]
1167        else:
1168            return s1[:-1] + [[s1[-1][0], s2[0][1], s2[0][2]]] + s2[1:]
1169
1170    if len(s) == 0:
1171        return []
1172    if len(s) == 1:
1173        return s[0]
1174    result = s[0]
1175    for s1 in s[1:]:
1176        result = concat(result, s1)
1177    return result
1178
1179
1180def csp_subpaths_end_to_start_distance2(s1, s2):
1181    return (s1[-1][1][0] - s2[0][1][0]) ** 2 + (s1[-1][1][1] - s2[0][1][1]) ** 2
1182
1183
1184def csp_clip_by_line(csp, l1, l2):
1185    result = []
1186    for i in range(len(csp)):
1187        s = csp[i]
1188        intersections = []
1189        for j in range(1, len(s)):
1190            intersections += [[j, int_] for int_ in csp_line_intersection(l1, l2, s[j - 1], s[j])]
1191        splitted_s = csp_subpath_split_by_points(s, intersections)
1192        for s in splitted_s[:]:
1193            clip = False
1194            for p in csp_true_bounds([s]):
1195                if (l1[1] - l2[1]) * p[0] + (l2[0] - l1[0]) * p[1] + (l1[0] * l2[1] - l2[0] * l1[1]) < -0.01:
1196                    clip = True
1197                    break
1198            if clip:
1199                splitted_s.remove(s)
1200        result += splitted_s
1201    return result
1202
1203
1204def csp_subpath_line_to(subpath, points, prepend=False):
1205    # Appends subpath with line or polyline.
1206    if len(points) > 0:
1207        if not prepend:
1208            if len(subpath) > 0:
1209                subpath[-1][2] = subpath[-1][1][:]
1210            if type(points[0]) == type([1, 1]):
1211                for p in points:
1212                    subpath += [[p[:], p[:], p[:]]]
1213            else:
1214                subpath += [[points, points, points]]
1215        else:
1216            if len(subpath) > 0:
1217                subpath[0][0] = subpath[0][1][:]
1218            if type(points[0]) == type([1, 1]):
1219                for p in points:
1220                    subpath = [[p[:], p[:], p[:]]] + subpath
1221            else:
1222                subpath = [[points, points, points]] + subpath
1223    return subpath
1224
1225
1226def csp_join_subpaths(csp):
1227    result = csp[:]
1228    done_smf = True
1229    joined_result = []
1230    while done_smf:
1231        done_smf = False
1232        while len(result) > 0:
1233            s1 = result[-1][:]
1234            del (result[-1])
1235            j = 0
1236            joined_smf = False
1237            while j < len(joined_result):
1238                if csp_subpaths_end_to_start_distance2(joined_result[j], s1) < 0.000001:
1239                    joined_result[j] = csp_concat_subpaths(joined_result[j], s1)
1240                    done_smf = True
1241                    joined_smf = True
1242                    break
1243                if csp_subpaths_end_to_start_distance2(s1, joined_result[j]) < 0.000001:
1244                    joined_result[j] = csp_concat_subpaths(s1, joined_result[j])
1245                    done_smf = True
1246                    joined_smf = True
1247                    break
1248                j += 1
1249            if not joined_smf:
1250                joined_result += [s1[:]]
1251        if done_smf:
1252            result = joined_result[:]
1253            joined_result = []
1254    return joined_result
1255
1256
1257def triangle_cross(a, b, c):
1258    return (a[0] - b[0]) * (c[1] - b[1]) - (c[0] - b[0]) * (a[1] - b[1])
1259
1260
1261def csp_segment_convex_hull(sp1, sp2):
1262    a = sp1[1][:]
1263    b = sp1[2][:]
1264    c = sp2[0][:]
1265    d = sp2[1][:]
1266
1267    abc = triangle_cross(a, b, c)
1268    abd = triangle_cross(a, b, d)
1269    bcd = triangle_cross(b, c, d)
1270    cad = triangle_cross(c, a, d)
1271    if abc == 0 and abd == 0:
1272        return [min(a, b, c, d), max(a, b, c, d)]
1273    if abc == 0:
1274        return [d, min(a, b, c), max(a, b, c)]
1275    if abd == 0:
1276        return [c, min(a, b, d), max(a, b, d)]
1277    if bcd == 0:
1278        return [a, min(b, c, d), max(b, c, d)]
1279    if cad == 0:
1280        return [b, min(c, a, d), max(c, a, d)]
1281
1282    m1 = abc * abd > 0
1283    m2 = abc * bcd > 0
1284    m3 = abc * cad > 0
1285
1286    if m1 and m2 and m3:
1287        return [a, b, c]
1288    if m1 and m2 and not m3:
1289        return [a, b, c, d]
1290    if m1 and not m2 and m3:
1291        return [a, b, d, c]
1292    if not m1 and m2 and m3:
1293        return [a, d, b, c]
1294    if m1 and not (m2 and m3):
1295        return [a, b, d]
1296    if not (m1 and m2) and m3:
1297        return [c, a, d]
1298    if not (m1 and m3) and m2:
1299        return [b, c, d]
1300
1301    raise ValueError("csp_segment_convex_hull happened which is something that shouldn't happen!")
1302
1303
1304################################################################################
1305# Bezier additional functions
1306################################################################################
1307
1308def bez_bounds_intersect(bez1, bez2):
1309    return bounds_intersect(bez_bound(bez2), bez_bound(bez1))
1310
1311
1312def bez_bound(bez):
1313    return [
1314        min(bez[0][0], bez[1][0], bez[2][0], bez[3][0]),
1315        min(bez[0][1], bez[1][1], bez[2][1], bez[3][1]),
1316        max(bez[0][0], bez[1][0], bez[2][0], bez[3][0]),
1317        max(bez[0][1], bez[1][1], bez[2][1], bez[3][1]),
1318    ]
1319
1320
1321def bounds_intersect(a, b):
1322    return not ((a[0] > b[2]) or (b[0] > a[2]) or (a[1] > b[3]) or (b[1] > a[3]))
1323
1324
1325def tpoint(xy1, xy2, t):
1326    (x1, y1) = xy1
1327    (x2, y2) = xy2
1328    return [x1 + t * (x2 - x1), y1 + t * (y2 - y1)]
1329
1330
1331def bez_split(a, t=0.5):
1332    a1 = tpoint(a[0], a[1], t)
1333    at = tpoint(a[1], a[2], t)
1334    b2 = tpoint(a[2], a[3], t)
1335    a2 = tpoint(a1, at, t)
1336    b1 = tpoint(b2, at, t)
1337    a3 = tpoint(a2, b1, t)
1338    return [a[0], a1, a2, a3], [a3, b1, b2, a[3]]
1339
1340
1341################################################################################
1342# Some vector functions
1343################################################################################
1344
1345def normalize(xy):
1346    (x, y) = xy
1347    l = math.sqrt(x ** 2 + y ** 2)
1348    if l == 0:
1349        return [0., 0.]
1350    else:
1351        return [x / l, y / l]
1352
1353
1354def cross(a, b):
1355    return a[1] * b[0] - a[0] * b[1]
1356
1357
1358def dot(a, b):
1359    return a[0] * b[0] + a[1] * b[1]
1360
1361
1362def rotate_ccw(d):
1363    return [-d[1], d[0]]
1364
1365
1366def rotate_cw(d):
1367    return [d[1], -d[0]]
1368
1369
1370def vectors_ccw(a, b):
1371    return a[0] * b[1] - b[0] * a[1] < 0
1372
1373
1374################################################################################
1375# Common functions
1376################################################################################
1377
1378def inv_2x2(a):  # invert matrix 2x2
1379    det = a[0][0] * a[1][1] - a[1][0] * a[0][1]
1380    if det == 0:
1381        return None
1382    return [
1383        [a[1][1] / det, -a[0][1] / det],
1384        [-a[1][0] / det, a[0][0] / det]
1385    ]
1386
1387
1388def small(a):
1389    global small_tolerance
1390    return abs(a) < small_tolerance
1391
1392
1393def atan2(*arg):
1394    if len(arg) == 1 and (type(arg[0]) == type([0., 0.]) or type(arg[0]) == type((0., 0.))):
1395        return (math.pi / 2 - math.atan2(arg[0][0], arg[0][1])) % TAU
1396    elif len(arg) == 2:
1397        return (math.pi / 2 - math.atan2(arg[0], arg[1])) % TAU
1398    else:
1399        raise ValueError("Bad argumets for atan! ({})".format(*arg))
1400
1401
1402def draw_text(text, x, y, group=None, style=None, font_size=10, gcodetools_tag=None):
1403    if style is None:
1404        style = "font-family:DejaVu Sans;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:DejaVu Sans;fill:#000000;fill-opacity:1;stroke:none;"
1405    style += "font-size:{:f}px;".format(font_size)
1406    attributes = {'x': str(x), 'y': str(y), 'style': style}
1407    if gcodetools_tag is not None:
1408        attributes["gcodetools"] = str(gcodetools_tag)
1409
1410    if group is None:
1411        group = options.doc_root
1412
1413    text_elem = group.add(TextElement(**attributes))
1414    text_elem.set("xml:space", "preserve")
1415    text = str(text).split("\n")
1416    for string in text:
1417        span = text_elem.add(Tspan(x=str(x), y=str(y)))
1418        span.set('sodipodi:role', 'line')
1419        y += font_size
1420        span.text = str(string)
1421
1422
1423def draw_csp(csp, stroke="#f00", fill="none", comment="", width=0.354, group=None, style=None):
1424    if group is None:
1425        group = options.doc_root
1426    node = group.add(PathElement())
1427
1428    node.style = style if style is not None else \
1429        {'fill': fill, 'fill-opacity': 1, 'stroke': stroke, 'stroke-width': width}
1430
1431    node.path = CubicSuperPath(csp)
1432
1433    if comment != '':
1434        node.set('comment', comment)
1435
1436    return node
1437
1438
1439def draw_pointer(x, color="#f00", figure="cross", group=None, comment="", fill=None, width=.1, size=10., text=None, font_size=None, pointer_type=None, attrib=None):
1440    size = size / 2
1441    if attrib is None:
1442        attrib = {}
1443    if pointer_type is None:
1444        pointer_type = "Pointer"
1445    attrib["gcodetools"] = pointer_type
1446    if group is None:
1447        group = options.self.svg.get_current_layer()
1448    if text is not None:
1449        if font_size is None:
1450            font_size = 7
1451        group = group.add(Group(gcodetools=pointer_type + " group"))
1452        draw_text(text, x[0] + size * 2.2, x[1] - size, group=group, font_size=font_size)
1453    if figure == "line":
1454        s = ""
1455        for i in range(1, len(x) / 2):
1456            s += " {}, {} ".format(x[i * 2], x[i * 2 + 1])
1457        attrib.update({"d": "M {},{} L {}".format(x[0], x[1], s), "style": "fill:none;stroke:{};stroke-width:{:f};".format(color, width), "comment": str(comment)})
1458    elif figure == "arrow":
1459        if fill is None:
1460            fill = "#12b3ff"
1461        fill_opacity = "0.8"
1462        d = "m {},{} ".format(x[0], x[1]) + re.sub("([0-9\\-.e]+)", (lambda match: str(float(match.group(1)) * size * 2.)), "0.88464,-0.40404 c -0.0987,-0.0162 -0.186549,-0.0589 -0.26147,-0.1173 l 0.357342,-0.35625 c 0.04631,-0.039 0.0031,-0.13174 -0.05665,-0.12164 -0.0029,-1.4e-4 -0.0058,-1.4e-4 -0.0087,0 l -2.2e-5,2e-5 c -0.01189,0.004 -0.02257,0.0119 -0.0305,0.0217 l -0.357342,0.35625 c -0.05818,-0.0743 -0.102813,-0.16338 -0.117662,-0.26067 l -0.409636,0.88193 z")
1463        attrib.update({"d": d, "style": "fill:{};stroke:none;fill-opacity:{};".format(fill, fill_opacity), "comment": str(comment)})
1464    else:
1465        attrib.update({"d": "m {},{} l {:f},{:f} {:f},{:f} {:f},{:f} {:f},{:f} , {:f},{:f}".format(x[0], x[1], size, size, -2 * size, -2 * size, size, size, size, -size, -2 * size, 2 * size), "style": "fill:none;stroke:{};stroke-width:{:f};".format(color, width), "comment": str(comment)})
1466    group.add(PathElement(**attrib))
1467
1468
1469def straight_segments_intersection(a, b, true_intersection=True):  # (True intersection means check ta and tb are in [0,1])
1470    ax = a[0][0]
1471    bx = a[1][0]
1472    cx = b[0][0]
1473    dx = b[1][0]
1474    ay = a[0][1]
1475    by = a[1][1]
1476    cy = b[0][1]
1477    dy = b[1][1]
1478    if (ax == bx and ay == by) or (cx == dx and cy == dy):
1479        return False, 0, 0
1480    if (bx - ax) * (dy - cy) - (by - ay) * (dx - cx) == 0:  # Lines are parallel
1481        ta = (ax - cx) / (dx - cx) if cx != dx else (ay - cy) / (dy - cy)
1482        tb = (bx - cx) / (dx - cx) if cx != dx else (by - cy) / (dy - cy)
1483        tc = (cx - ax) / (bx - ax) if ax != bx else (cy - ay) / (by - ay)
1484        td = (dx - ax) / (bx - ax) if ax != bx else (dy - ay) / (by - ay)
1485        return ("Overlap" if 0 <= ta <= 1 or 0 <= tb <= 1 or 0 <= tc <= 1 or 0 <= td <= 1 or not true_intersection else False), (ta, tb), (tc, td)
1486    else:
1487        ta = ((ay - cy) * (dx - cx) - (ax - cx) * (dy - cy)) / ((bx - ax) * (dy - cy) - (by - ay) * (dx - cx))
1488        tb = (ax - cx + ta * (bx - ax)) / (dx - cx) if dx != cx else (ay - cy + ta * (by - ay)) / (dy - cy)
1489        return (0 <= ta <= 1 and 0 <= tb <= 1 or not true_intersection), ta, tb
1490
1491
1492def between(c, x, y):
1493    return x - STRAIGHT_TOLERANCE <= c <= y + STRAIGHT_TOLERANCE or y - STRAIGHT_TOLERANCE <= c <= x + STRAIGHT_TOLERANCE
1494
1495
1496def cubic_solver_real(a, b, c, d):
1497    # returns only real roots of a cubic equation.
1498    roots = cubic_solver(a, b, c, d)
1499    res = []
1500    for root in roots:
1501        if type(root) is complex:
1502            if -1e-10 < root.imag < 1e-10:
1503                res.append(root.real)
1504        else:
1505            res.append(root)
1506    return res
1507
1508
1509def cubic_solver(a, b, c, d):
1510    if a != 0:
1511        #    Monics formula see http://en.wikipedia.org/wiki/Cubic_function#Monic_formula_of_roots
1512        a, b, c = (b / a, c / a, d / a)
1513        m = 2 * a ** 3 - 9 * a * b + 27 * c
1514        k = a ** 2 - 3 * b
1515        n = m ** 2 - 4 * k ** 3
1516        w1 = -.5 + .5 * cmath.sqrt(3) * 1j
1517        w2 = -.5 - .5 * cmath.sqrt(3) * 1j
1518        if n >= 0:
1519            t = m + math.sqrt(n)
1520            m1 = pow(t / 2, 1. / 3) if t >= 0 else -pow(-t / 2, 1. / 3)
1521            t = m - math.sqrt(n)
1522            n1 = pow(t / 2, 1. / 3) if t >= 0 else -pow(-t / 2, 1. / 3)
1523        else:
1524            m1 = pow(complex((m + cmath.sqrt(n)) / 2), 1. / 3)
1525            n1 = pow(complex((m - cmath.sqrt(n)) / 2), 1. / 3)
1526        x1 = -1. / 3 * (a + m1 + n1)
1527        x2 = -1. / 3 * (a + w1 * m1 + w2 * n1)
1528        x3 = -1. / 3 * (a + w2 * m1 + w1 * n1)
1529        return [x1, x2, x3]
1530    elif b != 0:
1531        det = c ** 2 - 4 * b * d
1532        if det > 0:
1533            return [(-c + math.sqrt(det)) / (2 * b), (-c - math.sqrt(det)) / (2 * b)]
1534        elif d == 0:
1535            return [-c / (b * b)]
1536        else:
1537            return [(-c + cmath.sqrt(det)) / (2 * b), (-c - cmath.sqrt(det)) / (2 * b)]
1538    elif c != 0:
1539        return [-d / c]
1540    else:
1541        return []
1542
1543
1544################################################################################
1545# print_ prints any arguments into specified log file
1546################################################################################
1547
1548def print_(*arg):
1549    with open(options.log_filename, "ab") as f:
1550        for s in arg:
1551            s = unicode(s).encode('unicode_escape') + b" "
1552            f.write(s)
1553        f.write(b"\n")
1554
1555
1556################################################################################
1557# Point (x,y) operations
1558################################################################################
1559class P(object):
1560    def __init__(self, x, y=None):
1561        if not y is None:
1562            self.x = float(x)
1563            self.y = float(y)
1564        else:
1565            self.x = float(x[0])
1566            self.y = float(x[1])
1567
1568    def __add__(self, other):
1569        return P(self.x + other.x, self.y + other.y)
1570
1571    def __sub__(self, other):
1572        return P(self.x - other.x, self.y - other.y)
1573
1574    def __neg__(self):
1575        return P(-self.x, -self.y)
1576
1577    def __mul__(self, other):
1578        if isinstance(other, P):
1579            return self.x * other.x + self.y * other.y
1580        return P(self.x * other, self.y * other)
1581
1582    __rmul__ = __mul__
1583
1584    def __div__(self, other):
1585        return P(self.x / other, self.y / other)
1586
1587    def __truediv__(self, other):
1588        return self.__div__(other)
1589
1590    def mag(self):
1591        return math.hypot(self.x, self.y)
1592
1593    def unit(self):
1594        h_mag = self.mag()
1595        if h_mag:
1596            return self / h_mag
1597        return P(0, 0)
1598
1599    def dot(self, other):
1600        return self.x * other.x + self.y * other.y
1601
1602    def rot(self, theta):
1603        c = math.cos(theta)
1604        s = math.sin(theta)
1605        return P(self.x * c - self.y * s, self.x * s + self.y * c)
1606
1607    def angle(self):
1608        return math.atan2(self.y, self.x)
1609
1610    def __repr__(self):
1611        return '{:f},{:f}'.format(self.x, self.y)
1612
1613    def pr(self):
1614        return "{:.2f},{:.2f}".format(self.x, self.y)
1615
1616    def to_list(self):
1617        return [self.x, self.y]
1618
1619    def ccw(self):
1620        return P(-self.y, self.x)
1621
1622    def l2(self):
1623        return self.x * self.x + self.y * self.y
1624
1625
1626class Line(object):
1627    def __init__(self, st, end):
1628        if st.__class__ == P:
1629            st = st.to_list()
1630        if end.__class__ == P:
1631            end = end.to_list()
1632        self.st = P(st)
1633        self.end = P(end)
1634        self.l = self.length()
1635        if self.l != 0:
1636            self.n = ((self.end - self.st) / self.l).ccw()
1637        else:
1638            self.n = [0, 1]
1639
1640    def offset(self, r):
1641        self.st -= self.n * r
1642        self.end -= self.n * r
1643
1644    def l2(self):
1645        return (self.st - self.end).l2()
1646
1647    def length(self):
1648        return (self.st - self.end).mag()
1649
1650    def draw(self, group, style, layer, transform, num=0, reverse_angle=1):
1651        st = gcodetools.transform(self.st.to_list(), layer, True)
1652        end = gcodetools.transform(self.end.to_list(), layer, True)
1653
1654        attr = {'style': style['line'],
1655                'd': 'M {},{} L {},{}'.format(st[0], st[1], end[0], end[1]),
1656                "gcodetools": "Preview",
1657                }
1658        if transform:
1659            attr["transform"] = transform
1660        group.add(PathElement(**attr))
1661
1662    def intersect(self, b):
1663        if b.__class__ == Line:
1664            if self.l < 10e-8 or b.l < 10e-8:
1665                return []
1666            v1 = self.end - self.st
1667            v2 = b.end - b.st
1668            x = v1.x * v2.y - v2.x * v1.y
1669            if x == 0:
1670                # lines are parallel
1671                res = []
1672
1673                if (self.st.x - b.st.x) * v1.y - (self.st.y - b.st.y) * v1.x == 0:
1674                    # lines are the same
1675                    if v1.x != 0:
1676                        if 0 <= (self.st.x - b.st.x) / v2.x <= 1:
1677                            res.append(self.st)
1678                        if 0 <= (self.end.x - b.st.x) / v2.x <= 1:
1679                            res.append(self.end)
1680                        if 0 <= (b.st.x - self.st.x) / v1.x <= 1:
1681                            res.append(b.st)
1682                        if 0 <= (b.end.x - b.st.x) / v1.x <= 1:
1683                            res.append(b.end)
1684                    else:
1685                        if 0 <= (self.st.y - b.st.y) / v2.y <= 1:
1686                            res.append(self.st)
1687                        if 0 <= (self.end.y - b.st.y) / v2.y <= 1:
1688                            res.append(self.end)
1689                        if 0 <= (b.st.y - self.st.y) / v1.y <= 1:
1690                            res.append(b.st)
1691                        if 0 <= (b.end.y - b.st.y) / v1.y <= 1:
1692                            res.append(b.end)
1693                return res
1694            else:
1695                t1 = (-v1.x * (b.end.y - self.end.y) + v1.y * (b.end.x - self.end.x)) / x
1696                t2 = (-v1.y * (self.st.x - b.st.x) + v1.x * (self.st.y - b.st.y)) / x
1697
1698                gcodetools.error(str((x, t1, t2)))
1699                if 0 <= t1 <= 1 and 0 <= t2 <= 1:
1700                    return [self.st + v1 * t1]
1701                else:
1702                    return []
1703        else:
1704            return []
1705
1706
1707################################################################################
1708#
1709# Offset function
1710#
1711# This function offsets given cubic super path.
1712# It's based on src/livarot/PathOutline.cpp from Inkscape's source code.
1713#
1714#
1715################################################################################
1716def csp_offset(csp, r):
1717    offset_tolerance = 0.05
1718    offset_subdivision_depth = 10
1719    time_ = time.time()
1720    time_start = time_
1721    print_("Offset start at {}".format(time_))
1722    print_("Offset radius {}".format(r))
1723
1724    def csp_offset_segment(sp1, sp2, r):
1725        result = []
1726        t = csp_get_t_at_curvature(sp1, sp2, 1 / r)
1727        if len(t) == 0:
1728            t = [0., 1.]
1729        t.sort()
1730        if t[0] > .00000001:
1731            t = [0.] + t
1732        if t[-1] < .99999999:
1733            t.append(1.)
1734        for st, end in zip(t, t[1:]):
1735            c = csp_curvature_at_t(sp1, sp2, (st + end) / 2)
1736            sp = csp_split_by_two_points(sp1, sp2, st, end)
1737            if sp[1] != sp[2]:
1738                if c > 1 / r and r < 0 or c < 1 / r and r > 0:
1739                    offset = offset_segment_recursion(sp[1], sp[2], r, offset_subdivision_depth, offset_tolerance)
1740                else:  # This part will be clipped for sure... TODO Optimize it...
1741                    offset = offset_segment_recursion(sp[1], sp[2], r, offset_subdivision_depth, offset_tolerance)
1742
1743                if not result:
1744                    result = offset[:]
1745                else:
1746                    if csp_subpaths_end_to_start_distance2(result, offset) < 0.0001:
1747                        result = csp_concat_subpaths(result, offset)
1748                    else:
1749
1750                        intersection = csp_get_subapths_last_first_intersection(result, offset)
1751                        if intersection:
1752                            i, t1, j, t2 = intersection
1753                            sp1_, sp2_, sp3_ = csp_split(result[i - 1], result[i], t1)
1754                            result = result[:i - 1] + [sp1_, sp2_]
1755                            sp1_, sp2_, sp3_ = csp_split(offset[j - 1], offset[j], t2)
1756                            result = csp_concat_subpaths(result, [sp2_, sp3_] + offset[j + 1:])
1757                        else:
1758                            pass  # ???
1759        return result
1760
1761    def create_offset_segment(sp1, sp2, r):
1762        # See    Gernot Hoffmann "Bezier Curves"  p.34 -> 7.1 Bezier Offset Curves
1763        p0 = P(sp1[1])
1764        p1 = P(sp1[2])
1765        p2 = P(sp2[0])
1766        p3 = P(sp2[1])
1767
1768        s0 = p1 - p0
1769        s1 = p2 - p1
1770        s3 = p3 - p2
1771
1772        n0 = s0.ccw().unit() if s0.l2() != 0 else P(csp_normalized_normal(sp1, sp2, 0))
1773        n3 = s3.ccw().unit() if s3.l2() != 0 else P(csp_normalized_normal(sp1, sp2, 1))
1774        n1 = s1.ccw().unit() if s1.l2() != 0 else (n0.unit() + n3.unit()).unit()
1775
1776        q0 = p0 + r * n0
1777        q3 = p3 + r * n3
1778        c = csp_curvature_at_t(sp1, sp2, 0)
1779        q1 = q0 + (p1 - p0) * (1 - (r * c if abs(c) < 100 else 0))
1780        c = csp_curvature_at_t(sp1, sp2, 1)
1781        q2 = q3 + (p2 - p3) * (1 - (r * c if abs(c) < 100 else 0))
1782
1783        return [[q0.to_list(), q0.to_list(), q1.to_list()], [q2.to_list(), q3.to_list(), q3.to_list()]]
1784
1785    def csp_get_subapths_last_first_intersection(s1, s2):
1786        _break = False
1787        for i in range(1, len(s1)):
1788            sp11 = s1[-i - 1]
1789            sp12 = s1[-i]
1790            for j in range(1, len(s2)):
1791                sp21 = s2[j - 1]
1792                sp22 = s2[j]
1793                intersection = csp_segments_true_intersection(sp11, sp12, sp21, sp22)
1794                if intersection:
1795                    _break = True
1796                    break
1797            if _break:
1798                break
1799        if _break:
1800            intersection = max(intersection)
1801            return [len(s1) - i, intersection[0], j, intersection[1]]
1802        else:
1803            return []
1804
1805    def csp_join_offsets(prev, next, sp1, sp2, sp1_l, sp2_l, r):
1806        if len(next) > 1:
1807            if (P(prev[-1][1]) - P(next[0][1])).l2() < 0.001:
1808                return prev, [], next
1809            intersection = csp_get_subapths_last_first_intersection(prev, next)
1810            if intersection:
1811                i, t1, j, t2 = intersection
1812                sp1_, sp2_, sp3_ = csp_split(prev[i - 1], prev[i], t1)
1813                sp3_, sp4_, sp5_ = csp_split(next[j - 1], next[j], t2)
1814                return prev[:i - 1] + [sp1_, sp2_], [], [sp4_, sp5_] + next[j + 1:]
1815
1816        # Offsets do not intersect... will add an arc...
1817        start = (P(csp_at_t(sp1_l, sp2_l, 1.)) + r * P(csp_normalized_normal(sp1_l, sp2_l, 1.))).to_list()
1818        end = (P(csp_at_t(sp1, sp2, 0.)) + r * P(csp_normalized_normal(sp1, sp2, 0.))).to_list()
1819        arc = csp_from_arc(start, end, sp1[1], r, csp_normalized_slope(sp1_l, sp2_l, 1.))
1820        if not arc:
1821            return prev, [], next
1822        else:
1823            # Clip prev by arc
1824            if csp_subpaths_end_to_start_distance2(prev, arc) > 0.00001:
1825                intersection = csp_get_subapths_last_first_intersection(prev, arc)
1826                if intersection:
1827                    i, t1, j, t2 = intersection
1828                    sp1_, sp2_, sp3_ = csp_split(prev[i - 1], prev[i], t1)
1829                    sp3_, sp4_, sp5_ = csp_split(arc[j - 1], arc[j], t2)
1830                    prev = prev[:i - 1] + [sp1_, sp2_]
1831                    arc = [sp4_, sp5_] + arc[j + 1:]
1832            # Clip next by arc
1833            if not next:
1834                return prev, [], arc
1835            if csp_subpaths_end_to_start_distance2(arc, next) > 0.00001:
1836                intersection = csp_get_subapths_last_first_intersection(arc, next)
1837                if intersection:
1838                    i, t1, j, t2 = intersection
1839                    sp1_, sp2_, sp3_ = csp_split(arc[i - 1], arc[i], t1)
1840                    sp3_, sp4_, sp5_ = csp_split(next[j - 1], next[j], t2)
1841                    arc = arc[:i - 1] + [sp1_, sp2_]
1842                    next = [sp4_, sp5_] + next[j + 1:]
1843
1844            return prev, arc, next
1845
1846    def offset_segment_recursion(sp1, sp2, r, depth, tolerance):
1847        sp1_r, sp2_r = create_offset_segment(sp1, sp2, r)
1848        err = max(
1849                csp_seg_to_point_distance(sp1_r, sp2_r, (P(csp_at_t(sp1, sp2, .25)) + P(csp_normalized_normal(sp1, sp2, .25)) * r).to_list())[0],
1850                csp_seg_to_point_distance(sp1_r, sp2_r, (P(csp_at_t(sp1, sp2, .50)) + P(csp_normalized_normal(sp1, sp2, .50)) * r).to_list())[0],
1851                csp_seg_to_point_distance(sp1_r, sp2_r, (P(csp_at_t(sp1, sp2, .75)) + P(csp_normalized_normal(sp1, sp2, .75)) * r).to_list())[0],
1852        )
1853
1854        if err > tolerance ** 2 and depth > 0:
1855            if depth > offset_subdivision_depth - 2:
1856                t = csp_max_curvature(sp1, sp2)
1857                t = max(.1, min(.9, t))
1858            else:
1859                t = .5
1860            sp3, sp4, sp5 = csp_split(sp1, sp2, t)
1861            r1 = offset_segment_recursion(sp3, sp4, r, depth - 1, tolerance)
1862            r2 = offset_segment_recursion(sp4, sp5, r, depth - 1, tolerance)
1863            return r1[:-1] + [[r1[-1][0], r1[-1][1], r2[0][2]]] + r2[1:]
1864        else:
1865            return [sp1_r, sp2_r]
1866
1867    ############################################################################
1868    # Some small definitions
1869    ############################################################################
1870    csp_len = len(csp)
1871
1872    ############################################################################
1873    # Prepare the path
1874    ############################################################################
1875    # Remove all small segments (segment length < 0.001)
1876
1877    for i in xrange(len(csp)):
1878        for j in xrange(len(csp[i])):
1879            sp = csp[i][j]
1880            if (P(sp[1]) - P(sp[0])).mag() < 0.001:
1881                csp[i][j][0] = sp[1]
1882            if (P(sp[2]) - P(sp[0])).mag() < 0.001:
1883                csp[i][j][2] = sp[1]
1884    for i in xrange(len(csp)):
1885        for j in xrange(1, len(csp[i])):
1886            if cspseglength(csp[i][j - 1], csp[i][j]) < 0.001:
1887                csp[i] = csp[i][:j] + csp[i][j + 1:]
1888        if cspseglength(csp[i][-1], csp[i][0]) > 0.001:
1889            csp[i][-1][2] = csp[i][-1][1]
1890            csp[i] += [[csp[i][0][1], csp[i][0][1], csp[i][0][1]]]
1891
1892    # TODO Get rid of self intersections.
1893
1894    original_csp = csp[:]
1895    # Clip segments which has curvature>1/r. Because their offset will be self-intersecting and very nasty.
1896
1897    print_("Offset prepared the path in {}".format(time.time() - time_))
1898    print_("Path length = {}".format(sum([len(i) for i in csp])))
1899    time_ = time.time()
1900
1901    ############################################################################
1902    # Offset
1903    ############################################################################
1904    # Create offsets for all segments in the path. And join them together inside each subpath.
1905    unclipped_offset = [[] for i in xrange(csp_len)]
1906
1907    intersection = [[] for i in xrange(csp_len)]
1908    for i in xrange(csp_len):
1909        subpath = csp[i]
1910        subpath_offset = []
1911        for sp1, sp2 in zip(subpath, subpath[1:]):
1912            segment_offset = csp_offset_segment(sp1, sp2, r)
1913            if not subpath_offset:
1914                subpath_offset = segment_offset
1915
1916                prev_l = len(subpath_offset)
1917            else:
1918                prev, arc, next = csp_join_offsets(subpath_offset[-prev_l:], segment_offset, sp1, sp2, sp1_l, sp2_l, r)
1919
1920                subpath_offset = csp_concat_subpaths(subpath_offset[:-prev_l + 1], prev, arc, next)
1921                prev_l = len(next)
1922            sp1_l = sp1[:]
1923            sp2_l = sp2[:]
1924
1925        # Join last and first offsets togother to close the curve
1926
1927        prev, arc, next = csp_join_offsets(subpath_offset[-prev_l:], subpath_offset[:2], subpath[0], subpath[1], sp1_l, sp2_l, r)
1928        subpath_offset[:2] = next[:]
1929        subpath_offset = csp_concat_subpaths(subpath_offset[:-prev_l + 1], prev, arc)
1930
1931        # Collect subpath's offset and save it to unclipped offset list.
1932        unclipped_offset[i] = subpath_offset[:]
1933
1934    print_("Offsetted path in {}".format(time.time() - time_))
1935    time_ = time.time()
1936
1937    ############################################################################
1938    # Now to the clipping.
1939    ############################################################################
1940    # First of all find all intersection's between all segments of all offset subpaths, including self intersections.
1941
1942    # TODO define offset tolerance here
1943    global small_tolerance
1944    small_tolerance = 0.01
1945    summ = 0
1946    summ1 = 0
1947    for subpath_i in xrange(csp_len):
1948        for subpath_j in xrange(subpath_i, csp_len):
1949            subpath = unclipped_offset[subpath_i]
1950            subpath1 = unclipped_offset[subpath_j]
1951            for i in xrange(1, len(subpath)):
1952                # If subpath_i==subpath_j we are looking for self intersections, so
1953                # we'll need search intersections only for xrange(i,len(subpath1))
1954                for j in (xrange(i, len(subpath1)) if subpath_i == subpath_j else xrange(len(subpath1))):
1955                    if subpath_i == subpath_j and j == i:
1956                        # Find self intersections of a segment
1957                        sp1, sp2, sp3 = csp_split(subpath[i - 1], subpath[i], .5)
1958                        intersections = csp_segments_intersection(sp1, sp2, sp2, sp3)
1959                        summ += 1
1960                        for t in intersections:
1961                            summ1 += 1
1962                            if not (small(t[0] - 1) and small(t[1])) and 0 <= t[0] <= 1 and 0 <= t[1] <= 1:
1963                                intersection[subpath_i] += [[i, t[0] / 2], [j, t[1] / 2 + .5]]
1964                    else:
1965                        intersections = csp_segments_intersection(subpath[i - 1], subpath[i], subpath1[j - 1], subpath1[j])
1966                        summ += 1
1967                        for t in intersections:
1968                            summ1 += 1
1969                            # TODO tolerance dependence to cpsp_length(t)
1970                            if len(t) == 2 and 0 <= t[0] <= 1 and 0 <= t[1] <= 1 and not (
1971                                    subpath_i == subpath_j and (
1972                                    (j - i - 1) % (len(subpath) - 1) == 0 and small(t[0] - 1) and small(t[1]) or
1973                                    (i - j - 1) % (len(subpath) - 1) == 0 and small(t[1] - 1) and small(t[0]))):
1974                                intersection[subpath_i] += [[i, t[0]]]
1975                                intersection[subpath_j] += [[j, t[1]]]
1976
1977                            elif len(t) == 5 and t[4] == "Overlap":
1978                                intersection[subpath_i] += [[i, t[0]], [i, t[1]]]
1979                                intersection[subpath_j] += [[j, t[1]], [j, t[3]]]
1980
1981    print_("Intersections found in {}".format(time.time() - time_))
1982    print_("Examined {} segments".format(summ))
1983    print_("found {} intersections".format(summ1))
1984    time_ = time.time()
1985
1986    ########################################################################
1987    # Split unclipped offset by intersection points into splitted_offset
1988    ########################################################################
1989    splitted_offset = []
1990    for i in xrange(csp_len):
1991        subpath = unclipped_offset[i]
1992        if len(intersection[i]) > 0:
1993            parts = csp_subpath_split_by_points(subpath, intersection[i])
1994            # Close    parts list to close path (The first and the last parts are joined together)
1995            if [1, 0.] not in intersection[i]:
1996                parts[0][0][0] = parts[-1][-1][0]
1997                parts[0] = csp_concat_subpaths(parts[-1], parts[0])
1998                splitted_offset += parts[:-1]
1999            else:
2000                splitted_offset += parts[:]
2001        else:
2002            splitted_offset += [subpath[:]]
2003
2004    print_("Split in {}".format(time.time() - time_))
2005    time_ = time.time()
2006
2007    ########################################################################
2008    # Clipping
2009    ########################################################################
2010    result = []
2011    for subpath_i in range(len(splitted_offset)):
2012        clip = False
2013        s1 = splitted_offset[subpath_i]
2014        for subpath_j in range(len(splitted_offset)):
2015            s2 = splitted_offset[subpath_j]
2016            if (P(s1[0][1]) - P(s2[-1][1])).l2() < 0.0001 and ((subpath_i + 1) % len(splitted_offset) != subpath_j):
2017                if dot(csp_normalized_normal(s2[-2], s2[-1], 1.), csp_normalized_slope(s1[0], s1[1], 0.)) * r < -0.0001:
2018                    clip = True
2019                    break
2020            if (P(s2[0][1]) - P(s1[-1][1])).l2() < 0.0001 and ((subpath_j + 1) % len(splitted_offset) != subpath_i):
2021                if dot(csp_normalized_normal(s2[0], s2[1], 0.), csp_normalized_slope(s1[-2], s1[-1], 1.)) * r > 0.0001:
2022                    clip = True
2023                    break
2024
2025        if not clip:
2026            result += [s1[:]]
2027        elif options.offset_draw_clippend_path:
2028            draw_csp([s1], width=.1)
2029            draw_pointer(csp_at_t(s2[-2], s2[-1], 1.) +
2030                         (P(csp_at_t(s2[-2], s2[-1], 1.)) + P(csp_normalized_normal(s2[-2], s2[-1], 1.)) * 10).to_list(), "Green", "line")
2031            draw_pointer(csp_at_t(s1[0], s1[1], 0.) +
2032                         (P(csp_at_t(s1[0], s1[1], 0.)) + P(csp_normalized_slope(s1[0], s1[1], 0.)) * 10).to_list(), "Red", "line")
2033
2034    # Now join all together and check closure and orientation of result
2035    joined_result = csp_join_subpaths(result)
2036    # Check if each subpath from joined_result is closed
2037
2038    for s in joined_result[:]:
2039        if csp_subpaths_end_to_start_distance2(s, s) > 0.001:
2040            # Remove open parts
2041            if options.offset_draw_clippend_path:
2042                draw_csp([s], width=1)
2043                draw_pointer(s[0][1], comment=csp_subpaths_end_to_start_distance2(s, s))
2044                draw_pointer(s[-1][1], comment=csp_subpaths_end_to_start_distance2(s, s))
2045            joined_result.remove(s)
2046        else:
2047            # Remove small parts
2048            minx, miny, maxx, maxy = csp_true_bounds([s])
2049            if (minx[0] - maxx[0]) ** 2 + (miny[1] - maxy[1]) ** 2 < 0.1:
2050                joined_result.remove(s)
2051    print_("Clipped and joined path in {}".format(time.time() - time_))
2052
2053    ########################################################################
2054    # Now to the Dummy clipping: remove parts from split offset if their
2055    # centers are  closer to the original path than offset radius.
2056    ########################################################################
2057
2058    if abs(r * .01) < 1:
2059        r1 = (0.99 * r) ** 2
2060        r2 = (1.01 * r) ** 2
2061    else:
2062        r1 = (abs(r) - 1) ** 2
2063        r2 = (abs(r) + 1) ** 2
2064
2065    for s in joined_result[:]:
2066        dist = csp_to_point_distance(original_csp, s[int(len(s) / 2)][1], dist_bounds=[r1, r2])
2067        if not r1 < dist[0] < r2:
2068            joined_result.remove(s)
2069            if options.offset_draw_clippend_path:
2070                draw_csp([s], comment=math.sqrt(dist[0]))
2071                draw_pointer(csp_at_t(csp[dist[1]][dist[2] - 1], csp[dist[1]][dist[2]], dist[3]) + s[int(len(s) / 2)][1], "blue", "line", comment=[math.sqrt(dist[0]), i, j, sp])
2072
2073    print_("-----------------------------")
2074    print_("Total offset time {}".format(time.time() - time_start))
2075    print_()
2076    return joined_result
2077
2078
2079################################################################################
2080#
2081# Biarc function
2082#
2083# Calculates biarc approximation of cubic super path segment
2084#  splits segment if needed or approximates it with straight line
2085#
2086################################################################################
2087def biarc(sp1, sp2, z1, z2, depth=0):
2088    def biarc_split(sp1, sp2, z1, z2, depth):
2089        if depth < options.biarc_max_split_depth:
2090            sp1, sp2, sp3 = csp_split(sp1, sp2)
2091            l1 = cspseglength(sp1, sp2)
2092            l2 = cspseglength(sp2, sp3)
2093            if l1 + l2 == 0:
2094                zm = z1
2095            else:
2096                zm = z1 + (z2 - z1) * l1 / (l1 + l2)
2097            return biarc(sp1, sp2, z1, zm, depth + 1) + biarc(sp2, sp3, zm, z2, depth + 1)
2098        else:
2099            return [[sp1[1], 'line', 0, 0, sp2[1], [z1, z2]]]
2100
2101    P0 = P(sp1[1])
2102    P4 = P(sp2[1])
2103    TS = (P(sp1[2]) - P0)
2104    TE = -(P(sp2[0]) - P4)
2105    v = P0 - P4
2106    tsa = TS.angle()
2107    tea = TE.angle()
2108    va = v.angle()
2109    if TE.mag() < STRAIGHT_DISTANCE_TOLERANCE and TS.mag() < STRAIGHT_DISTANCE_TOLERANCE:
2110        # Both tangents are zero - line straight
2111        return [[sp1[1], 'line', 0, 0, sp2[1], [z1, z2]]]
2112    if TE.mag() < STRAIGHT_DISTANCE_TOLERANCE:
2113        TE = -(TS + v).unit()
2114        r = TS.mag() / v.mag() * 2
2115    elif TS.mag() < STRAIGHT_DISTANCE_TOLERANCE:
2116        TS = -(TE + v).unit()
2117        r = 1 / (TE.mag() / v.mag() * 2)
2118    else:
2119        r = TS.mag() / TE.mag()
2120    TS = TS.unit()
2121    TE = TE.unit()
2122    tang_are_parallel = ((tsa - tea) % math.pi < STRAIGHT_TOLERANCE or math.pi - (tsa - tea) % math.pi < STRAIGHT_TOLERANCE)
2123    if (tang_are_parallel and
2124            ((v.mag() < STRAIGHT_DISTANCE_TOLERANCE or TE.mag() < STRAIGHT_DISTANCE_TOLERANCE or TS.mag() < STRAIGHT_DISTANCE_TOLERANCE) or
2125             1 - abs(TS * v / (TS.mag() * v.mag())) < STRAIGHT_TOLERANCE)):
2126        # Both tangents are parallel and start and end are the same - line straight
2127        # or one of tangents still smaller then tolerance
2128
2129        # Both tangents and v are parallel - line straight
2130        return [[sp1[1], 'line', 0, 0, sp2[1], [z1, z2]]]
2131
2132    c = v * v
2133    b = 2 * v * (r * TS + TE)
2134    a = 2 * r * (TS * TE - 1)
2135    if v.mag() == 0:
2136        return biarc_split(sp1, sp2, z1, z2, depth)
2137    asmall = abs(a) < 10 ** -10
2138    bsmall = abs(b) < 10 ** -10
2139    csmall = abs(c) < 10 ** -10
2140    if asmall and b != 0:
2141        beta = -c / b
2142    elif csmall and a != 0:
2143        beta = -b / a
2144    elif not asmall:
2145        discr = b * b - 4 * a * c
2146        if discr < 0:
2147            raise ValueError(a, b, c, discr)
2148        disq = discr ** .5
2149        beta1 = (-b - disq) / 2 / a
2150        beta2 = (-b + disq) / 2 / a
2151        if beta1 * beta2 > 0:
2152            raise ValueError(a, b, c, disq, beta1, beta2)
2153        beta = max(beta1, beta2)
2154    elif asmall and bsmall:
2155        return biarc_split(sp1, sp2, z1, z2, depth)
2156    alpha = beta * r
2157    ab = alpha + beta
2158    P1 = P0 + alpha * TS
2159    P3 = P4 - beta * TE
2160    P2 = (beta / ab) * P1 + (alpha / ab) * P3
2161
2162    def calculate_arc_params(P0, P1, P2):
2163        D = (P0 + P2) / 2
2164        if (D - P1).mag() == 0:
2165            return None, None
2166        R = D - ((D - P0).mag() ** 2 / (D - P1).mag()) * (P1 - D).unit()
2167        p0a = (P0 - R).angle() % (2 * math.pi)
2168        p1a = (P1 - R).angle() % (2 * math.pi)
2169        p2a = (P2 - R).angle() % (2 * math.pi)
2170        alpha = (p2a - p0a) % (2 * math.pi)
2171        if (p0a < p2a and (p1a < p0a or p2a < p1a)) or (p2a < p1a < p0a):
2172            alpha = -2 * math.pi + alpha
2173        if abs(R.x) > 1000000 or abs(R.y) > 1000000 or (R - P0).mag() < options.min_arc_radius ** 2:
2174            return None, None
2175        else:
2176            return R, alpha
2177
2178    R1, a1 = calculate_arc_params(P0, P1, P2)
2179    R2, a2 = calculate_arc_params(P2, P3, P4)
2180    if R1 is None or R2 is None or (R1 - P0).mag() < STRAIGHT_TOLERANCE or (R2 - P2).mag() < STRAIGHT_TOLERANCE:
2181        return [[sp1[1], 'line', 0, 0, sp2[1], [z1, z2]]]
2182
2183    d = csp_to_arc_distance(sp1, sp2, [P0, P2, R1, a1], [P2, P4, R2, a2])
2184    if d > options.biarc_tolerance and depth < options.biarc_max_split_depth:
2185        return biarc_split(sp1, sp2, z1, z2, depth)
2186    else:
2187        if R2.mag() * a2 == 0:
2188            zm = z2
2189        else:
2190            zm = z1 + (z2 - z1) * (abs(R1.mag() * a1)) / (abs(R2.mag() * a2) + abs(R1.mag() * a1))
2191
2192        l = (P0 - P2).l2()
2193        if l < EMC_TOLERANCE_EQUAL ** 2 or l < EMC_TOLERANCE_EQUAL ** 2 * R1.l2() / 100:
2194            # arc should be straight otherwise it could be treated as full circle
2195            arc1 = [sp1[1], 'line', 0, 0, [P2.x, P2.y], [z1, zm]]
2196        else:
2197            arc1 = [sp1[1], 'arc', [R1.x, R1.y], a1, [P2.x, P2.y], [z1, zm]]
2198
2199        l = (P4 - P2).l2()
2200        if l < EMC_TOLERANCE_EQUAL ** 2 or l < EMC_TOLERANCE_EQUAL ** 2 * R2.l2() / 100:
2201            # arc should be straight otherwise it could be treated as full circle
2202            arc2 = [[P2.x, P2.y], 'line', 0, 0, [P4.x, P4.y], [zm, z2]]
2203        else:
2204            arc2 = [[P2.x, P2.y], 'arc', [R2.x, R2.y], a2, [P4.x, P4.y], [zm, z2]]
2205
2206        return [arc1, arc2]
2207
2208
2209class Postprocessor(object):
2210    def __init__(self, error_function_handler):
2211        self.error = error_function_handler
2212        self.functions = {
2213            "remap": self.remap,
2214            "remapi": self.remapi,
2215            "scale": self.scale,
2216            "move": self.move,
2217            "flip": self.flip_axis,
2218            "flip_axis": self.flip_axis,
2219            "round": self.round_coordinates,
2220            "parameterize": self.parameterize,
2221            "regex": self.re_sub_on_gcode_lines
2222        }
2223
2224    def process(self, command):
2225        command = re.sub(r"\\\\", ":#:#:slash:#:#:", command)
2226        command = re.sub(r"\\;", ":#:#:semicolon:#:#:", command)
2227        command = command.split(";")
2228        for s in command:
2229            s = re.sub(":#:#:slash:#:#:", "\\\\", s)
2230            s = re.sub(":#:#:semicolon:#:#:", "\\;", s)
2231            s = s.strip()
2232            if s != "":
2233                self.parse_command(s)
2234
2235    def parse_command(self, command):
2236        r = re.match(r"([A-Za-z0-9_]+)\s*\(\s*(.*)\)", command)
2237        if not r:
2238            self.error("Parse error while postprocessing.\n(Command: '{}')".format(command), "error")
2239        function = r.group(1).lower()
2240        parameters = r.group(2)
2241        if function in self.functions:
2242            print_("Postprocessor: executing function {}({})".format(function, parameters))
2243            self.functions[function](parameters)
2244        else:
2245            self.error("Unrecognized function '{}' while postprocessing.\n(Command: '{}')".format(function, command), "error")
2246
2247    def re_sub_on_gcode_lines(self, parameters):
2248        gcode = self.gcode.split("\n")
2249        self.gcode = ""
2250        try:
2251            for line in gcode:
2252                self.gcode += eval("re.sub({},line)".format(parameters)) + "\n"
2253
2254        except Exception as ex:
2255            self.error("Bad parameters for regexp. "
2256                       "They should be as re.sub pattern and replacement parameters! "
2257                       "For example: r\"G0(\\d)\", r\"G\\1\" \n"
2258                       "(Parameters: '{}')\n {}".format(parameters, ex), "error")
2259
2260    def remapi(self, parameters):
2261        self.remap(parameters, case_sensitive=True)
2262
2263    def remap(self, parameters, case_sensitive=False):
2264        # remap parameters should be like "x->y,y->x"
2265        parameters = parameters.replace("\\,", ":#:#:coma:#:#:")
2266        parameters = parameters.split(",")
2267        pattern = []
2268        remap = []
2269        for s in parameters:
2270            s = s.replace(":#:#:coma:#:#:", "\\,")
2271            r = re.match("""\\s*(\'|\")(.*)\\1\\s*->\\s*(\'|\")(.*)\\3\\s*""", s)
2272            if not r:
2273                self.error("Bad parameters for remap.\n(Parameters: '{}')".format(parameters), "error")
2274            pattern += [r.group(2)]
2275            remap += [r.group(4)]
2276
2277        for i in range(len(pattern)):
2278            if case_sensitive:
2279                self.gcode = ireplace(self.gcode, pattern[i], ":#:#:remap_pattern{}:#:#:".format(i))
2280            else:
2281                self.gcode = self.gcode.replace(pattern[i], ":#:#:remap_pattern{}:#:#:".format(i))
2282
2283        for i in range(len(remap)):
2284            self.gcode = self.gcode.replace(":#:#:remap_pattern{}:#:#:".format(i), remap[i])
2285
2286    def transform(self, move, scale):
2287        axis = ["xi", "yj", "zk", "a"]
2288        flip = scale[0] * scale[1] * scale[2] < 0
2289        gcode = ""
2290        warned = []
2291        r_scale = scale[0]
2292        plane = "g17"
2293        for s in self.gcode.split("\n"):
2294            # get plane selection:
2295            s_wo_comments = re.sub(r"\([^\)]*\)", "", s)
2296            r = re.search(r"(?i)(G17|G18|G19)", s_wo_comments)
2297            if r:
2298                plane = r.group(1).lower()
2299                if plane == "g17":
2300                    r_scale = scale[0]  # plane XY -> scale x
2301                if plane == "g18":
2302                    r_scale = scale[0]  # plane XZ -> scale x
2303                if plane == "g19":
2304                    r_scale = scale[1]  # plane YZ -> scale y
2305            # Raise warning if scale factors are not the game for G02 and G03
2306            if plane not in warned:
2307                r = re.search(r"(?i)(G02|G03)", s_wo_comments)
2308                if r:
2309                    if plane == "g17" and scale[0] != scale[1]:
2310                        self.error("Post-processor: Scale factors for X and Y axis are not the same. G02 and G03 codes will be corrupted.")
2311                    if plane == "g18" and scale[0] != scale[2]:
2312                        self.error("Post-processor: Scale factors for X and Z axis are not the same. G02 and G03 codes will be corrupted.")
2313                    if plane == "g19" and scale[1] != scale[2]:
2314                        self.error("Post-processor: Scale factors for Y and Z axis are not the same. G02 and G03 codes will be corrupted.")
2315                    warned += [plane]
2316            # Transform
2317            for i in range(len(axis)):
2318                if move[i] != 0 or scale[i] != 1:
2319                    for a in axis[i]:
2320                        r = re.search(r"(?i)(" + a + r")\s*(-?)\s*(\d*\.?\d*)", s)
2321                        if r and r.group(3) != "":
2322                            s = re.sub(r"(?i)(" + a + r")\s*(-?)\s*(\d*\.?\d*)", r"\1 {:f}".format(float(r.group(2) + r.group(3)) * scale[i] + (move[i] if a not in ["i", "j", "k"] else 0)), s)
2323            # scale radius R
2324            if r_scale != 1:
2325                r = re.search(r"(?i)(r)\s*(-?\s*(\d*\.?\d*))", s)
2326                if r and r.group(3) != "":
2327                    try:
2328                        s = re.sub(r"(?i)(r)\s*(-?)\s*(\d*\.?\d*)", r"\1 {:f}".format(float(r.group(2) + r.group(3)) * r_scale), s)
2329                    except:
2330                        pass
2331
2332            gcode += s + "\n"
2333
2334        self.gcode = gcode
2335        if flip:
2336            self.remapi("'G02'->'G03', 'G03'->'G02'")
2337
2338    def parameterize(self, parameters):
2339        planes = []
2340        feeds = {}
2341        coords = []
2342        gcode = ""
2343        coords_def = {"x": "x", "y": "y", "z": "z", "i": "x", "j": "y", "k": "z", "a": "a"}
2344        for s in self.gcode.split("\n"):
2345            s_wo_comments = re.sub(r"\([^\)]*\)", "", s)
2346            # get Planes
2347            r = re.search(r"(?i)(G17|G18|G19)", s_wo_comments)
2348            if r:
2349                plane = r.group(1).lower()
2350                if plane not in planes:
2351                    planes += [plane]
2352            # get Feeds
2353            r = re.search(r"(?i)(F)\s*(-?)\s*(\d*\.?\d*)", s_wo_comments)
2354            if r:
2355                feed = float(r.group(2) + r.group(3))
2356                if feed not in feeds:
2357                    feeds[feed] = "#" + str(len(feeds) + 20)
2358
2359            # Coordinates
2360            for c in "xyzijka":
2361                r = re.search(r"(?i)(" + c + r")\s*(-?)\s*(\d*\.?\d*)", s_wo_comments)
2362                if r:
2363                    c = coords_def[r.group(1).lower()]
2364                    if c not in coords:
2365                        coords += [c]
2366        # Add offset parametrization
2367        offset = {"x": "#6", "y": "#7", "z": "#8", "a": "#9"}
2368        for c in coords:
2369            gcode += "{}  = 0 ({} axis offset)\n".format(offset[c], c.upper())
2370
2371        # Add scale parametrization
2372        if not planes:
2373            planes = ["g17"]
2374        if len(planes) > 1:  # have G02 and G03 in several planes scale_x = scale_y = scale_z required
2375            gcode += "#10 = 1 (Scale factor)\n"
2376            scale = {"x": "#10", "i": "#10", "y": "#10", "j": "#10", "z": "#10", "k": "#10", "r": "#10"}
2377        else:
2378            gcode += "#10 = 1 ({} Scale factor)\n".format({"g17": "XY", "g18": "XZ", "g19": "YZ"}[planes[0]])
2379            gcode += "#11 = 1 ({} Scale factor)\n".format({"g17": "Z", "g18": "Y", "g19": "X"}[planes[0]])
2380            scale = {"x": "#10", "i": "#10", "y": "#10", "j": "#10", "z": "#10", "k": "#10", "r": "#10"}
2381            if "g17" in planes:
2382                scale["z"] = "#11"
2383                scale["k"] = "#11"
2384            if "g18" in planes:
2385                scale["y"] = "#11"
2386                scale["j"] = "#11"
2387            if "g19" in planes:
2388                scale["x"] = "#11"
2389                scale["i"] = "#11"
2390        # Add a scale
2391        if "a" in coords:
2392            gcode += "#12  = 1 (A axis scale)\n"
2393            scale["a"] = "#12"
2394
2395        # Add feed parametrization
2396        for f in feeds:
2397            gcode += "{} = {:f} (Feed definition)\n".format(feeds[f], f)
2398
2399        # Parameterize Gcode
2400        for s in self.gcode.split("\n"):
2401            # feed replace :
2402            r = re.search(r"(?i)(F)\s*(-?)\s*(\d*\.?\d*)", s)
2403            if r and len(r.group(3)) > 0:
2404                s = re.sub(r"(?i)(F)\s*(-?)\s*(\d*\.?\d*)", "F [{}]".format(feeds[float(r.group(2) + r.group(3))]), s)
2405            # Coords XYZA replace
2406            for c in "xyza":
2407                r = re.search(r"(?i)((" + c + r")\s*(-?)\s*(\d*\.?\d*))", s)
2408                if r and len(r.group(4)) > 0:
2409                    s = re.sub(r"(?i)(" + c + r")\s*((-?)\s*(\d*\.?\d*))", r"\1[\2*{}+{}]".format(scale[c], offset[c]), s)
2410
2411            # Coords IJKR replace
2412            for c in "ijkr":
2413                r = re.search(r"(?i)((" + c + r")\s*(-?)\s*(\d*\.?\d*))", s)
2414                if r and len(r.group(4)) > 0:
2415                    s = re.sub(r"(?i)(" + c + r")\s*((-?)\s*(\d*\.?\d*))", r"\1[\2*{}]".format(scale[c]), s)
2416
2417            gcode += s + "\n"
2418
2419        self.gcode = gcode
2420
2421    def round_coordinates(self, parameters):
2422        try:
2423            round_ = int(parameters)
2424        except:
2425            self.error("Bad parameters for round. Round should be an integer! \n(Parameters: '{}')".format(parameters), "error")
2426        gcode = ""
2427        for s in self.gcode.split("\n"):
2428            for a in "xyzijkaf":
2429                r = re.search(r"(?i)(" + a + r")\s*(-?\s*(\d*\.?\d*))", s)
2430                if r:
2431
2432                    if r.group(2) != "":
2433                        s = re.sub(
2434                                r"(?i)(" + a + r")\s*(-?)\s*(\d*\.?\d*)",
2435                                (r"\1 %0." + str(round_) + "f" if round_ > 0 else r"\1 %d") % round(float(r.group(2)), round_),
2436                                s)
2437            gcode += s + "\n"
2438        self.gcode = gcode
2439
2440    def scale(self, parameters):
2441        parameters = parameters.split(",")
2442        scale = [1., 1., 1., 1.]
2443        try:
2444            for i in range(len(parameters)):
2445                if float(parameters[i]) == 0:
2446                    self.error("Bad parameters for scale. Scale should not be 0 at any axis! \n(Parameters: '{}')".format(parameters), "error")
2447                scale[i] = float(parameters[i])
2448        except:
2449            self.error("Bad parameters for scale.\n(Parameters: '{}')".format(parameters), "error")
2450        self.transform([0, 0, 0, 0], scale)
2451
2452    def move(self, parameters):
2453        parameters = parameters.split(",")
2454        move = [0., 0., 0., 0.]
2455        try:
2456            for i in range(len(parameters)):
2457                move[i] = float(parameters[i])
2458        except:
2459            self.error("Bad parameters for move.\n(Parameters: '{}')".format(parameters), "error")
2460        self.transform(move, [1., 1., 1., 1.])
2461
2462    def flip_axis(self, parameters):
2463        parameters = parameters.lower()
2464        axis = {"x": 1., "y": 1., "z": 1., "a": 1.}
2465        for p in parameters:
2466            if p in [",", " ", "    ", "\r", "'", '"']:
2467                continue
2468            if p not in ["x", "y", "z", "a"]:
2469                self.error("Bad parameters for flip_axis. Parameter should be string consists of 'xyza' \n(Parameters: '{}')".format(parameters), "error")
2470            axis[p] = -axis[p]
2471        self.scale("{:f},{:f},{:f},{:f}".format(axis["x"], axis["y"], axis["z"], axis["a"]))
2472
2473
2474################################################################################
2475# Polygon class
2476################################################################################
2477class Polygon(object):
2478    def __init__(self, polygon=None):
2479        self.polygon = [] if polygon is None else polygon[:]
2480
2481    def move(self, x, y):
2482        for i in range(len(self.polygon)):
2483            for j in range(len(self.polygon[i])):
2484                self.polygon[i][j][0] += x
2485                self.polygon[i][j][1] += y
2486
2487    def bounds(self):
2488        minx = 1e400
2489        miny = 1e400
2490        maxx = -1e400
2491        maxy = -1e400
2492        for poly in self.polygon:
2493            for p in poly:
2494                if minx > p[0]:
2495                    minx = p[0]
2496                if miny > p[1]:
2497                    miny = p[1]
2498                if maxx < p[0]:
2499                    maxx = p[0]
2500                if maxy < p[1]:
2501                    maxy = p[1]
2502        return minx * 1, miny * 1, maxx * 1, maxy * 1
2503
2504    def width(self):
2505        b = self.bounds()
2506        return b[2] - b[0]
2507
2508    def rotate_(self, sin, cos):
2509        self.polygon = [
2510            [
2511                [point[0] * cos - point[1] * sin, point[0] * sin + point[1] * cos] for point in subpoly
2512            ]
2513            for subpoly in self.polygon
2514        ]
2515
2516    def rotate(self, a):
2517        cos = math.cos(a)
2518        sin = math.sin(a)
2519        self.rotate_(sin, cos)
2520
2521    def drop_into_direction(self, direction, surface):
2522        # Polygon is a list of simple polygons
2523        # Surface is a polygon + line y = 0
2524        # Direction is [dx,dy]
2525        if len(self.polygon) == 0 or len(self.polygon[0]) == 0:
2526            return
2527        if direction[0] ** 2 + direction[1] ** 2 < 1e-10:
2528            return
2529        direction = normalize(direction)
2530        sin = direction[0]
2531        cos = -direction[1]
2532        self.rotate_(-sin, cos)
2533        surface.rotate_(-sin, cos)
2534        self.drop_down(surface, zerro_plane=False)
2535        self.rotate_(sin, cos)
2536        surface.rotate_(sin, cos)
2537
2538    def centroid(self):
2539        centroids = []
2540        sa = 0
2541        for poly in self.polygon:
2542            cx = 0
2543            cy = 0
2544            a = 0
2545            for i in range(len(poly)):
2546                [x1, y1] = poly[i - 1]
2547                [x2, y2] = poly[i]
2548                cx += (x1 + x2) * (x1 * y2 - x2 * y1)
2549                cy += (y1 + y2) * (x1 * y2 - x2 * y1)
2550                a += (x1 * y2 - x2 * y1)
2551            a *= 3.
2552            if abs(a) > 0:
2553                cx /= a
2554                cy /= a
2555                sa += abs(a)
2556                centroids += [[cx, cy, a]]
2557        if sa == 0:
2558            return [0., 0.]
2559        cx = 0
2560        cy = 0
2561        for c in centroids:
2562            cx += c[0] * c[2]
2563            cy += c[1] * c[2]
2564        cx /= sa
2565        cy /= sa
2566        return [cx, cy]
2567
2568    def drop_down(self, surface, zerro_plane=True):
2569        # Polygon is a list of simple polygons
2570        # Surface is a polygon + line y = 0
2571        # Down means min y (0,-1)
2572        if len(self.polygon) == 0 or len(self.polygon[0]) == 0:
2573            return
2574        # Get surface top point
2575        top = surface.bounds()[3]
2576        if zerro_plane:
2577            top = max(0, top)
2578        # Get polygon bottom point
2579        bottom = self.bounds()[1]
2580        self.move(0, top - bottom + 10)
2581        # Now get shortest distance from surface to polygon in positive x=0 direction
2582        # Such distance = min(distance(vertex, edge)...)  where edge from surface and
2583        # vertex from polygon and vice versa...
2584        dist = 1e300
2585        for poly in surface.polygon:
2586            for i in range(len(poly)):
2587                for poly1 in self.polygon:
2588                    for i1 in range(len(poly1)):
2589                        st = poly[i - 1]
2590                        end = poly[i]
2591                        vertex = poly1[i1]
2592                        if st[0] <= vertex[0] <= end[0] or end[0] <= vertex[0] <= st[0]:
2593                            if st[0] == end[0]:
2594                                d = min(vertex[1] - st[1], vertex[1] - end[1])
2595                            else:
2596                                d = vertex[1] - st[1] - (end[1] - st[1]) * (vertex[0] - st[0]) / (end[0] - st[0])
2597                            if dist > d:
2598                                dist = d
2599                        # and vice versa just change the sign because vertex now under the edge
2600                        st = poly1[i1 - 1]
2601                        end = poly1[i1]
2602                        vertex = poly[i]
2603                        if st[0] <= vertex[0] <= end[0] or end[0] <= vertex[0] <= st[0]:
2604                            if st[0] == end[0]:
2605                                d = min(- vertex[1] + st[1], -vertex[1] + end[1])
2606                            else:
2607                                d = - vertex[1] + st[1] + (end[1] - st[1]) * (vertex[0] - st[0]) / (end[0] - st[0])
2608                            if dist > d:
2609                                dist = d
2610
2611        if zerro_plane and dist > 10 + top:
2612            dist = 10 + top
2613        self.move(0, -dist)
2614
2615    def draw(self, color="#075", width=.1, group=None):
2616        csp = [csp_subpath_line_to([], poly + [poly[0]]) for poly in self.polygon]
2617        draw_csp(csp, width=width, group=group)
2618
2619    def add(self, add):
2620        if type(add) == type([]):
2621            self.polygon += add[:]
2622        else:
2623            self.polygon += add.polygon[:]
2624
2625    def point_inside(self, p):
2626        inside = False
2627        for poly in self.polygon:
2628            for i in range(len(poly)):
2629                st = poly[i - 1]
2630                end = poly[i]
2631                if p == st or p == end:
2632                    return True  # point is a vertex = point is on the edge
2633                if st[0] > end[0]:
2634                    st, end = end, st  # This will be needed to check that edge if open only at right end
2635                c = (p[1] - st[1]) * (end[0] - st[0]) - (end[1] - st[1]) * (p[0] - st[0])
2636                if st[0] <= p[0] < end[0]:
2637                    if c < 0:
2638                        inside = not inside
2639                    elif c == 0:
2640                        return True  # point is on the edge
2641                elif st[0] == end[0] == p[0] and (st[1] <= p[1] <= end[1] or end[1] <= p[1] <= st[1]):  # point is on the edge
2642                    return True
2643        return inside
2644
2645    def hull(self):
2646        # Add vertices at all self intersection points.
2647        hull = []
2648        for i1 in range(len(self.polygon)):
2649            poly1 = self.polygon[i1]
2650            poly_ = []
2651            for j1 in range(len(poly1)):
2652                s = poly1[j1 - 1]
2653                e = poly1[j1]
2654                poly_ += [s]
2655
2656                # Check self intersections
2657                for j2 in range(j1 + 1, len(poly1)):
2658                    s1 = poly1[j2 - 1]
2659                    e1 = poly1[j2]
2660                    int_ = line_line_intersection_points(s, e, s1, e1)
2661                    for p in int_:
2662                        if point_to_point_d2(p, s) > 0.000001 and point_to_point_d2(p, e) > 0.000001:
2663                            poly_ += [p]
2664                # Check self intersections with other polys
2665                for i2 in range(len(self.polygon)):
2666                    if i1 == i2:
2667                        continue
2668                    poly2 = self.polygon[i2]
2669                    for j2 in range(len(poly2)):
2670                        s1 = poly2[j2 - 1]
2671                        e1 = poly2[j2]
2672                        int_ = line_line_intersection_points(s, e, s1, e1)
2673                        for p in int_:
2674                            if point_to_point_d2(p, s) > 0.000001 and point_to_point_d2(p, e) > 0.000001:
2675                                poly_ += [p]
2676            hull += [poly_]
2677        # Create the dictionary containing all edges in both directions
2678        edges = {}
2679        for poly in self.polygon:
2680            for i in range(len(poly)):
2681                s = tuple(poly[i - 1])
2682                e = tuple(poly[i])
2683                if point_to_point_d2(e, s) < 0.000001:
2684                    continue
2685                break_s = False
2686                break_e = False
2687                for p in edges:
2688                    if point_to_point_d2(p, s) < 0.000001:
2689                        break_s = True
2690                        s = p
2691                    if point_to_point_d2(p, e) < 0.000001:
2692                        break_e = True
2693                        e = p
2694                    if break_s and break_e:
2695                        break
2696                l = point_to_point_d(s, e)
2697                if not break_s and not break_e:
2698                    edges[s] = [[s, e, l]]
2699                    edges[e] = [[e, s, l]]
2700                else:
2701                    if e in edges:
2702                        for edge in edges[e]:
2703                            if point_to_point_d2(edge[1], s) < 0.000001:
2704                                break
2705                        if point_to_point_d2(edge[1], s) > 0.000001:
2706                            edges[e] += [[e, s, l]]
2707                    else:
2708                        edges[e] = [[e, s, l]]
2709                    if s in edges:
2710                        for edge in edges[s]:
2711                            if point_to_point_d2(edge[1], e) < 0.000001:
2712                                break
2713                        if point_to_point_d2(edge[1], e) > 0.000001:
2714                            edges[s] += [[s, e, l]]
2715                    else:
2716                        edges[s] = [[s, e, l]]
2717
2718        def angle_quadrant(sin, cos):
2719            # quadrants are (0,pi/2], (pi/2,pi], (pi,3*pi/2], (3*pi/2, 2*pi], i.e. 0 is in the 4-th quadrant
2720            if sin > 0 and cos >= 0:
2721                return 1
2722            if sin >= 0 and cos < 0:
2723                return 2
2724            if sin < 0 and cos <= 0:
2725                return 3
2726            if sin <= 0 and cos > 0:
2727                return 4
2728
2729        def angle_is_less(sin, cos, sin1, cos1):
2730            # 0 = 2*pi is the largest angle
2731            if [sin1, cos1] == [0, 1]:
2732                return True
2733            if [sin, cos] == [0, 1]:
2734                return False
2735            if angle_quadrant(sin, cos) > angle_quadrant(sin1, cos1):
2736                return False
2737            if angle_quadrant(sin, cos) < angle_quadrant(sin1, cos1):
2738                return True
2739            if sin >= 0 and cos > 0:
2740                return sin < sin1
2741            if sin > 0 and cos <= 0:
2742                return sin > sin1
2743            if sin <= 0 and cos < 0:
2744                return sin > sin1
2745            if sin < 0 and cos >= 0:
2746                return sin < sin1
2747
2748        def get_closes_edge_by_angle(edges, last):
2749            # Last edge is normalized vector of the last edge.
2750            min_angle = [0, 1]
2751            next = last
2752            last_edge = [(last[0][0] - last[1][0]) / last[2], (last[0][1] - last[1][1]) / last[2]]
2753            for p in edges:
2754
2755                cur = [(p[1][0] - p[0][0]) / p[2], (p[1][1] - p[0][1]) / p[2]]
2756                cos = dot(cur, last_edge)
2757                sin = cross(cur, last_edge)
2758
2759                if angle_is_less(sin, cos, min_angle[0], min_angle[1]):
2760                    min_angle = [sin, cos]
2761                    next = p
2762
2763            return next
2764
2765        # Join edges together into new polygon cutting the vertexes inside new polygon
2766        self.polygon = []
2767        len_edges = sum([len(edges[p]) for p in edges])
2768        loops = 0
2769
2770        while len(edges) > 0:
2771            poly = []
2772            if loops > len_edges:
2773                raise ValueError("Hull error")
2774            loops += 1
2775            # Find left most vertex.
2776            start = (1e100, 1)
2777            for edge in edges:
2778                start = min(start, min(edges[edge]))
2779            last = [(start[0][0] - 1, start[0][1]), start[0], 1]
2780            first_run = True
2781            loops1 = 0
2782            while last[1] != start[0] or first_run:
2783                first_run = False
2784                if loops1 > len_edges:
2785                    raise ValueError("Hull error")
2786                loops1 += 1
2787                next = get_closes_edge_by_angle(edges[last[1]], last)
2788
2789                last = next
2790                poly += [list(last[0])]
2791            self.polygon += [poly]
2792            # Remove all edges that are intersects new poly (any vertex inside new poly)
2793            poly_ = Polygon([poly])
2794            for p in edges.keys()[:]:
2795                if poly_.point_inside(list(p)):
2796                    del edges[p]
2797        self.draw(color="Green", width=1)
2798
2799
2800################################################################################
2801#
2802# Gcodetools class
2803#
2804################################################################################
2805
2806class Gcodetools(inkex.EffectExtension):
2807    multi_inx = True # XXX Remove this after refactoring
2808
2809    def export_gcode(self, gcode, no_headers=False):
2810        if self.options.postprocessor != "" or self.options.postprocessor_custom != "":
2811            postprocessor = Postprocessor(self.error)
2812            postprocessor.gcode = gcode
2813            if self.options.postprocessor != "":
2814                postprocessor.process(self.options.postprocessor)
2815            if self.options.postprocessor_custom != "":
2816                postprocessor.process(self.options.postprocessor_custom)
2817
2818        if not no_headers:
2819            postprocessor.gcode = self.header + postprocessor.gcode + self.footer
2820
2821        with open(os.path.join(self.options.directory, self.options.file), "w") as f:
2822            f.write(postprocessor.gcode)
2823
2824    ################################################################################
2825    # In/out paths:
2826    # TODO move it to the bottom
2827    ################################################################################
2828    def plasma_prepare_path(self):
2829        self.get_info_plus()
2830
2831        def add_arc(sp1, sp2, end=False, l=10., r=10.):
2832            if not end:
2833                n = csp_normalized_normal(sp1, sp2, 0.)
2834                return csp_reverse([arc_from_s_r_n_l(sp1[1], r, n, -l)])[0]
2835            else:
2836                n = csp_normalized_normal(sp1, sp2, 1.)
2837                return arc_from_s_r_n_l(sp2[1], r, n, l)
2838
2839        def add_normal(sp1, sp2, end=False, l=10., r=10.):
2840            # r is needed only for be compatible with add_arc
2841            if not end:
2842                n = csp_normalized_normal(sp1, sp2, 0.)
2843                p = [n[0] * l + sp1[1][0], n[1] * l + sp1[1][1]]
2844                return csp_subpath_line_to([], [p, sp1[1]])
2845            else:
2846                n = csp_normalized_normal(sp1, sp2, 1.)
2847                p = [n[0] * l + sp2[1][0], n[1] * l + sp2[1][1]]
2848                return csp_subpath_line_to([], [sp2[1], p])
2849
2850        def add_tangent(sp1, sp2, end=False, l=10., r=10.):
2851            # r is needed only for be compatible with add_arc
2852            if not end:
2853                n = csp_normalized_slope(sp1, sp2, 0.)
2854                p = [-n[0] * l + sp1[1][0], -n[1] * l + sp1[1][1]]
2855                return csp_subpath_line_to([], [p, sp1[1]])
2856            else:
2857                n = csp_normalized_slope(sp1, sp2, 1.)
2858                p = [n[0] * l + sp2[1][0], n[1] * l + sp2[1][1]]
2859                return csp_subpath_line_to([], [sp2[1], p])
2860
2861        if not self.options.in_out_path and not self.options.plasma_prepare_corners and self.options.in_out_path_do_not_add_reference_point:
2862            self.error("Warning! Extension is not said to do anything! Enable one of Create in-out paths or Prepare corners checkboxes or disable Do not add in-out reference point!")
2863            return
2864
2865        # Add in-out-reference point if there is no one yet.
2866        if ((len(self.in_out_reference_points) == 0 and self.options.in_out_path
2867             or not self.options.in_out_path and not self.options.plasma_prepare_corners)
2868                and not self.options.in_out_path_do_not_add_reference_point):
2869            self.options.orientation_points_count = "in-out reference point"
2870            self.orientation()
2871
2872        if self.options.in_out_path or self.options.plasma_prepare_corners:
2873            self.set_markers()
2874            add_func = {"Round": add_arc, "Perpendicular": add_normal, "Tangent": add_tangent}[self.options.in_out_path_type]
2875            if self.options.in_out_path_type == "Round" and self.options.in_out_path_len > self.options.in_out_path_radius * 3 / 2 * math.pi:
2876                self.error("In-out len is to big for in-out radius will cropp it to be r*3/2*pi!")
2877
2878            if self.selected_paths == {} and self.options.auto_select_paths:
2879                self.selected_paths = self.paths
2880                self.error("No paths are selected! Trying to work on all available paths.")
2881
2882            if self.selected_paths == {}:
2883                self.error("Nothing is selected. Please select something.")
2884            a = self.options.plasma_prepare_corners_tolerance
2885            corner_tolerance = cross([1., 0.], [math.cos(a), math.sin(a)])
2886
2887            for layer in self.layers:
2888                if layer in self.selected_paths:
2889                    max_dist = self.transform_scalar(self.options.in_out_path_point_max_dist, layer, reverse=True)
2890                    l = self.transform_scalar(self.options.in_out_path_len, layer, reverse=True)
2891                    plasma_l = self.transform_scalar(self.options.plasma_prepare_corners_distance, layer, reverse=True)
2892                    r = self.transform_scalar(self.options.in_out_path_radius, layer, reverse=True)
2893                    l = min(l, r * 3 / 2 * math.pi)
2894
2895                    for path in self.selected_paths[layer]:
2896                        csp = self.apply_transforms(path, path.path.to_superpath())
2897                        csp = csp_remove_zero_segments(csp)
2898                        res = []
2899
2900                        for subpath in csp:
2901                            # Find closes point to in-out reference point
2902                            # If subpath is open skip this step
2903                            if self.options.in_out_path:
2904                                # split and reverse path for further add in-out points
2905                                if point_to_point_d2(subpath[0][1], subpath[-1][1]) < 1.e-10:
2906                                    d = [1e100, 1, 1, 1.]
2907                                    for p in self.in_out_reference_points:
2908                                        d1 = csp_to_point_distance([subpath], p, dist_bounds=[0, max_dist])
2909                                        if d1[0] < d[0]:
2910                                            d = d1[:]
2911                                            p_ = p
2912                                    if d[0] < max_dist ** 2:
2913                                        # Lets find is there any angles near this point to put in-out path in
2914                                        # the angle if it's possible
2915                                        # remove last node to make iterations easier
2916                                        subpath[0][0] = subpath[-1][0]
2917                                        del subpath[-1]
2918                                        max_cross = [-1e100, None]
2919                                        for j in range(len(subpath)):
2920                                            sp1 = subpath[j - 2]
2921                                            sp2 = subpath[j - 1]
2922                                            sp3 = subpath[j]
2923                                            if point_to_point_d2(sp2[1], p_) < max_dist ** 2:
2924                                                s1 = csp_normalized_slope(sp1, sp2, 1.)
2925                                                s2 = csp_normalized_slope(sp2, sp3, 0.)
2926                                                max_cross = max(max_cross, [cross(s1, s2), j - 1])
2927                                        # return back last point
2928                                        subpath.append(subpath[0])
2929                                        if max_cross[1] is not None and max_cross[0] > corner_tolerance:
2930                                            # there's an angle near the point
2931                                            j = max_cross[1]
2932                                            if j < 0:
2933                                                j -= 1
2934                                            if j != 0:
2935                                                subpath = csp_concat_subpaths(subpath[j:], subpath[:j + 1])
2936                                        else:
2937                                            # have to cut path's segment
2938                                            d, i, j, t = d
2939                                            sp1, sp2, sp3 = csp_split(subpath[j - 1], subpath[j], t)
2940                                            subpath = csp_concat_subpaths([sp2, sp3], subpath[j:], subpath[:j], [sp1, sp2])
2941
2942                            if self.options.plasma_prepare_corners:
2943                                # prepare corners
2944                                # find corners and add some nodes
2945                                # corner at path's start/end is ignored
2946                                res_ = [subpath[0]]
2947                                for sp2, sp3 in zip(subpath[1:], subpath[2:]):
2948                                    sp1 = res_[-1]
2949                                    s1 = csp_normalized_slope(sp1, sp2, 1.)
2950                                    s2 = csp_normalized_slope(sp2, sp3, 0.)
2951                                    if cross(s1, s2) > corner_tolerance:
2952                                        # got a corner to process
2953                                        S1 = P(s1)
2954                                        S2 = P(s2)
2955                                        N = (S1 - S2).unit() * plasma_l
2956                                        SP2 = P(sp2[1])
2957                                        P1 = (SP2 + N)
2958                                        res_ += [
2959                                            [sp2[0], sp2[1], (SP2 + S1 * plasma_l).to_list()],
2960                                            [(P1 - N.ccw() / 2).to_list(), P1.to_list(), (P1 + N.ccw() / 2).to_list()],
2961                                            [(SP2 - S2 * plasma_l).to_list(), sp2[1], sp2[2]]
2962                                        ]
2963                                    else:
2964                                        res_ += [sp2]
2965                                res_ += [sp3]
2966                                subpath = res_
2967                            if self.options.in_out_path:
2968                                # finally add let's add in-out paths...
2969                                subpath = csp_concat_subpaths(
2970                                    add_func(subpath[0], subpath[1], False, l, r),
2971                                    subpath,
2972                                    add_func(subpath[-2], subpath[-1], True, l, r)
2973                                )
2974
2975                            res += [subpath]
2976
2977                        if self.options.in_out_path_replace_original_path:
2978                            path.path = CubicSuperPath(self.apply_transforms(path, res, True))
2979                        else:
2980                            draw_csp(res, width=1, style=MARKER_STYLE["in_out_path_style"])
2981
2982    def add_arguments(self, pars):
2983        add_argument = pars.add_argument
2984        add_argument("-d", "--directory", default="/home/", help="Directory for gcode file")
2985        add_argument("-f", "--filename", dest="file", default="-1.0", help="File name")
2986        add_argument("--add-numeric-suffix-to-filename", type=inkex.Boolean, default=True, help="Add numeric suffix to filename")
2987        add_argument("--Zscale", type=float, default="1.0", help="Scale factor Z")
2988        add_argument("--Zoffset", type=float, default="0.0", help="Offset along Z")
2989        add_argument("-s", "--Zsafe", type=float, default="0.5", help="Z above all obstacles")
2990        add_argument("-z", "--Zsurface", type=float, default="0.0", help="Z of the surface")
2991        add_argument("-c", "--Zdepth", type=float, default="-0.125", help="Z depth of cut")
2992        add_argument("--Zstep", type=float, default="-0.125", help="Z step of cutting")
2993        add_argument("-p", "--feed", type=float, default="4.0", help="Feed rate in unit/min")
2994
2995        add_argument("--biarc-tolerance", type=float, default="1", help="Tolerance used when calculating biarc interpolation.")
2996        add_argument("--biarc-max-split-depth", type=int, default="4", help="Defines maximum depth of splitting while approximating using biarcs.")
2997        add_argument("--path-to-gcode-order", default="path by path", help="Defines cutting order path by path or layer by layer.")
2998        add_argument("--path-to-gcode-depth-function", default="zd", help="Path to gcode depth function.")
2999        add_argument("--path-to-gcode-sort-paths", type=inkex.Boolean, default=True, help="Sort paths to reduce rapid distance.")
3000        add_argument("--comment-gcode", default="", help="Comment Gcode")
3001        add_argument("--comment-gcode-from-properties", type=inkex.Boolean, default=False, help="Get additional comments from Object Properties")
3002
3003        add_argument("--tool-diameter", type=float, default="3", help="Tool diameter used for area cutting")
3004        add_argument("--max-area-curves", type=int, default="100", help="Maximum area curves for each area")
3005        add_argument("--area-inkscape-radius", type=float, default="0", help="Area curves overlapping (depends on tool diameter [0, 0.9])")
3006        add_argument("--area-tool-overlap", type=float, default="-10", help="Radius for preparing curves using inkscape")
3007        add_argument("--unit", default="G21 (All units in mm)", help="Units")
3008        add_argument("--active-tab", type=self.arg_method('tab'), default=self.tab_help, help="Defines which tab is active")
3009
3010        add_argument("--area-fill-angle", type=float, default="0", help="Fill area with lines heading this angle")
3011        add_argument("--area-fill-shift", type=float, default="0", help="Shift the lines by tool d * shift")
3012        add_argument("--area-fill-method", default="zig-zag", help="Filling method either zig-zag or spiral")
3013
3014        add_argument("--area-find-artefacts-diameter", type=float, default="1", help="Artefacts seeking radius")
3015        add_argument("--area-find-artefacts-action", default="mark with an arrow", help="Artefacts action type")
3016
3017        add_argument("--auto_select_paths", type=inkex.Boolean, default=True, help="Select all paths if nothing is selected.")
3018
3019        add_argument("--loft-distances", default="10", help="Distances between paths.")
3020        add_argument("--loft-direction", default="crosswise", help="Direction of loft's interpolation.")
3021        add_argument("--loft-interpolation-degree", type=float, default="2", help="Which interpolation use to loft the paths smooth interpolation or staright.")
3022
3023        add_argument("--min-arc-radius", type=float, default=".1", help="All arc having radius less than minimum will be considered as straight line")
3024
3025        add_argument("--engraving-sharp-angle-tollerance", type=float, default="150", help="All angles thar are less than engraving-sharp-angle-tollerance will be thought sharp")
3026        add_argument("--engraving-max-dist", type=float, default="10", help="Distance from original path where engraving is not needed (usually it's cutting tool diameter)")
3027        add_argument("--engraving-newton-iterations", type=int, default="4", help="Number of sample points used to calculate distance")
3028        add_argument("--engraving-draw-calculation-paths", type=inkex.Boolean, default=False, help="Draw additional graphics to debug engraving path")
3029        add_argument("--engraving-cutter-shape-function", default="w", help="Cutter shape function z(w). Ex. cone: w. ")
3030
3031        add_argument("--lathe-width", type=float, default=10., help="Lathe width")
3032        add_argument("--lathe-fine-cut-width", type=float, default=1., help="Fine cut width")
3033        add_argument("--lathe-fine-cut-count", type=int, default=1., help="Fine cut count")
3034        add_argument("--lathe-create-fine-cut-using", default="Move path", help="Create fine cut using")
3035        add_argument("--lathe-x-axis-remap", default="X", help="Lathe X axis remap")
3036        add_argument("--lathe-z-axis-remap", default="Z", help="Lathe Z axis remap")
3037
3038        add_argument("--lathe-rectangular-cutter-width", type=float, default="4", help="Rectangular cutter width")
3039
3040        add_argument("--create-log", type=inkex.Boolean, dest="log_create_log", default=False, help="Create log files")
3041        add_argument("--log-filename", default='', help="Create log files")
3042
3043        add_argument("--orientation-points-count", default="2", help="Orientation points count")
3044        add_argument("--tools-library-type", default='cylinder cutter', help="Create tools definition")
3045
3046        add_argument("--dxfpoints-action", default='replace', help="dxfpoint sign toggle")
3047
3048        add_argument("--help-language", default='http://www.cnc-club.ru/forum/viewtopic.php?f=33&t=35', help="Open help page in webbrowser.")
3049
3050        add_argument("--offset-radius", type=float, default=10., help="Offset radius")
3051        add_argument("--offset-step", type=float, default=10., help="Offset step")
3052        add_argument("--offset-draw-clippend-path", type=inkex.Boolean, default=False, help="Draw clipped path")
3053        add_argument("--offset-just-get-distance", type=inkex.Boolean, default=False, help="Don't do offset just get distance")
3054
3055        add_argument("--postprocessor", default='', help="Postprocessor command.")
3056        add_argument("--postprocessor-custom", default='', help="Postprocessor custom command.")
3057
3058        add_argument("--graffiti-max-seg-length", type=float, default=1., help="Graffiti maximum segment length.")
3059        add_argument("--graffiti-min-radius", type=float, default=10., help="Graffiti minimal connector's radius.")
3060        add_argument("--graffiti-start-pos", default="(0;0)", help="Graffiti Start position (x;y).")
3061        add_argument("--graffiti-create-linearization-preview", type=inkex.Boolean, default=True, help="Graffiti create linearization preview.")
3062        add_argument("--graffiti-create-preview", type=inkex.Boolean, default=True, help="Graffiti create preview.")
3063        add_argument("--graffiti-preview-size", type=int, default=800, help="Graffiti preview's size.")
3064        add_argument("--graffiti-preview-emmit", type=int, default=800, help="Preview's paint emmit (pts/s).")
3065
3066        add_argument("--in-out-path", type=inkex.Boolean, default=True, help="Create in-out paths")
3067        add_argument("--in-out-path-do-not-add-reference-point", type=inkex.Boolean, default=False, help="Just add reference in-out point")
3068        add_argument("--in-out-path-point-max-dist", type=float, default=10., help="In-out path max distance to reference point")
3069        add_argument("--in-out-path-type", default="Round", help="In-out path type")
3070        add_argument("--in-out-path-len", type=float, default=10., help="In-out path length")
3071        add_argument("--in-out-path-replace-original-path", type=inkex.Boolean, default=False, help="Replace original path")
3072        add_argument("--in-out-path-radius", type=float, default=10., help="In-out path radius for round path")
3073
3074        add_argument("--plasma-prepare-corners", type=inkex.Boolean, default=True, help="Prepare corners")
3075        add_argument("--plasma-prepare-corners-distance", type=float, default=10., help="Stepout distance for corners")
3076        add_argument("--plasma-prepare-corners-tolerance", type=float, default=10., help="Maximum angle for corner (0-180 deg)")
3077
3078    def __init__(self):
3079        super(Gcodetools, self).__init__()
3080        self.default_tool = {
3081            "name": "Default tool",
3082            "id": "default tool",
3083            "diameter": 10.,
3084            "shape": "10",
3085            "penetration angle": 90.,
3086            "penetration feed": 100.,
3087            "depth step": 1.,
3088            "feed": 400.,
3089            "in trajectotry": "",
3090            "out trajectotry": "",
3091            "gcode before path": "",
3092            "gcode after path": "",
3093            "sog": "",
3094            "spinlde rpm": "",
3095            "CW or CCW": "",
3096            "tool change gcode": " ",
3097            "4th axis meaning": " ",
3098            "4th axis scale": 1.,
3099            "4th axis offset": 0.,
3100            "passing feed": "800",
3101            "fine feed": "800",
3102        }
3103        self.tools_field_order = [
3104            'name',
3105            'id',
3106            'diameter',
3107            'feed',
3108            'shape',
3109            'penetration angle',
3110            'penetration feed',
3111            "passing feed",
3112            'depth step',
3113            "in trajectotry",
3114            "out trajectotry",
3115            "gcode before path",
3116            "gcode after path",
3117            "sog",
3118            "spinlde rpm",
3119            "CW or CCW",
3120            "tool change gcode",
3121        ]
3122
3123    def parse_curve(self, p, layer, w=None, f=None):
3124        c = []
3125        if len(p) == 0:
3126            return []
3127        p = self.transform_csp(p, layer)
3128
3129        # Sort to reduce Rapid distance
3130        k = list(range(1, len(p)))
3131        keys = [0]
3132        while len(k) > 0:
3133            end = p[keys[-1]][-1][1]
3134            dist = None
3135            for i in range(len(k)):
3136                start = p[k[i]][0][1]
3137                dist = max((-((end[0] - start[0]) ** 2 + (end[1] - start[1]) ** 2), i), dist)
3138            keys += [k[dist[1]]]
3139            del k[dist[1]]
3140        for k in keys:
3141            subpath = p[k]
3142            c += [[[subpath[0][1][0], subpath[0][1][1]], 'move', 0, 0]]
3143            for i in range(1, len(subpath)):
3144                sp1 = [[subpath[i - 1][j][0], subpath[i - 1][j][1]] for j in range(3)]
3145                sp2 = [[subpath[i][j][0], subpath[i][j][1]] for j in range(3)]
3146                c += biarc(sp1, sp2, 0, 0) if w is None else biarc(sp1, sp2, -f(w[k][i - 1]), -f(w[k][i]))
3147            c += [[[subpath[-1][1][0], subpath[-1][1][1]], 'end', 0, 0]]
3148        return c
3149
3150    ################################################################################
3151    # Draw csp
3152    ################################################################################
3153
3154    def draw_csp(self, csp, layer=None, group=None, fill='none', stroke='#178ade', width=0.354, style=None):
3155        if layer is not None:
3156            csp = self.transform_csp(csp, layer, reverse=True)
3157        if group is None and layer is None:
3158            group = self.document.getroot()
3159        elif group is None and layer is not None:
3160            group = layer
3161        csp = self.apply_transforms(group, csp, reverse=True)
3162        if style is not None:
3163            return draw_csp(csp, group=group, style=style)
3164        else:
3165            return draw_csp(csp, group=group, fill=fill, stroke=stroke, width=width)
3166
3167    def draw_curve(self, curve, layer, group=None, style=MARKER_STYLE["biarc_style"]):
3168        self.set_markers()
3169
3170        for i in [0, 1]:
3171            sid = 'biarc{}_r'.format(i)
3172            style[sid] = style['biarc{}'.format(i)].copy()
3173            style[sid]["marker-start"] = "url(#DrawCurveMarker_r)"
3174            del style[sid]["marker-end"]
3175
3176        if group is None:
3177            group = self.layers[min(1, len(self.layers) - 1)].add(Group(gcodetools="Preview group"))
3178            if not hasattr(self, "preview_groups"):
3179                self.preview_groups = {layer: group}
3180            elif layer not in self.preview_groups:
3181                self.preview_groups[layer] = group
3182            group = self.preview_groups[layer]
3183
3184        s = ''
3185        arcn = 0
3186
3187        transform = self.get_transforms(group)
3188        if transform:
3189            transform = self.reverse_transform(transform)
3190            transform = str(Transform(transform))
3191
3192        a = [0., 0.]
3193        b = [1., 0.]
3194        c = [0., 1.]
3195        k = (b[0] - a[0]) * (c[1] - a[1]) - (c[0] - a[0]) * (b[1] - a[1])
3196        a = self.transform(a, layer, True)
3197        b = self.transform(b, layer, True)
3198        c = self.transform(c, layer, True)
3199        if ((b[0] - a[0]) * (c[1] - a[1]) - (c[0] - a[0]) * (b[1] - a[1])) * k > 0:
3200            reverse_angle = 1
3201        else:
3202            reverse_angle = -1
3203        for sk in curve:
3204            si = sk[:]
3205            si[0] = self.transform(si[0], layer, True)
3206            si[2] = self.transform(si[2], layer, True) if type(si[2]) == type([]) and len(si[2]) == 2 else si[2]
3207
3208            if s != '':
3209                if s[1] == 'line':
3210                    elem = group.add(PathElement(gcodetools="Preview"))
3211                    elem.transform = transform
3212                    elem.style = style['line']
3213                    elem.path = 'M {},{} L {},{}'.format(s[0][0], s[0][1], si[0][0], si[0][1])
3214                elif s[1] == 'arc':
3215                    arcn += 1
3216                    sp = s[0]
3217                    c = s[2]
3218                    s[3] = s[3] * reverse_angle
3219
3220                    a = ((P(si[0]) - P(c)).angle() - (P(s[0]) - P(c)).angle()) % TAU  # s[3]
3221                    if s[3] * a < 0:
3222                        if a > 0:
3223                            a = a - TAU
3224                        else:
3225                            a = TAU + a
3226                    r = math.sqrt((sp[0] - c[0]) ** 2 + (sp[1] - c[1]) ** 2)
3227                    a_st = (math.atan2(sp[0] - c[0], - (sp[1] - c[1])) - math.pi / 2) % (math.pi * 2)
3228                    if a > 0:
3229                        a_end = a_st + a
3230                        st = style['biarc{}'.format(arcn % 2)]
3231                    else:
3232                        a_end = a_st * 1
3233                        a_st = a_st + a
3234                        st = style['biarc{}_r'.format(arcn % 2)]
3235
3236                    elem = group.add(PathElement.arc(c, r, start=a_st, end=a_end,
3237                                                     open=True, gcodetools="Preview"))
3238                    elem.transform = transform
3239                    elem.style = st
3240
3241            s = si
3242
3243    def check_dir(self):
3244        print_("Checking directory: '{}'".format(self.options.directory))
3245        if os.path.isdir(self.options.directory):
3246            if os.path.isfile(os.path.join(self.options.directory, 'header')):
3247                with open(os.path.join(self.options.directory, 'header')) as f:
3248                    self.header = f.read()
3249            else:
3250                self.header = defaults['header']
3251            if os.path.isfile(os.path.join(self.options.directory, 'footer')):
3252                with open(os.path.join(self.options.directory, 'footer')) as f:
3253                    self.footer = f.read()
3254            else:
3255                self.footer = defaults['footer']
3256            self.header += self.options.unit + "\n"
3257        else:
3258            self.error("Directory does not exist! Please specify existing directory at Preferences tab!", "error")
3259            return False
3260
3261        if self.options.add_numeric_suffix_to_filename:
3262            dir_list = os.listdir(self.options.directory)
3263            if "." in self.options.file:
3264                r = re.match(r"^(.*)(\..*)$", self.options.file)
3265                ext = r.group(2)
3266                name = r.group(1)
3267            else:
3268                ext = ""
3269                name = self.options.file
3270            max_n = 0
3271            for s in dir_list:
3272                r = re.match(r"^{}_0*(\d+){}$".format(re.escape(name), re.escape(ext)), s)
3273                if r:
3274                    max_n = max(max_n, int(r.group(1)))
3275            filename = name + "_" + ("0" * (4 - len(str(max_n + 1))) + str(max_n + 1)) + ext
3276            self.options.file = filename
3277
3278        try:
3279            with open(os.path.join(self.options.directory, self.options.file), "w") as f:
3280                pass
3281        except:
3282            self.error("Can not write to specified file!\n{}".format(os.path.join(self.options.directory, self.options.file)), "error")
3283            return False
3284        return True
3285
3286    ################################################################################
3287    #
3288    # Generate Gcode
3289    # Generates Gcode on given curve.
3290    #
3291    # Curve definition [start point, type = {'arc','line','move','end'}, arc center, arc angle, end point, [zstart, zend]]
3292    #
3293    ################################################################################
3294    def generate_gcode(self, curve, layer, depth):
3295        Zauto_scale = self.Zauto_scale[layer]
3296        tool = self.tools[layer][0]
3297        g = ""
3298
3299        def c(c):
3300            c = [c[i] if i < len(c) else None for i in range(6)]
3301            if c[5] == 0:
3302                c[5] = None
3303            s = [" X", " Y", " Z", " I", " J", " K"]
3304            s1 = ["", "", "", "", "", ""]
3305            m = [1, 1, self.options.Zscale * Zauto_scale, 1, 1, self.options.Zscale * Zauto_scale]
3306            a = [0, 0, self.options.Zoffset, 0, 0, 0]
3307            r = ''
3308            for i in range(6):
3309                if c[i] is not None:
3310                    r += s[i] + ("{:f}".format(c[i] * m[i] + a[i])) + s1[i]
3311            return r
3312
3313        def calculate_angle(a, current_a):
3314            return min(
3315                    [abs(a - current_a % TAU + TAU), a + current_a - current_a % TAU + TAU],
3316                    [abs(a - current_a % TAU - TAU), a + current_a - current_a % TAU - TAU],
3317                    [abs(a - current_a % TAU), a + current_a - current_a % TAU])[1]
3318
3319        if len(curve) == 0:
3320            return ""
3321
3322        try:
3323            self.last_used_tool is None
3324        except:
3325            self.last_used_tool = None
3326        print_("working on curve")
3327        print_(curve)
3328
3329        if tool != self.last_used_tool:
3330            g += ("(Change tool to {})\n".format(re.sub("\"'\\(\\)\\\\", " ", tool["name"]))) + tool["tool change gcode"] + "\n"
3331
3332        lg = 'G00'
3333        zs = self.options.Zsafe
3334        f = " F{:f}".format(tool['feed'])
3335        current_a = 0
3336        go_to_safe_distance = "G00" + c([None, None, zs]) + "\n"
3337        penetration_feed = " F{}".format(tool['penetration feed'])
3338        for i in range(1, len(curve)):
3339            #    Creating Gcode for curve between s=curve[i-1] and si=curve[i] start at s[0] end at s[4]=si[0]
3340            s = curve[i - 1]
3341            si = curve[i]
3342            feed = f if lg not in ['G01', 'G02', 'G03'] else ''
3343            if s[1] == 'move':
3344                g += go_to_safe_distance + "G00" + c(si[0]) + "\n" + tool['gcode before path'] + "\n"
3345                lg = 'G00'
3346            elif s[1] == 'end':
3347                g += go_to_safe_distance + tool['gcode after path'] + "\n"
3348                lg = 'G00'
3349            elif s[1] == 'line':
3350                if tool['4th axis meaning'] == "tangent knife":
3351                    a = atan2(si[0][0] - s[0][0], si[0][1] - s[0][1])
3352                    a = calculate_angle(a, current_a)
3353                    g += "G01 A{}\n".format(a * tool['4th axis scale'] + tool['4th axis offset'])
3354                    current_a = a
3355                if lg == "G00":
3356                    g += "G01" + c([None, None, s[5][0] + depth]) + penetration_feed + "(Penetrate)\n"
3357                g += "G01" + c(si[0] + [s[5][1] + depth]) + feed + "\n"
3358                lg = 'G01'
3359            elif s[1] == 'arc':
3360                r = [(s[2][0] - s[0][0]), (s[2][1] - s[0][1])]
3361                if tool['4th axis meaning'] == "tangent knife":
3362                    if s[3] < 0:  # CW
3363                        a1 = atan2(s[2][1] - s[0][1], -s[2][0] + s[0][0]) + math.pi
3364                    else:  # CCW
3365                        a1 = atan2(-s[2][1] + s[0][1], s[2][0] - s[0][0]) + math.pi
3366                    a = calculate_angle(a1, current_a)
3367                    g += "G01 A{}\n".format(a * tool['4th axis scale'] + tool['4th axis offset'])
3368                    current_a = a
3369                    axis4 = " A{}".format((current_a + s[3]) * tool['4th axis scale'] + tool['4th axis offset'])
3370                    current_a = current_a + s[3]
3371                else:
3372                    axis4 = ""
3373                if lg == "G00":
3374                    g += "G01" + c([None, None, s[5][0] + depth]) + penetration_feed + "(Penetrate)\n"
3375                if (r[0] ** 2 + r[1] ** 2) > self.options.min_arc_radius ** 2:
3376                    r1 = (P(s[0]) - P(s[2]))
3377                    r2 = (P(si[0]) - P(s[2]))
3378                    if abs(r1.mag() - r2.mag()) < 0.001:
3379                        g += ("G02" if s[3] < 0 else "G03") + c(si[0] + [s[5][1] + depth, (s[2][0] - s[0][0]), (s[2][1] - s[0][1])]) + feed + axis4 + "\n"
3380                    else:
3381                        r = (r1.mag() + r2.mag()) / 2
3382                        g += ("G02" if s[3] < 0 else "G03") + c(si[0] + [s[5][1] + depth]) + " R{:f}".format(r) + feed + axis4 + "\n"
3383                    lg = 'G02'
3384                else:
3385                    if tool['4th axis meaning'] == "tangent knife":
3386                        a = atan2(si[0][0] - s[0][0], si[0][1] - s[0][1]) + math.pi
3387                        a = calculate_angle(a, current_a)
3388                        g += "G01 A{}\n".format(a * tool['4th axis scale'] + tool['4th axis offset'])
3389                        current_a = a
3390                    g += "G01" + c(si[0] + [s[5][1] + depth]) + feed + "\n"
3391                    lg = 'G01'
3392        if si[1] == 'end':
3393            g += go_to_safe_distance + tool['gcode after path'] + "\n"
3394        return g
3395
3396    def get_transforms(self, g):
3397        root = self.document.getroot()
3398        trans = []
3399        while g != root:
3400            if 'transform' in g.keys():
3401                t = g.get('transform')
3402                t = Transform(t).matrix
3403                trans = (Transform(t) * Transform(trans)).matrix if trans != [] else t
3404
3405                print_(trans)
3406            g = g.getparent()
3407        return trans
3408
3409    def reverse_transform(self, transform):
3410        trans = numpy.array(transform + ([0, 0, 1],))
3411        if numpy.linalg.det(trans) != 0:
3412            trans = numpy.linalg.inv(trans).tolist()[:2]
3413            return trans
3414        else:
3415            return transform
3416
3417    def apply_transforms(self, g, csp, reverse=False):
3418        trans = self.get_transforms(g)
3419        if trans:
3420            if not reverse:
3421                # TODO: This was applyTransformToPath but was deprecated.   Candidate for refactoring.
3422                for comp in csp:
3423                    for ctl in comp:
3424                        for pt in ctl:
3425                            pt[0], pt[1] = Transform(trans).apply_to_point(pt)
3426
3427            else:
3428                # TODO: This was applyTransformToPath but was deprecated.   Candidate for refactoring.
3429                for comp in csp:
3430                    for ctl in comp:
3431                        for pt in ctl:
3432                            pt[0], pt[1] = Transform(self.reverse_transform(trans)).apply_to_point(pt)
3433        return csp
3434
3435    def transform_scalar(self, x, layer, reverse=False):
3436        return self.transform([x, 0], layer, reverse)[0] - self.transform([0, 0], layer, reverse)[0]
3437
3438    def transform(self, source_point, layer, reverse=False):
3439        if layer not in self.transform_matrix:
3440            for i in range(self.layers.index(layer), -1, -1):
3441                if self.layers[i] in self.orientation_points:
3442                    break
3443            if self.layers[i] not in self.orientation_points:
3444                self.error(f"Orientation points for '{layer.label}' layer have not been found! Please add orientation points using Orientation tab!", "error")
3445            elif self.layers[i] in self.transform_matrix:
3446                self.transform_matrix[layer] = self.transform_matrix[self.layers[i]]
3447                self.Zcoordinates[layer] = self.Zcoordinates[self.layers[i]]
3448            else:
3449                orientation_layer = self.layers[i]
3450                if len(self.orientation_points[orientation_layer]) > 1:
3451                    self.error(f"There are more than one orientation point groups in '{orientation_layer.label}' layer")
3452                points = self.orientation_points[orientation_layer][0]
3453                if len(points) == 2:
3454                    points += [[[(points[1][0][1] - points[0][0][1]) + points[0][0][0], -(points[1][0][0] - points[0][0][0]) + points[0][0][1]], [-(points[1][1][1] - points[0][1][1]) + points[0][1][0], points[1][1][0] - points[0][1][0] + points[0][1][1]]]]
3455                if len(points) == 3:
3456                    print_("Layer '{orientation_layer.label}' Orientation points: ")
3457                    for point in points:
3458                        print_(point)
3459                    #    Zcoordinates definition taken from Orientatnion point 1 and 2
3460                    self.Zcoordinates[layer] = [max(points[0][1][2], points[1][1][2]), min(points[0][1][2], points[1][1][2])]
3461                    matrix = numpy.array([
3462                        [points[0][0][0], points[0][0][1], 1, 0, 0, 0, 0, 0, 0],
3463                        [0, 0, 0, points[0][0][0], points[0][0][1], 1, 0, 0, 0],
3464                        [0, 0, 0, 0, 0, 0, points[0][0][0], points[0][0][1], 1],
3465                        [points[1][0][0], points[1][0][1], 1, 0, 0, 0, 0, 0, 0],
3466                        [0, 0, 0, points[1][0][0], points[1][0][1], 1, 0, 0, 0],
3467                        [0, 0, 0, 0, 0, 0, points[1][0][0], points[1][0][1], 1],
3468                        [points[2][0][0], points[2][0][1], 1, 0, 0, 0, 0, 0, 0],
3469                        [0, 0, 0, points[2][0][0], points[2][0][1], 1, 0, 0, 0],
3470                        [0, 0, 0, 0, 0, 0, points[2][0][0], points[2][0][1], 1]
3471                    ])
3472
3473                    if numpy.linalg.det(matrix) != 0:
3474                        m = numpy.linalg.solve(matrix,
3475                                               numpy.array(
3476                                                       [[points[0][1][0]], [points[0][1][1]], [1], [points[1][1][0]], [points[1][1][1]], [1], [points[2][1][0]], [points[2][1][1]], [1]]
3477                                               )
3478                                               ).tolist()
3479                        self.transform_matrix[layer] = [[m[j * 3 + i][0] for i in range(3)] for j in range(3)]
3480
3481                    else:
3482                        self.error("Orientation points are wrong! (if there are two orientation points they should not be the same. If there are three orientation points they should not be in a straight line.)", "error")
3483                else:
3484                    self.error("Orientation points are wrong! (if there are two orientation points they should not be the same. If there are three orientation points they should not be in a straight line.)", "error")
3485
3486            self.transform_matrix_reverse[layer] = numpy.linalg.inv(self.transform_matrix[layer]).tolist()
3487            print_(f"\n Layer '{layer.label}' transformation matrixes:")
3488            print_(self.transform_matrix)
3489            print_(self.transform_matrix_reverse)
3490
3491            # Zautoscale is obsolete
3492            self.Zauto_scale[layer] = 1
3493            print_("Z automatic scale = {} (computed according orientation points)".format(self.Zauto_scale[layer]))
3494
3495        x = source_point[0]
3496        y = source_point[1]
3497        if not reverse:
3498            t = self.transform_matrix[layer]
3499        else:
3500            t = self.transform_matrix_reverse[layer]
3501        return [t[0][0] * x + t[0][1] * y + t[0][2], t[1][0] * x + t[1][1] * y + t[1][2]]
3502
3503    def transform_csp(self, csp_, layer, reverse=False):
3504        csp = [[[csp_[i][j][0][:], csp_[i][j][1][:], csp_[i][j][2][:]] for j in range(len(csp_[i]))] for i in range(len(csp_))]
3505        for i in xrange(len(csp)):
3506            for j in xrange(len(csp[i])):
3507                for k in xrange(len(csp[i][j])):
3508                    csp[i][j][k] = self.transform(csp[i][j][k], layer, reverse)
3509        return csp
3510
3511    def error(self, s, msg_type="warning"):
3512        """
3513        Errors handling function
3514        warnings are printed into log file and warning message is displayed but
3515        extension continues working,
3516        errors causes log and execution is halted
3517        """
3518        if msg_type == "warning":
3519            print_(s)
3520            inkex.errormsg(s + "\n")
3521
3522        elif msg_type == "error":
3523            print_(s)
3524            raise inkex.AbortExtension(s)
3525
3526        else:
3527            print_("Unknown message type: {}".format(msg_type))
3528            print_(s)
3529            raise inkex.AbortExtension(s)
3530
3531    ################################################################################
3532    # Set markers
3533    ################################################################################
3534    def set_markers(self):
3535        """Make sure all markers are available"""
3536        def ensure_marker(elem_id, x=-4, polA='', polB='-', fill='#000044'):
3537            if self.svg.getElementById(elem_id) is None:
3538                marker = self.svg.defs.add(Marker(
3539                    id=elem_id, orient="auto", refX=str(x), refY="-1.687441",
3540                    style="overflow:visible"))
3541                path = marker.add(PathElement(
3542                    d="m {0}4.588864,-1.687441 0.0,0.0 L {0}9.177728,0.0 "\
3543                      "c {1}0.73311,-0.996261 {1}0.728882,-2.359329 0.0,-3.374882"\
3544                      .format(polA, polB)))
3545                path.style = "fill:{};fill-rule:evenodd;stroke:none;".format(fill)
3546
3547        ensure_marker("CheckToolsAndOPMarker")
3548        ensure_marker("DrawCurveMarker")
3549        ensure_marker("DrawCurveMarker_r", x=4, polA='-', polB='')
3550        ensure_marker("InOutPathMarker", fill='#0072a7')
3551
3552    def get_info(self):
3553        """Get Gcodetools info from the svg"""
3554        self.selected_paths = {}
3555        self.paths = {}
3556        self.tools = {}
3557        self.orientation_points = {}
3558        self.graffiti_reference_points = {}
3559        self.layers = [self.document.getroot()]
3560        self.Zcoordinates = {}
3561        self.transform_matrix = {}
3562        self.transform_matrix_reverse = {}
3563        self.Zauto_scale = {}
3564        self.in_out_reference_points = []
3565        self.my3Dlayer = None
3566
3567        def recursive_search(g, layer, selected=False):
3568            items = g.getchildren()
3569            items.reverse()
3570            for i in items:
3571                if selected:
3572                    self.svg.selected[i.get("id")] = i
3573                if isinstance(i, Layer):
3574                    if i.label == '3D':
3575                        self.my3Dlayer = i
3576                    else:
3577                        self.layers += [i]
3578                        recursive_search(i, i)
3579
3580                elif i.get('gcodetools') == "Gcodetools orientation group":
3581                    points = self.get_orientation_points(i)
3582                    if points is not None:
3583                        self.orientation_points[layer] = self.orientation_points[layer] + [points[:]] if layer in self.orientation_points else [points[:]]
3584                        print_(f"Found orientation points in '{layer.label}' layer: {points}")
3585                    else:
3586                        self.error(f"Warning! Found bad orientation points in '{layer.label}' layer. Resulting Gcode could be corrupt!")
3587
3588                # Need to recognise old files ver 1.6.04 and earlier
3589                elif i.get("gcodetools") == "Gcodetools tool definition" or i.get("gcodetools") == "Gcodetools tool definition":
3590                    tool = self.get_tool(i)
3591                    self.tools[layer] = self.tools[layer] + [tool.copy()] if layer in self.tools else [tool.copy()]
3592                    print_(f"Found tool in '{layer.label}' layer: {tool}")
3593
3594                elif i.get("gcodetools") == "Gcodetools graffiti reference point":
3595                    point = self.get_graffiti_reference_points(i)
3596                    if point:
3597                        self.graffiti_reference_points[layer] = self.graffiti_reference_points[layer] + [point[:]] if layer in self.graffiti_reference_points else [point]
3598                    else:
3599                        self.error(f"Warning! Found bad graffiti reference point in '{layer.label}' layer. Resulting Gcode could be corrupt!")
3600
3601                elif isinstance(i, inkex.PathElement):
3602                    if "gcodetools" not in i.keys():
3603                        self.paths[layer] = self.paths[layer] + [i] if layer in self.paths else [i]
3604                        if i.get("id") in self.svg.selected.ids:
3605                            self.selected_paths[layer] = self.selected_paths[layer] + [i] if layer in self.selected_paths else [i]
3606
3607                elif i.get("gcodetools") == "In-out reference point group":
3608                    items_ = i.getchildren()
3609                    items_.reverse()
3610                    for j in items_:
3611                        if j.get("gcodetools") == "In-out reference point":
3612                            self.in_out_reference_points.append(self.apply_transforms(j, j.path.to_superpath())[0][0][1])
3613
3614                elif isinstance(i, inkex.Group):
3615                    recursive_search(i, layer, (i.get("id") in self.svg.selected))
3616
3617                elif i.get("id") in self.svg.selected:
3618                    # xgettext:no-pango-format
3619                    self.error("This extension works with Paths and Dynamic Offsets and groups of them only! "
3620                               "All other objects will be ignored!\n"
3621                               "Solution 1: press Path->Object to path or Shift+Ctrl+C.\n"
3622                               "Solution 2: Path->Dynamic offset or Ctrl+J.\n"
3623                               "Solution 3: export all contours to PostScript level 2 (File->Save As->.ps) and File->Import this file.")
3624
3625        recursive_search(self.document.getroot(), self.document.getroot())
3626
3627        if len(self.layers) == 1:
3628            self.error("Document has no layers! Add at least one layer using layers panel (Ctrl+Shift+L)", "error")
3629        root = self.document.getroot()
3630
3631        if root in self.selected_paths or root in self.paths:
3632            self.error("Warning! There are some paths in the root of the document, but not in any layer! Using bottom-most layer for them.")
3633
3634        if root in self.selected_paths:
3635            if self.layers[-1] in self.selected_paths:
3636                self.selected_paths[self.layers[-1]] += self.selected_paths[root][:]
3637            else:
3638                self.selected_paths[self.layers[-1]] = self.selected_paths[root][:]
3639            del self.selected_paths[root]
3640
3641        if root in self.paths:
3642            if self.layers[-1] in self.paths:
3643                self.paths[self.layers[-1]] += self.paths[root][:]
3644            else:
3645                self.paths[self.layers[-1]] = self.paths[root][:]
3646            del self.paths[root]
3647
3648    def get_orientation_points(self, g):
3649        items = g.getchildren()
3650        items.reverse()
3651        p2 = []
3652        p3 = []
3653        p = None
3654        for i in items:
3655            if isinstance(i, inkex.Group):
3656                if i.get("gcodetools") == "Gcodetools orientation point (2 points)":
3657                    p2 += [i]
3658                if i.get("gcodetools") == "Gcodetools orientation point (3 points)":
3659                    p3 += [i]
3660        if len(p2) == 2:
3661            p = p2
3662        elif len(p3) == 3:
3663            p = p3
3664        if p is None:
3665            return None
3666        points = []
3667        for i in p:
3668            point = [[], []]
3669            for node in i:
3670                if node.get('gcodetools') == "Gcodetools orientation point arrow":
3671                    csp = node.path.transform(node.composed_transform()).to_superpath()
3672                    point[0] = csp[0][0][1]
3673                if node.get('gcodetools') == "Gcodetools orientation point text":
3674                    r = re.match(r'(?i)\s*\(\s*(-?\s*\d*(?:,|\.)*\d*)\s*;\s*(-?\s*\d*(?:,|\.)*\d*)\s*;\s*(-?\s*\d*(?:,|\.)*\d*)\s*\)\s*', node.get_text())
3675                    point[1] = [float(r.group(1)), float(r.group(2)), float(r.group(3))]
3676            if point[0] != [] and point[1] != []:
3677                points += [point]
3678        if len(points) == len(p2) == 2 or len(points) == len(p3) == 3:
3679            return points
3680        else:
3681            return None
3682
3683    def get_graffiti_reference_points(self, g):
3684        point = [[], '']
3685        for node in g:
3686            if node.get('gcodetools') == "Gcodetools graffiti reference point arrow":
3687                point[0] = self.apply_transforms(node, node.path.to_superpath())[0][0][1]
3688            if node.get('gcodetools') == "Gcodetools graffiti reference point text":
3689                point[1] = node.get_text()
3690        if point[0] != [] and point[1] != '':
3691            return point
3692        else:
3693            return []
3694
3695    def get_tool(self, g):
3696        tool = self.default_tool.copy()
3697        tool["self_group"] = g
3698        for i in g:
3699            # Get parameters
3700            if i.get("gcodetools") == "Gcodetools tool background":
3701                tool["style"] = dict(inkex.Style.parse_str(i.get("style")))
3702            elif i.get("gcodetools") == "Gcodetools tool parameter":
3703                key = None
3704                value = None
3705                for j in i:
3706                    # need to recognise old tools from ver 1.6.04
3707                    if j.get("gcodetools") == "Gcodetools tool definition field name" or j.get("gcodetools") == "Gcodetools tool defention field name":
3708                        key = j.get_text()
3709                    if j.get("gcodetools") == "Gcodetools tool definition field value" or j.get("gcodetools") == "Gcodetools tool defention field value":
3710                        value = j.get_text()
3711                        if value == "(None)":
3712                            value = ""
3713                if value is None or key is None:
3714                    continue
3715                if key in self.default_tool.keys():
3716                    try:
3717                        tool[key] = type(self.default_tool[key])(value)
3718                    except:
3719                        tool[key] = self.default_tool[key]
3720                        self.error("Warning! Tool's and default tool's parameter's ({}) types are not the same ( type('{}') != type('{}') ).".format(key, value, self.default_tool[key]))
3721                else:
3722                    tool[key] = value
3723                    self.error("Warning! Tool has parameter that default tool has not ( '{}': '{}' ).".format(key, value))
3724        return tool
3725
3726    def set_tool(self, layer):
3727        for i in range(self.layers.index(layer), -1, -1):
3728            if self.layers[i] in self.tools:
3729                break
3730        if self.layers[i] in self.tools:
3731            if self.layers[i] != layer:
3732                self.tools[layer] = self.tools[self.layers[i]]
3733            if len(self.tools[layer]) > 1:
3734                label = self.layers[i].label
3735                self.error(f"Layer '{label}' contains more than one tool!")
3736            return self.tools[layer]
3737        else:
3738            self.error(f"Can not find tool for '{layer.label}' layer! Please add one with Tools library tab!", "error")
3739
3740    ################################################################################
3741    #
3742    # Path to Gcode
3743    #
3744    ################################################################################
3745    def tab_path_to_gcode(self):
3746        self.get_info_plus()
3747        def get_boundaries(points):
3748            minx = None
3749            miny = None
3750            maxx = None
3751            maxy = None
3752            out = [[], [], [], []]
3753            for p in points:
3754                if minx == p[0]:
3755                    out[0] += [p]
3756                if minx is None or p[0] < minx:
3757                    minx = p[0]
3758                    out[0] = [p]
3759
3760                if miny == p[1]:
3761                    out[1] += [p]
3762                if miny is None or p[1] < miny:
3763                    miny = p[1]
3764                    out[1] = [p]
3765
3766                if maxx == p[0]:
3767                    out[2] += [p]
3768                if maxx is None or p[0] > maxx:
3769                    maxx = p[0]
3770                    out[2] = [p]
3771
3772                if maxy == p[1]:
3773                    out[3] += [p]
3774                if maxy is None or p[1] > maxy:
3775                    maxy = p[1]
3776                    out[3] = [p]
3777            return out
3778
3779        def remove_duplicates(points):
3780            i = 0
3781            out = []
3782            for p in points:
3783                for j in xrange(i, len(points)):
3784                    if p == points[j]:
3785                        points[j] = [None, None]
3786                if p != [None, None]:
3787                    out += [p]
3788            i += 1
3789            return out
3790
3791        def get_way_len(points):
3792            l = 0
3793            for i in xrange(1, len(points)):
3794                l += math.sqrt((points[i][0] - points[i - 1][0]) ** 2 + (points[i][1] - points[i - 1][1]) ** 2)
3795            return l
3796
3797        def sort_dxfpoints(points):
3798            points = remove_duplicates(points)
3799            ways = [
3800                # l=0, d=1, r=2, u=3
3801                [3, 0],  # ul
3802                [3, 2],  # ur
3803                [1, 0],  # dl
3804                [1, 2],  # dr
3805                [0, 3],  # lu
3806                [0, 1],  # ld
3807                [2, 3],  # ru
3808                [2, 1],  # rd
3809            ]
3810            minimal_way = []
3811            minimal_len = None
3812            for w in ways:
3813                tpoints = points[:]
3814                cw = []
3815                for j in xrange(0, len(points)):
3816                    p = get_boundaries(get_boundaries(tpoints)[w[0]])[w[1]]
3817                    tpoints.remove(p[0])
3818                    cw += p
3819                curlen = get_way_len(cw)
3820                if minimal_len is None or curlen < minimal_len:
3821                    minimal_len = curlen
3822                    minimal_way = cw
3823
3824            return minimal_way
3825
3826        def sort_lines(lines):
3827            if len(lines) == 0:
3828                return []
3829            lines = [[key] + lines[key] for key in range(len(lines))]
3830            keys = [0]
3831            end_point = lines[0][3:]
3832            print_("!!!", lines, "\n", end_point)
3833            del lines[0]
3834            while len(lines) > 0:
3835                dist = [[point_to_point_d2(end_point, lines[i][1:3]), i] for i in range(len(lines))]
3836                i = min(dist)[1]
3837                keys.append(lines[i][0])
3838                end_point = lines[i][3:]
3839                del lines[i]
3840            return keys
3841
3842        def sort_curves(curves):
3843            lines = []
3844            for curve in curves:
3845                lines += [curve[0][0][0] + curve[-1][-1][0]]
3846            return sort_lines(lines)
3847
3848        def print_dxfpoints(points):
3849            gcode = ""
3850            for point in points:
3851                gcode += "(drilling dxfpoint)\nG00 Z{:f}\nG00 X{:f} Y{:f}\nG01 Z{:f} F{:f}\nG04 P{:f}\nG00 Z{:f}\n".format(self.options.Zsafe, point[0], point[1], self.Zcoordinates[layer][1], self.tools[layer][0]["penetration feed"], 0.2, self.options.Zsafe)
3852            return gcode
3853
3854        def get_path_properties(node):
3855            res = {}
3856            done = False
3857            while not done and node != self.svg:
3858                for i in node.getchildren():
3859                    if isinstance(i, inkex.Desc):
3860                        res["Description"] = i.text
3861                    elif isinstance(i, inkex.Title):
3862                        res["Title"] = i.text
3863                    done = True
3864                node = node.getparent()
3865            return res
3866
3867        if self.selected_paths == {} and self.options.auto_select_paths:
3868            paths = self.paths
3869            self.error("No paths are selected! Trying to work on all available paths.")
3870        else:
3871            paths = self.selected_paths
3872        self.check_dir()
3873        gcode = ""
3874
3875        parent = list(self.selected_paths)[0] if self.selected_paths else self.layers[0]
3876        biarc_group = parent.add(Group())
3877        print_(("self.layers=", self.layers))
3878        print_(("paths=", paths))
3879        colors = {}
3880        for layer in self.layers:
3881            if layer in paths:
3882                print_(("layer", layer))
3883                # transform simple path to get all var about orientation
3884                self.transform_csp([[[[0, 0], [0, 0], [0, 0]], [[0, 0], [0, 0], [0, 0]]]], layer)
3885
3886                self.set_tool(layer)
3887                curves = []
3888                dxfpoints = []
3889
3890                try:
3891                    depth_func = eval('lambda c,d,s: ' + self.options.path_to_gcode_depth_function.strip('"'))
3892                except:
3893                    self.error("Bad depth function! Enter correct function at Path to Gcode tab!")
3894
3895                for path in paths[layer]:
3896                    if "d" not in path.keys():
3897                        self.error("Warning: One or more paths do not have 'd' parameter, try to Ungroup (Ctrl+Shift+G) and Object to Path (Ctrl+Shift+C)!")
3898                        continue
3899                    csp = path.path.to_superpath()
3900                    csp = self.apply_transforms(path, csp)
3901                    id_ = path.get("id")
3902
3903                    def set_comment(match, path):
3904                        if match.group(1) in path.keys():
3905                            return path.get(match.group(1))
3906                        else:
3907                            return "None"
3908
3909                    if self.options.comment_gcode != "":
3910                        comment = re.sub("\\[([A-Za-z_\\-\\:]+)\\]", partial(set_comment, path=path), self.options.comment_gcode)
3911                        comment = comment.replace(":newline:", "\n")
3912                        comment = gcode_comment_str(comment)
3913                    else:
3914                        comment = ""
3915                    if self.options.comment_gcode_from_properties:
3916                        tags = get_path_properties(path)
3917                        for tag in tags:
3918                            comment += gcode_comment_str("{}: {}".format(tag, tags[tag]))
3919
3920                    style = dict(inkex.Style.parse_str(path.get("style")))
3921                    colors[id_] = inkex.Color(style['stroke'] if "stroke" in style and style['stroke'] != 'none' else "#000").to_rgb()
3922                    if path.get("dxfpoint") == "1":
3923                        tmp_curve = self.transform_csp(csp, layer)
3924                        x = tmp_curve[0][0][0][0]
3925                        y = tmp_curve[0][0][0][1]
3926                        print_("got dxfpoint (scaled) at ({:f},{:f})".format(x, y))
3927                        dxfpoints += [[x, y]]
3928                    else:
3929
3930                        zd = self.Zcoordinates[layer][1]
3931                        zs = self.Zcoordinates[layer][0]
3932                        c = 1. - float(sum(colors[id_])) / 255 / 3
3933                        curves += [
3934                            [
3935                                [id_, depth_func(c, zd, zs), comment],
3936                                [self.parse_curve([subpath], layer) for subpath in csp]
3937                            ]
3938                        ]
3939                dxfpoints = sort_dxfpoints(dxfpoints)
3940                gcode += print_dxfpoints(dxfpoints)
3941
3942                for curve in curves:
3943                    for subcurve in curve[1]:
3944                        self.draw_curve(subcurve, layer)
3945
3946                if self.options.path_to_gcode_order == 'subpath by subpath':
3947                    curves_ = []
3948                    for curve in curves:
3949                        curves_ += [[curve[0], [subcurve]] for subcurve in curve[1]]
3950                    curves = curves_
3951
3952                    self.options.path_to_gcode_order = 'path by path'
3953
3954                if self.options.path_to_gcode_order == 'path by path':
3955                    if self.options.path_to_gcode_sort_paths:
3956                        keys = sort_curves([curve[1] for curve in curves])
3957                    else:
3958                        keys = range(len(curves))
3959                    for key in keys:
3960                        d = curves[key][0][1]
3961                        for step in range(0, int(math.ceil(abs((zs - d) / self.tools[layer][0]["depth step"])))):
3962                            z = max(d, zs - abs(self.tools[layer][0]["depth step"] * (step + 1)))
3963
3964                            gcode += gcode_comment_str("\nStart cutting path id: {}".format(curves[key][0][0]))
3965                            if curves[key][0][2] != "()":
3966                                gcode += curves[key][0][2]  # add comment
3967
3968                            for curve in curves[key][1]:
3969                                gcode += self.generate_gcode(curve, layer, z)
3970
3971                            gcode += gcode_comment_str("End cutting path id: {}\n\n".format(curves[key][0][0]))
3972
3973                else:  # pass by pass
3974                    mind = min([curve[0][1] for curve in curves])
3975                    for step in range(0, 1 + int(math.ceil(abs((zs - mind) / self.tools[layer][0]["depth step"])))):
3976                        z = zs - abs(self.tools[layer][0]["depth step"] * step)
3977                        curves_ = []
3978                        for curve in curves:
3979                            if curve[0][1] < z:
3980                                curves_.append(curve)
3981
3982                        z = zs - abs(self.tools[layer][0]["depth step"] * (step + 1))
3983                        gcode += "\n(Pass at depth {})\n".format(z)
3984
3985                        if self.options.path_to_gcode_sort_paths:
3986                            keys = sort_curves([curve[1] for curve in curves_])
3987                        else:
3988                            keys = range(len(curves_))
3989                        for key in keys:
3990
3991                            gcode += gcode_comment_str("Start cutting path id: {}".format(curves[key][0][0]))
3992                            if curves[key][0][2] != "()":
3993                                gcode += curves[key][0][2]  # add comment
3994
3995                            for subcurve in curves_[key][1]:
3996                                gcode += self.generate_gcode(subcurve, layer, max(z, curves_[key][0][1]))
3997
3998                            gcode += gcode_comment_str("End cutting path id: {}\n\n".format(curves[key][0][0]))
3999
4000        self.export_gcode(gcode)
4001
4002    ################################################################################
4003    #
4004    # dxfpoints
4005    #
4006    ################################################################################
4007    def tab_dxfpoints(self):
4008        self.get_info_plus()
4009        if self.selected_paths == {}:
4010            self.error("Nothing is selected. Please select something to convert to drill point (dxfpoint) or clear point sign.")
4011        for layer in self.layers:
4012            if layer in self.selected_paths:
4013                for path in self.selected_paths[layer]:
4014                    if self.options.dxfpoints_action == 'replace':
4015
4016                        path.set("dxfpoint", "1")
4017                        r = re.match("^\\s*.\\s*(\\S+)", path.get("d"))
4018                        if r is not None:
4019                            print_(("got path=", r.group(1)))
4020                            path.set("d", "m {} 2.9375,-6.343750000001 0.8125,1.90625 6.843748640396,-6.84374864039 0,0 0.6875,0.6875 -6.84375,6.84375 1.90625,0.812500000001 z".format(r.group(1)))
4021                            path.set("style", MARKER_STYLE["dxf_points"])
4022
4023                    if self.options.dxfpoints_action == 'save':
4024                        path.set("dxfpoint", "1")
4025
4026                    if self.options.dxfpoints_action == 'clear' and path.get("dxfpoint") == "1":
4027                        path.set("dxfpoint", "0")
4028
4029    ################################################################################
4030    #
4031    # Artefacts
4032    #
4033    ################################################################################
4034    def tab_area_artefacts(self):
4035        self.get_info_plus()
4036        if self.selected_paths == {} and self.options.auto_select_paths:
4037            paths = self.paths
4038            self.error("No paths are selected! Trying to work on all available paths.")
4039        else:
4040            paths = self.selected_paths
4041        for layer in paths:
4042            for path in paths[layer]:
4043                parent = path.getparent()
4044                if "d" not in path.keys():
4045                    self.error("Warning: One or more paths do not have 'd' parameter, try to Ungroup (Ctrl+Shift+G) and Object to Path (Ctrl+Shift+C)!")
4046                    continue
4047                csp = path.path.to_superpath()
4048                remove = []
4049                for i in range(len(csp)):
4050                    subpath = [[point[:] for point in points] for points in csp[i]]
4051                    subpath = self.apply_transforms(path, [subpath])[0]
4052                    bounds = csp_simple_bound([subpath])
4053                    if (bounds[2] - bounds[0]) ** 2 + (bounds[3] - bounds[1]) ** 2 < self.options.area_find_artefacts_diameter ** 2:
4054                        if self.options.area_find_artefacts_action == "mark with an arrow":
4055                            arrow = Path('m {},{} 2.9375,-6.343750000001 0.8125,1.90625 6.843748640396,-6.84374864039 0,0 0.6875,0.6875 -6.84375,6.84375 1.90625,0.812500000001 z'.format(subpath[0][1][0], subpath[0][1][1])).to_superpath()
4056                            arrow = self.apply_transforms(path, arrow, True)
4057                            node = parent.add(PathElement())
4058                            node.path = CubicSuperPath(arrow)
4059                            node.style = MARKER_STYLE["area artefact arrow"]
4060                            node.set('gcodetools', 'area artefact arrow')
4061                        elif self.options.area_find_artefacts_action == "mark with style":
4062                            node = parent.add(PathElement())
4063                            node.path = CubicSuperPath(csp[i])
4064                            node.style = MARKER_STYLE["area artefact"]
4065                            remove.append(i)
4066                        elif self.options.area_find_artefacts_action == "delete":
4067                            remove.append(i)
4068                            print_("Deleted artefact {}".format(subpath))
4069                remove.reverse()
4070                for i in remove:
4071                    del csp[i]
4072                if len(csp) == 0:
4073                    parent.remove(path)
4074                else:
4075                    path.path = CubicSuperPath(csp)
4076
4077        return
4078
4079    def tab_area(self):
4080        """Calculate area curves"""
4081        self.get_info_plus()
4082        if len(self.selected_paths) <= 0:
4083            self.error("This extension requires at least one selected path.")
4084            return
4085        for layer in self.layers:
4086            if layer in self.selected_paths:
4087                self.set_tool(layer)
4088                if self.tools[layer][0]['diameter'] <= 0:
4089                    self.error(f"Tool diameter must be > 0 but tool's diameter on '{layer.label}' layer is not!", "error")
4090
4091                for path in self.selected_paths[layer]:
4092                    print_(("doing path", path.get("style"), path.get("d")))
4093
4094                    area_group = path.getparent().add(Group())
4095
4096                    csp = path.path.to_superpath()
4097                    print_(csp)
4098                    if not csp:
4099                        print_("omitting non-path")
4100                        self.error("Warning: omitting non-path")
4101                        continue
4102
4103                    if path.get('sodipodi:type') != "inkscape:offset":
4104                        print_("Path {} is not an offset. Preparation started.".format(path.get("id")))
4105                        # Path is not offset. Preparation will be needed.
4106                        # Finding top most point in path (min y value)
4107
4108                        min_x, min_y, min_i, min_j, min_t = csp_true_bounds(csp)[1]
4109
4110                        # Reverse path if needed.
4111                        if min_y != float("-inf"):
4112                            # Move outline subpath to the beginning of csp
4113                            subp = csp[min_i]
4114                            del csp[min_i]
4115                            j = min_j
4116                            # Split by the topmost point and join again
4117                            if min_t in [0, 1]:
4118                                if min_t == 0:
4119                                    j = j - 1
4120                                subp[-1][2], subp[0][0] = subp[-1][1], subp[0][1]
4121                                subp = [[subp[j][1], subp[j][1], subp[j][2]]] + subp[j + 1:] + subp[:j] + [[subp[j][0], subp[j][1], subp[j][1]]]
4122                            else:
4123                                sp1, sp2, sp3 = csp_split(subp[j - 1], subp[j], min_t)
4124                                subp[-1][2], subp[0][0] = subp[-1][1], subp[0][1]
4125                                subp = [[sp2[1], sp2[1], sp2[2]]] + [sp3] + subp[j + 1:] + subp[:j - 1] + [sp1] + [[sp2[0], sp2[1], sp2[1]]]
4126                            csp = [subp] + csp
4127                            # reverse path if needed
4128                            if csp_subpath_ccw(csp[0]):
4129                                for i in range(len(csp)):
4130                                    n = []
4131                                    for j in csp[i]:
4132                                        n = [[j[2][:], j[1][:], j[0][:]]] + n
4133                                    csp[i] = n[:]
4134
4135                    # What the absolute fudge is this doing? Closing paths? Ugh.
4136                    # Not sure but it most be at this level and not in the if statement, or it will not work with dynamic offsets
4137                    d = str(CubicSuperPath(csp))
4138                    print_(("original  d=", d))
4139                    d = re.sub(r'(?i)(m[^mz]+)', r'\1 Z ', d)
4140                    d = re.sub(r'(?i)\s*z\s*z\s*', r' Z ', d)
4141                    d = re.sub(r'(?i)\s*([A-Za-z])\s*', r' \1 ', d)
4142                    print_(("formatted d=", d))
4143                    p0 = self.transform([0, 0], layer)
4144                    p1 = self.transform([0, 1], layer)
4145                    scale = (P(p0) - P(p1)).mag()
4146                    if scale == 0:
4147                        scale = 1.
4148                    else:
4149                        scale = 1. / scale
4150                    print_(scale)
4151                    tool_d = self.tools[layer][0]['diameter'] * scale
4152                    r = self.options.area_inkscape_radius * scale
4153                    sign = 1 if r > 0 else -1
4154                    print_("Tool diameter = {}, r = {}".format(tool_d, r))
4155
4156                    # avoiding infinite loops
4157                    if self.options.area_tool_overlap > 0.9:
4158                        self.options.area_tool_overlap = .9
4159
4160                    for i in range(self.options.max_area_curves):
4161                        radius = - tool_d * (i * (1 - self.options.area_tool_overlap) + 0.5) * sign
4162                        if abs(radius) > abs(r):
4163                            radius = -r
4164
4165                        elem = area_group.add(PathElement(style=str(MARKER_STYLE["biarc_style_i"]['area'])))
4166                        elem.set('sodipodi:type', 'inkscape:offset')
4167                        elem.set('inkscape:radius', radius)
4168                        elem.set('inkscape:original', d)
4169                        print_(("adding curve", area_group, d, str(MARKER_STYLE["biarc_style_i"]['area'])))
4170                        if radius == -r:
4171                            break
4172
4173    def tab_area_fill(self):
4174        """Fills area with lines"""
4175        self.get_info_plus()
4176        # convert degrees into rad
4177        self.options.area_fill_angle = self.options.area_fill_angle * math.pi / 180
4178        if len(self.selected_paths) <= 0:
4179            self.error("This extension requires at least one selected path.")
4180            return
4181        for layer in self.layers:
4182            if layer in self.selected_paths:
4183                self.set_tool(layer)
4184                if self.tools[layer][0]['diameter'] <= 0:
4185                    self.error(f"Tool diameter must be > 0 but tool's diameter on '{layer.label}' layer is not!", "error")
4186                tool = self.tools[layer][0]
4187                for path in self.selected_paths[layer]:
4188                    lines = []
4189                    print_(("doing path", path.get("style"), path.get("d")))
4190                    area_group = path.getparent().add(Group())
4191                    csp = path.path.to_superpath()
4192                    if not csp:
4193                        print_("omitting non-path")
4194                        self.error("Warning: omitting non-path")
4195                        continue
4196                    csp = self.apply_transforms(path, csp)
4197                    csp = csp_close_all_subpaths(csp)
4198                    csp = self.transform_csp(csp, layer)
4199
4200                    # rotate the path to get bounds in defined direction.
4201                    a = - self.options.area_fill_angle
4202                    rotated_path = [[[[point[0] * math.cos(a) - point[1] * math.sin(a), point[0] * math.sin(a) + point[1] * math.cos(a)] for point in sp] for sp in subpath] for subpath in csp]
4203                    bounds = csp_true_bounds(rotated_path)
4204
4205                    # Draw the lines
4206                    # Get path's bounds
4207                    b = [0.0, 0.0, 0.0, 0.0]  # [minx,miny,maxx,maxy]
4208                    for k in range(4):
4209                        i = bounds[k][2]
4210                        j = bounds[k][3]
4211                        t = bounds[k][4]
4212
4213                        b[k] = csp_at_t(rotated_path[i][j - 1], rotated_path[i][j], t)[k % 2]
4214
4215                    # Zig-zag
4216                    r = tool['diameter'] * (1 - self.options.area_tool_overlap)
4217                    if r <= 0:
4218                        self.error('Tools diameter must be greater than 0!', 'error')
4219                        return
4220
4221                    lines += [[]]
4222
4223                    if self.options.area_fill_method == 'zig-zag':
4224                        i = b[0] - self.options.area_fill_shift * r
4225                        top = True
4226                        last_one = True
4227                        while i < b[2] or last_one:
4228                            if i >= b[2]:
4229                                last_one = False
4230                            if not lines[-1]:
4231                                lines[-1] += [[i, b[3]]]
4232
4233                            if top:
4234                                lines[-1] += [[i, b[1]], [i + r, b[1]]]
4235
4236                            else:
4237                                lines[-1] += [[i, b[3]], [i + r, b[3]]]
4238
4239                            top = not top
4240                            i += r
4241                    else:
4242
4243                        w = b[2] - b[0] + self.options.area_fill_shift * r
4244                        h = b[3] - b[1] + self.options.area_fill_shift * r
4245                        x = b[0] - self.options.area_fill_shift * r
4246                        y = b[1] - self.options.area_fill_shift * r
4247                        lines[-1] += [[x, y]]
4248                        stage = 0
4249                        start = True
4250                        while w > 0 and h > 0:
4251                            stage = (stage + 1) % 4
4252                            if stage == 0:
4253                                y -= h
4254                                h -= r
4255                            elif stage == 1:
4256                                x += w
4257                                if not start:
4258                                    w -= r
4259                                start = False
4260                            elif stage == 2:
4261                                y += h
4262                                h -= r
4263                            elif stage == 3:
4264                                x -= w
4265                                w -= r
4266
4267                            lines[-1] += [[x, y]]
4268
4269                        stage = (stage + 1) % 4
4270                        if w <= 0 and h > 0:
4271                            y = y - h if stage == 0 else y + h
4272                        if h <= 0 and w > 0:
4273                            x = x - w if stage == 3 else x + w
4274                        lines[-1] += [[x, y]]
4275                    # Rotate created paths back
4276                    a = self.options.area_fill_angle
4277                    lines = [[[point[0] * math.cos(a) - point[1] * math.sin(a), point[0] * math.sin(a) + point[1] * math.cos(a)] for point in subpath] for subpath in lines]
4278
4279                    # get the intersection points
4280
4281                    splitted_line = [[lines[0][0]]]
4282                    intersections = {}
4283                    for l1, l2, in zip(lines[0], lines[0][1:]):
4284                        ints = []
4285
4286                        if l1[0] == l2[0] and l1[1] == l2[1]:
4287                            continue
4288                        for i in range(len(csp)):
4289                            for j in range(1, len(csp[i])):
4290                                sp1 = csp[i][j - 1]
4291                                sp2 = csp[i][j]
4292                                roots = csp_line_intersection(l1, l2, sp1, sp2)
4293                                for t in roots:
4294                                    p = tuple(csp_at_t(sp1, sp2, t))
4295                                    if l1[0] == l2[0]:
4296                                        t1 = (p[1] - l1[1]) / (l2[1] - l1[1])
4297                                    else:
4298                                        t1 = (p[0] - l1[0]) / (l2[0] - l1[0])
4299                                    if 0 <= t1 <= 1:
4300                                        ints += [[t1, p[0], p[1], i, j, t]]
4301                                        if p in intersections:
4302                                            intersections[p] += [[i, j, t]]
4303                                        else:
4304                                            intersections[p] = [[i, j, t]]
4305
4306                        ints.sort()
4307                        for i in ints:
4308                            splitted_line[-1] += [[i[1], i[2]]]
4309                            splitted_line += [[[i[1], i[2]]]]
4310                        splitted_line[-1] += [l2]
4311                        i = 0
4312                    print_(splitted_line)
4313                    while i < len(splitted_line):
4314                        # check if the middle point of the first lines segment is inside the path.
4315                        # and remove the subline if not.
4316                        l1 = splitted_line[i][0]
4317                        l2 = splitted_line[i][1]
4318                        p = [(l1[0] + l2[0]) / 2, (l1[1] + l2[1]) / 2]
4319                        if not point_inside_csp(p, csp):
4320                            del splitted_line[i]
4321                        else:
4322                            i += 1
4323
4324                    # and apply back transrormations to draw them
4325                    csp_line = csp_from_polyline(splitted_line)
4326                    csp_line = self.transform_csp(csp_line, layer, True)
4327
4328                    self.draw_csp(csp_line, group=area_group)
4329
4330    ################################################################################
4331    #
4332    # Engraving
4333    #
4334    # LT Notes to self: See wiki.inkscape.org/wiki/index.php/PythonEffectTutorial
4335    # To create anything in the Inkscape document, look at the XML editor for
4336    # details of how such an element looks in XML, then follow this model.
4337    # layer number n appears in XML as <svg:g id="layern" inkscape:label="layername">
4338    #
4339    # to create it, use
4340    # Mylayer = self.svg.add(Layer.new('layername'))
4341    #
4342    # group appears in XML as <svg:g id="gnnnnn"> where nnnnn is a number
4343    #
4344    # to create it, use
4345    # Mygroup = parent.add(Group(gcodetools="My group label")
4346    # where parent may be the layer or a parent group. To get the parent group, you can use
4347    # parent = self.selected_paths[layer][0].getparent()
4348    ################################################################################
4349    def tab_engraving(self):
4350        self.get_info_plus()
4351        global cspm
4352        global wl
4353        global nlLT
4354        global i
4355        global j
4356        global gcode_3Dleft
4357        global gcode_3Dright
4358        global max_dist  # minimum of tool radius and user's requested maximum distance
4359        global eye_dist
4360        eye_dist = 100  # 3D constant. Try varying it for your eyes
4361
4362        def bisect(nxy1, nxy2):
4363            """LT Find angle bisecting the normals n1 and n2
4364
4365            Parameters: Normalised normals
4366            Returns: nx - Normal of bisector, normalised to 1/cos(a)
4367                   ny -
4368                   sinBis2 - sin(angle turned/2): positive if turning in
4369            Note that bisect(n1,n2) and bisect(n2,n1) give opposite sinBis2 results
4370            If sinturn is less than the user's requested angle tolerance, I return 0
4371            """
4372            (nx1, ny1) = nxy1
4373            (nx2, ny2) = nxy2
4374            cosBis = math.sqrt(max(0, (1.0 + nx1 * nx2 - ny1 * ny2) / 2.0))
4375            # We can get correct sign of the sin, assuming cos is positive
4376            if (abs(ny1 - ny2) < ENGRAVING_TOLERANCE) or (abs(cosBis) < ENGRAVING_TOLERANCE):
4377                if abs(nx1 - nx2) < ENGRAVING_TOLERANCE:
4378                    return nx1, ny1, 0.0
4379                sinBis = math.copysign(1, ny1)
4380            else:
4381                sinBis = cosBis * (nx2 - nx1) / (ny1 - ny2)
4382            # We can correct signs by noting that the dot product
4383            # of bisector and either normal must be >0
4384            costurn = cosBis * nx1 + sinBis * ny1
4385            if costurn == 0:
4386                return ny1 * 100, -nx1 * 100, 1  # Path doubles back on itself
4387            sinturn = sinBis * nx1 - cosBis * ny1
4388            if costurn < 0:
4389                sinturn = -sinturn
4390            if 0 < sinturn * 114.6 < (180 - self.options.engraving_sharp_angle_tollerance):
4391                sinturn = 0  # set to zero if less than the user wants to see.
4392            return cosBis / costurn, sinBis / costurn, sinturn
4393            # end bisect
4394
4395        def get_radius_to_line(xy1, n_xy1, n_xy2, xy2, n_xy23, xy3, n_xy3):
4396            """LT find biggest circle we can engrave here, if constrained by line 2-3
4397
4398            Parameters:
4399                x1,y1,nx1,ny1 coordinates and normal of the line we're currently engraving
4400                nx2,ny2 angle bisector at point 2
4401                x2,y2 coordinates of first point of line 2-3
4402                nx23,ny23 normal to the line 2-3
4403                x3,y3 coordinates of the other end
4404                nx3,ny3 angle bisector at point 3
4405            Returns:
4406                radius or self.options.engraving_max_dist if line doesn't limit radius
4407            This function can be used in three ways:
4408            - With nx1=ny1=0 it finds circle centred at x1,y1
4409            - with nx1,ny1 normalised, it finds circle tangential at x1,y1
4410            - with nx1,ny1 scaled by 1/cos(a) it finds circle centred on an angle bisector
4411                 where a is the angle between the bisector and the previous/next normals
4412
4413            If the centre of the circle tangential to the line 2-3 is outside the
4414            angle bisectors at its ends, ignore this line.
4415
4416            Note that it handles corners in the conventional manner of letter cutting
4417            by mitering, not rounding.
4418            Algorithm uses dot products of normals to find radius
4419            and hence coordinates of centre
4420            """
4421            (x1, y1) = xy1
4422            (nx1, ny1) = n_xy1
4423            (nx2, ny2) = n_xy2
4424            (x2, y2) = xy2
4425            (nx23, ny23) = n_xy23
4426            (x3, y3) = xy3
4427            (nx3, ny3) = n_xy3
4428            global max_dist
4429
4430            # Start by converting coordinates to be relative to x1,y1
4431            x2, y2 = x2 - x1, y2 - y1
4432            x3, y3 = x3 - x1, y3 - y1
4433
4434            # The logic uses vector arithmetic.
4435            # The dot product of two vectors gives the product of their lengths
4436            # multiplied by the cos of the angle between them.
4437            # So, the perpendicular distance from x1y1 to the line 2-3
4438            # is equal to the dot product of its normal and x2y2 or x3y3
4439            # It is also equal to the projection of x1y1-xcyc on the line's normal
4440            # plus the radius. But, as the normal faces inside the path we must negate it.
4441
4442            # Make sure the line in question is facing x1,y1 and vice versa
4443            dist = -x2 * nx23 - y2 * ny23
4444            if dist < 0:
4445                return max_dist
4446            denom = 1. - nx23 * nx1 - ny23 * ny1
4447            if denom < ENGRAVING_TOLERANCE:
4448                return max_dist
4449
4450            # radius and centre are:
4451            r = dist / denom
4452            cx = r * nx1
4453            cy = r * ny1
4454            # if c is not between the angle bisectors at the ends of the line, ignore
4455            # Use vector cross products. Not sure if I need the .0001 safety margins:
4456            if (x2 - cx) * ny2 > (y2 - cy) * nx2 + 0.0001:
4457                return max_dist
4458            if (x3 - cx) * ny3 < (y3 - cy) * nx3 - 0.0001:
4459                return max_dist
4460            return min(r, max_dist)
4461            # end of get_radius_to_line
4462
4463        def get_radius_to_point(xy1, n_xy, xy2):
4464            """LT find biggest circle we can engrave here, constrained by point x2,y2
4465
4466            This function can be used in three ways:
4467            - With nx=ny=0 it finds circle centred at x1,y1
4468            - with nx,ny normalised, it finds circle tangential at x1,y1
4469            - with nx,ny scaled by 1/cos(a) it finds circle centred on an angle bisector
4470                 where a is the angle between the bisector and the previous/next normals
4471
4472            Note that I wrote this to replace find_cutter_centre. It is far less
4473            sophisticated but, I hope, far faster.
4474            It turns out that finding a circle touching a point is harder than a circle
4475            touching a line.
4476            """
4477            (x1, y1) = xy1
4478            (nx, ny) = n_xy
4479            (x2, y2) = xy2
4480            global max_dist
4481
4482            # Start by converting coordinates to be relative to x1,y1
4483            x2 = x2 - x1
4484            y2 = y2 - y1
4485            denom = nx ** 2 + ny ** 2 - 1
4486            if denom <= ENGRAVING_TOLERANCE:  # Not a corner bisector
4487                if denom == -1:  # Find circle centre x1,y1
4488                    return math.sqrt(x2 ** 2 + y2 ** 2)
4489                # if x2,y2 not in front of the normal...
4490                if x2 * nx + y2 * ny <= 0:
4491                    return max_dist
4492                return (x2 ** 2 + y2 ** 2) / (2 * (x2 * nx + y2 * ny))
4493            # It is a corner bisector, so..
4494            discriminator = (x2 * nx + y2 * ny) ** 2 - denom * (x2 ** 2 + y2 ** 2)
4495            if discriminator < 0:
4496                return max_dist  # this part irrelevant
4497            r = (x2 * nx + y2 * ny - math.sqrt(discriminator)) / denom
4498            return min(r, max_dist)
4499            # end of get_radius_to_point
4500
4501        def bez_divide(a, b, c, d):
4502            """LT recursively divide a Bezier.
4503
4504            Divides until difference between each
4505            part and a straight line is less than some limit
4506            Note that, as simple as this code is, it is mathematically correct.
4507            Parameters:
4508                a,b,c and d are each a list of x,y real values
4509                Bezier end points a and d, control points b and c
4510            Returns:
4511                a list of Beziers.
4512                    Each Bezier is a list with four members,
4513                        each a list holding a coordinate pair
4514                Note that the final point of one member is the same as
4515                the first point of the next, and the control points
4516                there are smooth and symmetrical. I use this fact later.
4517            """
4518            bx = b[0] - a[0]
4519            by = b[1] - a[1]
4520            cx = c[0] - a[0]
4521            cy = c[1] - a[1]
4522            dx = d[0] - a[0]
4523            dy = d[1] - a[1]
4524            limit = 8 * math.hypot(dx, dy) / self.options.engraving_newton_iterations
4525            # LT This is the only limit we get from the user currently
4526            if abs(dx * by - bx * dy) < limit and abs(dx * cy - cx * dy) < limit:
4527                return [[a, b, c, d]]
4528            abx = (a[0] + b[0]) / 2.0
4529            aby = (a[1] + b[1]) / 2.0
4530            bcx = (b[0] + c[0]) / 2.0
4531            bcy = (b[1] + c[1]) / 2.0
4532            cdx = (c[0] + d[0]) / 2.0
4533            cdy = (c[1] + d[1]) / 2.0
4534            abcx = (abx + bcx) / 2.0
4535            abcy = (aby + bcy) / 2.0
4536            bcdx = (bcx + cdx) / 2.0
4537            bcdy = (bcy + cdy) / 2.0
4538            m = [(abcx + bcdx) / 2.0, (abcy + bcdy) / 2.0]
4539            return bez_divide(a, [abx, aby], [abcx, abcy], m) + bez_divide(m, [bcdx, bcdy], [cdx, cdy], d)
4540            # end of bez_divide
4541
4542        def get_biggest(nxy1, nxy2):
4543            """LT Find biggest circle we can draw inside path at point x1,y1 normal nx,ny
4544
4545            Parameters:
4546                point - either on a line or at a reflex corner
4547                normal - normalised to 1 if on a line, to 1/cos(a) at a corner
4548            Returns:
4549                tuple (j,i,r)
4550                ..where j and i are indices of limiting segment, r is radius
4551            """
4552            (x1, y1) = nxy1
4553            (nx, ny) = nxy2
4554            global max_dist
4555            global nlLT
4556            global i
4557            global j
4558
4559            n1 = nlLT[j][i - 1]  # current node
4560            jjmin = -1
4561            iimin = -1
4562            r = max_dist
4563            # set limits within which to look for lines
4564            xmin = x1 + r * nx - r
4565            xmax = x1 + r * nx + r
4566            ymin = y1 + r * ny - r
4567            ymax = y1 + r * ny + r
4568            for jj in xrange(0, len(nlLT)):  # for every subpath of this object
4569                for ii in xrange(0, len(nlLT[jj])):  # for every point and line
4570                    if nlLT[jj][ii - 1][2]:  # if a point
4571                        if jj == j:  # except this one
4572                            if abs(ii - i) < 3 or abs(ii - i) > len(nlLT[j]) - 3:
4573                                continue
4574                        t1 = get_radius_to_point((x1, y1), (nx, ny), nlLT[jj][ii - 1][0])
4575                    else:  # doing a line
4576                        if jj == j:  # except this one
4577                            if abs(ii - i) < 2 or abs(ii - i) == len(nlLT[j]) - 1:
4578                                continue
4579                            if abs(ii - i) == 2 and nlLT[j][(ii + i) / 2 - 1][3] <= 0:
4580                                continue
4581                            if (abs(ii - i) == len(nlLT[j]) - 2) and nlLT[j][-1][3] <= 0:
4582                                continue
4583                        nx2, ny2 = nlLT[jj][ii - 2][1]
4584                        x2, y2 = nlLT[jj][ii - 1][0]
4585                        nx23, ny23 = nlLT[jj][ii - 1][1]
4586                        x3, y3 = nlLT[jj][ii][0]
4587                        nx3, ny3 = nlLT[jj][ii][1]
4588                        if nlLT[jj][ii - 2][3] > 0:  # acute, so use normal, not bisector
4589                            nx2 = nx23
4590                            ny2 = ny23
4591                        if nlLT[jj][ii][3] > 0:  # acute, so use normal, not bisector
4592                            nx3 = nx23
4593                            ny3 = ny23
4594                        x23min = min(x2, x3)
4595                        x23max = max(x2, x3)
4596                        y23min = min(y2, y3)
4597                        y23max = max(y2, y3)
4598                        # see if line in range
4599                        if n1[2] == False and (x23max < xmin or x23min > xmax or y23max < ymin or y23min > ymax):
4600                            continue
4601                        t1 = get_radius_to_line((x1, y1), (nx, ny), (nx2, ny2), (x2, y2), (nx23, ny23), (x3, y3), (nx3, ny3))
4602                    if 0 <= t1 < r:
4603                        r = t1
4604                        iimin = ii
4605                        jjmin = jj
4606                        xmin = x1 + r * nx - r
4607                        xmax = x1 + r * nx + r
4608                        ymin = y1 + r * ny - r
4609                        ymax = y1 + r * ny + r
4610                # next ii
4611            # next jj
4612            return jjmin, iimin, r
4613            # end of get_biggest
4614
4615        def line_divide(xy0, j0, i0, xy1, j1, i1, n_xy, length):
4616            """LT recursively divide a line as much as necessary
4617
4618            NOTE: This function is not currently used
4619            By noting which other path segment is touched by the circles at each end,
4620            we can see if anything is to be gained by a further subdivision, since
4621            if they touch the same bit of path we can move linearly between them.
4622            Also, we can handle points correctly.
4623            Parameters:
4624                end points and indices of limiting path, normal, length
4625            Returns:
4626                list of toolpath points
4627                    each a list of 3 reals: x, y coordinates, radius
4628
4629            """
4630            (x0, y0) = xy0
4631            (x1, y1) = xy1
4632            (nx, ny) = n_xy
4633            global nlLT
4634            global i
4635            global j
4636            global lmin
4637            x2 = (x0 + x1) / 2
4638            y2 = (y0 + y1) / 2
4639            j2, i2, r2 = get_biggest((x2, y2), (nx, ny))
4640            if length < lmin:
4641                return [[x2, y2, r2]]
4642            if j2 == j0 and i2 == i0:  # Same as left end. Don't subdivide this part any more
4643                return [[x2, y2, r2], line_divide((x2, y2), j2, i2, (x1, y1), j1, i1, (nx, ny), length / 2)]
4644            if j2 == j1 and i2 == i1:  # Same as right end. Don't subdivide this part any more
4645                return [line_divide((x0, y0), j0, i0, (x2, y2), j2, i2, (nx, ny), length / 2), [x2, y2, r2]]
4646            return [line_divide((x0, y0), j0, i0, (x2, y2), j2, i2, (nx, ny), length / 2), line_divide((x2, y2), j2, i2, (x1, y1), j1, i1, (nx, ny), length / 2)]
4647            # end of line_divide()
4648
4649        def save_point(xy, w, i, j, ii, jj):
4650            """LT Save this point and delete previous one if linear
4651
4652            The point is, we generate tons of points but many may be in a straight 3D line.
4653            There is no benefit in saving the intermediate points.
4654            """
4655            (x, y) = xy
4656            global wl
4657            global cspm
4658            x = round(x, 4)  # round to 4 decimals
4659            y = round(y, 4)  # round to 4 decimals
4660            w = round(w, 4)  # round to 4 decimals
4661            if len(cspm) > 1:
4662                xy1a, xy1, xy1b, i1, j1, ii1, jj1 = cspm[-1]
4663                w1 = wl[-1]
4664                if i == i1 and j == j1 and ii == ii1 and jj == jj1:  # one match
4665                    xy1a, xy2, xy1b, i1, j1, ii1, jj1 = cspm[-2]
4666                    w2 = wl[-2]
4667                    if i == i1 and j == j1 and ii == ii1 and jj == jj1:  # two matches. Now test linearity
4668                        length1 = math.hypot(xy1[0] - x, xy1[1] - y)
4669                        length2 = math.hypot(xy2[0] - x, xy2[1] - y)
4670                        length12 = math.hypot(xy2[0] - xy1[0], xy2[1] - xy1[1])
4671                        # get the xy distance of point 1 from the line 0-2
4672                        if length2 > length1 and length2 > length12:  # point 1 between them
4673                            xydist = abs((xy2[0] - x) * (xy1[1] - y) - (xy1[0] - x) * (xy2[1] - y)) / length2
4674                            if xydist < ENGRAVING_TOLERANCE:  # so far so good
4675                                wdist = w2 + (w - w2) * length1 / length2 - w1
4676                                if abs(wdist) < ENGRAVING_TOLERANCE:
4677                                    cspm.pop()
4678                                    wl.pop()
4679            cspm += [[[x, y], [x, y], [x, y], i, j, ii, jj]]
4680            wl += [w]
4681            # end of save_point
4682
4683        def draw_point(xy0, xy, w, t):
4684            """LT Draw this point as a circle with a 1px dot in the middle (x,y)
4685            and a 3D line from (x0,y0) down to x,y. 3D line thickness should be t/2
4686
4687            Note that points that are subsequently erased as being unneeded do get
4688            displayed, but this helps the user see the total area covered.
4689            """
4690            (x0, y0) = xy0
4691            (x, y) = xy
4692            global gcode_3Dleft
4693            global gcode_3Dright
4694            if self.options.engraving_draw_calculation_paths:
4695                elem = engraving_group.add(PathElement.arc((x, y), 1))
4696                elem.set('gcodetools', "Engraving calculation toolpath")
4697                elem.style = "fill:#ff00ff; fill-opacity:0.46; stroke:#000000; stroke-width:0.1;"
4698
4699                # Don't draw zero radius circles
4700                if w:
4701                    elem = engraving_group.add(PathElement.arc((x, y), w))
4702                    elem.set('gcodetools', "Engraving calculation paths")
4703                    elem.style = "fill:none; fill-opacity:0.46; stroke:#000000; stroke-width:0.1;"
4704
4705                    # Find slope direction for shading
4706                    s = math.atan2(y - y0, x - x0)  # -pi to pi
4707                    # convert to 2 hex digits as a shade of red
4708                    s2 = "#{0:x}0000".format(int(101 * (1.5 - math.sin(s + 0.5))))
4709                    style = "stroke:{}; stroke-opacity:1;stroke-width:{};fill:none".format(s2, t/2)
4710                    right = gcode_3Dleft.add(PathElement(style=style, gcodetools="Gcode G1R"))
4711                    right.path = "M {:f},{:f} L {:f},{:f}".format(
4712                        x0 - eye_dist, y0, x - eye_dist - 0.14 * w, y)
4713                    left = gcode_3Dright.add(PathElement(style=style, gcodetools="Gcode G1L"))
4714                    left.path = "M {:f},{:f} L {:f},{:f}".format(
4715                        x0 + eye_dist, y0, x + eye_dist + 0.14 * r, y)
4716
4717        # end of subfunction definitions. engraving() starts here:
4718        gcode = ''
4719        r = 0  # theoretical and tool-radius-limited radii in pixels
4720        w = 0
4721        wmax = 0
4722        cspe = []
4723        we = []
4724        if not self.selected_paths:
4725            self.error("Please select at least one path to engrave and run again.")
4726            return
4727        if not self.check_dir():
4728            return
4729        # Find what units the user uses
4730        unit = " mm"
4731        if self.options.unit == "G20 (All units in inches)":
4732            unit = " inches"
4733        elif self.options.unit != "G21 (All units in mm)":
4734            self.error("Unknown unit selected. mm assumed")
4735        print_("engraving_max_dist mm/inch", self.options.engraving_max_dist)
4736
4737        # LT See if we can use this parameter for line and Bezier subdivision:
4738        bitlen = 20 / self.options.engraving_newton_iterations
4739
4740        for layer in self.layers:
4741            if layer in self.selected_paths and layer in self.orientation_points:
4742                # Calculate scale in pixels per user unit (mm or inch)
4743                p1 = self.orientation_points[layer][0][0]
4744                p2 = self.orientation_points[layer][0][1]
4745                ol = math.hypot(p1[0][0] - p2[0][0], p1[0][1] - p2[0][1])
4746                oluu = math.hypot(p1[1][0] - p2[1][0], p1[1][1] - p2[1][1])
4747                print_("Orientation2 p1 p2 ol oluu", p1, p2, ol, oluu)
4748                orientation_scale = ol / oluu
4749
4750                self.set_tool(layer)
4751                shape = self.tools[layer][0]['shape']
4752                if re.search('w', shape):
4753                    toolshape = eval('lambda w: ' + shape.strip('"'))
4754                else:
4755                    self.error("Tool '{}' has no shape. 45 degree cone assumed!".format(self.tools[layer][0]['name']))
4756                    toolshape = lambda w: w
4757                # Get tool radius in pixels
4758                toolr = self.tools[layer][0]['diameter'] * orientation_scale / 2
4759                print_("tool radius in pixels=", toolr)
4760                # max dist from path to engrave in user's units
4761                max_distuu = min(self.tools[layer][0]['diameter'] / 2, self.options.engraving_max_dist)
4762                max_dist = max_distuu * orientation_scale
4763                print_("max_dist pixels", max_dist)
4764
4765                engraving_group = self.selected_paths[layer][0].getparent().add(Group())
4766                if self.options.engraving_draw_calculation_paths and (self.my3Dlayer is None):
4767                    self.svg.add(Layer.new("3D"))
4768                # Create groups for left and right eyes
4769                if self.options.engraving_draw_calculation_paths:
4770                    gcode_3Dleft = self.my3Dlayer.add(Group(gcodetools="Gcode 3D L"))
4771                    gcode_3Dright = self.my3Dlayer.add(Group(gcodetools="Gcode 3D R"))
4772
4773                for node in self.selected_paths[layer]:
4774                    if isinstance(node, inkex.PathElement):
4775                        cspi = node.path.to_superpath()
4776                        # LT: Create my own list. n1LT[j] is for subpath j
4777                        nlLT = []
4778                        for j in xrange(len(cspi)):  # LT For each subpath...
4779                            # Remove zero length segments, assume closed path
4780                            i = 0  # LT was from i=1
4781                            while i < len(cspi[j]):
4782                                if abs(cspi[j][i - 1][1][0] - cspi[j][i][1][0]) < ENGRAVING_TOLERANCE and abs(cspi[j][i - 1][1][1] - cspi[j][i][1][1]) < ENGRAVING_TOLERANCE:
4783                                    cspi[j][i - 1][2] = cspi[j][i][2]
4784                                    del cspi[j][i]
4785                                else:
4786                                    i += 1
4787                        for csp in cspi:  # LT6a For each subpath...
4788                            # Create copies in 3D layer
4789                            print_("csp is zz ", csp)
4790                            cspl = []
4791                            cspr = []
4792                            # create list containing lines and points, starting with a point
4793                            # line members: [x,y],[nx,ny],False,i
4794                            # x,y is start of line. Normal on engraved side.
4795                            # Normal is normalised (unit length)
4796                            # Note that Y axis increases down the page
4797                            # corner members: [x,y],[nx,ny],True,sin(halfangle)
4798                            # if halfangle>0: radius 0 here. normal is bisector
4799                            # if halfangle<0. reflex angle. normal is bisector
4800                            # corner normals are divided by cos(halfangle)
4801                            # so that they will engrave correctly
4802                            print_("csp is", csp)
4803                            nlLT.append([])
4804                            for i in range(0, len(csp)):  # LT for each point
4805                                sp0 = csp[i - 2]
4806                                sp1 = csp[i - 1]
4807                                sp2 = csp[i]
4808                                if self.options.engraving_draw_calculation_paths:
4809                                    # Copy it to 3D layer objects
4810                                    spl = []
4811                                    spr = []
4812                                    for j in range(0, 3):
4813                                        pl = [sp2[j][0] - eye_dist, sp2[j][1]]
4814                                        pr = [sp2[j][0] + eye_dist, sp2[j][1]]
4815                                        spl += [pl]
4816                                        spr += [pr]
4817                                    cspl += [spl]
4818                                    cspr += [spr]
4819                                # LT find angle between this and previous segment
4820                                x0, y0 = sp1[1]
4821                                nx1, ny1 = csp_normalized_normal(sp1, sp2, 0)
4822                                # I don't trust this function, so test result
4823                                if abs(1 - math.hypot(nx1, ny1)) > 0.00001:
4824                                    print_("csp_normalised_normal error t=0", nx1, ny1, sp1, sp2)
4825                                    self.error("csp_normalised_normal error. See log.")
4826
4827                                nx0, ny0 = csp_normalized_normal(sp0, sp1, 1)
4828                                if abs(1 - math.hypot(nx0, ny0)) > 0.00001:
4829                                    print_("csp_normalised_normal error t=1", nx0, ny0, sp1, sp2)
4830                                    self.error("csp_normalised_normal error. See log.")
4831                                bx, by, s = bisect((nx0, ny0), (nx1, ny1))
4832                                # record x,y,normal,ifCorner, sin(angle-turned/2)
4833                                nlLT[-1] += [[[x0, y0], [bx, by], True, s]]
4834
4835                                # LT now do the line
4836                                if sp1[1] == sp1[2] and sp2[0] == sp2[1]:  # straightline
4837                                    nlLT[-1] += [[sp1[1], [nx1, ny1], False, i]]
4838                                else:  # Bezier. First, recursively cut it up:
4839                                    nn = bez_divide(sp1[1], sp1[2], sp2[0], sp2[1])
4840                                    first = True  # Flag entry to divided Bezier
4841                                    for bLT in nn:  # save as two line segments
4842                                        for seg in range(3):
4843                                            if seg > 0 or first:
4844                                                nx1 = bLT[seg][1] - bLT[seg + 1][1]
4845                                                ny1 = bLT[seg + 1][0] - bLT[seg][0]
4846                                                l1 = math.hypot(nx1, ny1)
4847                                                if l1 < ENGRAVING_TOLERANCE:
4848                                                    continue
4849                                                nx1 = nx1 / l1  # normalise them
4850                                                ny1 = ny1 / l1
4851                                                nlLT[-1] += [[bLT[seg], [nx1, ny1], False, i]]
4852                                                first = False
4853                                            if seg < 2:  # get outgoing bisector
4854                                                nx0 = nx1
4855                                                ny0 = ny1
4856                                                nx1 = bLT[seg + 1][1] - bLT[seg + 2][1]
4857                                                ny1 = bLT[seg + 2][0] - bLT[seg + 1][0]
4858                                                l1 = math.hypot(nx1, ny1)
4859                                                if l1 < ENGRAVING_TOLERANCE:
4860                                                    continue
4861                                                nx1 = nx1 / l1  # normalise them
4862                                                ny1 = ny1 / l1
4863                                                # bisect
4864                                                bx, by, s = bisect((nx0, ny0), (nx1, ny1))
4865                                                nlLT[-1] += [[bLT[seg + 1], [bx, by], True, 0.]]
4866                            # LT for each segment - ends here.
4867                            print_(("engraving_draw_calculation_paths=", self.options.engraving_draw_calculation_paths))
4868                            if self.options.engraving_draw_calculation_paths:
4869                                # Copy complete paths to 3D layer
4870                                cspl += [cspl[0]]  # Close paths
4871                                cspr += [cspr[0]]  # Close paths
4872                                style = "stroke:#808080; stroke-opacity:1; stroke-width:0.6; fill:none"
4873                                elem = gcode_3Dleft.add(PathElement(style=style, gcodetools="G1L outline"))
4874                                elem.path = CubicSuperPath([cspl])
4875                                elem = gcode_3Dright.add(Pathelement(style=style, gcodetools="G1R outline"))
4876                                elem.path = CubicSuperPath([cspr])
4877
4878                                for p in nlLT[-1]:  # For last sub-path
4879                                    if p[2]:
4880                                        elem = engraving_group.add(PathElement(gcodetools="Engraving normals"))
4881                                        elem.path = "M {:f},{:f} L {:f},{:f}".format(p[0][0], p[0][1],
4882                                            p[0][0] + p[1][0] * 10, p[0][1] + p[1][1] * 10)
4883                                        elem.style = "stroke:#f000af; stroke-opacity:0.46; stroke-width:0.1; fill:none"
4884                                    else:
4885                                        elem = engraving_group.add(PathElement(gcodetools="Engraving bisectors"))
4886                                        elem.path = "M {:f},{:f} L {:f},{:f}".format(p[0][0], p[0][1],
4887                                            p[0][0] + p[1][0] * 10, p[0][1] + p[1][1] * 10)
4888                                        elem.style = "stroke:#0000ff; stroke-opacity:0.46; stroke-width:0.1; fill:none"
4889
4890                        # LT6a build nlLT[j] for each subpath - ends here
4891                        # Calculate offset points
4892                        reflex = False
4893                        for j in xrange(len(nlLT)):  # LT6b for each subpath
4894                            cspm = []  # Will be my output. List of csps.
4895                            wl = []  # Will be my w output list
4896                            w = r = 0  # LT initial, as first point is an angle
4897                            for i in xrange(len(nlLT[j])):  # LT for each node
4898                                # LT Note: Python enables wrapping of array indices
4899                                # backwards to -1, -2, but not forwards. Hence:
4900                                n0 = nlLT[j][i - 2]  # previous node
4901                                n1 = nlLT[j][i - 1]  # current node
4902                                n2 = nlLT[j][i]  # next node
4903                                # if n1[2] == True and n1[3]==0 : # A straight angle
4904                                # continue
4905                                x1a, y1a = n1[0]  # this point/start of this line
4906                                nx, ny = n1[1]
4907                                x1b, y1b = n2[0]  # next point/end of this line
4908                                if n1[2]:  # We're at a corner
4909                                    bits = 1
4910                                    bit0 = 0
4911                                    # lastr=r #Remember r from last line
4912                                    lastw = w  # Remember w from last line
4913                                    w = max_dist
4914                                    if n1[3] > 0:  # acute. Limit radius
4915                                        len1 = math.hypot((n0[0][0] - n1[0][0]), (n0[0][1] - n1[0][1]))
4916                                        if i < (len(nlLT[j]) - 1):
4917                                            len2 = math.hypot((nlLT[j][i + 1][0][0] - n1[0][0]), (nlLT[j][i + 1][0][1] - n1[0][1]))
4918                                        else:
4919                                            len2 = math.hypot((nlLT[j][0][0][0] - n1[0][0]), (nlLT[j][0][0][1] - n1[0][1]))
4920                                        # set initial r value, not to be exceeded
4921                                        w = math.sqrt(min(len1, len2)) / n1[3]
4922                                else:  # line. Cut it up if long.
4923                                    if n0[3] > 0 and not self.options.engraving_draw_calculation_paths:
4924                                        bit0 = r * n0[3]  # after acute corner
4925                                    else:
4926                                        bit0 = 0.0
4927                                    length = math.hypot((x1b - x1a), (y1a - y1b))
4928                                    bit0 = (min(length, bit0))
4929                                    bits = int((length - bit0) / bitlen)
4930                                    # split excess evenly at both ends
4931                                    bit0 += (length - bit0 - bitlen * bits) / 2
4932                                for b in xrange(bits):  # divide line into bits
4933                                    x1 = x1a + ny * (b * bitlen + bit0)
4934                                    y1 = y1a - nx * (b * bitlen + bit0)
4935                                    jjmin, iimin, w = get_biggest((x1, y1), (nx, ny))
4936                                    print_("i,j,jjmin,iimin,w", i, j, jjmin, iimin, w)
4937                                    wmax = max(wmax, w)
4938                                    if reflex:  # just after a reflex corner
4939                                        reflex = False
4940                                        if w < lastw:  # need to adjust it
4941                                            draw_point((x1, y1), (n0[0][0] + n0[1][0] * w, n0[0][1] + n0[1][1] * w), w, (lastw - w) / 2)
4942                                            save_point((n0[0][0] + n0[1][0] * w, n0[0][1] + n0[1][1] * w), w, i, j, iimin, jjmin)
4943                                    if n1[2]:  # We're at a corner
4944                                        if n1[3] > 0:  # acute
4945                                            save_point((x1 + nx * w, y1 + ny * w), w, i, j, iimin, jjmin)
4946                                            draw_point((x1, y1), (x1, y1), 0, 0)
4947                                            save_point((x1, y1), 0, i, j, iimin, jjmin)
4948                                        elif n1[3] < 0:  # reflex
4949                                            if w > lastw:
4950                                                draw_point((x1, y1), (x1 + nx * lastw, y1 + ny * lastw), w, (w - lastw) / 2)
4951                                                wmax = max(wmax, w)
4952                                                save_point((x1 + nx * w, y1 + ny * w), w, i, j, iimin, jjmin)
4953                                    elif b > 0 and n2[3] > 0 and not self.options.engraving_draw_calculation_paths:  # acute corner coming up
4954                                        if jjmin == j and iimin == i + 2:
4955                                            break
4956                                    draw_point((x1, y1), (x1 + nx * w, y1 + ny * w), w, bitlen)
4957                                    save_point((x1 + nx * w, y1 + ny * w), w, i, j, iimin, jjmin)
4958
4959                                # LT end of for each bit of this line
4960                                if n1[2] == True and n1[3] < 0:  # reflex angle
4961                                    reflex = True
4962                                lastw = w  # remember this w
4963                            # LT next i
4964                            cspm += [cspm[0]]
4965                            print_("cspm", cspm)
4966                            wl += [wl[0]]
4967                            print_("wl", wl)
4968                            # Note: Original csp_points was a list, each element
4969                            # being 4 points, with the first being the same as the
4970                            # last of the previous set.
4971                            # Each point is a list of [cx,cy,r,w]
4972                            # I have flattened it to a flat list of points.
4973
4974                            if self.options.engraving_draw_calculation_paths:
4975                                node = engraving_group.add(PathElement(
4976                                    gcodetools="Engraving calculation paths",
4977                                    style=MARKER_STYLE["biarc_style_i"]['biarc1']))
4978                                node.path = CubicSuperPath([cspm])
4979                                for i in xrange(len(cspm)):
4980                                    elem = engraving_group.add(PathElement.arc(cspm[i][1], wl[i]))
4981                                    elem.set('gcodetools', "Engraving calculation paths")
4982                                    elem.style = "fill:none;fill-opacity:0.46;stroke:#000000;stroke-width:0.1;"
4983                            cspe += [cspm]
4984                            wluu = []  # width list in user units: mm/inches
4985                            for w in wl:
4986                                wluu += [w / orientation_scale]
4987                            print_("wl in pixels", wl)
4988                            print_("wl in user units", wluu)
4989                            # LT previously, we was in pixels so gave wrong depth
4990                            we += [wluu]
4991                        # LT6b For each subpath - ends here
4992                    # LT5 if it is a path - ends here
4993                # LT4 for each selected object in this layer - ends here
4994
4995                if cspe:
4996                    curve = self.parse_curve(cspe, layer, we, toolshape)  # convert to lines
4997                    self.draw_curve(curve, layer, engraving_group)
4998                    gcode += self.generate_gcode(curve, layer, self.options.Zsurface)
4999
5000            # LT3 for layers loop ends here
5001        if gcode != '':
5002            self.header += "(Tool diameter should be at least " + str(2 * wmax / orientation_scale) + unit + ")\n"
5003            self.header += "(Depth, as a function of radius w, must be " + self.tools[layer][0]['shape'] + ")\n"
5004            self.header += "(Rapid feeds use safe Z=" + str(self.options.Zsafe) + unit + ")\n"
5005            self.header += "(Material surface at Z=" + str(self.options.Zsurface) + unit + ")\n"
5006            self.export_gcode(gcode)
5007        else:
5008            self.error("No need to engrave sharp angles.")
5009
5010    ################################################################################
5011    #
5012    # Orientation
5013    #
5014    ################################################################################
5015    def tab_orientation(self, layer=None):
5016        self.get_info()
5017
5018        if layer is None:
5019            layer = self.svg.get_current_layer() if self.svg.get_current_layer() is not None else self.document.getroot()
5020
5021        transform = self.get_transforms(layer)
5022        if transform:
5023            transform = self.reverse_transform(transform)
5024            transform = str(Transform(transform))
5025
5026        if self.options.orientation_points_count == "graffiti":
5027            print_(self.graffiti_reference_points)
5028            print_("Inserting graffiti points")
5029            if layer in self.graffiti_reference_points:
5030                graffiti_reference_points_count = len(self.graffiti_reference_points[layer])
5031            else:
5032                graffiti_reference_points_count = 0
5033            axis = ["X", "Y", "Z", "A"][graffiti_reference_points_count % 4]
5034            attr = {'gcodetools': "Gcodetools graffiti reference point"}
5035            if transform:
5036                attr["transform"] = transform
5037            group = layer.add(Group(**attr))
5038            elem = group.add(PathElement(style="stroke:none;fill:#00ff00;"))
5039            elem.set('gcodetools', "Gcodetools graffiti reference point arrow")
5040            elem.path = 'm {},{} 2.9375,-6.343750000001 0.8125,1.90625 6.843748640396,'\
5041                '-6.84374864039 0,0 0.6875,0.6875 -6.84375,6.84375 1.90625,0.8125000000'\
5042                '01 z z'.format(graffiti_reference_points_count * 100, 0)
5043
5044            draw_text(axis, graffiti_reference_points_count * 100 + 10, -10, group=group, gcodetools_tag="Gcodetools graffiti reference point text")
5045
5046        elif self.options.orientation_points_count == "in-out reference point":
5047            draw_pointer(group=self.svg.get_current_layer(), x=self.svg.namedview.center, figure="arrow", pointer_type="In-out reference point", text="In-out point")
5048
5049        else:
5050            print_("Inserting orientation points")
5051
5052            if layer in self.orientation_points:
5053                self.error("Active layer already has orientation points! Remove them or select another layer!", "error")
5054
5055            attr = {"gcodetools": "Gcodetools orientation group"}
5056            if transform:
5057                attr["transform"] = transform
5058
5059            orientation_group = layer.add(Group(**attr))
5060            doc_height = self.svg.unittouu(self.document.getroot().get('height'))
5061            if self.document.getroot().get('height') == "100%":
5062                doc_height = 1052.3622047
5063                print_("Overriding height from 100 percents to {}".format(doc_height))
5064            if self.options.unit == "G21 (All units in mm)":
5065                points = [[0., 0., self.options.Zsurface], [100., 0., self.options.Zdepth], [0., 100., 0.]]
5066            elif self.options.unit == "G20 (All units in inches)":
5067                points = [[0., 0., self.options.Zsurface], [5., 0., self.options.Zdepth], [0., 5., 0.]]
5068            if self.options.orientation_points_count == "2":
5069                points = points[:2]
5070            for i in points:
5071                name = "Gcodetools orientation point ({} points)".format(
5072                    self.options.orientation_points_count)
5073                grp = orientation_group.add(Group(gcodetools=name))
5074                elem = grp.add(PathElement(style="stroke:none;fill:#000000;"))
5075                elem.set('gcodetools', "Gcodetools orientation point arrow")
5076                elem.path = 'm {},{} 2.9375,-6.343750000001 0.8125,1.90625 6.843748640396,'\
5077                    '-6.84374864039 0,0 0.6875,0.6875 -6.84375,6.84375 1.90625,0.812500000'\
5078                    '001 z'.format(i[0], -i[1] + doc_height)
5079
5080                draw_text("({}; {}; {})".format(i[0], i[1], i[2]), (i[0] + 10), (-i[1] - 10 + doc_height), group=grp, gcodetools_tag="Gcodetools orientation point text")
5081
5082    ################################################################################
5083    #
5084    # Tools library
5085    #
5086    ################################################################################
5087    def tab_tools_library(self, layer=None):
5088        self.get_info()
5089
5090        if self.options.tools_library_type == "check":
5091            return self.check_tools_and_op()
5092
5093        # Add a tool to the drawing
5094        if layer is None:
5095            layer = self.svg.get_current_layer() if self.svg.get_current_layer() is not None else self.document.getroot()
5096        if layer in self.tools:
5097            self.error("Active layer already has a tool! Remove it or select another layer!", "error")
5098
5099        if self.options.tools_library_type == "cylinder cutter":
5100            tool = {
5101                "name": "Cylindrical cutter",
5102                "id": "Cylindrical cutter 0001",
5103                "diameter": 10,
5104                "penetration angle": 90,
5105                "feed": "400",
5106                "penetration feed": "100",
5107                "depth step": "1",
5108                "tool change gcode": " "
5109            }
5110        elif self.options.tools_library_type == "lathe cutter":
5111            tool = {
5112                "name": "Lathe cutter",
5113                "id": "Lathe cutter 0001",
5114                "diameter": 10,
5115                "penetration angle": 90,
5116                "feed": "400",
5117                "passing feed": "800",
5118                "fine feed": "100",
5119                "penetration feed": "100",
5120                "depth step": "1",
5121                "tool change gcode": " "
5122            }
5123        elif self.options.tools_library_type == "cone cutter":
5124            tool = {
5125                "name": "Cone cutter",
5126                "id": "Cone cutter 0001",
5127                "diameter": 10,
5128                "shape": "w",
5129                "feed": "400",
5130                "penetration feed": "100",
5131                "depth step": "1",
5132                "tool change gcode": " "
5133            }
5134        elif self.options.tools_library_type == "tangent knife":
5135            tool = {
5136                "name": "Tangent knife",
5137                "id": "Tangent knife 0001",
5138                "feed": "400",
5139                "penetration feed": "100",
5140                "depth step": "100",
5141                "4th axis meaning": "tangent knife",
5142                "4th axis scale": 1.,
5143                "4th axis offset": 0,
5144                "tool change gcode": " "
5145            }
5146
5147        elif self.options.tools_library_type == "plasma cutter":
5148            tool = {
5149                "name": "Plasma cutter",
5150                "id": "Plasma cutter 0001",
5151                "diameter": 10,
5152                "penetration feed": 100,
5153                "feed": 400,
5154                "gcode before path": """G31 Z-100 F500 (find metal)
5155G92 Z0 (zero z)
5156G00 Z10 F500 (going up)
5157M03 (turn on plasma)
5158G04 P0.2 (pause)
5159G01 Z1 (going to cutting z)\n""",
5160                "gcode after path": "M05 (turn off plasma)\n",
5161            }
5162        elif self.options.tools_library_type == "graffiti":
5163            tool = {
5164                "name": "Graffiti",
5165                "id": "Graffiti 0001",
5166                "diameter": 10,
5167                "penetration feed": 100,
5168                "feed": 400,
5169                "gcode before path": """M03 S1(Turn spray on)\n """,
5170                "gcode after path": "M05 (Turn spray off)\n ",
5171                "tool change gcode": "(Add G00 here to change sprayer if needed)\n",
5172
5173            }
5174
5175        else:
5176            tool = self.default_tool
5177
5178        tool_num = sum([len(self.tools[i]) for i in self.tools])
5179        colors = ["00ff00", "0000ff", "ff0000", "fefe00", "00fefe", "fe00fe", "fe7e00", "7efe00", "00fe7e", "007efe", "7e00fe", "fe007e"]
5180
5181        tools_group = layer.add(Group(gcodetools="Gcodetools tool definition"))
5182        bg = tools_group.add(PathElement(gcodetools="Gcodetools tool background"))
5183        bg.style = "fill-opacity:0.5;stroke:#444444;"
5184        bg.style['fill'] = colors[tool_num % len(colors)]
5185
5186        y = 0
5187        keys = []
5188        for key in self.tools_field_order:
5189            if key in tool:
5190                keys += [key]
5191        for key in tool:
5192            if key not in keys:
5193                keys += [key]
5194        for key in keys:
5195            g = tools_group.add(Group(gcodetools="Gcodetools tool parameter"))
5196            draw_text(key, 0, y, group=g, gcodetools_tag="Gcodetools tool definition field name", font_size=10 if key != 'name' else 20)
5197            param = tool[key]
5198            if type(param) == str and re.match("^\\s*$", param):
5199                param = "(None)"
5200            draw_text(param, 150, y, group=g, gcodetools_tag="Gcodetools tool definition field value", font_size=10 if key != 'name' else 20)
5201            v = str(param).split("\n")
5202            y += 15 * len(v) if key != 'name' else 20 * len(v)
5203
5204        bg.set('d', "m -20,-20 l 400,0 0,{:f} -400,0 z ".format(y + 50))
5205        tools_group.transform.add_translate(*self.svg.namedview.center)
5206        tools_group.transform.add_translate(-150, 0)
5207
5208    ################################################################################
5209    #
5210    # Check tools and OP assignment
5211    #
5212    ################################################################################
5213    def check_tools_and_op(self):
5214        if len(self.svg.selected) <= 0:
5215            self.error("Selection is empty! Will compute whole drawing.")
5216            paths = self.paths
5217        else:
5218            paths = self.selected_paths
5219        #    Set group
5220        parent = self.selected_paths.keys()[0] if len(self.selected_paths.keys()) > 0 else self.layers[0]
5221        group = parent.add(Group())
5222        trans_ = [[1, 0.3, 0], [0, 0.5, 0]]
5223
5224        self.set_markers()
5225
5226        bounds = [float('inf'), float('inf'), float('-inf'), float('-inf')]
5227        tools_bounds = {}
5228        for layer in self.layers:
5229            if layer in paths:
5230                self.set_tool(layer)
5231                tool = self.tools[layer][0]
5232                tools_bounds[layer] = tools_bounds[layer] if layer in tools_bounds else [float("inf"), float("-inf")]
5233                for path in paths[layer]:
5234                    group.insert(0, PathElement(**path.attrib))
5235                    new = group.getchildren()[0]
5236                    new.style = Style(
5237                        stroke='#000044', stroke_width=1,
5238                        marker_mid='url(#CheckToolsAndOPMarker)',
5239                        fill=tool["style"].get('fill', '#00ff00'),
5240                        fill_opacity=tool["style"].get('fill-opacity', 0.5))
5241
5242                    trans = trans_ * self.get_transforms(path)
5243                    csp = path.path.transform(trans).to_superpath()
5244
5245                    path_bounds = csp_simple_bound(csp)
5246                    trans = str(Transform(trans))
5247                    bounds = [min(bounds[0], path_bounds[0]), min(bounds[1], path_bounds[1]), max(bounds[2], path_bounds[2]), max(bounds[3], path_bounds[3])]
5248                    tools_bounds[layer] = [min(tools_bounds[layer][0], path_bounds[1]), max(tools_bounds[layer][1], path_bounds[3])]
5249
5250                    new.set("transform", trans)
5251                    trans_[1][2] += 20
5252                trans_[1][2] += 100
5253
5254        for layer in self.layers:
5255            if layer in self.tools:
5256                if layer in tools_bounds:
5257                    tool = self.tools[layer][0]
5258                    g = copy.deepcopy(tool["self_group"])
5259                    g.attrib["gcodetools"] = "Check tools and OP assignment"
5260                    trans = [[1, 0.3, bounds[2]], [0, 0.5, tools_bounds[layer][0]]]
5261                    g.set("transform", str(Transform(trans)))
5262                    group.insert(0, g)
5263
5264    ################################################################################
5265    # TODO Launch browser on help tab
5266    ################################################################################
5267    def tab_help(self):
5268        self.error("Switch to another tab to run the extensions.\n"
5269                   "No changes are made if the preferences or help tabs are active.\n\n"
5270                   "Tutorials, manuals and support can be found at\n"
5271                   " English support forum:\n"
5272                   "    http://www.cnc-club.ru/gcodetools\n"
5273                   "and Russian support forum:\n"
5274                   "    http://www.cnc-club.ru/gcodetoolsru")
5275        return
5276
5277    def tab_about(self):
5278        return self.tab_help()
5279
5280    def tab_preferences(self):
5281        return self.tab_help()
5282
5283    def tab_options(self):
5284        return self.tab_help()
5285
5286
5287    ################################################################################
5288    # Lathe
5289    ################################################################################
5290    def generate_lathe_gcode(self, subpath, layer, feed_type):
5291        if len(subpath) < 2:
5292            return ""
5293        feed = " F {:f}".format(self.tool[feed_type])
5294        x = self.options.lathe_x_axis_remap
5295        z = self.options.lathe_z_axis_remap
5296        flip_angle = -1 if x.lower() + z.lower() in ["xz", "yx", "zy"] else 1
5297        alias = {"X": "I", "Y": "J", "Z": "K", "x": "i", "y": "j", "z": "k"}
5298        i_ = alias[x]
5299        k_ = alias[z]
5300        c = [[subpath[0][1], "move", 0, 0, 0]]
5301        for sp1, sp2 in zip(subpath, subpath[1:]):
5302            c += biarc(sp1, sp2, 0, 0)
5303        for i in range(1, len(c)):  # Just in case check end point of each segment
5304            c[i - 1][4] = c[i][0][:]
5305        c += [[subpath[-1][1], "end", 0, 0, 0]]
5306        self.draw_curve(c, layer, style=MARKER_STYLE["biarc_style_lathe_{}".format(feed_type)])
5307
5308        gcode = ("G01 {} {:f} {} {:f}".format(x, c[0][4][0], z, c[0][4][1])) + feed + "\n"  # Just in case move to the start...
5309        for s in c:
5310            if s[1] == 'line':
5311                gcode += ("G01 {} {:f} {} {:f}".format(x, s[4][0], z, s[4][1])) + feed + "\n"
5312            elif s[1] == 'arc':
5313                r = [(s[2][0] - s[0][0]), (s[2][1] - s[0][1])]
5314                if (r[0] ** 2 + r[1] ** 2) > self.options.min_arc_radius ** 2:
5315                    r1 = (P(s[0]) - P(s[2]))
5316                    r2 = (P(s[4]) - P(s[2]))
5317                    if abs(r1.mag() - r2.mag()) < 0.001:
5318                        gcode += ("G02" if s[3] * flip_angle < 0 else "G03") + (" {} {:f} {} {:f} {} {:f} {} {:f}".format(x, s[4][0], z, s[4][1], i_, (s[2][0] - s[0][0]), k_, (s[2][1] - s[0][1]))) + feed + "\n"
5319                    else:
5320                        r = (r1.mag() + r2.mag()) / 2
5321                        gcode += ("G02" if s[3] * flip_angle < 0 else "G03") + (" {} {:f} {} {:f}".format(x, s[4][0], z, s[4][1])) + " R{:f}".format(r) + feed + "\n"
5322        return gcode
5323
5324    def tab_lathe(self):
5325        self.get_info_plus()
5326        if not self.check_dir():
5327            return
5328        x = self.options.lathe_x_axis_remap
5329        z = self.options.lathe_z_axis_remap
5330        x = re.sub("^\\s*([XYZxyz])\\s*$", r"\1", x)
5331        z = re.sub("^\\s*([XYZxyz])\\s*$", r"\1", z)
5332        if x not in ["X", "Y", "Z", "x", "y", "z"] or z not in ["X", "Y", "Z", "x", "y", "z"]:
5333            self.error("Lathe X and Z axis remap should be 'X', 'Y' or 'Z'. Exiting...")
5334            return
5335        if x.lower() == z.lower():
5336            self.error("Lathe X and Z axis remap should be the same. Exiting...")
5337            return
5338        if x.lower() + z.lower() in ["xy", "yx"]:
5339            gcode_plane_selection = "G17 (Using XY plane)\n"
5340        if x.lower() + z.lower() in ["xz", "zx"]:
5341            gcode_plane_selection = "G18 (Using XZ plane)\n"
5342        if x.lower() + z.lower() in ["zy", "yz"]:
5343            gcode_plane_selection = "G19 (Using YZ plane)\n"
5344        self.options.lathe_x_axis_remap = x
5345        self.options.lathe_z_axis_remap = z
5346
5347        paths = self.selected_paths
5348        self.tool = []
5349        gcode = ""
5350        for layer in self.layers:
5351            if layer in paths:
5352                self.set_tool(layer)
5353                if self.tool != self.tools[layer][0]:
5354                    self.tool = self.tools[layer][0]
5355                    self.tool["passing feed"] = float(self.tool["passing feed"] if "passing feed" in self.tool else self.tool["feed"])
5356                    self.tool["feed"] = float(self.tool["feed"])
5357                    self.tool["fine feed"] = float(self.tool["fine feed"] if "fine feed" in self.tool else self.tool["feed"])
5358                    gcode += ("(Change tool to {})\n".format(re.sub("\"'\\(\\)\\\\", " ", self.tool["name"]))) + self.tool["tool change gcode"] + "\n"
5359
5360                for path in paths[layer]:
5361                    csp = self.transform_csp(path.path.to_superpath(), layer)
5362
5363                    for subpath in csp:
5364                        # Offset the path if fine cut is defined.
5365                        fine_cut = subpath[:]
5366                        if self.options.lathe_fine_cut_width > 0:
5367                            r = self.options.lathe_fine_cut_width
5368                            if self.options.lathe_create_fine_cut_using == "Move path":
5369                                subpath = [[[i2[0], i2[1] + r] for i2 in i1] for i1 in subpath]
5370                            else:
5371                                # Close the path to make offset correct
5372                                bound = csp_simple_bound([subpath])
5373                                minx, miny, maxx, maxy = csp_true_bounds([subpath])
5374                                offsetted_subpath = csp_subpath_line_to(subpath[:], [[subpath[-1][1][0], miny[1] - r * 10], [subpath[0][1][0], miny[1] - r * 10], [subpath[0][1][0], subpath[0][1][1]]])
5375                                left = subpath[-1][1][0]
5376                                right = subpath[0][1][0]
5377                                if left > right:
5378                                    left, right = right, left
5379                                offsetted_subpath = csp_offset([offsetted_subpath], r if not csp_subpath_ccw(offsetted_subpath) else -r)
5380                                offsetted_subpath = csp_clip_by_line(offsetted_subpath, [left, 10], [left, 0])
5381                                offsetted_subpath = csp_clip_by_line(offsetted_subpath, [right, 0], [right, 10])
5382                                offsetted_subpath = csp_clip_by_line(offsetted_subpath, [0, miny[1] - r], [10, miny[1] - r])
5383                                # Join offsetted_subpath together
5384                                # Hope there won't be any circles
5385                                subpath = csp_join_subpaths(offsetted_subpath)[0]
5386
5387                        # Create solid object from path and lathe_width
5388                        bound = csp_simple_bound([subpath])
5389                        top_start = [subpath[0][1][0], self.options.lathe_width + self.options.Zsafe + self.options.lathe_fine_cut_width]
5390                        top_end = [subpath[-1][1][0], self.options.lathe_width + self.options.Zsafe + self.options.lathe_fine_cut_width]
5391
5392                        gcode += ("G01 {} {:f} F {:f} \n".format(z, top_start[1], self.tool["passing feed"]))
5393                        gcode += ("G01 {} {:f} {} {:f} F {:f} \n".format(x, top_start[0], z, top_start[1], self.tool["passing feed"]))
5394
5395                        subpath = csp_concat_subpaths(csp_subpath_line_to([], [top_start, subpath[0][1]]), subpath)
5396                        subpath = csp_subpath_line_to(subpath, [top_end, top_start])
5397
5398                        width = max(0, self.options.lathe_width - max(0, bound[1]))
5399                        step = self.tool['depth step']
5400                        steps = int(math.ceil(width / step))
5401                        for i in range(steps + 1):
5402                            current_width = self.options.lathe_width - step * i
5403                            intersections = []
5404                            for j in range(1, len(subpath)):
5405                                sp1 = subpath[j - 1]
5406                                sp2 = subpath[j]
5407                                intersections += [[j, k] for k in csp_line_intersection([bound[0] - 10, current_width], [bound[2] + 10, current_width], sp1, sp2)]
5408                                intersections += [[j, k] for k in csp_line_intersection([bound[0] - 10, current_width + step], [bound[2] + 10, current_width + step], sp1, sp2)]
5409                            parts = csp_subpath_split_by_points(subpath, intersections)
5410                            for part in parts:
5411                                minx, miny, maxx, maxy = csp_true_bounds([part])
5412                                y = (maxy[1] + miny[1]) / 2
5413                                if y > current_width + step:
5414                                    gcode += self.generate_lathe_gcode(part, layer, "passing feed")
5415                                elif current_width <= y <= current_width + step:
5416                                    gcode += self.generate_lathe_gcode(part, layer, "feed")
5417                                else:
5418                                    # full step cut
5419                                    part = csp_subpath_line_to([], [part[0][1], part[-1][1]])
5420                                    gcode += self.generate_lathe_gcode(part, layer, "feed")
5421
5422                        top_start = [fine_cut[0][1][0], self.options.lathe_width + self.options.Zsafe + self.options.lathe_fine_cut_width]
5423                        top_end = [fine_cut[-1][1][0], self.options.lathe_width + self.options.Zsafe + self.options.lathe_fine_cut_width]
5424                        gcode += "\n(Fine cutting start)\n(Calculating fine cut using {})\n".format(self.options.lathe_create_fine_cut_using)
5425                        for i in range(int(self.options.lathe_fine_cut_count)):
5426                            width = self.options.lathe_fine_cut_width * (1 - float(i + 1) / self.options.lathe_fine_cut_count)
5427                            if width == 0:
5428                                current_pass = fine_cut
5429                            else:
5430                                if self.options.lathe_create_fine_cut_using == "Move path":
5431                                    current_pass = [[[i2[0], i2[1] + width] for i2 in i1] for i1 in fine_cut]
5432                                else:
5433                                    minx, miny, maxx, maxy = csp_true_bounds([fine_cut])
5434                                    offsetted_subpath = csp_subpath_line_to(fine_cut[:], [[fine_cut[-1][1][0], miny[1] - r * 10], [fine_cut[0][1][0], miny[1] - r * 10], [fine_cut[0][1][0], fine_cut[0][1][1]]])
5435                                    left = fine_cut[-1][1][0]
5436                                    right = fine_cut[0][1][0]
5437                                    if left > right:
5438                                        left, right = right, left
5439                                    offsetted_subpath = csp_offset([offsetted_subpath], width if not csp_subpath_ccw(offsetted_subpath) else -width)
5440                                    offsetted_subpath = csp_clip_by_line(offsetted_subpath, [left, 10], [left, 0])
5441                                    offsetted_subpath = csp_clip_by_line(offsetted_subpath, [right, 0], [right, 10])
5442                                    offsetted_subpath = csp_clip_by_line(offsetted_subpath, [0, miny[1] - r], [10, miny[1] - r])
5443                                    current_pass = csp_join_subpaths(offsetted_subpath)[0]
5444
5445                            gcode += "\n(Fine cut {:d}-th cicle start)\n".format(i + 1)
5446                            gcode += ("G01 {} {:f} {} {:f} F {:f} \n".format(x, top_start[0], z, top_start[1], self.tool["passing feed"]))
5447                            gcode += ("G01 {} {:f} {} {:f} F {:f} \n".format(x, current_pass[0][1][0], z, current_pass[0][1][1] + self.options.lathe_fine_cut_width, self.tool["passing feed"]))
5448                            gcode += ("G01 {} {:f} {} {:f} F {:f} \n".format(x, current_pass[0][1][0], z, current_pass[0][1][1], self.tool["fine feed"]))
5449
5450                            gcode += self.generate_lathe_gcode(current_pass, layer, "fine feed")
5451                            gcode += ("G01 {} {:f} F {:f} \n".format(z, top_start[1], self.tool["passing feed"]))
5452                            gcode += ("G01 {} {:f} {} {:f} F {:f} \n".format(x, top_start[0], z, top_start[1], self.tool["passing feed"]))
5453
5454        self.export_gcode(gcode)
5455
5456    ################################################################################
5457    #
5458    # Lathe modify path
5459    # Modifies path to fit current cutter. As for now straight rect cutter.
5460    #
5461    ################################################################################
5462
5463    def tab_lathe_modify_path(self):
5464        self.get_info()
5465        if self.selected_paths == {} and self.options.auto_select_paths:
5466            paths = self.paths
5467            self.error("No paths are selected! Trying to work on all available paths.")
5468        else:
5469            paths = self.selected_paths
5470
5471        for layer in self.layers:
5472            if layer in paths:
5473                width = self.options.lathe_rectangular_cutter_width
5474                for path in paths[layer]:
5475                    csp = self.transform_csp(path.path.to_superpath(), layer)
5476                    new_csp = []
5477                    for subpath in csp:
5478                        orientation = subpath[-1][1][0] > subpath[0][1][0]
5479                        new_subpath = []
5480
5481                        # Split segment at x' and y' == 0
5482                        for sp1, sp2 in zip(subpath[:], subpath[1:]):
5483                            ax, ay, bx, by, cx, cy, dx, dy = csp_parameterize(sp1, sp2)
5484                            roots = cubic_solver_real(0, 3 * ax, 2 * bx, cx)
5485                            roots += cubic_solver_real(0, 3 * ay, 2 * by, cy)
5486                            new_subpath = csp_concat_subpaths(new_subpath, csp_seg_split(sp1, sp2, roots))
5487                        subpath = new_subpath
5488                        new_subpath = []
5489                        first_seg = True
5490                        for sp1, sp2 in zip(subpath[:], subpath[1:]):
5491                            n = csp_normalized_normal(sp1, sp2, 0)
5492                            a = math.atan2(n[0], n[1])
5493                            if a == 0 or a == math.pi:
5494                                n = csp_normalized_normal(sp1, sp2, 1)
5495                            a = math.atan2(n[0], n[1])
5496                            if a != 0 and a != math.pi:
5497                                o = 0 if 0 < a <= math.pi / 2 or -math.pi < a < -math.pi / 2 else 1
5498                                if not orientation:
5499                                    o = 1 - o
5500
5501                                # Add first horizontal straight line if needed
5502                                if not first_seg and new_subpath == []:
5503                                    new_subpath = [[[subpath[0][i][0] - width * o, subpath[0][i][1]] for i in range(3)]]
5504
5505                                new_subpath = csp_concat_subpaths(
5506                                        new_subpath,
5507                                        [
5508                                            [[sp1[i][0] - width * o, sp1[i][1]] for i in range(3)],
5509                                            [[sp2[i][0] - width * o, sp2[i][1]] for i in range(3)]
5510                                        ]
5511                                )
5512                            first_seg = False
5513
5514                        # Add last horizontal straight line if needed
5515                        if a == 0 or a == math.pi:
5516                            new_subpath += [[[subpath[-1][i][0] - width * o, subpath[-1][i][1]] for i in range(3)]]
5517
5518                    new_csp += [new_subpath]
5519                    self.draw_csp(new_csp, layer)
5520
5521    ################################################################################
5522    # Graffiti function generates Gcode for graffiti drawer
5523    ################################################################################
5524    def tab_graffiti(self):
5525        self.get_info_plus()
5526        # Get reference points.
5527
5528        def get_gcode_coordinates(point, layer):
5529            gcode = ''
5530            pos = []
5531            for ref_point in self.graffiti_reference_points[layer]:
5532                c = math.sqrt((point[0] - ref_point[0][0]) ** 2 + (point[1] - ref_point[0][1]) ** 2)
5533                gcode += " {} {:f}".format(ref_point[1], c)
5534                pos += [c]
5535            return pos, gcode
5536
5537        def graffiti_preview_draw_point(x1, y1, color, radius=.5):
5538            self.graffiti_preview = self.graffiti_preview
5539            r, g, b, a_ = color
5540            for x in range(int(x1 - 1 - math.ceil(radius)), int(x1 + 1 + math.ceil(radius) + 1)):
5541                for y in range(int(y1 - 1 - math.ceil(radius)), int(y1 + 1 + math.ceil(radius) + 1)):
5542                    if x >= 0 and y >= 0 and y < len(self.graffiti_preview) and x * 4 < len(self.graffiti_preview[0]):
5543                        d = math.sqrt((x1 - x) ** 2 + (y1 - y) ** 2)
5544                        a = float(a_) * (max(0, (1 - (d - radius))) if d > radius else 1) / 256
5545                        self.graffiti_preview[y][x * 4] = int(r * a + (1 - a) * self.graffiti_preview[y][x * 4])
5546                        self.graffiti_preview[y][x * 4 + 1] = int(g * a + (1 - a) * self.graffiti_preview[y][x * 4 + 1])
5547                        self.graffiti_preview[y][x * 4 + 2] = int(g * b + (1 - a) * self.graffiti_preview[y][x * 4 + 2])
5548                        self.graffiti_preview[y][x * 4 + 3] = min(255, int(self.graffiti_preview[y][x * 4 + 3] + a * 256))
5549
5550        def graffiti_preview_transform(x, y):
5551            tr = self.graffiti_preview_transform
5552            d = max(tr[2] - tr[0] + 2, tr[3] - tr[1] + 2)
5553            return [(x - tr[0] + 1) * self.options.graffiti_preview_size / d, self.options.graffiti_preview_size - (y - tr[1] + 1) * self.options.graffiti_preview_size / d]
5554
5555        def draw_graffiti_segment(layer, start, end, feed, color=(0, 255, 0, 40), emmit=1000):
5556            # Emit = dots per second
5557            l = math.sqrt(sum([(start[i] - end[i]) ** 2 for i in range(len(start))]))
5558            time_ = l / feed
5559            c1 = self.graffiti_reference_points[layer][0][0]
5560            c2 = self.graffiti_reference_points[layer][1][0]
5561            d = math.sqrt((c1[0] - c2[0]) ** 2 + (c1[1] - c2[1]) ** 2)
5562            if d == 0:
5563                raise ValueError("Error! Reference points should not be the same!")
5564            for i in range(int(time_ * emmit + 1)):
5565                t = i / (time_ * emmit)
5566                r1 = start[0] * (1 - t) + end[0] * t
5567                r2 = start[1] * (1 - t) + end[1] * t
5568                a = (r1 ** 2 - r2 ** 2 + d ** 2) / (2 * d)
5569                h = math.sqrt(r1 ** 2 - a ** 2)
5570                xa = c1[0] + a * (c2[0] - c1[0]) / d
5571                ya = c1[1] + a * (c2[1] - c1[1]) / d
5572
5573                x1 = xa + h * (c2[1] - c1[1]) / d
5574                x2 = xa - h * (c2[1] - c1[1]) / d
5575                y1 = ya - h * (c2[0] - c1[0]) / d
5576                y2 = ya + h * (c2[0] - c1[0]) / d
5577
5578                x = x1 if y1 < y2 else x2
5579                y = min(y1, y2)
5580                x, y = graffiti_preview_transform(x, y)
5581                graffiti_preview_draw_point(x, y, color)
5582
5583        def create_connector(p1, p2, t1, t2):
5584            P1 = P(p1)
5585            P2 = P(p2)
5586            N1 = P(rotate_ccw(t1))
5587            N2 = P(rotate_ccw(t2))
5588            r = self.options.graffiti_min_radius
5589            C1 = P1 + N1 * r
5590            C2 = P2 + N2 * r
5591            # Get closest possible centers of arcs, also we define that arcs are both ccw or both not.
5592            dc, N1, N2, m = (
5593                (
5594                    (((P2 - N1 * r) - (P1 - N2 * r)).l2(), -N1, -N2, 1)
5595                    if vectors_ccw(t1, t2) else
5596                    (((P2 + N1 * r) - (P1 + N2 * r)).l2(), N1, N2, -1)
5597                )
5598                if vectors_ccw((P1 - C1).to_list(), t1) == vectors_ccw((P2 - C2).to_list(), t2) else
5599                (
5600                    (((P2 + N1 * r) - (P1 - N2 * r)).l2(), N1, -N2, 1)
5601                    if vectors_ccw(t1, t2) else
5602                    (((P2 - N1 * r) - (P1 + N2 * r)).l2(), -N1, N2, 1)
5603                )
5604            )
5605            dc = math.sqrt(dc)
5606            C1 = P1 + N1 * r
5607            C2 = P2 + N2 * r
5608            Dc = C2 - C1
5609
5610            if dc == 0:
5611                # can be joined by one arc
5612                return csp_from_arc(p1, p2, C1.to_list(), r, t1)
5613
5614            cos = Dc.x / dc
5615            sin = Dc.y / dc
5616
5617            p1_end = [C1.x - r * sin * m, C1.y + r * cos * m]
5618            p2_st = [C2.x - r * sin * m, C2.y + r * cos * m]
5619            if point_to_point_d2(p1, p1_end) < 0.0001 and point_to_point_d2(p2, p2_st) < 0.0001:
5620                return [[p1, p1, p1], [p2, p2, p2]]
5621
5622            arc1 = csp_from_arc(p1, p1_end, C1.to_list(), r, t1)
5623            arc2 = csp_from_arc(p2_st, p2, C2.to_list(), r, [cos, sin])
5624            return csp_concat_subpaths(arc1, arc2)
5625
5626        if not self.check_dir():
5627            return
5628        if self.selected_paths == {} and self.options.auto_select_paths:
5629            paths = self.paths
5630            self.error("No paths are selected! Trying to work on all available paths.")
5631        else:
5632            paths = self.selected_paths
5633        self.tool = []
5634        gcode = """(Header)
5635(Generated by gcodetools from Inkscape.)
5636(Using graffiti extension.)
5637(Header end.)"""
5638
5639        minx = float("inf")
5640        miny = float("inf")
5641        maxx = float("-inf")
5642        maxy = float("-inf")
5643        # Get all reference points and path's bounds to make preview
5644
5645        for layer in self.layers:
5646            if layer in paths:
5647                # Set reference points
5648                if layer not in self.graffiti_reference_points:
5649                    reference_points = None
5650                    for i in range(self.layers.index(layer), -1, -1):
5651                        if self.layers[i] in self.graffiti_reference_points:
5652                            reference_points = self.graffiti_reference_points[self.layers[i]]
5653                            self.graffiti_reference_points[layer] = self.graffiti_reference_points[self.layers[i]]
5654                            break
5655                    if reference_points is None:
5656                        self.error('There are no graffiti reference points for layer {}'.format(layer), "error")
5657
5658                # Transform reference points
5659                for i in range(len(self.graffiti_reference_points[layer])):
5660                    self.graffiti_reference_points[layer][i][0] = self.transform(self.graffiti_reference_points[layer][i][0], layer)
5661                    point = self.graffiti_reference_points[layer][i]
5662                    gcode += "(Reference point {:f};{:f} for {} axis)\n".format(point[0][0], point[0][1], point[1])
5663
5664                if self.options.graffiti_create_preview:
5665                    for point in self.graffiti_reference_points[layer]:
5666                        minx = min(minx, point[0][0])
5667                        miny = min(miny, point[0][1])
5668                        maxx = max(maxx, point[0][0])
5669                        maxy = max(maxy, point[0][1])
5670                    for path in paths[layer]:
5671                        csp = path.path.to_superpath()
5672                        csp = self.apply_transforms(path, csp)
5673                        csp = self.transform_csp(csp, layer)
5674                        bounds = csp_simple_bound(csp)
5675                        minx = min(minx, bounds[0])
5676                        miny = min(miny, bounds[1])
5677                        maxx = max(maxx, bounds[2])
5678                        maxy = max(maxy, bounds[3])
5679
5680        if self.options.graffiti_create_preview:
5681            self.graffiti_preview = list([[255] * (4 * self.options.graffiti_preview_size) for _ in range(self.options.graffiti_preview_size)])
5682            self.graffiti_preview_transform = [minx, miny, maxx, maxy]
5683
5684        for layer in self.layers:
5685            if layer in paths:
5686
5687                r = re.match("\\s*\\(\\s*([0-9\\-,.]+)\\s*;\\s*([0-9\\-,.]+)\\s*\\)\\s*", self.options.graffiti_start_pos)
5688                if r:
5689                    start_point = [float(r.group(1)), float(r.group(2))]
5690                else:
5691                    start_point = [0., 0.]
5692                last_sp1 = [[start_point[0], start_point[1] - 10] for _ in range(3)]
5693                last_sp2 = [start_point for _ in range(3)]
5694
5695                self.set_tool(layer)
5696                self.tool = self.tools[layer][0]
5697                # Change tool every layer. (Probably layer = color so it'll be
5698                # better to change it even if the tool has not been changed)
5699                gcode += ("(Change tool to {})\n".format(re.sub("\"'\\(\\)\\\\", " ", self.tool["name"]))) + self.tool["tool change gcode"] + "\n"
5700
5701                subpaths = []
5702                for path in paths[layer]:
5703                    # Rebuild the paths to polyline.
5704                    csp = path.path.to_superpath()
5705                    csp = self.apply_transforms(path, csp)
5706                    csp = self.transform_csp(csp, layer)
5707                    subpaths += csp
5708                polylines = []
5709                while len(subpaths) > 0:
5710                    i = min([(point_to_point_d2(last_sp2[1], subpaths[i][0][1]), i) for i in range(len(subpaths))])[1]
5711                    subpath = subpaths[i][:]
5712                    del subpaths[i]
5713                    polylines += [
5714                        ['connector', create_connector(
5715                                last_sp2[1],
5716                                subpath[0][1],
5717                                csp_normalized_slope(last_sp1, last_sp2, 1.),
5718                                csp_normalized_slope(subpath[0], subpath[1], 0.),
5719                        )]
5720                    ]
5721                    polyline = []
5722                    spl = None
5723
5724                    #  remove zerro length segments
5725                    i = 0
5726                    while i < len(subpath) - 1:
5727                        if cspseglength(subpath[i], subpath[i + 1]) < 0.00000001:
5728                            subpath[i][2] = subpath[i + 1][2]
5729                            del subpath[i + 1]
5730                        else:
5731                            i += 1
5732
5733                    for sp1, sp2 in zip(subpath, subpath[1:]):
5734                        if spl is not None and abs(cross(csp_normalized_slope(spl, sp1, 1.), csp_normalized_slope(sp1, sp2, 0.))) > 0.1:  # TODO add coefficient into inx
5735                            # We've got sharp angle at sp1.
5736                            polyline += [sp1]
5737                            polylines += [['draw', polyline[:]]]
5738                            polylines += [
5739                                ['connector', create_connector(
5740                                        sp1[1],
5741                                        sp1[1],
5742                                        csp_normalized_slope(spl, sp1, 1.),
5743                                        csp_normalized_slope(sp1, sp2, 0.),
5744                                )]
5745                            ]
5746                            polyline = []
5747                        # max_segment_length
5748                        polyline += [sp1]
5749                        print_(polyline)
5750                        print_(sp1)
5751
5752                        spl = sp1
5753                    polyline += [sp2]
5754                    polylines += [['draw', polyline[:]]]
5755
5756                    last_sp1 = sp1
5757                    last_sp2 = sp2
5758
5759                # Add return to start_point
5760                if not polylines:
5761                    continue
5762                polylines += [["connect1", [[polylines[-1][1][-1][1] for _ in range(3)], [start_point for _ in range(3)]]]]
5763
5764                # Make polylines from polylines. They are still csp.
5765                for i in range(len(polylines)):
5766                    polyline = []
5767                    l = 0
5768                    print_("polylines", polylines)
5769                    print_(polylines[i])
5770                    for sp1, sp2 in zip(polylines[i][1], polylines[i][1][1:]):
5771                        print_(sp1, sp2)
5772                        l = cspseglength(sp1, sp2)
5773                        if l > 0.00000001:
5774                            polyline += [sp1[1]]
5775                            parts = int(math.ceil(l / self.options.graffiti_max_seg_length))
5776                            for j in range(1, parts):
5777                                polyline += [csp_at_length(sp1, sp2, float(j) / parts)]
5778                    if l > 0.00000001:
5779                        polyline += [sp2[1]]
5780                    print_(i)
5781                    polylines[i][1] = polyline
5782
5783                t = 0
5784                last_state = None
5785                for polyline_ in polylines:
5786                    polyline = polyline_[1]
5787                    # Draw linearization
5788                    if self.options.graffiti_create_linearization_preview:
5789                        t += 1
5790                        csp = [[polyline[i], polyline[i], polyline[i]] for i in range(len(polyline))]
5791                        draw_csp(self.transform_csp([csp], layer, reverse=True))
5792
5793                    # Export polyline to gcode
5794                    # we are making transform from XYZA coordinates to R1...Rn
5795                    # where R1...Rn are radius vectors from graffiti reference points
5796                    # to current (x,y) point. Also we need to assign custom feed rate
5797                    # for each segment. And we'll use only G01 gcode.
5798                    last_real_pos, g = get_gcode_coordinates(polyline[0], layer)
5799                    last_pos = polyline[0]
5800                    if polyline_[0] == "draw" and last_state != "draw":
5801                        gcode += self.tool['gcode before path'] + "\n"
5802                    for point in polyline:
5803                        real_pos, g = get_gcode_coordinates(point, layer)
5804                        real_l = sum([(real_pos[i] - last_real_pos[i]) ** 2 for i in range(len(last_real_pos))])
5805                        l = (last_pos[0] - point[0]) ** 2 + (last_pos[1] - point[1]) ** 2
5806                        if l != 0:
5807                            feed = self.tool['feed'] * math.sqrt(real_l / l)
5808                            gcode += "G01 " + g + " F {:f}\n".format(feed)
5809                            if self.options.graffiti_create_preview:
5810                                draw_graffiti_segment(layer, real_pos, last_real_pos, feed, color=(0, 0, 255, 200) if polyline_[0] == "draw" else (255, 0, 0, 200), emmit=self.options.graffiti_preview_emmit)
5811                            last_real_pos = real_pos
5812                            last_pos = point[:]
5813                    if polyline_[0] == "draw" and last_state != "draw":
5814                        gcode += self.tool['gcode after path'] + "\n"
5815                    last_state = polyline_[0]
5816        self.export_gcode(gcode, no_headers=True)
5817        if self.options.graffiti_create_preview:
5818            try:
5819                # Draw reference points
5820                for layer in self.graffiti_reference_points:
5821                    for point in self.graffiti_reference_points[layer]:
5822                        x, y = graffiti_preview_transform(point[0][0], point[0][1])
5823                        graffiti_preview_draw_point(x, y, (0, 255, 0, 255), radius=5)
5824
5825                import png
5826                writer = png.Writer(width=self.options.graffiti_preview_size, height=self.options.graffiti_preview_size, size=None, greyscale=False, alpha=True, bitdepth=8, palette=None, transparent=None, background=None, gamma=None, compression=None, interlace=False, bytes_per_sample=None, planes=None, colormap=None, maxval=None, chunk_limit=1048576)
5827                with open(os.path.join(self.options.directory, self.options.file + ".png"), 'wb') as f:
5828                    writer.write(f, self.graffiti_preview)
5829
5830            except:
5831                self.error("Png module have not been found!")
5832
5833    def get_info_plus(self):
5834        """Like get_info(), but checks some of the values"""
5835        self.get_info()
5836        if self.orientation_points == {}:
5837            self.error("Orientation points have not been defined! A default set of orientation points has been automatically added.")
5838            self.tab_orientation(self.layers[min(1, len(self.layers) - 1)])
5839            self.get_info()
5840        if self.tools == {}:
5841            self.error("Cutting tool has not been defined! A default tool has been automatically added.")
5842            self.options.tools_library_type = "default"
5843            self.tab_tools_library(self.layers[min(1, len(self.layers) - 1)])
5844            self.get_info()
5845
5846    ################################################################################
5847    #
5848    # Effect
5849    #
5850    # Main function of Gcodetools class
5851    #
5852    ################################################################################
5853    def effect(self):
5854        start_time = time.time()
5855        global options
5856        options = self.options
5857        options.self = self
5858        options.doc_root = self.document.getroot()
5859
5860        # define print_ function
5861        global print_
5862        if self.options.log_create_log:
5863            try:
5864                if os.path.isfile(self.options.log_filename):
5865                    os.remove(self.options.log_filename)
5866                with open(self.options.log_filename, "a") as fhl:
5867                    fhl.write("""Gcodetools log file.
5868Started at {}.
5869{}
5870""".format(time.strftime("%d.%m.%Y %H:%M:%S"), options.log_filename))
5871            except:
5872                print_ = lambda *x: None
5873        else:
5874            print_ = lambda *x: None
5875
5876        # This automatically calls any `tab_{tab_name_in_inx}` which in this
5877        # extension is A LOT of different functions. So see all method prefixed
5878        # with tab_ to find out what's supported here.
5879        self.options.active_tab()
5880
5881        print_("------------------------------------------")
5882        print_("Done in {:f} seconds".format(time.time() - start_time))
5883        print_("End at {}.".format(time.strftime("%d.%m.%Y %H:%M:%S")))
5884
5885
5886    def tab_offset(self):
5887        self.get_info()
5888        if self.options.offset_just_get_distance:
5889            for layer in self.selected_paths:
5890                if len(self.selected_paths[layer]) == 2:
5891                    csp1 = self.selected_paths[layer][0].path.to_superpath()
5892                    csp2 = self.selected_paths[layer][1].path.to_superpath()
5893                    dist = csp_to_csp_distance(csp1, csp2)
5894                    print_(dist)
5895                    draw_pointer(list(csp_at_t(csp1[dist[1]][dist[2] - 1], csp1[dist[1]][dist[2]], dist[3]))
5896                                 + list(csp_at_t(csp2[dist[4]][dist[5] - 1], csp2[dist[4]][dist[5]], dist[6])), "red", "line", comment=math.sqrt(dist[0]))
5897            return
5898        if self.options.offset_step == 0:
5899            self.options.offset_step = self.options.offset_radius
5900        if self.options.offset_step * self.options.offset_radius < 0:
5901            self.options.offset_step *= -1
5902        time_ = time.time()
5903        offsets_count = 0
5904        for layer in self.selected_paths:
5905            for path in self.selected_paths[layer]:
5906
5907                offset = self.options.offset_step / 2
5908                while abs(offset) <= abs(self.options.offset_radius):
5909                    offset_ = csp_offset(path.path.to_superpath(), offset)
5910                    offsets_count += 1
5911                    if offset_:
5912                        for iii in offset_:
5913                            draw_csp([iii], width=1)
5914                    else:
5915                        print_("------------Reached empty offset at radius {}".format(offset))
5916                        break
5917                    offset += self.options.offset_step
5918        print_()
5919        print_("-----------------------------------------------------------------------------------")
5920        print_("-----------------------------------------------------------------------------------")
5921        print_("-----------------------------------------------------------------------------------")
5922        print_()
5923        print_("Done in {}".format(time.time() - time_))
5924        print_("Total offsets count {}".format(offsets_count))
5925
5926
5927if __name__ == '__main__':
5928    Gcodetools().run()
5929