diff --git a/.coveragerc b/.coveragerc index 633108269c38d6b7eccb95f86cb42d915fbb0a41..42c9476a4f0ae7db13a7c422f694e53be1073e08 100644 --- a/.coveragerc +++ b/.coveragerc @@ -6,6 +6,7 @@ omit = tests/* index.py setup.py inkex.py + other/* [report] exclude_lines = diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 77d53f0699a68d25ee5096cd4cf17a017ba92631..674ebf97bde34c592a6d09995f97a9b22a5fc236 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -9,7 +9,7 @@ stages: - coverage - deploy variables: - GIT_DEPTH: "10" + GIT_DEPTH: "1" GIT_SUBMODULE_STRATEGY: recursive diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000000000000000000000000000000000000..a661c96cc50546c2ef2099a2c251a2abcade1b1f --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "other/gcodetools"] + path = other/gcodetools + url = https://gitlab.com/inkscape/extras/extensions-gcodetools.git diff --git a/gcodetools.py b/gcodetools.py deleted file mode 100755 index ef8bdd2d10c0a70f67c38e9b3e2babaff968b78c..0000000000000000000000000000000000000000 --- a/gcodetools.py +++ /dev/null @@ -1,5930 +0,0 @@ -#!/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 - # - # to create it, use - # Mylayer = self.svg.add(Layer.new('layername')) - # - # group appears in XML as 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() diff --git a/gcodetools_about.inx b/gcodetools_about.inx deleted file mode 100644 index 72f2b99c0baedcb148599f42d49fe446fcfd0ec1..0000000000000000000000000000000000000000 --- a/gcodetools_about.inx +++ /dev/null @@ -1,52 +0,0 @@ - - - - About - ru.cnc-club.filter.gcodetools_about_no_options_no_preferences - - - - - - - - - - - - - - path - - - - - - diff --git a/gcodetools_area.inx b/gcodetools_area.inx deleted file mode 100644 index 6a500e2a4ad3d390e264499bdf517d1277483dbe..0000000000000000000000000000000000000000 --- a/gcodetools_area.inx +++ /dev/null @@ -1,133 +0,0 @@ - - - - Area - ru.cnc-club.filter.gcodetools_area_area_fill_area_artefacts_ptg - - - - 100 - -10 - 0 - - - - - - 0 - 0 - 0 - - - - - - - - 5.0 - - - - - - - - - - 1 - 4 - - - - - - - d - true - - - - - - 1 - 0.0 - true - 0.05 - - false - - - - - output.ngc - true - - /home - - 5 - - - - - - - - - - - - - - - false - - - - - - - - - - - - path - - - - - - diff --git a/gcodetools_dxf_points.inx b/gcodetools_dxf_points.inx deleted file mode 100644 index 8cd782a2bc3065274ecb5472d1e9ac9926c2c7eb..0000000000000000000000000000000000000000 --- a/gcodetools_dxf_points.inx +++ /dev/null @@ -1,79 +0,0 @@ - - - - DXF Points - ru.cnc-club.filter.gcodetools_dxfpoints_no_options - - - - - - - - - - - - - - output.ngc - true - - /home - - 5 - - - - - - - - - - - - - - - false - - - - - - - - - - - - path - - - - - - diff --git a/gcodetools_engraving.inx b/gcodetools_engraving.inx deleted file mode 100644 index daf2d4ae2d5e4e1e5065d8bd4700fcf1b8c6cdc7..0000000000000000000000000000000000000000 --- a/gcodetools_engraving.inx +++ /dev/null @@ -1,91 +0,0 @@ - - - - Engraving - ru.cnc-club.filter.gcodetools_engraving - - - - 175 - 10 - 4 - false - - - - - - 1 - 0.0 - true - 0.05 - - false - - - - - output.ngc - true - - /home - - 5 - - - - - - - - - - - - - - - false - - - - - - - - - - - - path - - - - - - diff --git a/gcodetools_graffiti.inx b/gcodetools_graffiti.inx deleted file mode 100644 index ea428b7220873a9b9d7d7731b9776ac52e669d23..0000000000000000000000000000000000000000 --- a/gcodetools_graffiti.inx +++ /dev/null @@ -1,120 +0,0 @@ - - - - Graffiti - ru.cnc-club.filter.gcodetools_graffiti_orientation - - - - 10 - 10 - (0.0;0.0) - true - true - 800 - 1000 - - - - - - - - - - - - 0 - -1 - - - - - - - - - - 1 - 0.0 - true - 0.05 - - false - - - - - output.ngc - true - - /home - - 5 - - - - - - - - - - - - - - - false - - - - - - - - - - - - path - - - - - - diff --git a/gcodetools_lathe.inx b/gcodetools_lathe.inx deleted file mode 100644 index ac486bbc9e0490b55373eec6b8671d2c23f78485..0000000000000000000000000000000000000000 --- a/gcodetools_lathe.inx +++ /dev/null @@ -1,113 +0,0 @@ - - - - Lathe - ru.cnc-club.filter.gcodetools_lathe_lathe_modify_path_ptg - - - - 10 - 1 - 1 - - - - - X - Z - - - - - 4 - - - - - 1 - 4 - - - - - - - d - true - - - - - - 1 - 0.0 - true - 0.05 - - false - - - - - output.ngc - true - - /home - - 5 - - - - - - - - - - - - - - - false - - - - - - - - - - - - path - - - - - - diff --git a/gcodetools_orientation_points.inx b/gcodetools_orientation_points.inx deleted file mode 100644 index 41aeeb617ee925043fa56b9d82fa18d4bd6851a7..0000000000000000000000000000000000000000 --- a/gcodetools_orientation_points.inx +++ /dev/null @@ -1,57 +0,0 @@ - - - - Orientation points - ru.cnc-club.filter.gcodetools_orientation_no_options_no_preferences - - - - - - - - - - 0 - -1 - - - - - - - - - - - - - - - - - path - - - - - - diff --git a/gcodetools_path_to_gcode.inx b/gcodetools_path_to_gcode.inx deleted file mode 100644 index 9c6365c847109340661d6f68fe66e44ed00b0bb3..0000000000000000000000000000000000000000 --- a/gcodetools_path_to_gcode.inx +++ /dev/null @@ -1,93 +0,0 @@ - - - - Path to Gcode - ru.cnc-club.filter.gcodetools_ptg - - - - 1 - 4 - - - - - - - d - true - - - - - - 1 - 0.0 - true - 0.05 - - false - - - - - output.ngc - true - - /home - - 5 - - - - - - - - - - - - - - - false - - - - - - - - - - - - path - - - - - - diff --git a/gcodetools_prepare_path_for_plasma.inx b/gcodetools_prepare_path_for_plasma.inx deleted file mode 100644 index 53e60353a08b98dc4ef7987f435dd8c7b22e81d6..0000000000000000000000000000000000000000 --- a/gcodetools_prepare_path_for_plasma.inx +++ /dev/null @@ -1,59 +0,0 @@ - - - - Prepare path for plasma - ru.cnc-club.filter.gcodetools_plasma-prepare-path_no_options_no_preferences - - - - true - 10 - 10 - - - - - - 10 - false - false - - - true - 10 - 140 - - - - - - - - - - - - path - - - - - - diff --git a/gcodetools_tools_library.inx b/gcodetools_tools_library.inx deleted file mode 100644 index ca78a0c138bd7a5927d272620dfc8625a3f3c9f2..0000000000000000000000000000000000000000 --- a/gcodetools_tools_library.inx +++ /dev/null @@ -1,62 +0,0 @@ - - - - Tools library - ru.cnc-club.filter.gcodetools_tools_library_no_options_no_preferences - - - - - - - - - - - - - - - - - - - - - - - - - - - - - path - - - - - - diff --git a/other/gcodetools b/other/gcodetools new file mode 160000 index 0000000000000000000000000000000000000000..3d697de9e3d30c73ef6034e50d260dbe814374b5 --- /dev/null +++ b/other/gcodetools @@ -0,0 +1 @@ +Subproject commit 3d697de9e3d30c73ef6034e50d260dbe814374b5 diff --git a/tests/data/refs/gcodetools__--active-tab__orientation__--Zsurface__0__00000000000001e-5__--Zdepth__-9__71445146547012e-17__--orientation-points-count__3.out b/tests/data/refs/gcodetools__--active-tab__orientation__--Zsurface__0__00000000000001e-5__--Zdepth__-9__71445146547012e-17__--orientation-points-count__3.out deleted file mode 100644 index 61a62cc8fa36b1f5f62593c33859ca4e98d56fec..0000000000000000000000000000000000000000 --- a/tests/data/refs/gcodetools__--active-tab__orientation__--Zsurface__0__00000000000001e-5__--Zdepth__-9__71445146547012e-17__--orientation-points-count__3.out +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - - format: png -dpi: 96 -layout-disposition: bg-el-norepeat -layout-position-anchor: tl - - (0.0; 0.0; 0.00000)(100.0; 0.0; -0.00000)(0.0; 100.0; 0.0) - - - - - - - - - - - Hello World - flow text which wraps UPPER - Multi linetextFOO - - Grouped - text - - - - - - - \ No newline at end of file diff --git a/tests/data/refs/gcodetools__06eec9617e749f35cb949d850415f68d.out b/tests/data/refs/gcodetools__06eec9617e749f35cb949d850415f68d.out deleted file mode 100644 index e2ae9a37039de031af0232e073834f0fdb5d061a..0000000000000000000000000000000000000000 --- a/tests/data/refs/gcodetools__06eec9617e749f35cb949d850415f68d.out +++ /dev/null @@ -1,30 +0,0 @@ -% -(Header) -(Generated by gcodetools from Inkscape.) -(Using default header. To add your own header create file "header" in the output dir.) -M3 -(Header end.) -G21 (All units in mm) - -(Start cutting path id: p1) -(Change tool to Default tool) - -G00 Z5.000000 -G00 X100.000000 Y400.000000 - -G01 Z-0.125000 F100.0(Penetrate) -G01 X200.000000 Y300.000000 Z-0.125000 F400.000000 -G01 X300.000000 Y400.000000 Z-0.125000 -G01 X400.000000 Y300.000000 Z-0.125000 -G00 Z5.000000 - -(End cutting path id: p1) - - -(Footer) -M5 -G00 X0.0000 Y0.0000 -M2 -(Using default footer. To add your own footer create file "footer" in the output dir.) -(end) -% \ No newline at end of file diff --git a/tests/data/refs/gcodetools__2bf3b298fa730dafb8c6fd51921078f0.out b/tests/data/refs/gcodetools__2bf3b298fa730dafb8c6fd51921078f0.out deleted file mode 100644 index 8694e4326cb414d897ce0d5f4c67b28956e5aa13..0000000000000000000000000000000000000000 --- a/tests/data/refs/gcodetools__2bf3b298fa730dafb8c6fd51921078f0.out +++ /dev/null @@ -1,40 +0,0 @@ -% -(Header) -(Generated by gcodetools from Inkscape.) -(Using default header. To add your own header create file "header" in the output dir.) -M3 -(Header end.) -G21 (All units in mm) -(Change tool to Default tool) - -G01 Z 16.000000 F 800.000000 -G01 X 100.000000 Z 16.000000 F 800.000000 -G01 X 100.000000 Z 16.000000 F 800.000000 -G01 X 100.000000 Z 401.000000 F 800.000000 -G01 X 200.000000 Z 301.000000 F 800.000000 -G01 X 300.000000 Z 401.000000 F 800.000000 -G01 X 400.000000 Z 301.000000 F 800.000000 -G01 X 400.000000 Z 16.000000 F 800.000000 -G01 X 100.000000 Z 16.000000 F 800.000000 - -(Fine cutting start) -(Calculating fine cut using Move path) - -(Fine cut 1-th cicle start) -G01 X 100.000000 Z 16.000000 F 800.000000 -G01 X 100.000000 Z 401.000000 F 800.000000 -G01 X 100.000000 Z 400.000000 F 800.000000 -G01 X 100.000000 Z 400.000000 F 800.000000 -G01 X 200.000000 Z 300.000000 F 800.000000 -G01 X 300.000000 Z 400.000000 F 800.000000 -G01 X 400.000000 Z 300.000000 F 800.000000 -G01 Z 16.000000 F 800.000000 -G01 X 100.000000 Z 16.000000 F 800.000000 - -(Footer) -M5 -G00 X0.0000 Y0.0000 -M2 -(Using default footer. To add your own footer create file "footer" in the output dir.) -(end) -% \ No newline at end of file diff --git a/tests/data/refs/gcodetools__4a9fb751baf0533eadd4d394957c966d.out b/tests/data/refs/gcodetools__4a9fb751baf0533eadd4d394957c966d.out deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/tests/test_gcodetools.py b/tests/test_gcodetools.py deleted file mode 100644 index 0575729e7a42ca0201d318b24d57635dd6b7d18a..0000000000000000000000000000000000000000 --- a/tests/test_gcodetools.py +++ /dev/null @@ -1,65 +0,0 @@ -# coding=utf-8 - -import sys -import os - -from gcodetools import Gcodetools -from inkex.tester import ComparisonMixin, InkscapeExtensionTestMixin, TestCase -from inkex.tester.filters import CompareOrderIndependentBytes - -SETTINGS = ( - '--id=p1', '--max-area-curves=100', - '--area-inkscape-radius=-10', '--area-tool-overlap=0', - '--area-fill-angle=0', '--area-fill-shift=0', '--area-fill-method=0', - '--area-fill-method=0', '--area-find-artefacts-diameter=5', - '--area-find-artefacts-action=mark with an arrow', - '--biarc-tolerance=1', '--biarc-max-split-depth=4', - '--path-to-gcode-order=subpath by subpath', - '--path-to-gcode-depth-function=d', - '--path-to-gcode-sort-paths=false', '--Zscale=1', '--Zoffset=0', - '--auto_select_paths=true', '--min-arc-radius=0.05000000074505806', - '--comment-gcode-from-properties=false', '--create-log=false', - '--add-numeric-suffix-to-filename=false', '--Zsafe=5', - '--unit=G21 (All units in mm)', '--postprocessor= ', -) -FILESET = SETTINGS + ('--directory=/home', '--filename=output.ngc',) - -class TestGcodetoolsBasic(ComparisonMixin, InkscapeExtensionTestMixin, TestCase): - stderr_protect = False - effect_class = Gcodetools - comparisons = [ - FILESET + ('--active-tab="area_fill"',), - FILESET + ('--active-tab="area"',), - FILESET + ('--active-tab="area_artefacts"',), - FILESET + ('--active-tab="dxfpoints"',), - FILESET + ('--active-tab="orientation"',), - FILESET + ('--active-tab="tools_library"',), - FILESET + ('--active-tab="lathe_modify_path"',), - FILESET + ('--active-tab="offset"',), - FILESET + ('--active-tab="plasma-prepare-path"',), - ] - compare_filters = [CompareOrderIndependentBytes()] - compare_file_extension = 'dxf' - - def test_all_comparisons(self): - """ - gcodetools tries to write to a folder and filename specified - on the command line, this needs to be handled carefully. - """ - for tab in ( - ('--active-tab="path-to-gcode"',), - #('--active-tab="engraving"',), - #('--active-tab="graffiti"',), - ('--active-tab="lathe"',), - ): - args = SETTINGS + tab + ( - '--directory={}'.format(self.tempdir), - '--filename=output.ngc', - ) - outfile = os.path.join(self.tempdir, 'output.ngc') - self.assertCompare(self.compare_file, None, args, 'output.ngc') - -class TestGcodeToolsOrientationScientific(ComparisonMixin, TestCase): - effect_class = Gcodetools - compare_file = "svg/shapes.svg" - comparisons = [("--active-tab=orientation", "--Zsurface=0.00000000000001e-5", "--Zdepth=-9.71445146547012e-17", "--orientation-points-count=3")] diff --git a/tox.ini b/tox.ini index d7803a8394e7471db5988ca4c77c097e77ae7543..3e7a040b2251213b15ea553214cbc58bff7d3ed8 100644 --- a/tox.ini +++ b/tox.ini @@ -6,7 +6,10 @@ envlist = py{36,37,38,39,310}-normal setenv = COVERAGE_FILE=.coverage-{env:TOX_ENV_NAME} commands = - pytest --cov=. --cov-report html --cov-report term {posargs} + pytest --ignore=other --cov=. --cov-report html --cov-report term {posargs} deps = -rtests/dev_requirements.txt + +[pytest] +addopts = --ignore=other --cov=. --cov-report=xml