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