diff --git a/svgparse.py b/svgparse.py index f2ecb522..544f0ba6 100644 --- a/svgparse.py +++ b/svgparse.py @@ -64,6 +64,7 @@ def path2shapely(path, res=1.0): :param res: Resolution (minimum step along path) :return: Shapely geometry object """ + points = [] for component in path: @@ -86,6 +87,12 @@ def path2shapely(path, res=1.0): # How many points to use in the dicrete representation. length = component.length(res / 10.0) steps = int(length / res + 0.5) + + # solve error when step is below 1, + # it may cause other problems, but LineString needs at least two points + if steps == 0: + steps = 1 + frac = 1.0 / steps # print length, steps, frac @@ -117,9 +124,16 @@ def svgrect2shapely(rect, n_points=32): """ w = svgparselength(rect.get('width'))[0] h = svgparselength(rect.get('height'))[0] - x = svgparselength(rect.get('x'))[0] - y = svgparselength(rect.get('y'))[0] - + x_obj = rect.get('x') + if x_obj is not None: + x = svgparselength(x_obj)[0] + else: + x = 0 + y_obj = rect.get('y') + if y_obj is not None: + y = svgparselength(y_obj)[0] + else: + y = 0 rxstr = rect.get('rx') rystr = rect.get('ry') @@ -305,29 +319,31 @@ def getsvggeo(node): log.warning("Unknown kind: " + kind) geo = None - # Transformations - if 'transform' in node.attrib: - trstr = node.get('transform') - trlist = parse_svg_transform(trstr) - #log.debug(trlist) + # ignore transformation for unknown kind + if geo is not None: + # Transformations + if 'transform' in node.attrib: + trstr = node.get('transform') + trlist = parse_svg_transform(trstr) + #log.debug(trlist) - # Transformations are applied in reverse order - for tr in trlist[::-1]: - if tr[0] == 'translate': - geo = [translate(geoi, tr[1], tr[2]) for geoi in geo] - elif tr[0] == 'scale': - geo = [scale(geoi, tr[0], tr[1], origin=(0, 0)) - for geoi in geo] - elif tr[0] == 'rotate': - geo = [rotate(geoi, tr[1], origin=(tr[2], tr[3])) - for geoi in geo] - elif tr[0] == 'skew': - geo = [skew(geoi, tr[1], tr[2], origin=(0, 0)) - for geoi in geo] - elif tr[0] == 'matrix': - geo = [affine_transform(geoi, tr[1:]) for geoi in geo] - else: - raise Exception('Unknown transformation: %s', tr) + # Transformations are applied in reverse order + for tr in trlist[::-1]: + if tr[0] == 'translate': + geo = [translate(geoi, tr[1], tr[2]) for geoi in geo] + elif tr[0] == 'scale': + geo = [scale(geoi, tr[0], tr[1], origin=(0, 0)) + for geoi in geo] + elif tr[0] == 'rotate': + geo = [rotate(geoi, tr[1], origin=(tr[2], tr[3])) + for geoi in geo] + elif tr[0] == 'skew': + geo = [skew(geoi, tr[1], tr[2], origin=(0, 0)) + for geoi in geo] + elif tr[0] == 'matrix': + geo = [affine_transform(geoi, tr[1:]) for geoi in geo] + else: + raise Exception('Unknown transformation: %s', tr) return geo @@ -346,7 +362,7 @@ def parse_svg_point_list(ptliststr): pos = 0 i = 0 - for match in re.finditer(r'(\s*,\s*)|(\s+)', ptliststr): + for match in re.finditer(r'(\s*,\s*)|(\s+)', ptliststr.strip(' ')): val = float(ptliststr[pos:match.start()]) @@ -435,7 +451,7 @@ def parse_svg_transform(trstr): r'(?:' + comma_or_space_re_str + \ r'(' + number_re_str + r')' + \ comma_or_space_re_str + \ - r'(' + number_re_str + r'))?\*\)' + r'(' + number_re_str + r'))?\s*\)' matrix_re_str = r'matrix\s*\(\s*' + \ r'(' + number_re_str + r')' + comma_or_space_re_str + \ r'(' + number_re_str + r')' + comma_or_space_re_str + \ diff --git a/tclCommands/TclCommand.py b/tclCommands/TclCommand.py index b93ec752..470358fb 100644 --- a/tclCommands/TclCommand.py +++ b/tclCommands/TclCommand.py @@ -388,7 +388,11 @@ class TclCommandSignaled(TclCommand): return self.output except Exception as unknown: - error_info=sys.exc_info() + # if error happens inside thread execution, then pass correct error_info to display + if self.error_info is not None: + error_info = self.error_info + else: + error_info=sys.exc_info() self.log.error("TCL command '%s' failed." % str(self)) self.app.display_tcl_error(unknown, error_info) self.raise_tcl_unknown_error(unknown) \ No newline at end of file diff --git a/tclCommands/TclCommandImportSvg.py b/tclCommands/TclCommandImportSvg.py new file mode 100644 index 00000000..51cc1901 --- /dev/null +++ b/tclCommands/TclCommandImportSvg.py @@ -0,0 +1,81 @@ +from ObjectCollection import * +import TclCommand + + +class TclCommandImportSvg(TclCommand.TclCommandSignaled): + """ + Tcl shell command to import an SVG file as a Geometry Object. + """ + + # array of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon) + aliases = ['import_svg'] + + # dictionary of types from Tcl command, needs to be ordered + arg_names = collections.OrderedDict([ + ('filename', str) + ]) + + # dictionary of types from Tcl command, needs to be ordered , this is for options like -optionname value + option_types = collections.OrderedDict([ + ('type', str), + ('outname', str) + ]) + + # array of mandatory options for current Tcl command: required = {'name','outname'} + required = ['filename'] + + # structured help for current command, args needs to be ordered + help = { + 'main': "Import an SVG file as a Geometry Object..", + 'args': collections.OrderedDict([ + ('filename', 'Path to file to open.'), + ('type', 'Import as gerber or geometry(default).'), + ('outname', 'Name of the resulting Geometry object.') + ]), + 'examples': [] + } + + def execute(self, args, unnamed_args): + """ + execute current TCL shell command + + :param args: array of known named arguments and options + :param unnamed_args: array of other values which were passed into command + without -somename and we do not have them in known arg_names + :return: None or exception + """ + + # How the object should be initialized + def obj_init(geo_obj, app_obj): + + if not isinstance(geo_obj, Geometry): + self.raise_tcl_error('Expected Geometry or Gerber, got %s %s.' % (outname, type(geo_obj))) + + geo_obj.import_svg(filename) + + filename = args['filename'] + + if 'outname' in args: + outname = args['outname'] + else: + outname = filename.split('/')[-1].split('\\')[-1] + + if 'type' in args: + obj_type = args['type'] + else: + obj_type = 'geometry' + + if obj_type != "geometry" and obj_type != "gerber": + self.raise_tcl_error("Option type can be 'geopmetry' or 'gerber' only, got '%s'." % obj_type) + + with self.app.proc_container.new("Import SVG"): + + # Object creation + self.app.new_object(obj_type, outname, obj_init) + + # Register recent file + self.app.file_opened.emit("svg", filename) + + # GUI feedback + self.app.inform.emit("Opened: " + filename) + diff --git a/tclCommands/__init__.py b/tclCommands/__init__.py index 2f733017..0885d143 100644 --- a/tclCommands/__init__.py +++ b/tclCommands/__init__.py @@ -8,6 +8,7 @@ import tclCommands.TclCommandCncjob import tclCommands.TclCommandDrillcncjob import tclCommands.TclCommandExportGcode import tclCommands.TclCommandExteriors +import tclCommands.TclCommandImportSvg import tclCommands.TclCommandInteriors import tclCommands.TclCommandIsolate import tclCommands.TclCommandNew diff --git a/tests/svg/7segment_9,9.svg b/tests/svg/7segment_9,9.svg new file mode 100644 index 00000000..ffe7c653 --- /dev/null +++ b/tests/svg/7segment_9,9.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/svg/Arduino Nano3_pcb.svg b/tests/svg/Arduino Nano3_pcb.svg new file mode 100644 index 00000000..f1f3b0c2 --- /dev/null +++ b/tests/svg/Arduino Nano3_pcb.svg @@ -0,0 +1,468 @@ + + + + +Fritzing footprint generated by brd2svg + + + + element:J1 + + package:HEAD15-NOSS + + + + element:J2 + + package:HEAD15-NOSS-1 + + + + element:U2 + + package:SSOP28 + + + + element:U3 + + package:SOT223 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + layer 21 + + text:TX1 + + + TX1 + + + + + text:RX0 + + + RX0 + + + + + text:RST + + + RST + + + + + text:GND + + + GND + + + + + text:D2 + + + D2 + + + + + text:D3 + + + D3 + + + + + text:D4 + + + D4 + + + + + text:D5 + + + D5 + + + + + text:D6 + + + D6 + + + + + text:D7 + + + D7 + + + + + text:D8 + + + D8 + + + + + text:D9 + + + D9 + + + + + text:D10 + + + D10 + + + + + text:D11 + + + D11 + + + + + text:D12 + + + D12 + + + + + text:D13 + + + D13 + + + + + text:3V3 + + + 3V3 + + + + + text:REF + + + REF + + + + + text:A0 + + + A0 + + + + + text:A1 + + + A1 + + + + + text:A2 + + + A2 + + + + + text:A3 + + + A3 + + + + + text:A4 + + + A4 + + + + + text:A5 + + + A5 + + + + + text:A6 + + + A6 + + + + + text:A7 + + + A7 + + + + + text:5V + + + 5V + + + + + text:RST + + + RST + + + + + text:GND + + + GND + + + + + text:VIN + + + VIN + + + + + text:* + + + * + + + + + text:* + + + * + + + + + text:* + + + * + + + + + text:* + + + * + + + + + text:* + + + * + + + + + text:* + + + * + + + + + element:C1 + + package:CAP0805-NP + + + + element:C2 + + package:TAN-A + + + + element:C3 + + package:CAP0805-NP + + + + element:C4 + + package:CAP0805-NP + + + + element:C7 + + package:CAP0805-NP + + + + element:C8 + + package:TAN-A + + + + element:C9 + + package:CAP0805-NP + + + + element:D1 + + package:SOD-123 + + + + element:J1 + + package:HEAD15-NOSS + + + + element:J2 + + package:HEAD15-NOSS-1 + + + + element:RP1 + + package:RES4NT + + + + element:RP2 + + package:RES4NT + + + + element:U$4 + + package:FIDUCIAL-1X2 + + + + element:U$37 + + package:FIDUCIAL-1X2 + + + + element:U$53 + + package:FIDUCIAL-1X2 + + + + element:U$54 + + package:FIDUCIAL-1X2 + + + + element:U2 + + package:SSOP28 + + + + element:U3 + + package:SOT223 + + + + diff --git a/tests/svg/usb_connector.svg b/tests/svg/usb_connector.svg new file mode 100644 index 00000000..25db7071 --- /dev/null +++ b/tests/svg/usb_connector.svg @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/test_tcl_shell.py b/tests/test_tcl_shell.py index d36f30ed..cc415f7f 100644 --- a/tests/test_tcl_shell.py +++ b/tests/test_tcl_shell.py @@ -4,6 +4,8 @@ from PyQt4 import QtGui from PyQt4.QtCore import QThread from FlatCAMApp import App +from os import listdir +from os.path import isfile from FlatCAMObj import FlatCAMGerber, FlatCAMGeometry, FlatCAMCNCjob, FlatCAMExcellon from ObjectUI import GerberObjectUI, GeometryObjectUI from time import sleep @@ -12,11 +14,15 @@ import tempfile class TclShellTest(unittest.TestCase): + svg_files = 'tests/svg' + svg_filename = 'Arduino Nano3_pcb.svg' gerber_files = 'tests/gerber_files' copper_bottom_filename = 'detector_copper_bottom.gbr' copper_top_filename = 'detector_copper_top.gbr' cutout_filename = 'detector_contour.gbr' excellon_filename = 'detector_drill.txt' + gerber_name = "gerber" + geometry_name = "geometry" excellon_name = "excellon" gerber_top_name = "top" gerber_bottom_name = "bottom" @@ -177,4 +183,65 @@ class TclShellTest(unittest.TestCase): # mirror bottom excellon self.fc.exec_command_test('mirror %s -box %s -axis X' % (self.excellon_name, self.gerber_cutout_name)) - # TODO: tests for tcl \ No newline at end of file + # TODO: tests for tcl + + def test_import_svg(self): + """ + Test all SVG files inside svg directory. + Problematic SVG files shold be put there as test reference. + :return: + """ + + self.fc.exec_command_test('set_sys units MM') + self.fc.exec_command_test('new') + + file_list = listdir(self.svg_files) + + for svg_file in file_list: + + # import without outname + self.fc.exec_command_test('import_svg "%s/%s"' % (self.svg_files, svg_file)) + + obj = self.fc.collection.get_by_name(svg_file) + self.assertTrue(isinstance(obj, FlatCAMGeometry), + "Expected FlatCAMGeometry, instead, %s is %s" % + (svg_file, type(obj))) + + # import with outname + outname='%s-%s' % (self.geometry_name, svg_file) + self.fc.exec_command_test('import_svg "%s/%s" -outname "%s"' % (self.svg_files, svg_file, outname)) + + obj = self.fc.collection.get_by_name(outname) + self.assertTrue(isinstance(obj, FlatCAMGeometry), + "Expected FlatCAMGeometry, instead, %s is %s" % + (outname, type(obj))) + + names = self.fc.collection.get_names() + self.assertEqual(len(names), len(file_list)*2, + "Expected %d objects, found %d" % (len(file_list)*2, len(file_list))) + + def test_import_svg_as_geometry(self): + self.fc.exec_command_test('set_sys units MM') + self.fc.exec_command_test('new') + self.fc.exec_command_test('import_svg "%s/%s" -type geometry -outname "%s"' % (self.svg_files, self.svg_filename, self.geometry_name)) + + obj = self.fc.collection.get_by_name(self.geometry_name) + self.assertTrue(isinstance(obj, FlatCAMGeometry) and not isinstance(obj, FlatCAMGerber), + "Expected FlatCAMGeometry, instead, %s is %s" % + (self.geometry_name, type(obj))) + + def test_import_svg_as_gerber(self): + self.fc.exec_command_test('set_sys units MM') + self.fc.exec_command_test('new') + self.fc.exec_command_test('import_svg "%s/%s" -type gerber -outname "%s"' % (self.svg_files, self.svg_filename, self.gerber_name)) + + obj = self.fc.collection.get_by_name(self.gerber_name) + self.assertTrue(isinstance(obj, FlatCAMGerber), + "Expected FlatCAMGerber, instead, %s is %s" % + (self.gerber_name, type(obj))) + + self.fc.exec_command_test('isolate "%s"' % self.gerber_name) + obj = self.fc.collection.get_by_name(self.gerber_name+'_iso') + self.assertTrue(isinstance(obj, FlatCAMGeometry), + "Expected FlatCAMGeometry, instead, %s is %s" % + (self.gerber_name+'_iso', type(obj)))