diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 18:24:48 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 18:24:48 +0000 |
commit | cca66b9ec4e494c1d919bff0f71a820d8afab1fa (patch) | |
tree | 146f39ded1c938019e1ed42d30923c2ac9e86789 /share/extensions/other/gcodetools/gcodetools.py | |
parent | Initial commit. (diff) | |
download | inkscape-cca66b9ec4e494c1d919bff0f71a820d8afab1fa.tar.xz inkscape-cca66b9ec4e494c1d919bff0f71a820d8afab1fa.zip |
Adding upstream version 1.2.2.upstream/1.2.2upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rwxr-xr-x | share/extensions/other/gcodetools/gcodetools.py | 5930 |
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() |