summaryrefslogtreecommitdiffstats
path: root/share/extensions/other/gcodetools/gcodetools.py
diff options
context:
space:
mode:
Diffstat (limited to '')
-rwxr-xr-xshare/extensions/other/gcodetools/gcodetools.py5930
1 files changed, 5930 insertions, 0 deletions
diff --git a/share/extensions/other/gcodetools/gcodetools.py b/share/extensions/other/gcodetools/gcodetools.py
new file mode 100755
index 0000000..ef8bdd2
--- /dev/null
+++ b/share/extensions/other/gcodetools/gcodetools.py
@@ -0,0 +1,5930 @@
+#!/usr/bin/env python
+# coding=utf-8
+#
+# Copyright (C) 2005 Aaron Spike, aaron@ekips.org (super paths et al)
+# 2007 hugomatic... (gcode.py)
+# 2009 Nick Drobchenko, nick@cnc-club.ru (main developer)
+# 2011 Chris Lusby Taylor, clusbytaylor@enterprise.net (engraving functions)
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+"""
+Comments starting "#LT" or "#CLT" are by Chris Lusby Taylor who rewrote the engraving function in 2011.
+History of CLT changes to engraving and other functions it uses:
+9 May 2011 Changed test of tool diameter to square it
+10 May Note that there are many unused functions, including:
+ bound_to_bound_distance, csp_curvature_radius_at_t,
+ csp_special_points, csplength, rebuild_csp, csp_slope,
+ csp_simple_bound_to_point_distance, csp_bound_to_point_distance,
+ bez_at_t, bez_to_point_distance, bez_normalized_slope, matrix_mul, transpose
+ Fixed csp_point_inside_bound() to work if x outside bounds
+20 May Now encoding the bisectors of angles.
+23 May Using r/cos(a) instead of normalised normals for bisectors of angles.
+23 May Note that Z values generated for engraving are in pixels, not mm.
+ Removed the biarc curves - straight lines are better.
+24 May Changed Bezier slope calculation to be less sensitive to tiny differences in points.
+ Added use of self.options.engraving_newton_iterations to control accuracy
+25 May Big restructure and new recursive function.
+ Changed the way I treat corners - I now find if the centre of a proposed circle is
+ within the area bounded by the line being tested and the two angle bisectors at
+ its ends. See get_radius_to_line().
+29 May Eliminating redundant points. If A,B,C colinear, drop B
+30 May Eliminating redundant lines in divided Beziers. Changed subdivision of lines
+ 7Jun Try to show engraving in 3D
+ 8 Jun Displaying in stereo 3D.
+ Fixed a bug in bisect - it could go wrong due to rounding errors if
+ 1+x1.x2+y1.y2<0 which should never happen. BTW, I spotted a non-normalised normal
+ returned by csp_normalized_normal. Need to check for that.
+ 9 Jun Corrected spelling of 'definition' but still match previous 'defention' and 'defenition' if found in file
+ Changed get_tool to find 1.6.04 tools or new tools with corrected spelling
+10 Jun Put 3D into a separate layer called 3D, created unless it already exists
+ Changed csp_normalized_slope to reject lines shorter than 1e-9.
+10 Jun Changed all dimensions seen by user to be mm/inch, not pixels. This includes
+ tool diameter, maximum engraving distance, tool shape and all Z values.
+12 Jun ver 208 Now scales correctly if orientation points moved or stretched.
+12 Jun ver 209. Now detect if engraving toolshape not a function of radius
+ Graphics now indicate Gcode toolpath, limited by min(tool diameter/2,max-dist)
+24 Jan 2017 Removed hard-coded scale values from orientation point calculation
+TODO Change line division to be recursive, depending on what line is touched. See line_divide
+"""
+
+__version__ = '1.7'
+
+import cmath
+import copy
+import math
+import os
+import re
+import sys
+import time
+from functools import partial
+
+import numpy
+
+import inkex
+from inkex.bezier import bezierlength, bezierparameterize, beziertatlength
+from inkex import Transform, PathElement, TextElement, Tspan, Group, Layer, Marker, CubicSuperPath, Style
+
+if sys.version_info[0] > 2:
+ xrange = range
+ unicode = str
+
+def ireplace(self, old, new, count=0):
+ pattern = re.compile(re.escape(old), re.I)
+ return re.sub(pattern, new, self, count)
+
+
+################################################################################
+#
+# Styles and additional parameters
+#
+################################################################################
+
+TAU = math.pi * 2
+STRAIGHT_TOLERANCE = 0.0001
+STRAIGHT_DISTANCE_TOLERANCE = 0.0001
+ENGRAVING_TOLERANCE = 0.0001
+LOFT_LENGTHS_TOLERANCE = 0.0000001
+
+EMC_TOLERANCE_EQUAL = 0.00001
+
+options = {}
+defaults = {
+ 'header': """%
+(Header)
+(Generated by gcodetools from Inkscape.)
+(Using default header. To add your own header create file "header" in the output dir.)
+M3
+(Header end.)
+""",
+ 'footer': """
+(Footer)
+M5
+G00 X0.0000 Y0.0000
+M2
+(Using default footer. To add your own footer create file "footer" in the output dir.)
+(end)
+%"""
+}
+
+INTERSECTION_RECURSION_DEPTH = 10
+INTERSECTION_TOLERANCE = 0.00001
+
+def marker_style(stroke, marker='DrawCurveMarker', width=1):
+ """Set a marker style with some basic defaults"""
+ return Style(stroke=stroke, fill='none', stroke_width=width,
+ marker_end='url(#{})'.format(marker))
+
+MARKER_STYLE = {
+ "in_out_path_style": marker_style('#0072a7', 'InOutPathMarker'),
+ "loft_style": {
+ 'main curve': marker_style('#88f', 'Arrow2Mend'),
+ },
+ "biarc_style": {
+ 'biarc0': marker_style('#88f'),
+ 'biarc1': marker_style('#8f8'),
+ 'line': marker_style('#f88'),
+ 'area': marker_style('#777', width=0.1),
+ },
+ "biarc_style_dark": {
+ 'biarc0': marker_style('#33a'),
+ 'biarc1': marker_style('#3a3'),
+ 'line': marker_style('#a33'),
+ 'area': marker_style('#222', width=0.3),
+ },
+ "biarc_style_dark_area": {
+ 'biarc0': marker_style('#33a', width=0.1),
+ 'biarc1': marker_style('#3a3', width=0.1),
+ 'line': marker_style('#a33', width=0.1),
+ 'area': marker_style('#222', width=0.3),
+ },
+ "biarc_style_i": {
+ 'biarc0': marker_style('#880'),
+ 'biarc1': marker_style('#808'),
+ 'line': marker_style('#088'),
+ 'area': marker_style('#999', width=0.3),
+ },
+ "biarc_style_dark_i": {
+ 'biarc0': marker_style('#dd5'),
+ 'biarc1': marker_style('#d5d'),
+ 'line': marker_style('#5dd'),
+ 'area': marker_style('#aaa', width=0.3),
+ },
+ "biarc_style_lathe_feed": {
+ 'biarc0': marker_style('#07f', width=0.4),
+ 'biarc1': marker_style('#0f7', width=0.4),
+ 'line': marker_style('#f44', width=0.4),
+ 'area': marker_style('#aaa', width=0.3),
+ },
+ "biarc_style_lathe_passing feed": {
+ 'biarc0': marker_style('#07f', width=0.4),
+ 'biarc1': marker_style('#0f7', width=0.4),
+ 'line': marker_style('#f44', width=0.4),
+ 'area': marker_style('#aaa', width=0.3),
+ },
+ "biarc_style_lathe_fine feed": {
+ 'biarc0': marker_style('#7f0', width=0.4),
+ 'biarc1': marker_style('#f70', width=0.4),
+ 'line': marker_style('#744', width=0.4),
+ 'area': marker_style('#aaa', width=0.3),
+ },
+ "area artefact": Style(stroke='#ff0000', fill='#ffff00', stroke_width=1),
+ "area artefact arrow": Style(stroke='#ff0000', fill='#ffff00', stroke_width=1),
+ "dxf_points": Style(stroke="#ff0000", fill="#ff0000"),
+}
+
+
+################################################################################
+# Gcode additional functions
+################################################################################
+
+def gcode_comment_str(s, replace_new_line=False):
+ if replace_new_line:
+ s = re.sub(r"[\n\r]+", ".", s)
+ res = ""
+ if s[-1] == "\n":
+ s = s[:-1]
+ for a in s.split("\n"):
+ if a != "":
+ res += "(" + re.sub(r"[\(\)\\\n\r]", ".", a) + ")\n"
+ else:
+ res += "\n"
+ return res
+
+
+################################################################################
+# Cubic Super Path additional functions
+################################################################################
+
+
+def csp_from_polyline(line):
+ return [[[point[:] for _ in range(3)] for point in subline] for subline in line]
+
+
+def csp_remove_zero_segments(csp, tolerance=1e-7):
+ res = []
+ for subpath in csp:
+ if len(subpath) > 0:
+ res.append([subpath[0]])
+ for sp1, sp2 in zip(subpath, subpath[1:]):
+ 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:
+ res[-1][-1][2] = sp2[2]
+ else:
+ res[-1].append(sp2)
+ return res
+
+
+def point_inside_csp(p, csp, on_the_path=True):
+ # we'll do the raytracing and see how many intersections are there on the ray's way.
+ # if number of intersections is even then point is outside.
+ # ray will be x=p.x and y=>p.y
+ # you can assign any value to on_the_path, by default if point is on the path
+ # function will return thai it's inside the path.
+ x, y = p
+ ray_intersections_count = 0
+ for subpath in csp:
+
+ for i in range(1, len(subpath)):
+ sp1 = subpath[i - 1]
+ sp2 = subpath[i]
+ ax, ay, bx, by, cx, cy, dx, dy = csp_parameterize(sp1, sp2)
+ if ax == 0 and bx == 0 and cx == 0 and dx == x:
+ # we've got a special case here
+ b = csp_true_bounds([[sp1, sp2]])
+ if b[1][1] <= y <= b[3][1]:
+ # points is on the path
+ return on_the_path
+ else:
+ # we can skip this segment because it won't influence the answer.
+ pass
+ else:
+ for t in csp_line_intersection([x, y], [x, y + 5], sp1, sp2):
+ if t == 0 or t == 1:
+ # we've got another special case here
+ x1, y1 = csp_at_t(sp1, sp2, t)
+ if y1 == y:
+ # the point is on the path
+ return on_the_path
+ # if t == 0 we should have considered this case previously.
+ if t == 1:
+ # we have to check the next segment if it is on the same side of the ray
+ st_d = csp_normalized_slope(sp1, sp2, 1)[0]
+ if st_d == 0:
+ st_d = csp_normalized_slope(sp1, sp2, 0.99)[0]
+
+ for j in range(1, len(subpath) + 1):
+ if (i + j) % len(subpath) == 0:
+ continue # skip the closing segment
+ sp11 = subpath[(i - 1 + j) % len(subpath)]
+ sp22 = subpath[(i + j) % len(subpath)]
+ ax1, ay1, bx1, by1, cx1, cy1, dx1, dy1 = csp_parameterize(sp1, sp2)
+ if ax1 == 0 and bx1 == 0 and cx1 == 0 and dx1 == x:
+ continue # this segment parallel to the ray, so skip it
+ en_d = csp_normalized_slope(sp11, sp22, 0)[0]
+ if en_d == 0:
+ en_d = csp_normalized_slope(sp11, sp22, 0.01)[0]
+ if st_d * en_d <= 0:
+ ray_intersections_count += 1
+ break
+ else:
+ x1, y1 = csp_at_t(sp1, sp2, t)
+ if y1 == y:
+ # the point is on the path
+ return on_the_path
+ else:
+ if y1 > y and 3 * ax * t ** 2 + 2 * bx * t + cx != 0: # if it's 0 the path only touches the ray
+ ray_intersections_count += 1
+ return ray_intersections_count % 2 == 1
+
+
+def csp_close_all_subpaths(csp, tolerance=0.000001):
+ for i in range(len(csp)):
+ if point_to_point_d2(csp[i][0][1], csp[i][-1][1]) > tolerance ** 2:
+ csp[i][-1][2] = csp[i][-1][1][:]
+ csp[i] += [[csp[i][0][1][:] for _ in range(3)]]
+ else:
+ if csp[i][0][1] != csp[i][-1][1]:
+ csp[i][-1][1] = csp[i][0][1][:]
+ return csp
+
+
+def csp_simple_bound(csp):
+ minx = None
+ miny = None
+ maxx = None
+ maxy = None
+
+ for subpath in csp:
+ for sp in subpath:
+ for p in sp:
+ minx = min(minx, p[0]) if minx is not None else p[0]
+ miny = min(miny, p[1]) if miny is not None else p[1]
+ maxx = max(maxx, p[0]) if maxx is not None else p[0]
+ maxy = max(maxy, p[1]) if maxy is not None else p[1]
+ return minx, miny, maxx, maxy
+
+
+def csp_segment_to_bez(sp1, sp2):
+ return sp1[1:] + sp2[:2]
+
+
+def csp_to_point_distance(csp, p, dist_bounds=(0, 1e100)):
+ min_dist = [1e100, 0, 0, 0]
+ for j in range(len(csp)):
+ for i in range(1, len(csp[j])):
+ d = csp_seg_to_point_distance(csp[j][i - 1], csp[j][i], p, sample_points=5)
+ if d[0] < dist_bounds[0]:
+ return [d[0], j, i, d[1]]
+ else:
+ if d[0] < min_dist[0]:
+ min_dist = [d[0], j, i, d[1]]
+ return min_dist
+
+
+def csp_seg_to_point_distance(sp1, sp2, p, sample_points=5):
+ ax, ay, bx, by, cx, cy, dx, dy = csp_parameterize(sp1, sp2)
+ dx = dx - p[0]
+ dy = dy - p[1]
+ if sample_points < 2:
+ sample_points = 2
+ 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.])
+ for k in range(sample_points):
+ t = float(k) / (sample_points - 1)
+ i = 0
+ while i == 0 or abs(f) > 0.000001 and i < 20:
+ t2 = t ** 2
+ t3 = t ** 3
+ 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)
+ 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
+ if df != 0:
+ t = t - f / df
+ else:
+ break
+ i += 1
+ if 0 <= t <= 1:
+ p1 = csp_at_t(sp1, sp2, t)
+ d1 = (p1[0] - p[0]) ** 2 + (p1[1] - p[1]) ** 2
+ if d1 < d[0]:
+ d = [d1, t]
+ return d
+
+
+def csp_seg_to_csp_seg_distance(sp1, sp2, sp3, sp4, dist_bounds=(0, 1e100), sample_points=5, tolerance=.01):
+ # check the ending points first
+ dist = csp_seg_to_point_distance(sp1, sp2, sp3[1], sample_points)
+ dist += [0.]
+ if dist[0] <= dist_bounds[0]:
+ return dist
+ d = csp_seg_to_point_distance(sp1, sp2, sp4[1], sample_points)
+ if d[0] < dist[0]:
+ dist = d + [1.]
+ if dist[0] <= dist_bounds[0]:
+ return dist
+ d = csp_seg_to_point_distance(sp3, sp4, sp1[1], sample_points)
+ if d[0] < dist[0]:
+ dist = [d[0], 0., d[1]]
+ if dist[0] <= dist_bounds[0]:
+ return dist
+ d = csp_seg_to_point_distance(sp3, sp4, sp2[1], sample_points)
+ if d[0] < dist[0]:
+ dist = [d[0], 1., d[1]]
+ if dist[0] <= dist_bounds[0]:
+ return dist
+ sample_points -= 2
+ if sample_points < 1:
+ sample_points = 1
+ ax1, ay1, bx1, by1, cx1, cy1, dx1, dy1 = csp_parameterize(sp1, sp2)
+ ax2, ay2, bx2, by2, cx2, cy2, dx2, dy2 = csp_parameterize(sp3, sp4)
+ # try to find closes points using Newtons method
+ for k in range(sample_points):
+ for j in range(sample_points):
+ t1 = float(k + 1) / (sample_points + 1)
+ t2 = float(j) / (sample_points + 1)
+
+ t12 = t1 * t1
+ t13 = t1 * t1 * t1
+ t22 = t2 * t2
+ t23 = t2 * t2 * t2
+ i = 0
+
+ F1 = [0, 0]
+ F2 = [[0, 0], [0, 0]]
+ F = 1e100
+ x = ax1 * t13 + bx1 * t12 + cx1 * t1 + dx1 - (ax2 * t23 + bx2 * t22 + cx2 * t2 + dx2)
+ y = ay1 * t13 + by1 * t12 + cy1 * t1 + dy1 - (ay2 * t23 + by2 * t22 + cy2 * t2 + dy2)
+ while i < 2 or abs(F - Flast) > tolerance and i < 30:
+ f1x = 3 * ax1 * t12 + 2 * bx1 * t1 + cx1
+ f1y = 3 * ay1 * t12 + 2 * by1 * t1 + cy1
+ f2x = 3 * ax2 * t22 + 2 * bx2 * t2 + cx2
+ f2y = 3 * ay2 * t22 + 2 * by2 * t2 + cy2
+ F1[0] = 2 * f1x * x + 2 * f1y * y
+ F1[1] = -2 * f2x * x - 2 * f2y * y
+ F2[0][0] = 2 * (6 * ax1 * t1 + 2 * bx1) * x + 2 * f1x * f1x + 2 * (6 * ay1 * t1 + 2 * by1) * y + 2 * f1y * f1y
+ F2[0][1] = -2 * f1x * f2x - 2 * f1y * f2y
+ F2[1][0] = -2 * f2x * f1x - 2 * f2y * f1y
+ F2[1][1] = -2 * (6 * ax2 * t2 + 2 * bx2) * x + 2 * f2x * f2x - 2 * (6 * ay2 * t2 + 2 * by2) * y + 2 * f2y * f2y
+ F2 = inv_2x2(F2)
+ if F2 is not None:
+ t1 -= (F2[0][0] * F1[0] + F2[0][1] * F1[1])
+ t2 -= (F2[1][0] * F1[0] + F2[1][1] * F1[1])
+ t12 = t1 * t1
+ t13 = t1 * t1 * t1
+ t22 = t2 * t2
+ t23 = t2 * t2 * t2
+ x = ax1 * t13 + bx1 * t12 + cx1 * t1 + dx1 - (ax2 * t23 + bx2 * t22 + cx2 * t2 + dx2)
+ y = ay1 * t13 + by1 * t12 + cy1 * t1 + dy1 - (ay2 * t23 + by2 * t22 + cy2 * t2 + dy2)
+ Flast = F
+ F = x * x + y * y
+ else:
+ break
+ i += 1
+ if F < dist[0] and 0 <= t1 <= 1 and 0 <= t2 <= 1:
+ dist = [F, t1, t2]
+ if dist[0] <= dist_bounds[0]:
+ return dist
+ return dist
+
+
+def csp_to_csp_distance(csp1, csp2, dist_bounds=(0, 1e100), tolerance=.01):
+ dist = [1e100, 0, 0, 0, 0, 0, 0]
+ for i1 in range(len(csp1)):
+ for j1 in range(1, len(csp1[i1])):
+ for i2 in range(len(csp2)):
+ for j2 in range(1, len(csp2[i2])):
+ 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])
+ if d[0] >= dist_bounds[1]:
+ continue
+ if d[1] < dist_bounds[0]:
+ return [d[1], i1, j1, 1, i2, j2, 1]
+ 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)
+ if d[0] < dist[0]:
+ dist = [d[0], i1, j1, d[1], i2, j2, d[2]]
+ if dist[0] <= dist_bounds[0]:
+ return dist
+ if dist[0] >= dist_bounds[1]:
+ return dist
+ return dist
+
+
+def csp_split(sp1, sp2, t=.5):
+ [x1, y1] = sp1[1]
+ [x2, y2] = sp1[2]
+ [x3, y3] = sp2[0]
+ [x4, y4] = sp2[1]
+ x12 = x1 + (x2 - x1) * t
+ y12 = y1 + (y2 - y1) * t
+ x23 = x2 + (x3 - x2) * t
+ y23 = y2 + (y3 - y2) * t
+ x34 = x3 + (x4 - x3) * t
+ y34 = y3 + (y4 - y3) * t
+ x1223 = x12 + (x23 - x12) * t
+ y1223 = y12 + (y23 - y12) * t
+ x2334 = x23 + (x34 - x23) * t
+ y2334 = y23 + (y34 - y23) * t
+ x = x1223 + (x2334 - x1223) * t
+ y = y1223 + (y2334 - y1223) * t
+ return [sp1[0], sp1[1], [x12, y12]], [[x1223, y1223], [x, y], [x2334, y2334]], [[x34, y34], sp2[1], sp2[2]]
+
+
+def csp_true_bounds(csp):
+ # Finds minx,miny,maxx,maxy of the csp and return their (x,y,i,j,t)
+ minx = [float("inf"), 0, 0, 0]
+ maxx = [float("-inf"), 0, 0, 0]
+ miny = [float("inf"), 0, 0, 0]
+ maxy = [float("-inf"), 0, 0, 0]
+ for i in range(len(csp)):
+ for j in range(1, len(csp[i])):
+ 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]))
+ roots = cubic_solver(0, 3 * ax, 2 * bx, cx) + [0, 1]
+ for root in roots:
+ if type(root) is complex and abs(root.imag) < 1e-10:
+ root = root.real
+ if type(root) is not complex and 0 <= root <= 1:
+ y = ay * (root ** 3) + by * (root ** 2) + cy * root + y0
+ x = ax * (root ** 3) + bx * (root ** 2) + cx * root + x0
+ maxx = max([x, y, i, j, root], maxx)
+ minx = min([x, y, i, j, root], minx)
+
+ roots = cubic_solver(0, 3 * ay, 2 * by, cy) + [0, 1]
+ for root in roots:
+ if type(root) is complex and root.imag == 0:
+ root = root.real
+ if type(root) is not complex and 0 <= root <= 1:
+ y = ay * (root ** 3) + by * (root ** 2) + cy * root + y0
+ x = ax * (root ** 3) + bx * (root ** 2) + cx * root + x0
+ maxy = max([y, x, i, j, root], maxy)
+ miny = min([y, x, i, j, root], miny)
+ maxy[0], maxy[1] = maxy[1], maxy[0]
+ miny[0], miny[1] = miny[1], miny[0]
+
+ return minx, miny, maxx, maxy
+
+
+############################################################################
+# csp_segments_intersection(sp1,sp2,sp3,sp4)
+#
+# Returns array containing all intersections between two segments of cubic
+# super path. Results are [ta,tb], or [ta0, ta1, tb0, tb1, "Overlap"]
+# where ta, tb are values of t for the intersection point.
+############################################################################
+def csp_segments_intersection(sp1, sp2, sp3, sp4):
+ a = csp_segment_to_bez(sp1, sp2)
+ b = csp_segment_to_bez(sp3, sp4)
+
+ def polish_intersection(a, b, ta, tb, tolerance=INTERSECTION_TOLERANCE):
+ ax, ay, bx, by, cx, cy, dx, dy = bezierparameterize(a)
+ ax1, ay1, bx1, by1, cx1, cy1, dx1, dy1 = bezierparameterize(b)
+ i = 0
+ F = [.0, .0]
+ F1 = [[.0, .0], [.0, .0]]
+ while i == 0 or (abs(F[0]) ** 2 + abs(F[1]) ** 2 > tolerance and i < 10):
+ ta3 = ta ** 3
+ ta2 = ta ** 2
+ tb3 = tb ** 3
+ tb2 = tb ** 2
+ F[0] = ax * ta3 + bx * ta2 + cx * ta + dx - ax1 * tb3 - bx1 * tb2 - cx1 * tb - dx1
+ F[1] = ay * ta3 + by * ta2 + cy * ta + dy - ay1 * tb3 - by1 * tb2 - cy1 * tb - dy1
+ F1[0][0] = 3 * ax * ta2 + 2 * bx * ta + cx
+ F1[0][1] = -3 * ax1 * tb2 - 2 * bx1 * tb - cx1
+ F1[1][0] = 3 * ay * ta2 + 2 * by * ta + cy
+ F1[1][1] = -3 * ay1 * tb2 - 2 * by1 * tb - cy1
+ det = F1[0][0] * F1[1][1] - F1[0][1] * F1[1][0]
+ if det != 0:
+ F1 = [[F1[1][1] / det, -F1[0][1] / det], [-F1[1][0] / det, F1[0][0] / det]]
+ ta = ta - (F1[0][0] * F[0] + F1[0][1] * F[1])
+ tb = tb - (F1[1][0] * F[0] + F1[1][1] * F[1])
+ else:
+ break
+ i += 1
+
+ return ta, tb
+
+ def recursion(a, b, ta0, ta1, tb0, tb1, depth_a, depth_b):
+ global bezier_intersection_recursive_result
+ if a == b:
+ bezier_intersection_recursive_result += [[ta0, tb0, ta1, tb1, "Overlap"]]
+ return
+ tam = (ta0 + ta1) / 2
+ tbm = (tb0 + tb1) / 2
+ if depth_a > 0 and depth_b > 0:
+ a1, a2 = bez_split(a, 0.5)
+ b1, b2 = bez_split(b, 0.5)
+ if bez_bounds_intersect(a1, b1):
+ recursion(a1, b1, ta0, tam, tb0, tbm, depth_a - 1, depth_b - 1)
+ if bez_bounds_intersect(a2, b1):
+ recursion(a2, b1, tam, ta1, tb0, tbm, depth_a - 1, depth_b - 1)
+ if bez_bounds_intersect(a1, b2):
+ recursion(a1, b2, ta0, tam, tbm, tb1, depth_a - 1, depth_b - 1)
+ if bez_bounds_intersect(a2, b2):
+ recursion(a2, b2, tam, ta1, tbm, tb1, depth_a - 1, depth_b - 1)
+ elif depth_a > 0:
+ a1, a2 = bez_split(a, 0.5)
+ if bez_bounds_intersect(a1, b):
+ recursion(a1, b, ta0, tam, tb0, tb1, depth_a - 1, depth_b)
+ if bez_bounds_intersect(a2, b):
+ recursion(a2, b, tam, ta1, tb0, tb1, depth_a - 1, depth_b)
+ elif depth_b > 0:
+ b1, b2 = bez_split(b, 0.5)
+ if bez_bounds_intersect(a, b1):
+ recursion(a, b1, ta0, ta1, tb0, tbm, depth_a, depth_b - 1)
+ if bez_bounds_intersect(a, b2):
+ recursion(a, b2, ta0, ta1, tbm, tb1, depth_a, depth_b - 1)
+ else: # Both segments have been subdivided enough. Let's get some intersections :).
+ intersection, t1, t2 = straight_segments_intersection([a[0]] + [a[3]], [b[0]] + [b[3]])
+ if intersection:
+ if intersection == "Overlap":
+ t1 = (max(0, min(1, t1[0])) + max(0, min(1, t1[1]))) / 2
+ t2 = (max(0, min(1, t2[0])) + max(0, min(1, t2[1]))) / 2
+ bezier_intersection_recursive_result += [[ta0 + t1 * (ta1 - ta0), tb0 + t2 * (tb1 - tb0)]]
+
+ global bezier_intersection_recursive_result
+ bezier_intersection_recursive_result = []
+ recursion(a, b, 0., 1., 0., 1., INTERSECTION_RECURSION_DEPTH, INTERSECTION_RECURSION_DEPTH)
+ intersections = bezier_intersection_recursive_result
+ for i in range(len(intersections)):
+ if len(intersections[i]) < 5 or intersections[i][4] != "Overlap":
+ intersections[i] = polish_intersection(a, b, intersections[i][0], intersections[i][1])
+ return intersections
+
+
+def csp_segments_true_intersection(sp1, sp2, sp3, sp4):
+ intersections = csp_segments_intersection(sp1, sp2, sp3, sp4)
+ res = []
+ for intersection in intersections:
+ if (
+ (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))
+ or (0 <= intersection[0] <= 1 and 0 <= intersection[1] <= 1)
+ ):
+ res += [intersection]
+ return res
+
+
+def csp_get_t_at_curvature(sp1, sp2, c, sample_points=16):
+ # returns a list containing [t1,t2,t3,...,tn], 0<=ti<=1...
+ if sample_points < 2:
+ sample_points = 2
+ tolerance = .0000000001
+ res = []
+ ax, ay, bx, by, cx, cy, dx, dy = csp_parameterize(sp1, sp2)
+ for k in range(sample_points):
+ t = float(k) / (sample_points - 1)
+ i = 0
+ F = 1e100
+ while i < 2 or abs(F) > tolerance and i < 17:
+ try: # some numerical calculation could exceed the limits
+ t2 = t * t
+ # slopes...
+ f1x = 3 * ax * t2 + 2 * bx * t + cx
+ f1y = 3 * ay * t2 + 2 * by * t + cy
+ f2x = 6 * ax * t + 2 * bx
+ f2y = 6 * ay * t + 2 * by
+ f3x = 6 * ax
+ f3y = 6 * ay
+ d = (f1x ** 2 + f1y ** 2) ** 1.5
+ F1 = (
+ ((f1x * f3y - f3x * f1y) * d - (f1x * f2y - f2x * f1y) * 3. * (f2x * f1x + f2y * f1y) * ((f1x ** 2 + f1y ** 2) ** .5)) /
+ ((f1x ** 2 + f1y ** 2) ** 3)
+ )
+ F = (f1x * f2y - f1y * f2x) / d - c
+ t -= F / F1
+ except:
+ break
+ i += 1
+ if 0 <= t <= 1 and F <= tolerance:
+ if len(res) == 0:
+ res.append(t)
+ for i in res:
+ if abs(t - i) <= 0.001:
+ break
+ if not abs(t - i) <= 0.001:
+ res.append(t)
+ return res
+
+
+def csp_max_curvature(sp1, sp2):
+ ax, ay, bx, by, cx, cy, dx, dy = csp_parameterize(sp1, sp2)
+ tolerance = .0001
+ F = 0.
+ i = 0
+ while i < 2 or F - Flast < tolerance and i < 10:
+ t = .5
+ f1x = 3 * ax * t ** 2 + 2 * bx * t + cx
+ f1y = 3 * ay * t ** 2 + 2 * by * t + cy
+ f2x = 6 * ax * t + 2 * bx
+ f2y = 6 * ay * t + 2 * by
+ f3x = 6 * ax
+ f3y = 6 * ay
+ d = pow(f1x ** 2 + f1y ** 2, 1.5)
+ if d != 0:
+ Flast = F
+ F = (f1x * f2y - f1y * f2x) / d
+ F1 = (
+ (d * (f1x * f3y - f3x * f1y) - (f1x * f2y - f2x * f1y) * 3. * (f2x * f1x + f2y * f1y) * pow(f1x ** 2 + f1y ** 2, .5)) /
+ (f1x ** 2 + f1y ** 2) ** 3
+ )
+ i += 1
+ if F1 != 0:
+ t -= F / F1
+ else:
+ break
+ else:
+ break
+ return t
+
+
+def csp_curvature_at_t(sp1, sp2, t, depth=3):
+ ax, ay, bx, by, cx, cy, dx, dy = bezierparameterize(csp_segment_to_bez(sp1, sp2))
+
+ # curvature = (x'y''-y'x'') / (x'^2+y'^2)^1.5
+
+ f1x = 3 * ax * t ** 2 + 2 * bx * t + cx
+ f1y = 3 * ay * t ** 2 + 2 * by * t + cy
+ f2x = 6 * ax * t + 2 * bx
+ f2y = 6 * ay * t + 2 * by
+ d = (f1x ** 2 + f1y ** 2) ** 1.5
+ if d != 0:
+ return (f1x * f2y - f1y * f2x) / d
+ else:
+ t1 = f1x * f2y - f1y * f2x
+ if t1 > 0:
+ return 1e100
+ if t1 < 0:
+ return -1e100
+ # Use the Lapitals rule to solve 0/0 problem for 2 times...
+ t1 = 2 * (bx * ay - ax * by) * t + (ay * cx - ax * cy)
+ if t1 > 0:
+ return 1e100
+ if t1 < 0:
+ return -1e100
+ t1 = bx * ay - ax * by
+ if t1 > 0:
+ return 1e100
+ if t1 < 0:
+ return -1e100
+ if depth > 0:
+ # little hack ;^) hope it won't influence anything...
+ return csp_curvature_at_t(sp1, sp2, t * 1.004, depth - 1)
+ return 1e100
+
+
+def csp_subpath_ccw(subpath):
+ # Remove all zero length segments
+ s = 0
+ if (P(subpath[-1][1]) - P(subpath[0][1])).l2() > 1e-10:
+ subpath[-1][2] = subpath[-1][1]
+ subpath[0][0] = subpath[0][1]
+ subpath += [[subpath[0][1], subpath[0][1], subpath[0][1]]]
+ pl = subpath[-1][2]
+ for sp1 in subpath:
+ for p in sp1:
+ s += (p[0] - pl[0]) * (p[1] + pl[1])
+ pl = p
+ return s < 0
+
+
+def csp_at_t(sp1, sp2, t):
+ ax = sp1[1][0]
+ bx = sp1[2][0]
+ cx = sp2[0][0]
+ dx = sp2[1][0]
+
+ ay = sp1[1][1]
+ by = sp1[2][1]
+ cy = sp2[0][1]
+ dy = sp2[1][1]
+
+ x1 = ax + (bx - ax) * t
+ y1 = ay + (by - ay) * t
+
+ x2 = bx + (cx - bx) * t
+ y2 = by + (cy - by) * t
+
+ x3 = cx + (dx - cx) * t
+ y3 = cy + (dy - cy) * t
+
+ x4 = x1 + (x2 - x1) * t
+ y4 = y1 + (y2 - y1) * t
+
+ x5 = x2 + (x3 - x2) * t
+ y5 = y2 + (y3 - y2) * t
+
+ x = x4 + (x5 - x4) * t
+ y = y4 + (y5 - y4) * t
+
+ return [x, y]
+
+
+def csp_at_length(sp1, sp2, l=0.5, tolerance=0.01):
+ bez = (sp1[1][:], sp1[2][:], sp2[0][:], sp2[1][:])
+ t = beziertatlength(bez, l, tolerance)
+ return csp_at_t(sp1, sp2, t)
+
+
+def cspseglength(sp1, sp2, tolerance=0.01):
+ bez = (sp1[1][:], sp1[2][:], sp2[0][:], sp2[1][:])
+ return bezierlength(bez, tolerance)
+
+
+def csp_line_intersection(l1, l2, sp1, sp2):
+ dd = l1[0]
+ cc = l2[0] - l1[0]
+ bb = l1[1]
+ aa = l2[1] - l1[1]
+ if aa == cc == 0:
+ return []
+ if aa:
+ coef1 = cc / aa
+ coef2 = 1
+ else:
+ coef1 = 1
+ coef2 = aa / cc
+ bez = (sp1[1][:], sp1[2][:], sp2[0][:], sp2[1][:])
+ ax, ay, bx, by, cx, cy, x0, y0 = bezierparameterize(bez)
+ a = coef1 * ay - coef2 * ax
+ b = coef1 * by - coef2 * bx
+ c = coef1 * cy - coef2 * cx
+ d = coef1 * (y0 - bb) - coef2 * (x0 - dd)
+ roots = cubic_solver(a, b, c, d)
+ retval = []
+ for i in roots:
+ if type(i) is complex and abs(i.imag) < 1e-7:
+ i = i.real
+ if type(i) is not complex and -1e-10 <= i <= 1. + 1e-10:
+ retval.append(i)
+ return retval
+
+
+def csp_split_by_two_points(sp1, sp2, t1, t2):
+ if t1 > t2:
+ t1, t2 = t2, t1
+ if t1 == t2:
+ sp1, sp2, sp3 = csp_split(sp1, sp2, t1)
+ return [sp1, sp2, sp2, sp3]
+ elif t1 <= 1e-10 and t2 >= 1. - 1e-10:
+ return [sp1, sp1, sp2, sp2]
+ elif t1 <= 1e-10:
+ sp1, sp2, sp3 = csp_split(sp1, sp2, t2)
+ return [sp1, sp1, sp2, sp3]
+ elif t2 >= 1. - 1e-10:
+ sp1, sp2, sp3 = csp_split(sp1, sp2, t1)
+ return [sp1, sp2, sp3, sp3]
+ else:
+ sp1, sp2, sp3 = csp_split(sp1, sp2, t1)
+ sp2, sp3, sp4 = csp_split(sp2, sp3, (t2 - t1) / (1 - t1))
+ return [sp1, sp2, sp3, sp4]
+
+
+def csp_seg_split(sp1, sp2, points):
+ # points is float=t or list [t1, t2, ..., tn]
+ if type(points) is float:
+ points = [points]
+ points.sort()
+ res = [sp1, sp2]
+ last_t = 0
+ for t in points:
+ if 1e-10 < t < 1. - 1e-10:
+ sp3, sp4, sp5 = csp_split(res[-2], res[-1], (t - last_t) / (1 - last_t))
+ last_t = t
+ res[-2:] = [sp3, sp4, sp5]
+ return res
+
+
+def csp_subpath_split_by_points(subpath, points):
+ # points are [[i,t]...] where i-segment's number
+ points.sort()
+ points = [[1, 0.]] + points + [[len(subpath) - 1, 1.]]
+ parts = []
+ for int1, int2 in zip(points, points[1:]):
+ if int1 == int2:
+ continue
+ if int1[1] == 1.:
+ int1[0] += 1
+ int1[1] = 0.
+ if int1 == int2:
+ continue
+ if int2[1] == 0.:
+ int2[0] -= 1
+ int2[1] = 1.
+ if int1[0] == 0 and int2[0] == len(subpath) - 1: # and small(int1[1]) and small(int2[1]-1) :
+ continue
+ if int1[0] == int2[0]: # same segment
+ sp = csp_split_by_two_points(subpath[int1[0] - 1], subpath[int1[0]], int1[1], int2[1])
+ if sp[1] != sp[2]:
+ parts += [[sp[1], sp[2]]]
+ else:
+ sp5, sp1, sp2 = csp_split(subpath[int1[0] - 1], subpath[int1[0]], int1[1])
+ sp3, sp4, sp5 = csp_split(subpath[int2[0] - 1], subpath[int2[0]], int2[1])
+ if int1[0] == int2[0] - 1:
+ parts += [[sp1, [sp2[0], sp2[1], sp3[2]], sp4]]
+ else:
+ parts += [[sp1, sp2] + subpath[int1[0] + 1:int2[0] - 1] + [sp3, sp4]]
+ return parts
+
+
+def arc_from_s_r_n_l(s, r, n, l):
+ if abs(n[0] ** 2 + n[1] ** 2 - 1) > 1e-10:
+ n = normalize(n)
+ return arc_from_c_s_l([s[0] + n[0] * r, s[1] + n[1] * r], s, l)
+
+
+def arc_from_c_s_l(c, s, l):
+ r = point_to_point_d(c, s)
+ if r == 0:
+ return []
+ alpha = l / r
+ cos_ = math.cos(alpha)
+ sin_ = math.sin(alpha)
+ 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_]
+ n = [c[0] - s[0], c[1] - s[1]]
+ slope = rotate_cw(n) if l > 0 else rotate_ccw(n)
+ return csp_from_arc(s, e, c, r, slope)
+
+
+def csp_from_arc(start, end, center, r, slope_st):
+ # Creates csp that approximise specified arc
+ r = abs(r)
+ alpha = (atan2(end[0] - center[0], end[1] - center[1]) - atan2(start[0] - center[0], start[1] - center[1])) % TAU
+
+ sectors = int(abs(alpha) * 2 / math.pi) + 1
+ alpha_start = atan2(start[0] - center[0], start[1] - center[1])
+ cos_ = math.cos(alpha_start)
+ sin_ = math.sin(alpha_start)
+ k = (4. * math.tan(alpha / sectors / 4.) / 3.)
+ if dot(slope_st, [- sin_ * k * r, cos_ * k * r]) < 0:
+ if alpha > 0:
+ alpha -= TAU
+ else:
+ alpha += TAU
+ if abs(alpha * r) < 0.001:
+ return []
+
+ sectors = int(abs(alpha) * 2 / math.pi) + 1
+ k = (4. * math.tan(alpha / sectors / 4.) / 3.)
+ result = []
+ for i in range(sectors + 1):
+ cos_ = math.cos(alpha_start + alpha * i / sectors)
+ sin_ = math.sin(alpha_start + alpha * i / sectors)
+ sp = [[], [center[0] + cos_ * r, center[1] + sin_ * r], []]
+ sp[0] = [sp[1][0] + sin_ * k * r, sp[1][1] - cos_ * k * r]
+ sp[2] = [sp[1][0] - sin_ * k * r, sp[1][1] + cos_ * k * r]
+ result += [sp]
+ result[0][0] = result[0][1][:]
+ result[-1][2] = result[-1][1]
+
+ return result
+
+
+def point_to_arc_distance(p, arc):
+ # Distance calculattion from point to arc
+ P0, P2, c, a = arc
+ p = P(p)
+ r = (P0 - c).mag()
+ if r > 0:
+ i = c + (p - c).unit() * r
+ alpha = ((i - c).angle() - (P0 - c).angle())
+ if a * alpha < 0:
+ if alpha > 0:
+ alpha = alpha - TAU
+ else:
+ alpha = TAU + alpha
+ if between(alpha, 0, a) or min(abs(alpha), abs(alpha - a)) < STRAIGHT_TOLERANCE:
+ return (p - i).mag(), [i.x, i.y]
+ else:
+ d1 = (p - P0).mag()
+ d2 = (p - P2).mag()
+ if d1 < d2:
+ return d1, [P0.x, P0.y]
+ else:
+ return d2, [P2.x, P2.y]
+
+
+def csp_to_arc_distance(sp1, sp2, arc1, arc2, tolerance=0.01): # arc = [start,end,center,alpha]
+ n = 10
+ i = 0
+ d = (0, [0, 0])
+ d1 = (0, [0, 0])
+ dl = 0
+ while i < 1 or (abs(d1[0] - dl[0]) > tolerance and i < 4):
+ i += 1
+ dl = d1 * 1
+ for j in range(n + 1):
+ t = float(j) / n
+ p = csp_at_t(sp1, sp2, t)
+ d = min(point_to_arc_distance(p, arc1), point_to_arc_distance(p, arc2))
+ d1 = max(d1, d)
+ n = n * 2
+ return d1[0]
+
+
+def csp_point_inside_bound(sp1, sp2, p):
+ bez = [sp1[1], sp1[2], sp2[0], sp2[1]]
+ x, y = p
+ c = 0
+ # CLT added test of x in range
+ xmin = 1e100
+ xmax = -1e100
+ for i in range(4):
+ [x0, y0] = bez[i - 1]
+ [x1, y1] = bez[i]
+ xmin = min(xmin, x0)
+ xmax = max(xmax, x0)
+ if x0 - x1 != 0 and (y - y0) * (x1 - x0) >= (x - x0) * (y1 - y0) and x > min(x0, x1) and x <= max(x0, x1):
+ c += 1
+ return xmin <= x <= xmax and c % 2 == 0
+
+
+def line_line_intersect(p1, p2, p3, p4): # Return only true intersection.
+ if (p1[0] == p2[0] and p1[1] == p2[1]) or (p3[0] == p4[0] and p3[1] == p4[1]):
+ return False
+ x = (p2[0] - p1[0]) * (p4[1] - p3[1]) - (p2[1] - p1[1]) * (p4[0] - p3[0])
+ if x == 0: # Lines are parallel
+ if (p3[0] - p1[0]) * (p2[1] - p1[1]) == (p3[1] - p1[1]) * (p2[0] - p1[0]):
+ if p3[0] != p4[0]:
+ t11 = (p1[0] - p3[0]) / (p4[0] - p3[0])
+ t12 = (p2[0] - p3[0]) / (p4[0] - p3[0])
+ t21 = (p3[0] - p1[0]) / (p2[0] - p1[0])
+ t22 = (p4[0] - p1[0]) / (p2[0] - p1[0])
+ else:
+ t11 = (p1[1] - p3[1]) / (p4[1] - p3[1])
+ t12 = (p2[1] - p3[1]) / (p4[1] - p3[1])
+ t21 = (p3[1] - p1[1]) / (p2[1] - p1[1])
+ t22 = (p4[1] - p1[1]) / (p2[1] - p1[1])
+ return "Overlap" if (0 <= t11 <= 1 or 0 <= t12 <= 1) and (0 <= t21 <= 1 or 0 <= t22 <= 1) else False
+ else:
+ return False
+ else:
+ return (
+ 0 <= ((p4[0] - p3[0]) * (p1[1] - p3[1]) - (p4[1] - p3[1]) * (p1[0] - p3[0])) / x <= 1 and
+ 0 <= ((p2[0] - p1[0]) * (p1[1] - p3[1]) - (p2[1] - p1[1]) * (p1[0] - p3[0])) / x <= 1)
+
+
+def line_line_intersection_points(p1, p2, p3, p4): # Return only points [ (x,y) ]
+ if (p1[0] == p2[0] and p1[1] == p2[1]) or (p3[0] == p4[0] and p3[1] == p4[1]):
+ return []
+ x = (p2[0] - p1[0]) * (p4[1] - p3[1]) - (p2[1] - p1[1]) * (p4[0] - p3[0])
+ if x == 0: # Lines are parallel
+ if (p3[0] - p1[0]) * (p2[1] - p1[1]) == (p3[1] - p1[1]) * (p2[0] - p1[0]):
+ if p3[0] != p4[0]:
+ t11 = (p1[0] - p3[0]) / (p4[0] - p3[0])
+ t12 = (p2[0] - p3[0]) / (p4[0] - p3[0])
+ t21 = (p3[0] - p1[0]) / (p2[0] - p1[0])
+ t22 = (p4[0] - p1[0]) / (p2[0] - p1[0])
+ else:
+ t11 = (p1[1] - p3[1]) / (p4[1] - p3[1])
+ t12 = (p2[1] - p3[1]) / (p4[1] - p3[1])
+ t21 = (p3[1] - p1[1]) / (p2[1] - p1[1])
+ t22 = (p4[1] - p1[1]) / (p2[1] - p1[1])
+ res = []
+ if (0 <= t11 <= 1 or 0 <= t12 <= 1) and (0 <= t21 <= 1 or 0 <= t22 <= 1):
+ if 0 <= t11 <= 1:
+ res += [p1]
+ if 0 <= t12 <= 1:
+ res += [p2]
+ if 0 <= t21 <= 1:
+ res += [p3]
+ if 0 <= t22 <= 1:
+ res += [p4]
+ return res
+ else:
+ return []
+ else:
+ t1 = ((p4[0] - p3[0]) * (p1[1] - p3[1]) - (p4[1] - p3[1]) * (p1[0] - p3[0])) / x
+ t2 = ((p2[0] - p1[0]) * (p1[1] - p3[1]) - (p2[1] - p1[1]) * (p1[0] - p3[0])) / x
+ if 0 <= t1 <= 1 and 0 <= t2 <= 1:
+ return [[p1[0] * (1 - t1) + p2[0] * t1, p1[1] * (1 - t1) + p2[1] * t1]]
+ else:
+ return []
+
+
+def point_to_point_d2(a, b):
+ return (a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2
+
+
+def point_to_point_d(a, b):
+ return math.sqrt((a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2)
+
+
+def point_to_line_segment_distance_2(p1, p2, p3):
+ # p1 - point, p2,p3 - line segment
+ # draw_pointer(p1)
+ w0 = [p1[0] - p2[0], p1[1] - p2[1]]
+ v = [p3[0] - p2[0], p3[1] - p2[1]]
+ c1 = w0[0] * v[0] + w0[1] * v[1]
+ if c1 <= 0:
+ return w0[0] * w0[0] + w0[1] * w0[1]
+ c2 = v[0] * v[0] + v[1] * v[1]
+ if c2 <= c1:
+ return (p1[0] - p3[0]) ** 2 + (p1[1] - p3[1]) ** 2
+ return (p1[0] - p2[0] - v[0] * c1 / c2) ** 2 + (p1[1] - p2[1] - v[1] * c1 / c2)
+
+
+def line_to_line_distance_2(p1, p2, p3, p4):
+ if line_line_intersect(p1, p2, p3, p4):
+ return 0
+ return min(
+ point_to_line_segment_distance_2(p1, p3, p4),
+ point_to_line_segment_distance_2(p2, p3, p4),
+ point_to_line_segment_distance_2(p3, p1, p2),
+ point_to_line_segment_distance_2(p4, p1, p2))
+
+
+def csp_seg_bound_to_csp_seg_bound_max_min_distance(sp1, sp2, sp3, sp4):
+ bez1 = csp_segment_to_bez(sp1, sp2)
+ bez2 = csp_segment_to_bez(sp3, sp4)
+ min_dist = 1e100
+ max_dist = 0.
+ for i in range(4):
+ if csp_point_inside_bound(sp1, sp2, bez2[i]) or csp_point_inside_bound(sp3, sp4, bez1[i]):
+ min_dist = 0.
+ break
+ for i in range(4):
+ for j in range(4):
+ d = line_to_line_distance_2(bez1[i - 1], bez1[i], bez2[j - 1], bez2[j])
+ if d < min_dist:
+ min_dist = d
+ d = (bez2[j][0] - bez1[i][0]) ** 2 + (bez2[j][1] - bez1[i][1]) ** 2
+ if max_dist < d:
+ max_dist = d
+ return min_dist, max_dist
+
+
+def csp_reverse(csp):
+ for i in range(len(csp)):
+ n = []
+ for j in csp[i]:
+ n = [[j[2][:], j[1][:], j[0][:]]] + n
+ csp[i] = n[:]
+ return csp
+
+
+def csp_normalized_slope(sp1, sp2, t):
+ ax, ay, bx, by, cx, cy, dx, dy = bezierparameterize((sp1[1][:], sp1[2][:], sp2[0][:], sp2[1][:]))
+ if sp1[1] == sp2[1] == sp1[2] == sp2[0]:
+ return [1., 0.]
+ f1x = 3 * ax * t * t + 2 * bx * t + cx
+ f1y = 3 * ay * t * t + 2 * by * t + cy
+ if abs(f1x * f1x + f1y * f1y) > 1e-9: # LT changed this from 1e-20, which caused problems
+ l = math.sqrt(f1x * f1x + f1y * f1y)
+ return [f1x / l, f1y / l]
+
+ if t == 0:
+ f1x = sp2[0][0] - sp1[1][0]
+ f1y = sp2[0][1] - sp1[1][1]
+ if abs(f1x * f1x + f1y * f1y) > 1e-9: # LT changed this from 1e-20, which caused problems
+ l = math.sqrt(f1x * f1x + f1y * f1y)
+ return [f1x / l, f1y / l]
+ else:
+ f1x = sp2[1][0] - sp1[1][0]
+ f1y = sp2[1][1] - sp1[1][1]
+ if f1x * f1x + f1y * f1y != 0:
+ l = math.sqrt(f1x * f1x + f1y * f1y)
+ return [f1x / l, f1y / l]
+ elif t == 1:
+ f1x = sp2[1][0] - sp1[2][0]
+ f1y = sp2[1][1] - sp1[2][1]
+ if abs(f1x * f1x + f1y * f1y) > 1e-9:
+ l = math.sqrt(f1x * f1x + f1y * f1y)
+ return [f1x / l, f1y / l]
+ else:
+ f1x = sp2[1][0] - sp1[1][0]
+ f1y = sp2[1][1] - sp1[1][1]
+ if f1x * f1x + f1y * f1y != 0:
+ l = math.sqrt(f1x * f1x + f1y * f1y)
+ return [f1x / l, f1y / l]
+ else:
+ return [1., 0.]
+
+
+def csp_normalized_normal(sp1, sp2, t):
+ nx, ny = csp_normalized_slope(sp1, sp2, t)
+ return [-ny, nx]
+
+
+def csp_parameterize(sp1, sp2):
+ return bezierparameterize(csp_segment_to_bez(sp1, sp2))
+
+
+def csp_concat_subpaths(*s):
+ def concat(s1, s2):
+ if not s1:
+ return s2
+ if not s2:
+ return s1
+ if (s1[-1][1][0] - s2[0][1][0]) ** 2 + (s1[-1][1][1] - s2[0][1][1]) ** 2 > 0.00001:
+ return s1[:-1] + [[s1[-1][0], s1[-1][1], s1[-1][1]], [s2[0][1], s2[0][1], s2[0][2]]] + s2[1:]
+ else:
+ return s1[:-1] + [[s1[-1][0], s2[0][1], s2[0][2]]] + s2[1:]
+
+ if len(s) == 0:
+ return []
+ if len(s) == 1:
+ return s[0]
+ result = s[0]
+ for s1 in s[1:]:
+ result = concat(result, s1)
+ return result
+
+
+def csp_subpaths_end_to_start_distance2(s1, s2):
+ return (s1[-1][1][0] - s2[0][1][0]) ** 2 + (s1[-1][1][1] - s2[0][1][1]) ** 2
+
+
+def csp_clip_by_line(csp, l1, l2):
+ result = []
+ for i in range(len(csp)):
+ s = csp[i]
+ intersections = []
+ for j in range(1, len(s)):
+ intersections += [[j, int_] for int_ in csp_line_intersection(l1, l2, s[j - 1], s[j])]
+ splitted_s = csp_subpath_split_by_points(s, intersections)
+ for s in splitted_s[:]:
+ clip = False
+ for p in csp_true_bounds([s]):
+ if (l1[1] - l2[1]) * p[0] + (l2[0] - l1[0]) * p[1] + (l1[0] * l2[1] - l2[0] * l1[1]) < -0.01:
+ clip = True
+ break
+ if clip:
+ splitted_s.remove(s)
+ result += splitted_s
+ return result
+
+
+def csp_subpath_line_to(subpath, points, prepend=False):
+ # Appends subpath with line or polyline.
+ if len(points) > 0:
+ if not prepend:
+ if len(subpath) > 0:
+ subpath[-1][2] = subpath[-1][1][:]
+ if type(points[0]) == type([1, 1]):
+ for p in points:
+ subpath += [[p[:], p[:], p[:]]]
+ else:
+ subpath += [[points, points, points]]
+ else:
+ if len(subpath) > 0:
+ subpath[0][0] = subpath[0][1][:]
+ if type(points[0]) == type([1, 1]):
+ for p in points:
+ subpath = [[p[:], p[:], p[:]]] + subpath
+ else:
+ subpath = [[points, points, points]] + subpath
+ return subpath
+
+
+def csp_join_subpaths(csp):
+ result = csp[:]
+ done_smf = True
+ joined_result = []
+ while done_smf:
+ done_smf = False
+ while len(result) > 0:
+ s1 = result[-1][:]
+ del (result[-1])
+ j = 0
+ joined_smf = False
+ while j < len(joined_result):
+ if csp_subpaths_end_to_start_distance2(joined_result[j], s1) < 0.000001:
+ joined_result[j] = csp_concat_subpaths(joined_result[j], s1)
+ done_smf = True
+ joined_smf = True
+ break
+ if csp_subpaths_end_to_start_distance2(s1, joined_result[j]) < 0.000001:
+ joined_result[j] = csp_concat_subpaths(s1, joined_result[j])
+ done_smf = True
+ joined_smf = True
+ break
+ j += 1
+ if not joined_smf:
+ joined_result += [s1[:]]
+ if done_smf:
+ result = joined_result[:]
+ joined_result = []
+ return joined_result
+
+
+def triangle_cross(a, b, c):
+ return (a[0] - b[0]) * (c[1] - b[1]) - (c[0] - b[0]) * (a[1] - b[1])
+
+
+def csp_segment_convex_hull(sp1, sp2):
+ a = sp1[1][:]
+ b = sp1[2][:]
+ c = sp2[0][:]
+ d = sp2[1][:]
+
+ abc = triangle_cross(a, b, c)
+ abd = triangle_cross(a, b, d)
+ bcd = triangle_cross(b, c, d)
+ cad = triangle_cross(c, a, d)
+ if abc == 0 and abd == 0:
+ return [min(a, b, c, d), max(a, b, c, d)]
+ if abc == 0:
+ return [d, min(a, b, c), max(a, b, c)]
+ if abd == 0:
+ return [c, min(a, b, d), max(a, b, d)]
+ if bcd == 0:
+ return [a, min(b, c, d), max(b, c, d)]
+ if cad == 0:
+ return [b, min(c, a, d), max(c, a, d)]
+
+ m1 = abc * abd > 0
+ m2 = abc * bcd > 0
+ m3 = abc * cad > 0
+
+ if m1 and m2 and m3:
+ return [a, b, c]
+ if m1 and m2 and not m3:
+ return [a, b, c, d]
+ if m1 and not m2 and m3:
+ return [a, b, d, c]
+ if not m1 and m2 and m3:
+ return [a, d, b, c]
+ if m1 and not (m2 and m3):
+ return [a, b, d]
+ if not (m1 and m2) and m3:
+ return [c, a, d]
+ if not (m1 and m3) and m2:
+ return [b, c, d]
+
+ raise ValueError("csp_segment_convex_hull happened which is something that shouldn't happen!")
+
+
+################################################################################
+# Bezier additional functions
+################################################################################
+
+def bez_bounds_intersect(bez1, bez2):
+ return bounds_intersect(bez_bound(bez2), bez_bound(bez1))
+
+
+def bez_bound(bez):
+ return [
+ min(bez[0][0], bez[1][0], bez[2][0], bez[3][0]),
+ min(bez[0][1], bez[1][1], bez[2][1], bez[3][1]),
+ max(bez[0][0], bez[1][0], bez[2][0], bez[3][0]),
+ max(bez[0][1], bez[1][1], bez[2][1], bez[3][1]),
+ ]
+
+
+def bounds_intersect(a, b):
+ return not ((a[0] > b[2]) or (b[0] > a[2]) or (a[1] > b[3]) or (b[1] > a[3]))
+
+
+def tpoint(xy1, xy2, t):
+ (x1, y1) = xy1
+ (x2, y2) = xy2
+ return [x1 + t * (x2 - x1), y1 + t * (y2 - y1)]
+
+
+def bez_split(a, t=0.5):
+ a1 = tpoint(a[0], a[1], t)
+ at = tpoint(a[1], a[2], t)
+ b2 = tpoint(a[2], a[3], t)
+ a2 = tpoint(a1, at, t)
+ b1 = tpoint(b2, at, t)
+ a3 = tpoint(a2, b1, t)
+ return [a[0], a1, a2, a3], [a3, b1, b2, a[3]]
+
+
+################################################################################
+# Some vector functions
+################################################################################
+
+def normalize(xy):
+ (x, y) = xy
+ l = math.sqrt(x ** 2 + y ** 2)
+ if l == 0:
+ return [0., 0.]
+ else:
+ return [x / l, y / l]
+
+
+def cross(a, b):
+ return a[1] * b[0] - a[0] * b[1]
+
+
+def dot(a, b):
+ return a[0] * b[0] + a[1] * b[1]
+
+
+def rotate_ccw(d):
+ return [-d[1], d[0]]
+
+
+def rotate_cw(d):
+ return [d[1], -d[0]]
+
+
+def vectors_ccw(a, b):
+ return a[0] * b[1] - b[0] * a[1] < 0
+
+
+################################################################################
+# Common functions
+################################################################################
+
+def inv_2x2(a): # invert matrix 2x2
+ det = a[0][0] * a[1][1] - a[1][0] * a[0][1]
+ if det == 0:
+ return None
+ return [
+ [a[1][1] / det, -a[0][1] / det],
+ [-a[1][0] / det, a[0][0] / det]
+ ]
+
+
+def small(a):
+ global small_tolerance
+ return abs(a) < small_tolerance
+
+
+def atan2(*arg):
+ if len(arg) == 1 and (type(arg[0]) == type([0., 0.]) or type(arg[0]) == type((0., 0.))):
+ return (math.pi / 2 - math.atan2(arg[0][0], arg[0][1])) % TAU
+ elif len(arg) == 2:
+ return (math.pi / 2 - math.atan2(arg[0], arg[1])) % TAU
+ else:
+ raise ValueError("Bad argumets for atan! ({})".format(*arg))
+
+
+def draw_text(text, x, y, group=None, style=None, font_size=10, gcodetools_tag=None):
+ if style is None:
+ 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;"
+ style += "font-size:{:f}px;".format(font_size)
+ attributes = {'x': str(x), 'y': str(y), 'style': style}
+ if gcodetools_tag is not None:
+ attributes["gcodetools"] = str(gcodetools_tag)
+
+ if group is None:
+ group = options.doc_root
+
+ text_elem = group.add(TextElement(**attributes))
+ text_elem.set("xml:space", "preserve")
+ text = str(text).split("\n")
+ for string in text:
+ span = text_elem.add(Tspan(x=str(x), y=str(y)))
+ span.set('sodipodi:role', 'line')
+ y += font_size
+ span.text = str(string)
+
+
+def draw_csp(csp, stroke="#f00", fill="none", comment="", width=0.354, group=None, style=None):
+ if group is None:
+ group = options.doc_root
+ node = group.add(PathElement())
+
+ node.style = style if style is not None else \
+ {'fill': fill, 'fill-opacity': 1, 'stroke': stroke, 'stroke-width': width}
+
+ node.path = CubicSuperPath(csp)
+
+ if comment != '':
+ node.set('comment', comment)
+
+ return node
+
+
+def 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):
+ size = size / 2
+ if attrib is None:
+ attrib = {}
+ if pointer_type is None:
+ pointer_type = "Pointer"
+ attrib["gcodetools"] = pointer_type
+ if group is None:
+ group = options.self.svg.get_current_layer()
+ if text is not None:
+ if font_size is None:
+ font_size = 7
+ group = group.add(Group(gcodetools=pointer_type + " group"))
+ draw_text(text, x[0] + size * 2.2, x[1] - size, group=group, font_size=font_size)
+ if figure == "line":
+ s = ""
+ for i in range(1, len(x) / 2):
+ s += " {}, {} ".format(x[i * 2], x[i * 2 + 1])
+ attrib.update({"d": "M {},{} L {}".format(x[0], x[1], s), "style": "fill:none;stroke:{};stroke-width:{:f};".format(color, width), "comment": str(comment)})
+ elif figure == "arrow":
+ if fill is None:
+ fill = "#12b3ff"
+ fill_opacity = "0.8"
+ 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")
+ attrib.update({"d": d, "style": "fill:{};stroke:none;fill-opacity:{};".format(fill, fill_opacity), "comment": str(comment)})
+ else:
+ 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)})
+ group.add(PathElement(**attrib))
+
+
+def straight_segments_intersection(a, b, true_intersection=True): # (True intersection means check ta and tb are in [0,1])
+ ax = a[0][0]
+ bx = a[1][0]
+ cx = b[0][0]
+ dx = b[1][0]
+ ay = a[0][1]
+ by = a[1][1]
+ cy = b[0][1]
+ dy = b[1][1]
+ if (ax == bx and ay == by) or (cx == dx and cy == dy):
+ return False, 0, 0
+ if (bx - ax) * (dy - cy) - (by - ay) * (dx - cx) == 0: # Lines are parallel
+ ta = (ax - cx) / (dx - cx) if cx != dx else (ay - cy) / (dy - cy)
+ tb = (bx - cx) / (dx - cx) if cx != dx else (by - cy) / (dy - cy)
+ tc = (cx - ax) / (bx - ax) if ax != bx else (cy - ay) / (by - ay)
+ td = (dx - ax) / (bx - ax) if ax != bx else (dy - ay) / (by - ay)
+ 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)
+ else:
+ ta = ((ay - cy) * (dx - cx) - (ax - cx) * (dy - cy)) / ((bx - ax) * (dy - cy) - (by - ay) * (dx - cx))
+ tb = (ax - cx + ta * (bx - ax)) / (dx - cx) if dx != cx else (ay - cy + ta * (by - ay)) / (dy - cy)
+ return (0 <= ta <= 1 and 0 <= tb <= 1 or not true_intersection), ta, tb
+
+
+def between(c, x, y):
+ return x - STRAIGHT_TOLERANCE <= c <= y + STRAIGHT_TOLERANCE or y - STRAIGHT_TOLERANCE <= c <= x + STRAIGHT_TOLERANCE
+
+
+def cubic_solver_real(a, b, c, d):
+ # returns only real roots of a cubic equation.
+ roots = cubic_solver(a, b, c, d)
+ res = []
+ for root in roots:
+ if type(root) is complex:
+ if -1e-10 < root.imag < 1e-10:
+ res.append(root.real)
+ else:
+ res.append(root)
+ return res
+
+
+def cubic_solver(a, b, c, d):
+ if a != 0:
+ # Monics formula see http://en.wikipedia.org/wiki/Cubic_function#Monic_formula_of_roots
+ a, b, c = (b / a, c / a, d / a)
+ m = 2 * a ** 3 - 9 * a * b + 27 * c
+ k = a ** 2 - 3 * b
+ n = m ** 2 - 4 * k ** 3
+ w1 = -.5 + .5 * cmath.sqrt(3) * 1j
+ w2 = -.5 - .5 * cmath.sqrt(3) * 1j
+ if n >= 0:
+ t = m + math.sqrt(n)
+ m1 = pow(t / 2, 1. / 3) if t >= 0 else -pow(-t / 2, 1. / 3)
+ t = m - math.sqrt(n)
+ n1 = pow(t / 2, 1. / 3) if t >= 0 else -pow(-t / 2, 1. / 3)
+ else:
+ m1 = pow(complex((m + cmath.sqrt(n)) / 2), 1. / 3)
+ n1 = pow(complex((m - cmath.sqrt(n)) / 2), 1. / 3)
+ x1 = -1. / 3 * (a + m1 + n1)
+ x2 = -1. / 3 * (a + w1 * m1 + w2 * n1)
+ x3 = -1. / 3 * (a + w2 * m1 + w1 * n1)
+ return [x1, x2, x3]
+ elif b != 0:
+ det = c ** 2 - 4 * b * d
+ if det > 0:
+ return [(-c + math.sqrt(det)) / (2 * b), (-c - math.sqrt(det)) / (2 * b)]
+ elif d == 0:
+ return [-c / (b * b)]
+ else:
+ return [(-c + cmath.sqrt(det)) / (2 * b), (-c - cmath.sqrt(det)) / (2 * b)]
+ elif c != 0:
+ return [-d / c]
+ else:
+ return []
+
+
+################################################################################
+# print_ prints any arguments into specified log file
+################################################################################
+
+def print_(*arg):
+ with open(options.log_filename, "ab") as f:
+ for s in arg:
+ s = unicode(s).encode('unicode_escape') + b" "
+ f.write(s)
+ f.write(b"\n")
+
+
+################################################################################
+# Point (x,y) operations
+################################################################################
+class P(object):
+ def __init__(self, x, y=None):
+ if not y is None:
+ self.x = float(x)
+ self.y = float(y)
+ else:
+ self.x = float(x[0])
+ self.y = float(x[1])
+
+ def __add__(self, other):
+ return P(self.x + other.x, self.y + other.y)
+
+ def __sub__(self, other):
+ return P(self.x - other.x, self.y - other.y)
+
+ def __neg__(self):
+ return P(-self.x, -self.y)
+
+ def __mul__(self, other):
+ if isinstance(other, P):
+ return self.x * other.x + self.y * other.y
+ return P(self.x * other, self.y * other)
+
+ __rmul__ = __mul__
+
+ def __div__(self, other):
+ return P(self.x / other, self.y / other)
+
+ def __truediv__(self, other):
+ return self.__div__(other)
+
+ def mag(self):
+ return math.hypot(self.x, self.y)
+
+ def unit(self):
+ h_mag = self.mag()
+ if h_mag:
+ return self / h_mag
+ return P(0, 0)
+
+ def dot(self, other):
+ return self.x * other.x + self.y * other.y
+
+ def rot(self, theta):
+ c = math.cos(theta)
+ s = math.sin(theta)
+ return P(self.x * c - self.y * s, self.x * s + self.y * c)
+
+ def angle(self):
+ return math.atan2(self.y, self.x)
+
+ def __repr__(self):
+ return '{:f},{:f}'.format(self.x, self.y)
+
+ def pr(self):
+ return "{:.2f},{:.2f}".format(self.x, self.y)
+
+ def to_list(self):
+ return [self.x, self.y]
+
+ def ccw(self):
+ return P(-self.y, self.x)
+
+ def l2(self):
+ return self.x * self.x + self.y * self.y
+
+
+class Line(object):
+ def __init__(self, st, end):
+ if st.__class__ == P:
+ st = st.to_list()
+ if end.__class__ == P:
+ end = end.to_list()
+ self.st = P(st)
+ self.end = P(end)
+ self.l = self.length()
+ if self.l != 0:
+ self.n = ((self.end - self.st) / self.l).ccw()
+ else:
+ self.n = [0, 1]
+
+ def offset(self, r):
+ self.st -= self.n * r
+ self.end -= self.n * r
+
+ def l2(self):
+ return (self.st - self.end).l2()
+
+ def length(self):
+ return (self.st - self.end).mag()
+
+ def draw(self, group, style, layer, transform, num=0, reverse_angle=1):
+ st = gcodetools.transform(self.st.to_list(), layer, True)
+ end = gcodetools.transform(self.end.to_list(), layer, True)
+
+ attr = {'style': style['line'],
+ 'd': 'M {},{} L {},{}'.format(st[0], st[1], end[0], end[1]),
+ "gcodetools": "Preview",
+ }
+ if transform:
+ attr["transform"] = transform
+ group.add(PathElement(**attr))
+
+ def intersect(self, b):
+ if b.__class__ == Line:
+ if self.l < 10e-8 or b.l < 10e-8:
+ return []
+ v1 = self.end - self.st
+ v2 = b.end - b.st
+ x = v1.x * v2.y - v2.x * v1.y
+ if x == 0:
+ # lines are parallel
+ res = []
+
+ if (self.st.x - b.st.x) * v1.y - (self.st.y - b.st.y) * v1.x == 0:
+ # lines are the same
+ if v1.x != 0:
+ if 0 <= (self.st.x - b.st.x) / v2.x <= 1:
+ res.append(self.st)
+ if 0 <= (self.end.x - b.st.x) / v2.x <= 1:
+ res.append(self.end)
+ if 0 <= (b.st.x - self.st.x) / v1.x <= 1:
+ res.append(b.st)
+ if 0 <= (b.end.x - b.st.x) / v1.x <= 1:
+ res.append(b.end)
+ else:
+ if 0 <= (self.st.y - b.st.y) / v2.y <= 1:
+ res.append(self.st)
+ if 0 <= (self.end.y - b.st.y) / v2.y <= 1:
+ res.append(self.end)
+ if 0 <= (b.st.y - self.st.y) / v1.y <= 1:
+ res.append(b.st)
+ if 0 <= (b.end.y - b.st.y) / v1.y <= 1:
+ res.append(b.end)
+ return res
+ else:
+ t1 = (-v1.x * (b.end.y - self.end.y) + v1.y * (b.end.x - self.end.x)) / x
+ t2 = (-v1.y * (self.st.x - b.st.x) + v1.x * (self.st.y - b.st.y)) / x
+
+ gcodetools.error(str((x, t1, t2)))
+ if 0 <= t1 <= 1 and 0 <= t2 <= 1:
+ return [self.st + v1 * t1]
+ else:
+ return []
+ else:
+ return []
+
+
+################################################################################
+#
+# Offset function
+#
+# This function offsets given cubic super path.
+# It's based on src/livarot/PathOutline.cpp from Inkscape's source code.
+#
+#
+################################################################################
+def csp_offset(csp, r):
+ offset_tolerance = 0.05
+ offset_subdivision_depth = 10
+ time_ = time.time()
+ time_start = time_
+ print_("Offset start at {}".format(time_))
+ print_("Offset radius {}".format(r))
+
+ def csp_offset_segment(sp1, sp2, r):
+ result = []
+ t = csp_get_t_at_curvature(sp1, sp2, 1 / r)
+ if len(t) == 0:
+ t = [0., 1.]
+ t.sort()
+ if t[0] > .00000001:
+ t = [0.] + t
+ if t[-1] < .99999999:
+ t.append(1.)
+ for st, end in zip(t, t[1:]):
+ c = csp_curvature_at_t(sp1, sp2, (st + end) / 2)
+ sp = csp_split_by_two_points(sp1, sp2, st, end)
+ if sp[1] != sp[2]:
+ if c > 1 / r and r < 0 or c < 1 / r and r > 0:
+ offset = offset_segment_recursion(sp[1], sp[2], r, offset_subdivision_depth, offset_tolerance)
+ else: # This part will be clipped for sure... TODO Optimize it...
+ offset = offset_segment_recursion(sp[1], sp[2], r, offset_subdivision_depth, offset_tolerance)
+
+ if not result:
+ result = offset[:]
+ else:
+ if csp_subpaths_end_to_start_distance2(result, offset) < 0.0001:
+ result = csp_concat_subpaths(result, offset)
+ else:
+
+ intersection = csp_get_subapths_last_first_intersection(result, offset)
+ if intersection:
+ i, t1, j, t2 = intersection
+ sp1_, sp2_, sp3_ = csp_split(result[i - 1], result[i], t1)
+ result = result[:i - 1] + [sp1_, sp2_]
+ sp1_, sp2_, sp3_ = csp_split(offset[j - 1], offset[j], t2)
+ result = csp_concat_subpaths(result, [sp2_, sp3_] + offset[j + 1:])
+ else:
+ pass # ???
+ return result
+
+ def create_offset_segment(sp1, sp2, r):
+ # See Gernot Hoffmann "Bezier Curves" p.34 -> 7.1 Bezier Offset Curves
+ p0 = P(sp1[1])
+ p1 = P(sp1[2])
+ p2 = P(sp2[0])
+ p3 = P(sp2[1])
+
+ s0 = p1 - p0
+ s1 = p2 - p1
+ s3 = p3 - p2
+
+ n0 = s0.ccw().unit() if s0.l2() != 0 else P(csp_normalized_normal(sp1, sp2, 0))
+ n3 = s3.ccw().unit() if s3.l2() != 0 else P(csp_normalized_normal(sp1, sp2, 1))
+ n1 = s1.ccw().unit() if s1.l2() != 0 else (n0.unit() + n3.unit()).unit()
+
+ q0 = p0 + r * n0
+ q3 = p3 + r * n3
+ c = csp_curvature_at_t(sp1, sp2, 0)
+ q1 = q0 + (p1 - p0) * (1 - (r * c if abs(c) < 100 else 0))
+ c = csp_curvature_at_t(sp1, sp2, 1)
+ q2 = q3 + (p2 - p3) * (1 - (r * c if abs(c) < 100 else 0))
+
+ return [[q0.to_list(), q0.to_list(), q1.to_list()], [q2.to_list(), q3.to_list(), q3.to_list()]]
+
+ def csp_get_subapths_last_first_intersection(s1, s2):
+ _break = False
+ for i in range(1, len(s1)):
+ sp11 = s1[-i - 1]
+ sp12 = s1[-i]
+ for j in range(1, len(s2)):
+ sp21 = s2[j - 1]
+ sp22 = s2[j]
+ intersection = csp_segments_true_intersection(sp11, sp12, sp21, sp22)
+ if intersection:
+ _break = True
+ break
+ if _break:
+ break
+ if _break:
+ intersection = max(intersection)
+ return [len(s1) - i, intersection[0], j, intersection[1]]
+ else:
+ return []
+
+ def csp_join_offsets(prev, next, sp1, sp2, sp1_l, sp2_l, r):
+ if len(next) > 1:
+ if (P(prev[-1][1]) - P(next[0][1])).l2() < 0.001:
+ return prev, [], next
+ intersection = csp_get_subapths_last_first_intersection(prev, next)
+ if intersection:
+ i, t1, j, t2 = intersection
+ sp1_, sp2_, sp3_ = csp_split(prev[i - 1], prev[i], t1)
+ sp3_, sp4_, sp5_ = csp_split(next[j - 1], next[j], t2)
+ return prev[:i - 1] + [sp1_, sp2_], [], [sp4_, sp5_] + next[j + 1:]
+
+ # Offsets do not intersect... will add an arc...
+ start = (P(csp_at_t(sp1_l, sp2_l, 1.)) + r * P(csp_normalized_normal(sp1_l, sp2_l, 1.))).to_list()
+ end = (P(csp_at_t(sp1, sp2, 0.)) + r * P(csp_normalized_normal(sp1, sp2, 0.))).to_list()
+ arc = csp_from_arc(start, end, sp1[1], r, csp_normalized_slope(sp1_l, sp2_l, 1.))
+ if not arc:
+ return prev, [], next
+ else:
+ # Clip prev by arc
+ if csp_subpaths_end_to_start_distance2(prev, arc) > 0.00001:
+ intersection = csp_get_subapths_last_first_intersection(prev, arc)
+ if intersection:
+ i, t1, j, t2 = intersection
+ sp1_, sp2_, sp3_ = csp_split(prev[i - 1], prev[i], t1)
+ sp3_, sp4_, sp5_ = csp_split(arc[j - 1], arc[j], t2)
+ prev = prev[:i - 1] + [sp1_, sp2_]
+ arc = [sp4_, sp5_] + arc[j + 1:]
+ # Clip next by arc
+ if not next:
+ return prev, [], arc
+ if csp_subpaths_end_to_start_distance2(arc, next) > 0.00001:
+ intersection = csp_get_subapths_last_first_intersection(arc, next)
+ if intersection:
+ i, t1, j, t2 = intersection
+ sp1_, sp2_, sp3_ = csp_split(arc[i - 1], arc[i], t1)
+ sp3_, sp4_, sp5_ = csp_split(next[j - 1], next[j], t2)
+ arc = arc[:i - 1] + [sp1_, sp2_]
+ next = [sp4_, sp5_] + next[j + 1:]
+
+ return prev, arc, next
+
+ def offset_segment_recursion(sp1, sp2, r, depth, tolerance):
+ sp1_r, sp2_r = create_offset_segment(sp1, sp2, r)
+ err = max(
+ 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],
+ 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],
+ 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],
+ )
+
+ if err > tolerance ** 2 and depth > 0:
+ if depth > offset_subdivision_depth - 2:
+ t = csp_max_curvature(sp1, sp2)
+ t = max(.1, min(.9, t))
+ else:
+ t = .5
+ sp3, sp4, sp5 = csp_split(sp1, sp2, t)
+ r1 = offset_segment_recursion(sp3, sp4, r, depth - 1, tolerance)
+ r2 = offset_segment_recursion(sp4, sp5, r, depth - 1, tolerance)
+ return r1[:-1] + [[r1[-1][0], r1[-1][1], r2[0][2]]] + r2[1:]
+ else:
+ return [sp1_r, sp2_r]
+
+ ############################################################################
+ # Some small definitions
+ ############################################################################
+ csp_len = len(csp)
+
+ ############################################################################
+ # Prepare the path
+ ############################################################################
+ # Remove all small segments (segment length < 0.001)
+
+ for i in xrange(len(csp)):
+ for j in xrange(len(csp[i])):
+ sp = csp[i][j]
+ if (P(sp[1]) - P(sp[0])).mag() < 0.001:
+ csp[i][j][0] = sp[1]
+ if (P(sp[2]) - P(sp[0])).mag() < 0.001:
+ csp[i][j][2] = sp[1]
+ for i in xrange(len(csp)):
+ for j in xrange(1, len(csp[i])):
+ if cspseglength(csp[i][j - 1], csp[i][j]) < 0.001:
+ csp[i] = csp[i][:j] + csp[i][j + 1:]
+ if cspseglength(csp[i][-1], csp[i][0]) > 0.001:
+ csp[i][-1][2] = csp[i][-1][1]
+ csp[i] += [[csp[i][0][1], csp[i][0][1], csp[i][0][1]]]
+
+ # TODO Get rid of self intersections.
+
+ original_csp = csp[:]
+ # Clip segments which has curvature>1/r. Because their offset will be self-intersecting and very nasty.
+
+ print_("Offset prepared the path in {}".format(time.time() - time_))
+ print_("Path length = {}".format(sum([len(i) for i in csp])))
+ time_ = time.time()
+
+ ############################################################################
+ # Offset
+ ############################################################################
+ # Create offsets for all segments in the path. And join them together inside each subpath.
+ unclipped_offset = [[] for i in xrange(csp_len)]
+
+ intersection = [[] for i in xrange(csp_len)]
+ for i in xrange(csp_len):
+ subpath = csp[i]
+ subpath_offset = []
+ for sp1, sp2 in zip(subpath, subpath[1:]):
+ segment_offset = csp_offset_segment(sp1, sp2, r)
+ if not subpath_offset:
+ subpath_offset = segment_offset
+
+ prev_l = len(subpath_offset)
+ else:
+ prev, arc, next = csp_join_offsets(subpath_offset[-prev_l:], segment_offset, sp1, sp2, sp1_l, sp2_l, r)
+
+ subpath_offset = csp_concat_subpaths(subpath_offset[:-prev_l + 1], prev, arc, next)
+ prev_l = len(next)
+ sp1_l = sp1[:]
+ sp2_l = sp2[:]
+
+ # Join last and first offsets togother to close the curve
+
+ prev, arc, next = csp_join_offsets(subpath_offset[-prev_l:], subpath_offset[:2], subpath[0], subpath[1], sp1_l, sp2_l, r)
+ subpath_offset[:2] = next[:]
+ subpath_offset = csp_concat_subpaths(subpath_offset[:-prev_l + 1], prev, arc)
+
+ # Collect subpath's offset and save it to unclipped offset list.
+ unclipped_offset[i] = subpath_offset[:]
+
+ print_("Offsetted path in {}".format(time.time() - time_))
+ time_ = time.time()
+
+ ############################################################################
+ # Now to the clipping.
+ ############################################################################
+ # First of all find all intersection's between all segments of all offset subpaths, including self intersections.
+
+ # TODO define offset tolerance here
+ global small_tolerance
+ small_tolerance = 0.01
+ summ = 0
+ summ1 = 0
+ for subpath_i in xrange(csp_len):
+ for subpath_j in xrange(subpath_i, csp_len):
+ subpath = unclipped_offset[subpath_i]
+ subpath1 = unclipped_offset[subpath_j]
+ for i in xrange(1, len(subpath)):
+ # If subpath_i==subpath_j we are looking for self intersections, so
+ # we'll need search intersections only for xrange(i,len(subpath1))
+ for j in (xrange(i, len(subpath1)) if subpath_i == subpath_j else xrange(len(subpath1))):
+ if subpath_i == subpath_j and j == i:
+ # Find self intersections of a segment
+ sp1, sp2, sp3 = csp_split(subpath[i - 1], subpath[i], .5)
+ intersections = csp_segments_intersection(sp1, sp2, sp2, sp3)
+ summ += 1
+ for t in intersections:
+ summ1 += 1
+ if not (small(t[0] - 1) and small(t[1])) and 0 <= t[0] <= 1 and 0 <= t[1] <= 1:
+ intersection[subpath_i] += [[i, t[0] / 2], [j, t[1] / 2 + .5]]
+ else:
+ intersections = csp_segments_intersection(subpath[i - 1], subpath[i], subpath1[j - 1], subpath1[j])
+ summ += 1
+ for t in intersections:
+ summ1 += 1
+ # TODO tolerance dependence to cpsp_length(t)
+ if len(t) == 2 and 0 <= t[0] <= 1 and 0 <= t[1] <= 1 and not (
+ subpath_i == subpath_j and (
+ (j - i - 1) % (len(subpath) - 1) == 0 and small(t[0] - 1) and small(t[1]) or
+ (i - j - 1) % (len(subpath) - 1) == 0 and small(t[1] - 1) and small(t[0]))):
+ intersection[subpath_i] += [[i, t[0]]]
+ intersection[subpath_j] += [[j, t[1]]]
+
+ elif len(t) == 5 and t[4] == "Overlap":
+ intersection[subpath_i] += [[i, t[0]], [i, t[1]]]
+ intersection[subpath_j] += [[j, t[1]], [j, t[3]]]
+
+ print_("Intersections found in {}".format(time.time() - time_))
+ print_("Examined {} segments".format(summ))
+ print_("found {} intersections".format(summ1))
+ time_ = time.time()
+
+ ########################################################################
+ # Split unclipped offset by intersection points into splitted_offset
+ ########################################################################
+ splitted_offset = []
+ for i in xrange(csp_len):
+ subpath = unclipped_offset[i]
+ if len(intersection[i]) > 0:
+ parts = csp_subpath_split_by_points(subpath, intersection[i])
+ # Close parts list to close path (The first and the last parts are joined together)
+ if [1, 0.] not in intersection[i]:
+ parts[0][0][0] = parts[-1][-1][0]
+ parts[0] = csp_concat_subpaths(parts[-1], parts[0])
+ splitted_offset += parts[:-1]
+ else:
+ splitted_offset += parts[:]
+ else:
+ splitted_offset += [subpath[:]]
+
+ print_("Split in {}".format(time.time() - time_))
+ time_ = time.time()
+
+ ########################################################################
+ # Clipping
+ ########################################################################
+ result = []
+ for subpath_i in range(len(splitted_offset)):
+ clip = False
+ s1 = splitted_offset[subpath_i]
+ for subpath_j in range(len(splitted_offset)):
+ s2 = splitted_offset[subpath_j]
+ if (P(s1[0][1]) - P(s2[-1][1])).l2() < 0.0001 and ((subpath_i + 1) % len(splitted_offset) != subpath_j):
+ if dot(csp_normalized_normal(s2[-2], s2[-1], 1.), csp_normalized_slope(s1[0], s1[1], 0.)) * r < -0.0001:
+ clip = True
+ break
+ if (P(s2[0][1]) - P(s1[-1][1])).l2() < 0.0001 and ((subpath_j + 1) % len(splitted_offset) != subpath_i):
+ if dot(csp_normalized_normal(s2[0], s2[1], 0.), csp_normalized_slope(s1[-2], s1[-1], 1.)) * r > 0.0001:
+ clip = True
+ break
+
+ if not clip:
+ result += [s1[:]]
+ elif options.offset_draw_clippend_path:
+ draw_csp([s1], width=.1)
+ draw_pointer(csp_at_t(s2[-2], s2[-1], 1.) +
+ (P(csp_at_t(s2[-2], s2[-1], 1.)) + P(csp_normalized_normal(s2[-2], s2[-1], 1.)) * 10).to_list(), "Green", "line")
+ draw_pointer(csp_at_t(s1[0], s1[1], 0.) +
+ (P(csp_at_t(s1[0], s1[1], 0.)) + P(csp_normalized_slope(s1[0], s1[1], 0.)) * 10).to_list(), "Red", "line")
+
+ # Now join all together and check closure and orientation of result
+ joined_result = csp_join_subpaths(result)
+ # Check if each subpath from joined_result is closed
+
+ for s in joined_result[:]:
+ if csp_subpaths_end_to_start_distance2(s, s) > 0.001:
+ # Remove open parts
+ if options.offset_draw_clippend_path:
+ draw_csp([s], width=1)
+ draw_pointer(s[0][1], comment=csp_subpaths_end_to_start_distance2(s, s))
+ draw_pointer(s[-1][1], comment=csp_subpaths_end_to_start_distance2(s, s))
+ joined_result.remove(s)
+ else:
+ # Remove small parts
+ minx, miny, maxx, maxy = csp_true_bounds([s])
+ if (minx[0] - maxx[0]) ** 2 + (miny[1] - maxy[1]) ** 2 < 0.1:
+ joined_result.remove(s)
+ print_("Clipped and joined path in {}".format(time.time() - time_))
+
+ ########################################################################
+ # Now to the Dummy clipping: remove parts from split offset if their
+ # centers are closer to the original path than offset radius.
+ ########################################################################
+
+ if abs(r * .01) < 1:
+ r1 = (0.99 * r) ** 2
+ r2 = (1.01 * r) ** 2
+ else:
+ r1 = (abs(r) - 1) ** 2
+ r2 = (abs(r) + 1) ** 2
+
+ for s in joined_result[:]:
+ dist = csp_to_point_distance(original_csp, s[int(len(s) / 2)][1], dist_bounds=[r1, r2])
+ if not r1 < dist[0] < r2:
+ joined_result.remove(s)
+ if options.offset_draw_clippend_path:
+ draw_csp([s], comment=math.sqrt(dist[0]))
+ 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])
+
+ print_("-----------------------------")
+ print_("Total offset time {}".format(time.time() - time_start))
+ print_()
+ return joined_result
+
+
+################################################################################
+#
+# Biarc function
+#
+# Calculates biarc approximation of cubic super path segment
+# splits segment if needed or approximates it with straight line
+#
+################################################################################
+def biarc(sp1, sp2, z1, z2, depth=0):
+ def biarc_split(sp1, sp2, z1, z2, depth):
+ if depth < options.biarc_max_split_depth:
+ sp1, sp2, sp3 = csp_split(sp1, sp2)
+ l1 = cspseglength(sp1, sp2)
+ l2 = cspseglength(sp2, sp3)
+ if l1 + l2 == 0:
+ zm = z1
+ else:
+ zm = z1 + (z2 - z1) * l1 / (l1 + l2)
+ return biarc(sp1, sp2, z1, zm, depth + 1) + biarc(sp2, sp3, zm, z2, depth + 1)
+ else:
+ return [[sp1[1], 'line', 0, 0, sp2[1], [z1, z2]]]
+
+ P0 = P(sp1[1])
+ P4 = P(sp2[1])
+ TS = (P(sp1[2]) - P0)
+ TE = -(P(sp2[0]) - P4)
+ v = P0 - P4
+ tsa = TS.angle()
+ tea = TE.angle()
+ va = v.angle()
+ if TE.mag() < STRAIGHT_DISTANCE_TOLERANCE and TS.mag() < STRAIGHT_DISTANCE_TOLERANCE:
+ # Both tangents are zero - line straight
+ return [[sp1[1], 'line', 0, 0, sp2[1], [z1, z2]]]
+ if TE.mag() < STRAIGHT_DISTANCE_TOLERANCE:
+ TE = -(TS + v).unit()
+ r = TS.mag() / v.mag() * 2
+ elif TS.mag() < STRAIGHT_DISTANCE_TOLERANCE:
+ TS = -(TE + v).unit()
+ r = 1 / (TE.mag() / v.mag() * 2)
+ else:
+ r = TS.mag() / TE.mag()
+ TS = TS.unit()
+ TE = TE.unit()
+ tang_are_parallel = ((tsa - tea) % math.pi < STRAIGHT_TOLERANCE or math.pi - (tsa - tea) % math.pi < STRAIGHT_TOLERANCE)
+ if (tang_are_parallel and
+ ((v.mag() < STRAIGHT_DISTANCE_TOLERANCE or TE.mag() < STRAIGHT_DISTANCE_TOLERANCE or TS.mag() < STRAIGHT_DISTANCE_TOLERANCE) or
+ 1 - abs(TS * v / (TS.mag() * v.mag())) < STRAIGHT_TOLERANCE)):
+ # Both tangents are parallel and start and end are the same - line straight
+ # or one of tangents still smaller then tolerance
+
+ # Both tangents and v are parallel - line straight
+ return [[sp1[1], 'line', 0, 0, sp2[1], [z1, z2]]]
+
+ c = v * v
+ b = 2 * v * (r * TS + TE)
+ a = 2 * r * (TS * TE - 1)
+ if v.mag() == 0:
+ return biarc_split(sp1, sp2, z1, z2, depth)
+ asmall = abs(a) < 10 ** -10
+ bsmall = abs(b) < 10 ** -10
+ csmall = abs(c) < 10 ** -10
+ if asmall and b != 0:
+ beta = -c / b
+ elif csmall and a != 0:
+ beta = -b / a
+ elif not asmall:
+ discr = b * b - 4 * a * c
+ if discr < 0:
+ raise ValueError(a, b, c, discr)
+ disq = discr ** .5
+ beta1 = (-b - disq) / 2 / a
+ beta2 = (-b + disq) / 2 / a
+ if beta1 * beta2 > 0:
+ raise ValueError(a, b, c, disq, beta1, beta2)
+ beta = max(beta1, beta2)
+ elif asmall and bsmall:
+ return biarc_split(sp1, sp2, z1, z2, depth)
+ alpha = beta * r
+ ab = alpha + beta
+ P1 = P0 + alpha * TS
+ P3 = P4 - beta * TE
+ P2 = (beta / ab) * P1 + (alpha / ab) * P3
+
+ def calculate_arc_params(P0, P1, P2):
+ D = (P0 + P2) / 2
+ if (D - P1).mag() == 0:
+ return None, None
+ R = D - ((D - P0).mag() ** 2 / (D - P1).mag()) * (P1 - D).unit()
+ p0a = (P0 - R).angle() % (2 * math.pi)
+ p1a = (P1 - R).angle() % (2 * math.pi)
+ p2a = (P2 - R).angle() % (2 * math.pi)
+ alpha = (p2a - p0a) % (2 * math.pi)
+ if (p0a < p2a and (p1a < p0a or p2a < p1a)) or (p2a < p1a < p0a):
+ alpha = -2 * math.pi + alpha
+ if abs(R.x) > 1000000 or abs(R.y) > 1000000 or (R - P0).mag() < options.min_arc_radius ** 2:
+ return None, None
+ else:
+ return R, alpha
+
+ R1, a1 = calculate_arc_params(P0, P1, P2)
+ R2, a2 = calculate_arc_params(P2, P3, P4)
+ if R1 is None or R2 is None or (R1 - P0).mag() < STRAIGHT_TOLERANCE or (R2 - P2).mag() < STRAIGHT_TOLERANCE:
+ return [[sp1[1], 'line', 0, 0, sp2[1], [z1, z2]]]
+
+ d = csp_to_arc_distance(sp1, sp2, [P0, P2, R1, a1], [P2, P4, R2, a2])
+ if d > options.biarc_tolerance and depth < options.biarc_max_split_depth:
+ return biarc_split(sp1, sp2, z1, z2, depth)
+ else:
+ if R2.mag() * a2 == 0:
+ zm = z2
+ else:
+ zm = z1 + (z2 - z1) * (abs(R1.mag() * a1)) / (abs(R2.mag() * a2) + abs(R1.mag() * a1))
+
+ l = (P0 - P2).l2()
+ if l < EMC_TOLERANCE_EQUAL ** 2 or l < EMC_TOLERANCE_EQUAL ** 2 * R1.l2() / 100:
+ # arc should be straight otherwise it could be treated as full circle
+ arc1 = [sp1[1], 'line', 0, 0, [P2.x, P2.y], [z1, zm]]
+ else:
+ arc1 = [sp1[1], 'arc', [R1.x, R1.y], a1, [P2.x, P2.y], [z1, zm]]
+
+ l = (P4 - P2).l2()
+ if l < EMC_TOLERANCE_EQUAL ** 2 or l < EMC_TOLERANCE_EQUAL ** 2 * R2.l2() / 100:
+ # arc should be straight otherwise it could be treated as full circle
+ arc2 = [[P2.x, P2.y], 'line', 0, 0, [P4.x, P4.y], [zm, z2]]
+ else:
+ arc2 = [[P2.x, P2.y], 'arc', [R2.x, R2.y], a2, [P4.x, P4.y], [zm, z2]]
+
+ return [arc1, arc2]
+
+
+class Postprocessor(object):
+ def __init__(self, error_function_handler):
+ self.error = error_function_handler
+ self.functions = {
+ "remap": self.remap,
+ "remapi": self.remapi,
+ "scale": self.scale,
+ "move": self.move,
+ "flip": self.flip_axis,
+ "flip_axis": self.flip_axis,
+ "round": self.round_coordinates,
+ "parameterize": self.parameterize,
+ "regex": self.re_sub_on_gcode_lines
+ }
+
+ def process(self, command):
+ command = re.sub(r"\\\\", ":#:#:slash:#:#:", command)
+ command = re.sub(r"\\;", ":#:#:semicolon:#:#:", command)
+ command = command.split(";")
+ for s in command:
+ s = re.sub(":#:#:slash:#:#:", "\\\\", s)
+ s = re.sub(":#:#:semicolon:#:#:", "\\;", s)
+ s = s.strip()
+ if s != "":
+ self.parse_command(s)
+
+ def parse_command(self, command):
+ r = re.match(r"([A-Za-z0-9_]+)\s*\(\s*(.*)\)", command)
+ if not r:
+ self.error("Parse error while postprocessing.\n(Command: '{}')".format(command), "error")
+ function = r.group(1).lower()
+ parameters = r.group(2)
+ if function in self.functions:
+ print_("Postprocessor: executing function {}({})".format(function, parameters))
+ self.functions[function](parameters)
+ else:
+ self.error("Unrecognized function '{}' while postprocessing.\n(Command: '{}')".format(function, command), "error")
+
+ def re_sub_on_gcode_lines(self, parameters):
+ gcode = self.gcode.split("\n")
+ self.gcode = ""
+ try:
+ for line in gcode:
+ self.gcode += eval("re.sub({},line)".format(parameters)) + "\n"
+
+ except Exception as ex:
+ self.error("Bad parameters for regexp. "
+ "They should be as re.sub pattern and replacement parameters! "
+ "For example: r\"G0(\\d)\", r\"G\\1\" \n"
+ "(Parameters: '{}')\n {}".format(parameters, ex), "error")
+
+ def remapi(self, parameters):
+ self.remap(parameters, case_sensitive=True)
+
+ def remap(self, parameters, case_sensitive=False):
+ # remap parameters should be like "x->y,y->x"
+ parameters = parameters.replace("\\,", ":#:#:coma:#:#:")
+ parameters = parameters.split(",")
+ pattern = []
+ remap = []
+ for s in parameters:
+ s = s.replace(":#:#:coma:#:#:", "\\,")
+ r = re.match("""\\s*(\'|\")(.*)\\1\\s*->\\s*(\'|\")(.*)\\3\\s*""", s)
+ if not r:
+ self.error("Bad parameters for remap.\n(Parameters: '{}')".format(parameters), "error")
+ pattern += [r.group(2)]
+ remap += [r.group(4)]
+
+ for i in range(len(pattern)):
+ if case_sensitive:
+ self.gcode = ireplace(self.gcode, pattern[i], ":#:#:remap_pattern{}:#:#:".format(i))
+ else:
+ self.gcode = self.gcode.replace(pattern[i], ":#:#:remap_pattern{}:#:#:".format(i))
+
+ for i in range(len(remap)):
+ self.gcode = self.gcode.replace(":#:#:remap_pattern{}:#:#:".format(i), remap[i])
+
+ def transform(self, move, scale):
+ axis = ["xi", "yj", "zk", "a"]
+ flip = scale[0] * scale[1] * scale[2] < 0
+ gcode = ""
+ warned = []
+ r_scale = scale[0]
+ plane = "g17"
+ for s in self.gcode.split("\n"):
+ # get plane selection:
+ s_wo_comments = re.sub(r"\([^\)]*\)", "", s)
+ r = re.search(r"(?i)(G17|G18|G19)", s_wo_comments)
+ if r:
+ plane = r.group(1).lower()
+ if plane == "g17":
+ r_scale = scale[0] # plane XY -> scale x
+ if plane == "g18":
+ r_scale = scale[0] # plane XZ -> scale x
+ if plane == "g19":
+ r_scale = scale[1] # plane YZ -> scale y
+ # Raise warning if scale factors are not the game for G02 and G03
+ if plane not in warned:
+ r = re.search(r"(?i)(G02|G03)", s_wo_comments)
+ if r:
+ if plane == "g17" and scale[0] != scale[1]:
+ self.error("Post-processor: Scale factors for X and Y axis are not the same. G02 and G03 codes will be corrupted.")
+ if plane == "g18" and scale[0] != scale[2]:
+ self.error("Post-processor: Scale factors for X and Z axis are not the same. G02 and G03 codes will be corrupted.")
+ if plane == "g19" and scale[1] != scale[2]:
+ self.error("Post-processor: Scale factors for Y and Z axis are not the same. G02 and G03 codes will be corrupted.")
+ warned += [plane]
+ # Transform
+ for i in range(len(axis)):
+ if move[i] != 0 or scale[i] != 1:
+ for a in axis[i]:
+ r = re.search(r"(?i)(" + a + r")\s*(-?)\s*(\d*\.?\d*)", s)
+ if r and r.group(3) != "":
+ 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)
+ # scale radius R
+ if r_scale != 1:
+ r = re.search(r"(?i)(r)\s*(-?\s*(\d*\.?\d*))", s)
+ if r and r.group(3) != "":
+ try:
+ s = re.sub(r"(?i)(r)\s*(-?)\s*(\d*\.?\d*)", r"\1 {:f}".format(float(r.group(2) + r.group(3)) * r_scale), s)
+ except:
+ pass
+
+ gcode += s + "\n"
+
+ self.gcode = gcode
+ if flip:
+ self.remapi("'G02'->'G03', 'G03'->'G02'")
+
+ def parameterize(self, parameters):
+ planes = []
+ feeds = {}
+ coords = []
+ gcode = ""
+ coords_def = {"x": "x", "y": "y", "z": "z", "i": "x", "j": "y", "k": "z", "a": "a"}
+ for s in self.gcode.split("\n"):
+ s_wo_comments = re.sub(r"\([^\)]*\)", "", s)
+ # get Planes
+ r = re.search(r"(?i)(G17|G18|G19)", s_wo_comments)
+ if r:
+ plane = r.group(1).lower()
+ if plane not in planes:
+ planes += [plane]
+ # get Feeds
+ r = re.search(r"(?i)(F)\s*(-?)\s*(\d*\.?\d*)", s_wo_comments)
+ if r:
+ feed = float(r.group(2) + r.group(3))
+ if feed not in feeds:
+ feeds[feed] = "#" + str(len(feeds) + 20)
+
+ # Coordinates
+ for c in "xyzijka":
+ r = re.search(r"(?i)(" + c + r")\s*(-?)\s*(\d*\.?\d*)", s_wo_comments)
+ if r:
+ c = coords_def[r.group(1).lower()]
+ if c not in coords:
+ coords += [c]
+ # Add offset parametrization
+ offset = {"x": "#6", "y": "#7", "z": "#8", "a": "#9"}
+ for c in coords:
+ gcode += "{} = 0 ({} axis offset)\n".format(offset[c], c.upper())
+
+ # Add scale parametrization
+ if not planes:
+ planes = ["g17"]
+ if len(planes) > 1: # have G02 and G03 in several planes scale_x = scale_y = scale_z required
+ gcode += "#10 = 1 (Scale factor)\n"
+ scale = {"x": "#10", "i": "#10", "y": "#10", "j": "#10", "z": "#10", "k": "#10", "r": "#10"}
+ else:
+ gcode += "#10 = 1 ({} Scale factor)\n".format({"g17": "XY", "g18": "XZ", "g19": "YZ"}[planes[0]])
+ gcode += "#11 = 1 ({} Scale factor)\n".format({"g17": "Z", "g18": "Y", "g19": "X"}[planes[0]])
+ scale = {"x": "#10", "i": "#10", "y": "#10", "j": "#10", "z": "#10", "k": "#10", "r": "#10"}
+ if "g17" in planes:
+ scale["z"] = "#11"
+ scale["k"] = "#11"
+ if "g18" in planes:
+ scale["y"] = "#11"
+ scale["j"] = "#11"
+ if "g19" in planes:
+ scale["x"] = "#11"
+ scale["i"] = "#11"
+ # Add a scale
+ if "a" in coords:
+ gcode += "#12 = 1 (A axis scale)\n"
+ scale["a"] = "#12"
+
+ # Add feed parametrization
+ for f in feeds:
+ gcode += "{} = {:f} (Feed definition)\n".format(feeds[f], f)
+
+ # Parameterize Gcode
+ for s in self.gcode.split("\n"):
+ # feed replace :
+ r = re.search(r"(?i)(F)\s*(-?)\s*(\d*\.?\d*)", s)
+ if r and len(r.group(3)) > 0:
+ s = re.sub(r"(?i)(F)\s*(-?)\s*(\d*\.?\d*)", "F [{}]".format(feeds[float(r.group(2) + r.group(3))]), s)
+ # Coords XYZA replace
+ for c in "xyza":
+ r = re.search(r"(?i)((" + c + r")\s*(-?)\s*(\d*\.?\d*))", s)
+ if r and len(r.group(4)) > 0:
+ s = re.sub(r"(?i)(" + c + r")\s*((-?)\s*(\d*\.?\d*))", r"\1[\2*{}+{}]".format(scale[c], offset[c]), s)
+
+ # Coords IJKR replace
+ for c in "ijkr":
+ r = re.search(r"(?i)((" + c + r")\s*(-?)\s*(\d*\.?\d*))", s)
+ if r and len(r.group(4)) > 0:
+ s = re.sub(r"(?i)(" + c + r")\s*((-?)\s*(\d*\.?\d*))", r"\1[\2*{}]".format(scale[c]), s)
+
+ gcode += s + "\n"
+
+ self.gcode = gcode
+
+ def round_coordinates(self, parameters):
+ try:
+ round_ = int(parameters)
+ except:
+ self.error("Bad parameters for round. Round should be an integer! \n(Parameters: '{}')".format(parameters), "error")
+ gcode = ""
+ for s in self.gcode.split("\n"):
+ for a in "xyzijkaf":
+ r = re.search(r"(?i)(" + a + r")\s*(-?\s*(\d*\.?\d*))", s)
+ if r:
+
+ if r.group(2) != "":
+ s = re.sub(
+ r"(?i)(" + a + r")\s*(-?)\s*(\d*\.?\d*)",
+ (r"\1 %0." + str(round_) + "f" if round_ > 0 else r"\1 %d") % round(float(r.group(2)), round_),
+ s)
+ gcode += s + "\n"
+ self.gcode = gcode
+
+ def scale(self, parameters):
+ parameters = parameters.split(",")
+ scale = [1., 1., 1., 1.]
+ try:
+ for i in range(len(parameters)):
+ if float(parameters[i]) == 0:
+ self.error("Bad parameters for scale. Scale should not be 0 at any axis! \n(Parameters: '{}')".format(parameters), "error")
+ scale[i] = float(parameters[i])
+ except:
+ self.error("Bad parameters for scale.\n(Parameters: '{}')".format(parameters), "error")
+ self.transform([0, 0, 0, 0], scale)
+
+ def move(self, parameters):
+ parameters = parameters.split(",")
+ move = [0., 0., 0., 0.]
+ try:
+ for i in range(len(parameters)):
+ move[i] = float(parameters[i])
+ except:
+ self.error("Bad parameters for move.\n(Parameters: '{}')".format(parameters), "error")
+ self.transform(move, [1., 1., 1., 1.])
+
+ def flip_axis(self, parameters):
+ parameters = parameters.lower()
+ axis = {"x": 1., "y": 1., "z": 1., "a": 1.}
+ for p in parameters:
+ if p in [",", " ", " ", "\r", "'", '"']:
+ continue
+ if p not in ["x", "y", "z", "a"]:
+ self.error("Bad parameters for flip_axis. Parameter should be string consists of 'xyza' \n(Parameters: '{}')".format(parameters), "error")
+ axis[p] = -axis[p]
+ self.scale("{:f},{:f},{:f},{:f}".format(axis["x"], axis["y"], axis["z"], axis["a"]))
+
+
+################################################################################
+# Polygon class
+################################################################################
+class Polygon(object):
+ def __init__(self, polygon=None):
+ self.polygon = [] if polygon is None else polygon[:]
+
+ def move(self, x, y):
+ for i in range(len(self.polygon)):
+ for j in range(len(self.polygon[i])):
+ self.polygon[i][j][0] += x
+ self.polygon[i][j][1] += y
+
+ def bounds(self):
+ minx = 1e400
+ miny = 1e400
+ maxx = -1e400
+ maxy = -1e400
+ for poly in self.polygon:
+ for p in poly:
+ if minx > p[0]:
+ minx = p[0]
+ if miny > p[1]:
+ miny = p[1]
+ if maxx < p[0]:
+ maxx = p[0]
+ if maxy < p[1]:
+ maxy = p[1]
+ return minx * 1, miny * 1, maxx * 1, maxy * 1
+
+ def width(self):
+ b = self.bounds()
+ return b[2] - b[0]
+
+ def rotate_(self, sin, cos):
+ self.polygon = [
+ [
+ [point[0] * cos - point[1] * sin, point[0] * sin + point[1] * cos] for point in subpoly
+ ]
+ for subpoly in self.polygon
+ ]
+
+ def rotate(self, a):
+ cos = math.cos(a)
+ sin = math.sin(a)
+ self.rotate_(sin, cos)
+
+ def drop_into_direction(self, direction, surface):
+ # Polygon is a list of simple polygons
+ # Surface is a polygon + line y = 0
+ # Direction is [dx,dy]
+ if len(self.polygon) == 0 or len(self.polygon[0]) == 0:
+ return
+ if direction[0] ** 2 + direction[1] ** 2 < 1e-10:
+ return
+ direction = normalize(direction)
+ sin = direction[0]
+ cos = -direction[1]
+ self.rotate_(-sin, cos)
+ surface.rotate_(-sin, cos)
+ self.drop_down(surface, zerro_plane=False)
+ self.rotate_(sin, cos)
+ surface.rotate_(sin, cos)
+
+ def centroid(self):
+ centroids = []
+ sa = 0
+ for poly in self.polygon:
+ cx = 0
+ cy = 0
+ a = 0
+ for i in range(len(poly)):
+ [x1, y1] = poly[i - 1]
+ [x2, y2] = poly[i]
+ cx += (x1 + x2) * (x1 * y2 - x2 * y1)
+ cy += (y1 + y2) * (x1 * y2 - x2 * y1)
+ a += (x1 * y2 - x2 * y1)
+ a *= 3.
+ if abs(a) > 0:
+ cx /= a
+ cy /= a
+ sa += abs(a)
+ centroids += [[cx, cy, a]]
+ if sa == 0:
+ return [0., 0.]
+ cx = 0
+ cy = 0
+ for c in centroids:
+ cx += c[0] * c[2]
+ cy += c[1] * c[2]
+ cx /= sa
+ cy /= sa
+ return [cx, cy]
+
+ def drop_down(self, surface, zerro_plane=True):
+ # Polygon is a list of simple polygons
+ # Surface is a polygon + line y = 0
+ # Down means min y (0,-1)
+ if len(self.polygon) == 0 or len(self.polygon[0]) == 0:
+ return
+ # Get surface top point
+ top = surface.bounds()[3]
+ if zerro_plane:
+ top = max(0, top)
+ # Get polygon bottom point
+ bottom = self.bounds()[1]
+ self.move(0, top - bottom + 10)
+ # Now get shortest distance from surface to polygon in positive x=0 direction
+ # Such distance = min(distance(vertex, edge)...) where edge from surface and
+ # vertex from polygon and vice versa...
+ dist = 1e300
+ for poly in surface.polygon:
+ for i in range(len(poly)):
+ for poly1 in self.polygon:
+ for i1 in range(len(poly1)):
+ st = poly[i - 1]
+ end = poly[i]
+ vertex = poly1[i1]
+ if st[0] <= vertex[0] <= end[0] or end[0] <= vertex[0] <= st[0]:
+ if st[0] == end[0]:
+ d = min(vertex[1] - st[1], vertex[1] - end[1])
+ else:
+ d = vertex[1] - st[1] - (end[1] - st[1]) * (vertex[0] - st[0]) / (end[0] - st[0])
+ if dist > d:
+ dist = d
+ # and vice versa just change the sign because vertex now under the edge
+ st = poly1[i1 - 1]
+ end = poly1[i1]
+ vertex = poly[i]
+ if st[0] <= vertex[0] <= end[0] or end[0] <= vertex[0] <= st[0]:
+ if st[0] == end[0]:
+ d = min(- vertex[1] + st[1], -vertex[1] + end[1])
+ else:
+ d = - vertex[1] + st[1] + (end[1] - st[1]) * (vertex[0] - st[0]) / (end[0] - st[0])
+ if dist > d:
+ dist = d
+
+ if zerro_plane and dist > 10 + top:
+ dist = 10 + top
+ self.move(0, -dist)
+
+ def draw(self, color="#075", width=.1, group=None):
+ csp = [csp_subpath_line_to([], poly + [poly[0]]) for poly in self.polygon]
+ draw_csp(csp, width=width, group=group)
+
+ def add(self, add):
+ if type(add) == type([]):
+ self.polygon += add[:]
+ else:
+ self.polygon += add.polygon[:]
+
+ def point_inside(self, p):
+ inside = False
+ for poly in self.polygon:
+ for i in range(len(poly)):
+ st = poly[i - 1]
+ end = poly[i]
+ if p == st or p == end:
+ return True # point is a vertex = point is on the edge
+ if st[0] > end[0]:
+ st, end = end, st # This will be needed to check that edge if open only at right end
+ c = (p[1] - st[1]) * (end[0] - st[0]) - (end[1] - st[1]) * (p[0] - st[0])
+ if st[0] <= p[0] < end[0]:
+ if c < 0:
+ inside = not inside
+ elif c == 0:
+ return True # point is on the edge
+ 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
+ return True
+ return inside
+
+ def hull(self):
+ # Add vertices at all self intersection points.
+ hull = []
+ for i1 in range(len(self.polygon)):
+ poly1 = self.polygon[i1]
+ poly_ = []
+ for j1 in range(len(poly1)):
+ s = poly1[j1 - 1]
+ e = poly1[j1]
+ poly_ += [s]
+
+ # Check self intersections
+ for j2 in range(j1 + 1, len(poly1)):
+ s1 = poly1[j2 - 1]
+ e1 = poly1[j2]
+ int_ = line_line_intersection_points(s, e, s1, e1)
+ for p in int_:
+ if point_to_point_d2(p, s) > 0.000001 and point_to_point_d2(p, e) > 0.000001:
+ poly_ += [p]
+ # Check self intersections with other polys
+ for i2 in range(len(self.polygon)):
+ if i1 == i2:
+ continue
+ poly2 = self.polygon[i2]
+ for j2 in range(len(poly2)):
+ s1 = poly2[j2 - 1]
+ e1 = poly2[j2]
+ int_ = line_line_intersection_points(s, e, s1, e1)
+ for p in int_:
+ if point_to_point_d2(p, s) > 0.000001 and point_to_point_d2(p, e) > 0.000001:
+ poly_ += [p]
+ hull += [poly_]
+ # Create the dictionary containing all edges in both directions
+ edges = {}
+ for poly in self.polygon:
+ for i in range(len(poly)):
+ s = tuple(poly[i - 1])
+ e = tuple(poly[i])
+ if point_to_point_d2(e, s) < 0.000001:
+ continue
+ break_s = False
+ break_e = False
+ for p in edges:
+ if point_to_point_d2(p, s) < 0.000001:
+ break_s = True
+ s = p
+ if point_to_point_d2(p, e) < 0.000001:
+ break_e = True
+ e = p
+ if break_s and break_e:
+ break
+ l = point_to_point_d(s, e)
+ if not break_s and not break_e:
+ edges[s] = [[s, e, l]]
+ edges[e] = [[e, s, l]]
+ else:
+ if e in edges:
+ for edge in edges[e]:
+ if point_to_point_d2(edge[1], s) < 0.000001:
+ break
+ if point_to_point_d2(edge[1], s) > 0.000001:
+ edges[e] += [[e, s, l]]
+ else:
+ edges[e] = [[e, s, l]]
+ if s in edges:
+ for edge in edges[s]:
+ if point_to_point_d2(edge[1], e) < 0.000001:
+ break
+ if point_to_point_d2(edge[1], e) > 0.000001:
+ edges[s] += [[s, e, l]]
+ else:
+ edges[s] = [[s, e, l]]
+
+ def angle_quadrant(sin, cos):
+ # 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
+ if sin > 0 and cos >= 0:
+ return 1
+ if sin >= 0 and cos < 0:
+ return 2
+ if sin < 0 and cos <= 0:
+ return 3
+ if sin <= 0 and cos > 0:
+ return 4
+
+ def angle_is_less(sin, cos, sin1, cos1):
+ # 0 = 2*pi is the largest angle
+ if [sin1, cos1] == [0, 1]:
+ return True
+ if [sin, cos] == [0, 1]:
+ return False
+ if angle_quadrant(sin, cos) > angle_quadrant(sin1, cos1):
+ return False
+ if angle_quadrant(sin, cos) < angle_quadrant(sin1, cos1):
+ return True
+ if sin >= 0 and cos > 0:
+ return sin < sin1
+ if sin > 0 and cos <= 0:
+ return sin > sin1
+ if sin <= 0 and cos < 0:
+ return sin > sin1
+ if sin < 0 and cos >= 0:
+ return sin < sin1
+
+ def get_closes_edge_by_angle(edges, last):
+ # Last edge is normalized vector of the last edge.
+ min_angle = [0, 1]
+ next = last
+ last_edge = [(last[0][0] - last[1][0]) / last[2], (last[0][1] - last[1][1]) / last[2]]
+ for p in edges:
+
+ cur = [(p[1][0] - p[0][0]) / p[2], (p[1][1] - p[0][1]) / p[2]]
+ cos = dot(cur, last_edge)
+ sin = cross(cur, last_edge)
+
+ if angle_is_less(sin, cos, min_angle[0], min_angle[1]):
+ min_angle = [sin, cos]
+ next = p
+
+ return next
+
+ # Join edges together into new polygon cutting the vertexes inside new polygon
+ self.polygon = []
+ len_edges = sum([len(edges[p]) for p in edges])
+ loops = 0
+
+ while len(edges) > 0:
+ poly = []
+ if loops > len_edges:
+ raise ValueError("Hull error")
+ loops += 1
+ # Find left most vertex.
+ start = (1e100, 1)
+ for edge in edges:
+ start = min(start, min(edges[edge]))
+ last = [(start[0][0] - 1, start[0][1]), start[0], 1]
+ first_run = True
+ loops1 = 0
+ while last[1] != start[0] or first_run:
+ first_run = False
+ if loops1 > len_edges:
+ raise ValueError("Hull error")
+ loops1 += 1
+ next = get_closes_edge_by_angle(edges[last[1]], last)
+
+ last = next
+ poly += [list(last[0])]
+ self.polygon += [poly]
+ # Remove all edges that are intersects new poly (any vertex inside new poly)
+ poly_ = Polygon([poly])
+ for p in edges.keys()[:]:
+ if poly_.point_inside(list(p)):
+ del edges[p]
+ self.draw(color="Green", width=1)
+
+
+################################################################################
+#
+# Gcodetools class
+#
+################################################################################
+
+class Gcodetools(inkex.EffectExtension):
+ multi_inx = True # XXX Remove this after refactoring
+
+ def export_gcode(self, gcode, no_headers=False):
+ if self.options.postprocessor != "" or self.options.postprocessor_custom != "":
+ postprocessor = Postprocessor(self.error)
+ postprocessor.gcode = gcode
+ if self.options.postprocessor != "":
+ postprocessor.process(self.options.postprocessor)
+ if self.options.postprocessor_custom != "":
+ postprocessor.process(self.options.postprocessor_custom)
+
+ if not no_headers:
+ postprocessor.gcode = self.header + postprocessor.gcode + self.footer
+
+ with open(os.path.join(self.options.directory, self.options.file), "w") as f:
+ f.write(postprocessor.gcode)
+
+ ################################################################################
+ # In/out paths:
+ # TODO move it to the bottom
+ ################################################################################
+ def tab_plasma_prepare_path(self):
+ self.get_info_plus()
+
+ def add_arc(sp1, sp2, end=False, l=10., r=10.):
+ if not end:
+ n = csp_normalized_normal(sp1, sp2, 0.)
+ return csp_reverse([arc_from_s_r_n_l(sp1[1], r, n, -l)])[0]
+ else:
+ n = csp_normalized_normal(sp1, sp2, 1.)
+ return arc_from_s_r_n_l(sp2[1], r, n, l)
+
+ def add_normal(sp1, sp2, end=False, l=10., r=10.):
+ # r is needed only for be compatible with add_arc
+ if not end:
+ n = csp_normalized_normal(sp1, sp2, 0.)
+ p = [n[0] * l + sp1[1][0], n[1] * l + sp1[1][1]]
+ return csp_subpath_line_to([], [p, sp1[1]])
+ else:
+ n = csp_normalized_normal(sp1, sp2, 1.)
+ p = [n[0] * l + sp2[1][0], n[1] * l + sp2[1][1]]
+ return csp_subpath_line_to([], [sp2[1], p])
+
+ def add_tangent(sp1, sp2, end=False, l=10., r=10.):
+ # r is needed only for be compatible with add_arc
+ if not end:
+ n = csp_normalized_slope(sp1, sp2, 0.)
+ p = [-n[0] * l + sp1[1][0], -n[1] * l + sp1[1][1]]
+ return csp_subpath_line_to([], [p, sp1[1]])
+ else:
+ n = csp_normalized_slope(sp1, sp2, 1.)
+ p = [n[0] * l + sp2[1][0], n[1] * l + sp2[1][1]]
+ return csp_subpath_line_to([], [sp2[1], p])
+
+ 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:
+ 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!")
+ return
+
+ # Add in-out-reference point if there is no one yet.
+ if ((len(self.in_out_reference_points) == 0 and self.options.in_out_path
+ or not self.options.in_out_path and not self.options.plasma_prepare_corners)
+ and not self.options.in_out_path_do_not_add_reference_point):
+ self.options.orientation_points_count = "in-out reference point"
+ #self.orientation()
+
+ if self.options.in_out_path or self.options.plasma_prepare_corners:
+ self.set_markers()
+ add_func = {"Round": add_arc, "Perpendicular": add_normal, "Tangent": add_tangent}[self.options.in_out_path_type]
+ 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:
+ self.error("In-out len is to big for in-out radius will cropp it to be r*3/2*pi!")
+
+ if self.selected_paths == {} and self.options.auto_select_paths:
+ self.selected_paths = self.paths
+ self.error("No paths are selected! Trying to work on all available paths.")
+
+ if self.selected_paths == {}:
+ self.error("Nothing is selected. Please select something.")
+ a = self.options.plasma_prepare_corners_tolerance
+ corner_tolerance = cross([1., 0.], [math.cos(a), math.sin(a)])
+
+ for layer in self.layers:
+ if layer in self.selected_paths:
+ max_dist = self.transform_scalar(self.options.in_out_path_point_max_dist, layer, reverse=True)
+ l = self.transform_scalar(self.options.in_out_path_len, layer, reverse=True)
+ plasma_l = self.transform_scalar(self.options.plasma_prepare_corners_distance, layer, reverse=True)
+ r = self.transform_scalar(self.options.in_out_path_radius, layer, reverse=True)
+ l = min(l, r * 3 / 2 * math.pi)
+
+ for path in self.selected_paths[layer]:
+ csp = self.apply_transforms(path, path.path.to_superpath())
+ csp = csp_remove_zero_segments(csp)
+ res = []
+
+ for subpath in csp:
+ # Find closes point to in-out reference point
+ # If subpath is open skip this step
+ if self.options.in_out_path:
+ # split and reverse path for further add in-out points
+ if point_to_point_d2(subpath[0][1], subpath[-1][1]) < 1.e-10:
+ d = [1e100, 1, 1, 1.]
+ for p in self.in_out_reference_points:
+ d1 = csp_to_point_distance([subpath], p, dist_bounds=[0, max_dist])
+ if d1[0] < d[0]:
+ d = d1[:]
+ p_ = p
+ if d[0] < max_dist ** 2:
+ # Lets find is there any angles near this point to put in-out path in
+ # the angle if it's possible
+ # remove last node to make iterations easier
+ subpath[0][0] = subpath[-1][0]
+ del subpath[-1]
+ max_cross = [-1e100, None]
+ for j in range(len(subpath)):
+ sp1 = subpath[j - 2]
+ sp2 = subpath[j - 1]
+ sp3 = subpath[j]
+ if point_to_point_d2(sp2[1], p_) < max_dist ** 2:
+ s1 = csp_normalized_slope(sp1, sp2, 1.)
+ s2 = csp_normalized_slope(sp2, sp3, 0.)
+ max_cross = max(max_cross, [cross(s1, s2), j - 1])
+ # return back last point
+ subpath.append(subpath[0])
+ if max_cross[1] is not None and max_cross[0] > corner_tolerance:
+ # there's an angle near the point
+ j = max_cross[1]
+ if j < 0:
+ j -= 1
+ if j != 0:
+ subpath = csp_concat_subpaths(subpath[j:], subpath[:j + 1])
+ else:
+ # have to cut path's segment
+ d, i, j, t = d
+ sp1, sp2, sp3 = csp_split(subpath[j - 1], subpath[j], t)
+ subpath = csp_concat_subpaths([sp2, sp3], subpath[j:], subpath[:j], [sp1, sp2])
+
+ if self.options.plasma_prepare_corners:
+ # prepare corners
+ # find corners and add some nodes
+ # corner at path's start/end is ignored
+ res_ = [subpath[0]]
+ for sp2, sp3 in zip(subpath[1:], subpath[2:]):
+ sp1 = res_[-1]
+ s1 = csp_normalized_slope(sp1, sp2, 1.)
+ s2 = csp_normalized_slope(sp2, sp3, 0.)
+ if cross(s1, s2) > corner_tolerance:
+ # got a corner to process
+ S1 = P(s1)
+ S2 = P(s2)
+ N = (S1 - S2).unit() * plasma_l
+ SP2 = P(sp2[1])
+ P1 = (SP2 + N)
+ res_ += [
+ [sp2[0], sp2[1], (SP2 + S1 * plasma_l).to_list()],
+ [(P1 - N.ccw() / 2).to_list(), P1.to_list(), (P1 + N.ccw() / 2).to_list()],
+ [(SP2 - S2 * plasma_l).to_list(), sp2[1], sp2[2]]
+ ]
+ else:
+ res_ += [sp2]
+ res_ += [sp3]
+ subpath = res_
+ if self.options.in_out_path:
+ # finally add let's add in-out paths...
+ subpath = csp_concat_subpaths(
+ add_func(subpath[0], subpath[1], False, l, r),
+ subpath,
+ add_func(subpath[-2], subpath[-1], True, l, r)
+ )
+
+ res += [subpath]
+
+ if self.options.in_out_path_replace_original_path:
+ path.path = CubicSuperPath(self.apply_transforms(path, res, True))
+ else:
+ draw_csp(res, width=1, style=MARKER_STYLE["in_out_path_style"])
+
+ def add_arguments(self, pars):
+ add_argument = pars.add_argument
+ add_argument("-d", "--directory", default="/home/", help="Directory for gcode file")
+ add_argument("-f", "--filename", dest="file", default="-1.0", help="File name")
+ add_argument("--add-numeric-suffix-to-filename", type=inkex.Boolean, default=True, help="Add numeric suffix to filename")
+ add_argument("--Zscale", type=float, default="1.0", help="Scale factor Z")
+ add_argument("--Zoffset", type=float, default="0.0", help="Offset along Z")
+ add_argument("-s", "--Zsafe", type=float, default="0.5", help="Z above all obstacles")
+ add_argument("-z", "--Zsurface", type=float, default="0.0", help="Z of the surface")
+ add_argument("-c", "--Zdepth", type=float, default="-0.125", help="Z depth of cut")
+ add_argument("--Zstep", type=float, default="-0.125", help="Z step of cutting")
+ add_argument("-p", "--feed", type=float, default="4.0", help="Feed rate in unit/min")
+
+ add_argument("--biarc-tolerance", type=float, default="1", help="Tolerance used when calculating biarc interpolation.")
+ add_argument("--biarc-max-split-depth", type=int, default="4", help="Defines maximum depth of splitting while approximating using biarcs.")
+ add_argument("--path-to-gcode-order", default="path by path", help="Defines cutting order path by path or layer by layer.")
+ add_argument("--path-to-gcode-depth-function", default="zd", help="Path to gcode depth function.")
+ add_argument("--path-to-gcode-sort-paths", type=inkex.Boolean, default=True, help="Sort paths to reduce rapid distance.")
+ add_argument("--comment-gcode", default="", help="Comment Gcode")
+ add_argument("--comment-gcode-from-properties", type=inkex.Boolean, default=False, help="Get additional comments from Object Properties")
+
+ add_argument("--tool-diameter", type=float, default="3", help="Tool diameter used for area cutting")
+ add_argument("--max-area-curves", type=int, default="100", help="Maximum area curves for each area")
+ add_argument("--area-inkscape-radius", type=float, default="0", help="Area curves overlapping (depends on tool diameter [0, 0.9])")
+ add_argument("--area-tool-overlap", type=float, default="-10", help="Radius for preparing curves using inkscape")
+ add_argument("--unit", default="G21 (All units in mm)", help="Units")
+ add_argument("--active-tab", type=self.arg_method('tab'), default=self.tab_help, help="Defines which tab is active")
+
+ add_argument("--area-fill-angle", type=float, default="0", help="Fill area with lines heading this angle")
+ add_argument("--area-fill-shift", type=float, default="0", help="Shift the lines by tool d * shift")
+ add_argument("--area-fill-method", default="zig-zag", help="Filling method either zig-zag or spiral")
+
+ add_argument("--area-find-artefacts-diameter", type=float, default="1", help="Artefacts seeking radius")
+ add_argument("--area-find-artefacts-action", default="mark with an arrow", help="Artefacts action type")
+
+ add_argument("--auto_select_paths", type=inkex.Boolean, default=True, help="Select all paths if nothing is selected.")
+
+ add_argument("--loft-distances", default="10", help="Distances between paths.")
+ add_argument("--loft-direction", default="crosswise", help="Direction of loft's interpolation.")
+ add_argument("--loft-interpolation-degree", type=float, default="2", help="Which interpolation use to loft the paths smooth interpolation or staright.")
+
+ add_argument("--min-arc-radius", type=float, default=".1", help="All arc having radius less than minimum will be considered as straight line")
+
+ 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")
+ 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)")
+ add_argument("--engraving-newton-iterations", type=int, default="4", help="Number of sample points used to calculate distance")
+ add_argument("--engraving-draw-calculation-paths", type=inkex.Boolean, default=False, help="Draw additional graphics to debug engraving path")
+ add_argument("--engraving-cutter-shape-function", default="w", help="Cutter shape function z(w). Ex. cone: w. ")
+
+ add_argument("--lathe-width", type=float, default=10., help="Lathe width")
+ add_argument("--lathe-fine-cut-width", type=float, default=1., help="Fine cut width")
+ add_argument("--lathe-fine-cut-count", type=int, default=1., help="Fine cut count")
+ add_argument("--lathe-create-fine-cut-using", default="Move path", help="Create fine cut using")
+ add_argument("--lathe-x-axis-remap", default="X", help="Lathe X axis remap")
+ add_argument("--lathe-z-axis-remap", default="Z", help="Lathe Z axis remap")
+
+ add_argument("--lathe-rectangular-cutter-width", type=float, default="4", help="Rectangular cutter width")
+
+ add_argument("--create-log", type=inkex.Boolean, dest="log_create_log", default=False, help="Create log files")
+ add_argument("--log-filename", default='', help="Create log files")
+
+ add_argument("--orientation-points-count", default="2", help="Orientation points count")
+ add_argument("--tools-library-type", default='cylinder cutter', help="Create tools definition")
+
+ add_argument("--dxfpoints-action", default='replace', help="dxfpoint sign toggle")
+
+ add_argument("--help-language", default='http://www.cnc-club.ru/forum/viewtopic.php?f=33&t=35', help="Open help page in webbrowser.")
+
+ add_argument("--offset-radius", type=float, default=10., help="Offset radius")
+ add_argument("--offset-step", type=float, default=10., help="Offset step")
+ add_argument("--offset-draw-clippend-path", type=inkex.Boolean, default=False, help="Draw clipped path")
+ add_argument("--offset-just-get-distance", type=inkex.Boolean, default=False, help="Don't do offset just get distance")
+
+ add_argument("--postprocessor", default='', help="Postprocessor command.")
+ add_argument("--postprocessor-custom", default='', help="Postprocessor custom command.")
+
+ add_argument("--graffiti-max-seg-length", type=float, default=1., help="Graffiti maximum segment length.")
+ add_argument("--graffiti-min-radius", type=float, default=10., help="Graffiti minimal connector's radius.")
+ add_argument("--graffiti-start-pos", default="(0;0)", help="Graffiti Start position (x;y).")
+ add_argument("--graffiti-create-linearization-preview", type=inkex.Boolean, default=True, help="Graffiti create linearization preview.")
+ add_argument("--graffiti-create-preview", type=inkex.Boolean, default=True, help="Graffiti create preview.")
+ add_argument("--graffiti-preview-size", type=int, default=800, help="Graffiti preview's size.")
+ add_argument("--graffiti-preview-emmit", type=int, default=800, help="Preview's paint emmit (pts/s).")
+
+ add_argument("--in-out-path", type=inkex.Boolean, default=True, help="Create in-out paths")
+ add_argument("--in-out-path-do-not-add-reference-point", type=inkex.Boolean, default=False, help="Just add reference in-out point")
+ add_argument("--in-out-path-point-max-dist", type=float, default=10., help="In-out path max distance to reference point")
+ add_argument("--in-out-path-type", default="Round", help="In-out path type")
+ add_argument("--in-out-path-len", type=float, default=10., help="In-out path length")
+ add_argument("--in-out-path-replace-original-path", type=inkex.Boolean, default=False, help="Replace original path")
+ add_argument("--in-out-path-radius", type=float, default=10., help="In-out path radius for round path")
+
+ add_argument("--plasma-prepare-corners", type=inkex.Boolean, default=True, help="Prepare corners")
+ add_argument("--plasma-prepare-corners-distance", type=float, default=10., help="Stepout distance for corners")
+ add_argument("--plasma-prepare-corners-tolerance", type=float, default=10., help="Maximum angle for corner (0-180 deg)")
+
+ def __init__(self):
+ super(Gcodetools, self).__init__()
+ self.default_tool = {
+ "name": "Default tool",
+ "id": "default tool",
+ "diameter": 10.,
+ "shape": "10",
+ "penetration angle": 90.,
+ "penetration feed": 100.,
+ "depth step": 1.,
+ "feed": 400.,
+ "in trajectotry": "",
+ "out trajectotry": "",
+ "gcode before path": "",
+ "gcode after path": "",
+ "sog": "",
+ "spinlde rpm": "",
+ "CW or CCW": "",
+ "tool change gcode": " ",
+ "4th axis meaning": " ",
+ "4th axis scale": 1.,
+ "4th axis offset": 0.,
+ "passing feed": "800",
+ "fine feed": "800",
+ }
+ self.tools_field_order = [
+ 'name',
+ 'id',
+ 'diameter',
+ 'feed',
+ 'shape',
+ 'penetration angle',
+ 'penetration feed',
+ "passing feed",
+ 'depth step',
+ "in trajectotry",
+ "out trajectotry",
+ "gcode before path",
+ "gcode after path",
+ "sog",
+ "spinlde rpm",
+ "CW or CCW",
+ "tool change gcode",
+ ]
+
+ def parse_curve(self, p, layer, w=None, f=None):
+ c = []
+ if len(p) == 0:
+ return []
+ p = self.transform_csp(p, layer)
+
+ # Sort to reduce Rapid distance
+ k = list(range(1, len(p)))
+ keys = [0]
+ while len(k) > 0:
+ end = p[keys[-1]][-1][1]
+ dist = None
+ for i in range(len(k)):
+ start = p[k[i]][0][1]
+ dist = max((-((end[0] - start[0]) ** 2 + (end[1] - start[1]) ** 2), i), dist)
+ keys += [k[dist[1]]]
+ del k[dist[1]]
+ for k in keys:
+ subpath = p[k]
+ c += [[[subpath[0][1][0], subpath[0][1][1]], 'move', 0, 0]]
+ for i in range(1, len(subpath)):
+ sp1 = [[subpath[i - 1][j][0], subpath[i - 1][j][1]] for j in range(3)]
+ sp2 = [[subpath[i][j][0], subpath[i][j][1]] for j in range(3)]
+ c += biarc(sp1, sp2, 0, 0) if w is None else biarc(sp1, sp2, -f(w[k][i - 1]), -f(w[k][i]))
+ c += [[[subpath[-1][1][0], subpath[-1][1][1]], 'end', 0, 0]]
+ return c
+
+ ################################################################################
+ # Draw csp
+ ################################################################################
+
+ def draw_csp(self, csp, layer=None, group=None, fill='none', stroke='#178ade', width=0.354, style=None):
+ if layer is not None:
+ csp = self.transform_csp(csp, layer, reverse=True)
+ if group is None and layer is None:
+ group = self.document.getroot()
+ elif group is None and layer is not None:
+ group = layer
+ csp = self.apply_transforms(group, csp, reverse=True)
+ if style is not None:
+ return draw_csp(csp, group=group, style=style)
+ else:
+ return draw_csp(csp, group=group, fill=fill, stroke=stroke, width=width)
+
+ def draw_curve(self, curve, layer, group=None, style=MARKER_STYLE["biarc_style"]):
+ self.set_markers()
+
+ for i in [0, 1]:
+ sid = 'biarc{}_r'.format(i)
+ style[sid] = style['biarc{}'.format(i)].copy()
+ style[sid]["marker-start"] = "url(#DrawCurveMarker_r)"
+ del style[sid]["marker-end"]
+
+ if group is None:
+ group = self.layers[min(1, len(self.layers) - 1)].add(Group(gcodetools="Preview group"))
+ if not hasattr(self, "preview_groups"):
+ self.preview_groups = {layer: group}
+ elif layer not in self.preview_groups:
+ self.preview_groups[layer] = group
+ group = self.preview_groups[layer]
+
+ s = ''
+ arcn = 0
+
+ transform = self.get_transforms(group)
+ if transform:
+ transform = self.reverse_transform(transform)
+ transform = str(Transform(transform))
+
+ a = [0., 0.]
+ b = [1., 0.]
+ c = [0., 1.]
+ k = (b[0] - a[0]) * (c[1] - a[1]) - (c[0] - a[0]) * (b[1] - a[1])
+ a = self.transform(a, layer, True)
+ b = self.transform(b, layer, True)
+ c = self.transform(c, layer, True)
+ if ((b[0] - a[0]) * (c[1] - a[1]) - (c[0] - a[0]) * (b[1] - a[1])) * k > 0:
+ reverse_angle = 1
+ else:
+ reverse_angle = -1
+ for sk in curve:
+ si = sk[:]
+ si[0] = self.transform(si[0], layer, True)
+ si[2] = self.transform(si[2], layer, True) if type(si[2]) == type([]) and len(si[2]) == 2 else si[2]
+
+ if s != '':
+ if s[1] == 'line':
+ elem = group.add(PathElement(gcodetools="Preview"))
+ elem.transform = transform
+ elem.style = style['line']
+ elem.path = 'M {},{} L {},{}'.format(s[0][0], s[0][1], si[0][0], si[0][1])
+ elif s[1] == 'arc':
+ arcn += 1
+ sp = s[0]
+ c = s[2]
+ s[3] = s[3] * reverse_angle
+
+ a = ((P(si[0]) - P(c)).angle() - (P(s[0]) - P(c)).angle()) % TAU # s[3]
+ if s[3] * a < 0:
+ if a > 0:
+ a = a - TAU
+ else:
+ a = TAU + a
+ r = math.sqrt((sp[0] - c[0]) ** 2 + (sp[1] - c[1]) ** 2)
+ a_st = (math.atan2(sp[0] - c[0], - (sp[1] - c[1])) - math.pi / 2) % (math.pi * 2)
+ if a > 0:
+ a_end = a_st + a
+ st = style['biarc{}'.format(arcn % 2)]
+ else:
+ a_end = a_st * 1
+ a_st = a_st + a
+ st = style['biarc{}_r'.format(arcn % 2)]
+
+ elem = group.add(PathElement.arc(c, r, start=a_st, end=a_end,
+ open=True, gcodetools="Preview"))
+ elem.transform = transform
+ elem.style = st
+
+ s = si
+
+ def check_dir(self):
+ print_("Checking directory: '{}'".format(self.options.directory))
+ if os.path.isdir(self.options.directory):
+ if os.path.isfile(os.path.join(self.options.directory, 'header')):
+ with open(os.path.join(self.options.directory, 'header')) as f:
+ self.header = f.read()
+ else:
+ self.header = defaults['header']
+ if os.path.isfile(os.path.join(self.options.directory, 'footer')):
+ with open(os.path.join(self.options.directory, 'footer')) as f:
+ self.footer = f.read()
+ else:
+ self.footer = defaults['footer']
+ self.header += self.options.unit + "\n"
+ else:
+ self.error("Directory does not exist! Please specify existing directory at Preferences tab!", "error")
+ return False
+
+ if self.options.add_numeric_suffix_to_filename:
+ dir_list = os.listdir(self.options.directory)
+ if "." in self.options.file:
+ r = re.match(r"^(.*)(\..*)$", self.options.file)
+ ext = r.group(2)
+ name = r.group(1)
+ else:
+ ext = ""
+ name = self.options.file
+ max_n = 0
+ for s in dir_list:
+ r = re.match(r"^{}_0*(\d+){}$".format(re.escape(name), re.escape(ext)), s)
+ if r:
+ max_n = max(max_n, int(r.group(1)))
+ filename = name + "_" + ("0" * (4 - len(str(max_n + 1))) + str(max_n + 1)) + ext
+ self.options.file = filename
+
+ try:
+ with open(os.path.join(self.options.directory, self.options.file), "w") as f:
+ pass
+ except:
+ self.error("Can not write to specified file!\n{}".format(os.path.join(self.options.directory, self.options.file)), "error")
+ return False
+ return True
+
+ ################################################################################
+ #
+ # Generate Gcode
+ # Generates Gcode on given curve.
+ #
+ # Curve definition [start point, type = {'arc','line','move','end'}, arc center, arc angle, end point, [zstart, zend]]
+ #
+ ################################################################################
+ def generate_gcode(self, curve, layer, depth):
+ Zauto_scale = self.Zauto_scale[layer]
+ tool = self.tools[layer][0]
+ g = ""
+
+ def c(c):
+ c = [c[i] if i < len(c) else None for i in range(6)]
+ if c[5] == 0:
+ c[5] = None
+ s = [" X", " Y", " Z", " I", " J", " K"]
+ s1 = ["", "", "", "", "", ""]
+ m = [1, 1, self.options.Zscale * Zauto_scale, 1, 1, self.options.Zscale * Zauto_scale]
+ a = [0, 0, self.options.Zoffset, 0, 0, 0]
+ r = ''
+ for i in range(6):
+ if c[i] is not None:
+ r += s[i] + ("{:f}".format(c[i] * m[i] + a[i])) + s1[i]
+ return r
+
+ def calculate_angle(a, current_a):
+ return min(
+ [abs(a - current_a % TAU + TAU), a + current_a - current_a % TAU + TAU],
+ [abs(a - current_a % TAU - TAU), a + current_a - current_a % TAU - TAU],
+ [abs(a - current_a % TAU), a + current_a - current_a % TAU])[1]
+
+ if len(curve) == 0:
+ return ""
+
+ try:
+ self.last_used_tool is None
+ except:
+ self.last_used_tool = None
+ print_("working on curve")
+ print_(curve)
+
+ if tool != self.last_used_tool:
+ g += ("(Change tool to {})\n".format(re.sub("\"'\\(\\)\\\\", " ", tool["name"]))) + tool["tool change gcode"] + "\n"
+
+ lg = 'G00'
+ zs = self.options.Zsafe
+ f = " F{:f}".format(tool['feed'])
+ current_a = 0
+ go_to_safe_distance = "G00" + c([None, None, zs]) + "\n"
+ penetration_feed = " F{}".format(tool['penetration feed'])
+ for i in range(1, len(curve)):
+ # Creating Gcode for curve between s=curve[i-1] and si=curve[i] start at s[0] end at s[4]=si[0]
+ s = curve[i - 1]
+ si = curve[i]
+ feed = f if lg not in ['G01', 'G02', 'G03'] else ''
+ if s[1] == 'move':
+ g += go_to_safe_distance + "G00" + c(si[0]) + "\n" + tool['gcode before path'] + "\n"
+ lg = 'G00'
+ elif s[1] == 'end':
+ g += go_to_safe_distance + tool['gcode after path'] + "\n"
+ lg = 'G00'
+ elif s[1] == 'line':
+ if tool['4th axis meaning'] == "tangent knife":
+ a = atan2(si[0][0] - s[0][0], si[0][1] - s[0][1])
+ a = calculate_angle(a, current_a)
+ g += "G01 A{}\n".format(a * tool['4th axis scale'] + tool['4th axis offset'])
+ current_a = a
+ if lg == "G00":
+ g += "G01" + c([None, None, s[5][0] + depth]) + penetration_feed + "(Penetrate)\n"
+ g += "G01" + c(si[0] + [s[5][1] + depth]) + feed + "\n"
+ lg = 'G01'
+ elif s[1] == 'arc':
+ r = [(s[2][0] - s[0][0]), (s[2][1] - s[0][1])]
+ if tool['4th axis meaning'] == "tangent knife":
+ if s[3] < 0: # CW
+ a1 = atan2(s[2][1] - s[0][1], -s[2][0] + s[0][0]) + math.pi
+ else: # CCW
+ a1 = atan2(-s[2][1] + s[0][1], s[2][0] - s[0][0]) + math.pi
+ a = calculate_angle(a1, current_a)
+ g += "G01 A{}\n".format(a * tool['4th axis scale'] + tool['4th axis offset'])
+ current_a = a
+ axis4 = " A{}".format((current_a + s[3]) * tool['4th axis scale'] + tool['4th axis offset'])
+ current_a = current_a + s[3]
+ else:
+ axis4 = ""
+ if lg == "G00":
+ g += "G01" + c([None, None, s[5][0] + depth]) + penetration_feed + "(Penetrate)\n"
+ if (r[0] ** 2 + r[1] ** 2) > self.options.min_arc_radius ** 2:
+ r1 = (P(s[0]) - P(s[2]))
+ r2 = (P(si[0]) - P(s[2]))
+ if abs(r1.mag() - r2.mag()) < 0.001:
+ 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"
+ else:
+ r = (r1.mag() + r2.mag()) / 2
+ g += ("G02" if s[3] < 0 else "G03") + c(si[0] + [s[5][1] + depth]) + " R{:f}".format(r) + feed + axis4 + "\n"
+ lg = 'G02'
+ else:
+ if tool['4th axis meaning'] == "tangent knife":
+ a = atan2(si[0][0] - s[0][0], si[0][1] - s[0][1]) + math.pi
+ a = calculate_angle(a, current_a)
+ g += "G01 A{}\n".format(a * tool['4th axis scale'] + tool['4th axis offset'])
+ current_a = a
+ g += "G01" + c(si[0] + [s[5][1] + depth]) + feed + "\n"
+ lg = 'G01'
+ if si[1] == 'end':
+ g += go_to_safe_distance + tool['gcode after path'] + "\n"
+ return g
+
+ def get_transforms(self, g):
+ root = self.document.getroot()
+ trans = []
+ while g != root:
+ if 'transform' in g.keys():
+ t = g.get('transform')
+ t = Transform(t).matrix
+ trans = (Transform(t) * Transform(trans)).matrix if trans != [] else t
+
+ print_(trans)
+ g = g.getparent()
+ return trans
+
+ def reverse_transform(self, transform):
+ trans = numpy.array(transform + ([0, 0, 1],))
+ if numpy.linalg.det(trans) != 0:
+ trans = numpy.linalg.inv(trans).tolist()[:2]
+ return trans
+ else:
+ return transform
+
+ def apply_transforms(self, g, csp, reverse=False):
+ trans = self.get_transforms(g)
+ if trans:
+ if not reverse:
+ # TODO: This was applyTransformToPath but was deprecated. Candidate for refactoring.
+ for comp in csp:
+ for ctl in comp:
+ for pt in ctl:
+ pt[0], pt[1] = Transform(trans).apply_to_point(pt)
+
+ else:
+ # TODO: This was applyTransformToPath but was deprecated. Candidate for refactoring.
+ for comp in csp:
+ for ctl in comp:
+ for pt in ctl:
+ pt[0], pt[1] = Transform(self.reverse_transform(trans)).apply_to_point(pt)
+ return csp
+
+ def transform_scalar(self, x, layer, reverse=False):
+ return self.transform([x, 0], layer, reverse)[0] - self.transform([0, 0], layer, reverse)[0]
+
+ def transform(self, source_point, layer, reverse=False):
+ if layer not in self.transform_matrix:
+ for i in range(self.layers.index(layer), -1, -1):
+ if self.layers[i] in self.orientation_points:
+ break
+ if self.layers[i] not in self.orientation_points:
+ self.error(f"Orientation points for '{layer.label}' layer have not been found! Please add orientation points using Orientation tab!", "error")
+ elif self.layers[i] in self.transform_matrix:
+ self.transform_matrix[layer] = self.transform_matrix[self.layers[i]]
+ self.Zcoordinates[layer] = self.Zcoordinates[self.layers[i]]
+ else:
+ orientation_layer = self.layers[i]
+ if len(self.orientation_points[orientation_layer]) > 1:
+ self.error(f"There are more than one orientation point groups in '{orientation_layer.label}' layer")
+ points = self.orientation_points[orientation_layer][0]
+ if len(points) == 2:
+ 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]]]]
+ if len(points) == 3:
+ print_("Layer '{orientation_layer.label}' Orientation points: ")
+ for point in points:
+ print_(point)
+ # Zcoordinates definition taken from Orientatnion point 1 and 2
+ self.Zcoordinates[layer] = [max(points[0][1][2], points[1][1][2]), min(points[0][1][2], points[1][1][2])]
+ matrix = numpy.array([
+ [points[0][0][0], points[0][0][1], 1, 0, 0, 0, 0, 0, 0],
+ [0, 0, 0, points[0][0][0], points[0][0][1], 1, 0, 0, 0],
+ [0, 0, 0, 0, 0, 0, points[0][0][0], points[0][0][1], 1],
+ [points[1][0][0], points[1][0][1], 1, 0, 0, 0, 0, 0, 0],
+ [0, 0, 0, points[1][0][0], points[1][0][1], 1, 0, 0, 0],
+ [0, 0, 0, 0, 0, 0, points[1][0][0], points[1][0][1], 1],
+ [points[2][0][0], points[2][0][1], 1, 0, 0, 0, 0, 0, 0],
+ [0, 0, 0, points[2][0][0], points[2][0][1], 1, 0, 0, 0],
+ [0, 0, 0, 0, 0, 0, points[2][0][0], points[2][0][1], 1]
+ ])
+
+ if numpy.linalg.det(matrix) != 0:
+ m = numpy.linalg.solve(matrix,
+ numpy.array(
+ [[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]]
+ )
+ ).tolist()
+ self.transform_matrix[layer] = [[m[j * 3 + i][0] for i in range(3)] for j in range(3)]
+
+ else:
+ 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")
+ else:
+ 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")
+
+ self.transform_matrix_reverse[layer] = numpy.linalg.inv(self.transform_matrix[layer]).tolist()
+ print_(f"\n Layer '{layer.label}' transformation matrixes:")
+ print_(self.transform_matrix)
+ print_(self.transform_matrix_reverse)
+
+ # Zautoscale is obsolete
+ self.Zauto_scale[layer] = 1
+ print_("Z automatic scale = {} (computed according orientation points)".format(self.Zauto_scale[layer]))
+
+ x = source_point[0]
+ y = source_point[1]
+ if not reverse:
+ t = self.transform_matrix[layer]
+ else:
+ t = self.transform_matrix_reverse[layer]
+ return [t[0][0] * x + t[0][1] * y + t[0][2], t[1][0] * x + t[1][1] * y + t[1][2]]
+
+ def transform_csp(self, csp_, layer, reverse=False):
+ 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_))]
+ for i in xrange(len(csp)):
+ for j in xrange(len(csp[i])):
+ for k in xrange(len(csp[i][j])):
+ csp[i][j][k] = self.transform(csp[i][j][k], layer, reverse)
+ return csp
+
+ def error(self, s, msg_type="warning"):
+ """
+ Errors handling function
+ warnings are printed into log file and warning message is displayed but
+ extension continues working,
+ errors causes log and execution is halted
+ """
+ if msg_type == "warning":
+ print_(s)
+ inkex.errormsg(s + "\n")
+
+ elif msg_type == "error":
+ print_(s)
+ raise inkex.AbortExtension(s)
+
+ else:
+ print_("Unknown message type: {}".format(msg_type))
+ print_(s)
+ raise inkex.AbortExtension(s)
+
+ ################################################################################
+ # Set markers
+ ################################################################################
+ def set_markers(self):
+ """Make sure all markers are available"""
+ def ensure_marker(elem_id, x=-4, polA='', polB='-', fill='#000044'):
+ if self.svg.getElementById(elem_id) is None:
+ marker = self.svg.defs.add(Marker(
+ id=elem_id, orient="auto", refX=str(x), refY="-1.687441",
+ style="overflow:visible"))
+ path = marker.add(PathElement(
+ d="m {0}4.588864,-1.687441 0.0,0.0 L {0}9.177728,0.0 "\
+ "c {1}0.73311,-0.996261 {1}0.728882,-2.359329 0.0,-3.374882"\
+ .format(polA, polB)))
+ path.style = "fill:{};fill-rule:evenodd;stroke:none;".format(fill)
+
+ ensure_marker("CheckToolsAndOPMarker")
+ ensure_marker("DrawCurveMarker")
+ ensure_marker("DrawCurveMarker_r", x=4, polA='-', polB='')
+ ensure_marker("InOutPathMarker", fill='#0072a7')
+
+ def get_info(self):
+ """Get Gcodetools info from the svg"""
+ self.selected_paths = {}
+ self.paths = {}
+ self.tools = {}
+ self.orientation_points = {}
+ self.graffiti_reference_points = {}
+ self.layers = [self.document.getroot()]
+ self.Zcoordinates = {}
+ self.transform_matrix = {}
+ self.transform_matrix_reverse = {}
+ self.Zauto_scale = {}
+ self.in_out_reference_points = []
+ self.my3Dlayer = None
+
+ def recursive_search(g, layer, selected=False):
+ items = g.getchildren()
+ items.reverse()
+ for i in items:
+ if selected:
+ self.svg.selected[i.get("id")] = i
+ if isinstance(i, Layer):
+ if i.label == '3D':
+ self.my3Dlayer = i
+ else:
+ self.layers += [i]
+ recursive_search(i, i)
+
+ elif i.get('gcodetools') == "Gcodetools orientation group":
+ points = self.get_orientation_points(i)
+ if points is not None:
+ self.orientation_points[layer] = self.orientation_points[layer] + [points[:]] if layer in self.orientation_points else [points[:]]
+ print_(f"Found orientation points in '{layer.label}' layer: {points}")
+ else:
+ self.error(f"Warning! Found bad orientation points in '{layer.label}' layer. Resulting Gcode could be corrupt!")
+
+ # Need to recognise old files ver 1.6.04 and earlier
+ elif i.get("gcodetools") == "Gcodetools tool definition" or i.get("gcodetools") == "Gcodetools tool definition":
+ tool = self.get_tool(i)
+ self.tools[layer] = self.tools[layer] + [tool.copy()] if layer in self.tools else [tool.copy()]
+ print_(f"Found tool in '{layer.label}' layer: {tool}")
+
+ elif i.get("gcodetools") == "Gcodetools graffiti reference point":
+ point = self.get_graffiti_reference_points(i)
+ if point:
+ self.graffiti_reference_points[layer] = self.graffiti_reference_points[layer] + [point[:]] if layer in self.graffiti_reference_points else [point]
+ else:
+ self.error(f"Warning! Found bad graffiti reference point in '{layer.label}' layer. Resulting Gcode could be corrupt!")
+
+ elif isinstance(i, inkex.PathElement):
+ if "gcodetools" not in i.keys():
+ self.paths[layer] = self.paths[layer] + [i] if layer in self.paths else [i]
+ if i.get("id") in self.svg.selected.ids:
+ self.selected_paths[layer] = self.selected_paths[layer] + [i] if layer in self.selected_paths else [i]
+
+ elif i.get("gcodetools") == "In-out reference point group":
+ items_ = i.getchildren()
+ items_.reverse()
+ for j in items_:
+ if j.get("gcodetools") == "In-out reference point":
+ self.in_out_reference_points.append(self.apply_transforms(j, j.path.to_superpath())[0][0][1])
+
+ elif isinstance(i, inkex.Group):
+ recursive_search(i, layer, (i.get("id") in self.svg.selected))
+
+ elif i.get("id") in self.svg.selected:
+ # xgettext:no-pango-format
+ self.error("This extension works with Paths and Dynamic Offsets and groups of them only! "
+ "All other objects will be ignored!\n"
+ "Solution 1: press Path->Object to path or Shift+Ctrl+C.\n"
+ "Solution 2: Path->Dynamic offset or Ctrl+J.\n"
+ "Solution 3: export all contours to PostScript level 2 (File->Save As->.ps) and File->Import this file.")
+
+ recursive_search(self.document.getroot(), self.document.getroot())
+
+ if len(self.layers) == 1:
+ self.error("Document has no layers! Add at least one layer using layers panel (Ctrl+Shift+L)", "error")
+ root = self.document.getroot()
+
+ if root in self.selected_paths or root in self.paths:
+ self.error("Warning! There are some paths in the root of the document, but not in any layer! Using bottom-most layer for them.")
+
+ if root in self.selected_paths:
+ if self.layers[-1] in self.selected_paths:
+ self.selected_paths[self.layers[-1]] += self.selected_paths[root][:]
+ else:
+ self.selected_paths[self.layers[-1]] = self.selected_paths[root][:]
+ del self.selected_paths[root]
+
+ if root in self.paths:
+ if self.layers[-1] in self.paths:
+ self.paths[self.layers[-1]] += self.paths[root][:]
+ else:
+ self.paths[self.layers[-1]] = self.paths[root][:]
+ del self.paths[root]
+
+ def get_orientation_points(self, g):
+ items = g.getchildren()
+ items.reverse()
+ p2 = []
+ p3 = []
+ p = None
+ for i in items:
+ if isinstance(i, inkex.Group):
+ if i.get("gcodetools") == "Gcodetools orientation point (2 points)":
+ p2 += [i]
+ if i.get("gcodetools") == "Gcodetools orientation point (3 points)":
+ p3 += [i]
+ if len(p2) == 2:
+ p = p2
+ elif len(p3) == 3:
+ p = p3
+ if p is None:
+ return None
+ points = []
+ for i in p:
+ point = [[], []]
+ for node in i:
+ if node.get('gcodetools') == "Gcodetools orientation point arrow":
+ csp = node.path.transform(node.composed_transform()).to_superpath()
+ point[0] = csp[0][0][1]
+ if node.get('gcodetools') == "Gcodetools orientation point text":
+ inkex.errormsg(node.get_text())
+ 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())
+ point[1] = [float(r.group(1)), float(r.group(2)), float(r.group(3))]
+ if point[0] != [] and point[1] != []:
+ points += [point]
+ if len(points) == len(p2) == 2 or len(points) == len(p3) == 3:
+ return points
+ else:
+ return None
+
+ def get_graffiti_reference_points(self, g):
+ point = [[], '']
+ for node in g:
+ if node.get('gcodetools') == "Gcodetools graffiti reference point arrow":
+ point[0] = self.apply_transforms(node, node.path.to_superpath())[0][0][1]
+ if node.get('gcodetools') == "Gcodetools graffiti reference point text":
+ point[1] = node.get_text()
+ if point[0] != [] and point[1] != '':
+ return point
+ else:
+ return []
+
+ def get_tool(self, g):
+ tool = self.default_tool.copy()
+ tool["self_group"] = g
+ for i in g:
+ # Get parameters
+ if i.get("gcodetools") == "Gcodetools tool background":
+ tool["style"] = dict(i.style)
+ elif i.get("gcodetools") == "Gcodetools tool parameter":
+ key = None
+ value = None
+ for j in i:
+ # need to recognise old tools from ver 1.6.04
+ if j.get("gcodetools") == "Gcodetools tool definition field name" or j.get("gcodetools") == "Gcodetools tool defention field name":
+ key = j.get_text()
+ if j.get("gcodetools") == "Gcodetools tool definition field value" or j.get("gcodetools") == "Gcodetools tool defention field value":
+ value = j.get_text()
+ if value == "(None)":
+ value = ""
+ if value is None or key is None:
+ continue
+ if key in self.default_tool.keys():
+ try:
+ tool[key] = type(self.default_tool[key])(value)
+ except:
+ tool[key] = self.default_tool[key]
+ 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]))
+ else:
+ tool[key] = value
+ self.error("Warning! Tool has parameter that default tool has not ( '{}': '{}' ).".format(key, value))
+ return tool
+
+ def set_tool(self, layer):
+ for i in range(self.layers.index(layer), -1, -1):
+ if self.layers[i] in self.tools:
+ break
+ if self.layers[i] in self.tools:
+ if self.layers[i] != layer:
+ self.tools[layer] = self.tools[self.layers[i]]
+ if len(self.tools[layer]) > 1:
+ label = self.layers[i].label
+ self.error(f"Layer '{label}' contains more than one tool!")
+ return self.tools[layer]
+ else:
+ self.error(f"Can not find tool for '{layer.label}' layer! Please add one with Tools library tab!", "error")
+
+ ################################################################################
+ #
+ # Path to Gcode
+ #
+ ################################################################################
+ def tab_path_to_gcode(self):
+ self.get_info_plus()
+ def get_boundaries(points):
+ minx = None
+ miny = None
+ maxx = None
+ maxy = None
+ out = [[], [], [], []]
+ for p in points:
+ if minx == p[0]:
+ out[0] += [p]
+ if minx is None or p[0] < minx:
+ minx = p[0]
+ out[0] = [p]
+
+ if miny == p[1]:
+ out[1] += [p]
+ if miny is None or p[1] < miny:
+ miny = p[1]
+ out[1] = [p]
+
+ if maxx == p[0]:
+ out[2] += [p]
+ if maxx is None or p[0] > maxx:
+ maxx = p[0]
+ out[2] = [p]
+
+ if maxy == p[1]:
+ out[3] += [p]
+ if maxy is None or p[1] > maxy:
+ maxy = p[1]
+ out[3] = [p]
+ return out
+
+ def remove_duplicates(points):
+ i = 0
+ out = []
+ for p in points:
+ for j in xrange(i, len(points)):
+ if p == points[j]:
+ points[j] = [None, None]
+ if p != [None, None]:
+ out += [p]
+ i += 1
+ return out
+
+ def get_way_len(points):
+ l = 0
+ for i in xrange(1, len(points)):
+ l += math.sqrt((points[i][0] - points[i - 1][0]) ** 2 + (points[i][1] - points[i - 1][1]) ** 2)
+ return l
+
+ def sort_dxfpoints(points):
+ points = remove_duplicates(points)
+ ways = [
+ # l=0, d=1, r=2, u=3
+ [3, 0], # ul
+ [3, 2], # ur
+ [1, 0], # dl
+ [1, 2], # dr
+ [0, 3], # lu
+ [0, 1], # ld
+ [2, 3], # ru
+ [2, 1], # rd
+ ]
+ minimal_way = []
+ minimal_len = None
+ for w in ways:
+ tpoints = points[:]
+ cw = []
+ for j in xrange(0, len(points)):
+ p = get_boundaries(get_boundaries(tpoints)[w[0]])[w[1]]
+ tpoints.remove(p[0])
+ cw += p
+ curlen = get_way_len(cw)
+ if minimal_len is None or curlen < minimal_len:
+ minimal_len = curlen
+ minimal_way = cw
+
+ return minimal_way
+
+ def sort_lines(lines):
+ if len(lines) == 0:
+ return []
+ lines = [[key] + lines[key] for key in range(len(lines))]
+ keys = [0]
+ end_point = lines[0][3:]
+ print_("!!!", lines, "\n", end_point)
+ del lines[0]
+ while len(lines) > 0:
+ dist = [[point_to_point_d2(end_point, lines[i][1:3]), i] for i in range(len(lines))]
+ i = min(dist)[1]
+ keys.append(lines[i][0])
+ end_point = lines[i][3:]
+ del lines[i]
+ return keys
+
+ def sort_curves(curves):
+ lines = []
+ for curve in curves:
+ lines += [curve[0][0][0] + curve[-1][-1][0]]
+ return sort_lines(lines)
+
+ def print_dxfpoints(points):
+ gcode = ""
+ for point in points:
+ 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)
+ return gcode
+
+ def get_path_properties(node):
+ res = {}
+ done = False
+ while not done and node != self.svg:
+ for i in node.getchildren():
+ if isinstance(i, inkex.Desc):
+ res["Description"] = i.text
+ elif isinstance(i, inkex.Title):
+ res["Title"] = i.text
+ done = True
+ node = node.getparent()
+ return res
+
+ if self.selected_paths == {} and self.options.auto_select_paths:
+ paths = self.paths
+ self.error("No paths are selected! Trying to work on all available paths.")
+ else:
+ paths = self.selected_paths
+ self.check_dir()
+ gcode = ""
+
+ parent = list(self.selected_paths)[0] if self.selected_paths else self.layers[0]
+ biarc_group = parent.add(Group())
+ print_(("self.layers=", self.layers))
+ print_(("paths=", paths))
+ colors = {}
+ for layer in self.layers:
+ if layer in paths:
+ print_(("layer", layer))
+ # transform simple path to get all var about orientation
+ self.transform_csp([[[[0, 0], [0, 0], [0, 0]], [[0, 0], [0, 0], [0, 0]]]], layer)
+
+ self.set_tool(layer)
+ curves = []
+ dxfpoints = []
+
+ try:
+ depth_func = eval('lambda c,d,s: ' + self.options.path_to_gcode_depth_function.strip('"'))
+ except:
+ self.error("Bad depth function! Enter correct function at Path to Gcode tab!")
+
+ for path in paths[layer]:
+ if "d" not in path.keys():
+ 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)!")
+ continue
+ csp = path.path.to_superpath()
+ csp = self.apply_transforms(path, csp)
+ id_ = path.get("id")
+
+ def set_comment(match, path):
+ if match.group(1) in path.keys():
+ return path.get(match.group(1))
+ else:
+ return "None"
+
+ if self.options.comment_gcode != "":
+ comment = re.sub("\\[([A-Za-z_\\-\\:]+)\\]", partial(set_comment, path=path), self.options.comment_gcode)
+ comment = comment.replace(":newline:", "\n")
+ comment = gcode_comment_str(comment)
+ else:
+ comment = ""
+ if self.options.comment_gcode_from_properties:
+ tags = get_path_properties(path)
+ for tag in tags:
+ comment += gcode_comment_str("{}: {}".format(tag, tags[tag]))
+
+ stroke = path.style('stroke')
+ colors[id_] = inkex.Color(stroke if stroke != None else "#000").to_rgb()
+ if path.get("dxfpoint") == "1":
+ tmp_curve = self.transform_csp(csp, layer)
+ x = tmp_curve[0][0][0][0]
+ y = tmp_curve[0][0][0][1]
+ print_("got dxfpoint (scaled) at ({:f},{:f})".format(x, y))
+ dxfpoints += [[x, y]]
+ else:
+
+ zd = self.Zcoordinates[layer][1]
+ zs = self.Zcoordinates[layer][0]
+ c = 1. - float(sum(colors[id_])) / 255 / 3
+ curves += [
+ [
+ [id_, depth_func(c, zd, zs), comment],
+ [self.parse_curve([subpath], layer) for subpath in csp]
+ ]
+ ]
+ dxfpoints = sort_dxfpoints(dxfpoints)
+ gcode += print_dxfpoints(dxfpoints)
+
+ for curve in curves:
+ for subcurve in curve[1]:
+ self.draw_curve(subcurve, layer)
+
+ if self.options.path_to_gcode_order == 'subpath by subpath':
+ curves_ = []
+ for curve in curves:
+ curves_ += [[curve[0], [subcurve]] for subcurve in curve[1]]
+ curves = curves_
+
+ self.options.path_to_gcode_order = 'path by path'
+
+ if self.options.path_to_gcode_order == 'path by path':
+ if self.options.path_to_gcode_sort_paths:
+ keys = sort_curves([curve[1] for curve in curves])
+ else:
+ keys = range(len(curves))
+ for key in keys:
+ d = curves[key][0][1]
+ for step in range(0, int(math.ceil(abs((zs - d) / self.tools[layer][0]["depth step"])))):
+ z = max(d, zs - abs(self.tools[layer][0]["depth step"] * (step + 1)))
+
+ gcode += gcode_comment_str("\nStart cutting path id: {}".format(curves[key][0][0]))
+ if curves[key][0][2] != "()":
+ gcode += curves[key][0][2] # add comment
+
+ for curve in curves[key][1]:
+ gcode += self.generate_gcode(curve, layer, z)
+
+ gcode += gcode_comment_str("End cutting path id: {}\n\n".format(curves[key][0][0]))
+
+ else: # pass by pass
+ mind = min([curve[0][1] for curve in curves])
+ for step in range(0, 1 + int(math.ceil(abs((zs - mind) / self.tools[layer][0]["depth step"])))):
+ z = zs - abs(self.tools[layer][0]["depth step"] * step)
+ curves_ = []
+ for curve in curves:
+ if curve[0][1] < z:
+ curves_.append(curve)
+
+ z = zs - abs(self.tools[layer][0]["depth step"] * (step + 1))
+ gcode += "\n(Pass at depth {})\n".format(z)
+
+ if self.options.path_to_gcode_sort_paths:
+ keys = sort_curves([curve[1] for curve in curves_])
+ else:
+ keys = range(len(curves_))
+ for key in keys:
+
+ gcode += gcode_comment_str("Start cutting path id: {}".format(curves[key][0][0]))
+ if curves[key][0][2] != "()":
+ gcode += curves[key][0][2] # add comment
+
+ for subcurve in curves_[key][1]:
+ gcode += self.generate_gcode(subcurve, layer, max(z, curves_[key][0][1]))
+
+ gcode += gcode_comment_str("End cutting path id: {}\n\n".format(curves[key][0][0]))
+
+ self.export_gcode(gcode)
+
+ ################################################################################
+ #
+ # dxfpoints
+ #
+ ################################################################################
+ def tab_dxfpoints(self):
+ self.get_info_plus()
+ if self.selected_paths == {}:
+ self.error("Nothing is selected. Please select something to convert to drill point (dxfpoint) or clear point sign.")
+ for layer in self.layers:
+ if layer in self.selected_paths:
+ for path in self.selected_paths[layer]:
+ if self.options.dxfpoints_action == 'replace':
+
+ path.set("dxfpoint", "1")
+ r = re.match("^\\s*.\\s*(\\S+)", path.get("d"))
+ if r is not None:
+ print_(("got path=", r.group(1)))
+ 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)))
+ path.set("style", MARKER_STYLE["dxf_points"])
+
+ if self.options.dxfpoints_action == 'save':
+ path.set("dxfpoint", "1")
+
+ if self.options.dxfpoints_action == 'clear' and path.get("dxfpoint") == "1":
+ path.set("dxfpoint", "0")
+
+ ################################################################################
+ #
+ # Artefacts
+ #
+ ################################################################################
+ def tab_area_artefacts(self):
+ self.get_info_plus()
+ if self.selected_paths == {} and self.options.auto_select_paths:
+ paths = self.paths
+ self.error("No paths are selected! Trying to work on all available paths.")
+ else:
+ paths = self.selected_paths
+ for layer in paths:
+ for path in paths[layer]:
+ parent = path.getparent()
+ if "d" not in path.keys():
+ 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)!")
+ continue
+ csp = path.path.to_superpath()
+ remove = []
+ for i in range(len(csp)):
+ subpath = [[point[:] for point in points] for points in csp[i]]
+ subpath = self.apply_transforms(path, [subpath])[0]
+ bounds = csp_simple_bound([subpath])
+ if (bounds[2] - bounds[0]) ** 2 + (bounds[3] - bounds[1]) ** 2 < self.options.area_find_artefacts_diameter ** 2:
+ if self.options.area_find_artefacts_action == "mark with an arrow":
+ 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()
+ arrow = self.apply_transforms(path, arrow, True)
+ node = parent.add(PathElement())
+ node.path = CubicSuperPath(arrow)
+ node.style = MARKER_STYLE["area artefact arrow"]
+ node.set('gcodetools', 'area artefact arrow')
+ elif self.options.area_find_artefacts_action == "mark with style":
+ node = parent.add(PathElement())
+ node.path = CubicSuperPath(csp[i])
+ node.style = MARKER_STYLE["area artefact"]
+ remove.append(i)
+ elif self.options.area_find_artefacts_action == "delete":
+ remove.append(i)
+ print_("Deleted artefact {}".format(subpath))
+ remove.reverse()
+ for i in remove:
+ del csp[i]
+ if len(csp) == 0:
+ parent.remove(path)
+ else:
+ path.path = CubicSuperPath(csp)
+
+ return
+
+ def tab_area(self):
+ """Calculate area curves"""
+ self.get_info_plus()
+ if len(self.selected_paths) <= 0:
+ self.error("This extension requires at least one selected path.")
+ return
+ for layer in self.layers:
+ if layer in self.selected_paths:
+ self.set_tool(layer)
+ if self.tools[layer][0]['diameter'] <= 0:
+ self.error(f"Tool diameter must be > 0 but tool's diameter on '{layer.label}' layer is not!", "error")
+
+ for path in self.selected_paths[layer]:
+ print_(("doing path", path.get("style"), path.get("d")))
+
+ area_group = path.getparent().add(Group())
+
+ csp = path.path.to_superpath()
+ print_(csp)
+ if not csp:
+ print_("omitting non-path")
+ self.error("Warning: omitting non-path")
+ continue
+
+ if path.get('sodipodi:type') != "inkscape:offset":
+ print_("Path {} is not an offset. Preparation started.".format(path.get("id")))
+ # Path is not offset. Preparation will be needed.
+ # Finding top most point in path (min y value)
+
+ min_x, min_y, min_i, min_j, min_t = csp_true_bounds(csp)[1]
+
+ # Reverse path if needed.
+ if min_y != float("-inf"):
+ # Move outline subpath to the beginning of csp
+ subp = csp[min_i]
+ del csp[min_i]
+ j = min_j
+ # Split by the topmost point and join again
+ if min_t in [0, 1]:
+ if min_t == 0:
+ j = j - 1
+ subp[-1][2], subp[0][0] = subp[-1][1], subp[0][1]
+ subp = [[subp[j][1], subp[j][1], subp[j][2]]] + subp[j + 1:] + subp[:j] + [[subp[j][0], subp[j][1], subp[j][1]]]
+ else:
+ sp1, sp2, sp3 = csp_split(subp[j - 1], subp[j], min_t)
+ subp[-1][2], subp[0][0] = subp[-1][1], subp[0][1]
+ subp = [[sp2[1], sp2[1], sp2[2]]] + [sp3] + subp[j + 1:] + subp[:j - 1] + [sp1] + [[sp2[0], sp2[1], sp2[1]]]
+ csp = [subp] + csp
+ # reverse path if needed
+ if csp_subpath_ccw(csp[0]):
+ for i in range(len(csp)):
+ n = []
+ for j in csp[i]:
+ n = [[j[2][:], j[1][:], j[0][:]]] + n
+ csp[i] = n[:]
+
+ # What the absolute fudge is this doing? Closing paths? Ugh.
+ # Not sure but it most be at this level and not in the if statement, or it will not work with dynamic offsets
+ d = str(CubicSuperPath(csp))
+ print_(("original d=", d))
+ d = re.sub(r'(?i)(m[^mz]+)', r'\1 Z ', d)
+ d = re.sub(r'(?i)\s*z\s*z\s*', r' Z ', d)
+ d = re.sub(r'(?i)\s*([A-Za-z])\s*', r' \1 ', d)
+ print_(("formatted d=", d))
+ p0 = self.transform([0, 0], layer)
+ p1 = self.transform([0, 1], layer)
+ scale = (P(p0) - P(p1)).mag()
+ if scale == 0:
+ scale = 1.
+ else:
+ scale = 1. / scale
+ print_(scale)
+ tool_d = self.tools[layer][0]['diameter'] * scale
+ r = self.options.area_inkscape_radius * scale
+ sign = 1 if r > 0 else -1
+ print_("Tool diameter = {}, r = {}".format(tool_d, r))
+
+ # avoiding infinite loops
+ if self.options.area_tool_overlap > 0.9:
+ self.options.area_tool_overlap = .9
+
+ for i in range(self.options.max_area_curves):
+ radius = - tool_d * (i * (1 - self.options.area_tool_overlap) + 0.5) * sign
+ if abs(radius) > abs(r):
+ radius = -r
+
+ elem = area_group.add(PathElement(style=str(MARKER_STYLE["biarc_style_i"]['area'])))
+ elem.set('sodipodi:type', 'inkscape:offset')
+ elem.set('inkscape:radius', radius)
+ elem.set('inkscape:original', d)
+ print_(("adding curve", area_group, d, str(MARKER_STYLE["biarc_style_i"]['area'])))
+ if radius == -r:
+ break
+
+ def tab_area_fill(self):
+ """Fills area with lines"""
+ self.get_info_plus()
+ # convert degrees into rad
+ self.options.area_fill_angle = self.options.area_fill_angle * math.pi / 180
+ if len(self.selected_paths) <= 0:
+ self.error("This extension requires at least one selected path.")
+ return
+ for layer in self.layers:
+ if layer in self.selected_paths:
+ self.set_tool(layer)
+ if self.tools[layer][0]['diameter'] <= 0:
+ self.error(f"Tool diameter must be > 0 but tool's diameter on '{layer.label}' layer is not!", "error")
+ tool = self.tools[layer][0]
+ for path in self.selected_paths[layer]:
+ lines = []
+ print_(("doing path", path.get("style"), path.get("d")))
+ area_group = path.getparent().add(Group())
+ csp = path.path.to_superpath()
+ if not csp:
+ print_("omitting non-path")
+ self.error("Warning: omitting non-path")
+ continue
+ csp = self.apply_transforms(path, csp)
+ csp = csp_close_all_subpaths(csp)
+ csp = self.transform_csp(csp, layer)
+
+ # rotate the path to get bounds in defined direction.
+ a = - self.options.area_fill_angle
+ 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]
+ bounds = csp_true_bounds(rotated_path)
+
+ # Draw the lines
+ # Get path's bounds
+ b = [0.0, 0.0, 0.0, 0.0] # [minx,miny,maxx,maxy]
+ for k in range(4):
+ i = bounds[k][2]
+ j = bounds[k][3]
+ t = bounds[k][4]
+
+ b[k] = csp_at_t(rotated_path[i][j - 1], rotated_path[i][j], t)[k % 2]
+
+ # Zig-zag
+ r = tool['diameter'] * (1 - self.options.area_tool_overlap)
+ if r <= 0:
+ self.error('Tools diameter must be greater than 0!', 'error')
+ return
+
+ lines += [[]]
+
+ if self.options.area_fill_method == 'zig-zag':
+ i = b[0] - self.options.area_fill_shift * r
+ top = True
+ last_one = True
+ while i < b[2] or last_one:
+ if i >= b[2]:
+ last_one = False
+ if not lines[-1]:
+ lines[-1] += [[i, b[3]]]
+
+ if top:
+ lines[-1] += [[i, b[1]], [i + r, b[1]]]
+
+ else:
+ lines[-1] += [[i, b[3]], [i + r, b[3]]]
+
+ top = not top
+ i += r
+ else:
+
+ w = b[2] - b[0] + self.options.area_fill_shift * r
+ h = b[3] - b[1] + self.options.area_fill_shift * r
+ x = b[0] - self.options.area_fill_shift * r
+ y = b[1] - self.options.area_fill_shift * r
+ lines[-1] += [[x, y]]
+ stage = 0
+ start = True
+ while w > 0 and h > 0:
+ stage = (stage + 1) % 4
+ if stage == 0:
+ y -= h
+ h -= r
+ elif stage == 1:
+ x += w
+ if not start:
+ w -= r
+ start = False
+ elif stage == 2:
+ y += h
+ h -= r
+ elif stage == 3:
+ x -= w
+ w -= r
+
+ lines[-1] += [[x, y]]
+
+ stage = (stage + 1) % 4
+ if w <= 0 and h > 0:
+ y = y - h if stage == 0 else y + h
+ if h <= 0 and w > 0:
+ x = x - w if stage == 3 else x + w
+ lines[-1] += [[x, y]]
+ # Rotate created paths back
+ a = self.options.area_fill_angle
+ 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]
+
+ # get the intersection points
+
+ splitted_line = [[lines[0][0]]]
+ intersections = {}
+ for l1, l2, in zip(lines[0], lines[0][1:]):
+ ints = []
+
+ if l1[0] == l2[0] and l1[1] == l2[1]:
+ continue
+ for i in range(len(csp)):
+ for j in range(1, len(csp[i])):
+ sp1 = csp[i][j - 1]
+ sp2 = csp[i][j]
+ roots = csp_line_intersection(l1, l2, sp1, sp2)
+ for t in roots:
+ p = tuple(csp_at_t(sp1, sp2, t))
+ if l1[0] == l2[0]:
+ t1 = (p[1] - l1[1]) / (l2[1] - l1[1])
+ else:
+ t1 = (p[0] - l1[0]) / (l2[0] - l1[0])
+ if 0 <= t1 <= 1:
+ ints += [[t1, p[0], p[1], i, j, t]]
+ if p in intersections:
+ intersections[p] += [[i, j, t]]
+ else:
+ intersections[p] = [[i, j, t]]
+
+ ints.sort()
+ for i in ints:
+ splitted_line[-1] += [[i[1], i[2]]]
+ splitted_line += [[[i[1], i[2]]]]
+ splitted_line[-1] += [l2]
+ i = 0
+ print_(splitted_line)
+ while i < len(splitted_line):
+ # check if the middle point of the first lines segment is inside the path.
+ # and remove the subline if not.
+ l1 = splitted_line[i][0]
+ l2 = splitted_line[i][1]
+ p = [(l1[0] + l2[0]) / 2, (l1[1] + l2[1]) / 2]
+ if not point_inside_csp(p, csp):
+ del splitted_line[i]
+ else:
+ i += 1
+
+ # and apply back transrormations to draw them
+ csp_line = csp_from_polyline(splitted_line)
+ csp_line = self.transform_csp(csp_line, layer, True)
+
+ self.draw_csp(csp_line, group=area_group)
+
+ ################################################################################
+ #
+ # Engraving
+ #
+ # LT Notes to self: See wiki.inkscape.org/wiki/index.php/PythonEffectTutorial
+ # To create anything in the Inkscape document, look at the XML editor for
+ # details of how such an element looks in XML, then follow this model.
+ # layer number n appears in XML as <svg:g id="layern" inkscape:label="layername">
+ #
+ # to create it, use
+ # Mylayer = self.svg.add(Layer.new('layername'))
+ #
+ # group appears in XML as <svg:g id="gnnnnn"> where nnnnn is a number
+ #
+ # to create it, use
+ # Mygroup = parent.add(Group(gcodetools="My group label")
+ # where parent may be the layer or a parent group. To get the parent group, you can use
+ # parent = self.selected_paths[layer][0].getparent()
+ ################################################################################
+ def tab_engraving(self):
+ self.get_info_plus()
+ global cspm
+ global wl
+ global nlLT
+ global i
+ global j
+ global gcode_3Dleft
+ global gcode_3Dright
+ global max_dist # minimum of tool radius and user's requested maximum distance
+ global eye_dist
+ eye_dist = 100 # 3D constant. Try varying it for your eyes
+
+ def bisect(nxy1, nxy2):
+ """LT Find angle bisecting the normals n1 and n2
+
+ Parameters: Normalised normals
+ Returns: nx - Normal of bisector, normalised to 1/cos(a)
+ ny -
+ sinBis2 - sin(angle turned/2): positive if turning in
+ Note that bisect(n1,n2) and bisect(n2,n1) give opposite sinBis2 results
+ If sinturn is less than the user's requested angle tolerance, I return 0
+ """
+ (nx1, ny1) = nxy1
+ (nx2, ny2) = nxy2
+ cosBis = math.sqrt(max(0, (1.0 + nx1 * nx2 - ny1 * ny2) / 2.0))
+ # We can get correct sign of the sin, assuming cos is positive
+ if (abs(ny1 - ny2) < ENGRAVING_TOLERANCE) or (abs(cosBis) < ENGRAVING_TOLERANCE):
+ if abs(nx1 - nx2) < ENGRAVING_TOLERANCE:
+ return nx1, ny1, 0.0
+ sinBis = math.copysign(1, ny1)
+ else:
+ sinBis = cosBis * (nx2 - nx1) / (ny1 - ny2)
+ # We can correct signs by noting that the dot product
+ # of bisector and either normal must be >0
+ costurn = cosBis * nx1 + sinBis * ny1
+ if costurn == 0:
+ return ny1 * 100, -nx1 * 100, 1 # Path doubles back on itself
+ sinturn = sinBis * nx1 - cosBis * ny1
+ if costurn < 0:
+ sinturn = -sinturn
+ if 0 < sinturn * 114.6 < (180 - self.options.engraving_sharp_angle_tollerance):
+ sinturn = 0 # set to zero if less than the user wants to see.
+ return cosBis / costurn, sinBis / costurn, sinturn
+ # end bisect
+
+ def get_radius_to_line(xy1, n_xy1, n_xy2, xy2, n_xy23, xy3, n_xy3):
+ """LT find biggest circle we can engrave here, if constrained by line 2-3
+
+ Parameters:
+ x1,y1,nx1,ny1 coordinates and normal of the line we're currently engraving
+ nx2,ny2 angle bisector at point 2
+ x2,y2 coordinates of first point of line 2-3
+ nx23,ny23 normal to the line 2-3
+ x3,y3 coordinates of the other end
+ nx3,ny3 angle bisector at point 3
+ Returns:
+ radius or self.options.engraving_max_dist if line doesn't limit radius
+ This function can be used in three ways:
+ - With nx1=ny1=0 it finds circle centred at x1,y1
+ - with nx1,ny1 normalised, it finds circle tangential at x1,y1
+ - with nx1,ny1 scaled by 1/cos(a) it finds circle centred on an angle bisector
+ where a is the angle between the bisector and the previous/next normals
+
+ If the centre of the circle tangential to the line 2-3 is outside the
+ angle bisectors at its ends, ignore this line.
+
+ Note that it handles corners in the conventional manner of letter cutting
+ by mitering, not rounding.
+ Algorithm uses dot products of normals to find radius
+ and hence coordinates of centre
+ """
+ (x1, y1) = xy1
+ (nx1, ny1) = n_xy1
+ (nx2, ny2) = n_xy2
+ (x2, y2) = xy2
+ (nx23, ny23) = n_xy23
+ (x3, y3) = xy3
+ (nx3, ny3) = n_xy3
+ global max_dist
+
+ # Start by converting coordinates to be relative to x1,y1
+ x2, y2 = x2 - x1, y2 - y1
+ x3, y3 = x3 - x1, y3 - y1
+
+ # The logic uses vector arithmetic.
+ # The dot product of two vectors gives the product of their lengths
+ # multiplied by the cos of the angle between them.
+ # So, the perpendicular distance from x1y1 to the line 2-3
+ # is equal to the dot product of its normal and x2y2 or x3y3
+ # It is also equal to the projection of x1y1-xcyc on the line's normal
+ # plus the radius. But, as the normal faces inside the path we must negate it.
+
+ # Make sure the line in question is facing x1,y1 and vice versa
+ dist = -x2 * nx23 - y2 * ny23
+ if dist < 0:
+ return max_dist
+ denom = 1. - nx23 * nx1 - ny23 * ny1
+ if denom < ENGRAVING_TOLERANCE:
+ return max_dist
+
+ # radius and centre are:
+ r = dist / denom
+ cx = r * nx1
+ cy = r * ny1
+ # if c is not between the angle bisectors at the ends of the line, ignore
+ # Use vector cross products. Not sure if I need the .0001 safety margins:
+ if (x2 - cx) * ny2 > (y2 - cy) * nx2 + 0.0001:
+ return max_dist
+ if (x3 - cx) * ny3 < (y3 - cy) * nx3 - 0.0001:
+ return max_dist
+ return min(r, max_dist)
+ # end of get_radius_to_line
+
+ def get_radius_to_point(xy1, n_xy, xy2):
+ """LT find biggest circle we can engrave here, constrained by point x2,y2
+
+ This function can be used in three ways:
+ - With nx=ny=0 it finds circle centred at x1,y1
+ - with nx,ny normalised, it finds circle tangential at x1,y1
+ - with nx,ny scaled by 1/cos(a) it finds circle centred on an angle bisector
+ where a is the angle between the bisector and the previous/next normals
+
+ Note that I wrote this to replace find_cutter_centre. It is far less
+ sophisticated but, I hope, far faster.
+ It turns out that finding a circle touching a point is harder than a circle
+ touching a line.
+ """
+ (x1, y1) = xy1
+ (nx, ny) = n_xy
+ (x2, y2) = xy2
+ global max_dist
+
+ # Start by converting coordinates to be relative to x1,y1
+ x2 = x2 - x1
+ y2 = y2 - y1
+ denom = nx ** 2 + ny ** 2 - 1
+ if denom <= ENGRAVING_TOLERANCE: # Not a corner bisector
+ if denom == -1: # Find circle centre x1,y1
+ return math.sqrt(x2 ** 2 + y2 ** 2)
+ # if x2,y2 not in front of the normal...
+ if x2 * nx + y2 * ny <= 0:
+ return max_dist
+ return (x2 ** 2 + y2 ** 2) / (2 * (x2 * nx + y2 * ny))
+ # It is a corner bisector, so..
+ discriminator = (x2 * nx + y2 * ny) ** 2 - denom * (x2 ** 2 + y2 ** 2)
+ if discriminator < 0:
+ return max_dist # this part irrelevant
+ r = (x2 * nx + y2 * ny - math.sqrt(discriminator)) / denom
+ return min(r, max_dist)
+ # end of get_radius_to_point
+
+ def bez_divide(a, b, c, d):
+ """LT recursively divide a Bezier.
+
+ Divides until difference between each
+ part and a straight line is less than some limit
+ Note that, as simple as this code is, it is mathematically correct.
+ Parameters:
+ a,b,c and d are each a list of x,y real values
+ Bezier end points a and d, control points b and c
+ Returns:
+ a list of Beziers.
+ Each Bezier is a list with four members,
+ each a list holding a coordinate pair
+ Note that the final point of one member is the same as
+ the first point of the next, and the control points
+ there are smooth and symmetrical. I use this fact later.
+ """
+ bx = b[0] - a[0]
+ by = b[1] - a[1]
+ cx = c[0] - a[0]
+ cy = c[1] - a[1]
+ dx = d[0] - a[0]
+ dy = d[1] - a[1]
+ limit = 8 * math.hypot(dx, dy) / self.options.engraving_newton_iterations
+ # LT This is the only limit we get from the user currently
+ if abs(dx * by - bx * dy) < limit and abs(dx * cy - cx * dy) < limit:
+ return [[a, b, c, d]]
+ abx = (a[0] + b[0]) / 2.0
+ aby = (a[1] + b[1]) / 2.0
+ bcx = (b[0] + c[0]) / 2.0
+ bcy = (b[1] + c[1]) / 2.0
+ cdx = (c[0] + d[0]) / 2.0
+ cdy = (c[1] + d[1]) / 2.0
+ abcx = (abx + bcx) / 2.0
+ abcy = (aby + bcy) / 2.0
+ bcdx = (bcx + cdx) / 2.0
+ bcdy = (bcy + cdy) / 2.0
+ m = [(abcx + bcdx) / 2.0, (abcy + bcdy) / 2.0]
+ return bez_divide(a, [abx, aby], [abcx, abcy], m) + bez_divide(m, [bcdx, bcdy], [cdx, cdy], d)
+ # end of bez_divide
+
+ def get_biggest(nxy1, nxy2):
+ """LT Find biggest circle we can draw inside path at point x1,y1 normal nx,ny
+
+ Parameters:
+ point - either on a line or at a reflex corner
+ normal - normalised to 1 if on a line, to 1/cos(a) at a corner
+ Returns:
+ tuple (j,i,r)
+ ..where j and i are indices of limiting segment, r is radius
+ """
+ (x1, y1) = nxy1
+ (nx, ny) = nxy2
+ global max_dist
+ global nlLT
+ global i
+ global j
+
+ n1 = nlLT[j][i - 1] # current node
+ jjmin = -1
+ iimin = -1
+ r = max_dist
+ # set limits within which to look for lines
+ xmin = x1 + r * nx - r
+ xmax = x1 + r * nx + r
+ ymin = y1 + r * ny - r
+ ymax = y1 + r * ny + r
+ for jj in xrange(0, len(nlLT)): # for every subpath of this object
+ for ii in xrange(0, len(nlLT[jj])): # for every point and line
+ if nlLT[jj][ii - 1][2]: # if a point
+ if jj == j: # except this one
+ if abs(ii - i) < 3 or abs(ii - i) > len(nlLT[j]) - 3:
+ continue
+ t1 = get_radius_to_point((x1, y1), (nx, ny), nlLT[jj][ii - 1][0])
+ else: # doing a line
+ if jj == j: # except this one
+ if abs(ii - i) < 2 or abs(ii - i) == len(nlLT[j]) - 1:
+ continue
+ if abs(ii - i) == 2 and nlLT[j][(ii + i) / 2 - 1][3] <= 0:
+ continue
+ if (abs(ii - i) == len(nlLT[j]) - 2) and nlLT[j][-1][3] <= 0:
+ continue
+ nx2, ny2 = nlLT[jj][ii - 2][1]
+ x2, y2 = nlLT[jj][ii - 1][0]
+ nx23, ny23 = nlLT[jj][ii - 1][1]
+ x3, y3 = nlLT[jj][ii][0]
+ nx3, ny3 = nlLT[jj][ii][1]
+ if nlLT[jj][ii - 2][3] > 0: # acute, so use normal, not bisector
+ nx2 = nx23
+ ny2 = ny23
+ if nlLT[jj][ii][3] > 0: # acute, so use normal, not bisector
+ nx3 = nx23
+ ny3 = ny23
+ x23min = min(x2, x3)
+ x23max = max(x2, x3)
+ y23min = min(y2, y3)
+ y23max = max(y2, y3)
+ # see if line in range
+ if n1[2] == False and (x23max < xmin or x23min > xmax or y23max < ymin or y23min > ymax):
+ continue
+ t1 = get_radius_to_line((x1, y1), (nx, ny), (nx2, ny2), (x2, y2), (nx23, ny23), (x3, y3), (nx3, ny3))
+ if 0 <= t1 < r:
+ r = t1
+ iimin = ii
+ jjmin = jj
+ xmin = x1 + r * nx - r
+ xmax = x1 + r * nx + r
+ ymin = y1 + r * ny - r
+ ymax = y1 + r * ny + r
+ # next ii
+ # next jj
+ return jjmin, iimin, r
+ # end of get_biggest
+
+ def line_divide(xy0, j0, i0, xy1, j1, i1, n_xy, length):
+ """LT recursively divide a line as much as necessary
+
+ NOTE: This function is not currently used
+ By noting which other path segment is touched by the circles at each end,
+ we can see if anything is to be gained by a further subdivision, since
+ if they touch the same bit of path we can move linearly between them.
+ Also, we can handle points correctly.
+ Parameters:
+ end points and indices of limiting path, normal, length
+ Returns:
+ list of toolpath points
+ each a list of 3 reals: x, y coordinates, radius
+
+ """
+ (x0, y0) = xy0
+ (x1, y1) = xy1
+ (nx, ny) = n_xy
+ global nlLT
+ global i
+ global j
+ global lmin
+ x2 = (x0 + x1) / 2
+ y2 = (y0 + y1) / 2
+ j2, i2, r2 = get_biggest((x2, y2), (nx, ny))
+ if length < lmin:
+ return [[x2, y2, r2]]
+ if j2 == j0 and i2 == i0: # Same as left end. Don't subdivide this part any more
+ return [[x2, y2, r2], line_divide((x2, y2), j2, i2, (x1, y1), j1, i1, (nx, ny), length / 2)]
+ if j2 == j1 and i2 == i1: # Same as right end. Don't subdivide this part any more
+ return [line_divide((x0, y0), j0, i0, (x2, y2), j2, i2, (nx, ny), length / 2), [x2, y2, r2]]
+ 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)]
+ # end of line_divide()
+
+ def save_point(xy, w, i, j, ii, jj):
+ """LT Save this point and delete previous one if linear
+
+ The point is, we generate tons of points but many may be in a straight 3D line.
+ There is no benefit in saving the intermediate points.
+ """
+ (x, y) = xy
+ global wl
+ global cspm
+ x = round(x, 4) # round to 4 decimals
+ y = round(y, 4) # round to 4 decimals
+ w = round(w, 4) # round to 4 decimals
+ if len(cspm) > 1:
+ xy1a, xy1, xy1b, i1, j1, ii1, jj1 = cspm[-1]
+ w1 = wl[-1]
+ if i == i1 and j == j1 and ii == ii1 and jj == jj1: # one match
+ xy1a, xy2, xy1b, i1, j1, ii1, jj1 = cspm[-2]
+ w2 = wl[-2]
+ if i == i1 and j == j1 and ii == ii1 and jj == jj1: # two matches. Now test linearity
+ length1 = math.hypot(xy1[0] - x, xy1[1] - y)
+ length2 = math.hypot(xy2[0] - x, xy2[1] - y)
+ length12 = math.hypot(xy2[0] - xy1[0], xy2[1] - xy1[1])
+ # get the xy distance of point 1 from the line 0-2
+ if length2 > length1 and length2 > length12: # point 1 between them
+ xydist = abs((xy2[0] - x) * (xy1[1] - y) - (xy1[0] - x) * (xy2[1] - y)) / length2
+ if xydist < ENGRAVING_TOLERANCE: # so far so good
+ wdist = w2 + (w - w2) * length1 / length2 - w1
+ if abs(wdist) < ENGRAVING_TOLERANCE:
+ cspm.pop()
+ wl.pop()
+ cspm += [[[x, y], [x, y], [x, y], i, j, ii, jj]]
+ wl += [w]
+ # end of save_point
+
+ def draw_point(xy0, xy, w, t):
+ """LT Draw this point as a circle with a 1px dot in the middle (x,y)
+ and a 3D line from (x0,y0) down to x,y. 3D line thickness should be t/2
+
+ Note that points that are subsequently erased as being unneeded do get
+ displayed, but this helps the user see the total area covered.
+ """
+ (x0, y0) = xy0
+ (x, y) = xy
+ global gcode_3Dleft
+ global gcode_3Dright
+ if self.options.engraving_draw_calculation_paths:
+ elem = engraving_group.add(PathElement.arc((x, y), 1))
+ elem.set('gcodetools', "Engraving calculation toolpath")
+ elem.style = "fill:#ff00ff; fill-opacity:0.46; stroke:#000000; stroke-width:0.1;"
+
+ # Don't draw zero radius circles
+ if w:
+ elem = engraving_group.add(PathElement.arc((x, y), w))
+ elem.set('gcodetools', "Engraving calculation paths")
+ elem.style = "fill:none; fill-opacity:0.46; stroke:#000000; stroke-width:0.1;"
+
+ # Find slope direction for shading
+ s = math.atan2(y - y0, x - x0) # -pi to pi
+ # convert to 2 hex digits as a shade of red
+ s2 = "#{0:x}0000".format(int(101 * (1.5 - math.sin(s + 0.5))))
+ style = "stroke:{}; stroke-opacity:1;stroke-width:{};fill:none".format(s2, t/2)
+ right = gcode_3Dleft.add(PathElement(style=style, gcodetools="Gcode G1R"))
+ right.path = "M {:f},{:f} L {:f},{:f}".format(
+ x0 - eye_dist, y0, x - eye_dist - 0.14 * w, y)
+ left = gcode_3Dright.add(PathElement(style=style, gcodetools="Gcode G1L"))
+ left.path = "M {:f},{:f} L {:f},{:f}".format(
+ x0 + eye_dist, y0, x + eye_dist + 0.14 * r, y)
+
+ # end of subfunction definitions. engraving() starts here:
+ gcode = ''
+ r = 0 # theoretical and tool-radius-limited radii in pixels
+ w = 0
+ wmax = 0
+ cspe = []
+ we = []
+ if not self.selected_paths:
+ self.error("Please select at least one path to engrave and run again.")
+ return
+ if not self.check_dir():
+ return
+ # Find what units the user uses
+ unit = " mm"
+ if self.options.unit == "G20 (All units in inches)":
+ unit = " inches"
+ elif self.options.unit != "G21 (All units in mm)":
+ self.error("Unknown unit selected. mm assumed")
+ print_("engraving_max_dist mm/inch", self.options.engraving_max_dist)
+
+ # LT See if we can use this parameter for line and Bezier subdivision:
+ bitlen = 20 / self.options.engraving_newton_iterations
+
+ for layer in self.layers:
+ if layer in self.selected_paths and layer in self.orientation_points:
+ # Calculate scale in pixels per user unit (mm or inch)
+ p1 = self.orientation_points[layer][0][0]
+ p2 = self.orientation_points[layer][0][1]
+ ol = math.hypot(p1[0][0] - p2[0][0], p1[0][1] - p2[0][1])
+ oluu = math.hypot(p1[1][0] - p2[1][0], p1[1][1] - p2[1][1])
+ print_("Orientation2 p1 p2 ol oluu", p1, p2, ol, oluu)
+ orientation_scale = ol / oluu
+
+ self.set_tool(layer)
+ shape = self.tools[layer][0]['shape']
+ if re.search('w', shape):
+ toolshape = eval('lambda w: ' + shape.strip('"'))
+ else:
+ self.error("Tool '{}' has no shape. 45 degree cone assumed!".format(self.tools[layer][0]['name']))
+ toolshape = lambda w: w
+ # Get tool radius in pixels
+ toolr = self.tools[layer][0]['diameter'] * orientation_scale / 2
+ print_("tool radius in pixels=", toolr)
+ # max dist from path to engrave in user's units
+ max_distuu = min(self.tools[layer][0]['diameter'] / 2, self.options.engraving_max_dist)
+ max_dist = max_distuu * orientation_scale
+ print_("max_dist pixels", max_dist)
+
+ engraving_group = self.selected_paths[layer][0].getparent().add(Group())
+ if self.options.engraving_draw_calculation_paths and (self.my3Dlayer is None):
+ self.svg.add(Layer.new("3D"))
+ # Create groups for left and right eyes
+ if self.options.engraving_draw_calculation_paths:
+ gcode_3Dleft = self.my3Dlayer.add(Group(gcodetools="Gcode 3D L"))
+ gcode_3Dright = self.my3Dlayer.add(Group(gcodetools="Gcode 3D R"))
+
+ for node in self.selected_paths[layer]:
+ if isinstance(node, inkex.PathElement):
+ cspi = node.path.to_superpath()
+ # LT: Create my own list. n1LT[j] is for subpath j
+ nlLT = []
+ for j in xrange(len(cspi)): # LT For each subpath...
+ # Remove zero length segments, assume closed path
+ i = 0 # LT was from i=1
+ while i < len(cspi[j]):
+ 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:
+ cspi[j][i - 1][2] = cspi[j][i][2]
+ del cspi[j][i]
+ else:
+ i += 1
+ for csp in cspi: # LT6a For each subpath...
+ # Create copies in 3D layer
+ print_("csp is zz ", csp)
+ cspl = []
+ cspr = []
+ # create list containing lines and points, starting with a point
+ # line members: [x,y],[nx,ny],False,i
+ # x,y is start of line. Normal on engraved side.
+ # Normal is normalised (unit length)
+ # Note that Y axis increases down the page
+ # corner members: [x,y],[nx,ny],True,sin(halfangle)
+ # if halfangle>0: radius 0 here. normal is bisector
+ # if halfangle<0. reflex angle. normal is bisector
+ # corner normals are divided by cos(halfangle)
+ # so that they will engrave correctly
+ print_("csp is", csp)
+ nlLT.append([])
+ for i in range(0, len(csp)): # LT for each point
+ sp0 = csp[i - 2]
+ sp1 = csp[i - 1]
+ sp2 = csp[i]
+ if self.options.engraving_draw_calculation_paths:
+ # Copy it to 3D layer objects
+ spl = []
+ spr = []
+ for j in range(0, 3):
+ pl = [sp2[j][0] - eye_dist, sp2[j][1]]
+ pr = [sp2[j][0] + eye_dist, sp2[j][1]]
+ spl += [pl]
+ spr += [pr]
+ cspl += [spl]
+ cspr += [spr]
+ # LT find angle between this and previous segment
+ x0, y0 = sp1[1]
+ nx1, ny1 = csp_normalized_normal(sp1, sp2, 0)
+ # I don't trust this function, so test result
+ if abs(1 - math.hypot(nx1, ny1)) > 0.00001:
+ print_("csp_normalised_normal error t=0", nx1, ny1, sp1, sp2)
+ self.error("csp_normalised_normal error. See log.")
+
+ nx0, ny0 = csp_normalized_normal(sp0, sp1, 1)
+ if abs(1 - math.hypot(nx0, ny0)) > 0.00001:
+ print_("csp_normalised_normal error t=1", nx0, ny0, sp1, sp2)
+ self.error("csp_normalised_normal error. See log.")
+ bx, by, s = bisect((nx0, ny0), (nx1, ny1))
+ # record x,y,normal,ifCorner, sin(angle-turned/2)
+ nlLT[-1] += [[[x0, y0], [bx, by], True, s]]
+
+ # LT now do the line
+ if sp1[1] == sp1[2] and sp2[0] == sp2[1]: # straightline
+ nlLT[-1] += [[sp1[1], [nx1, ny1], False, i]]
+ else: # Bezier. First, recursively cut it up:
+ nn = bez_divide(sp1[1], sp1[2], sp2[0], sp2[1])
+ first = True # Flag entry to divided Bezier
+ for bLT in nn: # save as two line segments
+ for seg in range(3):
+ if seg > 0 or first:
+ nx1 = bLT[seg][1] - bLT[seg + 1][1]
+ ny1 = bLT[seg + 1][0] - bLT[seg][0]
+ l1 = math.hypot(nx1, ny1)
+ if l1 < ENGRAVING_TOLERANCE:
+ continue
+ nx1 = nx1 / l1 # normalise them
+ ny1 = ny1 / l1
+ nlLT[-1] += [[bLT[seg], [nx1, ny1], False, i]]
+ first = False
+ if seg < 2: # get outgoing bisector
+ nx0 = nx1
+ ny0 = ny1
+ nx1 = bLT[seg + 1][1] - bLT[seg + 2][1]
+ ny1 = bLT[seg + 2][0] - bLT[seg + 1][0]
+ l1 = math.hypot(nx1, ny1)
+ if l1 < ENGRAVING_TOLERANCE:
+ continue
+ nx1 = nx1 / l1 # normalise them
+ ny1 = ny1 / l1
+ # bisect
+ bx, by, s = bisect((nx0, ny0), (nx1, ny1))
+ nlLT[-1] += [[bLT[seg + 1], [bx, by], True, 0.]]
+ # LT for each segment - ends here.
+ print_(("engraving_draw_calculation_paths=", self.options.engraving_draw_calculation_paths))
+ if self.options.engraving_draw_calculation_paths:
+ # Copy complete paths to 3D layer
+ cspl += [cspl[0]] # Close paths
+ cspr += [cspr[0]] # Close paths
+ style = "stroke:#808080; stroke-opacity:1; stroke-width:0.6; fill:none"
+ elem = gcode_3Dleft.add(PathElement(style=style, gcodetools="G1L outline"))
+ elem.path = CubicSuperPath([cspl])
+ elem = gcode_3Dright.add(Pathelement(style=style, gcodetools="G1R outline"))
+ elem.path = CubicSuperPath([cspr])
+
+ for p in nlLT[-1]: # For last sub-path
+ if p[2]:
+ elem = engraving_group.add(PathElement(gcodetools="Engraving normals"))
+ elem.path = "M {:f},{:f} L {:f},{:f}".format(p[0][0], p[0][1],
+ p[0][0] + p[1][0] * 10, p[0][1] + p[1][1] * 10)
+ elem.style = "stroke:#f000af; stroke-opacity:0.46; stroke-width:0.1; fill:none"
+ else:
+ elem = engraving_group.add(PathElement(gcodetools="Engraving bisectors"))
+ elem.path = "M {:f},{:f} L {:f},{:f}".format(p[0][0], p[0][1],
+ p[0][0] + p[1][0] * 10, p[0][1] + p[1][1] * 10)
+ elem.style = "stroke:#0000ff; stroke-opacity:0.46; stroke-width:0.1; fill:none"
+
+ # LT6a build nlLT[j] for each subpath - ends here
+ # Calculate offset points
+ reflex = False
+ for j in xrange(len(nlLT)): # LT6b for each subpath
+ cspm = [] # Will be my output. List of csps.
+ wl = [] # Will be my w output list
+ w = r = 0 # LT initial, as first point is an angle
+ for i in xrange(len(nlLT[j])): # LT for each node
+ # LT Note: Python enables wrapping of array indices
+ # backwards to -1, -2, but not forwards. Hence:
+ n0 = nlLT[j][i - 2] # previous node
+ n1 = nlLT[j][i - 1] # current node
+ n2 = nlLT[j][i] # next node
+ # if n1[2] == True and n1[3]==0 : # A straight angle
+ # continue
+ x1a, y1a = n1[0] # this point/start of this line
+ nx, ny = n1[1]
+ x1b, y1b = n2[0] # next point/end of this line
+ if n1[2]: # We're at a corner
+ bits = 1
+ bit0 = 0
+ # lastr=r #Remember r from last line
+ lastw = w # Remember w from last line
+ w = max_dist
+ if n1[3] > 0: # acute. Limit radius
+ len1 = math.hypot((n0[0][0] - n1[0][0]), (n0[0][1] - n1[0][1]))
+ if i < (len(nlLT[j]) - 1):
+ len2 = math.hypot((nlLT[j][i + 1][0][0] - n1[0][0]), (nlLT[j][i + 1][0][1] - n1[0][1]))
+ else:
+ len2 = math.hypot((nlLT[j][0][0][0] - n1[0][0]), (nlLT[j][0][0][1] - n1[0][1]))
+ # set initial r value, not to be exceeded
+ w = math.sqrt(min(len1, len2)) / n1[3]
+ else: # line. Cut it up if long.
+ if n0[3] > 0 and not self.options.engraving_draw_calculation_paths:
+ bit0 = r * n0[3] # after acute corner
+ else:
+ bit0 = 0.0
+ length = math.hypot((x1b - x1a), (y1a - y1b))
+ bit0 = (min(length, bit0))
+ bits = int((length - bit0) / bitlen)
+ # split excess evenly at both ends
+ bit0 += (length - bit0 - bitlen * bits) / 2
+ for b in xrange(bits): # divide line into bits
+ x1 = x1a + ny * (b * bitlen + bit0)
+ y1 = y1a - nx * (b * bitlen + bit0)
+ jjmin, iimin, w = get_biggest((x1, y1), (nx, ny))
+ print_("i,j,jjmin,iimin,w", i, j, jjmin, iimin, w)
+ wmax = max(wmax, w)
+ if reflex: # just after a reflex corner
+ reflex = False
+ if w < lastw: # need to adjust it
+ draw_point((x1, y1), (n0[0][0] + n0[1][0] * w, n0[0][1] + n0[1][1] * w), w, (lastw - w) / 2)
+ save_point((n0[0][0] + n0[1][0] * w, n0[0][1] + n0[1][1] * w), w, i, j, iimin, jjmin)
+ if n1[2]: # We're at a corner
+ if n1[3] > 0: # acute
+ save_point((x1 + nx * w, y1 + ny * w), w, i, j, iimin, jjmin)
+ draw_point((x1, y1), (x1, y1), 0, 0)
+ save_point((x1, y1), 0, i, j, iimin, jjmin)
+ elif n1[3] < 0: # reflex
+ if w > lastw:
+ draw_point((x1, y1), (x1 + nx * lastw, y1 + ny * lastw), w, (w - lastw) / 2)
+ wmax = max(wmax, w)
+ save_point((x1 + nx * w, y1 + ny * w), w, i, j, iimin, jjmin)
+ elif b > 0 and n2[3] > 0 and not self.options.engraving_draw_calculation_paths: # acute corner coming up
+ if jjmin == j and iimin == i + 2:
+ break
+ draw_point((x1, y1), (x1 + nx * w, y1 + ny * w), w, bitlen)
+ save_point((x1 + nx * w, y1 + ny * w), w, i, j, iimin, jjmin)
+
+ # LT end of for each bit of this line
+ if n1[2] == True and n1[3] < 0: # reflex angle
+ reflex = True
+ lastw = w # remember this w
+ # LT next i
+ cspm += [cspm[0]]
+ print_("cspm", cspm)
+ wl += [wl[0]]
+ print_("wl", wl)
+ # Note: Original csp_points was a list, each element
+ # being 4 points, with the first being the same as the
+ # last of the previous set.
+ # Each point is a list of [cx,cy,r,w]
+ # I have flattened it to a flat list of points.
+
+ if self.options.engraving_draw_calculation_paths:
+ node = engraving_group.add(PathElement(
+ gcodetools="Engraving calculation paths",
+ style=MARKER_STYLE["biarc_style_i"]['biarc1']))
+ node.path = CubicSuperPath([cspm])
+ for i in xrange(len(cspm)):
+ elem = engraving_group.add(PathElement.arc(cspm[i][1], wl[i]))
+ elem.set('gcodetools', "Engraving calculation paths")
+ elem.style = "fill:none;fill-opacity:0.46;stroke:#000000;stroke-width:0.1;"
+ cspe += [cspm]
+ wluu = [] # width list in user units: mm/inches
+ for w in wl:
+ wluu += [w / orientation_scale]
+ print_("wl in pixels", wl)
+ print_("wl in user units", wluu)
+ # LT previously, we was in pixels so gave wrong depth
+ we += [wluu]
+ # LT6b For each subpath - ends here
+ # LT5 if it is a path - ends here
+ # LT4 for each selected object in this layer - ends here
+
+ if cspe:
+ curve = self.parse_curve(cspe, layer, we, toolshape) # convert to lines
+ self.draw_curve(curve, layer, engraving_group)
+ gcode += self.generate_gcode(curve, layer, self.options.Zsurface)
+
+ # LT3 for layers loop ends here
+ if gcode != '':
+ self.header += "(Tool diameter should be at least " + str(2 * wmax / orientation_scale) + unit + ")\n"
+ self.header += "(Depth, as a function of radius w, must be " + self.tools[layer][0]['shape'] + ")\n"
+ self.header += "(Rapid feeds use safe Z=" + str(self.options.Zsafe) + unit + ")\n"
+ self.header += "(Material surface at Z=" + str(self.options.Zsurface) + unit + ")\n"
+ self.export_gcode(gcode)
+ else:
+ self.error("No need to engrave sharp angles.")
+
+ ################################################################################
+ #
+ # Orientation
+ #
+ ################################################################################
+ def tab_orientation(self, layer=None):
+ self.get_info()
+ Zsurface = f"{self.options.Zsurface:.5f}"
+ Zdepth = f"{self.options.Zdepth:.5f}"
+ if layer is None:
+ layer = self.svg.get_current_layer() if self.svg.get_current_layer() is not None else self.document.getroot()
+
+ transform = self.get_transforms(layer)
+ if transform:
+ transform = self.reverse_transform(transform)
+ transform = str(Transform(transform))
+
+ if self.options.orientation_points_count == "graffiti":
+ print_(self.graffiti_reference_points)
+ print_("Inserting graffiti points")
+ if layer in self.graffiti_reference_points:
+ graffiti_reference_points_count = len(self.graffiti_reference_points[layer])
+ else:
+ graffiti_reference_points_count = 0
+ axis = ["X", "Y", "Z", "A"][graffiti_reference_points_count % 4]
+ attr = {'gcodetools': "Gcodetools graffiti reference point"}
+ if transform:
+ attr["transform"] = transform
+ group = layer.add(Group(**attr))
+ elem = group.add(PathElement(style="stroke:none;fill:#00ff00;"))
+ elem.set('gcodetools', "Gcodetools graffiti reference point arrow")
+ elem.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.8125000000'\
+ '01 z z'.format(graffiti_reference_points_count * 100, 0)
+
+ draw_text(axis, graffiti_reference_points_count * 100 + 10, -10, group=group, gcodetools_tag="Gcodetools graffiti reference point text")
+
+ elif self.options.orientation_points_count == "in-out reference point":
+ 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")
+
+ else:
+ print_("Inserting orientation points")
+
+ if layer in self.orientation_points:
+ self.error("Active layer already has orientation points! Remove them or select another layer!", "error")
+
+ attr = {"gcodetools": "Gcodetools orientation group"}
+ if transform:
+ attr["transform"] = transform
+
+ orientation_group = layer.add(Group(**attr))
+ doc_height = self.svg.unittouu(self.document.getroot().get('height'))
+ if self.document.getroot().get('height') == "100%":
+ doc_height = 1052.3622047
+ print_("Overriding height from 100 percents to {}".format(doc_height))
+ if self.options.unit == "G21 (All units in mm)":
+ points = [[0., 0., Zsurface], [100., 0., Zdepth], [0., 100., 0.]]
+ elif self.options.unit == "G20 (All units in inches)":
+ points = [[0., 0., Zsurface], [5., 0., Zdepth], [0., 5., 0.]]
+ if self.options.orientation_points_count == "2":
+ points = points[:2]
+ for i in points:
+ name = "Gcodetools orientation point ({} points)".format(
+ self.options.orientation_points_count)
+ grp = orientation_group.add(Group(gcodetools=name))
+ elem = grp.add(PathElement(style="stroke:none;fill:#000000;"))
+ elem.set('gcodetools', "Gcodetools orientation point arrow")
+ elem.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.812500000'\
+ '001 z'.format(i[0], -i[1] + doc_height)
+
+ 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")
+
+ ################################################################################
+ #
+ # Tools library
+ #
+ ################################################################################
+ def tab_tools_library(self, layer=None):
+ self.get_info()
+
+ if self.options.tools_library_type == "check":
+ return self.check_tools_and_op()
+
+ # Add a tool to the drawing
+ if layer is None:
+ layer = self.svg.get_current_layer() if self.svg.get_current_layer() is not None else self.document.getroot()
+ if layer in self.tools:
+ self.error("Active layer already has a tool! Remove it or select another layer!", "error")
+
+ if self.options.tools_library_type == "cylinder cutter":
+ tool = {
+ "name": "Cylindrical cutter",
+ "id": "Cylindrical cutter 0001",
+ "diameter": 10,
+ "penetration angle": 90,
+ "feed": "400",
+ "penetration feed": "100",
+ "depth step": "1",
+ "tool change gcode": " "
+ }
+ elif self.options.tools_library_type == "lathe cutter":
+ tool = {
+ "name": "Lathe cutter",
+ "id": "Lathe cutter 0001",
+ "diameter": 10,
+ "penetration angle": 90,
+ "feed": "400",
+ "passing feed": "800",
+ "fine feed": "100",
+ "penetration feed": "100",
+ "depth step": "1",
+ "tool change gcode": " "
+ }
+ elif self.options.tools_library_type == "cone cutter":
+ tool = {
+ "name": "Cone cutter",
+ "id": "Cone cutter 0001",
+ "diameter": 10,
+ "shape": "w",
+ "feed": "400",
+ "penetration feed": "100",
+ "depth step": "1",
+ "tool change gcode": " "
+ }
+ elif self.options.tools_library_type == "tangent knife":
+ tool = {
+ "name": "Tangent knife",
+ "id": "Tangent knife 0001",
+ "feed": "400",
+ "penetration feed": "100",
+ "depth step": "100",
+ "4th axis meaning": "tangent knife",
+ "4th axis scale": 1.,
+ "4th axis offset": 0,
+ "tool change gcode": " "
+ }
+
+ elif self.options.tools_library_type == "plasma cutter":
+ tool = {
+ "name": "Plasma cutter",
+ "id": "Plasma cutter 0001",
+ "diameter": 10,
+ "penetration feed": 100,
+ "feed": 400,
+ "gcode before path": """G31 Z-100 F500 (find metal)
+G92 Z0 (zero z)
+G00 Z10 F500 (going up)
+M03 (turn on plasma)
+G04 P0.2 (pause)
+G01 Z1 (going to cutting z)\n""",
+ "gcode after path": "M05 (turn off plasma)\n",
+ }
+ elif self.options.tools_library_type == "graffiti":
+ tool = {
+ "name": "Graffiti",
+ "id": "Graffiti 0001",
+ "diameter": 10,
+ "penetration feed": 100,
+ "feed": 400,
+ "gcode before path": """M03 S1(Turn spray on)\n """,
+ "gcode after path": "M05 (Turn spray off)\n ",
+ "tool change gcode": "(Add G00 here to change sprayer if needed)\n",
+
+ }
+
+ else:
+ tool = self.default_tool
+
+ tool_num = sum([len(self.tools[i]) for i in self.tools])
+ colors = ["00ff00", "0000ff", "ff0000", "fefe00", "00fefe", "fe00fe", "fe7e00", "7efe00", "00fe7e", "007efe", "7e00fe", "fe007e"]
+
+ tools_group = layer.add(Group(gcodetools="Gcodetools tool definition"))
+ bg = tools_group.add(PathElement(gcodetools="Gcodetools tool background"))
+ bg.style = "fill-opacity:0.5;stroke:#444444;"
+ bg.style['fill'] = "#" + colors[tool_num % len(colors)]
+
+ y = 0
+ keys = []
+ for key in self.tools_field_order:
+ if key in tool:
+ keys += [key]
+ for key in tool:
+ if key not in keys:
+ keys += [key]
+ for key in keys:
+ g = tools_group.add(Group(gcodetools="Gcodetools tool parameter"))
+ draw_text(key, 0, y, group=g, gcodetools_tag="Gcodetools tool definition field name", font_size=10 if key != 'name' else 20)
+ param = tool[key]
+ if type(param) == str and re.match("^\\s*$", param):
+ param = "(None)"
+ draw_text(param, 150, y, group=g, gcodetools_tag="Gcodetools tool definition field value", font_size=10 if key != 'name' else 20)
+ v = str(param).split("\n")
+ y += 15 * len(v) if key != 'name' else 20 * len(v)
+
+ bg.set('d', "m -20,-20 l 400,0 0,{:f} -400,0 z ".format(y + 50))
+ tools_group.transform.add_translate(*self.svg.namedview.center)
+ tools_group.transform.add_translate(-150, 0)
+
+ ################################################################################
+ #
+ # Check tools and OP assignment
+ #
+ ################################################################################
+ def check_tools_and_op(self):
+ if len(self.svg.selected) <= 0:
+ self.error("Selection is empty! Will compute whole drawing.")
+ paths = self.paths
+ else:
+ paths = self.selected_paths
+ # Set group
+ parent = self.selected_paths.keys()[0] if len(self.selected_paths.keys()) > 0 else self.layers[0]
+ group = parent.add(Group())
+ trans_ = [[1, 0.3, 0], [0, 0.5, 0]]
+
+ self.set_markers()
+
+ bounds = [float('inf'), float('inf'), float('-inf'), float('-inf')]
+ tools_bounds = {}
+ for layer in self.layers:
+ if layer in paths:
+ self.set_tool(layer)
+ tool = self.tools[layer][0]
+ tools_bounds[layer] = tools_bounds[layer] if layer in tools_bounds else [float("inf"), float("-inf")]
+ for path in paths[layer]:
+ group.insert(0, PathElement(**path.attrib))
+ new = group.getchildren()[0]
+ new.style = Style(
+ stroke='#000044', stroke_width=1,
+ marker_mid='url(#CheckToolsAndOPMarker)',
+ fill=tool["style"].get('fill', '#00ff00'),
+ fill_opacity=tool["style"].get('fill-opacity', 0.5))
+
+ trans = trans_ * self.get_transforms(path)
+ csp = path.path.transform(trans).to_superpath()
+
+ path_bounds = csp_simple_bound(csp)
+ trans = str(Transform(trans))
+ 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])]
+ tools_bounds[layer] = [min(tools_bounds[layer][0], path_bounds[1]), max(tools_bounds[layer][1], path_bounds[3])]
+
+ new.set("transform", trans)
+ trans_[1][2] += 20
+ trans_[1][2] += 100
+
+ for layer in self.layers:
+ if layer in self.tools:
+ if layer in tools_bounds:
+ tool = self.tools[layer][0]
+ g = copy.deepcopy(tool["self_group"])
+ g.attrib["gcodetools"] = "Check tools and OP assignment"
+ trans = [[1, 0.3, bounds[2]], [0, 0.5, tools_bounds[layer][0]]]
+ g.set("transform", str(Transform(trans)))
+ group.insert(0, g)
+
+ ################################################################################
+ # TODO Launch browser on help tab
+ ################################################################################
+ def tab_help(self):
+ self.error("Switch to another tab to run the extensions.\n"
+ "No changes are made if the preferences or help tabs are active.\n\n"
+ "Tutorials, manuals and support can be found at\n"
+ " English support forum:\n"
+ " http://www.cnc-club.ru/gcodetools\n"
+ "and Russian support forum:\n"
+ " http://www.cnc-club.ru/gcodetoolsru")
+ return
+
+ def tab_about(self):
+ return self.tab_help()
+
+ def tab_preferences(self):
+ return self.tab_help()
+
+ def tab_options(self):
+ return self.tab_help()
+
+
+ ################################################################################
+ # Lathe
+ ################################################################################
+ def generate_lathe_gcode(self, subpath, layer, feed_type):
+ if len(subpath) < 2:
+ return ""
+ feed = " F {:f}".format(self.tool[feed_type])
+ x = self.options.lathe_x_axis_remap
+ z = self.options.lathe_z_axis_remap
+ flip_angle = -1 if x.lower() + z.lower() in ["xz", "yx", "zy"] else 1
+ alias = {"X": "I", "Y": "J", "Z": "K", "x": "i", "y": "j", "z": "k"}
+ i_ = alias[x]
+ k_ = alias[z]
+ c = [[subpath[0][1], "move", 0, 0, 0]]
+ for sp1, sp2 in zip(subpath, subpath[1:]):
+ c += biarc(sp1, sp2, 0, 0)
+ for i in range(1, len(c)): # Just in case check end point of each segment
+ c[i - 1][4] = c[i][0][:]
+ c += [[subpath[-1][1], "end", 0, 0, 0]]
+ self.draw_curve(c, layer, style=MARKER_STYLE["biarc_style_lathe_{}".format(feed_type)])
+
+ gcode = ("G01 {} {:f} {} {:f}".format(x, c[0][4][0], z, c[0][4][1])) + feed + "\n" # Just in case move to the start...
+ for s in c:
+ if s[1] == 'line':
+ gcode += ("G01 {} {:f} {} {:f}".format(x, s[4][0], z, s[4][1])) + feed + "\n"
+ elif s[1] == 'arc':
+ r = [(s[2][0] - s[0][0]), (s[2][1] - s[0][1])]
+ if (r[0] ** 2 + r[1] ** 2) > self.options.min_arc_radius ** 2:
+ r1 = (P(s[0]) - P(s[2]))
+ r2 = (P(s[4]) - P(s[2]))
+ if abs(r1.mag() - r2.mag()) < 0.001:
+ 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"
+ else:
+ r = (r1.mag() + r2.mag()) / 2
+ 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"
+ return gcode
+
+ def tab_lathe(self):
+ self.get_info_plus()
+ if not self.check_dir():
+ return
+ x = self.options.lathe_x_axis_remap
+ z = self.options.lathe_z_axis_remap
+ x = re.sub("^\\s*([XYZxyz])\\s*$", r"\1", x)
+ z = re.sub("^\\s*([XYZxyz])\\s*$", r"\1", z)
+ if x not in ["X", "Y", "Z", "x", "y", "z"] or z not in ["X", "Y", "Z", "x", "y", "z"]:
+ self.error("Lathe X and Z axis remap should be 'X', 'Y' or 'Z'. Exiting...")
+ return
+ if x.lower() == z.lower():
+ self.error("Lathe X and Z axis remap should be the same. Exiting...")
+ return
+ if x.lower() + z.lower() in ["xy", "yx"]:
+ gcode_plane_selection = "G17 (Using XY plane)\n"
+ if x.lower() + z.lower() in ["xz", "zx"]:
+ gcode_plane_selection = "G18 (Using XZ plane)\n"
+ if x.lower() + z.lower() in ["zy", "yz"]:
+ gcode_plane_selection = "G19 (Using YZ plane)\n"
+ self.options.lathe_x_axis_remap = x
+ self.options.lathe_z_axis_remap = z
+
+ paths = self.selected_paths
+ self.tool = []
+ gcode = ""
+ for layer in self.layers:
+ if layer in paths:
+ self.set_tool(layer)
+ if self.tool != self.tools[layer][0]:
+ self.tool = self.tools[layer][0]
+ self.tool["passing feed"] = float(self.tool["passing feed"] if "passing feed" in self.tool else self.tool["feed"])
+ self.tool["feed"] = float(self.tool["feed"])
+ self.tool["fine feed"] = float(self.tool["fine feed"] if "fine feed" in self.tool else self.tool["feed"])
+ gcode += ("(Change tool to {})\n".format(re.sub("\"'\\(\\)\\\\", " ", self.tool["name"]))) + self.tool["tool change gcode"] + "\n"
+
+ for path in paths[layer]:
+ csp = self.transform_csp(path.path.to_superpath(), layer)
+
+ for subpath in csp:
+ # Offset the path if fine cut is defined.
+ fine_cut = subpath[:]
+ if self.options.lathe_fine_cut_width > 0:
+ r = self.options.lathe_fine_cut_width
+ if self.options.lathe_create_fine_cut_using == "Move path":
+ subpath = [[[i2[0], i2[1] + r] for i2 in i1] for i1 in subpath]
+ else:
+ # Close the path to make offset correct
+ bound = csp_simple_bound([subpath])
+ minx, miny, maxx, maxy = csp_true_bounds([subpath])
+ 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]]])
+ left = subpath[-1][1][0]
+ right = subpath[0][1][0]
+ if left > right:
+ left, right = right, left
+ offsetted_subpath = csp_offset([offsetted_subpath], r if not csp_subpath_ccw(offsetted_subpath) else -r)
+ offsetted_subpath = csp_clip_by_line(offsetted_subpath, [left, 10], [left, 0])
+ offsetted_subpath = csp_clip_by_line(offsetted_subpath, [right, 0], [right, 10])
+ offsetted_subpath = csp_clip_by_line(offsetted_subpath, [0, miny[1] - r], [10, miny[1] - r])
+ # Join offsetted_subpath together
+ # Hope there won't be any circles
+ subpath = csp_join_subpaths(offsetted_subpath)[0]
+
+ # Create solid object from path and lathe_width
+ bound = csp_simple_bound([subpath])
+ top_start = [subpath[0][1][0], self.options.lathe_width + self.options.Zsafe + self.options.lathe_fine_cut_width]
+ top_end = [subpath[-1][1][0], self.options.lathe_width + self.options.Zsafe + self.options.lathe_fine_cut_width]
+
+ gcode += ("G01 {} {:f} F {:f} \n".format(z, top_start[1], self.tool["passing feed"]))
+ gcode += ("G01 {} {:f} {} {:f} F {:f} \n".format(x, top_start[0], z, top_start[1], self.tool["passing feed"]))
+
+ subpath = csp_concat_subpaths(csp_subpath_line_to([], [top_start, subpath[0][1]]), subpath)
+ subpath = csp_subpath_line_to(subpath, [top_end, top_start])
+
+ width = max(0, self.options.lathe_width - max(0, bound[1]))
+ step = self.tool['depth step']
+ steps = int(math.ceil(width / step))
+ for i in range(steps + 1):
+ current_width = self.options.lathe_width - step * i
+ intersections = []
+ for j in range(1, len(subpath)):
+ sp1 = subpath[j - 1]
+ sp2 = subpath[j]
+ intersections += [[j, k] for k in csp_line_intersection([bound[0] - 10, current_width], [bound[2] + 10, current_width], sp1, sp2)]
+ intersections += [[j, k] for k in csp_line_intersection([bound[0] - 10, current_width + step], [bound[2] + 10, current_width + step], sp1, sp2)]
+ parts = csp_subpath_split_by_points(subpath, intersections)
+ for part in parts:
+ minx, miny, maxx, maxy = csp_true_bounds([part])
+ y = (maxy[1] + miny[1]) / 2
+ if y > current_width + step:
+ gcode += self.generate_lathe_gcode(part, layer, "passing feed")
+ elif current_width <= y <= current_width + step:
+ gcode += self.generate_lathe_gcode(part, layer, "feed")
+ else:
+ # full step cut
+ part = csp_subpath_line_to([], [part[0][1], part[-1][1]])
+ gcode += self.generate_lathe_gcode(part, layer, "feed")
+
+ top_start = [fine_cut[0][1][0], self.options.lathe_width + self.options.Zsafe + self.options.lathe_fine_cut_width]
+ top_end = [fine_cut[-1][1][0], self.options.lathe_width + self.options.Zsafe + self.options.lathe_fine_cut_width]
+ gcode += "\n(Fine cutting start)\n(Calculating fine cut using {})\n".format(self.options.lathe_create_fine_cut_using)
+ for i in range(int(self.options.lathe_fine_cut_count)):
+ width = self.options.lathe_fine_cut_width * (1 - float(i + 1) / self.options.lathe_fine_cut_count)
+ if width == 0:
+ current_pass = fine_cut
+ else:
+ if self.options.lathe_create_fine_cut_using == "Move path":
+ current_pass = [[[i2[0], i2[1] + width] for i2 in i1] for i1 in fine_cut]
+ else:
+ minx, miny, maxx, maxy = csp_true_bounds([fine_cut])
+ 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]]])
+ left = fine_cut[-1][1][0]
+ right = fine_cut[0][1][0]
+ if left > right:
+ left, right = right, left
+ offsetted_subpath = csp_offset([offsetted_subpath], width if not csp_subpath_ccw(offsetted_subpath) else -width)
+ offsetted_subpath = csp_clip_by_line(offsetted_subpath, [left, 10], [left, 0])
+ offsetted_subpath = csp_clip_by_line(offsetted_subpath, [right, 0], [right, 10])
+ offsetted_subpath = csp_clip_by_line(offsetted_subpath, [0, miny[1] - r], [10, miny[1] - r])
+ current_pass = csp_join_subpaths(offsetted_subpath)[0]
+
+ gcode += "\n(Fine cut {:d}-th cicle start)\n".format(i + 1)
+ gcode += ("G01 {} {:f} {} {:f} F {:f} \n".format(x, top_start[0], z, top_start[1], self.tool["passing feed"]))
+ 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"]))
+ gcode += ("G01 {} {:f} {} {:f} F {:f} \n".format(x, current_pass[0][1][0], z, current_pass[0][1][1], self.tool["fine feed"]))
+
+ gcode += self.generate_lathe_gcode(current_pass, layer, "fine feed")
+ gcode += ("G01 {} {:f} F {:f} \n".format(z, top_start[1], self.tool["passing feed"]))
+ gcode += ("G01 {} {:f} {} {:f} F {:f} \n".format(x, top_start[0], z, top_start[1], self.tool["passing feed"]))
+
+ self.export_gcode(gcode)
+
+ ################################################################################
+ #
+ # Lathe modify path
+ # Modifies path to fit current cutter. As for now straight rect cutter.
+ #
+ ################################################################################
+
+ def tab_lathe_modify_path(self):
+ self.get_info()
+ if self.selected_paths == {} and self.options.auto_select_paths:
+ paths = self.paths
+ self.error("No paths are selected! Trying to work on all available paths.")
+ else:
+ paths = self.selected_paths
+
+ for layer in self.layers:
+ if layer in paths:
+ width = self.options.lathe_rectangular_cutter_width
+ for path in paths[layer]:
+ csp = self.transform_csp(path.path.to_superpath(), layer)
+ new_csp = []
+ for subpath in csp:
+ orientation = subpath[-1][1][0] > subpath[0][1][0]
+ new_subpath = []
+
+ # Split segment at x' and y' == 0
+ for sp1, sp2 in zip(subpath[:], subpath[1:]):
+ ax, ay, bx, by, cx, cy, dx, dy = csp_parameterize(sp1, sp2)
+ roots = cubic_solver_real(0, 3 * ax, 2 * bx, cx)
+ roots += cubic_solver_real(0, 3 * ay, 2 * by, cy)
+ new_subpath = csp_concat_subpaths(new_subpath, csp_seg_split(sp1, sp2, roots))
+ subpath = new_subpath
+ new_subpath = []
+ first_seg = True
+ for sp1, sp2 in zip(subpath[:], subpath[1:]):
+ n = csp_normalized_normal(sp1, sp2, 0)
+ a = math.atan2(n[0], n[1])
+ if a == 0 or a == math.pi:
+ n = csp_normalized_normal(sp1, sp2, 1)
+ a = math.atan2(n[0], n[1])
+ if a != 0 and a != math.pi:
+ o = 0 if 0 < a <= math.pi / 2 or -math.pi < a < -math.pi / 2 else 1
+ if not orientation:
+ o = 1 - o
+
+ # Add first horizontal straight line if needed
+ if not first_seg and new_subpath == []:
+ new_subpath = [[[subpath[0][i][0] - width * o, subpath[0][i][1]] for i in range(3)]]
+
+ new_subpath = csp_concat_subpaths(
+ new_subpath,
+ [
+ [[sp1[i][0] - width * o, sp1[i][1]] for i in range(3)],
+ [[sp2[i][0] - width * o, sp2[i][1]] for i in range(3)]
+ ]
+ )
+ first_seg = False
+
+ # Add last horizontal straight line if needed
+ if a == 0 or a == math.pi:
+ new_subpath += [[[subpath[-1][i][0] - width * o, subpath[-1][i][1]] for i in range(3)]]
+
+ new_csp += [new_subpath]
+ self.draw_csp(new_csp, layer)
+
+ ################################################################################
+ # Graffiti function generates Gcode for graffiti drawer
+ ################################################################################
+ def tab_graffiti(self):
+ self.get_info_plus()
+ # Get reference points.
+
+ def get_gcode_coordinates(point, layer):
+ gcode = ''
+ pos = []
+ for ref_point in self.graffiti_reference_points[layer]:
+ c = math.sqrt((point[0] - ref_point[0][0]) ** 2 + (point[1] - ref_point[0][1]) ** 2)
+ gcode += " {} {:f}".format(ref_point[1], c)
+ pos += [c]
+ return pos, gcode
+
+ def graffiti_preview_draw_point(x1, y1, color, radius=.5):
+ self.graffiti_preview = self.graffiti_preview
+ r, g, b, a_ = color
+ for x in range(int(x1 - 1 - math.ceil(radius)), int(x1 + 1 + math.ceil(radius) + 1)):
+ for y in range(int(y1 - 1 - math.ceil(radius)), int(y1 + 1 + math.ceil(radius) + 1)):
+ if x >= 0 and y >= 0 and y < len(self.graffiti_preview) and x * 4 < len(self.graffiti_preview[0]):
+ d = math.sqrt((x1 - x) ** 2 + (y1 - y) ** 2)
+ a = float(a_) * (max(0, (1 - (d - radius))) if d > radius else 1) / 256
+ self.graffiti_preview[y][x * 4] = int(r * a + (1 - a) * self.graffiti_preview[y][x * 4])
+ self.graffiti_preview[y][x * 4 + 1] = int(g * a + (1 - a) * self.graffiti_preview[y][x * 4 + 1])
+ self.graffiti_preview[y][x * 4 + 2] = int(g * b + (1 - a) * self.graffiti_preview[y][x * 4 + 2])
+ self.graffiti_preview[y][x * 4 + 3] = min(255, int(self.graffiti_preview[y][x * 4 + 3] + a * 256))
+
+ def graffiti_preview_transform(x, y):
+ tr = self.graffiti_preview_transform
+ d = max(tr[2] - tr[0] + 2, tr[3] - tr[1] + 2)
+ 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]
+
+ def draw_graffiti_segment(layer, start, end, feed, color=(0, 255, 0, 40), emmit=1000):
+ # Emit = dots per second
+ l = math.sqrt(sum([(start[i] - end[i]) ** 2 for i in range(len(start))]))
+ time_ = l / feed
+ c1 = self.graffiti_reference_points[layer][0][0]
+ c2 = self.graffiti_reference_points[layer][1][0]
+ d = math.sqrt((c1[0] - c2[0]) ** 2 + (c1[1] - c2[1]) ** 2)
+ if d == 0:
+ raise ValueError("Error! Reference points should not be the same!")
+ for i in range(int(time_ * emmit + 1)):
+ t = i / (time_ * emmit)
+ r1 = start[0] * (1 - t) + end[0] * t
+ r2 = start[1] * (1 - t) + end[1] * t
+ a = (r1 ** 2 - r2 ** 2 + d ** 2) / (2 * d)
+ h = math.sqrt(r1 ** 2 - a ** 2)
+ xa = c1[0] + a * (c2[0] - c1[0]) / d
+ ya = c1[1] + a * (c2[1] - c1[1]) / d
+
+ x1 = xa + h * (c2[1] - c1[1]) / d
+ x2 = xa - h * (c2[1] - c1[1]) / d
+ y1 = ya - h * (c2[0] - c1[0]) / d
+ y2 = ya + h * (c2[0] - c1[0]) / d
+
+ x = x1 if y1 < y2 else x2
+ y = min(y1, y2)
+ x, y = graffiti_preview_transform(x, y)
+ graffiti_preview_draw_point(x, y, color)
+
+ def create_connector(p1, p2, t1, t2):
+ P1 = P(p1)
+ P2 = P(p2)
+ N1 = P(rotate_ccw(t1))
+ N2 = P(rotate_ccw(t2))
+ r = self.options.graffiti_min_radius
+ C1 = P1 + N1 * r
+ C2 = P2 + N2 * r
+ # Get closest possible centers of arcs, also we define that arcs are both ccw or both not.
+ dc, N1, N2, m = (
+ (
+ (((P2 - N1 * r) - (P1 - N2 * r)).l2(), -N1, -N2, 1)
+ if vectors_ccw(t1, t2) else
+ (((P2 + N1 * r) - (P1 + N2 * r)).l2(), N1, N2, -1)
+ )
+ if vectors_ccw((P1 - C1).to_list(), t1) == vectors_ccw((P2 - C2).to_list(), t2) else
+ (
+ (((P2 + N1 * r) - (P1 - N2 * r)).l2(), N1, -N2, 1)
+ if vectors_ccw(t1, t2) else
+ (((P2 - N1 * r) - (P1 + N2 * r)).l2(), -N1, N2, 1)
+ )
+ )
+ dc = math.sqrt(dc)
+ C1 = P1 + N1 * r
+ C2 = P2 + N2 * r
+ Dc = C2 - C1
+
+ if dc == 0:
+ # can be joined by one arc
+ return csp_from_arc(p1, p2, C1.to_list(), r, t1)
+
+ cos = Dc.x / dc
+ sin = Dc.y / dc
+
+ p1_end = [C1.x - r * sin * m, C1.y + r * cos * m]
+ p2_st = [C2.x - r * sin * m, C2.y + r * cos * m]
+ if point_to_point_d2(p1, p1_end) < 0.0001 and point_to_point_d2(p2, p2_st) < 0.0001:
+ return [[p1, p1, p1], [p2, p2, p2]]
+
+ arc1 = csp_from_arc(p1, p1_end, C1.to_list(), r, t1)
+ arc2 = csp_from_arc(p2_st, p2, C2.to_list(), r, [cos, sin])
+ return csp_concat_subpaths(arc1, arc2)
+
+ if not self.check_dir():
+ return
+ if self.selected_paths == {} and self.options.auto_select_paths:
+ paths = self.paths
+ self.error("No paths are selected! Trying to work on all available paths.")
+ else:
+ paths = self.selected_paths
+ self.tool = []
+ gcode = """(Header)
+(Generated by gcodetools from Inkscape.)
+(Using graffiti extension.)
+(Header end.)"""
+
+ minx = float("inf")
+ miny = float("inf")
+ maxx = float("-inf")
+ maxy = float("-inf")
+ # Get all reference points and path's bounds to make preview
+
+ for layer in self.layers:
+ if layer in paths:
+ # Set reference points
+ if layer not in self.graffiti_reference_points:
+ reference_points = None
+ for i in range(self.layers.index(layer), -1, -1):
+ if self.layers[i] in self.graffiti_reference_points:
+ reference_points = self.graffiti_reference_points[self.layers[i]]
+ self.graffiti_reference_points[layer] = self.graffiti_reference_points[self.layers[i]]
+ break
+ if reference_points is None:
+ self.error('There are no graffiti reference points for layer {}'.format(layer), "error")
+
+ # Transform reference points
+ for i in range(len(self.graffiti_reference_points[layer])):
+ self.graffiti_reference_points[layer][i][0] = self.transform(self.graffiti_reference_points[layer][i][0], layer)
+ point = self.graffiti_reference_points[layer][i]
+ gcode += "(Reference point {:f};{:f} for {} axis)\n".format(point[0][0], point[0][1], point[1])
+
+ if self.options.graffiti_create_preview:
+ for point in self.graffiti_reference_points[layer]:
+ minx = min(minx, point[0][0])
+ miny = min(miny, point[0][1])
+ maxx = max(maxx, point[0][0])
+ maxy = max(maxy, point[0][1])
+ for path in paths[layer]:
+ csp = path.path.to_superpath()
+ csp = self.apply_transforms(path, csp)
+ csp = self.transform_csp(csp, layer)
+ bounds = csp_simple_bound(csp)
+ minx = min(minx, bounds[0])
+ miny = min(miny, bounds[1])
+ maxx = max(maxx, bounds[2])
+ maxy = max(maxy, bounds[3])
+
+ if self.options.graffiti_create_preview:
+ self.graffiti_preview = list([[255] * (4 * self.options.graffiti_preview_size) for _ in range(self.options.graffiti_preview_size)])
+ self.graffiti_preview_transform = [minx, miny, maxx, maxy]
+
+ for layer in self.layers:
+ if layer in paths:
+
+ r = re.match("\\s*\\(\\s*([0-9\\-,.]+)\\s*;\\s*([0-9\\-,.]+)\\s*\\)\\s*", self.options.graffiti_start_pos)
+ if r:
+ start_point = [float(r.group(1)), float(r.group(2))]
+ else:
+ start_point = [0., 0.]
+ last_sp1 = [[start_point[0], start_point[1] - 10] for _ in range(3)]
+ last_sp2 = [start_point for _ in range(3)]
+
+ self.set_tool(layer)
+ self.tool = self.tools[layer][0]
+ # Change tool every layer. (Probably layer = color so it'll be
+ # better to change it even if the tool has not been changed)
+ gcode += ("(Change tool to {})\n".format(re.sub("\"'\\(\\)\\\\", " ", self.tool["name"]))) + self.tool["tool change gcode"] + "\n"
+
+ subpaths = []
+ for path in paths[layer]:
+ # Rebuild the paths to polyline.
+ csp = path.path.to_superpath()
+ csp = self.apply_transforms(path, csp)
+ csp = self.transform_csp(csp, layer)
+ subpaths += csp
+ polylines = []
+ while len(subpaths) > 0:
+ i = min([(point_to_point_d2(last_sp2[1], subpaths[i][0][1]), i) for i in range(len(subpaths))])[1]
+ subpath = subpaths[i][:]
+ del subpaths[i]
+ polylines += [
+ ['connector', create_connector(
+ last_sp2[1],
+ subpath[0][1],
+ csp_normalized_slope(last_sp1, last_sp2, 1.),
+ csp_normalized_slope(subpath[0], subpath[1], 0.),
+ )]
+ ]
+ polyline = []
+ spl = None
+
+ # remove zerro length segments
+ i = 0
+ while i < len(subpath) - 1:
+ if cspseglength(subpath[i], subpath[i + 1]) < 0.00000001:
+ subpath[i][2] = subpath[i + 1][2]
+ del subpath[i + 1]
+ else:
+ i += 1
+
+ for sp1, sp2 in zip(subpath, subpath[1:]):
+ 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
+ # We've got sharp angle at sp1.
+ polyline += [sp1]
+ polylines += [['draw', polyline[:]]]
+ polylines += [
+ ['connector', create_connector(
+ sp1[1],
+ sp1[1],
+ csp_normalized_slope(spl, sp1, 1.),
+ csp_normalized_slope(sp1, sp2, 0.),
+ )]
+ ]
+ polyline = []
+ # max_segment_length
+ polyline += [sp1]
+ print_(polyline)
+ print_(sp1)
+
+ spl = sp1
+ polyline += [sp2]
+ polylines += [['draw', polyline[:]]]
+
+ last_sp1 = sp1
+ last_sp2 = sp2
+
+ # Add return to start_point
+ if not polylines:
+ continue
+ polylines += [["connect1", [[polylines[-1][1][-1][1] for _ in range(3)], [start_point for _ in range(3)]]]]
+
+ # Make polylines from polylines. They are still csp.
+ for i in range(len(polylines)):
+ polyline = []
+ l = 0
+ print_("polylines", polylines)
+ print_(polylines[i])
+ for sp1, sp2 in zip(polylines[i][1], polylines[i][1][1:]):
+ print_(sp1, sp2)
+ l = cspseglength(sp1, sp2)
+ if l > 0.00000001:
+ polyline += [sp1[1]]
+ parts = int(math.ceil(l / self.options.graffiti_max_seg_length))
+ for j in range(1, parts):
+ polyline += [csp_at_length(sp1, sp2, float(j) / parts)]
+ if l > 0.00000001:
+ polyline += [sp2[1]]
+ print_(i)
+ polylines[i][1] = polyline
+
+ t = 0
+ last_state = None
+ for polyline_ in polylines:
+ polyline = polyline_[1]
+ # Draw linearization
+ if self.options.graffiti_create_linearization_preview:
+ t += 1
+ csp = [[polyline[i], polyline[i], polyline[i]] for i in range(len(polyline))]
+ draw_csp(self.transform_csp([csp], layer, reverse=True))
+
+ # Export polyline to gcode
+ # we are making transform from XYZA coordinates to R1...Rn
+ # where R1...Rn are radius vectors from graffiti reference points
+ # to current (x,y) point. Also we need to assign custom feed rate
+ # for each segment. And we'll use only G01 gcode.
+ last_real_pos, g = get_gcode_coordinates(polyline[0], layer)
+ last_pos = polyline[0]
+ if polyline_[0] == "draw" and last_state != "draw":
+ gcode += self.tool['gcode before path'] + "\n"
+ for point in polyline:
+ real_pos, g = get_gcode_coordinates(point, layer)
+ real_l = sum([(real_pos[i] - last_real_pos[i]) ** 2 for i in range(len(last_real_pos))])
+ l = (last_pos[0] - point[0]) ** 2 + (last_pos[1] - point[1]) ** 2
+ if l != 0:
+ feed = self.tool['feed'] * math.sqrt(real_l / l)
+ gcode += "G01 " + g + " F {:f}\n".format(feed)
+ if self.options.graffiti_create_preview:
+ 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)
+ last_real_pos = real_pos
+ last_pos = point[:]
+ if polyline_[0] == "draw" and last_state != "draw":
+ gcode += self.tool['gcode after path'] + "\n"
+ last_state = polyline_[0]
+ self.export_gcode(gcode, no_headers=True)
+ if self.options.graffiti_create_preview:
+ try:
+ # Draw reference points
+ for layer in self.graffiti_reference_points:
+ for point in self.graffiti_reference_points[layer]:
+ x, y = graffiti_preview_transform(point[0][0], point[0][1])
+ graffiti_preview_draw_point(x, y, (0, 255, 0, 255), radius=5)
+
+ import png
+ 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)
+ with open(os.path.join(self.options.directory, self.options.file + ".png"), 'wb') as f:
+ writer.write(f, self.graffiti_preview)
+
+ except:
+ self.error("Png module have not been found!")
+
+ def get_info_plus(self):
+ """Like get_info(), but checks some of the values"""
+ self.get_info()
+ if self.orientation_points == {}:
+ self.error("Orientation points have not been defined! A default set of orientation points has been automatically added.")
+ self.tab_orientation(self.layers[min(1, len(self.layers) - 1)])
+ self.get_info()
+ if self.tools == {}:
+ self.error("Cutting tool has not been defined! A default tool has been automatically added.")
+ self.options.tools_library_type = "default"
+ self.tab_tools_library(self.layers[min(1, len(self.layers) - 1)])
+ self.get_info()
+
+ ################################################################################
+ #
+ # Effect
+ #
+ # Main function of Gcodetools class
+ #
+ ################################################################################
+ def effect(self):
+ start_time = time.time()
+ global options
+ options = self.options
+ options.self = self
+ options.doc_root = self.document.getroot()
+
+ # define print_ function
+ global print_
+ if self.options.log_create_log:
+ try:
+ if os.path.isfile(self.options.log_filename):
+ os.remove(self.options.log_filename)
+ with open(self.options.log_filename, "a") as fhl:
+ fhl.write("""Gcodetools log file.
+Started at {}.
+{}
+""".format(time.strftime("%d.%m.%Y %H:%M:%S"), options.log_filename))
+ except:
+ print_ = lambda *x: None
+ else:
+ print_ = lambda *x: None
+
+ # This automatically calls any `tab_{tab_name_in_inx}` which in this
+ # extension is A LOT of different functions. So see all method prefixed
+ # with tab_ to find out what's supported here.
+ self.options.active_tab()
+
+ print_("------------------------------------------")
+ print_("Done in {:f} seconds".format(time.time() - start_time))
+ print_("End at {}.".format(time.strftime("%d.%m.%Y %H:%M:%S")))
+
+
+ def tab_offset(self):
+ self.get_info()
+ if self.options.offset_just_get_distance:
+ for layer in self.selected_paths:
+ if len(self.selected_paths[layer]) == 2:
+ csp1 = self.selected_paths[layer][0].path.to_superpath()
+ csp2 = self.selected_paths[layer][1].path.to_superpath()
+ dist = csp_to_csp_distance(csp1, csp2)
+ print_(dist)
+ draw_pointer(list(csp_at_t(csp1[dist[1]][dist[2] - 1], csp1[dist[1]][dist[2]], dist[3]))
+ + list(csp_at_t(csp2[dist[4]][dist[5] - 1], csp2[dist[4]][dist[5]], dist[6])), "red", "line", comment=math.sqrt(dist[0]))
+ return
+ if self.options.offset_step == 0:
+ self.options.offset_step = self.options.offset_radius
+ if self.options.offset_step * self.options.offset_radius < 0:
+ self.options.offset_step *= -1
+ time_ = time.time()
+ offsets_count = 0
+ for layer in self.selected_paths:
+ for path in self.selected_paths[layer]:
+
+ offset = self.options.offset_step / 2
+ while abs(offset) <= abs(self.options.offset_radius):
+ offset_ = csp_offset(path.path.to_superpath(), offset)
+ offsets_count += 1
+ if offset_:
+ for iii in offset_:
+ draw_csp([iii], width=1)
+ else:
+ print_("------------Reached empty offset at radius {}".format(offset))
+ break
+ offset += self.options.offset_step
+ print_()
+ print_("-----------------------------------------------------------------------------------")
+ print_("-----------------------------------------------------------------------------------")
+ print_("-----------------------------------------------------------------------------------")
+ print_()
+ print_("Done in {}".format(time.time() - time_))
+ print_("Total offsets count {}".format(offsets_count))
+
+
+if __name__ == '__main__':
+ Gcodetools().run()