From e36049d3d8507a09c459261e9fc1d89f32a14248 Mon Sep 17 00:00:00 2001 From: Jeff Moe Date: Sat, 21 Dec 2019 13:01:40 -0700 Subject: [PATCH] Add J-Tech Inkscape plugin --- .gitignore | 1 + equipment/laser/README.md | 24 + ...Laser_Tool_V2_2 - inkscape 9_2 version.zip | Bin 0 -> 40924 bytes equipment/laser/jtech/README.md | 6 + equipment/laser/jtech/dxf_input.inx | 37 + equipment/laser/jtech/dxf_input.py | 457 +++ equipment/laser/jtech/laser.inx | 36 + equipment/laser/jtech/laser.py | 3173 +++++++++++++++++ 8 files changed, 3734 insertions(+) create mode 100644 .gitignore create mode 100644 equipment/laser/README.md create mode 100644 equipment/laser/jtech/JTP_Laser_Tool_V2_2 - inkscape 9_2 version.zip create mode 100644 equipment/laser/jtech/README.md create mode 100644 equipment/laser/jtech/dxf_input.inx create mode 100644 equipment/laser/jtech/dxf_input.py create mode 100644 equipment/laser/jtech/laser.inx create mode 100644 equipment/laser/jtech/laser.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1377554 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.swp diff --git a/equipment/laser/README.md b/equipment/laser/README.md new file mode 100644 index 0000000..9f48e11 --- /dev/null +++ b/equipment/laser/README.md @@ -0,0 +1,24 @@ +# LASER + +Uses for a LASER to make PCBAs: + +* Cut solder paste stencil. + +* Etch PCB. + +* Serialization/Marking. + + +There are two Inkscape plugins that can be used to convert an SVG into gcode. + +* Under the `jtech` directory are Inkscape plugins from J-Tech Photonics +released under GPLv2+. +https://jtechphotonics.com/?page_id=2012 + + + +Source: + +marlin-lulzbot-laser -- Marlin firmware for TAZ 6 with J-Tech Photonics LASER. +https://code.forksand.com/forksand/marlin-lulzbot-laser + diff --git a/equipment/laser/jtech/JTP_Laser_Tool_V2_2 - inkscape 9_2 version.zip b/equipment/laser/jtech/JTP_Laser_Tool_V2_2 - inkscape 9_2 version.zip new file mode 100644 index 0000000000000000000000000000000000000000..dfaa21ba29ac6508656e24e74c972a2bd59ee5e9 GIT binary patch literal 40924 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πwWrx0VlS+OA~&=7H-JIk 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 literal 0 HcmV?d00001 diff --git a/equipment/laser/jtech/README.md b/equipment/laser/jtech/README.md new file mode 100644 index 0000000..7ed58ce --- /dev/null +++ b/equipment/laser/jtech/README.md @@ -0,0 +1,6 @@ +# J-Tech Photonics Inkscape LASER Plugin + +* Inkscape plugins from J-Tech Photonics released under GPLv2+. +https://jtechphotonics.com/?page_id=2012 +https://jtechphotonics.com/?page_id=1980 +https://jtechphotonics.com/Downloads/Inkscape/JTP_Laser_Tool_V2_2%20-%20inkscape%209_2%20version.zip diff --git a/equipment/laser/jtech/dxf_input.inx b/equipment/laser/jtech/dxf_input.inx new file mode 100644 index 0000000..1102d03 --- /dev/null +++ b/equipment/laser/jtech/dxf_input.inx @@ -0,0 +1,37 @@ + + + <_name>DXF Input + org.inkscape.input.dxf + dxf_input.py + inkex.py + + + true + 1.0 + false + ------------------------------------------------------------------------- + + Latin 1 + CP 1250 + CP 1252 + UTF 8 + + + + <_param name="inputhelp" type="description" xml:space="preserve">- AutoCAD Release 13 and newer. +- assume dxf drawing is in mm. +- assume svg drawing is in pixels, at 90 dpi. +- layers are preserved only on File->Open, not Import. +- limited support for BLOCKS, use AutoCAD Explode Blocks instead, if needed. + + + + .dxf + image/x-svgz + <_filetypename>AutoCAD DXF R13 (*.dxf) + <_filetypetooltip>Import AutoCAD's Document Exchange Format + + + diff --git a/equipment/laser/jtech/dxf_input.py b/equipment/laser/jtech/dxf_input.py new file mode 100644 index 0000000..742ed4a --- /dev/null +++ b/equipment/laser/jtech/dxf_input.py @@ -0,0 +1,457 @@ +#!/usr/bin/env python +''' +dxf_input.py - input a DXF file >= (AutoCAD Release 13 == AC1012) + +Copyright (C) 2008, 2009 Alvin Penner, penner@vaxxine.com +Copyright (C) 2009 Christian Mayer, inkscape@christianmayer.de +- thanks to Aaron Spike for inkex.py and simplestyle.py +- without which this would not have been possible + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +''' + +import inkex, simplestyle, math +from StringIO import StringIO +from urllib import quote + +def export_MTEXT(): + # mandatory group codes : (1 or 3, 10, 20) (text, x, y) + if (vals[groups['1']] or vals[groups['3']]) and vals[groups['10']] and vals[groups['20']]: + x = vals[groups['10']][0] + y = vals[groups['20']][0] + # optional group codes : (21, 40, 50) (direction, text height mm, text angle) + size = 12 # default fontsize in px + if vals[groups['40']]: + size = scale*vals[groups['40']][0] + attribs = {'x': '%f' % x, 'y': '%f' % y, 'style': 'font-size: %.1fpx; fill: %s' % (size, color)} + angle = 0 # default angle in degrees + if vals[groups['50']]: + angle = vals[groups['50']][0] + attribs.update({'transform': 'rotate (%f %f %f)' % (-angle, x, y)}) + elif vals[groups['21']]: + if vals[groups['21']][0] == 1.0: + attribs.update({'transform': 'rotate (%f %f %f)' % (-90, x, y)}) + elif vals[groups['21']][0] == -1.0: + attribs.update({'transform': 'rotate (%f %f %f)' % (90, x, y)}) + attribs.update({inkex.addNS('linespacing','sodipodi'): '125%'}) + node = inkex.etree.SubElement(layer, 'text', attribs) + text = '' + if vals[groups['3']]: + for i in range (0, len(vals[groups['3']])): + text += vals[groups['3']][i] + if vals[groups['1']]: + text += vals[groups['1']][0] + found = text.find('\P') # new line + while found > -1: + tspan = inkex.etree.SubElement(node , 'tspan', {inkex.addNS('role','sodipodi'): 'line'}) + tspan.text = text[:found] + text = text[(found+2):] + found = text.find('\P') + tspan = inkex.etree.SubElement(node , 'tspan', {inkex.addNS('role','sodipodi'): 'line'}) + tspan.text = text + +def export_POINT(): + # mandatory group codes : (10, 20) (x, y) + if vals[groups['10']] and vals[groups['20']]: + if options.gcodetoolspoints: + generate_gcodetools_point(vals[groups['10']][0], vals[groups['20']][0]) + else: + generate_ellipse(vals[groups['10']][0], vals[groups['20']][0], w/2, 0.0, 1.0, 0.0, 0.0) + +def export_LINE(): + # mandatory group codes : (10, 11, 20, 21) (x1, x2, y1, y2) + if vals[groups['10']] and vals[groups['11']] and vals[groups['20']] and vals[groups['21']]: + path = 'M %f,%f %f,%f' % (vals[groups['10']][0], vals[groups['20']][0], scale*(vals[groups['11']][0] - xmin), - scale*(vals[groups['21']][0] - ymax)) + attribs = {'d': path, 'style': style} + inkex.etree.SubElement(layer, 'path', attribs) + +def export_SPLINE(): + # mandatory group codes : (10, 20, 70) (x, y, flags) + if vals[groups['10']] and vals[groups['20']] and vals[groups['70']]: + if not (vals[groups['70']][0] & 3) and len(vals[groups['10']]) == 4 and len(vals[groups['20']]) == 4: + path = 'M %f,%f C %f,%f %f,%f %f,%f' % (vals[groups['10']][0], vals[groups['20']][0], vals[groups['10']][1], vals[groups['20']][1], vals[groups['10']][2], vals[groups['20']][2], vals[groups['10']][3], vals[groups['20']][3]) + attribs = {'d': path, 'style': style} + inkex.etree.SubElement(layer, 'path', attribs) + if not (vals[groups['70']][0] & 3) and len(vals[groups['10']]) == 3 and len(vals[groups['20']]) == 3: + path = 'M %f,%f Q %f,%f %f,%f' % (vals[groups['10']][0], vals[groups['20']][0], vals[groups['10']][1], vals[groups['20']][1], vals[groups['10']][2], vals[groups['20']][2]) + attribs = {'d': path, 'style': style} + inkex.etree.SubElement(layer, 'path', attribs) + +def export_CIRCLE(): + # mandatory group codes : (10, 20, 40) (x, y, radius) + if vals[groups['10']] and vals[groups['20']] and vals[groups['40']]: + generate_ellipse(vals[groups['10']][0], vals[groups['20']][0], scale*vals[groups['40']][0], 0.0, 1.0, 0.0, 0.0) + +def export_ARC(): + # mandatory group codes : (10, 20, 40, 50, 51) (x, y, radius, angle1, angle2) + if vals[groups['10']] and vals[groups['20']] and vals[groups['40']] and vals[groups['50']] and vals[groups['51']]: + generate_ellipse(vals[groups['10']][0], vals[groups['20']][0], scale*vals[groups['40']][0], 0.0, 1.0, vals[groups['50']][0]*math.pi/180.0, vals[groups['51']][0]*math.pi/180.0) + +def export_ELLIPSE(): + # mandatory group codes : (10, 11, 20, 21, 40, 41, 42) (xc, xm, yc, ym, width ratio, angle1, angle2) + if vals[groups['10']] and vals[groups['11']] and vals[groups['20']] and vals[groups['21']] and vals[groups['40']] and vals[groups['41']] and vals[groups['42']]: + generate_ellipse(vals[groups['10']][0], vals[groups['20']][0], scale*vals[groups['11']][0], scale*vals[groups['21']][0], vals[groups['40']][0], vals[groups['41']][0], vals[groups['42']][0]) + +def export_LEADER(): + # mandatory group codes : (10, 20) (x, y) + if vals[groups['10']] and vals[groups['20']]: + if len(vals[groups['10']]) > 1 and len(vals[groups['20']]) == len(vals[groups['10']]): + path = 'M %f,%f' % (vals[groups['10']][0], vals[groups['20']][0]) + for i in range (1, len(vals[groups['10']])): + path += ' %f,%f' % (vals[groups['10']][i], vals[groups['20']][i]) + attribs = {'d': path, 'style': style} + inkex.etree.SubElement(layer, 'path', attribs) + +def export_LWPOLYLINE(): + # mandatory group codes : (10, 20, 70) (x, y, flags) + if vals[groups['10']] and vals[groups['20']] and vals[groups['70']]: + if len(vals[groups['10']]) > 1 and len(vals[groups['20']]) == len(vals[groups['10']]): + a=seqs + if (seqs[-2]=='42' or seqs[-1]=='42') and vals[groups['70']][0]==1: + if seqs[-1]=='42': + a=seqs + a.append("10") + a.append("20") + else: + a=seqs[0:-1] + a.append("10") + a.append("20") + a.append(seqs[-1]) + vals[groups['10']].append(vals[groups['10']][0]) + vals[groups['20']].append(vals[groups['20']][0]) + # optional group codes : (42) (bulge) + iseqs = 0 + ibulge = 0 + while a[iseqs] != '20': + iseqs += 1 + path = 'M %f,%f' % (vals[groups['10']][0], vals[groups['20']][0]) + xold = vals[groups['10']][0] + yold = vals[groups['20']][0] + for i in range (1, len(vals[groups['10']])): + bulge = 0 + iseqs += 1 + while a[iseqs] != '20': + if a[iseqs] == '42': + bulge = vals[groups['42']][ibulge] + ibulge += 1 + iseqs += 1 + if bulge: + sweep = 0 # sweep CCW + if bulge < 0: + sweep = 1 # sweep CW + bulge = -bulge + large = 0 # large-arc-flag + if bulge > 1: + large = 1 + r = math.sqrt((vals[groups['10']][i] - xold)**2 + (vals[groups['20']][i] - yold)**2) + r = 0.25*r*(bulge + 1.0/bulge) + path += ' A %f,%f 0.0 %d %d %f,%f' % (r, r, large, sweep, vals[groups['10']][i], vals[groups['20']][i]) + else: + path += ' L %f,%f' % (vals[groups['10']][i], vals[groups['20']][i]) + xold = vals[groups['10']][i] + yold = vals[groups['20']][i] + if vals[groups['70']][0] == 1: # closed path + path += ' z' + attribs = {'d': path, 'style': style} + inkex.etree.SubElement(layer, 'path', attribs) + +def export_HATCH(): + # mandatory group codes : (10, 20, 70, 72, 92, 93) (x, y, fill, Edge Type, Path Type, Number of edges) + if vals[groups['10']] and vals[groups['20']] and vals[groups['70']] and vals[groups['72']] and vals[groups['92']] and vals[groups['93']]: + if vals[groups['70']][0] and len(vals[groups['10']]) > 1 and len(vals[groups['20']]) == len(vals[groups['10']]): + # optional group codes : (11, 21, 40, 50, 51, 73) (x, y, r, angle1, angle2, CCW) + i10 = 1 # count start points + i11 = 0 # count line end points + i40 = 0 # count circles + i72 = 0 # count edge type flags + path = '' + for i in range (0, len(vals[groups['93']])): + xc = vals[groups['10']][i10] + yc = vals[groups['20']][i10] + if vals[groups['72']][i72] == 2: # arc + rm = scale*vals[groups['40']][i40] + a1 = vals[groups['50']][i40] + path += 'M %f,%f ' % (xc + rm*math.cos(a1*math.pi/180.0), yc + rm*math.sin(a1*math.pi/180.0)) + else: + a1 = 0 + path += 'M %f,%f ' % (xc, yc) + for j in range(0, vals[groups['93']][i]): + if vals[groups['92']][i] & 2: # polyline + if j > 0: + path += 'L %f,%f ' % (vals[groups['10']][i10], vals[groups['20']][i10]) + if j == vals[groups['93']][i] - 1: + i72 += 1 + elif vals[groups['72']][i72] == 2: # arc + xc = vals[groups['10']][i10] + yc = vals[groups['20']][i10] + rm = scale*vals[groups['40']][i40] + a2 = vals[groups['51']][i40] + diff = (a2 - a1 + 360) % (360) + sweep = 1 - vals[groups['73']][i40] # sweep CCW + large = 0 # large-arc-flag + if diff: + path += 'A %f,%f 0.0 %d %d %f,%f ' % (rm, rm, large, sweep, xc + rm*math.cos(a2*math.pi/180.0), yc + rm*math.sin(a2*math.pi/180.0)) + else: + path += 'A %f,%f 0.0 %d %d %f,%f ' % (rm, rm, large, sweep, xc + rm*math.cos((a1+180.0)*math.pi/180.0), yc + rm*math.sin((a1+180.0)*math.pi/180.0)) + path += 'A %f,%f 0.0 %d %d %f,%f ' % (rm, rm, large, sweep, xc + rm*math.cos(a1*math.pi/180.0), yc + rm*math.sin(a1*math.pi/180.0)) + i40 += 1 + i72 += 1 + elif vals[groups['72']][i72] == 1: # line + path += 'L %f,%f ' % (scale*(vals[groups['11']][i11] - xmin), -scale*(vals[groups['21']][i11] - ymax)) + i11 += 1 + i72 += 1 + i10 += 1 + path += "z " + style = simplestyle.formatStyle({'fill': '%s' % color}) + attribs = {'d': path, 'style': style} + inkex.etree.SubElement(layer, 'path', attribs) + +def export_DIMENSION(): + # mandatory group codes : (10, 11, 13, 14, 20, 21, 23, 24) (x1..4, y1..4) + if vals[groups['10']] and vals[groups['11']] and vals[groups['13']] and vals[groups['14']] and vals[groups['20']] and vals[groups['21']] and vals[groups['23']] and vals[groups['24']]: + dx = abs(vals[groups['10']][0] - vals[groups['13']][0]) + dy = abs(vals[groups['20']][0] - vals[groups['23']][0]) + if (vals[groups['10']][0] == vals[groups['14']][0]) and dx > 0.00001: + d = dx/scale + dy = 0 + path = 'M %f,%f %f,%f' % (vals[groups['10']][0], vals[groups['20']][0], vals[groups['13']][0], vals[groups['20']][0]) + elif (vals[groups['20']][0] == vals[groups['24']][0]) and dy > 0.00001: + d = dy/scale + dx = 0 + path = 'M %f,%f %f,%f' % (vals[groups['10']][0], vals[groups['20']][0], vals[groups['10']][0], vals[groups['23']][0]) + else: + return + attribs = {'d': path, 'style': style + '; marker-start: url(#DistanceX); marker-end: url(#DistanceX)'} + inkex.etree.SubElement(layer, 'path', attribs) + x = scale*(vals[groups['11']][0] - xmin) + y = - scale*(vals[groups['21']][0] - ymax) + size = 12 # default fontsize in px + if vals[groups['3']]: + if DIMTXT.has_key(vals[groups['3']][0]): + size = scale*DIMTXT[vals[groups['3']][0]] + if size < 2: + size = 2 + attribs = {'x': '%f' % x, 'y': '%f' % y, 'style': 'font-size: %.1fpx; fill: %s' % (size, color)} + if dx == 0: + attribs.update({'transform': 'rotate (%f %f %f)' % (-90, x, y)}) + node = inkex.etree.SubElement(layer, 'text', attribs) + tspan = inkex.etree.SubElement(node , 'tspan', {inkex.addNS('role','sodipodi'): 'line'}) + tspan.text = str(float('%.2f' % d)) + +def export_INSERT(): + # mandatory group codes : (2, 10, 20) (block name, x, y) + if vals[groups['2']] and vals[groups['10']] and vals[groups['20']]: + x = vals[groups['10']][0] + y = vals[groups['20']][0] - scale*ymax + attribs = {'x': '%f' % x, 'y': '%f' % y, inkex.addNS('href','xlink'): '#' + quote(vals[groups['2']][0].encode("utf-8"))} + inkex.etree.SubElement(layer, 'use', attribs) + +def export_BLOCK(): + # mandatory group codes : (2) (block name) + if vals[groups['2']]: + global block + block = inkex.etree.SubElement(defs, 'symbol', {'id': vals[groups['2']][0]}) + +def export_ENDBLK(): + global block + block = defs # initiallize with dummy + +def export_ATTDEF(): + # mandatory group codes : (1, 2) (default, tag) + if vals[groups['1']] and vals[groups['2']]: + vals[groups['1']][0] = vals[groups['2']][0] + export_MTEXT() + +def generate_ellipse(xc, yc, xm, ym, w, a1, a2): + rm = math.sqrt(xm*xm + ym*ym) + a = math.atan2(ym, xm) + diff = (a2 - a1 + 2*math.pi) % (2*math.pi) + if abs(diff) > 0.0000001 and abs(diff - 2*math.pi) > 0.0000001: # open arc + large = 0 # large-arc-flag + if diff > math.pi: + large = 1 + xt = rm*math.cos(a1) + yt = w*rm*math.sin(a1) + x1 = xt*math.cos(a) - yt*math.sin(a) + y1 = xt*math.sin(a) + yt*math.cos(a) + xt = rm*math.cos(a2) + yt = w*rm*math.sin(a2) + x2 = xt*math.cos(a) - yt*math.sin(a) + y2 = xt*math.sin(a) + yt*math.cos(a) + path = 'M %f,%f A %f,%f %f %d 0 %f,%f' % (xc+x1, yc-y1, rm, w*rm, -180.0*a/math.pi, large, xc+x2, yc-y2) + else: # closed arc + path = 'M %f,%f A %f,%f %f 1 0 %f,%f %f,%f %f 1 0 %f,%f z' % (xc+xm, yc-ym, rm, w*rm, -180.0*a/math.pi, xc-xm, yc+ym, rm, w*rm, -180.0*a/math.pi, xc+xm, yc-ym) + attribs = {'d': path, 'style': style} + inkex.etree.SubElement(layer, 'path', attribs) + +def generate_gcodetools_point(xc, yc): + path= 'm %s,%s 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' % (xc,yc) + attribs = {'d': path, 'dxfpoint':'1', 'style': 'stroke:#ff0000;fill:#ff0000'} + inkex.etree.SubElement(layer, 'path', attribs) + +def get_line(): + return (stream.readline().strip(), stream.readline().strip()) + +def get_group(group): + line = get_line() + if line[0] == group: + return float(line[1]) + else: + return 0.0 + +# define DXF Entities and specify which Group Codes to monitor + +entities = {'MTEXT': export_MTEXT, 'TEXT': export_MTEXT, 'POINT': export_POINT, 'LINE': export_LINE, 'SPLINE': export_SPLINE, 'CIRCLE': export_CIRCLE, 'ARC': export_ARC, 'ELLIPSE': export_ELLIPSE, 'LEADER': export_LEADER, 'LWPOLYLINE': export_LWPOLYLINE, 'HATCH': export_HATCH, 'DIMENSION': export_DIMENSION, 'INSERT': export_INSERT, 'BLOCK': export_BLOCK, 'ENDBLK': export_ENDBLK, 'ATTDEF': export_ATTDEF, 'DICTIONARY': False} +groups = {'1': 0, '2': 1, '3': 2, '6': 3, '8': 4, '10': 5, '11': 6, '13': 7, '14': 8, '20': 9, '21': 10, '23': 11, '24': 12, '40': 13, '41': 14, '42': 15, '50': 16, '51': 17, '62': 18, '70': 19, '72': 20, '73': 21, '92': 22, '93': 23, '370': 24} +colors = { 1: '#FF0000', 2: '#FFFF00', 3: '#00FF00', 4: '#00FFFF', 5: '#0000FF', + 6: '#FF00FF', 8: '#414141', 9: '#808080', 12: '#BD0000', 30: '#FF7F00', + 250: '#333333', 251: '#505050', 252: '#696969', 253: '#828282', 254: '#BEBEBE', 255: '#FFFFFF'} + +parser = inkex.optparse.OptionParser(usage="usage: %prog [options] SVGfile", option_class=inkex.InkOption) +parser.add_option("--auto", action="store", type="inkbool", dest="auto", default=True) +parser.add_option("--scale", action="store", type="string", dest="scale", default="1.0") +parser.add_option("--gcodetoolspoints", action="store", type="inkbool", dest="gcodetoolspoints", default=True) +parser.add_option("--encoding", action="store", type="string", dest="input_encode", default="latin_1") +parser.add_option("--tab", action="store", type="string", dest="tab", default="Options") +parser.add_option("--inputhelp", action="store", type="string", dest="inputhelp", default="") +(options, args) = parser.parse_args(inkex.sys.argv[1:]) +doc = inkex.etree.parse(StringIO('')) +desc = inkex.etree.SubElement(doc.getroot(), 'desc', {}) +defs = inkex.etree.SubElement(doc.getroot(), 'defs', {}) +marker = inkex.etree.SubElement(defs, 'marker', {'id': 'DistanceX', 'orient': 'auto', 'refX': '0.0', 'refY': '0.0', 'style': 'overflow:visible'}) +inkex.etree.SubElement(marker, 'path', {'d': 'M 3,-3 L -3,3 M 0,-5 L 0,5', 'style': 'stroke:#000000; stroke-width:0.5'}) +stream = open(args[0], 'r') +xmax = xmin = 0.0 +ymax = 297.0 # default A4 height in mm +line = get_line() +flag = 0 # (0, 1, 2, 3) = (none, LAYER, LTYPE, DIMTXT) +layer_colors = {} # store colors by layer +layer_nodes = {} # store nodes by layer +linetypes = {} # store linetypes by name +DIMTXT = {} # store DIMENSION text sizes + +while line[0] and line[1] != 'BLOCKS': + line = get_line() + if options.auto: + if line[1] == '$EXTMIN': + xmin = get_group('10') + if line[1] == '$EXTMAX': + xmax = get_group('10') + ymax = get_group('20') + if flag == 1 and line[0] == '2': + layername = unicode(line[1], options.input_encode) + attribs = {inkex.addNS('groupmode','inkscape'): 'layer', inkex.addNS('label','inkscape'): '%s' % layername} + layer_nodes[layername] = inkex.etree.SubElement(doc.getroot(), 'g', attribs) + if flag == 2 and line[0] == '2': + linename = unicode(line[1], options.input_encode) + linetypes[linename] = [] + if flag == 3 and line[0] == '2': + stylename = unicode(line[1], options.input_encode) + if line[0] == '2' and line[1] == 'LAYER': + flag = 1 + if line[0] == '2' and line[1] == 'LTYPE': + flag = 2 + if line[0] == '2' and line[1] == 'DIMSTYLE': + flag = 3 + if flag == 1 and line[0] == '62': + layer_colors[layername] = int(line[1]) + if flag == 2 and line[0] == '49': + linetypes[linename].append(float(line[1])) + if flag == 3 and line[0] == '140': + DIMTXT[stylename] = float(line[1]) + if line[0] == '0' and line[1] == 'ENDTAB': + flag = 0 + +if options.auto: + scale = 1.0 + if xmax > xmin: + scale = 210.0/(xmax - xmin) # scale to A4 width +else: + scale = float(options.scale) # manual scale factor +desc.text = '%s - scale = %f' % (unicode(args[0], options.input_encode), scale) +scale *= 90.0/25.4 # convert from mm to pixels + +if not layer_nodes: + attribs = {inkex.addNS('groupmode','inkscape'): 'layer', inkex.addNS('label','inkscape'): '0'} + layer_nodes['0'] = inkex.etree.SubElement(doc.getroot(), 'g', attribs) + layer_colors['0'] = 7 + +for linename in linetypes.keys(): # scale the dashed lines + linetype = '' + for length in linetypes[linename]: + linetype += '%.4f,' % math.fabs(length*scale) + linetypes[linename] = 'stroke-dasharray:' + linetype + +entity = '' +block = defs # initiallize with dummy +while line[0] and line[1] != 'DICTIONARY': + line = get_line() + if entity and groups.has_key(line[0]): + seqs.append(line[0]) # list of group codes + if line[0] == '1' or line[0] == '2' or line[0] == '3' or line[0] == '6' or line[0] == '8': # text value + val = line[1].replace('\~', ' ') + val = inkex.re.sub( '\\\\A.*;', '', val) + val = inkex.re.sub( '\\\\H.*;', '', val) + val = inkex.re.sub( '\\^I', '', val) + val = inkex.re.sub( '{\\\\L', '', val) + val = inkex.re.sub( '}', '', val) + val = inkex.re.sub( '\\\\S.*;', '', val) + val = inkex.re.sub( '\\\\W.*;', '', val) + val = unicode(val, options.input_encode) + val = val.encode('unicode_escape') + val = inkex.re.sub( '\\\\\\\\U\+([0-9A-Fa-f]{4})', '\\u\\1', val) + val = val.decode('unicode_escape') + elif line[0] == '62' or line[0] == '70' or line[0] == '92' or line[0] == '93': + val = int(line[1]) + elif line[0] == '10' or line[0] == '13' or line[0] == '14': # scaled float x value + val = scale*(float(line[1]) - xmin) + elif line[0] == '20' or line[0] == '23' or line[0] == '24': # scaled float y value + val = - scale*(float(line[1]) - ymax) + else: # unscaled float value + val = float(line[1]) + vals[groups[line[0]]].append(val) + elif entities.has_key(line[1]): + if entities.has_key(entity): + if block != defs: # in a BLOCK + layer = block + elif vals[groups['8']]: # use Common Layer Name + layer = layer_nodes[vals[groups['8']][0]] + color = '#000000' # default color + if vals[groups['8']]: + if layer_colors.has_key(vals[groups['8']][0]): + if colors.has_key(layer_colors[vals[groups['8']][0]]): + color = colors[layer_colors[vals[groups['8']][0]]] + if vals[groups['62']]: # Common Color Number + if colors.has_key(vals[groups['62']][0]): + color = colors[vals[groups['62']][0]] + style = simplestyle.formatStyle({'stroke': '%s' % color, 'fill': 'none'}) + w = 0.5 # default lineweight for POINT + if vals[groups['370']]: # Common Lineweight + if vals[groups['370']][0] > 0: + w = 90.0/25.4*vals[groups['370']][0]/00.0 + if w < 0.5: + w = 0.5 + style = simplestyle.formatStyle({'stroke': '%s' % color, 'fill': 'none', 'stroke-width': '%.1f' % w}) + if vals[groups['6']]: # Common Linetype + if linetypes.has_key(vals[groups['6']][0]): + style += ';' + linetypes[vals[groups['6']][0]] + entities[entity]() + entity = line[1] + vals = [[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[],[]] + seqs = [] + +doc.write(inkex.sys.stdout) + +# vim: expandtab shiftwidth=4 tabstop=8 softtabstop=4 encoding=utf-8 textwidth=99 diff --git a/equipment/laser/jtech/laser.inx b/equipment/laser/jtech/laser.inx new file mode 100644 index 0000000..41327ee --- /dev/null +++ b/equipment/laser/jtech/laser.inx @@ -0,0 +1,36 @@ + + + <_name>J Tech Photonics Laser Tool + jtechphotonics.com + laser.py + inkex.py + + M03 + M05 + 3000 + 750 + 255 + 0 + 1 + 1 + + output.gcode + true + + + mm + in + + + + + + + path + + + + + diff --git a/equipment/laser/jtech/laser.py b/equipment/laser/jtech/laser.py new file mode 100644 index 0000000..3837e1d --- /dev/null +++ b/equipment/laser/jtech/laser.py @@ -0,0 +1,3173 @@ +#!/usr/bin/env python +""" +Modified by Jay Johnson 2015, J Tech Photonics, Inc., jtechphotonics.com +modified by Adam Polak 2014, polakiumengineering.org + +based on Copyright (C) 2009 Nick Drobchenko, nick@cnc-club.ru +based on gcode.py (C) 2007 hugomatic... +based on addnodes.py (C) 2005,2007 Aaron Spike, aaron@ekips.org +based on dots.py (C) 2005 Aaron Spike, aaron@ekips.org +based on interp.py (C) 2005 Aaron Spike, aaron@ekips.org +based on bezmisc.py (C) 2005 Aaron Spike, aaron@ekips.org +based on cubicsuperpath.py (C) 2005 Aaron Spike, aaron@ekips.org + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +""" +import inkex, simplestyle, simplepath +import cubicsuperpath, simpletransform, bezmisc + +import os +import math +import bezmisc +import re +import copy +import sys +import time +import cmath +import numpy +import codecs +import random +import gettext +_ = gettext.gettext + + +### Check if inkex has errormsg (0.46 version doesnot have one.) Could be removed later. +if "errormsg" not in dir(inkex): + inkex.errormsg = lambda msg: sys.stderr.write((unicode(msg) + "\n").encode("UTF-8")) + + +def bezierslopeatt(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)),t): + ax,ay,bx,by,cx,cy,x0,y0=bezmisc.bezierparameterize(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3))) + dx=3*ax*(t**2)+2*bx*t+cx + dy=3*ay*(t**2)+2*by*t+cy + if dx==dy==0 : + dx = 6*ax*t+2*bx + dy = 6*ay*t+2*by + if dx==dy==0 : + dx = 6*ax + dy = 6*ay + if dx==dy==0 : + print_("Slope error x = %s*t^3+%s*t^2+%s*t+%s, y = %s*t^3+%s*t^2+%s*t+%s, t = %s, dx==dy==0" % (ax,bx,cx,dx,ay,by,cy,dy,t)) + print_(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3))) + dx, dy = 1, 1 + + return dx,dy +bezmisc.bezierslopeatt = bezierslopeatt + + +def ireplace(self,old,new,count=0): + pattern = re.compile(re.escape(old),re.I) + return re.sub(pattern,new,self,count) + +################################################################################ +### +### Styles and additional parameters +### +################################################################################ + +math.pi2 = math.pi*2 +straight_tolerance = 0.0001 +straight_distance_tolerance = 0.0001 +engraving_tolerance = 0.0001 +loft_lengths_tolerance = 0.0000001 +options = {} +defaults = { +'header': """ +G90 +""", +'footer': """G1 X0 Y0 + +""" +} + +intersection_recursion_depth = 10 +intersection_tolerance = 0.00001 + +styles = { + "loft_style" : { + 'main curve': simplestyle.formatStyle({ 'stroke': '#88f', 'fill': 'none', 'stroke-width':'1', 'marker-end':'url(#Arrow2Mend)' }), + }, + "biarc_style" : { + 'biarc0': simplestyle.formatStyle({ 'stroke': '#88f', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'1' }), + 'biarc1': simplestyle.formatStyle({ 'stroke': '#8f8', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'1' }), + 'line': simplestyle.formatStyle({ 'stroke': '#f88', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'1' }), + 'area': simplestyle.formatStyle({ 'stroke': '#777', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'0.1' }), + }, + "biarc_style_dark" : { + 'biarc0': simplestyle.formatStyle({ 'stroke': '#33a', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'1' }), + 'biarc1': simplestyle.formatStyle({ 'stroke': '#3a3', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'1' }), + 'line': simplestyle.formatStyle({ 'stroke': '#a33', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'1' }), + 'area': simplestyle.formatStyle({ 'stroke': '#222', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'0.3' }), + }, + "biarc_style_dark_area" : { + 'biarc0': simplestyle.formatStyle({ 'stroke': '#33a', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'0.1' }), + 'biarc1': simplestyle.formatStyle({ 'stroke': '#3a3', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'0.1' }), + 'line': simplestyle.formatStyle({ 'stroke': '#a33', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'0.1' }), + 'area': simplestyle.formatStyle({ 'stroke': '#222', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'0.3' }), + }, + "biarc_style_i" : { + 'biarc0': simplestyle.formatStyle({ 'stroke': '#880', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'1' }), + 'biarc1': simplestyle.formatStyle({ 'stroke': '#808', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'1' }), + 'line': simplestyle.formatStyle({ 'stroke': '#088', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'1' }), + 'area': simplestyle.formatStyle({ 'stroke': '#999', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'0.3' }), + }, + "biarc_style_dark_i" : { + 'biarc0': simplestyle.formatStyle({ 'stroke': '#dd5', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'1' }), + 'biarc1': simplestyle.formatStyle({ 'stroke': '#d5d', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'1' }), + 'line': simplestyle.formatStyle({ 'stroke': '#5dd', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'1' }), + 'area': simplestyle.formatStyle({ 'stroke': '#aaa', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'0.3' }), + }, + "biarc_style_lathe_feed" : { + 'biarc0': simplestyle.formatStyle({ 'stroke': '#07f', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'.4' }), + 'biarc1': simplestyle.formatStyle({ 'stroke': '#0f7', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'.4' }), + 'line': simplestyle.formatStyle({ 'stroke': '#f44', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'.4' }), + 'area': simplestyle.formatStyle({ 'stroke': '#aaa', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'0.3' }), + }, + "biarc_style_lathe_passing feed" : { + 'biarc0': simplestyle.formatStyle({ 'stroke': '#07f', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'.4' }), + 'biarc1': simplestyle.formatStyle({ 'stroke': '#0f7', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'.4' }), + 'line': simplestyle.formatStyle({ 'stroke': '#f44', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'.4' }), + 'area': simplestyle.formatStyle({ 'stroke': '#aaa', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'0.3' }), + }, + "biarc_style_lathe_fine feed" : { + 'biarc0': simplestyle.formatStyle({ 'stroke': '#7f0', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'.4' }), + 'biarc1': simplestyle.formatStyle({ 'stroke': '#f70', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'.4' }), + 'line': simplestyle.formatStyle({ 'stroke': '#744', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'.4' }), + 'area': simplestyle.formatStyle({ 'stroke': '#aaa', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'0.3' }), + }, + "area artefact": simplestyle.formatStyle({ 'stroke': '#ff0000', 'fill': '#ffff00', 'stroke-width':'1' }), + "area artefact arrow": simplestyle.formatStyle({ 'stroke': '#ff0000', 'fill': '#ffff00', 'stroke-width':'1' }), + "dxf_points": simplestyle.formatStyle({ "stroke": "#ff0000", "fill": "#ff0000"}), + + } + + +################################################################################ +### Cubic Super Path additional functions +################################################################################ + +def csp_simple_bound(csp): + minx,miny,maxx,maxy = None,None,None,None + for subpath in csp: + for sp in subpath : + for p in sp: + minx = min(minx,p[0]) if minx!=None else p[0] + miny = min(miny,p[1]) if miny!=None else p[1] + maxx = max(maxx,p[0]) if maxx!=None else p[0] + maxy = max(maxy,p[1]) if maxy!=None else p[1] + return minx,miny,maxx,maxy + + +def csp_segment_to_bez(sp1,sp2) : + return sp1[1:]+sp2[:2] + + +def bound_to_bound_distance(sp1,sp2,sp3,sp4) : + min_dist = 1e100 + max_dist = 0 + points1 = csp_segment_to_bez(sp1,sp2) + points2 = csp_segment_to_bez(sp3,sp4) + for i in range(4) : + for j in range(4) : + min_, max_ = line_to_line_min_max_distance_2(points1[i-1], points1[i], points2[j-1], points2[j]) + min_dist = min(min_dist,min_) + max_dist = max(max_dist,max_) + print_("bound_to_bound", min_dist, max_dist) + return min_dist, max_dist + +def csp_to_point_distance(csp, p, dist_bounds = [0,1e100], tolerance=.01) : + min_dist = [1e100,0,0,0] + for j in range(len(csp)) : + for i in range(1,len(csp[j])) : + d = csp_seg_to_point_distance(csp[j][i-1],csp[j][i],p,sample_points = 5, tolerance = .01) + if d[0] < dist_bounds[0] : +# draw_pointer( list(csp_at_t(subpath[dist[2]-1],subpath[dist[2]],dist[3])) +# +list(csp_at_t(csp[dist[4]][dist[5]-1],csp[dist[4]][dist[5]],dist[6])),"red","line", comment = math.sqrt(dist[0])) + return [d[0],j,i,d[1]] + else : + if d[0] < min_dist[0] : min_dist = [d[0],j,i,d[1]] + return min_dist + +def csp_seg_to_point_distance(sp1,sp2,p,sample_points = 5, tolerance = .01) : + ax,ay,bx,by,cx,cy,dx,dy = csp_parameterize(sp1,sp2) + dx, dy = dx-p[0], dy-p[1] + if sample_points < 2 : sample_points = 2 + d = min( [(p[0]-sp1[1][0])**2 + (p[1]-sp1[1][1])**2,0.], [(p[0]-sp2[1][0])**2 + (p[1]-sp2[1][1])**2,1.] ) + for k in range(sample_points) : + t = float(k)/(sample_points-1) + i = 0 + while i==0 or abs(f)>0.000001 and i<20 : + t2,t3 = t**2,t**3 + f = (ax*t3+bx*t2+cx*t+dx)*(3*ax*t2+2*bx*t+cx) + (ay*t3+by*t2+cy*t+dy)*(3*ay*t2+2*by*t+cy) + df = (6*ax*t+2*bx)*(ax*t3+bx*t2+cx*t+dx) + (3*ax*t2+2*bx*t+cx)**2 + (6*ay*t+2*by)*(ay*t3+by*t2+cy*t+dy) + (3*ay*t2+2*by*t+cy)**2 + if df!=0 : + t = t - f/df + else : + break + i += 1 + if 0<=t<=1 : + p1 = csp_at_t(sp1,sp2,t) + d1 = (p1[0]-p[0])**2 + (p1[1]-p[1])**2 + if d1 < d[0] : + d = [d1,t] + return d + + +def csp_seg_to_csp_seg_distance(sp1,sp2,sp3,sp4, dist_bounds = [0,1e100], sample_points = 5, tolerance=.01) : + # check the ending points first + dist = csp_seg_to_point_distance(sp1,sp2,sp3[1],sample_points, tolerance) + dist += [0.] + if dist[0] <= dist_bounds[0] : return dist + d = csp_seg_to_point_distance(sp1,sp2,sp4[1],sample_points, tolerance) + if d[0]tolerance and i<30 : + #draw_pointer(csp_at_t(sp1,sp2,t1)) + f1x = 3*ax1*t12+2*bx1*t1+cx1 + f1y = 3*ay1*t12+2*by1*t1+cy1 + f2x = 3*ax2*t22+2*bx2*t2+cx2 + f2y = 3*ay2*t22+2*by2*t2+cy2 + F1[0] = 2*f1x*x + 2*f1y*y + F1[1] = -2*f2x*x - 2*f2y*y + F2[0][0] = 2*(6*ax1*t1+2*bx1)*x + 2*f1x*f1x + 2*(6*ay1*t1+2*by1)*y +2*f1y*f1y + F2[0][1] = -2*f1x*f2x - 2*f1y*f2y + F2[1][0] = -2*f2x*f1x - 2*f2y*f1y + F2[1][1] = -2*(6*ax2*t2+2*bx2)*x + 2*f2x*f2x - 2*(6*ay2*t2+2*by2)*y + 2*f2y*f2y + F2 = inv_2x2(F2) + if F2!=None : + t1 -= ( F2[0][0]*F1[0] + F2[0][1]*F1[1] ) + t2 -= ( F2[1][0]*F1[0] + F2[1][1]*F1[1] ) + t12, t13, t22, t23 = t1*t1, t1*t1*t1, t2*t2, t2*t2*t2 + x,y = ax1*t13+bx1*t12+cx1*t1+dx1 - (ax2*t23+bx2*t22+cx2*t2+dx2), ay1*t13+by1*t12+cy1*t1+dy1 - (ay2*t23+by2*t22+cy2*t2+dy2) + Flast = F + F = x*x+y*y + else : + break + i += 1 + if F < dist[0] and 0<=t1<=1 and 0<=t2<=1: + dist = [F,t1,t2] + if dist[0] <= dist_bounds[0] : + return dist + return dist + + +def csp_to_csp_distance(csp1,csp2, dist_bounds = [0,1e100], tolerance=.01) : + dist = [1e100,0,0,0,0,0,0] + for i1 in range(len(csp1)) : + for j1 in range(1,len(csp1[i1])) : + for i2 in range(len(csp2)) : + for j2 in range(1,len(csp2[i2])) : + d = csp_seg_bound_to_csp_seg_bound_max_min_distance(csp1[i1][j1-1],csp1[i1][j1],csp2[i2][j2-1],csp2[i2][j2]) + if d[0] >= dist_bounds[1] : continue + if d[1] < dist_bounds[0] : return [d[1],i1,j1,1,i2,j2,1] + d = csp_seg_to_csp_seg_distance(csp1[i1][j1-1],csp1[i1][j1],csp2[i2][j2-1],csp2[i2][j2], dist_bounds, tolerance=tolerance) + if d[0] < dist[0] : + dist = [d[0], i1,j1,d[1], i2,j2,d[2]] + if dist[0] <= dist_bounds[0] : + return dist + if dist[0] >= dist_bounds[1] : + return dist + return dist +# draw_pointer( list(csp_at_t(csp1[dist[1]][dist[2]-1],csp1[dist[1]][dist[2]],dist[3])) +# + list(csp_at_t(csp2[dist[4]][dist[5]-1],csp2[dist[4]][dist[5]],dist[6])), "#507","line") + + +def csp_split(sp1,sp2,t=.5) : + [x1,y1],[x2,y2],[x3,y3],[x4,y4] = sp1[1], sp1[2], sp2[0], sp2[1] + x12 = x1+(x2-x1)*t + y12 = y1+(y2-y1)*t + x23 = x2+(x3-x2)*t + y23 = y2+(y3-y2)*t + x34 = x3+(x4-x3)*t + y34 = y3+(y4-y3)*t + x1223 = x12+(x23-x12)*t + y1223 = y12+(y23-y12)*t + x2334 = x23+(x34-x23)*t + y2334 = y23+(y34-y23)*t + x = x1223+(x2334-x1223)*t + y = y1223+(y2334-y1223)*t + return [sp1[0],sp1[1],[x12,y12]], [[x1223,y1223],[x,y],[x2334,y2334]], [[x34,y34],sp2[1],sp2[2]] + +def csp_true_bounds(csp) : + # Finds minx,miny,maxx,maxy of the csp and return their (x,y,i,j,t) + minx = [float("inf"), 0, 0, 0] + maxx = [float("-inf"), 0, 0, 0] + miny = [float("inf"), 0, 0, 0] + maxy = [float("-inf"), 0, 0, 0] + for i in range(len(csp)): + for j in range(1,len(csp[i])): + ax,ay,bx,by,cx,cy,x0,y0 = bezmisc.bezierparameterize((csp[i][j-1][1],csp[i][j-1][2],csp[i][j][0],csp[i][j][1])) + roots = cubic_solver(0, 3*ax, 2*bx, cx) + [0,1] + for root in roots : + if type(root) is complex and abs(root.imag)<1e-10: + root = root.real + if type(root) is not complex and 0<=root<=1: + y = ay*(root**3)+by*(root**2)+cy*root+y0 + x = ax*(root**3)+bx*(root**2)+cx*root+x0 + maxx = max([x,y,i,j,root],maxx) + minx = min([x,y,i,j,root],minx) + + roots = cubic_solver(0, 3*ay, 2*by, cy) + [0,1] + for root in roots : + if type(root) is complex and root.imag==0: + root = root.real + if type(root) is not complex and 0<=root<=1: + y = ay*(root**3)+by*(root**2)+cy*root+y0 + x = ax*(root**3)+bx*(root**2)+cx*root+x0 + maxy = max([y,x,i,j,root],maxy) + miny = min([y,x,i,j,root],miny) + maxy[0],maxy[1] = maxy[1],maxy[0] + miny[0],miny[1] = miny[1],miny[0] + + return minx,miny,maxx,maxy + + +############################################################################ +### csp_segments_intersection(sp1,sp2,sp3,sp4) +### +### Returns array containig all intersections between two segmets of cubic +### super path. Results are [ta,tb], or [ta0, ta1, tb0, tb1, "Overlap"] +### where ta, tb are values of t for the intersection point. +############################################################################ +def csp_segments_intersection(sp1,sp2,sp3,sp4) : + a, b = csp_segment_to_bez(sp1,sp2), csp_segment_to_bez(sp3,sp4) + + def polish_intersection(a,b,ta,tb, tolerance = intersection_tolerance) : + ax,ay,bx,by,cx,cy,dx,dy = bezmisc.bezierparameterize(a) + ax1,ay1,bx1,by1,cx1,cy1,dx1,dy1 = bezmisc.bezierparameterize(b) + i = 0 + F, F1 = [.0,.0], [[.0,.0],[.0,.0]] + while i==0 or (abs(F[0])**2+abs(F[1])**2 > tolerance and i<10): + ta3, ta2, tb3, tb2 = ta**3, ta**2, tb**3, tb**2 + F[0] = ax*ta3+bx*ta2+cx*ta+dx-ax1*tb3-bx1*tb2-cx1*tb-dx1 + F[1] = ay*ta3+by*ta2+cy*ta+dy-ay1*tb3-by1*tb2-cy1*tb-dy1 + F1[0][0] = 3*ax *ta2 + 2*bx *ta + cx + F1[0][1] = -3*ax1*tb2 - 2*bx1*tb - cx1 + F1[1][0] = 3*ay *ta2 + 2*by *ta + cy + F1[1][1] = -3*ay1*tb2 - 2*by1*tb - cy1 + det = F1[0][0]*F1[1][1] - F1[0][1]*F1[1][0] + if det!=0 : + F1 = [ [ F1[1][1]/det, -F1[0][1]/det], [-F1[1][0]/det, F1[0][0]/det] ] + ta = ta - ( F1[0][0]*F[0] + F1[0][1]*F[1] ) + tb = tb - ( F1[1][0]*F[0] + F1[1][1]*F[1] ) + else: break + i += 1 + + return ta, tb + + + def recursion(a,b, ta0,ta1,tb0,tb1, depth_a,depth_b) : + global bezier_intersection_recursive_result + if a==b : + bezier_intersection_recursive_result += [[ta0,tb0,ta1,tb1,"Overlap"]] + return + tam, tbm = (ta0+ta1)/2, (tb0+tb1)/2 + if depth_a>0 and depth_b>0 : + a1,a2 = bez_split(a,0.5) + b1,b2 = bez_split(b,0.5) + if bez_bounds_intersect(a1,b1) : recursion(a1,b1, ta0,tam,tb0,tbm, depth_a-1,depth_b-1) + if bez_bounds_intersect(a2,b1) : recursion(a2,b1, tam,ta1,tb0,tbm, depth_a-1,depth_b-1) + if bez_bounds_intersect(a1,b2) : recursion(a1,b2, ta0,tam,tbm,tb1, depth_a-1,depth_b-1) + if bez_bounds_intersect(a2,b2) : recursion(a2,b2, tam,ta1,tbm,tb1, depth_a-1,depth_b-1) + elif depth_a>0 : + a1,a2 = bez_split(a,0.5) + if bez_bounds_intersect(a1,b) : recursion(a1,b, ta0,tam,tb0,tb1, depth_a-1,depth_b) + if bez_bounds_intersect(a2,b) : recursion(a2,b, tam,ta1,tb0,tb1, depth_a-1,depth_b) + elif depth_b>0 : + b1,b2 = bez_split(b,0.5) + if bez_bounds_intersect(a,b1) : recursion(a,b1, ta0,ta1,tb0,tbm, depth_a,depth_b-1) + if bez_bounds_intersect(a,b2) : recursion(a,b2, ta0,ta1,tbm,tb1, depth_a,depth_b-1) + else : # Both segments have been subdevided enougth. Let's get some intersections :). + intersection, t1, t2 = straight_segments_intersection([a[0]]+[a[3]],[b[0]]+[b[3]]) + if intersection : + if intersection == "Overlap" : + t1 = ( max(0,min(1,t1[0]))+max(0,min(1,t1[1])) )/2 + t2 = ( max(0,min(1,t2[0]))+max(0,min(1,t2[1])) )/2 + bezier_intersection_recursive_result += [[ta0+t1*(ta1-ta0),tb0+t2*(tb1-tb0)]] + + global bezier_intersection_recursive_result + bezier_intersection_recursive_result = [] + recursion(a,b,0.,1.,0.,1.,intersection_recursion_depth,intersection_recursion_depth) + intersections = bezier_intersection_recursive_result + for i in range(len(intersections)) : + if len(intersections[i])<5 or intersections[i][4] != "Overlap" : + intersections[i] = polish_intersection(a,b,intersections[i][0],intersections[i][1]) + return intersections + + +def csp_segments_true_intersection(sp1,sp2,sp3,sp4) : + intersections = csp_segments_intersection(sp1,sp2,sp3,sp4) + res = [] + for intersection in intersections : + if ( + (len(intersection)==5 and intersection[4] == "Overlap" and (0<=intersection[0]<=1 or 0<=intersection[1]<=1) and (0<=intersection[2]<=1 or 0<=intersection[3]<=1) ) + or ( 0<=intersection[0]<=1 and 0<=intersection[1]<=1 ) + ) : + res += [intersection] + return res + + +def csp_get_t_at_curvature(sp1,sp2,c, sample_points = 16): + # returns a list containning [t1,t2,t3,...,tn], 0<=ti<=1... + if sample_points < 2 : sample_points = 2 + tolerance = .0000000001 + res = [] + ax,ay,bx,by,cx,cy,dx,dy = csp_parameterize(sp1,sp2) + for k in range(sample_points) : + t = float(k)/(sample_points-1) + i, F = 0, 1e100 + while i<2 or abs(F)>tolerance and i<17 : + try : # some numerical calculation could exceed the limits + t2 = t*t + #slopes... + f1x = 3*ax*t2+2*bx*t+cx + f1y = 3*ay*t2+2*by*t+cy + f2x = 6*ax*t+2*bx + f2y = 6*ay*t+2*by + f3x = 6*ax + f3y = 6*ay + d = (f1x**2+f1y**2)**1.5 + F1 = ( + ( (f1x*f3y-f3x*f1y)*d - (f1x*f2y-f2x*f1y)*3.*(f2x*f1x+f2y*f1y)*((f1x**2+f1y**2)**.5) ) / + ((f1x**2+f1y**2)**3) + ) + F = (f1x*f2y-f1y*f2x)/d - c + t -= F/F1 + except: + break + i += 1 + if 0<=t<=1 and F<=tolerance: + if len(res) == 0 : + res.append(t) + for i in res : + if abs(t-i)<=0.001 : + break + if not abs(t-i)<=0.001 : + res.append(t) + return res + + +def csp_max_curvature(sp1,sp2): + ax,ay,bx,by,cx,cy,dx,dy = csp_parameterize(sp1,sp2) + tolerance = .0001 + F = 0. + i = 0 + while i<2 or F-Flast 0 : return 1e100 + if t1 < 0 : return -1e100 + # Use the Lapitals rule to solve 0/0 problem for 2 times... + t1 = 2*(bx*ay-ax*by)*t+(ay*cx-ax*cy) + if t1 > 0 : return 1e100 + if t1 < 0 : return -1e100 + t1 = bx*ay-ax*by + if t1 > 0 : return 1e100 + if t1 < 0 : return -1e100 + if depth>0 : + # little hack ;^) hope it wont influence anything... + return csp_curvature_at_t(sp1,sp2,t*1.004, depth-1) + return 1e100 + + +def csp_curvature_radius_at_t(sp1,sp2,t) : + c = csp_curvature_at_t(sp1,sp2,t) + if c == 0 : return 1e100 + else: return 1/c + + +def csp_special_points(sp1,sp2) : + # special points = curvature == 0 + ax,ay,bx,by,cx,cy,dx,dy = bezmisc.bezierparameterize((sp1[1],sp1[2],sp2[0],sp2[1])) + a = 3*ax*by-3*ay*bx + b = 3*ax*cy-3*cx*ay + c = bx*cy-cx*by + roots = cubic_solver(0, a, b, c) + res = [] + for i in roots : + if type(i) is complex and i.imag==0: + i = i.real + if type(i) is not complex and 0<=i<=1: + res.append(i) + return res + + +def csp_subpath_ccw(subpath): + # Remove all zerro length segments + s = 0 + #subpath = subpath[:] + if (P(subpath[-1][1])-P(subpath[0][1])).l2() > 1e-10 : + subpath[-1][2] = subpath[-1][1] + subpath[0][0] = subpath[0][1] + subpath += [ [subpath[0][1],subpath[0][1],subpath[0][1]] ] + pl = subpath[-1][2] + for sp1 in subpath: + for p in sp1 : + s += (p[0]-pl[0])*(p[1]+pl[1]) + pl = p + return s<0 + + +def csp_at_t(sp1,sp2,t): + ax,bx,cx,dx = sp1[1][0], sp1[2][0], sp2[0][0], sp2[1][0] + ay,by,cy,dy = sp1[1][1], sp1[2][1], sp2[0][1], sp2[1][1] + + x1, y1 = ax+(bx-ax)*t, ay+(by-ay)*t + x2, y2 = bx+(cx-bx)*t, by+(cy-by)*t + x3, y3 = cx+(dx-cx)*t, cy+(dy-cy)*t + + x4,y4 = x1+(x2-x1)*t, y1+(y2-y1)*t + x5,y5 = x2+(x3-x2)*t, y2+(y3-y2)*t + + x,y = x4+(x5-x4)*t, y4+(y5-y4)*t + return [x,y] + + +def csp_splitatlength(sp1, sp2, l = 0.5, tolerance = 0.01): + bez = (sp1[1][:],sp1[2][:],sp2[0][:],sp2[1][:]) + t = bezmisc.beziertatlength(bez, l, tolerance) + return csp_split(sp1, sp2, t) + + +def cspseglength(sp1,sp2, tolerance = 0.001): + bez = (sp1[1][:],sp1[2][:],sp2[0][:],sp2[1][:]) + return bezmisc.bezierlength(bez, tolerance) + + +def csplength(csp): + total = 0 + lengths = [] + for sp in csp: + for i in xrange(1,len(sp)): + l = cspseglength(sp[i-1],sp[i]) + lengths.append(l) + total += l + return lengths, total + + +def csp_segments(csp): + l, seg = 0, [0] + for sp in csp: + for i in xrange(1,len(sp)): + l += cspseglength(sp[i-1],sp[i]) + seg += [ l ] + + if l>0 : + seg = [seg[i]/l for i in xrange(len(seg))] + return seg,l + + +def rebuild_csp (csp, segs, s=None): + # rebuild_csp() adds to csp control points making it's segments looks like segs + if s==None : s, l = csp_segments(csp) + + if len(s)>len(segs) : return None + segs = segs[:] + segs.sort() + for i in xrange(len(s)): + d = None + for j in xrange(len(segs)): + d = min( [abs(s[i]-segs[j]),j], d) if d!=None else [abs(s[i]-segs[j]),j] + del segs[d[1]] + for i in xrange(len(segs)): + for j in xrange(0,len(s)): + if segs[i]t2 : t1, t2 = t2, t1 + if t1 == t2 : + sp1,sp2,sp3 = csp_split(sp1,sp2,t) + return [sp1,sp2,sp2,sp3] + elif t1 <= 1e-10 and t2 >= 1.-1e-10 : + return [sp1,sp1,sp2,sp2] + elif t1 <= 1e-10: + sp1,sp2,sp3 = csp_split(sp1,sp2,t2) + return [sp1,sp1,sp2,sp3] + elif t2 >= 1.-1e-10 : + sp1,sp2,sp3 = csp_split(sp1,sp2,t1) + return [sp1,sp2,sp3,sp3] + else: + sp1,sp2,sp3 = csp_split(sp1,sp2,t1) + sp2,sp3,sp4 = csp_split(sp2,sp3,(t2-t1)/(1-t1) ) + return [sp1,sp2,sp3,sp4] + + +def csp_subpath_split_by_points(subpath, points) : + # points are [[i,t]...] where i-segment's number + points.sort() + points = [[1,0.]] + points + [[len(subpath)-1,1.]] + parts = [] + for int1,int2 in zip(points,points[1:]) : + if int1==int2 : + continue + if int1[1] == 1. : + int1[0] += 1 + int1[1] = 0. + if int1==int2 : + continue + if int2[1] == 0. : + int2[0] -= 1 + int2[1] = 1. + if int1[0] == 0 and int2[0]==len(subpath)-1:# and small(int1[1]) and small(int2[1]-1) : + continue + if int1[0]==int2[0] : # same segment + sp = csp_split_by_two_points(subpath[int1[0]-1],subpath[int1[0]],int1[1], int2[1]) + if sp[1]!=sp[2] : + parts += [ [sp[1],sp[2]] ] + else : + sp5,sp1,sp2 = csp_split(subpath[int1[0]-1],subpath[int1[0]],int1[1]) + sp3,sp4,sp5 = csp_split(subpath[int2[0]-1],subpath[int2[0]],int2[1]) + if int1[0]==int2[0]-1 : + parts += [ [sp1, [sp2[0],sp2[1],sp3[2]], sp4] ] + else : + parts += [ [sp1,sp2]+subpath[int1[0]+1:int2[0]-1]+[sp3,sp4] ] + return parts + + +def csp_from_arc(start, end, center, r, slope_st) : + # Creates csp that approximise specified arc + r = abs(r) + alpha = (atan2(end[0]-center[0],end[1]-center[1]) - atan2(start[0]-center[0],start[1]-center[1])) % math.pi2 + + sectors = int(abs(alpha)*2/math.pi)+1 + alpha_start = atan2(start[0]-center[0],start[1]-center[1]) + cos_,sin_ = math.cos(alpha_start), math.sin(alpha_start) + k = (4.*math.tan(alpha/sectors/4.)/3.) + if dot(slope_st , [- sin_*k*r, cos_*k*r]) < 0 : + if alpha>0 : alpha -= math.pi2 + else: alpha += math.pi2 + if abs(alpha*r)<0.001 : + return [] + + sectors = int(abs(alpha)*2/math.pi)+1 + k = (4.*math.tan(alpha/sectors/4.)/3.) + result = [] + for i in range(sectors+1) : + cos_,sin_ = math.cos(alpha_start + alpha*i/sectors), math.sin(alpha_start + alpha*i/sectors) + sp = [ [], [center[0] + cos_*r, center[1] + sin_*r], [] ] + sp[0] = [sp[1][0] + sin_*k*r, sp[1][1] - cos_*k*r ] + sp[2] = [sp[1][0] - sin_*k*r, sp[1][1] + cos_*k*r ] + result += [sp] + result[0][0] = result[0][1][:] + result[-1][2] = result[-1][1] + + return result + + +def point_to_arc_distance(p, arc): + ### Distance calculattion from point to arc + P0,P2,c,a = arc + dist = None + p = P(p) + r = (P0-c).mag() + if r>0 : + i = c + (p-c).unit()*r + alpha = ((i-c).angle() - (P0-c).angle()) + if a*alpha<0: + if alpha>0: alpha = alpha-math.pi2 + else: alpha = math.pi2+alpha + if between(alpha,0,a) or min(abs(alpha),abs(alpha-a))tolerance and i<4): + i += 1 + dl = d1*1 + for j in range(n+1): + t = float(j)/n + p = csp_at_t(sp1,sp2,t) + d = min(point_to_arc_distance(p,arc1), point_to_arc_distance(p,arc2)) + d1 = max(d1,d) + n=n*2 + return d1[0] + + +def csp_simple_bound_to_point_distance(p, csp): + minx,miny,maxx,maxy = None,None,None,None + for subpath in csp: + for sp in subpath: + for p_ in sp: + minx = min(minx,p_[0]) if minx!=None else p_[0] + miny = min(miny,p_[1]) if miny!=None else p_[1] + maxx = max(maxx,p_[0]) if maxx!=None else p_[0] + maxy = max(maxy,p_[1]) if maxy!=None else p_[1] + return math.sqrt(max(minx-p[0],p[0]-maxx,0)**2+max(miny-p[1],p[1]-maxy,0)**2) + + +def csp_point_inside_bound(sp1, sp2, p): + bez = [sp1[1],sp1[2],sp2[0],sp2[1]] + x,y = p + c = 0 + for i in range(4): + [x0,y0], [x1,y1] = bez[i-1], bez[i] + if x0-x1!=0 and (y-y0)*(x1-x0)>=(x-x0)*(y1-y0) and x>min(x0,x1) and x<=max(x0,x1) : + c +=1 + return c%2==0 + + +def csp_bound_to_point_distance(sp1, sp2, p): + if csp_point_inside_bound(sp1, sp2, p) : + return 0. + bez = csp_segment_to_bez(sp1,sp2) + min_dist = 1e100 + for i in range(0,4): + d = point_to_line_segment_distance_2(p, bez[i-1],bez[i]) + if d <= min_dist : min_dist = d + return min_dist + + +def line_line_intersect(p1,p2,p3,p4) : # Return only true intersection. + if (p1[0]==p2[0] and p1[1]==p2[1]) or (p3[0]==p4[0] and p3[1]==p4[1]) : return False + x = (p2[0]-p1[0])*(p4[1]-p3[1]) - (p2[1]-p1[1])*(p4[0]-p3[0]) + if x==0 : # Lines are parallel + if (p3[0]-p1[0])*(p2[1]-p1[1]) == (p3[1]-p1[1])*(p2[0]-p1[0]) : + if p3[0]!=p4[0] : + t11 = (p1[0]-p3[0])/(p4[0]-p3[0]) + t12 = (p2[0]-p3[0])/(p4[0]-p3[0]) + t21 = (p3[0]-p1[0])/(p2[0]-p1[0]) + t22 = (p4[0]-p1[0])/(p2[0]-p1[0]) + else: + t11 = (p1[1]-p3[1])/(p4[1]-p3[1]) + t12 = (p2[1]-p3[1])/(p4[1]-p3[1]) + t21 = (p3[1]-p1[1])/(p2[1]-p1[1]) + t22 = (p4[1]-p1[1])/(p2[1]-p1[1]) + return ("Overlap" if (0<=t11<=1 or 0<=t12<=1) and (0<=t21<=1 or 0<=t22<=1) else False) + else: return False + else : + return ( + 0<=((p4[0]-p3[0])*(p1[1]-p3[1]) - (p4[1]-p3[1])*(p1[0]-p3[0]))/x<=1 and + 0<=((p2[0]-p1[0])*(p1[1]-p3[1]) - (p2[1]-p1[1])*(p1[0]-p3[0]))/x<=1 ) + + +def line_line_intersection_points(p1,p2,p3,p4) : # Return only points [ (x,y) ] + if (p1[0]==p2[0] and p1[1]==p2[1]) or (p3[0]==p4[0] and p3[1]==p4[1]) : return [] + x = (p2[0]-p1[0])*(p4[1]-p3[1]) - (p2[1]-p1[1])*(p4[0]-p3[0]) + if x==0 : # Lines are parallel + if (p3[0]-p1[0])*(p2[1]-p1[1]) == (p3[1]-p1[1])*(p2[0]-p1[0]) : + if p3[0]!=p4[0] : + t11 = (p1[0]-p3[0])/(p4[0]-p3[0]) + t12 = (p2[0]-p3[0])/(p4[0]-p3[0]) + t21 = (p3[0]-p1[0])/(p2[0]-p1[0]) + t22 = (p4[0]-p1[0])/(p2[0]-p1[0]) + else: + t11 = (p1[1]-p3[1])/(p4[1]-p3[1]) + t12 = (p2[1]-p3[1])/(p4[1]-p3[1]) + t21 = (p3[1]-p1[1])/(p2[1]-p1[1]) + t22 = (p4[1]-p1[1])/(p2[1]-p1[1]) + res = [] + if (0<=t11<=1 or 0<=t12<=1) and (0<=t21<=1 or 0<=t22<=1) : + if 0<=t11<=1 : res += [p1] + if 0<=t12<=1 : res += [p2] + if 0<=t21<=1 : res += [p3] + if 0<=t22<=1 : res += [p4] + return res + else: return [] + else : + t1 = ((p4[0]-p3[0])*(p1[1]-p3[1]) - (p4[1]-p3[1])*(p1[0]-p3[0]))/x + t2 = ((p2[0]-p1[0])*(p1[1]-p3[1]) - (p2[1]-p1[1])*(p1[0]-p3[0]))/x + if 0<=t1<=1 and 0<=t2<=1 : return [ [p1[0]*(1-t1)+p2[0]*t1, p1[1]*(1-t1)+p2[1]*t1] ] + else : return [] + + +def point_to_point_d2(a,b): + return (a[0]-b[0])**2 + (a[1]-b[1])**2 + + +def point_to_point_d(a,b): + return math.sqrt((a[0]-b[0])**2 + (a[1]-b[1])**2) + + +def point_to_line_segment_distance_2(p1, p2,p3) : + # p1 - point, p2,p3 - line segment + #draw_pointer(p1) + w0 = [p1[0]-p2[0], p1[1]-p2[1]] + v = [p3[0]-p2[0], p3[1]-p2[1]] + c1 = w0[0]*v[0] + w0[1]*v[1] + if c1 <= 0 : + return w0[0]*w0[0]+w0[1]*w0[1] + c2 = v[0]*v[0] + v[1]*v[1] + if c2 <= c1 : + return (p1[0]-p3[0])**2 + (p1[1]-p3[1])**2 + return (p1[0]- p2[0]-v[0]*c1/c2)**2 + (p1[1]- p2[1]-v[1]*c1/c2) + + +def line_to_line_distance_2(p1,p2,p3,p4): + if line_line_intersect(p1,p2,p3,p4) : return 0 + return min( + point_to_line_segment_distance_2(p1,p3,p4), + point_to_line_segment_distance_2(p2,p3,p4), + point_to_line_segment_distance_2(p3,p1,p2), + point_to_line_segment_distance_2(p4,p1,p2)) + + +def csp_seg_bound_to_csp_seg_bound_max_min_distance(sp1,sp2,sp3,sp4) : + bez1 = csp_segment_to_bez(sp1,sp2) + bez2 = csp_segment_to_bez(sp3,sp4) + min_dist = 1e100 + max_dist = 0. + for i in range(4) : + if csp_point_inside_bound(sp1, sp2, bez2[i]) or csp_point_inside_bound(sp3, sp4, bez1[i]) : + min_dist = 0. + break + for i in range(4) : + for j in range(4) : + d = line_to_line_distance_2(bez1[i-1],bez1[i],bez2[j-1],bez2[j]) + if d < min_dist : min_dist = d + d = (bez2[j][0]-bez1[i][0])**2 + (bez2[j][1]-bez1[i][1])**2 + if max_dist < d : max_dist = d + return min_dist, max_dist + + +def csp_reverse(csp) : + for i in range(len(csp)) : + n = [] + for j in csp[i] : + n = [ [j[2][:],j[1][:],j[0][:]] ] + n + csp[i] = n[:] + return csp + + +def csp_normalized_slope(sp1,sp2,t) : + ax,ay,bx,by,cx,cy,dx,dy=bezmisc.bezierparameterize((sp1[1][:],sp1[2][:],sp2[0][:],sp2[1][:])) + if sp1[1]==sp2[1]==sp1[2]==sp2[0] : return [1.,0.] + f1x = 3*ax*t*t+2*bx*t+cx + f1y = 3*ay*t*t+2*by*t+cy + if abs(f1x*f1x+f1y*f1y) > 1e-20 : + l = math.sqrt(f1x*f1x+f1y*f1y) + return [f1x/l, f1y/l] + + if t == 0 : + f1x = sp2[0][0]-sp1[1][0] + f1y = sp2[0][1]-sp1[1][1] + if abs(f1x*f1x+f1y*f1y) > 1e-20 : + l = math.sqrt(f1x*f1x+f1y*f1y) + return [f1x/l, f1y/l] + else : + f1x = sp2[1][0]-sp1[1][0] + f1y = sp2[1][1]-sp1[1][1] + if f1x*f1x+f1y*f1y != 0 : + l = math.sqrt(f1x*f1x+f1y*f1y) + return [f1x/l, f1y/l] + elif t == 1 : + f1x = sp2[1][0]-sp1[2][0] + f1y = sp2[1][1]-sp1[2][1] + if abs(f1x*f1x+f1y*f1y) > 1e-20 : + l = math.sqrt(f1x*f1x+f1y*f1y) + return [f1x/l, f1y/l] + else : + f1x = sp2[1][0]-sp1[1][0] + f1y = sp2[1][1]-sp1[1][1] + if f1x*f1x+f1y*f1y != 0 : + l = math.sqrt(f1x*f1x+f1y*f1y) + return [f1x/l, f1y/l] + else : + return [1.,0.] + + +def csp_normalized_normal(sp1,sp2,t) : + nx,ny = csp_normalized_slope(sp1,sp2,t) + return [-ny, nx] + + +def csp_parameterize(sp1,sp2): + return bezmisc.bezierparameterize(csp_segment_to_bez(sp1,sp2)) + + +def csp_concat_subpaths(*s): + + def concat(s1,s2) : + if s1 == [] : return s2 + if s2 == [] : return s1 + if (s1[-1][1][0]-s2[0][1][0])**2 + (s1[-1][1][1]-s2[0][1][1])**2 > 0.00001 : + return s1[:-1]+[ [s1[-1][0],s1[-1][1],s1[-1][1]], [s2[0][1],s2[0][1],s2[0][2]] ] + s2[1:] + else : + return s1[:-1]+[ [s1[-1][0],s2[0][1],s2[0][2]] ] + s2[1:] + + if len(s) == 0 : return [] + if len(s) ==1 : return s[0] + result = s[0] + for s1 in s[1:]: + result = concat(result,s1) + return result + + +def csp_draw(csp, color="#05f", group = None, style="fill:none;", width = .1, comment = "") : + if csp!=[] and csp!=[[]] : + if group == None : group = options.doc_root + style += "stroke:"+color+";"+ "stroke-width:%0.4fpx;"%width + args = {"d": cubicsuperpath.formatPath(csp), "style":style} + if comment!="" : args["comment"] = str(comment) + inkex.etree.SubElement( group, inkex.addNS('path','svg'), args ) + + +def csp_subpaths_end_to_start_distance2(s1,s2): + return (s1[-1][1][0]-s2[0][1][0])**2 + (s1[-1][1][1]-s2[0][1][1])**2 + + +def csp_clip_by_line(csp,l1,l2) : + result = [] + for i in range(len(csp)): + s = csp[i] + intersections = [] + for j in range(1,len(s)) : + intersections += [ [j,int_] for int_ in csp_line_intersection(l1,l2,s[j-1],s[j])] + splitted_s = csp_subpath_split_by_points(s, intersections) + for s in splitted_s[:] : + clip = False + for p in csp_true_bounds([s]) : + if (l1[1]-l2[1])*p[0] + (l2[0]-l1[0])*p[1] + (l1[0]*l2[1]-l2[0]*l1[1])<-0.01 : + clip = True + break + if clip : + splitted_s.remove(s) + result += splitted_s + return result + + +def csp_subpath_line_to(subpath, points) : + # Appends subpath with line or polyline. + if len(points)>0 : + if len(subpath)>0: + subpath[-1][2] = subpath[-1][1][:] + if type(points[0]) == type([1,1]) : + for p in points : + subpath += [ [p[:],p[:],p[:]] ] + else: + subpath += [ [points,points,points] ] + return subpath + + +def csp_join_subpaths(csp) : + result = csp[:] + done_smf = True + joined_result = [] + while done_smf : + done_smf = False + while len(result)>0: + s1 = result[-1][:] + del(result[-1]) + j = 0 + joined_smf = False + while j0, abc*bcd>0, abc*cad>0 + if m1 and m2 and m3 : return [a,b,c] + if m1 and m2 and not m3 : return [a,b,c,d] + if m1 and not m2 and m3 : return [a,b,d,c] + if not m1 and m2 and m3 : return [a,d,b,c] + if m1 and not (m2 and m3) : return [a,b,d] + if not (m1 and m2) and m3 : return [c,a,d] + if not (m1 and m3) and m2 : return [b,c,d] + + raise ValueError, "csp_segment_convex_hull happend something that shouldnot happen!" + + +################################################################################ +### Bezier additional functions +################################################################################ + +def bez_bounds_intersect(bez1, bez2) : + return bounds_intersect(bez_bound(bez2), bez_bound(bez1)) + + +def bez_bound(bez) : + return [ + min(bez[0][0], bez[1][0], bez[2][0], bez[3][0]), + min(bez[0][1], bez[1][1], bez[2][1], bez[3][1]), + max(bez[0][0], bez[1][0], bez[2][0], bez[3][0]), + max(bez[0][1], bez[1][1], bez[2][1], bez[3][1]), + ] + + +def bounds_intersect(a, b) : + return not ( (a[0]>b[2]) or (b[0]>a[2]) or (a[1]>b[3]) or (b[1]>a[3]) ) + + +def tpoint((x1,y1),(x2,y2),t): + return [x1+t*(x2-x1),y1+t*(y2-y1)] + + +def bez_to_csp_segment(bez) : + return [bez[0],bez[0],bez[1]], [bez[2],bez[3],bez[3]] + + +def bez_split(a,t=0.5) : + a1 = tpoint(a[0],a[1],t) + at = tpoint(a[1],a[2],t) + b2 = tpoint(a[2],a[3],t) + a2 = tpoint(a1,at,t) + b1 = tpoint(b2,at,t) + a3 = tpoint(a2,b1,t) + return [a[0],a1,a2,a3], [a3,b1,b2,a[3]] + + +def bez_at_t(bez,t) : + return csp_at_t([bez[0],bez[0],bez[1]],[bez[2],bez[3],bez[3]],t) + + +def bez_to_point_distance(bez,p,needed_dist=[0.,1e100]): + # returns [d^2,t] + return csp_seg_to_point_distance(bez_to_csp_segment(bez),p,needed_dist) + + +def bez_normalized_slope(bez,t): + return csp_normalized_slope([bez[0],bez[0],bez[1]], [bez[2],bez[3],bez[3]],t) + +################################################################################ +### Some vector functions +################################################################################ + +def normalize((x,y)) : + l = math.sqrt(x**2+y**2) + if l == 0 : return [0.,0.] + else : return [x/l, y/l] + + +def cross(a,b) : + return a[1] * b[0] - a[0] * b[1] + + +def dot(a,b) : + return a[0] * b[0] + a[1] * b[1] + + +def rotate_ccw(d) : + return [-d[1],d[0]] + + +def vectors_ccw(a,b): + return a[0]*b[1]-b[0]*a[1] < 0 + + +def vector_from_to_length(a,b): + return math.sqrt((a[0]-b[0])*(a[0]-b[0]) + (a[1]-b[1])*(a[1]-b[1])) + +################################################################################ +### Common functions +################################################################################ + +def matrix_mul(a,b) : + return [ [ sum([a[i][k]*b[k][j] for k in range(len(a[0])) ]) for j in range(len(b[0]))] for i in range(len(a))] + try : + return [ [ sum([a[i][k]*b[k][j] for k in range(len(a[0])) ]) for j in range(len(b[0]))] for i in range(len(a))] + except : + return None + + +def transpose(a) : + try : + return [ [ a[i][j] for i in range(len(a)) ] for j in range(len(a[0])) ] + except : + return None + + +def det_3x3(a): + return float( + a[0][0]*a[1][1]*a[2][2] + a[0][1]*a[1][2]*a[2][0] + a[1][0]*a[2][1]*a[0][2] + - a[0][2]*a[1][1]*a[2][0] - a[0][0]*a[2][1]*a[1][2] - a[0][1]*a[2][2]*a[1][0] + ) + + +def inv_3x3(a): # invert matrix 3x3 + det = det_3x3(a) + if det==0: return None + return [ + [ (a[1][1]*a[2][2] - a[2][1]*a[1][2])/det, -(a[0][1]*a[2][2] - a[2][1]*a[0][2])/det, (a[0][1]*a[1][2] - a[1][1]*a[0][2])/det ], + [ -(a[1][0]*a[2][2] - a[2][0]*a[1][2])/det, (a[0][0]*a[2][2] - a[2][0]*a[0][2])/det, -(a[0][0]*a[1][2] - a[1][0]*a[0][2])/det ], + [ (a[1][0]*a[2][1] - a[2][0]*a[1][1])/det, -(a[0][0]*a[2][1] - a[2][0]*a[0][1])/det, (a[0][0]*a[1][1] - a[1][0]*a[0][1])/det ] + ] + + +def inv_2x2(a): # invert matrix 2x2 + det = a[0][0]*a[1][1] - a[1][0]*a[0][1] + if det==0: return None + return [ + [a[1][1]/det, -a[0][1]/det], + [-a[1][0]/det, a[0][0]/det] + ] + + +def small(a) : + global small_tolerance + return abs(a)=0 : + t = m+math.sqrt(n) + m1 = pow(t/2,1./3) if t>=0 else -pow(-t/2,1./3) + t = m-math.sqrt(n) + n1 = pow(t/2,1./3) if t>=0 else -pow(-t/2,1./3) + else : + m1 = pow(complex((m+cmath.sqrt(n))/2),1./3) + n1 = pow(complex((m-cmath.sqrt(n))/2),1./3) + x1 = -1./3 * (a + m1 + n1) + x2 = -1./3 * (a + w1*m1 + w2*n1) + x3 = -1./3 * (a + w2*m1 + w1*n1) + return [x1,x2,x3] + elif b!=0: + det = c**2-4*b*d + if det>0 : + return [(-c+math.sqrt(det))/(2*b),(-c-math.sqrt(det))/(2*b)] + elif d == 0 : + return [-c/(b*b)] + else : + return [(-c+cmath.sqrt(det))/(2*b),(-c-cmath.sqrt(det))/(2*b)] + elif c!=0 : + return [-d/c] + else : return [] + + +################################################################################ +### print_ prints any arguments into specified log file +################################################################################ + +def print_(*arg): + f = open(options.log_filename,"a") + for s in arg : + s = str(unicode(s).encode('unicode_escape'))+" " + f.write( s ) + f.write("\n") + f.close() + + +################################################################################ +### Point (x,y) operations +################################################################################ +class P: + def __init__(self, x, y=None): + if not y==None: + self.x, self.y = float(x), float(y) + else: + self.x, self.y = float(x[0]), float(x[1]) + def __add__(self, other): return P(self.x + other.x, self.y + other.y) + def __sub__(self, other): return P(self.x - other.x, self.y - other.y) + def __neg__(self): return P(-self.x, -self.y) + def __mul__(self, other): + if isinstance(other, P): + return self.x * other.x + self.y * other.y + return P(self.x * other, self.y * other) + __rmul__ = __mul__ + def __div__(self, other): return P(self.x / other, self.y / other) + def mag(self): return math.hypot(self.x, self.y) + def unit(self): + h = self.mag() + if h: return self / h + else: return P(0,0) + def dot(self, other): return self.x * other.x + self.y * other.y + def rot(self, theta): + c = math.cos(theta) + s = math.sin(theta) + return P(self.x * c - self.y * s, self.x * s + self.y * c) + def angle(self): return math.atan2(self.y, self.x) + def __repr__(self): return '%f,%f' % (self.x, self.y) + def pr(self): return "%.2f,%.2f" % (self.x, self.y) + def to_list(self): return [self.x, self.y] + def ccw(self): return P(-self.y,self.x) + def l2(self): return self.x*self.x + self.y*self.y + +################################################################################ +### +### Offset function +### +### This function offsets given cubic super path. +### It's based on src/livarot/PathOutline.cpp from Inkscape's source code. +### +### +################################################################################ +def csp_offset(csp, r) : + offset_tolerance = 0.05 + offset_subdivision_depth = 10 + time_ = time.time() + time_start = time_ + print_("Offset start at %s"% time_) + print_("Offset radius %s"% r) + + + def csp_offset_segment(sp1,sp2,r) : + result = [] + t = csp_get_t_at_curvature(sp1,sp2,1/r) + if len(t) == 0 : t =[0.,1.] + t.sort() + if t[0]>.00000001 : t = [0.]+t + if t[-1]<.99999999 : t.append(1.) + for st,end in zip(t,t[1:]) : + c = csp_curvature_at_t(sp1,sp2,(st+end)/2) + sp = csp_split_by_two_points(sp1,sp2,st,end) + if sp[1]!=sp[2]: + if (c>1/r and r<0 or c<1/r and r>0) : + offset = offset_segment_recursion(sp[1],sp[2],r, offset_subdivision_depth, offset_tolerance) + else : # This part will be clipped for sure... TODO Optimize it... + offset = offset_segment_recursion(sp[1],sp[2],r, offset_subdivision_depth, offset_tolerance) + + if result==[] : + result = offset[:] + else: + if csp_subpaths_end_to_start_distance2(result,offset)<0.0001 : + result = csp_concat_subpaths(result,offset) + else: + + intersection = csp_get_subapths_last_first_intersection(result,offset) + if intersection != [] : + i,t1,j,t2 = intersection + sp1_,sp2_,sp3_ = csp_split(result[i-1],result[i],t1) + result = result[:i-1] + [ sp1_, sp2_ ] + sp1_,sp2_,sp3_ = csp_split(offset[j-1],offset[j],t2) + result = csp_concat_subpaths( result, [sp2_,sp3_] + offset[j+1:] ) + else : + pass # ??? + #raise ValueError, "Offset curvature clipping error" + #csp_draw([result]) + return result + + + def create_offset_segment(sp1,sp2,r) : + # See Gernot Hoffmann "Bezier Curves" p.34 -> 7.1 Bezier Offset Curves + p0,p1,p2,p3 = P(sp1[1]),P(sp1[2]),P(sp2[0]),P(sp2[1]) + s0,s1,s3 = p1-p0,p2-p1,p3-p2 + n0 = s0.ccw().unit() if s0.l2()!=0 else P(csp_normalized_normal(sp1,sp2,0)) + n3 = s3.ccw().unit() if s3.l2()!=0 else P(csp_normalized_normal(sp1,sp2,1)) + n1 = s1.ccw().unit() if s1.l2()!=0 else (n0.unit()+n3.unit()).unit() + + q0,q3 = p0+r*n0, p3+r*n3 + c = csp_curvature_at_t(sp1,sp2,0) + q1 = q0 + (p1-p0)*(1- (r*c if abs(c)<100 else 0) ) + c = csp_curvature_at_t(sp1,sp2,1) + q2 = q3 + (p2-p3)*(1- (r*c if abs(c)<100 else 0) ) + + + return [[q0.to_list(), q0.to_list(), q1.to_list()],[q2.to_list(), q3.to_list(), q3.to_list()]] + + + def csp_get_subapths_last_first_intersection(s1,s2): + _break = False + for i in range(1,len(s1)) : + sp11, sp12 = s1[-i-1], s1[-i] + for j in range(1,len(s2)) : + sp21,sp22 = s2[j-1], s2[j] + intersection = csp_segments_true_intersection(sp11,sp12,sp21,sp22) + if intersection != [] : + _break = True + break + if _break:break + if _break : + intersection = max(intersection) + return [len(s1)-i,intersection[0], j,intersection[1]] + else : + return [] + + + def csp_join_offsets(prev,next,sp1,sp2,sp1_l,sp2_l,r): + if len(next)>1 : + if (P(prev[-1][1])-P(next[0][1])).l2()<0.001 : + return prev,[],next + intersection = csp_get_subapths_last_first_intersection(prev,next) + if intersection != [] : + i,t1,j,t2 = intersection + sp1_,sp2_,sp3_ = csp_split(prev[i-1],prev[i],t1) + sp3_,sp4_,sp5_ = csp_split(next[j-1], next[j],t2) + return prev[:i-1] + [ sp1_, sp2_ ], [], [sp4_,sp5_] + next[j+1:] + + # Offsets do not intersect... will add an arc... + start = (P(csp_at_t(sp1_l,sp2_l,1.)) + r*P(csp_normalized_normal(sp1_l,sp2_l,1.))).to_list() + end = (P(csp_at_t(sp1,sp2,0.)) + r*P(csp_normalized_normal(sp1,sp2,0.))).to_list() + arc = csp_from_arc(start, end, sp1[1], r, csp_normalized_slope(sp1_l,sp2_l,1.) ) + if arc == [] : + return prev,[],next + else: + # Clip prev by arc + if csp_subpaths_end_to_start_distance2(prev,arc)>0.00001 : + intersection = csp_get_subapths_last_first_intersection(prev,arc) + if intersection != [] : + i,t1,j,t2 = intersection + sp1_,sp2_,sp3_ = csp_split(prev[i-1],prev[i],t1) + sp3_,sp4_,sp5_ = csp_split(arc[j-1],arc[j],t2) + prev = prev[:i-1] + [ sp1_, sp2_ ] + arc = [sp4_,sp5_] + arc[j+1:] + #else : raise ValueError, "Offset curvature clipping error" + # Clip next by arc + if next == [] : + return prev,[],arc + if csp_subpaths_end_to_start_distance2(arc,next)>0.00001 : + intersection = csp_get_subapths_last_first_intersection(arc,next) + if intersection != [] : + i,t1,j,t2 = intersection + sp1_,sp2_,sp3_ = csp_split(arc[i-1],arc[i],t1) + sp3_,sp4_,sp5_ = csp_split(next[j-1],next[j],t2) + arc = arc[:i-1] + [ sp1_, sp2_ ] + next = [sp4_,sp5_] + next[j+1:] + #else : raise ValueError, "Offset curvature clipping error" + + return prev,arc,next + + + def offset_segment_recursion(sp1,sp2,r, depth, tolerance) : + sp1_r,sp2_r = create_offset_segment(sp1,sp2,r) + err = max( + csp_seg_to_point_distance(sp1_r,sp2_r, (P(csp_at_t(sp1,sp2,.25)) + P(csp_normalized_normal(sp1,sp2,.25))*r).to_list())[0], + csp_seg_to_point_distance(sp1_r,sp2_r, (P(csp_at_t(sp1,sp2,.50)) + P(csp_normalized_normal(sp1,sp2,.50))*r).to_list())[0], + csp_seg_to_point_distance(sp1_r,sp2_r, (P(csp_at_t(sp1,sp2,.75)) + P(csp_normalized_normal(sp1,sp2,.75))*r).to_list())[0], + ) + + if err>tolerance**2 and depth>0: + #print_(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], tolerance) + if depth > offset_subdivision_depth-2 : + t = csp_max_curvature(sp1,sp2) + t = max(.1,min(.9 ,t)) + else : + t = .5 + sp3,sp4,sp5 = csp_split(sp1,sp2,t) + r1 = offset_segment_recursion(sp3,sp4,r, depth-1, tolerance) + r2 = offset_segment_recursion(sp4,sp5,r, depth-1, tolerance) + return r1[:-1]+ [[r1[-1][0],r1[-1][1],r2[0][2]]] + r2[1:] + else : + #csp_draw([[sp1_r,sp2_r]]) + #draw_pointer(sp1[1]+sp1_r[1], "#057", "line") + #draw_pointer(sp2[1]+sp2_r[1], "#705", "line") + return [sp1_r,sp2_r] + + + ############################################################################ + # Some small definitions + ############################################################################ + csp_len = len(csp) + + ############################################################################ + # Prepare the path + ############################################################################ + # Remove all small segments (segment length < 0.001) + + for i in xrange(len(csp)) : + for j in xrange(len(csp[i])) : + sp = csp[i][j] + if (P(sp[1])-P(sp[0])).mag() < 0.001 : + csp[i][j][0] = sp[1] + if (P(sp[2])-P(sp[0])).mag() < 0.001 : + csp[i][j][2] = sp[1] + for i in xrange(len(csp)) : + for j in xrange(1,len(csp[i])) : + if cspseglength(csp[i][j-1], csp[i][j])<0.001 : + csp[i] = csp[i][:j] + csp[i][j+1:] + if cspseglength(csp[i][-1],csp[i][0])>0.001 : + csp[i][-1][2] = csp[i][-1][1] + csp[i]+= [ [csp[i][0][1],csp[i][0][1],csp[i][0][1]] ] + + # TODO Get rid of self intersections. + + original_csp = csp[:] + # Clip segments which has curvature>1/r. Because their offset will be selfintersecting and very nasty. + + print_("Offset prepared the path in %s"%(time.time()-time_)) + print_("Path length = %s"% sum([len(i)for i in csp] ) ) + time_ = time.time() + + ############################################################################ + # Offset + ############################################################################ + # Create offsets for all segments in the path. And join them together inside each subpath. + unclipped_offset = [[] for i in xrange(csp_len)] + offsets_original = [[] for i in xrange(csp_len)] + join_points = [[] for i in xrange(csp_len)] + intersection = [[] for i in xrange(csp_len)] + for i in xrange(csp_len) : + subpath = csp[i] + subpath_offset = [] + last_offset_len = 0 + for sp1,sp2 in zip(subpath, subpath[1:]) : + segment_offset = csp_offset_segment(sp1,sp2,r) + if subpath_offset == [] : + subpath_offset = segment_offset + + prev_l = len(subpath_offset) + else : + prev, arc, next = csp_join_offsets(subpath_offset[-prev_l:],segment_offset,sp1,sp2,sp1_l,sp2_l,r) + #csp_draw([prev],"Blue") + #csp_draw([arc],"Magenta") + subpath_offset = csp_concat_subpaths(subpath_offset[:-prev_l+1],prev,arc,next) + prev_l = len(next) + sp1_l, sp2_l = sp1[:], sp2[:] + + # Join last and first offsets togother to close the curve + + prev, arc, next = csp_join_offsets(subpath_offset[-prev_l:], subpath_offset[:2], subpath[0], subpath[1], sp1_l,sp2_l, r) + subpath_offset[:2] = next[:] + subpath_offset = csp_concat_subpaths(subpath_offset[:-prev_l+1],prev,arc) + #csp_draw([prev],"Blue") + #csp_draw([arc],"Red") + #csp_draw([next],"Red") + + # Collect subpath's offset and save it to unclipped offset list. + unclipped_offset[i] = subpath_offset[:] + + #for k,t in intersection[i]: + # draw_pointer(csp_at_t(subpath_offset[k-1], subpath_offset[k], t)) + + #inkex.etree.SubElement( options.doc_root, inkex.addNS('path','svg'), {"d": cubicsuperpath.formatPath(unclipped_offset), "style":"fill:none;stroke:#0f0;"} ) + print_("Offsetted path in %s"%(time.time()-time_)) + time_ = time.time() + + #for i in range(len(unclipped_offset)): + # csp_draw([unclipped_offset[i]], color = ["Green","Red","Blue"][i%3], width = .1) + #return [] + ############################################################################ + # Now to the clipping. + ############################################################################ + # First of all find all intersection's between all segments of all offseted subpaths, including self intersections. + + #TODO define offset tolerance here + global small_tolerance + small_tolerance = 0.01 + summ = 0 + summ1 = 0 + for subpath_i in xrange(csp_len) : + for subpath_j in xrange(subpath_i,csp_len) : + subpath = unclipped_offset[subpath_i] + subpath1 = unclipped_offset[subpath_j] + for i in xrange(1,len(subpath)) : + # If subpath_i==subpath_j we are looking for self intersections, so + # we'll need search intersections only for xrange(i,len(subpath1)) + for j in ( xrange(i,len(subpath1)) if subpath_i==subpath_j else xrange(len(subpath1))) : + if subpath_i==subpath_j and j==i : + # Find self intersections of a segment + sp1,sp2,sp3 = csp_split(subpath[i-1],subpath[i],.5) + intersections = csp_segments_intersection(sp1,sp2,sp2,sp3) + summ +=1 + for t in intersections : + summ1 += 1 + if not ( small(t[0]-1) and small(t[1]) ) and 0<=t[0]<=1 and 0<=t[1]<=1 : + intersection[subpath_i] += [ [i,t[0]/2],[j,t[1]/2+.5] ] + else : + intersections = csp_segments_intersection(subpath[i-1],subpath[i],subpath1[j-1],subpath1[j]) + summ +=1 + for t in intersections : + summ1 += 1 + #TODO tolerance dependence to cpsp_length(t) + if len(t) == 2 and 0<=t[0]<=1 and 0<=t[1]<=1 and not ( + subpath_i==subpath_j and ( + (j-i-1) % (len(subpath)-1) == 0 and small(t[0]-1) and small(t[1]) or + (i-j-1) % (len(subpath)-1) == 0 and small(t[1]-1) and small(t[0]) ) ) : + intersection[subpath_i] += [ [i,t[0]] ] + intersection[subpath_j] += [ [j,t[1]] ] + #draw_pointer(csp_at_t(subpath[i-1],subpath[i],t[0]),"#f00") + #print_(t) + #print_(i,j) + elif len(t)==5 and t[4]=="Overlap": + intersection[subpath_i] += [ [i,t[0]], [i,t[1]] ] + intersection[subpath_j] += [ [j,t[1]], [j,t[3]] ] + + print_("Intersections found in %s"%(time.time()-time_)) + print_("Examined %s segments"%(summ)) + print_("found %s intersections"%(summ1)) + time_ = time.time() + + ######################################################################## + # Split unclipped offset by intersection points into splitted_offset + ######################################################################## + splitted_offset = [] + for i in xrange(csp_len) : + subpath = unclipped_offset[i] + if len(intersection[i]) > 0 : + parts = csp_subpath_split_by_points(subpath, intersection[i]) + # Close parts list to close path (The first and the last parts are joined together) + if [1,0.] not in intersection[i] : + parts[0][0][0] = parts[-1][-1][0] + parts[0] = csp_concat_subpaths(parts[-1], parts[0]) + splitted_offset += parts[:-1] + else: + splitted_offset += parts[:] + else : + splitted_offset += [subpath[:]] + + #for i in range(len(splitted_offset)): + # csp_draw([splitted_offset[i]], color = ["Green","Red","Blue"][i%3]) + print_("Splitted in %s"%(time.time()-time_)) + time_ = time.time() + + + ######################################################################## + # Clipping + ######################################################################## + result = [] + for subpath_i in range(len(splitted_offset)): + clip = False + s1 = splitted_offset[subpath_i] + for subpath_j in range(len(splitted_offset)): + s2 = splitted_offset[subpath_j] + if (P(s1[0][1])-P(s2[-1][1])).l2()<0.0001 and ( (subpath_i+1) % len(splitted_offset) != subpath_j ): + if dot(csp_normalized_normal(s2[-2],s2[-1],1.),csp_normalized_slope(s1[0],s1[1],0.))*r<-0.0001 : + clip = True + break + if (P(s2[0][1])-P(s1[-1][1])).l2()<0.0001 and ( (subpath_j+1) % len(splitted_offset) != subpath_i ): + if dot(csp_normalized_normal(s2[0],s2[1],0.),csp_normalized_slope(s1[-2],s1[-1],1.))*r>0.0001 : + clip = True + break + + if not clip : + result += [s1[:]] + elif options.offset_draw_clippend_path : + csp_draw([s1],color="Red",width=.1) + draw_pointer( csp_at_t(s2[-2],s2[-1],1.)+ + (P(csp_at_t(s2[-2],s2[-1],1.))+ P(csp_normalized_normal(s2[-2],s2[-1],1.))*10).to_list(),"Green", "line" ) + draw_pointer( csp_at_t(s1[0],s1[1],0.)+ + (P(csp_at_t(s1[0],s1[1],0.))+ P(csp_normalized_slope(s1[0],s1[1],0.))*10).to_list(),"Red", "line" ) + + # Now join all together and check closure and orientation of result + joined_result = csp_join_subpaths(result) + # Check if each subpath from joined_result is closed + #csp_draw(joined_result,color="Green",width=1) + + + for s in joined_result[:] : + if csp_subpaths_end_to_start_distance2(s,s) > 0.001 : + # Remove open parts + if options.offset_draw_clippend_path: + csp_draw([s],color="Orange",width=1) + draw_pointer(s[0][1], comment= csp_subpaths_end_to_start_distance2(s,s)) + draw_pointer(s[-1][1], comment = csp_subpaths_end_to_start_distance2(s,s)) + joined_result.remove(s) + else : + # Remove small parts + minx,miny,maxx,maxy = csp_true_bounds([s]) + if (minx[0]-maxx[0])**2 + (miny[1]-maxy[1])**2 < 0.1 : + joined_result.remove(s) + print_("Clipped and joined path in %s"%(time.time()-time_)) + time_ = time.time() + + ######################################################################## + # Now to the Dummy cliping: remove parts from splitted offset if their + # centers are closer to the original path than offset radius. + ######################################################################## + + r1,r2 = ( (0.99*r)**2, (1.01*r)**2 ) if abs(r*.01)<1 else ((abs(r)-1)**2, (abs(r)+1)**2) + for s in joined_result[:]: + dist = csp_to_point_distance(original_csp, s[int(len(s)/2)][1], dist_bounds = [r1,r2], tolerance = .000001) + if not r1 < dist[0] < r2 : + joined_result.remove(s) + if options.offset_draw_clippend_path: + csp_draw([s], comment = math.sqrt(dist[0])) + draw_pointer(csp_at_t(csp[dist[1]][dist[2]-1],csp[dist[1]][dist[2]],dist[3])+s[int(len(s)/2)][1],"blue", "line", comment = [math.sqrt(dist[0]),i,j,sp] ) + + print_("-----------------------------") + print_("Total offset time %s"%(time.time()-time_start)) + print_() + return joined_result + + + + + +################################################################################ +### +### Biarc function +### +### Calculates biarc approximation of cubic super path segment +### splits segment if needed or approximates it with straight line +### +################################################################################ +def biarc(sp1, sp2, z1, z2, depth=0): + def biarc_split(sp1,sp2, z1, z2, depth): + if depth 0 : raise ValueError, (a,b,c,disq,beta1,beta2) + beta = max(beta1, beta2) + elif asmall and bsmall: + return biarc_split(sp1, sp2, z1, z2, depth) + alpha = beta * r + ab = alpha + beta + P1 = P0 + alpha * TS + P3 = P4 - beta * TE + P2 = (beta / ab) * P1 + (alpha / ab) * P3 + + + def calculate_arc_params(P0,P1,P2): + D = (P0+P2)/2 + if (D-P1).mag()==0: return None, None + R = D - ( (D-P0).mag()**2/(D-P1).mag() )*(P1-D).unit() + p0a, p1a, p2a = (P0-R).angle()%(2*math.pi), (P1-R).angle()%(2*math.pi), (P2-R).angle()%(2*math.pi) + alpha = (p2a - p0a) % (2*math.pi) + if (p0a1000000 or abs(R.y)>1000000 or (R-P0).mag<.1 : + return None, None + else : + return R, alpha + R1,a1 = calculate_arc_params(P0,P1,P2) + R2,a2 = calculate_arc_params(P2,P3,P4) + if R1==None or R2==None or (R1-P0).mag() 1 and depthls : + res += [seg] + else : + if seg[1] == "arc" : + r = math.sqrt((seg[0][0]-seg[2][0])**2+(seg[0][1]-seg[2][1])**2) + x,y = seg[0][0]-seg[2][0], seg[0][1]-seg[2][1] + a = seg[3]/ls*(l-lc) + x,y = x*math.cos(a) - y*math.sin(a), x*math.sin(a) + y*math.cos(a) + x,y = x+seg[2][0], y+seg[2][1] + res += [[ seg[0], "arc", seg[2], a, [x,y], [seg[5][0],seg[5][1]/ls*(l-lc)] ]] + if seg[1] == "line" : + res += [[ seg[0], "line", 0, 0, [(seg[4][0]-seg[0][0])/ls*(l-lc),(seg[4][1]-seg[0][1])/ls*(l-lc)], [seg[5][0],seg[5][1]/ls*(l-lc)] ]] + i += 1 + if i >= len(subcurve) and not subcurve_closed: + reverse = not reverse + i = i%len(subcurve) + return res + +################################################################################ +### Polygon class +################################################################################ +class Polygon: + def __init__(self, polygon=None): + self.polygon = [] if polygon==None else polygon[:] + + + def move(self, x, y) : + for i in range(len(self.polygon)) : + for j in range(len(self.polygon[i])) : + self.polygon[i][j][0] += x + self.polygon[i][j][1] += y + + + def bounds(self) : + minx,miny,maxx,maxy = 1e400, 1e400, -1e400, -1e400 + for poly in self.polygon : + for p in poly : + if minx > p[0] : minx = p[0] + if miny > p[1] : miny = p[1] + if maxx < p[0] : maxx = p[0] + if maxy < p[1] : maxy = p[1] + return minx*1,miny*1,maxx*1,maxy*1 + + + def width(self): + b = self.bounds() + return b[2]-b[0] + + + def rotate_(self,sin,cos) : + for i in range(len(self.polygon)) : + for j in range(len(self.polygon[i])) : + x,y = self.polygon[i][j][0], self.polygon[i][j][1] + self.polygon[i][j][0] = x*cos - y*sin + self.polygon[i][j][1] = x*sin + y*cos + + + def rotate(self, a): + cos, sin = math.cos(a), math.sin(a) + self.rotate_(sin,cos) + + + def drop_into_direction(self, direction, surface) : + # Polygon is a list of simple polygons + # Surface is a polygon + line y = 0 + # Direction is [dx,dy] + if len(self.polygon) == 0 or len(self.polygon[0])==0 : return + if direction[0]**2 + direction[1]**2 <1e-10 : return + direction = normalize(direction) + sin,cos = direction[0], -direction[1] + self.rotate_(-sin,cos) + surface.rotate_(-sin,cos) + self.drop_down(surface, zerro_plane = False) + self.rotate_(sin,cos) + surface.rotate_(sin,cos) + + + def centroid(self): + centroids = [] + sa = 0 + for poly in self.polygon: + cx,cy,a = 0,0,0 + for i in range(len(poly)): + [x1,y1],[x2,y2] = poly[i-1],poly[i] + cx += (x1+x2)*(x1*y2-x2*y1) + cy += (y1+y2)*(x1*y2-x2*y1) + a += (x1*y2-x2*y1) + a *= 3. + if abs(a)>0 : + cx /= a + cy /= a + sa += abs(a) + centroids += [ [cx,cy,a] ] + if sa == 0 : return [0.,0.] + cx,cy = 0.,0. + for c in centroids : + cx += c[0]*c[2] + cy += c[1]*c[2] + cx /= sa + cy /= sa + return [cx,cy] + + + def drop_down(self, surface, zerro_plane = True) : + # Polygon is a list of simple polygons + # Surface is a polygon + line y = 0 + # Down means min y (0,-1) + if len(self.polygon) == 0 or len(self.polygon[0])==0 : return + # Get surface top point + top = surface.bounds()[3] + if zerro_plane : top = max(0, top) + # Get polygon bottom point + bottom = self.bounds()[1] + self.move(0, top - bottom + 10) + # Now get shortest distance from surface to polygon in positive x=0 direction + # Such distance = min(distance(vertex, edge)...) where edge from surface and + # vertex from polygon and vice versa... + dist = 1e300 + for poly in surface.polygon : + for i in range(len(poly)) : + for poly1 in self.polygon : + for i1 in range(len(poly1)) : + st,end = poly[i-1], poly[i] + vertex = poly1[i1] + if st[0]<=vertex[0]<= end[0] or end[0]<=vertex[0]<=st[0] : + if st[0]==end[0] : d = min(vertex[1]-st[1],vertex[1]-end[1]) + else : d = vertex[1] - st[1] - (end[1]-st[1])*(vertex[0]-st[0])/(end[0]-st[0]) + if dist > d : dist = d + # and vice versa just change the sign because vertex now under the edge + st,end = poly1[i1-1], poly1[i1] + vertex = poly[i] + if st[0]<=vertex[0]<=end[0] or end[0]<=vertex[0]<=st[0] : + if st[0]==end[0] : d = min(- vertex[1]+st[1],-vertex[1]+end[1]) + else : d = - vertex[1] + st[1] + (end[1]-st[1])*(vertex[0]-st[0])/(end[0]-st[0]) + if dist > d : dist = d + + if zerro_plane and dist > 10 + top : dist = 10 + top + #print_(dist, top, bottom) + #self.draw() + self.move(0, -dist) + + + def draw(self,color="#075",width=.1) : + for poly in self.polygon : + csp_draw( [csp_subpath_line_to([],poly+[poly[0]])], color=color,width=width ) + + + def add(self, add) : + if type(add) == type([]) : + self.polygon += add[:] + else : + self.polygon += add.polygon[:] + + + def point_inside(self,p) : + inside = False + for poly in self.polygon : + for i in range(len(poly)): + st,end = poly[i-1], poly[i] + if p==st or p==end : return True # point is a vertex = point is on the edge + if st[0]>end[0] : st, end = end, st # This will be needed to check that edge if open only at rigth end + c = (p[1]-st[1])*(end[0]-st[0])-(end[1]-st[1])*(p[0]-st[0]) + #print_(c) + if st[0]<=p[0]0.000001 and point_to_point_d2(p,e)>0.000001 : + poly_ += [p] + # Check self intersections with other polys + for i2 in range(len(self.polygon)): + if i1==i2 : continue + poly2 = self.polygon[i2] + for j2 in range(len(poly2)): + s1, e1 = poly2[j2-1],poly2[j2] + int_ = line_line_intersection_points(s,e,s1,e1) + for p in int_ : + if point_to_point_d2(p,s)>0.000001 and point_to_point_d2(p,e)>0.000001 : + poly_ += [p] + hull += [poly_] + # Create the dictionary containing all edges in both directions + edges = {} + for poly in self.polygon : + for i in range(len(poly)): + s,e = tuple(poly[i-1]), tuple(poly[i]) + if (point_to_point_d2(e,s)<0.000001) : continue + break_s, break_e = False, False + for p in edges : + if point_to_point_d2(p,s)<0.000001 : + break_s = True + s = p + if point_to_point_d2(p,e)<0.000001 : + break_e = True + e = p + if break_s and break_e : break + l = point_to_point_d(s,e) + if not break_s and not break_e : + edges[s] = [ [s,e,l] ] + edges[e] = [ [e,s,l] ] + #draw_pointer(s+e,"red","line") + #draw_pointer(s+e,"red","line") + else : + if e in edges : + for edge in edges[e] : + if point_to_point_d2(edge[1],s)<0.000001 : + break + if point_to_point_d2(edge[1],s)>0.000001 : + edges[e] += [ [e,s,l] ] + #draw_pointer(s+e,"red","line") + + else : + edges[e] = [ [e,s,l] ] + #draw_pointer(s+e,"green","line") + if s in edges : + for edge in edges[s] : + if point_to_point_d2(edge[1],e)<0.000001 : + break + if point_to_point_d2(edge[1],e)>0.000001 : + edges[s] += [ [s,e, l] ] + #draw_pointer(s+e,"red","line") + else : + edges[s] = [ [s,e,l] ] + #draw_pointer(s+e,"green","line") + + + def angle_quadrant(sin,cos): + # quadrants are (0,pi/2], (pi/2,pi], (pi,3*pi/2], (3*pi/2, 2*pi], i.e. 0 is in the 4-th quadrant + if sin>0 and cos>=0 : return 1 + if sin>=0 and cos<0 : return 2 + if sin<0 and cos<=0 : return 3 + if sin<=0 and cos>0 : return 4 + + + def angle_is_less(sin,cos,sin1,cos1): + # 0 = 2*pi is the largest angle + if [sin1, cos1] == [0,1] : return True + if [sin, cos] == [0,1] : return False + if angle_quadrant(sin,cos)>angle_quadrant(sin1,cos1) : + return False + if angle_quadrant(sin,cos)=0 and cos>0 : return sin0 and cos<=0 : return sin>sin1 + if sin<=0 and cos<0 : return sin>sin1 + if sin<0 and cos>=0 : return sin len_edges : raise ValueError, "Hull error" + loops1 += 1 + next = get_closes_edge_by_angle(edges[last[1]],last) + #draw_pointer(next[0]+next[1],"Green","line", comment=i, width= 1) + #print_(next[0],"-",next[1]) + + last = next + poly += [ list(last[0]) ] + self.polygon += [ poly ] + # Remove all edges that are intersects new poly (any vertex inside new poly) + poly_ = Polygon([poly]) + for p in edges.keys()[:] : + if poly_.point_inside(list(p)) : del edges[p] + self.draw(color="Green", width=1) + + +class Arangement_Genetic: + # gene = [fittness, order, rotation, xposition] + # spieces = [gene]*shapes count + # population = [spieces] + def __init__(self, polygons, material_width): + self.population = [] + self.genes_count = len(polygons) + self.polygons = polygons + self.width = material_width + self.mutation_factor = 0.1 + self.order_mutate_factor = 1. + self.move_mutate_factor = 1. + + + def add_random_species(self,count): + for i in range(count): + specimen = [] + order = range(self.genes_count) + random.shuffle(order) + for j in order: + specimen += [ [j, random.random(), random.random()] ] + self.population += [ [None,specimen] ] + + + def species_distance2(self,sp1,sp2) : + # retun distance, each component is normalized + s = 0 + for j in range(self.genes_count) : + s += ((sp1[j][0]-sp2[j][0])/self.genes_count)**2 + (( sp1[j][1]-sp2[j][1]))**2 + ((sp1[j][2]-sp2[j][2]))**2 + return s + + + def similarity(self,sp1,top) : + # Define similarity as a simple distance between two points in len(gene)*len(spiece) -th dimentions + # for sp2 in top_spieces sum(|sp1-sp2|)/top_count + sim = 0 + for sp2 in top : + sim += math.sqrt(species_distance2(sp1,sp2[1])) + return sim/len(top) + + + def leave_top_species(self,count): + self.population.sort() + res = [ copy.deepcopy(self.population[0]) ] + del self.population[0] + for i in range(count-1) : + t = [] + for j in range(20) : + i1 = random.randint(0,len(self.population)-1) + t += [ [self.population[i1][0],i1] ] + t.sort() + res += [ copy.deepcopy(self.population[t[0][1]]) ] + del self.population[t[0][1]] + self.population = res + #del self.population[0] + #for c in range(count-1) : + # rank = [] + # for i in range(len(self.population)) : + # sim = self.similarity(self.population[i][1],res) + # rank += [ [self.population[i][0] / sim if sim>0 else 1e100,i] ] + # rank.sort() + # res += [ copy.deepcopy(self.population[rank[0][1]]) ] + # print_(rank[0],self.population[rank[0][1]][0]) + # print_(res[-1]) + # del self.population[rank[0][1]] + + self.population = res + + + def populate_species(self,count, parent_count): + self.population.sort() + self.inc = 0 + for c in range(count): + parent1 = random.randint(0,parent_count-1) + parent2 = random.randint(0,parent_count-1) + if parent1==parent2 : parent2 = (parent2+1) % parent_count + parent1, parent2 = self.population[parent1][1], self.population[parent2][1] + i1,i2 = 0, 0 + genes_order = [] + specimen = [ [0,0.,0.] for i in range(self.genes_count) ] + + self.incest_mutation_multiplyer = 1. + self.incest_mutation_count_multiplyer = 1. + + if self.species_distance2(parent1, parent2) <= .01/self.genes_count : + # OMG it's a incest :O!!! + # Damn you bastards! + self.inc +=1 + self.incest_mutation_multiplyer = 2. + self.incest_mutation_count_multiplyer = 2. + else : + if random.random()<.01 : print_(self.species_distance2(parent1, parent2)) + start_gene = random.randint(0,self.genes_count) + end_gene = (max(1,random.randint(0,self.genes_count),int(self.genes_count/4))+start_gene) % self.genes_count + if end_gene0: + end = p[keys[-1]][-1][1] + dist = None + for i in range(len(k)): + start = p[k[i]][0][1] + dist = max( ( -( ( end[0]-start[0])**2+(end[1]-start[1])**2 ) ,i) , dist ) + keys += [k[dist[1]]] + del k[dist[1]] + for k in keys: + subpath = p[k] + c += [ [ [subpath[0][1][0],subpath[0][1][1]] , 'move', 0, 0] ] + for i in range(1,len(subpath)): + sp1 = [ [subpath[i-1][j][0], subpath[i-1][j][1]] for j in range(3)] + sp2 = [ [subpath[i ][j][0], subpath[i ][j][1]] for j in range(3)] + c += biarc(sp1,sp2,0,0) if w==None else biarc(sp1,sp2,-f(w[k][i-1]),-f(w[k][i])) +# l1 = biarc(sp1,sp2,0,0) if w==None else biarc(sp1,sp2,-f(w[k][i-1]),-f(w[k][i])) +# print_((-f(w[k][i-1]),-f(w[k][i]), [i1[5] for i1 in l1]) ) + c += [ [ [subpath[-1][1][0],subpath[-1][1][1]] ,'end',0,0] ] + print_("Curve: " + str(c)) + return c + + + def draw_curve(self, curve, layer, group=None, style=styles["biarc_style"]): + + self.get_defs() + # Add marker to defs if it doesnot exists + if "DrawCurveMarker" not in self.defs : + defs = inkex.etree.SubElement( self.document.getroot(), inkex.addNS("defs","svg")) + marker = inkex.etree.SubElement( defs, inkex.addNS("marker","svg"), {"id":"DrawCurveMarker","orient":"auto","refX":"-8","refY":"-2.41063","style":"overflow:visible"}) + inkex.etree.SubElement( marker, inkex.addNS("path","svg"), + { "d":"m -6.55552,-2.41063 0,0 L -13.11104,0 c 1.0473,-1.42323 1.04126,-3.37047 0,-4.82126", + "style": "fill:#000044; fill-rule:evenodd;stroke-width:0.62500000;stroke-linejoin:round;" } + ) + if "DrawCurveMarker_r" not in self.defs : + defs = inkex.etree.SubElement( self.document.getroot(), inkex.addNS("defs","svg")) + marker = inkex.etree.SubElement( defs, inkex.addNS("marker","svg"), {"id":"DrawCurveMarker_r","orient":"auto","refX":"8","refY":"-2.41063","style":"overflow:visible"}) + inkex.etree.SubElement( marker, inkex.addNS("path","svg"), + { "d":"m 6.55552,-2.41063 0,0 L 13.11104,0 c -1.0473,-1.42323 -1.04126,-3.37047 0,-4.82126", + "style": "fill:#000044; fill-rule:evenodd;stroke-width:0.62500000;stroke-linejoin:round;" } + ) + for i in [0,1]: + style['biarc%s_r'%i] = simplestyle.parseStyle(style['biarc%s'%i]) + style['biarc%s_r'%i]["marker-start"] = "url(#DrawCurveMarker_r)" + del(style['biarc%s_r'%i]["marker-end"]) + style['biarc%s_r'%i] = simplestyle.formatStyle(style['biarc%s_r'%i]) + + if group==None: + group = inkex.etree.SubElement( self.layers[min(1,len(self.layers)-1)], inkex.addNS('g','svg'), {"gcodetools": "Preview group"} ) + s, arcn = '', 0 + + + a,b,c = [0.,0.], [1.,0.], [0.,1.] + k = (b[0]-a[0])*(c[1]-a[1])-(c[0]-a[0])*(b[1]-a[1]) + a,b,c = self.transform(a, layer, True), self.transform(b, layer, True), self.transform(c, layer, True) + if ((b[0]-a[0])*(c[1]-a[1])-(c[0]-a[0])*(b[1]-a[1]))*k > 0 : reverse_angle = 1 + else : reverse_angle = -1 + for sk in curve: + si = sk[:] + si[0], si[2] = self.transform(si[0], layer, True), (self.transform(si[2], layer, True) if type(si[2])==type([]) and len(si[2])==2 else si[2]) + + if s!='': + if s[1] == 'line': + inkex.etree.SubElement( group, inkex.addNS('path','svg'), + { + 'style': style['line'], + 'd':'M %s,%s L %s,%s' % (s[0][0], s[0][1], si[0][0], si[0][1]), + "gcodetools": "Preview", + } + ) + elif s[1] == 'arc': + arcn += 1 + sp = s[0] + c = s[2] + s[3] = s[3]*reverse_angle + + a = ( (P(si[0])-P(c)).angle() - (P(s[0])-P(c)).angle() )%math.pi2 #s[3] + if s[3]*a<0: + if a>0: a = a-math.pi2 + else: a = math.pi2+a + r = math.sqrt( (sp[0]-c[0])**2 + (sp[1]-c[1])**2 ) + a_st = ( math.atan2(sp[0]-c[0],- (sp[1]-c[1])) - math.pi/2 ) % (math.pi*2) + st = style['biarc%s' % (arcn%2)][:] + if a>0: + a_end = a_st+a + st = style['biarc%s'%(arcn%2)] + else: + a_end = a_st*1 + a_st = a_st+a + st = style['biarc%s_r'%(arcn%2)] + inkex.etree.SubElement( group, inkex.addNS('path','svg'), + { + 'style': st, + inkex.addNS('cx','sodipodi'): str(c[0]), + inkex.addNS('cy','sodipodi'): str(c[1]), + inkex.addNS('rx','sodipodi'): str(r), + inkex.addNS('ry','sodipodi'): str(r), + inkex.addNS('start','sodipodi'): str(a_st), + inkex.addNS('end','sodipodi'): str(a_end), + inkex.addNS('open','sodipodi'): 'true', + inkex.addNS('type','sodipodi'): 'arc', + "gcodetools": "Preview", + }) + s = si + + + def check_dir(self): + if self.options.directory[-1] not in ["/","\\"]: + if "\\" in self.options.directory : + self.options.directory += "\\" + else : + self.options.directory += "/" + print_("Checking direcrory: '%s'"%self.options.directory) + if (os.path.isdir(self.options.directory)): + if (os.path.isfile(self.options.directory+'header')): + f = open(self.options.directory+'header', 'r') + self.header = f.read() + f.close() + else: + self.header = defaults['header'] + if (os.path.isfile(self.options.directory+'footer')): + f = open(self.options.directory+'footer','r') + self.footer = f.read() + f.close() + else: + self.footer = defaults['footer'] + + if self.options.unit == "G21 (All units in mm)" : + self.header += "G21\n" + elif self.options.unit == "G20 (All units in inches)" : + self.header += "G20\n" + else: + self.error(_("Directory does not exist! Please specify existing directory at options tab!"),"error") + return False + + if self.options.add_numeric_suffix_to_filename : + dir_list = os.listdir(self.options.directory) + if "." in self.options.file : + r = re.match(r"^(.*)(\..*)$",self.options.file) + ext = r.group(2) + name = r.group(1) + else: + ext = "" + name = self.options.file + max_n = 0 + for s in dir_list : + r = re.match(r"^%s_0*(\d+)%s$"%(re.escape(name),re.escape(ext) ), s) + if r : + max_n = max(max_n,int(r.group(1))) + filename = name + "_" + ( "0"*(4-len(str(max_n+1))) + str(max_n+1) ) + ext + self.options.file = filename + + print_("Testing writing rights on '%s'"%(self.options.directory+self.options.file)) + try: + f = open(self.options.directory+self.options.file, "w") + f.close() + except: + self.error(_("Can not write to specified file!\n%s"%(self.options.directory+self.options.file)),"error") + return False + return True + + + +################################################################################ +### +### Generate Gcode +### Generates Gcode on given curve. +### +### Crve defenitnion [start point, type = {'arc','line','move','end'}, arc center, arc angle, end point, [zstart, zend]] +### +################################################################################ + def generate_gcode(self, curve, layer, depth): + tool = self.tools + print_("Tool in g-code generator: " + str(tool)) + def c(c): + c = [c[i] if i.1: + r1, r2 = (P(s[0])-P(s[2])), (P(si[0])-P(s[2])) + if abs(r1.mag()-r2.mag()) < 0.001 : + g += ("G2" if s[3]<0 else "G3") + c(si[0]+[ None, (s[2][0]-s[0][0]),(s[2][1]-s[0][1]) ]) + "\n" + else: + r = (r1.mag()+r2.mag())/2 + g += ("G2" if s[3]<0 else "G3") + c(si[0]) + " R%f" % (r) + "\n" + lg = 'G02' + else: + g += "G1 " + c(si[0]) + " " + feed + "\n" + lg = 'G01' + if si[1] == 'end': + g += tool['gcode after path'] + "\n" + return g + + + def get_transforms(self,g): + root = self.document.getroot() + trans = [] + while (g!=root): + if 'transform' in g.keys(): + t = g.get('transform') + t = simpletransform.parseTransform(t) + trans = simpletransform.composeTransform(t,trans) if trans != [] else t + print_(trans) + g=g.getparent() + return trans + + + def apply_transforms(self,g,csp): + trans = self.get_transforms(g) + if trans != []: + simpletransform.applyTransformToPath(trans, csp) + return csp + + + def transform(self, source_point, layer, reverse=False): + if layer == None : + layer = self.current_layer if self.current_layer is not None else self.document.getroot() + if layer not in self.transform_matrix: + for i in range(self.layers.index(layer),-1,-1): + if self.layers[i] in self.orientation_points : + break + + print_(str(self.layers)) + print_(str("I: " + str(i))) + print_("Transform: " + str(self.layers[i])) + if self.layers[i] not in self.orientation_points : + self.error(_("Orientation points for '%s' layer have not been found! Please add orientation points using Orientation tab!") % layer.get(inkex.addNS('label','inkscape')),"no_orientation_points") + elif self.layers[i] in self.transform_matrix : + self.transform_matrix[layer] = self.transform_matrix[self.layers[i]] + else : + orientation_layer = self.layers[i] + if len(self.orientation_points[orientation_layer])>1 : + self.error(_("There are more than one orientation point groups in '%s' layer") % orientation_layer.get(inkex.addNS('label','inkscape')),"more_than_one_orientation_point_groups") + points = self.orientation_points[orientation_layer][0] + if len(points)==2: + points += [ [ [(points[1][0][1]-points[0][0][1])+points[0][0][0], -(points[1][0][0]-points[0][0][0])+points[0][0][1]], [-(points[1][1][1]-points[0][1][1])+points[0][1][0], points[1][1][0]-points[0][1][0]+points[0][1][1]] ] ] + if len(points)==3: + print_("Layer '%s' Orientation points: " % orientation_layer.get(inkex.addNS('label','inkscape'))) + for point in points: + print_(point) + # Zcoordinates definition taken from Orientatnion point 1 and 2 + self.Zcoordinates[layer] = [max(points[0][1][2],points[1][1][2]), min(points[0][1][2],points[1][1][2])] + matrix = numpy.array([ + [points[0][0][0], points[0][0][1], 1, 0, 0, 0, 0, 0, 0], + [0, 0, 0, points[0][0][0], points[0][0][1], 1, 0, 0, 0], + [0, 0, 0, 0, 0, 0, points[0][0][0], points[0][0][1], 1], + [points[1][0][0], points[1][0][1], 1, 0, 0, 0, 0, 0, 0], + [0, 0, 0, points[1][0][0], points[1][0][1], 1, 0, 0, 0], + [0, 0, 0, 0, 0, 0, points[1][0][0], points[1][0][1], 1], + [points[2][0][0], points[2][0][1], 1, 0, 0, 0, 0, 0, 0], + [0, 0, 0, points[2][0][0], points[2][0][1], 1, 0, 0, 0], + [0, 0, 0, 0, 0, 0, points[2][0][0], points[2][0][1], 1] + ]) + + if numpy.linalg.det(matrix)!=0 : + m = numpy.linalg.solve(matrix, + numpy.array( + [[points[0][1][0]], [points[0][1][1]], [1], [points[1][1][0]], [points[1][1][1]], [1], [points[2][1][0]], [points[2][1][1]], [1]] + ) + ).tolist() + self.transform_matrix[layer] = [[m[j*3+i][0] for i in range(3)] for j in range(3)] + + else : + self.error(_("Orientation points are wrong! (if there are two orientation points they sould not be the same. If there are three orientation points they should not be in a straight line.)"),"wrong_orientation_points") + else : + self.error(_("Orientation points are wrong! (if there are two orientation points they sould not be the same. If there are three orientation points they should not be in a straight line.)"),"wrong_orientation_points") + + self.transform_matrix_reverse[layer] = numpy.linalg.inv(self.transform_matrix[layer]).tolist() + print_("\n Layer '%s' transformation matrixes:" % layer.get(inkex.addNS('label','inkscape')) ) + print_(self.transform_matrix) + print_(self.transform_matrix_reverse) + + ###self.Zauto_scale[layer] = math.sqrt( (self.transform_matrix[layer][0][0]**2 + self.transform_matrix[layer][1][1]**2)/2 ) + ### Zautoscale is absolete + self.Zauto_scale[layer] = 1 + print_("Z automatic scale = %s (computed according orientation points)" % self.Zauto_scale[layer]) + + x,y = source_point[0], source_point[1] + if not reverse : + t = self.transform_matrix[layer] + else : + t = self.transform_matrix_reverse[layer] + return [t[0][0]*x+t[0][1]*y+t[0][2], t[1][0]*x+t[1][1]*y+t[1][2]] + + + def transform_csp(self, csp_, layer, reverse = False): + csp = [ [ [csp_[i][j][0][:],csp_[i][j][1][:],csp_[i][j][2][:]] for j in range(len(csp_[i])) ] for i in range(len(csp_)) ] + for i in xrange(len(csp)): + for j in xrange(len(csp[i])): + for k in xrange(len(csp[i][j])): + csp[i][j][k] = self.transform(csp[i][j][k],layer, reverse) + return csp + + +################################################################################ +### Errors handling function, notes are just printed into Logfile, +### warnings are printed into log file and warning message is displayed but +### extension continues working, errors causes log and execution is halted +### Notes, warnings adn errors could be assigned to space or comma or dot +### sepparated strings (case is ignoreg). +################################################################################ + def error(self, s, type_= "Warning"): + notes = "Note " + warnings = """ + Warning tools_warning + bad_orientation_points_in_some_layers + more_than_one_orientation_point_groups + more_than_one_tool + orientation_have_not_been_defined + tool_have_not_been_defined + selection_does_not_contain_paths + selection_does_not_contain_paths_will_take_all + selection_is_empty_will_comupe_drawing + selection_contains_objects_that_are_not_paths + """ + errors = """ + Error + wrong_orientation_points + area_tools_diameter_error + no_tool_error + active_layer_already_has_tool + active_layer_already_has_orientation_points + """ + if type_.lower() in re.split("[\s\n,\.]+", errors.lower()) : + print_(s) + inkex.errormsg(s+"\n") + sys.exit() + elif type_.lower() in re.split("[\s\n,\.]+", warnings.lower()) : + print_(s) + if not self.options.suppress_all_messages : + inkex.errormsg(s+"\n") + elif type_.lower() in re.split("[\s\n,\.]+", notes.lower()) : + print_(s) + else : + print_(s) + inkex.errormsg(s) + sys.exit() + + +################################################################################ +### Get defs from svg +################################################################################ + def get_defs(self) : + self.defs = {} + def recursive(g) : + for i in g: + if i.tag == inkex.addNS("defs","svg") : + for j in i: + self.defs[j.get("id")] = i + if i.tag ==inkex.addNS("g",'svg') : + recursive(i) + recursive(self.document.getroot()) + + +################################################################################ +### +### Get Gcodetools info from the svg +### +################################################################################ + def get_info(self): + self.selected_paths = {} + self.paths = {} + self.orientation_points = {} + self.layers = [self.document.getroot()] + self.Zcoordinates = {} + self.transform_matrix = {} + self.transform_matrix_reverse = {} + self.Zauto_scale = {} + + def recursive_search(g, layer, selected=False): + items = g.getchildren() + items.reverse() + for i in items: + if selected: + self.selected[i.get("id")] = i + if i.tag == inkex.addNS("g",'svg') and i.get(inkex.addNS('groupmode','inkscape')) == 'layer': + self.layers += [i] + recursive_search(i,i) + elif i.get('gcodetools') == "Gcodetools orientation group" : + points = self.get_orientation_points(i) + if points != None : + self.orientation_points[layer] = self.orientation_points[layer]+[points[:]] if layer in self.orientation_points else [points[:]] + print_("Found orientation points in '%s' layer: %s" % (layer.get(inkex.addNS('label','inkscape')), points)) + else : + self.error(_("Warning! Found bad orientation points in '%s' layer. Resulting Gcode could be corrupt!") % layer.get(inkex.addNS('label','inkscape')), "bad_orientation_points_in_some_layers") + elif i.tag == inkex.addNS('path','svg'): + if "gcodetools" not in i.keys() : + self.paths[layer] = self.paths[layer] + [i] if layer in self.paths else [i] + if i.get("id") in self.selected : + self.selected_paths[layer] = self.selected_paths[layer] + [i] if layer in self.selected_paths else [i] + elif i.tag == inkex.addNS("g",'svg'): + recursive_search(i,layer, (i.get("id") in self.selected) ) + elif i.get("id") in self.selected : + self.error(_("This extension works with Paths and Dynamic Offsets and groups of them only! All other objects will be ignored!\nSolution 1: press Path->Object to path or Shift+Ctrl+C.\nSolution 2: Path->Dynamic offset or Ctrl+J.\nSolution 3: export all contours to PostScript level 2 (File->Save As->.ps) and File->Import this file."),"selection_contains_objects_that_are_not_paths") + + + recursive_search(self.document.getroot(),self.document.getroot()) + + + def get_orientation_points(self,g): + items = g.getchildren() + items.reverse() + p2, p3 = [], [] + p = None + for i in items: + if i.tag == inkex.addNS("g",'svg') and i.get("gcodetools") == "Gcodetools orientation point (2 points)": + p2 += [i] + if i.tag == inkex.addNS("g",'svg') and i.get("gcodetools") == "Gcodetools orientation point (3 points)": + p3 += [i] + if len(p2)==2 : p=p2 + elif len(p3)==3 : p=p3 + if p==None : return None + points = [] + for i in p : + point = [[],[]] + for node in i : + if node.get('gcodetools') == "Gcodetools orientation point arrow": + point[0] = self.apply_transforms(node,cubicsuperpath.parsePath(node.get("d")))[0][0][1] + if node.get('gcodetools') == "Gcodetools orientation point text": + r = re.match(r'(?i)\s*\(\s*(-?\s*\d*(?:,|\.)*\d*)\s*;\s*(-?\s*\d*(?:,|\.)*\d*)\s*;\s*(-?\s*\d*(?:,|\.)*\d*)\s*\)\s*',node.text) + point[1] = [float(r.group(1)),float(r.group(2)),float(r.group(3))] + if point[0]!=[] and point[1]!=[]: points += [point] + if len(points)==len(p2)==2 or len(points)==len(p3)==3 : return points + else : return None + +################################################################################ +### +### dxfpoints +### +################################################################################ + def dxfpoints(self): + if self.selected_paths == {}: + self.error(_("Noting is selected. Please select something to convert to drill point (dxfpoint) or clear point sign."),"warning") + for layer in self.layers : + if layer in self.selected_paths : + for path in self.selected_paths[layer]: + if self.options.dxfpoints_action == 'replace': + path.set("dxfpoint","1") + r = re.match("^\s*.\s*(\S+)",path.get("d")) + if r!=None: + print_(("got path=",r.group(1))) + path.set("d","m %s 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" % r.group(1)) + path.set("style",styles["dxf_points"]) + + if self.options.dxfpoints_action == 'save': + path.set("dxfpoint","1") + + if self.options.dxfpoints_action == 'clear' and path.get("dxfpoint") == "1": + path.set("dxfpoint","0") + +################################################################################ +### +### Laser +### +################################################################################ + def laser(self) : + + def get_boundaries(points): + minx,miny,maxx,maxy=None,None,None,None + out=[[],[],[],[]] + for p in points: + if minx==p[0]: + out[0]+=[p] + if minx==None or p[0]maxx: + maxx=p[0] + out[2]=[p] + + if maxy==p[1]: + out[3]+=[p] + if maxy==None or p[1]>maxy: + maxy=p[1] + out[3]=[p] + return out + + + def remove_duplicates(points): + i=0 + out=[] + for p in points: + for j in xrange(i,len(points)): + if p==points[j]: points[j]=[None,None] + if p!=[None,None]: out+=[p] + i+=1 + return(out) + + + def get_way_len(points): + l=0 + for i in xrange(1,len(points)): + l+=math.sqrt((points[i][0]-points[i-1][0])**2 + (points[i][1]-points[i-1][1])**2) + return l + + + def sort_dxfpoints(points): + points=remove_duplicates(points) + + ways=[ + # l=0, d=1, r=2, u=3 + [3,0], # ul + [3,2], # ur + [1,0], # dl + [1,2], # dr + [0,3], # lu + [0,1], # ld + [2,3], # ru + [2,1], # rd + ] + + minimal_way=[] + minimal_len=None + minimal_way_type=None + for w in ways: + tpoints=points[:] + cw=[] + for j in xrange(0,len(points)): + p=get_boundaries(get_boundaries(tpoints)[w[0]])[w[1]] + tpoints.remove(p[0]) + cw+=p + curlen = get_way_len(cw) + if minimal_len==None or curlen < minimal_len: + minimal_len=curlen + minimal_way=cw + minimal_way_type=w + + return minimal_way + + if self.selected_paths == {} : + paths=self.paths + self.error(_("No paths are selected! Trying to work on all available paths."),"warning") + else : + paths = self.selected_paths + + self.check_dir() + gcode = "" + + biarc_group = inkex.etree.SubElement( self.selected_paths.keys()[0] if len(self.selected_paths.keys())>0 else self.layers[0], inkex.addNS('g','svg') ) + print_(("self.layers=",self.layers)) + print_(("paths=",paths)) + for layer in self.layers : + if layer in paths : + print_(("layer",layer)) + p = [] + dxfpoints = [] + for path in paths[layer] : + print_(str(layer)) + if "d" not in path.keys() : + self.error(_("Warning: One or more paths dont have 'd' parameter, try to Ungroup (Ctrl+Shift+G) and Object to Path (Ctrl+Shift+C)!"),"selection_contains_objects_that_are_not_paths") + continue + csp = cubicsuperpath.parsePath(path.get("d")) + csp = self.apply_transforms(path, csp) + if path.get("dxfpoint") == "1": + tmp_curve=self.transform_csp(csp, layer) + x=tmp_curve[0][0][0][0] + y=tmp_curve[0][0][0][1] + print_("got dxfpoint (scaled) at (%f,%f)" % (x,y)) + dxfpoints += [[x,y]] + else: + p += csp + dxfpoints=sort_dxfpoints(dxfpoints) + curve = self.parse_curve(p, layer) + self.draw_curve(curve, layer, biarc_group) + gcode += self.generate_gcode(curve, layer, 0) + + self.export_gcode(gcode) + +################################################################################ +### +### Orientation +### +################################################################################ + def orientation(self, layer=None) : + print_("entering orientations") + if layer == None : + layer = self.current_layer if self.current_layer is not None else self.document.getroot() + if layer in self.orientation_points: + self.error(_("Active layer already has orientation points! Remove them or select another layer!"),"active_layer_already_has_orientation_points") + + orientation_group = inkex.etree.SubElement(layer, inkex.addNS('g','svg'), {"gcodetools":"Gcodetools orientation group"}) + + # translate == ['0', '-917.7043'] + if layer.get("transform") != None : + translate = layer.get("transform").replace("translate(", "").replace(")", "").split(",") + else : + translate = [0,0] + + # doc height in pixels (38 mm == 143.62204724px) + doc_height = self.unittouu(self.document.getroot().xpath('@height', namespaces=inkex.NSS)[0]) + + if self.document.getroot().get('height') == "100%" : + doc_height = 1052.3622047 + print_("Overruding height from 100 percents to %s" % doc_height) + + print_("Document height: " + str(doc_height)); + + if self.options.unit == "G21 (All units in mm)" : + points = [[0.,0.,0.],[100.,0.,0.],[0.,100.,0.]] + orientation_scale = 1 + print_("orientation_scale < 0 ===> switching to mm units=%0.10f"%orientation_scale ) + elif self.options.unit == "G20 (All units in inches)" : + points = [[0.,0.,0.],[5.,0.,0.],[0.,5.,0.]] + orientation_scale = 90 + print_("orientation_scale < 0 ===> switching to inches units=%0.10f"%orientation_scale ) + + points = points[:2] + + print_(("using orientation scale",orientation_scale,"i=",points)) + for i in points : + # X == Correct! + # si == x,y coordinate in px + # si have correct coordinates + # if layer have any tranform it will be in translate so lets add that + si = [i[0]*orientation_scale, (i[1]*orientation_scale)+float(translate[1])] + g = inkex.etree.SubElement(orientation_group, inkex.addNS('g','svg'), {'gcodetools': "Gcodetools orientation point (2 points)"}) + inkex.etree.SubElement( g, inkex.addNS('path','svg'), + { + 'style': "stroke:none;fill:#000000;", + 'd':'m %s,%s 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 z' % (si[0], -si[1]+doc_height), + 'gcodetools': "Gcodetools orientation point arrow" + }) + t = inkex.etree.SubElement( g, inkex.addNS('text','svg'), + { + 'style': "font-size:10px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#000000;fill-opacity:1;stroke:none;", + inkex.addNS("space","xml"):"preserve", + 'x': str(si[0]+10), + 'y': str(-si[1]-10+doc_height), + 'gcodetools': "Gcodetools orientation point text" + }) + t.text = "(%s; %s; %s)" % (i[0],i[1],i[2]) + + +################################################################################ +### +### Effect +### +### Main function of Gcodetools class +### +################################################################################ + def effect(self) : + global options + options = self.options + options.self = self + options.doc_root = self.document.getroot() + # define print_ function + global print_ + if self.options.log_create_log : + try : + if os.path.isfile(self.options.log_filename) : os.remove(self.options.log_filename) + f = open(self.options.log_filename,"a") + f.write("Gcodetools log file.\nStarted at %s.\n%s\n" % (time.strftime("%d.%m.%Y %H:%M:%S"),options.log_filename)) + f.write("%s tab is active.\n" % self.options.active_tab) + f.close() + except : + print_ = lambda *x : None + else : print_ = lambda *x : None + self.get_info() + if self.orientation_points == {} : + self.error(_("Orientation points have not been defined! A default set of orientation points has been automatically added."),"warning") + self.orientation( self.layers[min(0,len(self.layers)-1)] ) + self.get_info() + + self.tools = { + "name": "Laser Engraver", + "id": "Laser Engraver", + "penetration feed": self.options.laser_speed, + "feed": self.options.laser_speed, + "gcode before path": ("G4 P0 \n" + self.options.laser_command + " S" + str(int(self.options.laser_power)) + "\nG4 P" + self.options.power_delay), + "gcode after path": ("G4 P0 \n" + self.options.laser_off_command + " S0" + "\n" + "G1 F" + self.options.travel_speed), + } + + self.get_info() + self.laser() + +e = laser_gcode() +e.affect()