From 8f6a455562f3958021eeb8b98bc5af7038f9c2cb Mon Sep 17 00:00:00 2001 From: Denvi Date: Sat, 5 Dec 2015 00:14:00 +0500 Subject: [PATCH 001/134] Pan view test. --- FlatCAMDraw.py | 3 ++- PlotCanvas.py | 59 ++++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 52 insertions(+), 10 deletions(-) diff --git a/FlatCAMDraw.py b/FlatCAMDraw.py index 29eb478..fbfdc05 100644 --- a/FlatCAMDraw.py +++ b/FlatCAMDraw.py @@ -896,7 +896,8 @@ class FlatCAMDraw(QtCore.QObject): :param event: Event object dispatched by Matplotlib :return: None """ - if self.active_tool is not None: + # Selection with left mouse button + if self.active_tool is not None and event.button is 1: # Dispatch event to active_tool msg = self.active_tool.click(self.snap(event.xdata, event.ydata)) self.app.info(msg) diff --git a/PlotCanvas.py b/PlotCanvas.py index 8e4f58d..0fdef5c 100644 --- a/PlotCanvas.py +++ b/PlotCanvas.py @@ -66,6 +66,8 @@ class PlotCanvas: self.background = self.canvas.copy_from_bbox(self.axes.bbox) # Events + self.canvas.mpl_connect('button_press_event', self.on_mouse_press) + self.canvas.mpl_connect('button_release_event', self.on_mouse_release) self.canvas.mpl_connect('motion_notify_event', self.on_mouse_move) #self.canvas.connect('configure-event', self.auto_adjust_axes) self.canvas.mpl_connect('resize_event', self.auto_adjust_axes) @@ -74,10 +76,14 @@ class PlotCanvas: self.canvas.mpl_connect('scroll_event', self.on_scroll) self.canvas.mpl_connect('key_press_event', self.on_key_down) self.canvas.mpl_connect('key_release_event', self.on_key_up) + self.canvas.mpl_connect('draw_event', self.on_draw) self.mouse = [0, 0] self.key = None + self.pan_axes = [] + self.panning = False + def on_key_down(self, event): """ @@ -148,7 +154,7 @@ class PlotCanvas: self.axes.grid(True) # Re-draw - self.canvas.draw() + self.canvas.draw_idle() def adjust_axes(self, xmin, ymin, xmax, ymax): """ @@ -204,9 +210,8 @@ class PlotCanvas: ax.set_ylim((ymin, ymax)) ax.set_position([x_ratio, y_ratio, 1 - 2 * x_ratio, 1 - 2 * y_ratio]) - # Re-draw + # Sync re-draw to proper paint on form resize self.canvas.draw() - self.background = self.canvas.copy_from_bbox(self.axes.bbox) def auto_adjust_axes(self, *args): """ @@ -257,9 +262,8 @@ class PlotCanvas: ax.set_xlim((xmin, xmax)) ax.set_ylim((ymin, ymax)) - # Re-draw - self.canvas.draw() - self.background = self.canvas.copy_from_bbox(self.axes.bbox) + # Async re-draw + self.canvas.draw_idle() def pan(self, x, y): xmin, xmax = self.axes.get_xlim() @@ -273,8 +277,7 @@ class PlotCanvas: ax.set_ylim((ymin + y * height, ymax + y * height)) # Re-draw - self.canvas.draw() - self.background = self.canvas.copy_from_bbox(self.axes.bbox) + self.canvas.draw_idle() def new_axes(self, name): """ @@ -326,12 +329,50 @@ class PlotCanvas: self.pan(0, -0.3) return + def on_mouse_press(self, event): + + # Check for middle mouse button press + if event.button == 2: + + # Prepare axes for pan (using 'matplotlib' pan function) + self.pan_axes = [] + for a in self.figure.get_axes(): + if (event.x is not None and event.y is not None and a.in_axes(event) and + a.get_navigate() and a.can_pan()): + a.start_pan(event.x, event.y, 1) + self.pan_axes.append(a) + + # Set pan view flag + if len(self.pan_axes) > 0: self.panning = True; + + def on_mouse_release(self, event): + + # Check for middle mouse button release to complete pan procedure + if event.button == 2: + for a in self.pan_axes: + a.end_pan() + + # Clear pan flag + self.panning = False + def on_mouse_move(self, event): """ - Mouse movement event hadler. Stores the coordinates. + Mouse movement event hadler. Stores the coordinates. Updates view on pan. :param event: Contains information about the event. :return: None """ self.mouse = [event.xdata, event.ydata] + # Update pan view on mouse move + if self.panning is True: + for a in self.pan_axes: + a.drag_pan(1, event.key, event.x, event.y) + + # Async re-draw (redraws only on thread idle state, uses timer on backend) + self.canvas.draw_idle() + + def on_draw(self, renderer): + + # Store background on canvas redraw + self.background = self.canvas.copy_from_bbox(self.axes.bbox) From fdf809774f5688e7ae6a38239d3ec152dfe09240 Mon Sep 17 00:00:00 2001 From: Juan Pablo Caram Date: Fri, 18 Dec 2015 12:49:52 -0500 Subject: [PATCH 002/134] Basic support for importing SVG. Via shell only at this time. See issue #179. --- FlatCAMApp.py | 45 +++++++ PlotCanvas.py | 2 +- camlib.py | 41 ++++++- svgparse.py | 268 ++++++++++++++++++++++++++++++++++++++++++ tests/svg/drawing.svg | 126 ++++++++++++++++++++ 5 files changed, 480 insertions(+), 2 deletions(-) create mode 100644 svgparse.py create mode 100644 tests/svg/drawing.svg diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 9e4eab4..9ed0c8e 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -1604,6 +1604,34 @@ class App(QtCore.QObject): else: self.inform.emit("Project copy saved to: " + self.project_filename) + def import_svg(self, filename, outname=None): + """ + Adds a new Geometry Object to the projects and populates + it with shapes extracted from the SVG file. + + :param filename: Path to the SVG file. + :param outname: + :return: + """ + + def obj_init(geo_obj, app_obj): + + geo_obj.import_svg(filename) + + with self.proc_container.new("Importing SVG") as proc: + + # Object name + name = outname or filename.split('/')[-1].split('\\')[-1] + + self.new_object("geometry", name, obj_init) + + # TODO: No support for this yet. + # Register recent file + # self.file_opened.emit("gerber", filename) + + # GUI feedback + self.inform.emit("Opened: " + filename) + def open_gerber(self, filename, follow=False, outname=None): """ Opens a Gerber file, parses it and creates a new object for @@ -1959,6 +1987,17 @@ class App(QtCore.QObject): return a, kwa + def import_svg(filename, *args): + a, kwa = h(*args) + types = {'outname': str} + + for key in kwa: + if key not in types: + return 'Unknown parameter: %s' % key + kwa[key] = types[key](kwa[key]) + + self.import_svg(str(filename), **kwa) + def open_gerber(filename, *args): a, kwa = h(*args) types = {'follow': bool, @@ -2556,6 +2595,12 @@ class App(QtCore.QObject): 'fcn': shelp, 'help': "Shows list of commands." }, + 'import_svg': { + 'fcn': import_svg, + 'help': "Import an SVG file as a Geometry Object.\n" + + "> import_svg " + + " filename: Path to the file to import." + }, 'open_gerber': { 'fcn': open_gerber, 'help': "Opens a Gerber file.\n' +" diff --git a/PlotCanvas.py b/PlotCanvas.py index 0fdef5c..c7fb19b 100644 --- a/PlotCanvas.py +++ b/PlotCanvas.py @@ -124,7 +124,7 @@ class PlotCanvas: def connect(self, event_name, callback): """ - Attach an event handler to the canvas through the native GTK interface. + Attach an event handler to the canvas through the native Qt interface. :param event_name: Name of the event :type event_name: str diff --git a/camlib.py b/camlib.py index ae85dda..15ef5c0 100644 --- a/camlib.py +++ b/camlib.py @@ -42,6 +42,16 @@ import simplejson as json # TODO: Commented for FlatCAM packaging with cx_freeze #from matplotlib.pyplot import plot, subplot +import xml.etree.ElementTree as ET +from svg.path import Path, Line, Arc, CubicBezier, QuadraticBezier, parse_path +import itertools + +import xml.etree.ElementTree as ET +from svg.path import Path, Line, Arc, CubicBezier, QuadraticBezier, parse_path + + +from svgparse import * + import logging log = logging.getLogger('base2') @@ -193,7 +203,6 @@ class Geometry(object): return interiors - def get_exteriors(self, geometry=None): """ Returns all exteriors of polygons in geometry. Uses @@ -344,6 +353,36 @@ class Geometry(object): return False + def import_svg(self, filename): + """ + Imports shapes from an SVG file into the object's geometry. + + :param filename: Path to the SVG file. + :return: None + """ + + # Parse into list of shapely objects + svg_tree = ET.parse(filename) + svg_root = svg_tree.getroot() + + # Change origin to bottom left + h = float(svg_root.get('height')) + # w = float(svg_root.get('width')) + geos = getsvggeo(svg_root) + geo_flip = [translate(scale(g, 1.0, -1.0, origin=(0, 0)), yoff=h) for g in geos] + + # Add to object + if self.solid_geometry is None: + self.solid_geometry = [] + + if type(self.solid_geometry) is list: + self.solid_geometry.append(cascaded_union(geo_flip)) + else: # It's shapely geometry + self.solid_geometry = cascaded_union([self.solid_geometry, + cascaded_union(geo_flip)]) + + return + def size(self): """ Returns (width, height) of rectangular diff --git a/svgparse.py b/svgparse.py new file mode 100644 index 0000000..89f858e --- /dev/null +++ b/svgparse.py @@ -0,0 +1,268 @@ +############################################################ +# FlatCAM: 2D Post-processing for Manufacturing # +# http://flatcam.org # +# Author: Juan Pablo Caram (c) # +# Date: 12/18/2015 # +# MIT Licence # +############################################################ + +import xml.etree.ElementTree as ET +import re +import itertools +from svg.path import Path, Line, Arc, CubicBezier, QuadraticBezier, parse_path +from shapely.geometry import LinearRing, LineString +from shapely.affinity import translate, rotate, scale, skew, affine_transform + + +def path2shapely(path, res=1.0): + """ + Converts an svg.path.Path into a Shapely + LinearRing or LinearString. + + :rtype : LinearRing + :rtype : LineString + :param path: svg.path.Path instance + :param res: Resolution (minimum step along path) + :return: Shapely geometry object + """ + points = [] + + for component in path: + + # Line + if isinstance(component, Line): + start = component.start + x, y = start.real, start.imag + if len(points) == 0 or points[-1] != (x, y): + points.append((x, y)) + end = component.end + points.append((end.real, end.imag)) + continue + + # Arc, CubicBezier or QuadraticBezier + if isinstance(component, Arc) or \ + isinstance(component, CubicBezier) or \ + isinstance(component, QuadraticBezier): + length = component.length(res / 10.0) + steps = int(length / res + 0.5) + frac = 1.0 / steps + print length, steps, frac + for i in range(steps): + point = component.point(i * frac) + x, y = point.real, point.imag + if len(points) == 0 or points[-1] != (x, y): + points.append((x, y)) + end = component.point(1.0) + points.append((end.real, end.imag)) + continue + + print "I don't know what this is:", component + continue + + if path.closed: + return LinearRing(points) + else: + return LineString(points) + + +def svgrect2shapely(rect): + w = float(rect.get('width')) + h = float(rect.get('height')) + x = float(rect.get('x')) + y = float(rect.get('y')) + pts = [ + (x, y), (x + w, y), (x + w, y + h), (x, y + h), (x, y) + ] + return LinearRing(pts) + + +def getsvggeo(node): + """ + Extracts and flattens all geometry from an SVG node + into a list of Shapely geometry. + + :param node: + :return: + """ + kind = re.search('(?:\{.*\})?(.*)$', node.tag).group(1) + geo = [] + + # Recurse + if len(node) > 0: + for child in node: + subgeo = getsvggeo(child) + if subgeo is not None: + geo += subgeo + + # Parse + elif kind == 'path': + print "***PATH***" + P = parse_path(node.get('d')) + P = path2shapely(P) + geo = [P] + + elif kind == 'rect': + print "***RECT***" + R = svgrect2shapely(node) + geo = [R] + + else: + print "Unknown kind:", kind + geo = None + + # Transformations + if 'transform' in node.attrib: + trstr = node.get('transform') + trlist = parse_svg_transform(trstr) + print 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) + + return geo + + +def parse_svg_transform(trstr): + """ + Parses an SVG transform string into a list + of transform names and their parameters. + + Possible transformations are: + + * Translate: translate( []), which specifies + a translation by tx and ty. If is not provided, + it is assumed to be zero. Result is + ['translate', tx, ty] + + * Scale: scale( []), which specifies a scale operation + by sx and sy. If is not provided, it is assumed to be + equal to . Result is: ['scale', sx, sy] + + * Rotate: rotate( [ ]), which specifies + a rotation by degrees about a given point. + If optional parameters and are not supplied, + the rotate is about the origin of the current user coordinate + system. Result is: ['rotate', rotate-angle, cx, cy] + + * Skew: skewX(), which specifies a skew + transformation along the x-axis. skewY(), which + specifies a skew transformation along the y-axis. + Result is ['skew', angle-x, angle-y] + + * Matrix: matrix( ), which specifies a + transformation in the form of a transformation matrix of six + values. matrix(a,b,c,d,e,f) is equivalent to applying the + transformation matrix [a b c d e f]. Result is + ['matrix', a, b, c, d, e, f] + + :param trstr: SVG transform string. + :type trstr: str + :return: List of transforms. + :rtype: list + """ + trlist = [] + + assert isinstance(trstr, str) + trstr = trstr.strip(' ') + + num_re_str = r'[\+\-]?[0-9\.e]+' # TODO: Negative exponents missing + comma_or_space_re_str = r'(?:(?:\s+)|(?:\s*,\s*))' + translate_re_str = r'translate\s*\(\s*(' + \ + num_re_str + r')' + \ + r'(?:' + comma_or_space_re_str + \ + r'(' + num_re_str + r'))?\s*\)' + scale_re_str = r'scale\s*\(\s*(' + \ + num_re_str + r')' + \ + r'(?:' + comma_or_space_re_str + \ + r'(' + num_re_str + r'))?\s*\)' + skew_re_str = r'skew([XY])\s*\(\s*(' + \ + num_re_str + r')\s*\)' + rotate_re_str = r'rotate\s*\(\s*(' + \ + num_re_str + r')' + \ + r'(?:' + comma_or_space_re_str + \ + r'(' + num_re_str + r')' + \ + comma_or_space_re_str + \ + r'(' + num_re_str + r'))?\*\)' + matrix_re_str = r'matrix\s*\(\s*' + \ + r'(' + num_re_str + r')' + comma_or_space_re_str + \ + r'(' + num_re_str + r')' + comma_or_space_re_str + \ + r'(' + num_re_str + r')' + comma_or_space_re_str + \ + r'(' + num_re_str + r')' + comma_or_space_re_str + \ + r'(' + num_re_str + r')' + comma_or_space_re_str + \ + r'(' + num_re_str + r')\s*\)' + + while len(trstr) > 0: + match = re.search(r'^' + translate_re_str, trstr) + if match: + trlist.append([ + 'translate', + float(match.group(1)), + float(match.group(2)) if match.group else 0.0 + ]) + trstr = trstr[len(match.group(0)):].strip(' ') + continue + + match = re.search(r'^' + scale_re_str, trstr) + if match: + trlist.append([ + 'translate', + float(match.group(1)), + float(match.group(2)) if match.group else float(match.group(1)) + ]) + trstr = trstr[len(match.group(0)):].strip(' ') + continue + + match = re.search(r'^' + skew_re_str, trstr) + if match: + trlist.append([ + 'skew', + float(match.group(2)) if match.group(1) == 'X' else 0.0, + float(match.group(2)) if match.group(1) == 'Y' else 0.0 + ]) + trstr = trstr[len(match.group(0)):].strip(' ') + continue + + match = re.search(r'^' + rotate_re_str, trstr) + if match: + trlist.append([ + 'rotate', + float(match.group(1)), + float(match.group(2)) if match.group(2) else 0.0, + float(match.group(3)) if match.group(3) else 0.0 + ]) + trstr = trstr[len(match.group(0)):].strip(' ') + continue + + match = re.search(r'^' + matrix_re_str, trstr) + if match: + trlist.append(['matrix'] + [float(x) for x in match.groups()]) + trstr = trstr[len(match.group(0)):].strip(' ') + continue + + raise Exception("Don't know how to parse: %s" % trstr) + + return trlist + + +if __name__ == "__main__": + tree = ET.parse('tests/svg/drawing.svg') + root = tree.getroot() + ns = re.search(r'\{(.*)\}', root.tag).group(1) + print ns + for geo in getsvggeo(root): + print geo \ No newline at end of file diff --git a/tests/svg/drawing.svg b/tests/svg/drawing.svg new file mode 100644 index 0000000..7feb03a --- /dev/null +++ b/tests/svg/drawing.svg @@ -0,0 +1,126 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + From d3ed12e5def933997c9f3e13995339c5e671c3d6 Mon Sep 17 00:00:00 2001 From: Juan Pablo Caram Date: Fri, 18 Dec 2015 16:43:47 -0500 Subject: [PATCH 003/134] Added SVG importing support to the GUI menu. See issue #179. --- FlatCAMApp.py | 24 ++++++++++++++++++++++++ FlatCAMGUI.py | 6 +++++- camlib.py | 1 + svgparse.py | 49 ++++++++++++++++++++++++++++++++++++++++++++++--- 4 files changed, 76 insertions(+), 4 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 9ed0c8e..c15347f 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -430,6 +430,7 @@ class App(QtCore.QObject): self.ui.menufileopenexcellon.triggered.connect(self.on_fileopenexcellon) self.ui.menufileopengcode.triggered.connect(self.on_fileopengcode) self.ui.menufileopenproject.triggered.connect(self.on_file_openproject) + self.ui.menufileimportsvg.triggered.connect(self.on_file_importsvg) self.ui.menufilesaveproject.triggered.connect(self.on_file_saveproject) self.ui.menufilesaveprojectas.triggered.connect(self.on_file_saveprojectas) self.ui.menufilesaveprojectcopy.triggered.connect(lambda: self.on_file_saveprojectas(make_copy=True)) @@ -1543,6 +1544,29 @@ class App(QtCore.QObject): # thread safe. The new_project() self.open_project(filename) + def on_file_importsvg(self): + """ + Callback for menu item File->Import SVG. + + :return: None + """ + self.report_usage("on_file_importsvg") + App.log.debug("on_file_importsvg()") + + try: + filename = QtGui.QFileDialog.getOpenFileName(caption="Import SVG", + directory=self.get_last_folder()) + except TypeError: + filename = QtGui.QFileDialog.getOpenFileName(caption="Import SVG") + + filename = str(filename) + + if str(filename) == "": + self.inform.emit("Open cancelled.") + else: + self.worker_task.emit({'fcn': self.import_svg, + 'params': [filename]}) + def on_file_saveproject(self): """ Callback for menu item File->Save Project. Saves the project to diff --git a/FlatCAMGUI.py b/FlatCAMGUI.py index 4e5d467..a0ffe96 100644 --- a/FlatCAMGUI.py +++ b/FlatCAMGUI.py @@ -28,7 +28,7 @@ class FlatCAMGUI(QtGui.QMainWindow): # Recent self.recent = self.menufile.addMenu(QtGui.QIcon('share/folder16.png'), "Open recent ...") - # Open gerber + # Open gerber ... self.menufileopengerber = QtGui.QAction(QtGui.QIcon('share/folder16.png'), 'Open &Gerber ...', self) self.menufile.addAction(self.menufileopengerber) @@ -40,6 +40,10 @@ class FlatCAMGUI(QtGui.QMainWindow): self.menufileopengcode = QtGui.QAction(QtGui.QIcon('share/folder16.png'), 'Open G-&Code ...', self) self.menufile.addAction(self.menufileopengcode) + # Import SVG ... + self.menufileimportsvg = QtGui.QAction(QtGui.QIcon('share/folder16.png'), 'Import &SVG ...', self) + self.menufile.addAction(self.menufileimportsvg) + # Open Project ... self.menufileopenproject = QtGui.QAction(QtGui.QIcon('share/folder16.png'), 'Open &Project ...', self) self.menufile.addAction(self.menufileopenproject) diff --git a/camlib.py b/camlib.py index 15ef5c0..576aac0 100644 --- a/camlib.py +++ b/camlib.py @@ -358,6 +358,7 @@ class Geometry(object): Imports shapes from an SVG file into the object's geometry. :param filename: Path to the SVG file. + :type filename: str :return: None """ diff --git a/svgparse.py b/svgparse.py index 89f858e..7643a6d 100644 --- a/svgparse.py +++ b/svgparse.py @@ -2,15 +2,24 @@ # FlatCAM: 2D Post-processing for Manufacturing # # http://flatcam.org # # Author: Juan Pablo Caram (c) # -# Date: 12/18/2015 # +# Date: 12/18/2015 # # MIT Licence # +# # +# SVG Features supported: # +# * Groups # +# * Rectangles # +# * Circles # +# * Paths # +# * All transformations # +# # +# Reference: www.w3.org/TR/SVG/Overview.html # ############################################################ import xml.etree.ElementTree as ET import re import itertools from svg.path import Path, Line, Arc, CubicBezier, QuadraticBezier, parse_path -from shapely.geometry import LinearRing, LineString +from shapely.geometry import LinearRing, LineString, Point from shapely.affinity import translate, rotate, scale, skew, affine_transform @@ -43,10 +52,13 @@ def path2shapely(path, res=1.0): if isinstance(component, Arc) or \ isinstance(component, CubicBezier) or \ isinstance(component, QuadraticBezier): + + # How many points to use in the dicrete representation. length = component.length(res / 10.0) steps = int(length / res + 0.5) frac = 1.0 / steps - print length, steps, frac + + # print length, steps, frac for i in range(steps): point = component.point(i * frac) x, y = point.real, point.imag @@ -66,6 +78,16 @@ def path2shapely(path, res=1.0): def svgrect2shapely(rect): + """ + Converts an SVG rect into Shapely geometry. + + :param rect: Rect Element + :type rect: xml.etree.ElementTree.Element + :return: shapely.geometry.polygon.LinearRing + + :param rect: + :return: + """ w = float(rect.get('width')) h = float(rect.get('height')) x = float(rect.get('x')) @@ -76,6 +98,22 @@ def svgrect2shapely(rect): return LinearRing(pts) +def svgcircle2shapely(circle): + """ + Converts an SVG circle into Shapely geometry. + + :param circle: Circle Element + :type circle: xml.etree.ElementTree.Element + :return: shapely.geometry.polygon.LinearRing + """ + cx = float(circle.get('cx')) + cy = float(circle.get('cy')) + r = float(circle.get('r')) + + # TODO: No resolution specified. + return Point(cx, cy).buffer(r) + + def getsvggeo(node): """ Extracts and flattens all geometry from an SVG node @@ -106,6 +144,11 @@ def getsvggeo(node): R = svgrect2shapely(node) geo = [R] + elif kind == 'circle': + print "***CIRCLE***" + C = svgcircle2shapely(node) + geo = [C] + else: print "Unknown kind:", kind geo = None From 67ef16e77673291c1d44275b6c084c0033ee39f7 Mon Sep 17 00:00:00 2001 From: Juan Pablo Caram Date: Sun, 20 Dec 2015 20:51:33 -0500 Subject: [PATCH 004/134] SVG: Accept but ignore units in length. --- FlatCAMGUI.py | 8 ++++---- camlib.py | 13 ++++++++----- svgparse.py | 23 ++++++++++++++++++++--- 3 files changed, 32 insertions(+), 12 deletions(-) diff --git a/FlatCAMGUI.py b/FlatCAMGUI.py index a0ffe96..bf9988d 100644 --- a/FlatCAMGUI.py +++ b/FlatCAMGUI.py @@ -40,14 +40,14 @@ class FlatCAMGUI(QtGui.QMainWindow): self.menufileopengcode = QtGui.QAction(QtGui.QIcon('share/folder16.png'), 'Open G-&Code ...', self) self.menufile.addAction(self.menufileopengcode) - # Import SVG ... - self.menufileimportsvg = QtGui.QAction(QtGui.QIcon('share/folder16.png'), 'Import &SVG ...', self) - self.menufile.addAction(self.menufileimportsvg) - # Open Project ... self.menufileopenproject = QtGui.QAction(QtGui.QIcon('share/folder16.png'), 'Open &Project ...', self) self.menufile.addAction(self.menufileopenproject) + # Import SVG ... + self.menufileimportsvg = QtGui.QAction(QtGui.QIcon('share/folder16.png'), 'Import &SVG ...', self) + self.menufile.addAction(self.menufileimportsvg) + # Save Project self.menufilesaveproject = QtGui.QAction(QtGui.QIcon('share/floppy16.png'), '&Save Project', self) self.menufile.addAction(self.menufilesaveproject) diff --git a/camlib.py b/camlib.py index 576aac0..83721f3 100644 --- a/camlib.py +++ b/camlib.py @@ -353,7 +353,7 @@ class Geometry(object): return False - def import_svg(self, filename): + def import_svg(self, filename, flip=True): """ Imports shapes from an SVG file into the object's geometry. @@ -367,20 +367,23 @@ class Geometry(object): svg_root = svg_tree.getroot() # Change origin to bottom left - h = float(svg_root.get('height')) + # h = float(svg_root.get('height')) # w = float(svg_root.get('width')) + h = svgparselength(svg_root.get('height'))[0] # TODO: No units support yet geos = getsvggeo(svg_root) - geo_flip = [translate(scale(g, 1.0, -1.0, origin=(0, 0)), yoff=h) for g in geos] + + if flip: + geos = [translate(scale(g, 1.0, -1.0, origin=(0, 0)), yoff=h) for g in geos] # Add to object if self.solid_geometry is None: self.solid_geometry = [] if type(self.solid_geometry) is list: - self.solid_geometry.append(cascaded_union(geo_flip)) + self.solid_geometry.append(cascaded_union(geos)) else: # It's shapely geometry self.solid_geometry = cascaded_union([self.solid_geometry, - cascaded_union(geo_flip)]) + cascaded_union(geos)]) return diff --git a/svgparse.py b/svgparse.py index 7643a6d..b980b31 100644 --- a/svgparse.py +++ b/svgparse.py @@ -23,6 +23,20 @@ from shapely.geometry import LinearRing, LineString, Point from shapely.affinity import translate, rotate, scale, skew, affine_transform +def svgparselength(lengthstr): + + integer_re_str = r'[+-]?[0-9]+' + number_re_str = r'(?:' + integer_re_str + r'(?:[Ee]' + integer_re_str + r')?' + r')|' + \ + r'(?: [+-]?[0-9]*\.[0-9]+(?:[Ee]' + integer_re_str + ')?)' + length_re_str = r'(' + number_re_str + r')(em|ex|px|in|cm|mm|pt|pc|%)?' + + match = re.search(length_re_str, lengthstr) + if match: + return float(match.group(1)), match.group(2) + + raise Exception('Cannot parse SVG length: %s' % lengthstr) + + def path2shapely(path, res=1.0): """ Converts an svg.path.Path into a Shapely @@ -106,9 +120,12 @@ def svgcircle2shapely(circle): :type circle: xml.etree.ElementTree.Element :return: shapely.geometry.polygon.LinearRing """ - cx = float(circle.get('cx')) - cy = float(circle.get('cy')) - r = float(circle.get('r')) + # cx = float(circle.get('cx')) + # cy = float(circle.get('cy')) + # r = float(circle.get('r')) + cx = svgparselength(circle.get('cx'))[0] # TODO: No units support yet + cy = svgparselength(circle.get('cy'))[1] # TODO: No units support yet + r = svgparselength(circle.get('r'))[0] # TODO: No units support yet # TODO: No resolution specified. return Point(cx, cy).buffer(r) From aa41d8093afee08c5d8ccd18bd59bc8e1b89b624 Mon Sep 17 00:00:00 2001 From: Juan Pablo Caram Date: Sun, 20 Dec 2015 21:49:48 -0500 Subject: [PATCH 005/134] Fixed regex for SVG numbers. --- svgparse.py | 43 +++++++++++++++++++++++++------------------ 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/svgparse.py b/svgparse.py index b980b31..8b04dac 100644 --- a/svgparse.py +++ b/svgparse.py @@ -26,8 +26,8 @@ from shapely.affinity import translate, rotate, scale, skew, affine_transform def svgparselength(lengthstr): integer_re_str = r'[+-]?[0-9]+' - number_re_str = r'(?:' + integer_re_str + r'(?:[Ee]' + integer_re_str + r')?' + r')|' + \ - r'(?: [+-]?[0-9]*\.[0-9]+(?:[Ee]' + integer_re_str + ')?)' + number_re_str = r'(?:[+-]?[0-9]*\.[0-9]+(?:[Ee]' + integer_re_str + ')?' + r')|' + \ + r'(?:' + integer_re_str + r'(?:[Ee]' + integer_re_str + r')?)' length_re_str = r'(' + number_re_str + r')(em|ex|px|in|cm|mm|pt|pc|%)?' match = re.search(length_re_str, lengthstr) @@ -230,6 +230,9 @@ def parse_svg_transform(trstr): transformation matrix [a b c d e f]. Result is ['matrix', a, b, c, d, e, f] + Note: All parameters to the transformations are "numbers", + i.e. no units present. + :param trstr: SVG transform string. :type trstr: str :return: List of transforms. @@ -240,31 +243,35 @@ def parse_svg_transform(trstr): assert isinstance(trstr, str) trstr = trstr.strip(' ') - num_re_str = r'[\+\-]?[0-9\.e]+' # TODO: Negative exponents missing + integer_re_str = r'[+-]?[0-9]+' + number_re_str = r'(?:[+-]?[0-9]*\.[0-9]+(?:[Ee]' + integer_re_str + ')?' + r')|' + \ + r'(?:' + integer_re_str + r'(?:[Ee]' + integer_re_str + r')?)' + + # num_re_str = r'[\+\-]?[0-9\.e]+' # TODO: Negative exponents missing comma_or_space_re_str = r'(?:(?:\s+)|(?:\s*,\s*))' translate_re_str = r'translate\s*\(\s*(' + \ - num_re_str + r')' + \ - r'(?:' + comma_or_space_re_str + \ - r'(' + num_re_str + r'))?\s*\)' + number_re_str + r')(?:' + \ + comma_or_space_re_str + \ + r'(' + number_re_str + r'))?\s*\)' scale_re_str = r'scale\s*\(\s*(' + \ - num_re_str + r')' + \ + number_re_str + r')' + \ r'(?:' + comma_or_space_re_str + \ - r'(' + num_re_str + r'))?\s*\)' + r'(' + number_re_str + r'))?\s*\)' skew_re_str = r'skew([XY])\s*\(\s*(' + \ - num_re_str + r')\s*\)' + number_re_str + r')\s*\)' rotate_re_str = r'rotate\s*\(\s*(' + \ - num_re_str + r')' + \ + number_re_str + r')' + \ r'(?:' + comma_or_space_re_str + \ - r'(' + num_re_str + r')' + \ + r'(' + number_re_str + r')' + \ comma_or_space_re_str + \ - r'(' + num_re_str + r'))?\*\)' + r'(' + number_re_str + r'))?\*\)' matrix_re_str = r'matrix\s*\(\s*' + \ - r'(' + num_re_str + r')' + comma_or_space_re_str + \ - r'(' + num_re_str + r')' + comma_or_space_re_str + \ - r'(' + num_re_str + r')' + comma_or_space_re_str + \ - r'(' + num_re_str + r')' + comma_or_space_re_str + \ - r'(' + num_re_str + r')' + comma_or_space_re_str + \ - r'(' + num_re_str + r')\s*\)' + r'(' + number_re_str + r')' + comma_or_space_re_str + \ + r'(' + number_re_str + r')' + comma_or_space_re_str + \ + r'(' + number_re_str + r')' + comma_or_space_re_str + \ + r'(' + number_re_str + r')' + comma_or_space_re_str + \ + r'(' + number_re_str + r')' + comma_or_space_re_str + \ + r'(' + number_re_str + r')\s*\)' while len(trstr) > 0: match = re.search(r'^' + translate_re_str, trstr) From 2354116e377976743369e37483c87f56aebbe86a Mon Sep 17 00:00:00 2001 From: Juan Pablo Caram Date: Sun, 20 Dec 2015 21:57:27 -0500 Subject: [PATCH 006/134] SVG supported in "recent files" menu. --- FlatCAMApp.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index c15347f..69d4fa1 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -1649,9 +1649,8 @@ class App(QtCore.QObject): self.new_object("geometry", name, obj_init) - # TODO: No support for this yet. # Register recent file - # self.file_opened.emit("gerber", filename) + self.file_opened.emit("svg", filename) # GUI feedback self.inform.emit("Opened: " + filename) @@ -2896,14 +2895,16 @@ class App(QtCore.QObject): "gerber": "share/flatcam_icon16.png", "excellon": "share/drill16.png", "cncjob": "share/cnc16.png", - "project": "share/project16.png" + "project": "share/project16.png", + "svg": "share/geometry16.png" } openers = { 'gerber': lambda fname: self.worker_task.emit({'fcn': self.open_gerber, 'params': [fname]}), 'excellon': lambda fname: self.worker_task.emit({'fcn': self.open_excellon, 'params': [fname]}), 'cncjob': lambda fname: self.worker_task.emit({'fcn': self.open_gcode, 'params': [fname]}), - 'project': self.open_project + 'project': self.open_project, + 'svg': self.import_svg } # Open file From b46d2b5f2d7853ae76b454ac636345f76efc5344 Mon Sep 17 00:00:00 2001 From: Juan Pablo Caram Date: Thu, 24 Dec 2015 11:10:41 -0500 Subject: [PATCH 007/134] SVG ellipse support. --- FlatCAMGUI.py | 2 +- svgparse.py | 32 +++++++++++++++++++++++++++++++- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/FlatCAMGUI.py b/FlatCAMGUI.py index bf9988d..64cdac0 100644 --- a/FlatCAMGUI.py +++ b/FlatCAMGUI.py @@ -244,7 +244,7 @@ class FlatCAMGUI(QtGui.QMainWindow): self.setWindowIcon(self.app_icon) self.setGeometry(100, 100, 1024, 650) - self.setWindowTitle('FlatCAM %s' % version) + self.setWindowTitle('FlatCAM %s - Development Version' % version) self.show() def closeEvent(self, event): diff --git a/svgparse.py b/svgparse.py index 8b04dac..69331d3 100644 --- a/svgparse.py +++ b/svgparse.py @@ -21,6 +21,7 @@ import itertools from svg.path import Path, Line, Arc, CubicBezier, QuadraticBezier, parse_path from shapely.geometry import LinearRing, LineString, Point from shapely.affinity import translate, rotate, scale, skew, affine_transform +import numpy as np def svgparselength(lengthstr): @@ -124,13 +125,37 @@ def svgcircle2shapely(circle): # cy = float(circle.get('cy')) # r = float(circle.get('r')) cx = svgparselength(circle.get('cx'))[0] # TODO: No units support yet - cy = svgparselength(circle.get('cy'))[1] # TODO: No units support yet + cy = svgparselength(circle.get('cy'))[0] # TODO: No units support yet r = svgparselength(circle.get('r'))[0] # TODO: No units support yet # TODO: No resolution specified. return Point(cx, cy).buffer(r) +def svgellipse2shapely(ellipse, n_points=32): + """ + Converts an SVG ellipse into Shapely geometry + + :param ellipse: Ellipse Element + :type ellipse: xml.etree.ElementTree.Element + :param n_points: Number of discrete points in output. + :return: shapely.geometry.polygon.LinearRing + """ + + cx = svgparselength(ellipse.get('cx'))[0] # TODO: No units support yet + cy = svgparselength(ellipse.get('cy'))[0] # TODO: No units support yet + + rx = svgparselength(ellipse.get('rx'))[0] # TODO: No units support yet + ry = svgparselength(ellipse.get('ry'))[0] # TODO: No units support yet + + t = np.arange(n_points, dtype=float) / n_points + x = cx + rx * np.cos(2 * np.pi * t) + y = cy + ry * np.sin(2 * np.pi * t) + pts = [(x[i], y[i]) for i in range(n_points)] + + return LinearRing(pts) + + def getsvggeo(node): """ Extracts and flattens all geometry from an SVG node @@ -166,6 +191,11 @@ def getsvggeo(node): C = svgcircle2shapely(node) geo = [C] + elif kind == 'ellipse': + print "***ELLIPSE***" + E = svgellipse2shapely(node) + geo = [E] + else: print "Unknown kind:", kind geo = None From 8927a37f682c56cb4d3adce7aa766546e4ef61d6 Mon Sep 17 00:00:00 2001 From: Juan Pablo Caram Date: Sat, 26 Dec 2015 16:38:58 -0500 Subject: [PATCH 008/134] SVG Line, polygon and polyline. --- svgparse.py | 98 +++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 91 insertions(+), 7 deletions(-) diff --git a/svgparse.py b/svgparse.py index 69331d3..6e9a53f 100644 --- a/svgparse.py +++ b/svgparse.py @@ -9,6 +9,10 @@ # * Groups # # * Rectangles # # * Circles # +# * Ellipses # +# * Polygons # +# * Polylines # +# * Lines # # * Paths # # * All transformations # # # @@ -22,6 +26,9 @@ from svg.path import Path, Line, Arc, CubicBezier, QuadraticBezier, parse_path from shapely.geometry import LinearRing, LineString, Point from shapely.affinity import translate, rotate, scale, skew, affine_transform import numpy as np +import logging + +log = logging.getLogger('base2') def svgparselength(lengthstr): @@ -83,7 +90,7 @@ def path2shapely(path, res=1.0): points.append((end.real, end.imag)) continue - print "I don't know what this is:", component + log.warning("I don't know what this is:", component) continue if path.closed: @@ -156,6 +163,32 @@ def svgellipse2shapely(ellipse, n_points=32): return LinearRing(pts) +def svgline2shapely(line): + + x1 = svgparselength(line.get('x1')) + y1 = svgparselength(line.get('y1')) + x2 = svgparselength(line.get('x2')) + y2 = svgparselength(line.get('y2')) + + return LineString([(x1, y1), (x2, y2)]) + + +def svgpolyline2shapely(polyline): + + ptliststr = polyline.get('points') + points = parse_svg_point_list(ptliststr) + + return LineString(points) + + +def svgpolygon2shapely(polygon): + + ptliststr = polygon.get('points') + points = parse_svg_point_list(ptliststr) + + return LinearRing(points) + + def getsvggeo(node): """ Extracts and flattens all geometry from an SVG node @@ -176,35 +209,50 @@ def getsvggeo(node): # Parse elif kind == 'path': - print "***PATH***" + log.debug("***PATH***") P = parse_path(node.get('d')) P = path2shapely(P) geo = [P] elif kind == 'rect': - print "***RECT***" + log.debug("***RECT***") R = svgrect2shapely(node) geo = [R] elif kind == 'circle': - print "***CIRCLE***" + log.debug("***CIRCLE***") C = svgcircle2shapely(node) geo = [C] elif kind == 'ellipse': - print "***ELLIPSE***" + log.debug("***ELLIPSE***") E = svgellipse2shapely(node) geo = [E] + elif kind == 'polygon': + log.debug("***POLYGON***") + poly = svgpolygon2shapely(node) + geo = [poly] + + elif kind == 'line': + log.debug("***LINE***") + line = svgline2shapely(node) + geo = [line] + + elif kind == 'polyline': + log.debug("***POLYLINE***") + pline = svgpolyline2shapely(node) + geo = [pline] + else: - print "Unknown kind:", kind + log.warning("Unknown kind: " + kind) geo = None # Transformations if 'transform' in node.attrib: trstr = node.get('transform') trlist = parse_svg_transform(trstr) - print trlist + #log.debug(trlist) # Transformations are applied in reverse order for tr in trlist[::-1]: @@ -227,6 +275,42 @@ def getsvggeo(node): return geo +def parse_svg_point_list(ptliststr): + """ + Returns a list of coordinate pairs extracted from the "points" + attribute in SVG polygons and polylines. + + :param ptliststr: "points" attribute string in polygon or polyline. + :return: List of tuples with coordinates. + """ + + pairs = [] + last = None + pos = 0 + i = 0 + + for match in re.finditer(r'(\s*,\s*)|(\s+)', ptliststr): + + val = float(ptliststr[pos:match.start()]) + + if i % 2 == 1: + pairs.append((last, val)) + else: + last = val + + pos = match.end() + i += 1 + + # Check for last element + val = float(ptliststr[pos:]) + if i % 2 == 1: + pairs.append((last, val)) + else: + log.warning("Incomplete coordinates.") + + return pairs + + def parse_svg_transform(trstr): """ Parses an SVG transform string into a list From 7db3ee7be6189bd3686e6604a49ef101308aebc6 Mon Sep 17 00:00:00 2001 From: Juan Pablo Caram Date: Sat, 26 Dec 2015 21:15:55 -0500 Subject: [PATCH 009/134] SVG rectangles with rounded corners. --- svgparse.py | 99 +++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 78 insertions(+), 21 deletions(-) diff --git a/svgparse.py b/svgparse.py index 6e9a53f..f2ecb52 100644 --- a/svgparse.py +++ b/svgparse.py @@ -7,7 +7,7 @@ # # # SVG Features supported: # # * Groups # -# * Rectangles # +# * Rectangles (w/ rounded corners) # # * Circles # # * Ellipses # # * Polygons # @@ -32,6 +32,14 @@ log = logging.getLogger('base2') def svgparselength(lengthstr): + """ + Parse an SVG length string into a float and a units + string, if any. + + :param lengthstr: SVG length string. + :return: Number and units pair. + :rtype: tuple(float, str|None) + """ integer_re_str = r'[+-]?[0-9]+' number_re_str = r'(?:[+-]?[0-9]*\.[0-9]+(?:[Ee]' + integer_re_str + ')?' + r')|' + \ @@ -99,24 +107,63 @@ def path2shapely(path, res=1.0): return LineString(points) -def svgrect2shapely(rect): +def svgrect2shapely(rect, n_points=32): """ Converts an SVG rect into Shapely geometry. :param rect: Rect Element :type rect: xml.etree.ElementTree.Element :return: shapely.geometry.polygon.LinearRing - - :param rect: - :return: """ - w = float(rect.get('width')) - h = float(rect.get('height')) - x = float(rect.get('x')) - y = float(rect.get('y')) - pts = [ - (x, y), (x + w, y), (x + w, y + h), (x, y + h), (x, y) - ] + w = svgparselength(rect.get('width'))[0] + h = svgparselength(rect.get('height'))[0] + x = svgparselength(rect.get('x'))[0] + y = svgparselength(rect.get('y'))[0] + + rxstr = rect.get('rx') + rystr = rect.get('ry') + + if rxstr is None and rystr is None: # Sharp corners + pts = [ + (x, y), (x + w, y), (x + w, y + h), (x, y + h), (x, y) + ] + + else: # Rounded corners + rx = 0.0 if rxstr is None else svgparselength(rxstr)[0] + ry = 0.0 if rystr is None else svgparselength(rystr)[0] + + n_points = int(n_points / 4 + 0.5) + t = np.arange(n_points, dtype=float) / n_points / 4 + + x_ = (x + w - rx) + rx * np.cos(2 * np.pi * (t + 0.75)) + y_ = (y + ry) + ry * np.sin(2 * np.pi * (t + 0.75)) + + lower_right = [(x_[i], y_[i]) for i in range(n_points)] + + x_ = (x + w - rx) + rx * np.cos(2 * np.pi * t) + y_ = (y + h - ry) + ry * np.sin(2 * np.pi * t) + + upper_right = [(x_[i], y_[i]) for i in range(n_points)] + + x_ = (x + rx) + rx * np.cos(2 * np.pi * (t + 0.25)) + y_ = (y + h - ry) + ry * np.sin(2 * np.pi * (t + 0.25)) + + upper_left = [(x_[i], y_[i]) for i in range(n_points)] + + x_ = (x + rx) + rx * np.cos(2 * np.pi * (t + 0.5)) + y_ = (y + ry) + ry * np.sin(2 * np.pi * (t + 0.5)) + + lower_left = [(x_[i], y_[i]) for i in range(n_points)] + + pts = [(x + rx, y), (x - rx + w, y)] + \ + lower_right + \ + [(x + w, y + ry), (x + w, y + h - ry)] + \ + upper_right + \ + [(x + w - rx, y + h), (x + rx, y + h)] + \ + upper_left + \ + [(x, y + h - ry), (x, y + ry)] + \ + lower_left + return LinearRing(pts) @@ -126,7 +173,8 @@ def svgcircle2shapely(circle): :param circle: Circle Element :type circle: xml.etree.ElementTree.Element - :return: shapely.geometry.polygon.LinearRing + :return: Shapely representation of the circle. + :rtype: shapely.geometry.polygon.LinearRing """ # cx = float(circle.get('cx')) # cy = float(circle.get('cy')) @@ -139,14 +187,15 @@ def svgcircle2shapely(circle): return Point(cx, cy).buffer(r) -def svgellipse2shapely(ellipse, n_points=32): +def svgellipse2shapely(ellipse, n_points=64): """ Converts an SVG ellipse into Shapely geometry :param ellipse: Ellipse Element :type ellipse: xml.etree.ElementTree.Element :param n_points: Number of discrete points in output. - :return: shapely.geometry.polygon.LinearRing + :return: Shapely representation of the ellipse. + :rtype: shapely.geometry.polygon.LinearRing """ cx = svgparselength(ellipse.get('cx'))[0] # TODO: No units support yet @@ -164,11 +213,18 @@ def svgellipse2shapely(ellipse, n_points=32): def svgline2shapely(line): + """ - x1 = svgparselength(line.get('x1')) - y1 = svgparselength(line.get('y1')) - x2 = svgparselength(line.get('x2')) - y2 = svgparselength(line.get('y2')) + :param line: Line element + :type line: xml.etree.ElementTree.Element + :return: Shapely representation on the line. + :rtype: shapely.geometry.polygon.LinearRing + """ + + x1 = svgparselength(line.get('x1'))[0] + y1 = svgparselength(line.get('y1'))[0] + x2 = svgparselength(line.get('x2'))[0] + y2 = svgparselength(line.get('y2'))[0] return LineString([(x1, y1), (x2, y2)]) @@ -194,8 +250,9 @@ def getsvggeo(node): Extracts and flattens all geometry from an SVG node into a list of Shapely geometry. - :param node: - :return: + :param node: xml.etree.ElementTree.Element + :return: List of Shapely geometry + :rtype: list """ kind = re.search('(?:\{.*\})?(.*)$', node.tag).group(1) geo = [] From 4fe841086ee0831ff56ed8029c23191188f1cde6 Mon Sep 17 00:00:00 2001 From: Juan Pablo Caram Date: Mon, 28 Dec 2015 17:59:22 -0500 Subject: [PATCH 010/134] Added skeleton for threaded bitmap cache. --- PlotCanvas.py | 51 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/PlotCanvas.py b/PlotCanvas.py index c7fb19b..be1e5c0 100644 --- a/PlotCanvas.py +++ b/PlotCanvas.py @@ -14,14 +14,55 @@ mpl_use("Qt4Agg") from matplotlib.figure import Figure from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas +from matplotlib.backends.backend_agg import FigureCanvasAgg import FlatCAMApp -class PlotCanvas: +class CanvasCache(QtCore.QObject): + + # Signals + new_screen = QtCore.pyqtSignal() + + def __init__(self, plotcanvas, dpi=50): + + super(CanvasCache, self).__init__() + + self.plotcanvas = plotcanvas + self.dpi = dpi + + self.figure = Figure(dpi=dpi) + + self.axes = self.figure.add_axes([0.0, 0.0, 1.0, 1.0], alpha=1.0) + self.axes.set_frame_on(False) + self.axes.set_xticks([]) + self.axes.set_yticks([]) + + self.canvas = FigureCanvasAgg(self.figure) + + self.cache = None + + def run(self): + + self.plotcanvas.update_screen_request.connect(self.on_update_req) + + def on_update_req(self, extents): + + # Move the requested screen portion to the main thread + # and inform about the update: + + self.new_screen.emit() + + # Continue to update the cache. + + +class PlotCanvas(QtCore.QObject): """ Class handling the plotting area in the application. """ + # Signals + update_screen_request = QtCore.pyqtSignal(list) + def __init__(self, container): """ The constructor configures the Matplotlib figure that @@ -65,6 +106,14 @@ class PlotCanvas: # Update every time the canvas is re-drawn. self.background = self.canvas.copy_from_bbox(self.axes.bbox) + # Bitmap Cache + self.cache = CanvasCache(self) + self.cache_thread = QtCore.QThread() + self.cache.moveToThread(self.cache_thread) + super(PlotCanvas, self).connect(self.cache_thread, QtCore.SIGNAL("started()"), self.cache.run) + # self.connect() + self.cache_thread.start() + # Events self.canvas.mpl_connect('button_press_event', self.on_mouse_press) self.canvas.mpl_connect('button_release_event', self.on_mouse_release) From 7d63ce33c62fdbb39341e21380be19d17c571c1b Mon Sep 17 00:00:00 2001 From: Juan Pablo Caram Date: Mon, 28 Dec 2015 18:08:25 -0500 Subject: [PATCH 011/134] Fix to skeleton for threaded bitmap cache. --- PlotCanvas.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/PlotCanvas.py b/PlotCanvas.py index be1e5c0..1046cde 100644 --- a/PlotCanvas.py +++ b/PlotCanvas.py @@ -20,7 +20,8 @@ import FlatCAMApp class CanvasCache(QtCore.QObject): - # Signals + # Signals: + # A bitmap is ready to be displayed. new_screen = QtCore.pyqtSignal() def __init__(self, plotcanvas, dpi=50): @@ -43,9 +44,16 @@ class CanvasCache(QtCore.QObject): def run(self): + print "CanvasCache Thread Started!" + self.plotcanvas.update_screen_request.connect(self.on_update_req) def on_update_req(self, extents): + """ + Event handler for an updated display request. + + :param extents: [xmin, xmax, ymin, ymax, zoom(optional)] + """ # Move the requested screen portion to the main thread # and inform about the update: @@ -60,7 +68,9 @@ class PlotCanvas(QtCore.QObject): Class handling the plotting area in the application. """ - # Signals + # Signals: + # Request for new bitmap to display. The parameter + # is a list with [xmin, xmax, ymin, ymax, zoom(optional)] update_screen_request = QtCore.pyqtSignal(list) def __init__(self, container): @@ -72,6 +82,9 @@ class PlotCanvas(QtCore.QObject): :param container: The parent container in which to draw plots. :rtype: PlotCanvas """ + + super(PlotCanvas, self).__init__() + # Options self.x_margin = 15 # pixels self.y_margin = 25 # Pixels From 705d038e1ce8a3d2da18d8b7f32074ca2db62451 Mon Sep 17 00:00:00 2001 From: Juan Pablo Caram Date: Tue, 29 Dec 2015 14:43:43 -0500 Subject: [PATCH 012/134] Added signal triggers and handlers for canvas cache. --- PlotCanvas.py | 58 ++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 55 insertions(+), 3 deletions(-) diff --git a/PlotCanvas.py b/PlotCanvas.py index 1046cde..cdee8de 100644 --- a/PlotCanvas.py +++ b/PlotCanvas.py @@ -16,6 +16,9 @@ from matplotlib.figure import Figure from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas from matplotlib.backends.backend_agg import FigureCanvasAgg import FlatCAMApp +import logging + +log = logging.getLogger('base') class CanvasCache(QtCore.QObject): @@ -44,7 +47,7 @@ class CanvasCache(QtCore.QObject): def run(self): - print "CanvasCache Thread Started!" + log.debug("CanvasCache Thread Started!") self.plotcanvas.update_screen_request.connect(self.on_update_req) @@ -55,6 +58,8 @@ class CanvasCache(QtCore.QObject): :param extents: [xmin, xmax, ymin, ymax, zoom(optional)] """ + log.debug("Canvas update requested: %s" % str(extents)) + # Move the requested screen portion to the main thread # and inform about the update: @@ -102,7 +107,7 @@ class PlotCanvas(QtCore.QObject): self.axes.set_aspect(1) self.axes.grid(True) - # The canvas is the top level container (Gtk.DrawingArea) + # The canvas is the top level container (FigureCanvasQTAgg) self.canvas = FigureCanvas(self.figure) # self.canvas.setFocusPolicy(QtCore.Qt.ClickFocus) # self.canvas.setFocus() @@ -119,13 +124,14 @@ class PlotCanvas(QtCore.QObject): # Update every time the canvas is re-drawn. self.background = self.canvas.copy_from_bbox(self.axes.bbox) - # Bitmap Cache + ### Bitmap Cache self.cache = CanvasCache(self) self.cache_thread = QtCore.QThread() self.cache.moveToThread(self.cache_thread) super(PlotCanvas, self).connect(self.cache_thread, QtCore.SIGNAL("started()"), self.cache.run) # self.connect() self.cache_thread.start() + self.cache.new_screen.connect(self.on_new_screen) # Events self.canvas.mpl_connect('button_press_event', self.on_mouse_press) @@ -146,6 +152,10 @@ class PlotCanvas(QtCore.QObject): self.pan_axes = [] self.panning = False + def on_new_screen(self): + + log.debug("Cache updated the screen!") + def on_key_down(self, event): """ @@ -275,6 +285,9 @@ class PlotCanvas(QtCore.QObject): # Sync re-draw to proper paint on form resize self.canvas.draw() + ##### Temporary place-holder for cached update ##### + self.update_screen_request.emit([0, 0, 0, 0, 0]) + def auto_adjust_axes(self, *args): """ Calls ``adjust_axes()`` using the extents of the base axes. @@ -327,6 +340,9 @@ class PlotCanvas(QtCore.QObject): # Async re-draw self.canvas.draw_idle() + ##### Temporary place-holder for cached update ##### + self.update_screen_request.emit([0, 0, 0, 0, 0]) + def pan(self, x, y): xmin, xmax = self.axes.get_xlim() ymin, ymax = self.axes.get_ylim() @@ -341,6 +357,9 @@ class PlotCanvas(QtCore.QObject): # Re-draw self.canvas.draw_idle() + ##### Temporary place-holder for cached update ##### + self.update_screen_request.emit([0, 0, 0, 0, 0]) + def new_axes(self, name): """ Creates and returns an Axes object attached to this object's Figure. @@ -434,7 +453,40 @@ class PlotCanvas(QtCore.QObject): # Async re-draw (redraws only on thread idle state, uses timer on backend) self.canvas.draw_idle() + ##### Temporary place-holder for cached update ##### + self.update_screen_request.emit([0, 0, 0, 0, 0]) + def on_draw(self, renderer): # Store background on canvas redraw self.background = self.canvas.copy_from_bbox(self.axes.bbox) + + def get_axes_pixelsize(self): + """ + Axes size in pixels. + + :return: Pixel width and height + :rtype: tuple + """ + bbox = self.axes.get_window_extent().transformed(self.figure.dpi_scale_trans.inverted()) + width, height = bbox.width, bbox.height + width *= self.figure.dpi + height *= self.figure.dpi + return width, height + + def get_density(self): + """ + Returns unit length per pixel on horizontal + and vertical axes. + + :return: X and Y density + :rtype: tuple + """ + xpx, ypx = self.get_axes_pixelsize() + + xmin, xmax = self.axes.get_xlim() + ymin, ymax = self.axes.get_ylim() + width = xmax - xmin + height = ymax - ymin + + return width / xpx, height / ypx From ea27748697023504e9a0fec8423806725efd8807 Mon Sep 17 00:00:00 2001 From: Juan Pablo Caram Date: Tue, 29 Dec 2015 16:34:13 -0500 Subject: [PATCH 013/134] Use Decimal for depth calculation. Fixes #130. --- PlotCanvas.py | 7 +++++++ camlib.py | 16 ++++++++++++---- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/PlotCanvas.py b/PlotCanvas.py index cdee8de..1634c99 100644 --- a/PlotCanvas.py +++ b/PlotCanvas.py @@ -60,6 +60,13 @@ class CanvasCache(QtCore.QObject): log.debug("Canvas update requested: %s" % str(extents)) + # Note: This information here might be out of date. Establish + # a protocol regarding when to change the canvas in the main + # thread and when to check these values here in the background, + # or pass this data in the signal (safer). + log.debug("Size: %s [px]" % str(self.plotcanvas.get_axes_pixelsize())) + log.debug("Density: %s [units/px]" % str(self.plotcanvas.get_density())) + # Move the requested screen portion to the main thread # and inform about the update: diff --git a/camlib.py b/camlib.py index 83721f3..1448bea 100644 --- a/camlib.py +++ b/camlib.py @@ -16,6 +16,7 @@ from matplotlib.figure import Figure import re import sys import traceback +from decimal import Decimal import collections import numpy as np @@ -2865,17 +2866,24 @@ class CNCjob(Geometry): #--------- Multi-pass --------- else: + if isinstance(self.z_cut, Decimal): + z_cut = self.z_cut + else: + z_cut = Decimal(self.z_cut).quantize(Decimal('0.000000001')) + if depthpercut is None: - depthpercut = self.z_cut + depthpercut = z_cut + elif not isinstance(depthpercut, Decimal): + depthpercut = Decimal(depthpercut).quantize(Decimal('0.000000001')) depth = 0 reverse = False - while depth > self.z_cut: + while depth > z_cut: # Increase depth. Limit to z_cut. depth -= depthpercut - if depth < self.z_cut: - depth = self.z_cut + if depth < z_cut: + depth = z_cut # Cut at specific depth and do not lift the tool. # Note: linear2gcode() will use G00 to move to the From d5c99463fbf8ee702c4bdd8193e67d19f7ba5899 Mon Sep 17 00:00:00 2001 From: Juan Pablo Caram Date: Tue, 29 Dec 2015 16:37:52 -0500 Subject: [PATCH 014/134] Added svg.path to ubuntu installation script. --- setup_ubuntu.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/setup_ubuntu.sh b/setup_ubuntu.sh index cc9aae9..bb1d7cc 100755 --- a/setup_ubuntu.sh +++ b/setup_ubuntu.sh @@ -13,3 +13,4 @@ pip install --upgrade matplotlib pip install --upgrade Shapely apt-get install libspatialindex-dev pip install rtree +pip install svg.path \ No newline at end of file From 3940408da5e65586a8ce76766e690f779c470c88 Mon Sep 17 00:00:00 2001 From: Juan Pablo Caram Date: Tue, 29 Dec 2015 17:35:43 -0500 Subject: [PATCH 015/134] Added non-compliant support for "-" in ApertureMacro names. Temporary fix for #185. --- camlib.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/camlib.py b/camlib.py index 1448bea..52f756d 100644 --- a/camlib.py +++ b/camlib.py @@ -1307,7 +1307,9 @@ class Gerber (Geometry): self.comm_re = re.compile(r'^G0?4(.*)$') # AD - Aperture definition - self.ad_re = re.compile(r'^%ADD(\d\d+)([a-zA-Z_$\.][a-zA-Z0-9_$\.]*)(?:,(.*))?\*%$') + # Aperture Macro names: Name = [a-zA-Z_.$]{[a-zA-Z_.0-9]+} + # NOTE: Adding "-" to support output from Upverter. + self.ad_re = re.compile(r'^%ADD(\d\d+)([a-zA-Z_$\.][a-zA-Z0-9_$\.\-]*)(?:,(.*))?\*%$') # AM - Aperture Macro # Beginning of macro (Ends with *%): From 7fd026c254f02fccaba5b76e9f7290b922519830 Mon Sep 17 00:00:00 2001 From: Juan Pablo Caram Date: Wed, 30 Dec 2015 10:26:19 -0500 Subject: [PATCH 016/134] Fix to Gerber parser, corrects line splitting. Fixes #183. --- camlib.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/camlib.py b/camlib.py index 52f756d..c4edaf4 100644 --- a/camlib.py +++ b/camlib.py @@ -1169,6 +1169,8 @@ class ApertureMacro: :param modifiers: Modifiers (parameters) for this macro :type modifiers: list + :return: Shapely geometry + :rtype: shapely.geometry.polygon """ ## Primitive makers @@ -1189,11 +1191,11 @@ class ApertureMacro: modifiers = [float(m) for m in modifiers] self.locvars = {} for i in range(0, len(modifiers)): - self.locvars[str(i+1)] = modifiers[i] + self.locvars[str(i + 1)] = modifiers[i] ## Parse self.primitives = [] # Cleanup - self.geometry = None + self.geometry = Polygon() self.parse_content() ## Make the geometry @@ -1202,9 +1204,9 @@ class ApertureMacro: prim_geo = makers[str(int(primitive[0]))](primitive[1:]) # Add it (according to polarity) - if self.geometry is None and prim_geo['pol'] == 1: - self.geometry = prim_geo['geometry'] - continue + # if self.geometry is None and prim_geo['pol'] == 1: + # self.geometry = prim_geo['geometry'] + # continue if prim_geo['pol'] == 1: self.geometry = self.geometry.union(prim_geo['geometry']) continue @@ -1567,7 +1569,8 @@ class Gerber (Geometry): # Otherwise leave as is. else: - yield cleanline + # yield cleanline + yield line break self.parse_lines(line_generator(), follow=follow) @@ -1648,7 +1651,7 @@ class Gerber (Geometry): match = self.am1_re.search(gline) # Start macro if match, else not an AM, carry on. if match: - log.info("Starting macro. Line %d: %s" % (line_num, gline)) + log.debug("Starting macro. Line %d: %s" % (line_num, gline)) current_macro = match.group(1) self.aperture_macros[current_macro] = ApertureMacro(name=current_macro) if match.group(2): # Append @@ -1656,13 +1659,13 @@ class Gerber (Geometry): if match.group(3): # Finish macro #self.aperture_macros[current_macro].parse_content() current_macro = None - log.info("Macro complete in 1 line.") + log.debug("Macro complete in 1 line.") continue else: # Continue macro - log.info("Continuing macro. Line %d." % line_num) + log.debug("Continuing macro. Line %d." % line_num) match = self.am2_re.search(gline) if match: # Finish macro - log.info("End of macro. Line %d." % line_num) + log.debug("End of macro. Line %d." % line_num) self.aperture_macros[current_macro].append(match.group(1)) #self.aperture_macros[current_macro].parse_content() current_macro = None @@ -2163,8 +2166,11 @@ class Gerber (Geometry): if aperture['type'] == 'AM': # Aperture Macro loc = location.coords[0] flash_geo = aperture['macro'].make_geometry(aperture['modifiers']) + if flash_geo.is_empty: + log.warning("Empty geometry for Aperture Macro: %s" % str(aperture['macro'].name)) return affinity.translate(flash_geo, xoff=loc[0], yoff=loc[1]) + log.warning("Unknown aperture type: %s" % aperture['type']) return None def create_geometry(self): From 96885c80a4946fbd1a8e36b6734b0c55f5d4c17f Mon Sep 17 00:00:00 2001 From: Juan Pablo Caram Date: Wed, 30 Dec 2015 11:45:05 -0500 Subject: [PATCH 017/134] Fixes #158. --- FlatCAMApp.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 69d4fa1..2db3add 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -1973,7 +1973,8 @@ class App(QtCore.QObject): def shelp(p=None): if not p: - return "Available commands:\n" + '\n'.join([' ' + cmd for cmd in commands]) + \ + return "Available commands:\n" + \ + '\n'.join([' ' + cmd for cmd in sorted(commands)]) + \ "\n\nType help for usage.\n Example: help open_gerber" if p not in commands: @@ -2626,8 +2627,8 @@ class App(QtCore.QObject): }, 'open_gerber': { 'fcn': open_gerber, - 'help': "Opens a Gerber file.\n' +" - "> open_gerber [-follow <0|1>] [-outname ]\n' +" + 'help': "Opens a Gerber file.\n" + "> open_gerber [-follow <0|1>] [-outname ]\n" " filename: Path to file to open.\n" + " follow: If 1, does not create polygons, just follows the gerber path.\n" + " outname: Name of the created gerber object." From 3b206493a302052a45d58c838a76454bf1d49b45 Mon Sep 17 00:00:00 2001 From: Juan Pablo Caram Date: Thu, 31 Dec 2015 23:28:23 -0500 Subject: [PATCH 018/134] Canvas performance test scripts. --- PlotCanvas.py | 15 ++++++ tests/canvas/performance.py | 95 +++++++++++++++++++++++++++++++++++++ tests/canvas/prof.sh | 6 +++ 3 files changed, 116 insertions(+) create mode 100644 tests/canvas/performance.py create mode 100755 tests/canvas/prof.sh diff --git a/PlotCanvas.py b/PlotCanvas.py index 1634c99..a41e371 100644 --- a/PlotCanvas.py +++ b/PlotCanvas.py @@ -22,6 +22,21 @@ log = logging.getLogger('base') class CanvasCache(QtCore.QObject): + """ + + Case story #1: + + 1) No objects in the project. + 2) Object is created (new_object() emits object_created(obj)). + on_object_created() adds (i) object to collection and emits + (ii) new_object_available() then calls (iii) object.plot() + 3) object.plot() creates axes if necessary on + app.collection.figure. Then plots on it. + 4) Plots on a cache-size canvas (in background). + 5) Plot completes. Bitmap is generated. + 6) Visible canvas is painted. + + """ # Signals: # A bitmap is ready to be displayed. diff --git a/tests/canvas/performance.py b/tests/canvas/performance.py new file mode 100644 index 0000000..9478bcb --- /dev/null +++ b/tests/canvas/performance.py @@ -0,0 +1,95 @@ +from __future__ import division +import matplotlib +matplotlib.use('Agg') +import matplotlib.pyplot as plt +import numpy as np +import cStringIO +from matplotlib.backends.backend_agg import FigureCanvasAgg +from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg +from matplotlib.figure import Figure +import cProfile +import sys + + +def gen_data(): + N = 100000 + x = np.random.rand(N) * 10 + y = np.random.rand(N) * 10 + colors = np.random.rand(N) + area = np.pi * (15 * np.random.rand(N))**2 # 0 to 15 point radiuses + data = x, y, area, colors + return data + + +# @profile +def large_plot(data): + x, y, area, colors = data + + fig = Figure(figsize=(10, 10), dpi=80) + axes = fig.add_axes([0.0, 0.0, 1.0, 1.0], alpha=1.0) + axes.set_frame_on(False) + axes.set_xticks([]) + axes.set_yticks([]) + # axes.set_xlim(0, 10) + # axes.set_ylim(0, 10) + + axes.scatter(x, y, s=area, c=colors, alpha=0.5) + + axes.set_xlim(0, 10) + axes.set_ylim(0, 10) + + canvas = FigureCanvasAgg(fig) + canvas.draw() + # canvas = FigureCanvasQTAgg(fig) + # buf = canvas.tostring_rgb() + buf = fig.canvas.tostring_rgb() + + ncols, nrows = fig.canvas.get_width_height() + img = np.fromstring(buf, dtype=np.uint8).reshape(nrows, ncols, 3) + + return img + + +def small_plot(data): + x, y, area, colors = data + + fig = Figure(figsize=(3, 3), dpi=80) + axes = fig.add_axes([0.0, 0.0, 1.0, 1.0], alpha=1.0) + axes.set_frame_on(False) + axes.set_xticks([]) + axes.set_yticks([]) + # axes.set_xlim(5, 6) + # axes.set_ylim(5, 6) + + axes.scatter(x, y, s=area, c=colors, alpha=0.5) + + axes.set_xlim(4, 7) + axes.set_ylim(4, 7) + + canvas = FigureCanvasAgg(fig) + canvas.draw() + # canvas = FigureCanvasQTAgg(fig) + # buf = canvas.tostring_rgb() + buf = fig.canvas.tostring_rgb() + + ncols, nrows = fig.canvas.get_width_height() + img = np.fromstring(buf, dtype=np.uint8).reshape(nrows, ncols, 3) + + return img + +def doit(): + d = gen_data() + img = large_plot(d) + return img + + +if __name__ == "__main__": + + d = gen_data() + + if sys.argv[1] == 'large': + cProfile.runctx('large_plot(d)', None, locals()) + else: + cProfile.runctx('small_plot(d)', None, locals()) + + diff --git a/tests/canvas/prof.sh b/tests/canvas/prof.sh new file mode 100755 index 0000000..b907584 --- /dev/null +++ b/tests/canvas/prof.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +echo "*** LARGE ***" +python performance.py large | egrep "(\(scatter\))|(\(draw\))|(tostring_rgb)|(fromstring)" +echo "*** SMALL ***" +python performance.py small | egrep "(\(scatter\))|(\(draw\))|(tostring_rgb)|(fromstring)" \ No newline at end of file From 2bf78920ae6d6eaa877f21a2d2bfdcc9ca1885a6 Mon Sep 17 00:00:00 2001 From: Juan Pablo Caram Date: Sun, 3 Jan 2016 16:38:24 -0500 Subject: [PATCH 019/134] PlotCanvas now stores reference to app. --- FlatCAMApp.py | 24 ++++++++++++++++++++---- PlotCanvas.py | 18 ++++++++++++++---- 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 2db3add..28a6941 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -72,11 +72,26 @@ class App(QtCore.QObject): ## Manual URL manual_url = "http://flatcam.org/manual/index.html" - ## Signals - inform = QtCore.pyqtSignal(str) # Message - worker_task = QtCore.pyqtSignal(dict) # Worker task + ################## + ## Signals ## + ################## + + # Inform the user + # Handled by: + # * App.info() --> Print on the status bar + inform = QtCore.pyqtSignal(str) + + # General purpose background task + worker_task = QtCore.pyqtSignal(dict) + + # File opened + # Handled by: + # * register_folder() + # * register_recent() file_opened = QtCore.pyqtSignal(str, str) # File type and filename + progress = QtCore.pyqtSignal(int) # Percentage of progress + plots_updated = QtCore.pyqtSignal() # Emitted by new_object() and passes the new object as argument. @@ -87,6 +102,7 @@ class App(QtCore.QObject): # Emitted when a new object has been added to the collection # and is ready to be used. new_object_available = QtCore.pyqtSignal(object) + message = QtCore.pyqtSignal(str, str, str) def __init__(self, user_defaults=True, post_gui=None): @@ -161,7 +177,7 @@ class App(QtCore.QObject): #### Plot Area #### # self.plotcanvas = PlotCanvas(self.ui.splitter) - self.plotcanvas = PlotCanvas(self.ui.right_layout) + self.plotcanvas = PlotCanvas(self.ui.right_layout, self) self.plotcanvas.mpl_connect('button_press_event', self.on_click_over_plot) self.plotcanvas.mpl_connect('motion_notify_event', self.on_mouse_move_over_plot) self.plotcanvas.mpl_connect('key_press_event', self.on_key_over_plot) diff --git a/PlotCanvas.py b/PlotCanvas.py index a41e371..94469d2 100644 --- a/PlotCanvas.py +++ b/PlotCanvas.py @@ -42,10 +42,12 @@ class CanvasCache(QtCore.QObject): # A bitmap is ready to be displayed. new_screen = QtCore.pyqtSignal() - def __init__(self, plotcanvas, dpi=50): + def __init__(self, plotcanvas, app, dpi=50): super(CanvasCache, self).__init__() + self.app = app + self.plotcanvas = plotcanvas self.dpi = dpi @@ -66,6 +68,8 @@ class CanvasCache(QtCore.QObject): self.plotcanvas.update_screen_request.connect(self.on_update_req) + self.app.new_object_available.connect(self.on_new_object_available) + def on_update_req(self, extents): """ Event handler for an updated display request. @@ -75,7 +79,7 @@ class CanvasCache(QtCore.QObject): log.debug("Canvas update requested: %s" % str(extents)) - # Note: This information here might be out of date. Establish + # Note: This information below might be out of date. Establish # a protocol regarding when to change the canvas in the main # thread and when to check these values here in the background, # or pass this data in the signal (safer). @@ -89,6 +93,10 @@ class CanvasCache(QtCore.QObject): # Continue to update the cache. + def on_new_object_available(self): + + log.debug("A new object is available. Should plot it!") + class PlotCanvas(QtCore.QObject): """ @@ -100,7 +108,7 @@ class PlotCanvas(QtCore.QObject): # is a list with [xmin, xmax, ymin, ymax, zoom(optional)] update_screen_request = QtCore.pyqtSignal(list) - def __init__(self, container): + def __init__(self, container, app): """ The constructor configures the Matplotlib figure that will contain all plots, creates the base axes and connects @@ -112,6 +120,8 @@ class PlotCanvas(QtCore.QObject): super(PlotCanvas, self).__init__() + self.app = app + # Options self.x_margin = 15 # pixels self.y_margin = 25 # Pixels @@ -147,7 +157,7 @@ class PlotCanvas(QtCore.QObject): self.background = self.canvas.copy_from_bbox(self.axes.bbox) ### Bitmap Cache - self.cache = CanvasCache(self) + self.cache = CanvasCache(self, self.app) self.cache_thread = QtCore.QThread() self.cache.moveToThread(self.cache_thread) super(PlotCanvas, self).connect(self.cache_thread, QtCore.SIGNAL("started()"), self.cache.run) From a7b29065dfd30dd271c85390d162379045b5dc43 Mon Sep 17 00:00:00 2001 From: Marius Stanciu Date: Sun, 14 Feb 2016 07:40:32 +0200 Subject: [PATCH 020/134] Solved issue #188: Order of the drill bits in Gcode generation from Excellon file The tools are ordered by diameter as I found that the tools order in the Excellon file is not always diameter based. There is also a plated / no-plated holes criteria. The tools in the GUI tool-list are selected all by default. If the user wants to select only some tools, he should be carefull when selecting the tools as the order of the selection will be the actual order of the tools in G-code. --- FlatCAMObj.py | 6 ++++++ camlib.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/FlatCAMObj.py b/FlatCAMObj.py index e19a0f9..8b7126a 100644 --- a/FlatCAMObj.py +++ b/FlatCAMObj.py @@ -648,6 +648,12 @@ class FlatCAMExcellon(FlatCAMObj, Excellon): dia.setFlags(QtCore.Qt.ItemIsEnabled) self.ui.tools_table.setItem(i, 1, dia) # Diameter i += 1 + + # sort the tool diameter column + self.ui.tools_table.sortItems(1) + # all the tools are selected by default + self.ui.tools_table.selectColumn(0) + self.ui.tools_table.resizeColumnsToContents() self.ui.tools_table.resizeRowsToContents() self.ui.tools_table.horizontalHeader().setStretchLastSection(True) diff --git a/camlib.py b/camlib.py index c4edaf4..7e77d3a 100644 --- a/camlib.py +++ b/camlib.py @@ -2746,7 +2746,7 @@ class CNCjob(Geometry): gcode += self.pausecode + "\n" - for tool in points: + for tool in tools: # Tool change sequence (optional) if toolchange: From a35a422bcce0c8579bf7b282692db6a9de607bc8 Mon Sep 17 00:00:00 2001 From: Marius Stanciu Date: Mon, 15 Feb 2016 23:40:08 +0200 Subject: [PATCH 021/134] This is a implementation of the the sorting of the tools found in Excellon file done in Python language and independent of the UI. There is no need to revert the previous solution as that one will make the sorting visible in GUI. --- camlib.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/camlib.py b/camlib.py index 7e77d3a..62ac63f 100644 --- a/camlib.py +++ b/camlib.py @@ -55,6 +55,8 @@ from svgparse import * import logging +import operator + log = logging.getLogger('base2') log.setLevel(logging.DEBUG) # log.setLevel(logging.WARNING) @@ -2708,12 +2710,18 @@ class CNCjob(Geometry): log.debug("Creating CNC Job from Excellon...") # Tools + + #sort the tools list by the second item in tuple (here we have a dict with diameter of the tool) + #so we actually are sorting the tools by diameter + sorted_tools = sorted(exobj.tools.items(), key = operator.itemgetter(1)) if tools == "all": - tools = [tool for tool in exobj.tools] + tools = str([i[0] for i in sorted_tools]) #we get a string of ordered tools + log.debug("Tools 'all' and sorted are: %s" % str(tools)) else: - tools = [x.strip() for x in tools.split(",")] - tools = filter(lambda i: i in exobj.tools, tools) - log.debug("Tools are: %s" % str(tools)) + selected_tools = [x.strip() for x in tools.split(",")] #we strip spaces and also separate the tools by ',' + selected_tools = filter(lambda i: i in selected_tools, selected_tools) + tools = [i for i,j in sorted_tools for k in selected_tools if i == k] #create a list of tools from the sorted_tools list only if the tools is in the selected tools + log.debug("Tools selected and sorted are: %s" % str(tools)) # Points (Group by tool) points = {} From cfa078a1e5e99fc779a4908bf9e90fde34f4b977 Mon Sep 17 00:00:00 2001 From: Marius Stanciu Date: Mon, 15 Feb 2016 22:35:22 +0000 Subject: [PATCH 022/134] camlib.py (edited a comment) edited online with Bitbucket --- camlib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/camlib.py b/camlib.py index 62ac63f..325d120 100644 --- a/camlib.py +++ b/camlib.py @@ -2720,7 +2720,7 @@ class CNCjob(Geometry): else: selected_tools = [x.strip() for x in tools.split(",")] #we strip spaces and also separate the tools by ',' selected_tools = filter(lambda i: i in selected_tools, selected_tools) - tools = [i for i,j in sorted_tools for k in selected_tools if i == k] #create a list of tools from the sorted_tools list only if the tools is in the selected tools + tools = [i for i,j in sorted_tools for k in selected_tools if i == k] #create a sorted list of selected tools from the sorted_tools list log.debug("Tools selected and sorted are: %s" % str(tools)) # Points (Group by tool) From 6dc107e4621399ecdac6020881ea61a7699baa19 Mon Sep 17 00:00:00 2001 From: Marius Stanciu Date: Tue, 16 Feb 2016 22:47:21 +0200 Subject: [PATCH 023/134] Bug fixed: the Toolchange Z parameter is not saved in the program/project defaults. Solution: Added: 'Toolchange Z' entry in the Options -> Excellon Options Also made sure that the "Toolchange Z" parameter is saved in the defaults.json file and also loaded. Added it into the dimensions list so it can be converted in between IN and MM units. --- FlatCAMApp.py | 6 +++++- FlatCAMGUI.py | 12 ++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 28a6941..7c72897 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -221,6 +221,7 @@ class App(QtCore.QObject): "excellon_travelz": self.defaults_form.excellon_group.travelz_entry, "excellon_feedrate": self.defaults_form.excellon_group.feedrate_entry, "excellon_spindlespeed": self.defaults_form.excellon_group.spindlespeed_entry, + "excellon_toolchangez": self.defaults_form.excellon_group.toolchangez_entry, "geometry_plot": self.defaults_form.geometry_group.plot_cb, "geometry_cutz": self.defaults_form.geometry_group.cutz_entry, "geometry_travelz": self.defaults_form.geometry_group.travelz_entry, @@ -262,6 +263,7 @@ class App(QtCore.QObject): "excellon_travelz": 0.1, "excellon_feedrate": 3.0, "excellon_spindlespeed": None, + "excellon_toolchangez": 1.0, "geometry_plot": True, "geometry_cutz": -0.002, "geometry_travelz": 0.1, @@ -346,6 +348,7 @@ class App(QtCore.QObject): "excellon_travelz": self.options_form.excellon_group.travelz_entry, "excellon_feedrate": self.options_form.excellon_group.feedrate_entry, "excellon_spindlespeed": self.options_form.excellon_group.spindlespeed_entry, + "excellon_toolchangez": self.options_form.excellon_group.toolchangez_entry, "geometry_plot": self.options_form.geometry_group.plot_cb, "geometry_cutz": self.options_form.geometry_group.cutz_entry, "geometry_travelz": self.options_form.geometry_group.travelz_entry, @@ -386,6 +389,7 @@ class App(QtCore.QObject): "excellon_travelz": 0.1, "excellon_feedrate": 3.0, "excellon_spindlespeed": None, + "excellon_toolchangez": 1.0, "geometry_plot": True, "geometry_cutz": -0.002, "geometry_travelz": 0.1, @@ -1146,7 +1150,7 @@ class App(QtCore.QObject): # Options to scale dimensions = ['gerber_isotooldia', 'gerber_cutoutmargin', 'gerber_cutoutgapsize', 'gerber_noncoppermargin', 'gerber_bboxmargin', 'excellon_drillz', - 'excellon_travelz', 'excellon_feedrate', 'cncjob_tooldia', + 'excellon_travelz', 'excellon_feedrate', 'excellon_toolchangez', 'cncjob_tooldia', 'geometry_cutz', 'geometry_travelz', 'geometry_feedrate', 'geometry_cnctooldia', 'geometry_painttooldia', 'geometry_paintoverlap', 'geometry_paintmargin'] diff --git a/FlatCAMGUI.py b/FlatCAMGUI.py index 64cdac0..8bb2445 100644 --- a/FlatCAMGUI.py +++ b/FlatCAMGUI.py @@ -586,14 +586,22 @@ class ExcellonOptionsGroupUI(OptionsGroupUI): self.feedrate_entry = LengthEntry() grid1.addWidget(self.feedrate_entry, 2, 1) + toolchangezlabel = QtGui.QLabel('Toolchange Z:') + toolchangezlabel.setToolTip( + "Tool Z where user can change drill bit\n" + ) + grid1.addWidget(toolchangezlabel, 3, 0) + self.toolchangez_entry = LengthEntry() + grid1.addWidget(self.toolchangez_entry, 3, 1) + spdlabel = QtGui.QLabel('Spindle speed:') spdlabel.setToolTip( "Speed of the spindle\n" "in RPM (optional)" ) - grid1.addWidget(spdlabel, 3, 0) + grid1.addWidget(spdlabel, 4, 0) self.spindlespeed_entry = IntEntry(allow_empty=True) - grid1.addWidget(self.spindlespeed_entry, 3, 1) + grid1.addWidget(self.spindlespeed_entry, 4, 1) class GeometryOptionsGroupUI(OptionsGroupUI): From 1a7e001a6690c46d74e8da3c20e569063b600f9f Mon Sep 17 00:00:00 2001 From: Marius Stanciu Date: Tue, 16 Feb 2016 22:53:01 +0200 Subject: [PATCH 024/134] Added spaces after '#' in the comments --- camlib.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/camlib.py b/camlib.py index 325d120..1296f4f 100644 --- a/camlib.py +++ b/camlib.py @@ -2711,16 +2711,16 @@ class CNCjob(Geometry): # Tools - #sort the tools list by the second item in tuple (here we have a dict with diameter of the tool) - #so we actually are sorting the tools by diameter + # sort the tools list by the second item in tuple (here we have a dict with diameter of the tool) + # so we actually are sorting the tools by diameter sorted_tools = sorted(exobj.tools.items(), key = operator.itemgetter(1)) if tools == "all": - tools = str([i[0] for i in sorted_tools]) #we get a string of ordered tools + tools = str([i[0] for i in sorted_tools]) # we get a string of ordered tools log.debug("Tools 'all' and sorted are: %s" % str(tools)) else: - selected_tools = [x.strip() for x in tools.split(",")] #we strip spaces and also separate the tools by ',' + selected_tools = [x.strip() for x in tools.split(",")] # we strip spaces and also separate the tools by ',' selected_tools = filter(lambda i: i in selected_tools, selected_tools) - tools = [i for i,j in sorted_tools for k in selected_tools if i == k] #create a sorted list of selected tools from the sorted_tools list + tools = [i for i,j in sorted_tools for k in selected_tools if i == k] # create a sorted list of selected tools from the sorted_tools list log.debug("Tools selected and sorted are: %s" % str(tools)) # Points (Group by tool) From 1be364d065a6e3a884ffae3eee5800075fbd49dc Mon Sep 17 00:00:00 2001 From: Marius Stanciu Date: Tue, 16 Feb 2016 23:25:58 +0200 Subject: [PATCH 025/134] Issue #188: Adopted the solution suggested by JP to not use the operator module when performing the sorting on exobj,tools and use instead the lambda function. --- camlib.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/camlib.py b/camlib.py index 1296f4f..d4c8240 100644 --- a/camlib.py +++ b/camlib.py @@ -55,8 +55,6 @@ from svgparse import * import logging -import operator - log = logging.getLogger('base2') log.setLevel(logging.DEBUG) # log.setLevel(logging.WARNING) @@ -2713,7 +2711,7 @@ class CNCjob(Geometry): # sort the tools list by the second item in tuple (here we have a dict with diameter of the tool) # so we actually are sorting the tools by diameter - sorted_tools = sorted(exobj.tools.items(), key = operator.itemgetter(1)) + sorted_tools = sorted(exobj.tools.items(), key = (lambda x: x[1])) if tools == "all": tools = str([i[0] for i in sorted_tools]) # we get a string of ordered tools log.debug("Tools 'all' and sorted are: %s" % str(tools)) From 71a81173bdfdbcf575f8cc80247139043141ca9a Mon Sep 17 00:00:00 2001 From: Marius Stanciu Date: Tue, 16 Feb 2016 21:59:54 +0000 Subject: [PATCH 026/134] camlib.py edited online with Bitbucket; removed the paranthesis around lambda function as it was making an tuple which it was not the intention. --- camlib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/camlib.py b/camlib.py index d4c8240..7d756a1 100644 --- a/camlib.py +++ b/camlib.py @@ -2711,7 +2711,7 @@ class CNCjob(Geometry): # sort the tools list by the second item in tuple (here we have a dict with diameter of the tool) # so we actually are sorting the tools by diameter - sorted_tools = sorted(exobj.tools.items(), key = (lambda x: x[1])) + sorted_tools = sorted(exobj.tools.items(), key = lambda x: x[1]) if tools == "all": tools = str([i[0] for i in sorted_tools]) # we get a string of ordered tools log.debug("Tools 'all' and sorted are: %s" % str(tools)) From cf51e4ce2c292f35177c7db9fcc1a51d5f82f803 Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Sat, 20 Feb 2016 10:56:32 +0100 Subject: [PATCH 027/134] implement del_polygon from geometry --- camlib.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/camlib.py b/camlib.py index 7d756a1..b919f39 100644 --- a/camlib.py +++ b/camlib.py @@ -136,6 +136,29 @@ class Geometry(object): log.error("Failed to run union on polygons.") raise + def del_polygon(self, points): + """ + Delete a polygon from the object + + :param points: The vertices of the polygon. + :return: None + """ + if self.solid_geometry is None: + self.solid_geometry = [] + + + flat_geometry = self.flatten(pathonly=True) + log.debug("%d paths" % len(flat_geometry)) + polygon=Polygon(points) + toolgeo=cascaded_union(polygon) + diffs=[] + for target in flat_geometry: + if type(target) == LineString or type(target) == LinearRing: + diffs.append(target.difference(toolgeo)) + else: + log.warning("Not implemented.") + return cascaded_union(diffs) + def bounds(self): """ Returns coordinates of rectangular bounds From 5acdbd51e3b4b5630e44d3cae1e964a5e9b7bf18 Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Sat, 20 Feb 2016 11:38:35 +0100 Subject: [PATCH 028/134] implement some new shell commands, which helps automate system of milling and cutting out shapes like arduino uno board etc. shell commands: aligndrill - Create excellon with drills for aligment. geocutout - Cut holding gaps closed geometry. del_poly - Remove a polygon from the given Geometry object. del_rect - Delete a rectange from the given Geometry object. --- FlatCAMApp.py | 252 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 252 insertions(+) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 7c72897..11f3d46 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -2134,6 +2134,42 @@ class App(QtCore.QObject): return 'Ok' + + def geocutout(name, *args): + """ + cut gaps in current geometry + + :param name: + :param args: + :return: + """ + a, kwa = h(*args) + types = {'dia': float, + 'gapsize': float, + 'gaps': str} + + for key in kwa: + if key not in types: + return 'Unknown parameter: %s' % key + kwa[key] = types[key](kwa[key]) + + try: + obj = self.collection.get_by_name(str(name)) + except: + return "Could not retrieve object: %s" % name + + + + xmin, ymin, xmax, ymax = obj.bounds() + px = 0.5 * (xmin + xmax) + py = 0.5 * (ymin + ymax) + gapsize = kwa['gapsize']+kwa['dia']/2 + if kwa['gaps'] == '4' or kwa['gaps']=='lr': + del_rectangle(name,xmin-gapsize,py-gapsize,xmax+gapsize,py+gapsize) + if kwa['gaps'] == '4' or kwa['gaps']=='tb': + del_rectangle(name,px-gapsize,ymin-gapsize,px+gapsize,ymax+gapsize) + return 'Ok' + def mirror(name, *args): a, kwa = h(*args) types = {'box': str, @@ -2202,6 +2238,142 @@ class App(QtCore.QObject): return 'Ok' + def aligndrill(name, *args): + a, kwa = h(*args) + types = {'box': str, + 'axis': str, + 'holes': str, + 'grid': float, + 'gridoffset': float, + 'axisoffset': float, + 'dia': float, + 'dist': float} + + for key in kwa: + if key not in types: + return 'Unknown parameter: %s' % key + kwa[key] = types[key](kwa[key]) + + # Get source object. + try: + obj = self.collection.get_by_name(str(name)) + except: + return "Could not retrieve object: %s" % name + + if obj is None: + return "Object not found: %s" % name + + if not isinstance(obj, FlatCAMGerber) and not isinstance(obj, FlatCAMExcellon): + return "ERROR: Only Gerber and Excellon objects can be used." + + + # Axis + try: + axis = kwa['axis'].upper() + except KeyError: + return "ERROR: Specify -axis X or -axis Y" + + if not ('holes' in kwa or ('grid' in kwa and 'gridoffset' in kwa)): + return "ERROR: Specify -holes or -grid with -gridoffset " + + if 'holes' in kwa: + try: + holes = eval("[" + kwa['holes'] + "]") + except KeyError: + return "ERROR: Wrong -holes format (X1,Y1),(X2,Y2)" + + xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis] + + # Tools + tools = {"1": {"C": kwa['dia']}} + + def alligndrill_init_me(init_obj, app_obj): + + drills = [] + if 'holes' in kwa: + for hole in holes: + point = Point(hole) + point_mirror = affinity.scale(point, xscale, yscale, origin=(px, py)) + drills.append({"point": point, "tool": "1"}) + drills.append({"point": point_mirror, "tool": "1"}) + else: + if not 'box' in kwa: + return "ERROR: -grid can be used only for -box" + + if 'axisoffset' in kwa: + axisoffset=kwa['axisoffset'] + else: + axisoffset=0 + + + if axis == "X": + firstpoint=-kwa['gridoffset']+xmin + #-5 + minlenght=(xmax-xmin+2*kwa['gridoffset']) + #57+10=67 + gridstripped=(minlenght//kwa['grid'])*kwa['grid'] + #67//10=60 + if (minlenght-gridstripped) >kwa['gridoffset']: + gridstripped=gridstripped+kwa['grid'] + lastpoint=(firstpoint+gridstripped) + localHoles=(firstpoint,axisoffset),(lastpoint,axisoffset) + else: + firstpoint=-kwa['gridoffset']+ymin + minlenght=(ymax-ymin+2*kwa['gridoffset']) + gridstripped=minlenght//kwa['grid']*kwa['grid'] + if (minlenght-gridstripped) >kwa['gridoffset']: + gridstripped=gridstripped+kwa['grid'] + lastpoint=(firstpoint+gridstripped) + localHoles=(axisoffset,firstpoint),(axisoffset,lastpoint) + + for hole in localHoles: + point = Point(hole) + point_mirror = affinity.scale(point, xscale, yscale, origin=(px, py)) + drills.append({"point": point, "tool": "1"}) + drills.append({"point": point_mirror, "tool": "1"}) + + init_obj.tools = tools + init_obj.drills = drills + init_obj.create_geometry() + + # Box + if 'box' in kwa: + try: + box = self.collection.get_by_name(kwa['box']) + except: + return "Could not retrieve object box: %s" % kwa['box'] + + if box is None: + return "Object box not found: %s" % kwa['box'] + + try: + xmin, ymin, xmax, ymax = box.bounds() + px = 0.5 * (xmin + xmax) + py = 0.5 * (ymin + ymax) + + obj.app.new_object("excellon", name + "_aligndrill", alligndrill_init_me) + + except Exception, e: + return "Operation failed: %s" % str(e) + + else: + try: + dist = float(kwa['dist']) + except KeyError: + dist = 0.0 + except ValueError: + return "Invalid distance: %s" % kwa['dist'] + + try: + px=dist + py=dist + obj.app.new_object("excellon", name + "_alligndrill", alligndrill_init_me) + except Exception, e: + return "Operation failed: %s" % str(e) + + return 'Ok' + + def drillcncjob(name, *args): a, kwa = h(*args) types = {'tools': str, @@ -2492,6 +2664,34 @@ class App(QtCore.QObject): return add_poly(obj_name, botleft_x, botleft_y, botleft_x, topright_y, topright_x, topright_y, topright_x, botleft_y) + def del_poly(obj_name, *args): + if len(args) % 2 != 0: + return "Incomplete coordinate." + + points = [[float(args[2*i]), float(args[2*i+1])] for i in range(len(args)/2)] + + try: + obj = self.collection.get_by_name(str(obj_name)) + except: + return "Could not retrieve object: %s" % obj_name + if obj is None: + return "Object not found: %s" % obj_name + + def init_obj_me(init_obj, app): + assert isinstance(init_obj, FlatCAMGeometry) + init_obj.solid_geometry=cascaded_union(diff) + + diff= obj.del_polygon(points) + try: + delete(obj_name) + obj.app.new_object("geometry", obj_name, init_obj_me) + except Exception as e: + return "Failed: %s" % str(e) + + def del_rectangle(obj_name, botleft_x, botleft_y, topright_x, topright_y): + return del_poly(obj_name, botleft_x, botleft_y, botleft_x, topright_y, + topright_x, topright_y, topright_x, botleft_y) + def add_circle(obj_name, center_x, center_y, radius): try: obj = self.collection.get_by_name(str(obj_name)) @@ -2721,6 +2921,30 @@ class App(QtCore.QObject): " gapsize: size of gap\n" + " gaps: type of gaps" }, + 'geocutout': { + 'fcn': geocutout, + 'help': "Cut holding gaps closed geometry.\n" + + "> geocutout [-dia <3.0 (float)>] [-margin <0.0 (float)>] [-gapsize <0.5 (float)>] [-gaps ]\n" + + " name: Name of the geometry object\n" + + " dia: Tool diameter\n" + + " margin: Margin over bounds\n" + + " gapsize: size of gap\n" + + " gaps: type of gaps\n" + + "\n" + + " example:\n" + + "\n" + + " #isolate margin for example from fritzing arduino shield or any svg etc\n" + + " isolate BCu_margin -dia 3 -overlap 1\n" + + "\n" + + " #create exteriors from isolated object\n" + + " exteriors BCu_margin_iso -outname BCu_margin_iso_exterior\n" + + "\n" + + " #delete isolated object if you dond need id anymore\n" + + " delete BCu_margin_iso\n" + + "\n" + + " #finally cut holding gaps\n" + + " geocutout BCu_margin_iso_exterior -dia 3 -gapsize 0.6 -gaps 4\n" + }, 'mirror': { 'fcn': mirror, 'help': "Mirror a layer.\n" + @@ -2730,6 +2954,19 @@ class App(QtCore.QObject): " axis: Mirror axis parallel to the X or Y axis.\n" + " dist: Distance of the mirror axis to the X or Y axis." }, + 'aligndrill': { + 'fcn': aligndrill, + 'help': "Create excellon with drills for aligment.\n" + + "> aligndrill [-dia <3.0 (float)>] -axis [-box [-grid <10 (float)> -gridoffset <5 (float)> [-axisoffset <0 (float)>]] | -dist ]\n" + + " name: Name of the object (Gerber or Excellon) to mirror.\n" + + " dia: Tool diameter\n" + + " box: Name of object which act as box (cutout for example.)\n" + + " grid: aligning to grid, for thouse, who have aligning pins inside table in grid (-5,0),(5,0),(15,0)..." + + " gridoffset: offset from pcb from 0 position and minimal offset to grid on max" + + " axisoffset: offset on second axis before aligment holes" + + " axis: Mirror axis parallel to the X or Y axis.\n" + + " dist: Distance of the mirror axis to the X or Y axis." + }, 'exteriors': { 'fcn': exteriors, 'help': "Get exteriors of polygons.\n" + @@ -2827,6 +3064,13 @@ class App(QtCore.QObject): ' name: Name of the geometry object to which to append the polygon.\n' + ' xi, yi: Coordinates of points in the polygon.' }, + 'del_poly': { + 'fcn': del_poly, + 'help': ' - Remove a polygon from the given Geometry object.\n' + + '> del_poly [x3 y3 [...]]\n' + + ' name: Name of the geometry object to which to remove the polygon.\n' + + ' xi, yi: Coordinates of points in the polygon.' + }, 'delete': { 'fcn': delete, 'help': 'Deletes the give object.\n' + @@ -2850,6 +3094,14 @@ class App(QtCore.QObject): ' out_name: Name of the new geometry object.' + ' obj_name_0... names of the objects to join' }, + 'del_rect': { + 'fcn': del_rectangle, + 'help': 'Delete a rectange from the given Geometry object.\n' + + '> del_rect \n' + + ' name: Name of the geometry object to which to remove the rectangle.\n' + + ' botleft_x, botleft_y: Coordinates of the bottom left corner.\n' + + ' topright_x, topright_y Coordinates of the top right corner.' + }, 'add_rect': { 'fcn': add_rectangle, 'help': 'Creates a rectange in the given Geometry object.\n' + From 9d897d0fcb52135fc8e3f56bc716e65ec41b5f70 Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Sat, 20 Feb 2016 12:21:46 +0000 Subject: [PATCH 029/134] README.md edited online with Bitbucket --- README.md | 99 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/README.md b/README.md index 5b156d1..0ccea79 100644 --- a/README.md +++ b/README.md @@ -6,3 +6,102 @@ FlatCAM: 2D Computer-Aided PCB Manufacturing FlatCAM is a program for preparing CNC jobs for making PCBs on a CNC router. Among other things, it can take a Gerber file generated by your favorite PCB CAD program, and create G-Code for Isolation routing. + + +####### my own shell script + +new +set_sys units MM + + +# ######### BOTTOM layer + +# LOAD +open_gerber /path/to/Gerber/Loop_contour.gm1 -outname BCu_margin +open_gerber /path/to/Gerber/Loop_copperBottom.gbl -outname BCu +open_excellon /path/to/Gerber/Loop_drill.txt -outname BCu_drills + +#MIRROR +mirror BCu -box BCu_margin -axis X +mirror BCu_drills -box BCu_margin -axis X + +#ALIGNHOLES +aligndrill BCu_margin -dia 3 -box BCu_margin -grid 10 -gridoffset 5 -axisoffset 0 -axis X + +#CUTOUT +isolate BCu_margin -dia 3 -overlap 1 +exteriors BCu_margin_iso -outname BCu_margin_iso_exterior +delete BCu_margin_iso +geocutout BCu_margin_iso_exterior -dia 3 -gapsize 0.2 -gaps 4 + +#ISOLATE TRACES +exteriors BCu_margin -outname BCu_exterior +isolate BCu -dia 0.8 -overlap 1 + +#JOIN TRACES and basic exterior +join_geometries BCu_join_iso BCu_iso BCu_exterior + +#CNCJOBS +drillcncjob BCu_drills -tools 100,101,102,103,104 -drillz -2 -travelz 2 -feedrate 5 -outname BCu_drills_0.8 +drillcncjob BCu_margin_aligndrill -tools 1 -drillz -2 -travelz 2 -feedrate 5 -outname BCu_drills_3 + +cncjob BCu_join_iso -tooldia 0.6 +#cncjob BCu_margin_cutout -tooldia 3 +cncjob BCu_margin_iso_exterior -tooldia 3 + + + +#GENERATE GCODE + +write_gcode BCu_join_iso_cnc /path/to/Gerber/output/Loop-BCu.pngc +write_gcode BCu_margin_iso_exterior_cnc /path/to/Gerber/output/Loop-BCu-Margin.ngc +write_gcode BCu_drills_0.8 /path/to/Gerber/output/Loop-BCu.drl_0.8.ngc +write_gcode BCu_drills_3 /path/to/Gerber/output/Loop-BCu.drl_3.ngc + + + +# ######### TOP layer + + +# LOAD +open_gerber /path/to/Gerber/Loop_contour.gm1 -outname FCu_margin +open_gerber /path/to/Gerber/Loop_copperTop.gtl -outname FCu +open_excellon /path/to/Gerber/Loop_drill.txt -outname FCu_drills + +#ALIGNHOLES +aligndrill FCu_margin -dia 3 -box FCu_margin -grid 10 -gridoffset 5 -axisoffset 0 -axis X + +#CUTOUT +isolate FCu_margin -dia 3 -overlap 1 +exteriors FCu_margin_iso -outname FCu_margin_iso_exterior +delete FCu_margin_iso +geocutout FCu_margin_iso_exterior -dia 3 -gapsize 0.2 -gaps 4 + +#ISOLATE TRACES +exteriors FCu_margin -outname FCu_exterior +isolate FCu -dia 0.8 -overlap 1 + +#JOIN TRACES and basic exterior +join_geometries FCu_join_iso FCu_iso FCu_exterior + +#CNCJOBS +drillcncjob FCu_drills -tools 100,101,102,103,104 -drillz -2 -travelz 2 -feedrate 5 -outname FCu_drills_0.8 +drillcncjob FCu_margin_aligndrill -tools 1 -drillz -2 -travelz 2 -feedrate 5 -outname FCu_drills_3 + +cncjob FCu_join_iso -tooldia 0.6 +#cncjob FCu_margin_cutout -tooldia 3 +cncjob FCu_margin_iso_exterior -tooldia 3 + + + +#GENERATE GCODE + +write_gcode FCu_join_iso_cnc /path/to/Gerber/output/Loop-FCu.pngc +write_gcode FCu_margin_iso_exterior_cnc /path/to/Gerber/output/Loop-FCu-Margin.ngc +write_gcode FCu_drills_0.8 /path/to/Gerber/output/Loop-FCu.drl_0.8.ngc +write_gcode FCu_drills_3 /path/to/Gerber/output/Loop-FCu.drl_3.ngc + + + + + From 2b8b9e12702a9624120cee08f0195e341267374f Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Sat, 20 Feb 2016 12:22:20 +0000 Subject: [PATCH 030/134] README.md edited online with Bitbucket --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 0ccea79..3961abd 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,11 @@ Among other things, it can take a Gerber file generated by your favorite PCB CAD program, and create G-Code for Isolation routing. -####### my own shell script +### my own shell script + +``` +#!python + new set_sys units MM @@ -100,8 +104,4 @@ write_gcode FCu_join_iso_cnc /path/to/Gerber/output/Loop-FCu.pngc write_gcode FCu_margin_iso_exterior_cnc /path/to/Gerber/output/Loop-FCu-Margin.ngc write_gcode FCu_drills_0.8 /path/to/Gerber/output/Loop-FCu.drl_0.8.ngc write_gcode FCu_drills_3 /path/to/Gerber/output/Loop-FCu.drl_3.ngc - - - - - +``` \ No newline at end of file From e94fe513b3a466bd3ca45945c508b38c9ac1d970 Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Sat, 20 Feb 2016 12:26:46 +0000 Subject: [PATCH 031/134] README.md edited online with Bitbucket --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3961abd..c044962 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Among other things, it can take a Gerber file generated by your favorite PCB CAD program, and create G-Code for Isolation routing. -### my own shell script +### double sided shell script used for Loop Arduino Shield example from [fritzing](Link URL)http://fritzing.org/ ``` #!python From d7bdfe231dd2e254c09c0c7b7fc151e055e21083 Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Sat, 20 Feb 2016 13:38:42 +0100 Subject: [PATCH 032/134] Revert "README.md edited online with Bitbucket" This reverts commit e94fe513b3a466bd3ca45945c508b38c9ac1d970. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c044962..3961abd 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Among other things, it can take a Gerber file generated by your favorite PCB CAD program, and create G-Code for Isolation routing. -### double sided shell script used for Loop Arduino Shield example from [fritzing](Link URL)http://fritzing.org/ +### my own shell script ``` #!python From 2e07b6dfa58052bd4b978a24dc826b0eb3cd0fbc Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Sat, 20 Feb 2016 13:40:15 +0100 Subject: [PATCH 033/134] Revert "README.md edited online with Bitbucket" This reverts commit 2b8b9e12702a9624120cee08f0195e341267374f. --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 3961abd..0ccea79 100644 --- a/README.md +++ b/README.md @@ -8,11 +8,7 @@ Among other things, it can take a Gerber file generated by your favorite PCB CAD program, and create G-Code for Isolation routing. -### my own shell script - -``` -#!python - +####### my own shell script new set_sys units MM @@ -104,4 +100,8 @@ write_gcode FCu_join_iso_cnc /path/to/Gerber/output/Loop-FCu.pngc write_gcode FCu_margin_iso_exterior_cnc /path/to/Gerber/output/Loop-FCu-Margin.ngc write_gcode FCu_drills_0.8 /path/to/Gerber/output/Loop-FCu.drl_0.8.ngc write_gcode FCu_drills_3 /path/to/Gerber/output/Loop-FCu.drl_3.ngc -``` \ No newline at end of file + + + + + From 14be36f277c5868ea313a12f5d493c1008ed3634 Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Sat, 20 Feb 2016 13:41:16 +0100 Subject: [PATCH 034/134] Revert "README.md edited online with Bitbucket" This reverts commit 9d897d0fcb52135fc8e3f56bc716e65ec41b5f70. --- README.md | 99 ------------------------------------------------------- 1 file changed, 99 deletions(-) diff --git a/README.md b/README.md index 0ccea79..5b156d1 100644 --- a/README.md +++ b/README.md @@ -6,102 +6,3 @@ FlatCAM: 2D Computer-Aided PCB Manufacturing FlatCAM is a program for preparing CNC jobs for making PCBs on a CNC router. Among other things, it can take a Gerber file generated by your favorite PCB CAD program, and create G-Code for Isolation routing. - - -####### my own shell script - -new -set_sys units MM - - -# ######### BOTTOM layer - -# LOAD -open_gerber /path/to/Gerber/Loop_contour.gm1 -outname BCu_margin -open_gerber /path/to/Gerber/Loop_copperBottom.gbl -outname BCu -open_excellon /path/to/Gerber/Loop_drill.txt -outname BCu_drills - -#MIRROR -mirror BCu -box BCu_margin -axis X -mirror BCu_drills -box BCu_margin -axis X - -#ALIGNHOLES -aligndrill BCu_margin -dia 3 -box BCu_margin -grid 10 -gridoffset 5 -axisoffset 0 -axis X - -#CUTOUT -isolate BCu_margin -dia 3 -overlap 1 -exteriors BCu_margin_iso -outname BCu_margin_iso_exterior -delete BCu_margin_iso -geocutout BCu_margin_iso_exterior -dia 3 -gapsize 0.2 -gaps 4 - -#ISOLATE TRACES -exteriors BCu_margin -outname BCu_exterior -isolate BCu -dia 0.8 -overlap 1 - -#JOIN TRACES and basic exterior -join_geometries BCu_join_iso BCu_iso BCu_exterior - -#CNCJOBS -drillcncjob BCu_drills -tools 100,101,102,103,104 -drillz -2 -travelz 2 -feedrate 5 -outname BCu_drills_0.8 -drillcncjob BCu_margin_aligndrill -tools 1 -drillz -2 -travelz 2 -feedrate 5 -outname BCu_drills_3 - -cncjob BCu_join_iso -tooldia 0.6 -#cncjob BCu_margin_cutout -tooldia 3 -cncjob BCu_margin_iso_exterior -tooldia 3 - - - -#GENERATE GCODE - -write_gcode BCu_join_iso_cnc /path/to/Gerber/output/Loop-BCu.pngc -write_gcode BCu_margin_iso_exterior_cnc /path/to/Gerber/output/Loop-BCu-Margin.ngc -write_gcode BCu_drills_0.8 /path/to/Gerber/output/Loop-BCu.drl_0.8.ngc -write_gcode BCu_drills_3 /path/to/Gerber/output/Loop-BCu.drl_3.ngc - - - -# ######### TOP layer - - -# LOAD -open_gerber /path/to/Gerber/Loop_contour.gm1 -outname FCu_margin -open_gerber /path/to/Gerber/Loop_copperTop.gtl -outname FCu -open_excellon /path/to/Gerber/Loop_drill.txt -outname FCu_drills - -#ALIGNHOLES -aligndrill FCu_margin -dia 3 -box FCu_margin -grid 10 -gridoffset 5 -axisoffset 0 -axis X - -#CUTOUT -isolate FCu_margin -dia 3 -overlap 1 -exteriors FCu_margin_iso -outname FCu_margin_iso_exterior -delete FCu_margin_iso -geocutout FCu_margin_iso_exterior -dia 3 -gapsize 0.2 -gaps 4 - -#ISOLATE TRACES -exteriors FCu_margin -outname FCu_exterior -isolate FCu -dia 0.8 -overlap 1 - -#JOIN TRACES and basic exterior -join_geometries FCu_join_iso FCu_iso FCu_exterior - -#CNCJOBS -drillcncjob FCu_drills -tools 100,101,102,103,104 -drillz -2 -travelz 2 -feedrate 5 -outname FCu_drills_0.8 -drillcncjob FCu_margin_aligndrill -tools 1 -drillz -2 -travelz 2 -feedrate 5 -outname FCu_drills_3 - -cncjob FCu_join_iso -tooldia 0.6 -#cncjob FCu_margin_cutout -tooldia 3 -cncjob FCu_margin_iso_exterior -tooldia 3 - - - -#GENERATE GCODE - -write_gcode FCu_join_iso_cnc /path/to/Gerber/output/Loop-FCu.pngc -write_gcode FCu_margin_iso_exterior_cnc /path/to/Gerber/output/Loop-FCu-Margin.ngc -write_gcode FCu_drills_0.8 /path/to/Gerber/output/Loop-FCu.drl_0.8.ngc -write_gcode FCu_drills_3 /path/to/Gerber/output/Loop-FCu.drl_3.ngc - - - - - From 84322882e92a05a1cea3786a65dee219ca53a92c Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Sun, 21 Feb 2016 17:03:59 +0100 Subject: [PATCH 035/134] fix FlatCamObj.offset - offset does not work on joined geometries, if tree was not flat it send list into affinity.translate. implement FlatCAMExcellon.merge - to be able join more excellons into one job --- FlatCAMObj.py | 84 ++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 79 insertions(+), 5 deletions(-) diff --git a/FlatCAMObj.py b/FlatCAMObj.py index 8b7126a..fb785ae 100644 --- a/FlatCAMObj.py +++ b/FlatCAMObj.py @@ -630,6 +630,75 @@ class FlatCAMExcellon(FlatCAMObj, Excellon): # from predecessors. self.ser_attrs += ['options', 'kind'] + @staticmethod + def merge(exc_list, exc_final): + FlatCAMExcellon.merge(exc_list,exc_final,False) + + @staticmethod + def merge(exc_list, exc_final, copy_options): + """ + Merges(copy if used on one) the excellon of objects in exc_list into + options have same like exc_final + the geometry of geo_final. + + :param exc_list: List of FlatCAMExcellon Objects to join. + :param exc_final: Destination FlatCAMExcellon object. + :return: None + """ + + if type(exc_list) is not list: + exc_list_real= list() + exc_list_real.append(exc_list) + else: + exc_list_real=exc_list + + + for exc in exc_list_real: + # Expand lists + if type(exc) is list: + FlatCAMExcellon.merge(exc, exc_final, copy_options) + + # If not list, just append + else: + if copy_options is True: + exc_final.options["plot"]=exc.options["plot"] + exc_final.options["solid"]=exc.options["solid"] + exc_final.options["drillz"]=exc.options["drillz"] + exc_final.options["travelz"]=exc.options["travelz"] + exc_final.options["feedrate"]=exc.options["feedrate"] + exc_final.options["tooldia"]=exc.options["tooldia"] + exc_final.options["toolchange"]=exc.options["toolchange"] + exc_final.options["toolchangez"]=exc.options["toolchangez"] + exc_final.options["spindlespeed"]=exc.options["spindlespeed"] + + + for drill in exc.drills: + point = Point(drill['point'].x,drill['point'].y) + exc_final.drills.append({"point": point, "tool": drill['tool']}) + toolsrework=dict() + max_numeric_tool=0 + for toolname in exc.tools.iterkeys(): + numeric_tool=int(toolname) + if numeric_tool>max_numeric_tool: + max_numeric_tool=numeric_tool + toolsrework[exc.tools[toolname]['C']]=toolname + + #final as last becouse names from final tools will be used + for toolname in exc_final.tools.iterkeys(): + numeric_tool=int(toolname) + if numeric_tool>max_numeric_tool: + max_numeric_tool=numeric_tool + toolsrework[exc_final.tools[toolname]['C']]=toolname + + for toolvalues in toolsrework.iterkeys(): + if toolsrework[toolvalues] in exc_final.tools: + if exc_final.tools[toolsrework[toolvalues]]!={"C": toolvalues}: + exc_final.tools[str(max_numeric_tool+1)]={"C": toolvalues} + else: + exc_final.tools[toolsrework[toolvalues]]={"C": toolvalues} + exc_final.create_geometry() + + def build_ui(self): FlatCAMObj.build_ui(self) @@ -1264,11 +1333,16 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): dx, dy = vect - if type(self.solid_geometry) == list: - self.solid_geometry = [affinity.translate(g, xoff=dx, yoff=dy) - for g in self.solid_geometry] - else: - self.solid_geometry = affinity.translate(self.solid_geometry, xoff=dx, yoff=dy) + def translate_recursion(geom): + if type(geom) == list: + geoms=list() + for local_geom in geom: + geoms.append(translate_recursion(local_geom)) + return geoms + else: + return affinity.translate(geom, xoff=dx, yoff=dy) + + self.solid_geometry=translate_recursion(self.solid_geometry) def convert_units(self, units): factor = Geometry.convert_units(self, units) From 62816a614e10b8799e19166ce196cefb85b08407 Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Sun, 21 Feb 2016 17:17:05 +0100 Subject: [PATCH 036/134] OK python does not allow overloading for methods --- FlatCAMObj.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/FlatCAMObj.py b/FlatCAMObj.py index fb785ae..69eea0d 100644 --- a/FlatCAMObj.py +++ b/FlatCAMObj.py @@ -630,10 +630,6 @@ class FlatCAMExcellon(FlatCAMObj, Excellon): # from predecessors. self.ser_attrs += ['options', 'kind'] - @staticmethod - def merge(exc_list, exc_final): - FlatCAMExcellon.merge(exc_list,exc_final,False) - @staticmethod def merge(exc_list, exc_final, copy_options): """ From f73c1b81dcb81595854939680508786bb181ef81 Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Sun, 21 Feb 2016 17:21:51 +0100 Subject: [PATCH 037/134] implement some new shell commands, which helps panelize milling operations shell commands: join_excellons - ability to join excellons together panelize - placing geometries and excellons in columns and rows --- FlatCAMApp.py | 134 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 11f3d46..c32fdce 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -2741,6 +2741,120 @@ class App(QtCore.QObject): if objs is not None: self.new_object("geometry", obj_name, initialize) + def join_excellons(obj_name, *obj_names): + objs = [] + for obj_n in obj_names: + obj = self.collection.get_by_name(str(obj_n)) + if obj is None: + return "Object not found: %s" % obj_n + else: + objs.append(obj) + + def initialize(obj, app): + FlatCAMExcellon.merge(objs, obj,True) + + if objs is not None: + self.new_object("excellon", obj_name, initialize) + + def panelize(name, *args): + a, kwa = h(*args) + types = {'box': str, + 'spacing_columns': float, + 'spacing_rows': float, + 'columns': int, + 'rows': int, + 'outname': str} + + for key in kwa: + if key not in types: + return 'Unknown parameter: %s' % key + kwa[key] = types[key](kwa[key]) + + # Get source object. + try: + obj = self.collection.get_by_name(str(name)) + except: + return "Could not retrieve object: %s" % name + + if obj is None: + return "Object not found: %s" % name + + if 'box' in kwa: + boxname=kwa['box'] + try: + box = self.collection.get_by_name(boxname) + except: + return "Could not retrieve object: %s" % name + else: + box=obj + + if 'columns' not in kwa or 'rows' not in kwa: + return "ERROR: Specify -columns and -rows" + + if 'outname' in kwa: + outname=kwa['outname'] + else: + outname=name+'_panelized' + + if 'spacing_columns' in kwa: + spacing_columns=kwa['spacing_columns'] + else: + spacing_columns=5 + + if 'spacing_rows' in kwa: + spacing_rows=kwa['spacing_rows'] + else: + spacing_rows=5 + + xmin, ymin, xmax, ymax = box.bounds() + lenghtx = xmax-xmin+spacing_columns + lenghty = ymax-ymin+spacing_rows + + currenty=0 + def initialize_local(obj_init, app): + obj_init.solid_geometry = obj.solid_geometry + obj_init.offset([float(currentx), float(currenty)]), + + def initialize_local_excellon(obj_init, app): + FlatCAMExcellon.merge(obj, obj_init,True) + obj_init.offset([float(currentx), float(currenty)]), + + def initialize_geometry(obj_init, app): + FlatCAMGeometry.merge(objs, obj_init) + + def initialize_excellon(obj_init, app): + FlatCAMExcellon.merge(objs, obj_init,True) + + objs=[] + if obj is not None: + + for row in range(kwa['rows']): + currentx=0 + for col in range(kwa['columns']): + local_outname=outname+".tmp."+str(col)+"."+str(row) + if isinstance(obj, FlatCAMExcellon): + new_obj=self.new_object("excellon", local_outname, initialize_local_excellon) + else: + new_obj=self.new_object("geometry", local_outname, initialize_local) + objs.append(new_obj) + currentx=currentx+lenghtx + currenty=currenty+lenghty + + if isinstance(obj, FlatCAMExcellon): + self.new_object("excellon", outname, initialize_excellon) + else: + self.new_object("geometry", outname, initialize_geometry) + + + for delobj in objs: + self.collection.set_active(delobj.options['name']) + self.on_delete() + + else: + return "ERROR: obj is None" + + return "Ok" + def make_docs(): output = '' import collections @@ -3094,6 +3208,26 @@ class App(QtCore.QObject): ' out_name: Name of the new geometry object.' + ' obj_name_0... names of the objects to join' }, + 'join_excellons': { + 'fcn': join_excellons, + 'help': 'Runs a merge operation (join) on the excellon ' + + 'objects.' + + '> join_excellons ....\n' + + ' out_name: Name of the new excellon object.' + + ' obj_name_0... names of the objects to join' + }, + 'panelize': { + 'fcn': panelize, + 'help': "Simple panelize geometries.\n" + + "> panelize [-box ] [-spacing_columns <5 (float)>] [-spacing_rows <5 (float)>] -columns -rows [-outname ]\n" + + " name: Name of the object to panelize.\n" + + " box: Name of object which act as box (cutout for example.) for cutout boundary. Object from name is used if not specified.\n" + + " spacing_columns: spacing between columns\n"+ + " spacing_rows: spacing between rows\n"+ + " columns: number of columns\n"+ + " rows: number of rows\n"+ + " outname: Name of the new geometry object." + }, 'del_rect': { 'fcn': del_rectangle, 'help': 'Delete a rectange from the given Geometry object.\n' + From 1d663c4efe4ecfa149c9b448c51021b55dcb8e81 Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Sun, 21 Feb 2016 17:39:26 +0100 Subject: [PATCH 038/134] allow use aligndrill also for geometries --- FlatCAMApp.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index c32fdce..2e6b0ad 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -2263,9 +2263,8 @@ class App(QtCore.QObject): if obj is None: return "Object not found: %s" % name - if not isinstance(obj, FlatCAMGerber) and not isinstance(obj, FlatCAMExcellon): - return "ERROR: Only Gerber and Excellon objects can be used." - + if not isinstance(obj, FlatCAMGeometry) and not isinstance(obj, FlatCAMGerber) and not isinstance(obj, FlatCAMExcellon): + return "ERROR: Only Gerber, Geometry and Excellon objects can be used." # Axis try: From f119f4de034098ef7a83fb7e6a99ff5cd2d8780e Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Mon, 22 Feb 2016 10:50:06 +0100 Subject: [PATCH 039/134] implement command aligndrillgrid, which creates grid of holes to bed --- FlatCAMApp.py | 68 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 2e6b0ad..4af101b 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -2238,6 +2238,61 @@ class App(QtCore.QObject): return 'Ok' + def aligndrillgrid(outname, *args): + a, kwa = h(*args) + types = {'gridx': float, + 'gridy': float, + 'gridoffsetx': float, + 'gridoffsety': float, + 'columns':int, + 'rows':int, + 'dia': float + } + for key in kwa: + if key not in types: + return 'Unknown parameter: %s' % key + kwa[key] = types[key](kwa[key]) + + + if 'columns' not in kwa or 'rows' not in kwa: + return "ERROR: Specify -columns and -rows" + + if 'gridx' not in kwa or 'gridy' not in kwa: + return "ERROR: Specify -gridx and -gridy" + + if 'dia' not in kwa: + return "ERROR: Specify -dia" + + if 'gridoffsetx' not in kwa: + gridoffsetx=0 + else: + gridoffsetx=kwa['gridoffsetx'] + + if 'gridoffsety' not in kwa: + gridoffsety=0 + else: + gridoffsety=kwa['gridoffsety'] + + + # Tools + tools = {"1": {"C": kwa['dia']}} + + def aligndrillgrid_init_me(init_obj, app_obj): + drills = [] + currenty=0 + for row in range(kwa['rows']): + currentx=0 + for col in range(kwa['columns']): + point = Point(currentx-gridoffsetx,currenty-gridoffsety) + drills.append({"point": point, "tool": "1"}) + currentx=currentx+kwa['gridx'] + currenty=currenty+kwa['gridy'] + init_obj.tools = tools + init_obj.drills = drills + init_obj.create_geometry() + + self.new_object("excellon", outname , aligndrillgrid_init_me) + def aligndrill(name, *args): a, kwa = h(*args) types = {'box': str, @@ -3067,6 +3122,19 @@ class App(QtCore.QObject): " axis: Mirror axis parallel to the X or Y axis.\n" + " dist: Distance of the mirror axis to the X or Y axis." }, + 'aligndrillgrid': { + 'fcn': aligndrillgrid, + 'help': "Create excellon with drills for aligment grid.\n" + + "> aligndrillgrid [-dia <3.0 (float)>] -gridx [-gridoffsetx <0 (float)>] -gridy [-gridoffsety <0 (float)>] -columns -rows \n" + + " outname: Name of the object to create.\n" + + " dia: Tool diameter\n" + + " gridx: grid size in X axis\n" + + " gridoffsetx: move grid from origin\n" + + " gridy: grid size in Y axis\n" + + " gridoffsety: move grid from origin\n" + + " colums: grid holes on X axis\n" + + " rows: grid holes on Y axis\n" + }, 'aligndrill': { 'fcn': aligndrill, 'help': "Create excellon with drills for aligment.\n" + From 23d5d7bd64889372747dc7bcc55300f1e9ae0e22 Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Mon, 22 Feb 2016 11:19:30 +0100 Subject: [PATCH 040/134] aligndrillgrid - fix offset direction -5 should be -x axis --- FlatCAMApp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 4af101b..9455ed8 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -2283,7 +2283,7 @@ class App(QtCore.QObject): for row in range(kwa['rows']): currentx=0 for col in range(kwa['columns']): - point = Point(currentx-gridoffsetx,currenty-gridoffsety) + point = Point(currentx+gridoffsetx,currenty+gridoffsety) drills.append({"point": point, "tool": "1"}) currentx=currentx+kwa['gridx'] currenty=currenty+kwa['gridy'] From a827e184b74b49bac96fb1b1e0fd388b3148d15d Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Tue, 23 Feb 2016 00:23:27 +0100 Subject: [PATCH 041/134] rename del_polygon to subtract_polygon correctly modify current geometry and dont leave it as path fix shellcommands to follow new names tweak geocutout to be able cut 8 gaps --- FlatCAMApp.py | 80 +++++++++++++++++++++++++++++---------------------- camlib.py | 10 +++---- 2 files changed, 50 insertions(+), 40 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 9455ed8..ed30657 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -2137,7 +2137,7 @@ class App(QtCore.QObject): def geocutout(name, *args): """ - cut gaps in current geometry + subtract gaps from geometry, this will not create new object :param name: :param args: @@ -2148,6 +2148,14 @@ class App(QtCore.QObject): 'gapsize': float, 'gaps': str} + #way gaps wil be rendered: + # lr - left + right + # tb - top + bottom + # 4 - left + right +top + bottom + # 2lr - 2*left + 2*right + # 2tb - 2*top + 2*bottom + # 8 - 2*left + 2*right +2*top + 2*bottom + for key in kwa: if key not in types: return 'Unknown parameter: %s' % key @@ -2159,15 +2167,23 @@ class App(QtCore.QObject): return "Could not retrieve object: %s" % name - + #get min and max data for each object as we just cut rectangles across X or Y xmin, ymin, xmax, ymax = obj.bounds() px = 0.5 * (xmin + xmax) py = 0.5 * (ymin + ymax) + lenghtx = (xmax - xmin) + lenghty = (ymax - ymin) gapsize = kwa['gapsize']+kwa['dia']/2 + if kwa['gaps'] == '8' or kwa['gaps']=='2lr': + subtract_rectangle(name,xmin-gapsize,py-gapsize+lenghty/4,xmax+gapsize,py+gapsize+lenghty/4) + subtract_rectangle(name,xmin-gapsize,py-gapsize-lenghty/4,xmax+gapsize,py+gapsize-lenghty/4) + if kwa['gaps'] == '8' or kwa['gaps']=='2tb': + subtract_rectangle(name,px-gapsize+lenghtx/4,ymin-gapsize,px+gapsize+lenghtx/4,ymax+gapsize) + subtract_rectangle(name,px-gapsize-lenghtx/4,ymin-gapsize,px+gapsize-lenghtx/4,ymax+gapsize) if kwa['gaps'] == '4' or kwa['gaps']=='lr': - del_rectangle(name,xmin-gapsize,py-gapsize,xmax+gapsize,py+gapsize) + subtract_rectangle(name,xmin-gapsize,py-gapsize,xmax+gapsize,py+gapsize) if kwa['gaps'] == '4' or kwa['gaps']=='tb': - del_rectangle(name,px-gapsize,ymin-gapsize,px+gapsize,ymax+gapsize) + subtract_rectangle(name,px-gapsize,ymin-gapsize,px+gapsize,ymax+gapsize) return 'Ok' def mirror(name, *args): @@ -2456,26 +2472,26 @@ class App(QtCore.QObject): return "ERROR: Only Excellon objects can be drilled." try: - + # Get the tools from the list job_name = kwa["outname"] - + # Object initialization function for app.new_object() def job_init(job_obj, app_obj): assert isinstance(job_obj, FlatCAMCNCjob), \ "Initializer expected FlatCAMCNCjob, got %s" % type(job_obj) - + job_obj.z_cut = kwa["drillz"] job_obj.z_move = kwa["travelz"] job_obj.feedrate = kwa["feedrate"] job_obj.spindlespeed = kwa["spindlespeed"] if "spindlespeed" in kwa else None toolchange = True if "toolchange" in kwa and kwa["toolchange"] == 1 else False job_obj.generate_from_excellon_by_tool(obj, kwa["tools"], toolchange) - + job_obj.gcode_parse() - + job_obj.create_geometry() - + obj.app.new_object("cncjob", job_name, job_init) except Exception, e: @@ -2600,7 +2616,7 @@ class App(QtCore.QObject): types = {'dia': float, 'passes': int, 'overlap': float, - 'outname': str, + 'outname': str, 'combine': int} for key in kwa: @@ -2718,7 +2734,7 @@ class App(QtCore.QObject): return add_poly(obj_name, botleft_x, botleft_y, botleft_x, topright_y, topright_x, topright_y, topright_x, botleft_y) - def del_poly(obj_name, *args): + def subtract_poly(obj_name, *args): if len(args) % 2 != 0: return "Incomplete coordinate." @@ -2731,19 +2747,13 @@ class App(QtCore.QObject): if obj is None: return "Object not found: %s" % obj_name - def init_obj_me(init_obj, app): - assert isinstance(init_obj, FlatCAMGeometry) - init_obj.solid_geometry=cascaded_union(diff) + obj.subtract_polygon(points) + obj.plot() - diff= obj.del_polygon(points) - try: - delete(obj_name) - obj.app.new_object("geometry", obj_name, init_obj_me) - except Exception as e: - return "Failed: %s" % str(e) + return "OK." - def del_rectangle(obj_name, botleft_x, botleft_y, topright_x, topright_y): - return del_poly(obj_name, botleft_x, botleft_y, botleft_x, topright_y, + def subtract_rectangle(obj_name, botleft_x, botleft_y, topright_x, topright_y): + return subtract_poly(obj_name, botleft_x, botleft_y, botleft_x, topright_y, topright_x, topright_y, topright_x, botleft_y) def add_circle(obj_name, center_x, center_y, radius): @@ -3091,8 +3101,8 @@ class App(QtCore.QObject): }, 'geocutout': { 'fcn': geocutout, - 'help': "Cut holding gaps closed geometry.\n" + - "> geocutout [-dia <3.0 (float)>] [-margin <0.0 (float)>] [-gapsize <0.5 (float)>] [-gaps ]\n" + + 'help': "Cut holding gaps from geometry.\n" + + "> geocutout [-dia <3.0 (float)>] [-margin <0.0 (float)>] [-gapsize <0.5 (float)>] [-gaps ]\n" + " name: Name of the geometry object\n" + " dia: Tool diameter\n" + " margin: Margin over bounds\n" + @@ -3245,11 +3255,11 @@ class App(QtCore.QObject): ' name: Name of the geometry object to which to append the polygon.\n' + ' xi, yi: Coordinates of points in the polygon.' }, - 'del_poly': { - 'fcn': del_poly, - 'help': ' - Remove a polygon from the given Geometry object.\n' + - '> del_poly [x3 y3 [...]]\n' + - ' name: Name of the geometry object to which to remove the polygon.\n' + + 'subtract_poly': { + 'fcn': subtract_poly, + 'help': 'Subtract polygon from the given Geometry object.\n' + + '> subtract_poly [x3 y3 [...]]\n' + + ' name: Name of the geometry object, which will be sutracted.\n' + ' xi, yi: Coordinates of points in the polygon.' }, 'delete': { @@ -3295,11 +3305,11 @@ class App(QtCore.QObject): " rows: number of rows\n"+ " outname: Name of the new geometry object." }, - 'del_rect': { - 'fcn': del_rectangle, - 'help': 'Delete a rectange from the given Geometry object.\n' + - '> del_rect \n' + - ' name: Name of the geometry object to which to remove the rectangle.\n' + + 'subtract_rect': { + 'fcn': subtract_rectangle, + 'help': 'Subtract rectange from the given Geometry object.\n' + + '> subtract_rect \n' + + ' name: Name of the geometry object, which will be subtracted.\n' + ' botleft_x, botleft_y: Coordinates of the bottom left corner.\n' + ' topright_x, topright_y Coordinates of the top right corner.' }, diff --git a/camlib.py b/camlib.py index b919f39..f576ed9 100644 --- a/camlib.py +++ b/camlib.py @@ -136,17 +136,17 @@ class Geometry(object): log.error("Failed to run union on polygons.") raise - def del_polygon(self, points): + def subtract_polygon(self, points): """ - Delete a polygon from the object + Subtract polygon from the given object. This only operates on the paths in the original geometry, i.e. it converts polygons into paths. :param points: The vertices of the polygon. - :return: None + :return: none """ if self.solid_geometry is None: self.solid_geometry = [] - + #pathonly should be allways True, otherwise polygons are not subtracted flat_geometry = self.flatten(pathonly=True) log.debug("%d paths" % len(flat_geometry)) polygon=Polygon(points) @@ -157,7 +157,7 @@ class Geometry(object): diffs.append(target.difference(toolgeo)) else: log.warning("Not implemented.") - return cascaded_union(diffs) + self.solid_geometry=cascaded_union(diffs) def bounds(self): """ From a3ccbac3621009b14ea75257b353f6a4b8c048a1 Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Tue, 23 Feb 2016 12:00:30 +0100 Subject: [PATCH 042/134] add set_all_inactive and set_inactive, to be able deselect objects mainly to avoid accidental delete --- ObjectCollection.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/ObjectCollection.py b/ObjectCollection.py index 005f2bc..727358d 100644 --- a/ObjectCollection.py +++ b/ObjectCollection.py @@ -244,6 +244,27 @@ class ObjectCollection(QtCore.QAbstractListModel): iobj = self.createIndex(self.get_names().index(name), 0) # Column 0 self.view.selectionModel().select(iobj, QtGui.QItemSelectionModel.Select) + def set_inactive(self, name): + """ + Unselect object by name from the project list. This triggers the + list_selection_changed event and call on_list_selection_changed. + + :param name: Name of the FlatCAM Object + :return: None + """ + iobj = self.createIndex(self.get_names().index(name), 0) # Column 0 + self.view.selectionModel().select(iobj, QtGui.QItemSelectionModel.Deselect) + + def set_all_inactive(self): + """ + Unselect all objects from the project list. This triggers the + list_selection_changed event and call on_list_selection_changed. + + :return: None + """ + for name in self.get_names(): + self.set_inactive(name) + def on_list_selection_change(self, current, previous): FlatCAMApp.App.log.debug("on_list_selection_change()") FlatCAMApp.App.log.debug("Current: %s, Previous %s" % (str(current), str(previous))) From c3e544ac6c75eaa7fd347c81184adbbbd61b211d Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Tue, 23 Feb 2016 12:21:57 +0100 Subject: [PATCH 043/134] FlatCAMObj - to_form,read_form,read_form_item cleanups for better debuging and cleanup Excellon merge method FlatCAMApp - fix accidentall delete issue, change calling to understand FlatCAMObj changes --- FlatCAMApp.py | 11 +++++--- FlatCAMObj.py | 74 ++++++++++++++++++++++++++++++++------------------- 2 files changed, 53 insertions(+), 32 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index ed30657..4d36fc9 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -2774,6 +2774,8 @@ class App(QtCore.QObject): def delete(obj_name): try: + #deselect all to avoid delete selected object when run delete from shell + self.collection.set_all_inactive() self.collection.set_active(str(obj_name)) self.on_delete() except Exception, e: @@ -2815,7 +2817,7 @@ class App(QtCore.QObject): objs.append(obj) def initialize(obj, app): - FlatCAMExcellon.merge(objs, obj,True) + FlatCAMExcellon.merge(objs, obj) if objs is not None: self.new_object("excellon", obj_name, initialize) @@ -2880,14 +2882,14 @@ class App(QtCore.QObject): obj_init.offset([float(currentx), float(currenty)]), def initialize_local_excellon(obj_init, app): - FlatCAMExcellon.merge(obj, obj_init,True) + FlatCAMExcellon.merge(obj, obj_init) obj_init.offset([float(currentx), float(currenty)]), def initialize_geometry(obj_init, app): FlatCAMGeometry.merge(objs, obj_init) def initialize_excellon(obj_init, app): - FlatCAMExcellon.merge(objs, obj_init,True) + FlatCAMExcellon.merge(objs, obj_init) objs=[] if obj is not None: @@ -2909,7 +2911,8 @@ class App(QtCore.QObject): else: self.new_object("geometry", outname, initialize_geometry) - + #deselect all to avoid delete selected object when run delete from shell + self.collection.set_all_inactive() for delobj in objs: self.collection.set_active(delobj.options['name']) self.on_delete() diff --git a/FlatCAMObj.py b/FlatCAMObj.py index 69eea0d..c604302 100644 --- a/FlatCAMObj.py +++ b/FlatCAMObj.py @@ -123,8 +123,12 @@ class FlatCAMObj(QtCore.QObject): :return: None """ + FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> FlatCAMObj.to_form()") for option in self.options: - self.set_form_item(option) + try: + self.set_form_item(option) + except: + self.app.log.warning("Unexpected error:", sys.exc_info()) def read_form(self): """ @@ -135,7 +139,11 @@ class FlatCAMObj(QtCore.QObject): """ FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> FlatCAMObj.read_form()") for option in self.options: - self.read_form_item(option) + try: + self.read_form_item(option) + except: + self.app.log.warning("Unexpected error:", sys.exc_info()) + def build_ui(self): """ @@ -191,11 +199,16 @@ class FlatCAMObj(QtCore.QObject): :type option: str :return: None """ - - try: - self.options[option] = self.form_fields[option].get_value() - except KeyError: - self.app.log.warning("Failed to read option from field: %s" % option) + #try read field only when option have equivalent in form_fields + if option in self.form_fields: + option_type=type(self.options[option]) + try: + value=self.form_fields[option].get_value() + #catch per option as it was ignored anyway, also when syntax error (probably uninitialized field),don't read either. + except (KeyError,SyntaxError): + self.app.log.warning("Failed to read option from field: %s" % option) + else: + self.app.log.warning("Form fied does not exists: %s" % option) def plot(self): """ @@ -631,13 +644,16 @@ class FlatCAMExcellon(FlatCAMObj, Excellon): self.ser_attrs += ['options', 'kind'] @staticmethod - def merge(exc_list, exc_final, copy_options): + def merge(exc_list, exc_final): """ - Merges(copy if used on one) the excellon of objects in exc_list into - options have same like exc_final - the geometry of geo_final. + Merge excellons in exc_list into exc_final. + Options are allways copied from source . - :param exc_list: List of FlatCAMExcellon Objects to join. + Tools are also merged, if name for tool is same and size differs, then as name is used next available number from both lists + + if only one object is specified in exc_list then this acts as copy only + + :param exc_list: List or one object of FlatCAMExcellon Objects to join. :param exc_final: Destination FlatCAMExcellon object. :return: None """ @@ -648,26 +664,27 @@ class FlatCAMExcellon(FlatCAMObj, Excellon): else: exc_list_real=exc_list - for exc in exc_list_real: # Expand lists if type(exc) is list: - FlatCAMExcellon.merge(exc, exc_final, copy_options) - - # If not list, just append + FlatCAMExcellon.merge(exc, exc_final) + # If not list, merge excellons else: - if copy_options is True: - exc_final.options["plot"]=exc.options["plot"] - exc_final.options["solid"]=exc.options["solid"] - exc_final.options["drillz"]=exc.options["drillz"] - exc_final.options["travelz"]=exc.options["travelz"] - exc_final.options["feedrate"]=exc.options["feedrate"] - exc_final.options["tooldia"]=exc.options["tooldia"] - exc_final.options["toolchange"]=exc.options["toolchange"] - exc_final.options["toolchangez"]=exc.options["toolchangez"] - exc_final.options["spindlespeed"]=exc.options["spindlespeed"] + # TODO: I realize forms does not save values into options , when object is deselected + # leave this here for future use + # this reinitialize options based on forms, all steps may not be necessary + # exc.app.collection.set_active(exc.options['name']) + # exc.to_form() + # exc.read_form() + for option in exc.options: + if option is not 'name': + try: + exc_final.options[option] = exc.options[option] + except: + exc.app.log.warning("Failed to copy option.",option) + #deep copy of all drills,to avoid any references for drill in exc.drills: point = Point(drill['point'].x,drill['point'].y) exc_final.drills.append({"point": point, "tool": drill['tool']}) @@ -679,7 +696,7 @@ class FlatCAMExcellon(FlatCAMObj, Excellon): max_numeric_tool=numeric_tool toolsrework[exc.tools[toolname]['C']]=toolname - #final as last becouse names from final tools will be used + #exc_final as last because names from final tools will be used for toolname in exc_final.tools.iterkeys(): numeric_tool=int(toolname) if numeric_tool>max_numeric_tool: @@ -692,9 +709,10 @@ class FlatCAMExcellon(FlatCAMObj, Excellon): exc_final.tools[str(max_numeric_tool+1)]={"C": toolvalues} else: exc_final.tools[toolsrework[toolvalues]]={"C": toolvalues} + #this value was not co + exc_final.zeros=exc.zeros exc_final.create_geometry() - def build_ui(self): FlatCAMObj.build_ui(self) From ba94aef0692d4d440dd7301bf3c9604cd4d3d861 Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Wed, 24 Feb 2016 22:37:23 +0100 Subject: [PATCH 044/134] fix aligndrill and also logicical errors in it --- FlatCAMApp.py | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 4d36fc9..4d87137 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -2315,6 +2315,7 @@ class App(QtCore.QObject): 'axis': str, 'holes': str, 'grid': float, + 'minoffset': float, 'gridoffset': float, 'axisoffset': float, 'dia': float, @@ -2375,25 +2376,22 @@ class App(QtCore.QObject): else: axisoffset=0 - + #this will align hole to given aligngridoffset and minimal offset from pcb, based on selected axis if axis == "X": - firstpoint=-kwa['gridoffset']+xmin - #-5 - minlenght=(xmax-xmin+2*kwa['gridoffset']) - #57+10=67 - gridstripped=(minlenght//kwa['grid'])*kwa['grid'] - #67//10=60 - if (minlenght-gridstripped) >kwa['gridoffset']: - gridstripped=gridstripped+kwa['grid'] - lastpoint=(firstpoint+gridstripped) + firstpoint=kwa['gridoffset'] + while (xmin-kwa['minoffset'])lastpoint: + lastpoint=lastpoint+kwa['grid'] localHoles=(firstpoint,axisoffset),(lastpoint,axisoffset) else: - firstpoint=-kwa['gridoffset']+ymin - minlenght=(ymax-ymin+2*kwa['gridoffset']) - gridstripped=minlenght//kwa['grid']*kwa['grid'] - if (minlenght-gridstripped) >kwa['gridoffset']: - gridstripped=gridstripped+kwa['grid'] - lastpoint=(firstpoint+gridstripped) + firstpoint=kwa['gridoffset'] + while (ymin-kwa['minoffset'])lastpoint: + lastpoint=lastpoint+kwa['grid'] localHoles=(axisoffset,firstpoint),(axisoffset,lastpoint) for hole in localHoles: @@ -3151,12 +3149,13 @@ class App(QtCore.QObject): 'aligndrill': { 'fcn': aligndrill, 'help': "Create excellon with drills for aligment.\n" + - "> aligndrill [-dia <3.0 (float)>] -axis [-box [-grid <10 (float)> -gridoffset <5 (float)> [-axisoffset <0 (float)>]] | -dist ]\n" + + "> aligndrill [-dia <3.0 (float)>] -axis [-box -minoffset [-grid <10 (float)> -gridoffset <5 (float)> [-axisoffset <0 (float)>]] | -dist ]\n" + " name: Name of the object (Gerber or Excellon) to mirror.\n" + " dia: Tool diameter\n" + " box: Name of object which act as box (cutout for example.)\n" + " grid: aligning to grid, for thouse, who have aligning pins inside table in grid (-5,0),(5,0),(15,0)..." + - " gridoffset: offset from pcb from 0 position and minimal offset to grid on max" + + " gridoffset: offset of grid from 0 position" + + " minoffset: min and max distance between align hole and pcb" + " axisoffset: offset on second axis before aligment holes" + " axis: Mirror axis parallel to the X or Y axis.\n" + " dist: Distance of the mirror axis to the X or Y axis." From 9420aaad60fc77c84b99cdeae5fca229a2ba6c02 Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Thu, 25 Feb 2016 00:03:19 +0100 Subject: [PATCH 045/134] add multidepth and depthperpass to cncjob shell command --- FlatCAMApp.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 4d87137..c4138a6 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -2648,7 +2648,9 @@ class App(QtCore.QObject): 'feedrate': float, 'tooldia': float, 'outname': str, - 'spindlespeed': int + 'spindlespeed': int, + 'multidepth' : bool, + 'depthperpass' : float } for key in kwa: From 26189960ff789d0981def4e260baf3c5d07f5f4f Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Thu, 25 Feb 2016 16:31:57 +0100 Subject: [PATCH 046/134] update help --- FlatCAMApp.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index c4138a6..95541e8 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -3221,14 +3221,17 @@ class App(QtCore.QObject): 'cncjob': { 'fcn': cncjob, 'help': 'Generates a CNC Job from a Geometry Object.\n' + - '> cncjob [-z_cut ] [-z_move ] [-feedrate ] [-tooldia ] [-spindlespeed (int)] [-outname ]\n' + + '> cncjob [-z_cut ] [-z_move ] [-feedrate ] [-tooldia ] [-spindlespeed ] [-multidepth ] [-depthperpass ] [-outname ]\n' + ' name: Name of the source object\n' + ' z_cut: Z-axis cutting position\n' + ' z_move: Z-axis moving position\n' + ' feedrate: Moving speed when cutting\n' + ' tooldia: Tool diameter to show on screen\n' + ' spindlespeed: Speed of the spindle in rpm (example: 4000)\n' + + ' multidepth: Use or not multidepth cnccut\n'+ + ' depthperpass: Height of one layer for multidepth\n'+ ' outname: Name of the output object' + }, 'write_gcode': { 'fcn': write_gcode, From dc374a8233b293abf8874a2ae5af33a58df80ba2 Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Thu, 25 Feb 2016 16:33:44 +0100 Subject: [PATCH 047/134] remove blank line --- FlatCAMApp.py | 1 - 1 file changed, 1 deletion(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 95541e8..6af4afe 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -3231,7 +3231,6 @@ class App(QtCore.QObject): ' multidepth: Use or not multidepth cnccut\n'+ ' depthperpass: Height of one layer for multidepth\n'+ ' outname: Name of the output object' - }, 'write_gcode': { 'fcn': write_gcode, From 0cc60576ab30bc6827b8aab96d222bbc89d128f2 Mon Sep 17 00:00:00 2001 From: Juan Pablo Caram Date: Mon, 29 Feb 2016 13:59:20 -0500 Subject: [PATCH 048/134] Reverted changes to read_form_item(). See #193. --- FlatCAMObj.py | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/FlatCAMObj.py b/FlatCAMObj.py index c604302..b8315fc 100644 --- a/FlatCAMObj.py +++ b/FlatCAMObj.py @@ -144,7 +144,6 @@ class FlatCAMObj(QtCore.QObject): except: self.app.log.warning("Unexpected error:", sys.exc_info()) - def build_ui(self): """ Sets up the UI/form for this object. Show the UI @@ -199,16 +198,22 @@ class FlatCAMObj(QtCore.QObject): :type option: str :return: None """ - #try read field only when option have equivalent in form_fields - if option in self.form_fields: - option_type=type(self.options[option]) - try: - value=self.form_fields[option].get_value() - #catch per option as it was ignored anyway, also when syntax error (probably uninitialized field),don't read either. - except (KeyError,SyntaxError): - self.app.log.warning("Failed to read option from field: %s" % option) - else: - self.app.log.warning("Form fied does not exists: %s" % option) + + try: + self.options[option] = self.form_fields[option].get_value() + except KeyError: + self.app.log.warning("Failed to read option from field: %s" % option) + + # #try read field only when option have equivalent in form_fields + # if option in self.form_fields: + # option_type=type(self.options[option]) + # try: + # value=self.form_fields[option].get_value() + # #catch per option as it was ignored anyway, also when syntax error (probably uninitialized field),don't read either. + # except (KeyError,SyntaxError): + # self.app.log.warning("Failed to read option from field: %s" % option) + # else: + # self.app.log.warning("Form fied does not exists: %s" % option) def plot(self): """ From ced43df1bc6bd19ed8b5a4e1a6ce503f6fcbb23f Mon Sep 17 00:00:00 2001 From: Juan Pablo Caram Date: Mon, 29 Feb 2016 14:18:50 -0500 Subject: [PATCH 049/134] Catch when recent file type is not supported. See #192. --- FlatCAMApp.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 6af4afe..711a6d6 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -3428,13 +3428,17 @@ class App(QtCore.QObject): for recent in self.recent: filename = recent['filename'].split('/')[-1].split('\\')[-1] - action = QtGui.QAction(QtGui.QIcon(icons[recent["kind"]]), filename, self) + try: + action = QtGui.QAction(QtGui.QIcon(icons[recent["kind"]]), filename, self) - # Attach callback - o = make_callback(openers[recent["kind"]], recent['filename']) - action.triggered.connect(o) + # Attach callback + o = make_callback(openers[recent["kind"]], recent['filename']) + action.triggered.connect(o) - self.ui.recent.addAction(action) + self.ui.recent.addAction(action) + + except KeyError: + App.log.error("Unsupported file type: %s" % recent["kind"]) # self.builder.get_object('open_recent').set_submenu(recent_menu) # self.ui.menufilerecent.set_submenu(recent_menu) From 3878ddb78227be5fe2e67be9f616121297a50de2 Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Mon, 29 Feb 2016 22:22:23 +0100 Subject: [PATCH 050/134] display more precise answer if something in TCL shell fail --- FlatCAMApp.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 711a6d6..0edea07 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -659,7 +659,11 @@ class App(QtCore.QObject): result = self.tcl.eval(str(text)) self.shell.append_output(result + '\n') except Tkinter.TclError, e: - self.shell.append_error('ERROR: ' + str(e) + '\n') + #this will display more precise answer if something in TCL shell fail + result = self.tcl.eval("set errorInfo") + self.log.error("Exec command Exception: %s" % (str(e)+ '\n' + result + '\n')) + self.shell.append_error('ERROR: ' + str(e) + '\n' + result + '\n') + raise e return """ From fd869ad88c727a24db438193902a4f6c837aa950 Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Tue, 1 Mar 2016 18:15:38 +0100 Subject: [PATCH 051/134] remove raise, it does not kill app, but raise is unnecessary here --- FlatCAMApp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 0edea07..78f606c 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -663,7 +663,7 @@ class App(QtCore.QObject): result = self.tcl.eval("set errorInfo") self.log.error("Exec command Exception: %s" % (str(e)+ '\n' + result + '\n')) self.shell.append_error('ERROR: ' + str(e) + '\n' + result + '\n') - raise e + #show error in console and just return return """ From 2cc3d811c58ec0c25a135626b5791c99fdb7b14c Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Tue, 1 Mar 2016 18:22:57 +0100 Subject: [PATCH 052/134] remove duplicity when print error --- FlatCAMApp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 78f606c..e87d5be 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -662,7 +662,7 @@ class App(QtCore.QObject): #this will display more precise answer if something in TCL shell fail result = self.tcl.eval("set errorInfo") self.log.error("Exec command Exception: %s" % (str(e)+ '\n' + result + '\n')) - self.shell.append_error('ERROR: ' + str(e) + '\n' + result + '\n') + self.shell.append_error('ERROR: ' + result + '\n') #show error in console and just return return From 3fd9b361b8e47a9b1f1ed6c64569cada35df63bb Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Wed, 2 Mar 2016 00:41:54 +0100 Subject: [PATCH 053/134] implement raiseTclError and as example use it in drillcncjob --- FlatCAMApp.py | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index e87d5be..15adb17 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -644,6 +644,16 @@ class App(QtCore.QObject): else: self.defaults['stats'][resource] = 1 + def raiseTclError(self, text): + """ + this method pass exception from python into TCL as error, so we get stacktrace and reason + :param text: text of error + :return: raise exception + """ + self.tcl.eval('error "%s"' % text) + raise Exception(text) + + def exec_command(self, text): """ Handles input from the shell. See FlatCAMApp.setup_shell for shell commands. @@ -661,7 +671,7 @@ class App(QtCore.QObject): except Tkinter.TclError, e: #this will display more precise answer if something in TCL shell fail result = self.tcl.eval("set errorInfo") - self.log.error("Exec command Exception: %s" % (str(e)+ '\n' + result + '\n')) + self.log.error("Exec command Exception: %s" % (result + '\n')) self.shell.append_error('ERROR: ' + result + '\n') #show error in console and just return return @@ -2446,7 +2456,9 @@ class App(QtCore.QObject): return 'Ok' - def drillcncjob(name, *args): + def drillcncjob(name=None, *args): + #name should not be none, but we set it to default, because TCL return error without reason if argument is missing + #we should check it inside shell commamnd instead a, kwa = h(*args) types = {'tools': str, 'outname': str, @@ -2457,21 +2469,24 @@ class App(QtCore.QObject): 'toolchange': int } + if name is None: + self.raiseTclError('Argument name is missing.') + for key in kwa: if key not in types: - return 'Unknown parameter: %s' % key + self.raiseTclError('Unknown parameter: %s' % key) kwa[key] = types[key](kwa[key]) try: obj = self.collection.get_by_name(str(name)) except: - return "Could not retrieve object: %s" % name + self.raiseTclError("Could not retrieve object: %s" % name) if obj is None: - return "Object not found: %s" % name + self.raiseTclError('Object not found: %s' % name) if not isinstance(obj, FlatCAMExcellon): - return "ERROR: Only Excellon objects can be drilled." + self.raiseTclError('Only Excellon objects can be drilled: %s' % name) try: @@ -2497,7 +2512,7 @@ class App(QtCore.QObject): obj.app.new_object("cncjob", job_name, job_init) except Exception, e: - return "Operation failed: %s" % str(e) + self.raiseTclError("Operation failed: %s" % str(e)) return 'Ok' From a8159dee16f4c1c67fa3f637325942ae4c8750a2 Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Wed, 2 Mar 2016 00:45:49 +0100 Subject: [PATCH 054/134] "return -code error XXX" display error in better way --- FlatCAMApp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 15adb17..719d1c2 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -650,7 +650,7 @@ class App(QtCore.QObject): :param text: text of error :return: raise exception """ - self.tcl.eval('error "%s"' % text) + self.tcl.eval('return -code error "%s"' % text) raise Exception(text) From b4abef8317bf0f6f9fae0eafef216279a809a693 Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Wed, 2 Mar 2016 00:46:23 +0100 Subject: [PATCH 055/134] remove Empty line --- FlatCAMApp.py | 1 - 1 file changed, 1 deletion(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 719d1c2..835625f 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -653,7 +653,6 @@ class App(QtCore.QObject): self.tcl.eval('return -code error "%s"' % text) raise Exception(text) - def exec_command(self, text): """ Handles input from the shell. See FlatCAMApp.setup_shell for shell commands. From e3c43f6de1a138dc2c9580876b37002e8b7abbbc Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Wed, 2 Mar 2016 00:49:51 +0100 Subject: [PATCH 056/134] remove Empty line --- FlatCAMApp.py | 1 - 1 file changed, 1 deletion(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 835625f..9671f02 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -2454,7 +2454,6 @@ class App(QtCore.QObject): return 'Ok' - def drillcncjob(name=None, *args): #name should not be none, but we set it to default, because TCL return error without reason if argument is missing #we should check it inside shell commamnd instead From 0f438db833fd2fc04493276e8c6bf22d022b0917 Mon Sep 17 00:00:00 2001 From: jpcgt Date: Thu, 3 Mar 2016 14:51:36 +0000 Subject: [PATCH 057/134] Several PEP8 cleanups in shell commands. --- FlatCAMApp.py | 105 ++++++++++++++++++++++++++++++++++---------------- 1 file changed, 72 insertions(+), 33 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 9671f02..a5d37f1 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -2150,7 +2150,7 @@ class App(QtCore.QObject): def geocutout(name, *args): """ - subtract gaps from geometry, this will not create new object + Subtract gaps from geometry, this will not create new object :param name: :param args: @@ -2161,7 +2161,7 @@ class App(QtCore.QObject): 'gapsize': float, 'gaps': str} - #way gaps wil be rendered: + # How gaps wil be rendered: # lr - left + right # tb - top + bottom # 4 - left + right +top + bottom @@ -2179,24 +2179,53 @@ class App(QtCore.QObject): except: return "Could not retrieve object: %s" % name - - #get min and max data for each object as we just cut rectangles across X or Y + # Get min and max data for each object as we just cut rectangles across X or Y xmin, ymin, xmax, ymax = obj.bounds() px = 0.5 * (xmin + xmax) py = 0.5 * (ymin + ymax) lenghtx = (xmax - xmin) lenghty = (ymax - ymin) - gapsize = kwa['gapsize']+kwa['dia']/2 + gapsize = kwa['gapsize'] + kwa['dia'] / 2 + if kwa['gaps'] == '8' or kwa['gaps']=='2lr': - subtract_rectangle(name,xmin-gapsize,py-gapsize+lenghty/4,xmax+gapsize,py+gapsize+lenghty/4) - subtract_rectangle(name,xmin-gapsize,py-gapsize-lenghty/4,xmax+gapsize,py+gapsize-lenghty/4) + + subtract_rectangle(name, + xmin - gapsize, + py - gapsize + lenghty / 4, + xmax + gapsize, + py + gapsize + lenghty / 4) + subtract_rectangle(name, + xmin-gapsize, + py - gapsize - lenghty / 4, + xmax + gapsize, + py + gapsize - lenghty / 4) + if kwa['gaps'] == '8' or kwa['gaps']=='2tb': - subtract_rectangle(name,px-gapsize+lenghtx/4,ymin-gapsize,px+gapsize+lenghtx/4,ymax+gapsize) - subtract_rectangle(name,px-gapsize-lenghtx/4,ymin-gapsize,px+gapsize-lenghtx/4,ymax+gapsize) + subtract_rectangle(name, + px - gapsize + lenghtx / 4, + ymin-gapsize, + px + gapsize + lenghtx / 4, + ymax + gapsize) + subtract_rectangle(name, + px - gapsize - lenghtx / 4, + ymin - gapsize, + px + gapsize - lenghtx / 4, + ymax + gapsize) + if kwa['gaps'] == '4' or kwa['gaps']=='lr': - subtract_rectangle(name,xmin-gapsize,py-gapsize,xmax+gapsize,py+gapsize) + subtract_rectangle(name, + xmin - gapsize, + py - gapsize, + xmax + gapsize, + py + gapsize) + if kwa['gaps'] == '4' or kwa['gaps']=='tb': - subtract_rectangle(name,px-gapsize,ymin-gapsize,px+gapsize,ymax+gapsize) + subtract_rectangle(name, + px - gapsize, + ymin - gapsize, + px + gapsize, + ymax + gapsize) + return 'Ok' def mirror(name, *args): @@ -2222,14 +2251,12 @@ class App(QtCore.QObject): if not isinstance(obj, FlatCAMGerber) and not isinstance(obj, FlatCAMExcellon): return "ERROR: Only Gerber and Excellon objects can be mirrored." - # Axis try: axis = kwa['axis'].upper() except KeyError: return "ERROR: Specify -axis X or -axis Y" - # Box if 'box' in kwa: try: @@ -2302,20 +2329,23 @@ class App(QtCore.QObject): else: gridoffsety=kwa['gridoffsety'] - # Tools tools = {"1": {"C": kwa['dia']}} def aligndrillgrid_init_me(init_obj, app_obj): drills = [] currenty=0 + for row in range(kwa['rows']): currentx=0 + for col in range(kwa['columns']): - point = Point(currentx+gridoffsetx,currenty+gridoffsety) + point = Point(currentx + gridoffsetx, currenty + gridoffsety) drills.append({"point": point, "tool": "1"}) - currentx=currentx+kwa['gridx'] - currenty=currenty+kwa['gridy'] + currentx = currentx + kwa['gridx'] + + currenty = currenty + kwa['gridy'] + init_obj.tools = tools init_obj.drills = drills init_obj.create_geometry() @@ -2389,23 +2419,32 @@ class App(QtCore.QObject): else: axisoffset=0 - #this will align hole to given aligngridoffset and minimal offset from pcb, based on selected axis + # This will align hole to given aligngridoffset and minimal offset from pcb, based on selected axis if axis == "X": - firstpoint=kwa['gridoffset'] - while (xmin-kwa['minoffset'])lastpoint: - lastpoint=lastpoint+kwa['grid'] - localHoles=(firstpoint,axisoffset),(lastpoint,axisoffset) + firstpoint = kwa['gridoffset'] + + while (xmin - kwa['minoffset']) < firstpoint: + firstpoint = firstpoint - kwa['grid'] + + lastpoint = kwa['gridoffset'] + + while (xmax + kwa['minoffset']) > lastpoint: + lastpoint = lastpoint + kwa['grid'] + + localHoles = (firstpoint, axisoffset), (lastpoint, axisoffset) + else: - firstpoint=kwa['gridoffset'] - while (ymin-kwa['minoffset'])lastpoint: + firstpoint = kwa['gridoffset'] + + while (ymin - kwa['minoffset']) < firstpoint: + firstpoint = firstpoint - kwa['grid'] + + lastpoint = kwa['gridoffset'] + + while (ymax + kwa['minoffset']) > lastpoint: lastpoint=lastpoint+kwa['grid'] - localHoles=(axisoffset,firstpoint),(axisoffset,lastpoint) + + localHoles = (axisoffset, firstpoint), (axisoffset, lastpoint) for hole in localHoles: point = Point(hole) @@ -2455,8 +2494,8 @@ class App(QtCore.QObject): return 'Ok' def drillcncjob(name=None, *args): - #name should not be none, but we set it to default, because TCL return error without reason if argument is missing - #we should check it inside shell commamnd instead + # name should not be none, but we set it to default, because TCL return error without reason if argument is missing + # we should check it inside shell commamnd instead a, kwa = h(*args) types = {'tools': str, 'outname': str, From 4f2f989bdfdc96ac2d08c0b1766b2074d9615671 Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Mon, 7 Mar 2016 11:05:42 +0100 Subject: [PATCH 058/134] set rules for TCL shell commands implement TCL shell rules for: drillcncjob, millholes(renamed from drillcncjobgeometry), exteriors, interiors, isolate --- FlatCAMApp.py | 180 +++++++++++++++++++++++++++++++++++--------------- 1 file changed, 127 insertions(+), 53 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index a5d37f1..d1fcf45 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -2494,8 +2494,12 @@ class App(QtCore.QObject): return 'Ok' def drillcncjob(name=None, *args): - # name should not be none, but we set it to default, because TCL return error without reason if argument is missing - # we should check it inside shell commamnd instead + ''' + TCL shell command - see help section + :param name: name of object + :param args: array of arguments + :return: "Ok" if completed without errors + ''' a, kwa = h(*args) types = {'tools': str, 'outname': str, @@ -2512,7 +2516,10 @@ class App(QtCore.QObject): for key in kwa: if key not in types: self.raiseTclError('Unknown parameter: %s' % key) - kwa[key] = types[key](kwa[key]) + try: + kwa[key] = types[key](kwa[key]) + except Exception, e: + self.raiseTclError("Cannot cast argument '%s' to type %s." % (key, str(types[key]))) try: obj = self.collection.get_by_name(str(name)) @@ -2523,27 +2530,21 @@ class App(QtCore.QObject): self.raiseTclError('Object not found: %s' % name) if not isinstance(obj, FlatCAMExcellon): - self.raiseTclError('Only Excellon objects can be drilled: %s' % name) + self.raiseTclError('Only Excellon objects can be drilled, got %s %s.' % (name, type(obj))) try: - # Get the tools from the list job_name = kwa["outname"] # Object initialization function for app.new_object() def job_init(job_obj, app_obj): - assert isinstance(job_obj, FlatCAMCNCjob), \ - "Initializer expected FlatCAMCNCjob, got %s" % type(job_obj) - job_obj.z_cut = kwa["drillz"] job_obj.z_move = kwa["travelz"] job_obj.feedrate = kwa["feedrate"] job_obj.spindlespeed = kwa["spindlespeed"] if "spindlespeed" in kwa else None toolchange = True if "toolchange" in kwa and kwa["toolchange"] == 1 else False job_obj.generate_from_excellon_by_tool(obj, kwa["tools"], toolchange) - job_obj.gcode_parse() - job_obj.create_geometry() obj.app.new_object("cncjob", job_name, job_init) @@ -2553,65 +2554,87 @@ class App(QtCore.QObject): return 'Ok' - def drillmillgeometry(name, *args): + def millholes(name=None, *args): + ''' + TCL shell command - see help section + :param name: name of object + :param args: array of arguments + :return: "Ok" if completed without errors + ''' a, kwa = h(*args) types = {'tooldia': float, 'tools': str, 'outname': str} + if name is None: + self.raiseTclError('Argument name is missing.') + for key in kwa: if key not in types: - return 'Unknown parameter: %s' % key - kwa[key] = types[key](kwa[key]) + self.raiseTclError('Unknown parameter: %s' % key) + try: + kwa[key] = types[key](kwa[key]) + except Exception, e: + self.raiseTclError("Cannot cast argument '%s' to type %s." % (key, types[key])) try: if 'tools' in kwa: kwa['tools'] = [x.strip() for x in kwa['tools'].split(",")] except Exception as e: - return "Bad tools: %s" % str(e) + self.raiseTclError("Bad tools: %s" % str(e)) try: obj = self.collection.get_by_name(str(name)) except: - return "Could not retrieve object: %s" % name + self.raiseTclError("Could not retrieve object: %s" % name) if obj is None: - return "Object not found: %s" % name + self.raiseTclError("Object not found: %s" % name) - assert isinstance(obj, FlatCAMExcellon), \ - "Expected a FlatCAMExcellon object, got %s" % type(obj) + if not isinstance(obj, FlatCAMExcellon): + self.raiseTclError('Only Excellon objects can be mill drilled, got %s %s.' % (name, type(obj))) try: success, msg = obj.generate_milling(**kwa) except Exception as e: - return "Operation failed: %s" % str(e) + self.raiseTclError("Operation failed: %s" % str(e)) if not success: - return msg + self.raiseTclError(msg) return 'Ok' - def exteriors(obj_name, *args): + def exteriors(name=None, *args): + ''' + TCL shell command - see help section + :param name: name of object + :param args: array of arguments + :return: "Ok" if completed without errors + ''' a, kwa = h(*args) types = {'outname': str} + if name is None: + self.raiseTclError('Argument name is missing.') + for key in kwa: if key not in types: - return 'Unknown parameter: %s' % key - kwa[key] = types[key](kwa[key]) + self.raiseTclError('Unknown parameter: %s' % key) + try: + kwa[key] = types[key](kwa[key]) + except Exception, e: + self.raiseTclError("Cannot cast argument '%s' to type %s." % (key, types[key])) try: - obj = self.collection.get_by_name(str(obj_name)) + obj = self.collection.get_by_name(str(name)) except: - return "Could not retrieve object: %s" % obj_name + self.raiseTclError("Could not retrieve object: %s" % name) if obj is None: - return "Object not found: %s" % obj_name + self.raiseTclError("Object not found: %s" % name) - assert isinstance(obj, Geometry), \ - "Expected a Geometry, got %s" % type(obj) - - obj_exteriors = obj.get_exteriors() + if not isinstance(obj, Geometry): + self.raiseTclError('Expected Geometry, got %s %s.' % (name, type(obj))) def geo_init(geo_obj, app_obj): geo_obj.solid_geometry = obj_exteriors @@ -2619,36 +2642,47 @@ class App(QtCore.QObject): if 'outname' in kwa: outname = kwa['outname'] else: - outname = obj_name + ".exteriors" + outname = name + ".exteriors" try: + obj_exteriors = obj.get_exteriors() self.new_object('geometry', outname, geo_init) except Exception as e: - return "Failed: %s" % str(e) + self.raiseTclError("Failed: %s" % str(e)) return 'Ok' - def interiors(obj_name, *args): + def interiors(name=None, *args): + ''' + TCL shell command - see help section + :param name: name of object + :param args: array of arguments + :return: "Ok" if completed without errors + ''' a, kwa = h(*args) - types = {} + types = {'outname': str} for key in kwa: if key not in types: - return 'Unknown parameter: %s' % key - kwa[key] = types[key](kwa[key]) + self.raiseTclError('Unknown parameter: %s' % key) + try: + kwa[key] = types[key](kwa[key]) + except Exception, e: + self.raiseTclError("Cannot cast argument '%s' to type %s." % (key, types[key])) + + if name is None: + self.raiseTclError('Argument name is missing.') try: - obj = self.collection.get_by_name(str(obj_name)) + obj = self.collection.get_by_name(str(name)) except: - return "Could not retrieve object: %s" % obj_name + self.raiseTclError("Could not retrieve object: %s" % name) if obj is None: - return "Object not found: %s" % obj_name + self.raiseTclError("Object not found: %s" % name) - assert isinstance(obj, Geometry), \ - "Expected a Geometry, got %s" % type(obj) - - obj_interiors = obj.get_interiors() + if not isinstance(obj, Geometry): + self.raiseTclError('Expected Geometry, got %s %s.' % (name, type(obj))) def geo_init(geo_obj, app_obj): geo_obj.solid_geometry = obj_interiors @@ -2656,16 +2690,23 @@ class App(QtCore.QObject): if 'outname' in kwa: outname = kwa['outname'] else: - outname = obj_name + ".interiors" + outname = name + ".interiors" try: + obj_interiors = obj.get_interiors() self.new_object('geometry', outname, geo_init) except Exception as e: - return "Failed: %s" % str(e) + self.raiseTclError("Failed: %s" % str(e)) return 'Ok' - def isolate(name, *args): + def isolate(name=None, *args): + ''' + TCL shell command - see help section + :param name: name of object + :param args: array of arguments + :return: "Ok" if completed without errors + ''' a, kwa = h(*args) types = {'dia': float, 'passes': int, @@ -2675,24 +2716,29 @@ class App(QtCore.QObject): for key in kwa: if key not in types: - return 'Unknown parameter: %s' % key - kwa[key] = types[key](kwa[key]) - + self.raiseTclError('Unknown parameter: %s' % key) + try: + kwa[key] = types[key](kwa[key]) + except Exception, e: + self.raiseTclError("Cannot cast argument '%s' to type %s." % (key, types[key])) try: obj = self.collection.get_by_name(str(name)) except: - return "Could not retrieve object: %s" % name + self.raiseTclError("Could not retrieve object: %s" % name) if obj is None: - return "Object not found: %s" % name + self.raiseTclError("Object not found: %s" % name) assert isinstance(obj, FlatCAMGerber), \ "Expected a FlatCAMGerber, got %s" % type(obj) + if not isinstance(obj, FlatCAMGerber): + self.raiseTclError('Expected FlatCAMGerber, got %s %s.' % (name, type(obj))) + try: obj.isolate(**kwa) except Exception, e: - return "Operation failed: %s" % str(e) + self.raiseTclError("Operation failed: %s" % str(e)) return 'Ok' @@ -3071,6 +3117,34 @@ class App(QtCore.QObject): return "ERROR: No such system parameter." + ''' + Howto implement TCL shell commands: + + All parameters passed to command should be possible to set as None and test it afterwards. + This is because we need to see error caused in tcl, + if None value as default parameter is not allowed TCL will return empty error. + Use: + def mycommand(name=None,...): + + Test it like this: + if name is None: + + self.raiseTclError('Argument name is missing.') + + When error ocurre, always use raiseTclError, never return "sometext" on error, + otherwise we will miss it and processing will silently continue. + Method raiseTclError pass error into TCL interpreter, then raise python exception, + which is catched in exec_command and displayed in TCL shell console with red background. + Error in console is displayed with TCL trace. + + This behavior works only within main thread, + errors with promissed tasks can be catched and detected only with log. + TODO: this problem have to be addressed somehow, maybe rewrite promissing to be blocking somehow for TCL shell. + + Kamil's comment: I will rewrite existing TCL commands from time to time to follow this rules. + + ''' + commands = { 'help': { 'fcn': shelp, @@ -3248,7 +3322,7 @@ class App(QtCore.QObject): " toolchange: Enable tool changes (example: 1)\n" }, 'millholes': { - 'fcn': drillmillgeometry, + 'fcn': millholes, 'help': "Create Geometry Object for milling holes from Excellon.\n" + "> millholes -tools -tooldia -outname \n" + " name: Name of the Excellon Object\n" + From 6b527fa256f9059383caa5ce48ff6e4bf247a1ea Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Thu, 10 Mar 2016 16:01:50 +0100 Subject: [PATCH 059/134] example howto handle Exceptions in shell --- FlatCAMApp.py | 86 ++++++++++++++++++++++++++++++++------------------- 1 file changed, 55 insertions(+), 31 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index d1fcf45..63e8fb7 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -644,14 +644,32 @@ class App(QtCore.QObject): else: self.defaults['stats'][resource] = 1 + class TclErrorException(Exception): + """ + this exception is deffined here, to be able catch it if we sucessfully handle all errors from shell command + """ + + pass + + def raiseTclUnknownError(self, unknownException): + """ + raise Exception if is different type than TclErrorException + :param unknownException: + :return: + """ + + if not isinstance(unknownException, self.TclErrorException): + self.raiseTclError("Unknown error: %s" % str(unknownException)) + def raiseTclError(self, text): """ this method pass exception from python into TCL as error, so we get stacktrace and reason :param text: text of error :return: raise exception """ + self.tcl.eval('return -code error "%s"' % text) - raise Exception(text) + raise self.TclErrorException(text) def exec_command(self, text): """ @@ -660,6 +678,7 @@ class App(QtCore.QObject): :param text: Input command :return: None """ + self.report_usage('exec_command') text = str(text) @@ -2659,44 +2678,49 @@ class App(QtCore.QObject): :param args: array of arguments :return: "Ok" if completed without errors ''' - a, kwa = h(*args) - types = {'outname': str} - for key in kwa: - if key not in types: - self.raiseTclError('Unknown parameter: %s' % key) + try: + a, kwa = h(*args) + types = {'outname': str} + + for key in kwa: + if key not in types: + self.raiseTclError('Unknown parameter: %s' % key) + try: + kwa[key] = types[key](kwa[key]) + except Exception, e: + self.raiseTclError("Cannot cast argument '%s' to type %s." % (key, types[key])) + + if name is None: + self.raiseTclError('Argument name is missing.') + try: - kwa[key] = types[key](kwa[key]) - except Exception, e: - self.raiseTclError("Cannot cast argument '%s' to type %s." % (key, types[key])) + obj = self.collection.get_by_name(str(name)) + except: + self.raiseTclError("Could not retrieve object: %s" % name) - if name is None: - self.raiseTclError('Argument name is missing.') + if obj is None: + self.raiseTclError("Object not found: %s" % name) - try: - obj = self.collection.get_by_name(str(name)) - except: - self.raiseTclError("Could not retrieve object: %s" % name) + if not isinstance(obj, Geometry): + self.raiseTclError('Expected Geometry, got %s %s.' % (name, type(obj))) - if obj is None: - self.raiseTclError("Object not found: %s" % name) + def geo_init(geo_obj, app_obj): + geo_obj.solid_geometry = obj_interiors - if not isinstance(obj, Geometry): - self.raiseTclError('Expected Geometry, got %s %s.' % (name, type(obj))) + if 'outname' in kwa: + outname = kwa['outname'] + else: + outname = name + ".interiors" - def geo_init(geo_obj, app_obj): - geo_obj.solid_geometry = obj_interiors + try: + obj_interiors = obj.get_interiors() + self.new_object('geometry', outname, geo_init) + except Exception as e: + self.raiseTclError("Failed: %s" % str(e)) - if 'outname' in kwa: - outname = kwa['outname'] - else: - outname = name + ".interiors" - - try: - obj_interiors = obj.get_interiors() - self.new_object('geometry', outname, geo_init) - except Exception as e: - self.raiseTclError("Failed: %s" % str(e)) + except Exception as unknown: + self.raiseTclUnknownError(unknown) return 'Ok' From 3bb2cfbc22b0ed46961d48bd3db2b8826a4cb4eb Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Thu, 10 Mar 2016 19:01:07 +0100 Subject: [PATCH 060/134] fix gcode verification in tests --- tests/test_gerber_flow.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/test_gerber_flow.py b/tests/test_gerber_flow.py index ac51b1c..34a2a5f 100644 --- a/tests/test_gerber_flow.py +++ b/tests/test_gerber_flow.py @@ -6,7 +6,7 @@ from FlatCAMObj import FlatCAMGerber, FlatCAMGeometry, FlatCAMCNCjob from ObjectUI import GerberObjectUI, GeometryObjectUI from time import sleep import os - +import tempfile class GerberFlowTestCase(unittest.TestCase): @@ -128,7 +128,10 @@ class GerberFlowTestCase(unittest.TestCase): # Export G-Code, check output #----------------------------------------- assert isinstance(cnc_obj, FlatCAMCNCjob) - output_filename = "tests/tmp/" + cnc_name + ".gcode" + output_filename = "" + #get system temporary file(try create it and delete also) + with tempfile.NamedTemporaryFile(prefix="unittest.",suffix="."+cnc_name+".gcode",delete=True) as tmpfile: + output_filename = tmpfile.name cnc_obj.export_gcode(output_filename) self.assertTrue(os.path.isfile(output_filename)) os.remove(output_filename) From f645dba041785de038c5a3048072b7912d3e1649 Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Thu, 10 Mar 2016 21:45:47 +0100 Subject: [PATCH 061/134] update formating --- tests/test_gerber_flow.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_gerber_flow.py b/tests/test_gerber_flow.py index 34a2a5f..9eac0ef 100644 --- a/tests/test_gerber_flow.py +++ b/tests/test_gerber_flow.py @@ -129,9 +129,9 @@ class GerberFlowTestCase(unittest.TestCase): #----------------------------------------- assert isinstance(cnc_obj, FlatCAMCNCjob) output_filename = "" - #get system temporary file(try create it and delete also) - with tempfile.NamedTemporaryFile(prefix="unittest.",suffix="."+cnc_name+".gcode",delete=True) as tmpfile: - output_filename = tmpfile.name + # get system temporary file(try create it and delete also) + with tempfile.NamedTemporaryFile(prefix='unittest.', suffix="." + cnc_name + '.gcode', delete=True) as tmp_file: + output_filename = tmp_file.name cnc_obj.export_gcode(output_filename) self.assertTrue(os.path.isfile(output_filename)) os.remove(output_filename) From fd1c8afef9bb0a9953962ad33a8663acee1e9408 Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Fri, 11 Mar 2016 01:50:12 +0100 Subject: [PATCH 062/134] implement basic set of tests for tcl_shell, need to be completed --- FlatCAMApp.py | 412 ++-- tests/gerber_files/detector_contour.gbr | 26 + tests/gerber_files/detector_copper_bottom.gbr | 2146 +++++++++++++++++ tests/gerber_files/detector_copper_top.gbr | 71 + tests/gerber_files/detector_drill.txt | 46 + tests/test_tcl_shell.py | 155 ++ 6 files changed, 2666 insertions(+), 190 deletions(-) create mode 100644 tests/gerber_files/detector_contour.gbr create mode 100644 tests/gerber_files/detector_copper_bottom.gbr create mode 100644 tests/gerber_files/detector_copper_top.gbr create mode 100644 tests/gerber_files/detector_drill.txt create mode 100644 tests/test_tcl_shell.py diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 63e8fb7..7729cb6 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -675,8 +675,19 @@ class App(QtCore.QObject): """ Handles input from the shell. See FlatCAMApp.setup_shell for shell commands. + :param text: + :return: output if there was any + """ + + return self.exec_command_test(self, text, False) + + def exec_command_test(self, text, reraise=True): + """ + Handles input from the shell. See FlatCAMApp.setup_shell for shell commands. + :param text: Input command - :return: None + :param reraise: raise exception and not hide it, used mainly in unittests + :return: output if there was any """ self.report_usage('exec_command') @@ -691,8 +702,10 @@ class App(QtCore.QObject): result = self.tcl.eval("set errorInfo") self.log.error("Exec command Exception: %s" % (result + '\n')) self.shell.append_error('ERROR: ' + result + '\n') - #show error in console and just return - return + #show error in console and just return or in test raise exception + if reraise: + raise e + return result """ Code below is unsused. Saved for later. @@ -2167,85 +2180,96 @@ class App(QtCore.QObject): return 'Ok' - def geocutout(name, *args): - """ + def geocutout(name=None, *args): + ''' + TCL shell command - see help section + Subtract gaps from geometry, this will not create new object - :param name: - :param args: - :return: - """ - a, kwa = h(*args) - types = {'dia': float, - 'gapsize': float, - 'gaps': str} - - # How gaps wil be rendered: - # lr - left + right - # tb - top + bottom - # 4 - left + right +top + bottom - # 2lr - 2*left + 2*right - # 2tb - 2*top + 2*bottom - # 8 - 2*left + 2*right +2*top + 2*bottom - - for key in kwa: - if key not in types: - return 'Unknown parameter: %s' % key - kwa[key] = types[key](kwa[key]) + :param name: name of object + :param args: array of arguments + :return: "Ok" if completed without errors + ''' try: - obj = self.collection.get_by_name(str(name)) - except: - return "Could not retrieve object: %s" % name + a, kwa = h(*args) + types = {'dia': float, + 'gapsize': float, + 'gaps': str} - # Get min and max data for each object as we just cut rectangles across X or Y - xmin, ymin, xmax, ymax = obj.bounds() - px = 0.5 * (xmin + xmax) - py = 0.5 * (ymin + ymax) - lenghtx = (xmax - xmin) - lenghty = (ymax - ymin) - gapsize = kwa['gapsize'] + kwa['dia'] / 2 - - if kwa['gaps'] == '8' or kwa['gaps']=='2lr': - - subtract_rectangle(name, - xmin - gapsize, - py - gapsize + lenghty / 4, - xmax + gapsize, - py + gapsize + lenghty / 4) - subtract_rectangle(name, - xmin-gapsize, - py - gapsize - lenghty / 4, - xmax + gapsize, - py + gapsize - lenghty / 4) - - if kwa['gaps'] == '8' or kwa['gaps']=='2tb': - subtract_rectangle(name, - px - gapsize + lenghtx / 4, - ymin-gapsize, - px + gapsize + lenghtx / 4, - ymax + gapsize) - subtract_rectangle(name, - px - gapsize - lenghtx / 4, - ymin - gapsize, - px + gapsize - lenghtx / 4, - ymax + gapsize) - - if kwa['gaps'] == '4' or kwa['gaps']=='lr': - subtract_rectangle(name, - xmin - gapsize, - py - gapsize, - xmax + gapsize, - py + gapsize) - - if kwa['gaps'] == '4' or kwa['gaps']=='tb': - subtract_rectangle(name, - px - gapsize, - ymin - gapsize, - px + gapsize, - ymax + gapsize) - - return 'Ok' + # How gaps wil be rendered: + # lr - left + right + # tb - top + bottom + # 4 - left + right +top + bottom + # 2lr - 2*left + 2*right + # 2tb - 2*top + 2*bottom + # 8 - 2*left + 2*right +2*top + 2*bottom + + if name is None: + self.raiseTclError('Argument name is missing.') + + for key in kwa: + if key not in types: + self.raiseTclError('Unknown parameter: %s' % key) + try: + kwa[key] = types[key](kwa[key]) + except Exception, e: + self.raiseTclError("Cannot cast argument '%s' to type %s." % (key, str(types[key]))) + + try: + obj = self.collection.get_by_name(str(name)) + except: + self.raiseTclError("Could not retrieve object: %s" % name) + + # Get min and max data for each object as we just cut rectangles across X or Y + xmin, ymin, xmax, ymax = obj.bounds() + px = 0.5 * (xmin + xmax) + py = 0.5 * (ymin + ymax) + lenghtx = (xmax - xmin) + lenghty = (ymax - ymin) + gapsize = kwa['gapsize'] + kwa['dia'] / 2 + + if kwa['gaps'] == '8' or kwa['gaps']=='2lr': + + subtract_rectangle(name, + xmin - gapsize, + py - gapsize + lenghty / 4, + xmax + gapsize, + py + gapsize + lenghty / 4) + subtract_rectangle(name, + xmin-gapsize, + py - gapsize - lenghty / 4, + xmax + gapsize, + py + gapsize - lenghty / 4) + + if kwa['gaps'] == '8' or kwa['gaps']=='2tb': + subtract_rectangle(name, + px - gapsize + lenghtx / 4, + ymin-gapsize, + px + gapsize + lenghtx / 4, + ymax + gapsize) + subtract_rectangle(name, + px - gapsize - lenghtx / 4, + ymin - gapsize, + px + gapsize - lenghtx / 4, + ymax + gapsize) + + if kwa['gaps'] == '4' or kwa['gaps']=='lr': + subtract_rectangle(name, + xmin - gapsize, + py - gapsize, + xmax + gapsize, + py + gapsize) + + if kwa['gaps'] == '4' or kwa['gaps']=='tb': + subtract_rectangle(name, + px - gapsize, + ymin - gapsize, + px + gapsize, + ymax + gapsize) + + except Exception as unknown: + self.raiseTclUnknownError(unknown) def mirror(name, *args): a, kwa = h(*args) @@ -2519,59 +2543,63 @@ class App(QtCore.QObject): :param args: array of arguments :return: "Ok" if completed without errors ''' - a, kwa = h(*args) - types = {'tools': str, - 'outname': str, - 'drillz': float, - 'travelz': float, - 'feedrate': float, - 'spindlespeed': int, - 'toolchange': int - } - if name is None: - self.raiseTclError('Argument name is missing.') + try: + a, kwa = h(*args) + types = {'tools': str, + 'outname': str, + 'drillz': float, + 'travelz': float, + 'feedrate': float, + 'spindlespeed': int, + 'toolchange': int + } + + if name is None: + self.raiseTclError('Argument name is missing.') + + for key in kwa: + if key not in types: + self.raiseTclError('Unknown parameter: %s' % key) + try: + kwa[key] = types[key](kwa[key]) + except Exception, e: + self.raiseTclError("Cannot cast argument '%s' to type %s." % (key, str(types[key]))) - for key in kwa: - if key not in types: - self.raiseTclError('Unknown parameter: %s' % key) try: - kwa[key] = types[key](kwa[key]) + obj = self.collection.get_by_name(str(name)) + except: + self.raiseTclError("Could not retrieve object: %s" % name) + + if obj is None: + self.raiseTclError('Object not found: %s' % name) + + if not isinstance(obj, FlatCAMExcellon): + self.raiseTclError('Only Excellon objects can be drilled, got %s %s.' % (name, type(obj))) + + try: + # Get the tools from the list + job_name = kwa["outname"] + + # Object initialization function for app.new_object() + def job_init(job_obj, app_obj): + job_obj.z_cut = kwa["drillz"] + job_obj.z_move = kwa["travelz"] + job_obj.feedrate = kwa["feedrate"] + job_obj.spindlespeed = kwa["spindlespeed"] if "spindlespeed" in kwa else None + toolchange = True if "toolchange" in kwa and kwa["toolchange"] == 1 else False + job_obj.generate_from_excellon_by_tool(obj, kwa["tools"], toolchange) + job_obj.gcode_parse() + job_obj.create_geometry() + + obj.app.new_object("cncjob", job_name, job_init) + except Exception, e: - self.raiseTclError("Cannot cast argument '%s' to type %s." % (key, str(types[key]))) + self.raiseTclError("Operation failed: %s" % str(e)) - try: - obj = self.collection.get_by_name(str(name)) - except: - self.raiseTclError("Could not retrieve object: %s" % name) + except Exception as unknown: + self.raiseTclUnknownError(unknown) - if obj is None: - self.raiseTclError('Object not found: %s' % name) - - if not isinstance(obj, FlatCAMExcellon): - self.raiseTclError('Only Excellon objects can be drilled, got %s %s.' % (name, type(obj))) - - try: - # Get the tools from the list - job_name = kwa["outname"] - - # Object initialization function for app.new_object() - def job_init(job_obj, app_obj): - job_obj.z_cut = kwa["drillz"] - job_obj.z_move = kwa["travelz"] - job_obj.feedrate = kwa["feedrate"] - job_obj.spindlespeed = kwa["spindlespeed"] if "spindlespeed" in kwa else None - toolchange = True if "toolchange" in kwa and kwa["toolchange"] == 1 else False - job_obj.generate_from_excellon_by_tool(obj, kwa["tools"], toolchange) - job_obj.gcode_parse() - job_obj.create_geometry() - - obj.app.new_object("cncjob", job_name, job_init) - - except Exception, e: - self.raiseTclError("Operation failed: %s" % str(e)) - - return 'Ok' def millholes(name=None, *args): ''' @@ -2580,48 +2608,51 @@ class App(QtCore.QObject): :param args: array of arguments :return: "Ok" if completed without errors ''' - a, kwa = h(*args) - types = {'tooldia': float, - 'tools': str, - 'outname': str} - if name is None: - self.raiseTclError('Argument name is missing.') + try: + a, kwa = h(*args) + types = {'tooldia': float, + 'tools': str, + 'outname': str} + + if name is None: + self.raiseTclError('Argument name is missing.') + + for key in kwa: + if key not in types: + self.raiseTclError('Unknown parameter: %s' % key) + try: + kwa[key] = types[key](kwa[key]) + except Exception, e: + self.raiseTclError("Cannot cast argument '%s' to type %s." % (key, types[key])) - for key in kwa: - if key not in types: - self.raiseTclError('Unknown parameter: %s' % key) try: - kwa[key] = types[key](kwa[key]) - except Exception, e: - self.raiseTclError("Cannot cast argument '%s' to type %s." % (key, types[key])) + if 'tools' in kwa: + kwa['tools'] = [x.strip() for x in kwa['tools'].split(",")] + except Exception as e: + self.raiseTclError("Bad tools: %s" % str(e)) - try: - if 'tools' in kwa: - kwa['tools'] = [x.strip() for x in kwa['tools'].split(",")] - except Exception as e: - self.raiseTclError("Bad tools: %s" % str(e)) + try: + obj = self.collection.get_by_name(str(name)) + except: + self.raiseTclError("Could not retrieve object: %s" % name) - try: - obj = self.collection.get_by_name(str(name)) - except: - self.raiseTclError("Could not retrieve object: %s" % name) + if obj is None: + self.raiseTclError("Object not found: %s" % name) - if obj is None: - self.raiseTclError("Object not found: %s" % name) + if not isinstance(obj, FlatCAMExcellon): + self.raiseTclError('Only Excellon objects can be mill drilled, got %s %s.' % (name, type(obj))) - if not isinstance(obj, FlatCAMExcellon): - self.raiseTclError('Only Excellon objects can be mill drilled, got %s %s.' % (name, type(obj))) + try: + success, msg = obj.generate_milling(**kwa) + except Exception as e: + self.raiseTclError("Operation failed: %s" % str(e)) - try: - success, msg = obj.generate_milling(**kwa) - except Exception as e: - self.raiseTclError("Operation failed: %s" % str(e)) + if not success: + self.raiseTclError(msg) - if not success: - self.raiseTclError(msg) - - return 'Ok' + except Exception as unknown: + self.raiseTclUnknownError(unknown) def exteriors(name=None, *args): ''' @@ -2630,46 +2661,49 @@ class App(QtCore.QObject): :param args: array of arguments :return: "Ok" if completed without errors ''' - a, kwa = h(*args) - types = {'outname': str} - if name is None: - self.raiseTclError('Argument name is missing.') + try: + a, kwa = h(*args) + types = {'outname': str} + + if name is None: + self.raiseTclError('Argument name is missing.') + + for key in kwa: + if key not in types: + self.raiseTclError('Unknown parameter: %s' % key) + try: + kwa[key] = types[key](kwa[key]) + except Exception, e: + self.raiseTclError("Cannot cast argument '%s' to type %s." % (key, types[key])) - for key in kwa: - if key not in types: - self.raiseTclError('Unknown parameter: %s' % key) try: - kwa[key] = types[key](kwa[key]) - except Exception, e: - self.raiseTclError("Cannot cast argument '%s' to type %s." % (key, types[key])) + obj = self.collection.get_by_name(str(name)) + except: + self.raiseTclError("Could not retrieve object: %s" % name) - try: - obj = self.collection.get_by_name(str(name)) - except: - self.raiseTclError("Could not retrieve object: %s" % name) + if obj is None: + self.raiseTclError("Object not found: %s" % name) - if obj is None: - self.raiseTclError("Object not found: %s" % name) + if not isinstance(obj, Geometry): + self.raiseTclError('Expected Geometry, got %s %s.' % (name, type(obj))) - if not isinstance(obj, Geometry): - self.raiseTclError('Expected Geometry, got %s %s.' % (name, type(obj))) + def geo_init(geo_obj, app_obj): + geo_obj.solid_geometry = obj_exteriors - def geo_init(geo_obj, app_obj): - geo_obj.solid_geometry = obj_exteriors + if 'outname' in kwa: + outname = kwa['outname'] + else: + outname = name + ".exteriors" - if 'outname' in kwa: - outname = kwa['outname'] - else: - outname = name + ".exteriors" + try: + obj_exteriors = obj.get_exteriors() + self.new_object('geometry', outname, geo_init) + except Exception as e: + self.raiseTclError("Failed: %s" % str(e)) - try: - obj_exteriors = obj.get_exteriors() - self.new_object('geometry', outname, geo_init) - except Exception as e: - self.raiseTclError("Failed: %s" % str(e)) - - return 'Ok' + except Exception as unknown: + self.raiseTclUnknownError(unknown) def interiors(name=None, *args): ''' @@ -2722,8 +2756,6 @@ class App(QtCore.QObject): except Exception as unknown: self.raiseTclUnknownError(unknown) - return 'Ok' - def isolate(name=None, *args): ''' TCL shell command - see help section diff --git a/tests/gerber_files/detector_contour.gbr b/tests/gerber_files/detector_contour.gbr new file mode 100644 index 0000000..93adef0 --- /dev/null +++ b/tests/gerber_files/detector_contour.gbr @@ -0,0 +1,26 @@ +G04 MADE WITH FRITZING* +G04 WWW.FRITZING.ORG* +G04 DOUBLE SIDED* +G04 HOLES PLATED* +G04 CONTOUR ON CENTER OF CONTOUR VECTOR* +%ASAXBY*% +%FSLAX23Y23*% +%MOIN*% +%OFA0B0*% +%SFA1.0B1.0*% +%ADD10R,1.771650X1.181100*% +%ADD11C,0.008000*% +%ADD10C,0.008*% +%LNCONTOUR*% +G90* +G70* +G54D10* +G54D11* +X4Y1177D02* +X1768Y1177D01* +X1768Y4D01* +X4Y4D01* +X4Y1177D01* +D02* +G04 End of contour* +M02* \ No newline at end of file diff --git a/tests/gerber_files/detector_copper_bottom.gbr b/tests/gerber_files/detector_copper_bottom.gbr new file mode 100644 index 0000000..d3bca48 --- /dev/null +++ b/tests/gerber_files/detector_copper_bottom.gbr @@ -0,0 +1,2146 @@ +G04 MADE WITH FRITZING* +G04 WWW.FRITZING.ORG* +G04 DOUBLE SIDED* +G04 HOLES PLATED* +G04 CONTOUR ON CENTER OF CONTOUR VECTOR* +%ASAXBY*% +%FSLAX23Y23*% +%MOIN*% +%OFA0B0*% +%SFA1.0B1.0*% +%ADD10C,0.075000*% +%ADD11C,0.099055*% +%ADD12C,0.078740*% +%ADD13R,0.075000X0.075000*% +%ADD14C,0.048000*% +%ADD15C,0.020000*% +%ADD16R,0.001000X0.001000*% +%LNCOPPER0*% +G90* +G70* +G54D10* +X1149Y872D03* +X1349Y872D03* +X749Y722D03* +X749Y522D03* +X1149Y522D03* +X1449Y522D03* +X1149Y422D03* +X1449Y422D03* +X1149Y322D03* +X1449Y322D03* +X1149Y222D03* +X1449Y222D03* +X949Y472D03* +X949Y72D03* +G54D11* +X749Y972D03* +X599Y972D03* +X349Y322D03* +X349Y472D03* +X349Y672D03* +X349Y822D03* +G54D10* +X699Y122D03* +X699Y322D03* +G54D12* +X699Y222D03* +X949Y972D03* +X749Y622D03* +X1049Y222D03* +X1249Y872D03* +G54D13* +X1149Y872D03* +X1149Y522D03* +G54D14* +X949Y373D02* +X949Y433D01* +D02* +X999Y323D02* +X949Y373D01* +D02* +X1109Y322D02* +X999Y323D01* +D02* +X499Y873D02* +X1109Y872D01* +D02* +X1299Y73D02* +X989Y72D01* +D02* +X1399Y322D02* +X1349Y272D01* +D02* +X1349Y272D02* +X1349Y122D01* +D02* +X1349Y122D02* +X1299Y73D01* +D02* +X1409Y322D02* +X1399Y322D01* +D02* +X909Y72D02* +X749Y73D01* +D02* +X749Y73D02* +X727Y94D01* +D02* +X649Y522D02* +X709Y522D01* +D02* +X599Y473D02* +X649Y522D01* +D02* +X401Y472D02* +X599Y473D01* +D02* +X789Y522D02* +X899Y522D01* +D02* +X709Y722D02* +X599Y722D01* +D02* +X599Y722D02* +X549Y673D01* +D02* +X549Y673D02* +X401Y672D01* +D02* +X1149Y562D02* +X1149Y833D01* +D02* +X499Y972D02* +X499Y873D01* +D02* +X547Y972D02* +X499Y972D01* +D02* +X699Y283D02* +X699Y260D01* +D02* +X749Y562D02* +X749Y584D01* +D02* +X499Y873D02* +X499Y972D01* +D02* +X499Y972D02* +X547Y972D01* +D02* +X401Y823D02* +X449Y823D01* +D02* +X899Y522D02* +X921Y500D01* +D02* +X1309Y872D02* +X1287Y872D01* +D02* +X449Y823D02* +X499Y873D01* +D02* +X1349Y422D02* +X1349Y833D01* +D02* +X1189Y422D02* +X1349Y422D01* +D02* +X1399Y322D02* +X1409Y322D01* +D02* +X1349Y372D02* +X1399Y322D01* +D02* +X1349Y422D02* +X1349Y372D01* +D02* +X1189Y422D02* +X1349Y422D01* +D02* +X801Y972D02* +X911Y972D01* +D02* +X1109Y222D02* +X1087Y222D01* +D02* +X401Y322D02* +X659Y322D01* +D02* +X1399Y972D02* +X987Y972D01* +D02* +X1449Y923D02* +X1399Y972D01* +D02* +X1449Y562D02* +X1449Y923D01* +G54D15* +X776Y695D02* +X721Y695D01* +X721Y750D01* +X776Y750D01* +X776Y695D01* +D02* +X671Y150D02* +X726Y150D01* +X726Y95D01* +X671Y95D01* +X671Y150D01* +D02* +G54D16* +X766Y1112D02* +X769Y1112D01* +X764Y1111D02* +X771Y1111D01* +X763Y1110D02* +X772Y1110D01* +X762Y1109D02* +X772Y1109D01* +X762Y1108D02* +X773Y1108D01* +X762Y1107D02* +X773Y1107D01* +X762Y1106D02* +X773Y1106D01* +X762Y1105D02* +X773Y1105D01* +X762Y1104D02* +X773Y1104D01* +X762Y1103D02* +X773Y1103D01* +X762Y1102D02* +X773Y1102D01* +X762Y1101D02* +X773Y1101D01* +X762Y1100D02* +X773Y1100D01* +X762Y1099D02* +X773Y1099D01* +X762Y1098D02* +X773Y1098D01* +X762Y1097D02* +X773Y1097D01* +X762Y1096D02* +X773Y1096D01* +X762Y1095D02* +X773Y1095D01* +X762Y1094D02* +X773Y1094D01* +X762Y1093D02* +X773Y1093D01* +X762Y1092D02* +X773Y1092D01* +X762Y1091D02* +X773Y1091D01* +X762Y1090D02* +X773Y1090D01* +X762Y1089D02* +X773Y1089D01* +X566Y1088D02* +X618Y1088D01* +X741Y1088D02* +X793Y1088D01* +X565Y1087D02* +X620Y1087D01* +X740Y1087D02* +X795Y1087D01* +X564Y1086D02* +X621Y1086D01* +X739Y1086D02* +X796Y1086D01* +X563Y1085D02* +X621Y1085D01* +X738Y1085D02* +X796Y1085D01* +X563Y1084D02* +X622Y1084D01* +X738Y1084D02* +X796Y1084D01* +X563Y1083D02* +X622Y1083D01* +X738Y1083D02* +X796Y1083D01* +X563Y1082D02* +X622Y1082D01* +X738Y1082D02* +X796Y1082D01* +X563Y1081D02* +X622Y1081D01* +X738Y1081D02* +X796Y1081D01* +X563Y1080D02* +X622Y1080D01* +X738Y1080D02* +X796Y1080D01* +X563Y1079D02* +X622Y1079D01* +X739Y1079D02* +X795Y1079D01* +X563Y1078D02* +X622Y1078D01* +X739Y1078D02* +X795Y1078D01* +X563Y1077D02* +X622Y1077D01* +X741Y1077D02* +X794Y1077D01* +X563Y1076D02* +X622Y1076D01* +X762Y1076D02* +X773Y1076D01* +X563Y1075D02* +X621Y1075D01* +X762Y1075D02* +X773Y1075D01* +X563Y1074D02* +X621Y1074D01* +X762Y1074D02* +X773Y1074D01* +X564Y1073D02* +X620Y1073D01* +X762Y1073D02* +X773Y1073D01* +X565Y1072D02* +X619Y1072D01* +X762Y1072D02* +X773Y1072D01* +X569Y1071D02* +X615Y1071D01* +X762Y1071D02* +X773Y1071D01* +X762Y1070D02* +X773Y1070D01* +X762Y1069D02* +X773Y1069D01* +X762Y1068D02* +X773Y1068D01* +X762Y1067D02* +X773Y1067D01* +X762Y1066D02* +X773Y1066D01* +X762Y1065D02* +X773Y1065D01* +X762Y1064D02* +X773Y1064D01* +X762Y1063D02* +X773Y1063D01* +X762Y1062D02* +X773Y1062D01* +X762Y1061D02* +X773Y1061D01* +X762Y1060D02* +X773Y1060D01* +X762Y1059D02* +X773Y1059D01* +X762Y1058D02* +X773Y1058D01* +X762Y1057D02* +X773Y1057D01* +X762Y1056D02* +X773Y1056D01* +X763Y1055D02* +X772Y1055D01* +X763Y1054D02* +X771Y1054D01* +X765Y1053D02* +X770Y1053D01* +X1661Y878D02* +X1697Y878D01* +X1658Y877D02* +X1698Y877D01* +X1656Y876D02* +X1700Y876D01* +X1653Y875D02* +X1701Y875D01* +X1651Y874D02* +X1701Y874D01* +X1648Y873D02* +X1702Y873D01* +X1645Y872D02* +X1702Y872D01* +X1643Y871D02* +X1702Y871D01* +X1640Y870D02* +X1702Y870D01* +X1638Y869D02* +X1703Y869D01* +X1635Y868D02* +X1702Y868D01* +X1633Y867D02* +X1702Y867D01* +X1630Y866D02* +X1702Y866D01* +X1627Y865D02* +X1701Y865D01* +X1625Y864D02* +X1701Y864D01* +X1622Y863D02* +X1700Y863D01* +X1620Y862D02* +X1699Y862D01* +X1617Y861D02* +X1697Y861D01* +X1615Y860D02* +X1664Y860D01* +X1612Y859D02* +X1661Y859D01* +X1609Y858D02* +X1659Y858D01* +X1607Y857D02* +X1656Y857D01* +X1604Y856D02* +X1653Y856D01* +X1602Y855D02* +X1651Y855D01* +X1599Y854D02* +X1648Y854D01* +X1597Y853D02* +X1646Y853D01* +X1594Y852D02* +X1643Y852D01* +X1592Y851D02* +X1641Y851D01* +X1589Y850D02* +X1638Y850D01* +X1586Y849D02* +X1635Y849D01* +X1584Y848D02* +X1633Y848D01* +X1581Y847D02* +X1630Y847D01* +X1579Y846D02* +X1628Y846D01* +X1576Y845D02* +X1625Y845D01* +X1574Y844D02* +X1623Y844D01* +X1571Y843D02* +X1620Y843D01* +X1569Y842D02* +X1618Y842D01* +X1567Y841D02* +X1615Y841D01* +X1566Y840D02* +X1612Y840D01* +X1565Y839D02* +X1610Y839D01* +X1564Y838D02* +X1607Y838D01* +X1564Y837D02* +X1605Y837D01* +X1563Y836D02* +X1602Y836D01* +X1563Y835D02* +X1600Y835D01* +X1563Y834D02* +X1597Y834D01* +X1563Y833D02* +X1599Y833D01* +X1563Y832D02* +X1601Y832D01* +X1564Y831D02* +X1604Y831D01* +X1564Y830D02* +X1606Y830D01* +X1564Y829D02* +X1609Y829D01* +X1565Y828D02* +X1611Y828D01* +X1566Y827D02* +X1614Y827D01* +X1567Y826D02* +X1616Y826D01* +X1569Y825D02* +X1619Y825D01* +X1572Y824D02* +X1622Y824D01* +X1574Y823D02* +X1624Y823D01* +X1577Y822D02* +X1627Y822D01* +X1580Y821D02* +X1629Y821D01* +X1582Y820D02* +X1632Y820D01* +X1585Y819D02* +X1634Y819D01* +X1587Y818D02* +X1637Y818D01* +X1590Y817D02* +X1639Y817D01* +X1592Y816D02* +X1642Y816D01* +X1595Y815D02* +X1645Y815D01* +X1598Y814D02* +X1647Y814D01* +X1600Y813D02* +X1650Y813D01* +X1603Y812D02* +X1652Y812D01* +X1605Y811D02* +X1655Y811D01* +X1608Y810D02* +X1657Y810D01* +X1610Y809D02* +X1660Y809D01* +X1613Y808D02* +X1662Y808D01* +X1616Y807D02* +X1695Y807D01* +X1618Y806D02* +X1698Y806D01* +X1621Y805D02* +X1699Y805D01* +X1623Y804D02* +X1700Y804D01* +X1626Y803D02* +X1701Y803D01* +X1628Y802D02* +X1702Y802D01* +X1631Y801D02* +X1702Y801D01* +X1634Y800D02* +X1702Y800D01* +X1636Y799D02* +X1702Y799D01* +X1639Y798D02* +X1703Y798D01* +X1641Y797D02* +X1702Y797D01* +X1644Y796D02* +X1702Y796D01* +X1646Y795D02* +X1702Y795D01* +X1649Y794D02* +X1702Y794D01* +X1652Y793D02* +X1701Y793D01* +X1654Y792D02* +X1700Y792D01* +X1657Y791D02* +X1699Y791D01* +X1659Y790D02* +X1698Y790D01* +X1662Y789D02* +X1694Y789D01* +X191Y786D02* +X194Y786D01* +X106Y785D02* +X117Y785D01* +X189Y785D02* +X198Y785D01* +X104Y784D02* +X119Y784D01* +X187Y784D02* +X200Y784D01* +X102Y783D02* +X121Y783D01* +X186Y783D02* +X202Y783D01* +X101Y782D02* +X122Y782D01* +X186Y782D02* +X204Y782D01* +X100Y781D02* +X123Y781D01* +X185Y781D02* +X205Y781D01* +X99Y780D02* +X125Y780D01* +X185Y780D02* +X206Y780D01* +X98Y779D02* +X126Y779D01* +X185Y779D02* +X207Y779D01* +X97Y778D02* +X127Y778D01* +X185Y778D02* +X208Y778D01* +X97Y777D02* +X128Y777D01* +X185Y777D02* +X208Y777D01* +X96Y776D02* +X130Y776D01* +X185Y776D02* +X209Y776D01* +X96Y775D02* +X131Y775D01* +X186Y775D02* +X210Y775D01* +X96Y774D02* +X132Y774D01* +X186Y774D02* +X210Y774D01* +X95Y773D02* +X134Y773D01* +X187Y773D02* +X211Y773D01* +X95Y772D02* +X135Y772D01* +X188Y772D02* +X211Y772D01* +X95Y771D02* +X136Y771D01* +X191Y771D02* +X211Y771D01* +X95Y770D02* +X109Y770D01* +X113Y770D02* +X137Y770D01* +X195Y770D02* +X211Y770D01* +X95Y769D02* +X109Y769D01* +X114Y769D02* +X139Y769D01* +X196Y769D02* +X212Y769D01* +X95Y768D02* +X109Y768D01* +X116Y768D02* +X140Y768D01* +X197Y768D02* +X212Y768D01* +X95Y767D02* +X109Y767D01* +X117Y767D02* +X141Y767D01* +X197Y767D02* +X212Y767D01* +X95Y766D02* +X109Y766D01* +X118Y766D02* +X143Y766D01* +X198Y766D02* +X212Y766D01* +X95Y765D02* +X109Y765D01* +X120Y765D02* +X144Y765D01* +X198Y765D02* +X212Y765D01* +X95Y764D02* +X109Y764D01* +X121Y764D02* +X145Y764D01* +X198Y764D02* +X212Y764D01* +X95Y763D02* +X109Y763D01* +X122Y763D02* +X146Y763D01* +X198Y763D02* +X212Y763D01* +X95Y762D02* +X109Y762D01* +X123Y762D02* +X148Y762D01* +X198Y762D02* +X212Y762D01* +X95Y761D02* +X109Y761D01* +X125Y761D02* +X149Y761D01* +X198Y761D02* +X212Y761D01* +X95Y760D02* +X109Y760D01* +X126Y760D02* +X150Y760D01* +X198Y760D02* +X212Y760D01* +X95Y759D02* +X109Y759D01* +X127Y759D02* +X152Y759D01* +X198Y759D02* +X212Y759D01* +X95Y758D02* +X109Y758D01* +X129Y758D02* +X153Y758D01* +X198Y758D02* +X212Y758D01* +X95Y757D02* +X109Y757D01* +X130Y757D02* +X154Y757D01* +X198Y757D02* +X212Y757D01* +X95Y756D02* +X109Y756D01* +X131Y756D02* +X155Y756D01* +X198Y756D02* +X212Y756D01* +X95Y755D02* +X109Y755D01* +X132Y755D02* +X157Y755D01* +X198Y755D02* +X212Y755D01* +X95Y754D02* +X109Y754D01* +X134Y754D02* +X158Y754D01* +X198Y754D02* +X212Y754D01* +X95Y753D02* +X109Y753D01* +X135Y753D02* +X159Y753D01* +X198Y753D02* +X212Y753D01* +X95Y752D02* +X109Y752D01* +X136Y752D02* +X161Y752D01* +X198Y752D02* +X212Y752D01* +X95Y751D02* +X109Y751D01* +X138Y751D02* +X162Y751D01* +X198Y751D02* +X212Y751D01* +X95Y750D02* +X109Y750D01* +X139Y750D02* +X163Y750D01* +X198Y750D02* +X212Y750D01* +X95Y749D02* +X109Y749D01* +X140Y749D02* +X164Y749D01* +X198Y749D02* +X212Y749D01* +X95Y748D02* +X109Y748D01* +X141Y748D02* +X166Y748D01* +X198Y748D02* +X212Y748D01* +X1569Y748D02* +X1620Y748D01* +X95Y747D02* +X109Y747D01* +X143Y747D02* +X167Y747D01* +X198Y747D02* +X212Y747D01* +X1567Y747D02* +X1622Y747D01* +X95Y746D02* +X109Y746D01* +X144Y746D02* +X168Y746D01* +X198Y746D02* +X212Y746D01* +X1566Y746D02* +X1623Y746D01* +X95Y745D02* +X109Y745D01* +X145Y745D02* +X170Y745D01* +X198Y745D02* +X212Y745D01* +X1565Y745D02* +X1624Y745D01* +X95Y744D02* +X109Y744D01* +X147Y744D02* +X171Y744D01* +X198Y744D02* +X212Y744D01* +X1565Y744D02* +X1625Y744D01* +X95Y743D02* +X109Y743D01* +X148Y743D02* +X172Y743D01* +X198Y743D02* +X212Y743D01* +X1564Y743D02* +X1626Y743D01* +X95Y742D02* +X109Y742D01* +X149Y742D02* +X173Y742D01* +X198Y742D02* +X212Y742D01* +X1564Y742D02* +X1626Y742D01* +X95Y741D02* +X109Y741D01* +X151Y741D02* +X175Y741D01* +X198Y741D02* +X212Y741D01* +X1563Y741D02* +X1626Y741D01* +X95Y740D02* +X109Y740D01* +X152Y740D02* +X176Y740D01* +X198Y740D02* +X212Y740D01* +X1563Y740D02* +X1626Y740D01* +X95Y739D02* +X109Y739D01* +X153Y739D02* +X177Y739D01* +X198Y739D02* +X212Y739D01* +X1563Y739D02* +X1626Y739D01* +X95Y738D02* +X109Y738D01* +X154Y738D02* +X179Y738D01* +X198Y738D02* +X212Y738D01* +X1563Y738D02* +X1626Y738D01* +X95Y737D02* +X109Y737D01* +X156Y737D02* +X180Y737D01* +X198Y737D02* +X212Y737D01* +X1563Y737D02* +X1626Y737D01* +X95Y736D02* +X109Y736D01* +X157Y736D02* +X181Y736D01* +X198Y736D02* +X212Y736D01* +X1563Y736D02* +X1626Y736D01* +X95Y735D02* +X109Y735D01* +X158Y735D02* +X182Y735D01* +X198Y735D02* +X212Y735D01* +X1563Y735D02* +X1626Y735D01* +X95Y734D02* +X109Y734D01* +X160Y734D02* +X184Y734D01* +X198Y734D02* +X212Y734D01* +X1563Y734D02* +X1626Y734D01* +X95Y733D02* +X109Y733D01* +X161Y733D02* +X185Y733D01* +X198Y733D02* +X212Y733D01* +X1563Y733D02* +X1626Y733D01* +X95Y732D02* +X109Y732D01* +X162Y732D02* +X186Y732D01* +X198Y732D02* +X212Y732D01* +X1563Y732D02* +X1626Y732D01* +X95Y731D02* +X109Y731D01* +X163Y731D02* +X188Y731D01* +X198Y731D02* +X212Y731D01* +X1563Y731D02* +X1626Y731D01* +X95Y730D02* +X109Y730D01* +X165Y730D02* +X189Y730D01* +X198Y730D02* +X212Y730D01* +X1563Y730D02* +X1581Y730D01* +X1609Y730D02* +X1626Y730D01* +X95Y729D02* +X110Y729D01* +X166Y729D02* +X190Y729D01* +X198Y729D02* +X212Y729D01* +X1563Y729D02* +X1580Y729D01* +X1609Y729D02* +X1626Y729D01* +X95Y728D02* +X110Y728D01* +X167Y728D02* +X191Y728D01* +X198Y728D02* +X212Y728D01* +X1563Y728D02* +X1580Y728D01* +X1609Y728D02* +X1626Y728D01* +X95Y727D02* +X111Y727D01* +X169Y727D02* +X193Y727D01* +X198Y727D02* +X212Y727D01* +X1563Y727D02* +X1580Y727D01* +X1609Y727D02* +X1626Y727D01* +X96Y726D02* +X114Y726D01* +X170Y726D02* +X194Y726D01* +X196Y726D02* +X212Y726D01* +X1563Y726D02* +X1580Y726D01* +X1609Y726D02* +X1626Y726D01* +X96Y725D02* +X118Y725D01* +X171Y725D02* +X212Y725D01* +X1563Y725D02* +X1580Y725D01* +X1609Y725D02* +X1626Y725D01* +X96Y724D02* +X119Y724D01* +X172Y724D02* +X212Y724D01* +X1563Y724D02* +X1580Y724D01* +X1609Y724D02* +X1626Y724D01* +X97Y723D02* +X120Y723D01* +X174Y723D02* +X211Y723D01* +X1563Y723D02* +X1580Y723D01* +X1609Y723D02* +X1626Y723D01* +X97Y722D02* +X121Y722D01* +X175Y722D02* +X211Y722D01* +X1563Y722D02* +X1580Y722D01* +X1609Y722D02* +X1626Y722D01* +X98Y721D02* +X122Y721D01* +X176Y721D02* +X211Y721D01* +X1563Y721D02* +X1580Y721D01* +X1609Y721D02* +X1626Y721D01* +X98Y720D02* +X122Y720D01* +X178Y720D02* +X210Y720D01* +X1563Y720D02* +X1580Y720D01* +X1609Y720D02* +X1626Y720D01* +X99Y719D02* +X122Y719D01* +X179Y719D02* +X210Y719D01* +X1563Y719D02* +X1580Y719D01* +X1609Y719D02* +X1626Y719D01* +X100Y718D02* +X122Y718D01* +X180Y718D02* +X209Y718D01* +X1563Y718D02* +X1580Y718D01* +X1609Y718D02* +X1626Y718D01* +X101Y717D02* +X122Y717D01* +X181Y717D02* +X208Y717D01* +X1563Y717D02* +X1580Y717D01* +X1609Y717D02* +X1626Y717D01* +X102Y716D02* +X122Y716D01* +X183Y716D02* +X207Y716D01* +X1563Y716D02* +X1580Y716D01* +X1609Y716D02* +X1626Y716D01* +X103Y715D02* +X121Y715D01* +X184Y715D02* +X206Y715D01* +X1563Y715D02* +X1580Y715D01* +X1609Y715D02* +X1626Y715D01* +X104Y714D02* +X121Y714D01* +X185Y714D02* +X205Y714D01* +X1563Y714D02* +X1580Y714D01* +X1609Y714D02* +X1626Y714D01* +X106Y713D02* +X120Y713D01* +X187Y713D02* +X204Y713D01* +X1563Y713D02* +X1580Y713D01* +X1609Y713D02* +X1626Y713D01* +X108Y712D02* +X119Y712D01* +X189Y712D02* +X202Y712D01* +X1563Y712D02* +X1580Y712D01* +X1609Y712D02* +X1626Y712D01* +X112Y711D02* +X117Y711D01* +X192Y711D02* +X198Y711D01* +X1563Y711D02* +X1580Y711D01* +X1609Y711D02* +X1626Y711D01* +X1563Y710D02* +X1580Y710D01* +X1609Y710D02* +X1626Y710D01* +X1563Y709D02* +X1580Y709D01* +X1609Y709D02* +X1626Y709D01* +X1563Y708D02* +X1580Y708D01* +X1609Y708D02* +X1626Y708D01* +X1563Y707D02* +X1580Y707D01* +X1609Y707D02* +X1626Y707D01* +X1563Y706D02* +X1580Y706D01* +X1609Y706D02* +X1626Y706D01* +X1563Y705D02* +X1580Y705D01* +X1609Y705D02* +X1626Y705D01* +X1563Y704D02* +X1580Y704D01* +X1609Y704D02* +X1626Y704D01* +X1563Y703D02* +X1580Y703D01* +X1609Y703D02* +X1626Y703D01* +X1563Y702D02* +X1580Y702D01* +X1609Y702D02* +X1626Y702D01* +X1563Y701D02* +X1580Y701D01* +X1609Y701D02* +X1626Y701D01* +X1563Y700D02* +X1580Y700D01* +X1609Y700D02* +X1626Y700D01* +X1563Y699D02* +X1580Y699D01* +X1609Y699D02* +X1626Y699D01* +X1563Y698D02* +X1580Y698D01* +X1609Y698D02* +X1626Y698D01* +X1563Y697D02* +X1580Y697D01* +X1609Y697D02* +X1626Y697D01* +X1563Y696D02* +X1580Y696D01* +X1609Y696D02* +X1626Y696D01* +X1563Y695D02* +X1580Y695D01* +X1609Y695D02* +X1626Y695D01* +X1563Y694D02* +X1580Y694D01* +X1609Y694D02* +X1626Y694D01* +X1563Y693D02* +X1580Y693D01* +X1609Y693D02* +X1626Y693D01* +X1563Y692D02* +X1580Y692D01* +X1609Y692D02* +X1626Y692D01* +X1563Y691D02* +X1580Y691D01* +X1609Y691D02* +X1626Y691D01* +X1563Y690D02* +X1580Y690D01* +X1609Y690D02* +X1626Y690D01* +X1563Y689D02* +X1580Y689D01* +X1609Y689D02* +X1626Y689D01* +X1563Y688D02* +X1580Y688D01* +X1609Y688D02* +X1626Y688D01* +X1563Y687D02* +X1580Y687D01* +X1609Y687D02* +X1626Y687D01* +X1563Y686D02* +X1580Y686D01* +X1609Y686D02* +X1626Y686D01* +X1563Y685D02* +X1580Y685D01* +X1609Y685D02* +X1626Y685D01* +X1690Y685D02* +X1698Y685D01* +X1563Y684D02* +X1580Y684D01* +X1609Y684D02* +X1626Y684D01* +X1689Y684D02* +X1699Y684D01* +X1563Y683D02* +X1580Y683D01* +X1609Y683D02* +X1626Y683D01* +X1688Y683D02* +X1700Y683D01* +X1563Y682D02* +X1580Y682D01* +X1609Y682D02* +X1626Y682D01* +X1687Y682D02* +X1701Y682D01* +X1563Y681D02* +X1580Y681D01* +X1609Y681D02* +X1626Y681D01* +X1686Y681D02* +X1702Y681D01* +X1563Y680D02* +X1580Y680D01* +X1609Y680D02* +X1626Y680D01* +X1686Y680D02* +X1702Y680D01* +X1563Y679D02* +X1580Y679D01* +X1609Y679D02* +X1626Y679D01* +X1686Y679D02* +X1702Y679D01* +X1563Y678D02* +X1580Y678D01* +X1609Y678D02* +X1626Y678D01* +X1685Y678D02* +X1702Y678D01* +X1563Y677D02* +X1581Y677D01* +X1609Y677D02* +X1627Y677D01* +X1685Y677D02* +X1703Y677D01* +X1563Y676D02* +X1703Y676D01* +X1563Y675D02* +X1703Y675D01* +X1563Y674D02* +X1703Y674D01* +X1563Y673D02* +X1703Y673D01* +X1563Y672D02* +X1703Y672D01* +X1563Y671D02* +X1703Y671D01* +X1563Y670D02* +X1703Y670D01* +X1563Y669D02* +X1703Y669D01* +X1563Y668D02* +X1703Y668D01* +X1563Y667D02* +X1702Y667D01* +X1563Y666D02* +X1702Y666D01* +X1564Y665D02* +X1702Y665D01* +X1564Y664D02* +X1702Y664D01* +X1565Y663D02* +X1701Y663D01* +X1566Y662D02* +X1700Y662D01* +X1567Y661D02* +X1699Y661D01* +X1568Y660D02* +X1698Y660D01* +X1572Y659D02* +X1694Y659D01* +X1623Y618D02* +X1635Y618D01* +X1621Y617D02* +X1637Y617D01* +X1620Y616D02* +X1639Y616D01* +X1619Y615D02* +X1640Y615D01* +X1618Y614D02* +X1640Y614D01* +X1617Y613D02* +X1641Y613D01* +X1617Y612D02* +X1641Y612D01* +X1617Y611D02* +X1641Y611D01* +X1617Y610D02* +X1642Y610D01* +X1617Y609D02* +X1642Y609D01* +X1617Y608D02* +X1642Y608D01* +X1617Y607D02* +X1642Y607D01* +X1617Y606D02* +X1642Y606D01* +X1617Y605D02* +X1642Y605D01* +X1617Y604D02* +X1642Y604D01* +X1617Y603D02* +X1642Y603D01* +X1617Y602D02* +X1642Y602D01* +X1617Y601D02* +X1642Y601D01* +X1617Y600D02* +X1642Y600D01* +X1617Y599D02* +X1642Y599D01* +X1617Y598D02* +X1642Y598D01* +X1617Y597D02* +X1642Y597D01* +X1617Y596D02* +X1642Y596D01* +X1617Y595D02* +X1642Y595D01* +X1617Y594D02* +X1642Y594D01* +X1617Y593D02* +X1642Y593D01* +X1617Y592D02* +X1642Y592D01* +X1617Y591D02* +X1642Y591D01* +X1617Y590D02* +X1642Y590D01* +X1617Y589D02* +X1642Y589D01* +X1617Y588D02* +X1642Y588D01* +X1617Y587D02* +X1642Y587D01* +X1617Y586D02* +X1642Y586D01* +X1617Y585D02* +X1642Y585D01* +X1617Y584D02* +X1642Y584D01* +X1617Y583D02* +X1642Y583D01* +X1617Y582D02* +X1642Y582D01* +X1617Y581D02* +X1642Y581D01* +X1617Y580D02* +X1642Y580D01* +X1617Y579D02* +X1642Y579D01* +X1617Y578D02* +X1642Y578D01* +X1617Y577D02* +X1642Y577D01* +X1617Y576D02* +X1642Y576D01* +X1617Y575D02* +X1642Y575D01* +X1617Y574D02* +X1642Y574D01* +X1617Y573D02* +X1642Y573D01* +X1617Y572D02* +X1642Y572D01* +X1617Y571D02* +X1642Y571D01* +X1617Y570D02* +X1642Y570D01* +X1617Y569D02* +X1642Y569D01* +X1617Y568D02* +X1642Y568D01* +X1617Y567D02* +X1642Y567D01* +X1617Y566D02* +X1642Y566D01* +X1617Y565D02* +X1642Y565D01* +X1617Y564D02* +X1642Y564D01* +X1617Y563D02* +X1642Y563D01* +X1617Y562D02* +X1642Y562D01* +X1617Y561D02* +X1642Y561D01* +X1617Y560D02* +X1642Y560D01* +X1617Y559D02* +X1642Y559D01* +X1617Y558D02* +X1642Y558D01* +X1617Y557D02* +X1642Y557D01* +X1617Y556D02* +X1642Y556D01* +X1617Y555D02* +X1642Y555D01* +X1617Y554D02* +X1642Y554D01* +X1617Y553D02* +X1642Y553D01* +X1617Y552D02* +X1642Y552D01* +X1617Y551D02* +X1642Y551D01* +X1617Y550D02* +X1642Y550D01* +X1617Y549D02* +X1642Y549D01* +X1617Y548D02* +X1642Y548D01* +X1617Y547D02* +X1642Y547D01* +X1617Y546D02* +X1642Y546D01* +X1617Y545D02* +X1642Y545D01* +X1617Y544D02* +X1642Y544D01* +X1617Y543D02* +X1642Y543D01* +X1617Y542D02* +X1642Y542D01* +X1617Y541D02* +X1642Y541D01* +X1617Y540D02* +X1642Y540D01* +X1617Y539D02* +X1642Y539D01* +X1617Y538D02* +X1642Y538D01* +X1617Y537D02* +X1642Y537D01* +X1617Y536D02* +X1641Y536D01* +X1617Y535D02* +X1641Y535D01* +X1618Y534D02* +X1641Y534D01* +X1618Y533D02* +X1640Y533D01* +X1619Y532D02* +X1639Y532D01* +X1620Y531D02* +X1638Y531D01* +X1621Y530D02* +X1637Y530D01* +X1625Y529D02* +X1633Y529D01* +X1627Y488D02* +X1638Y488D01* +X1623Y487D02* +X1643Y487D01* +X1620Y486D02* +X1646Y486D01* +X1617Y485D02* +X1649Y485D01* +X1615Y484D02* +X1651Y484D01* +X1613Y483D02* +X1653Y483D01* +X1611Y482D02* +X1655Y482D01* +X1609Y481D02* +X1657Y481D01* +X1607Y480D02* +X1659Y480D01* +X1605Y479D02* +X1661Y479D01* +X1603Y478D02* +X1663Y478D01* +X1601Y477D02* +X1665Y477D01* +X1599Y476D02* +X1667Y476D01* +X1597Y475D02* +X1669Y475D01* +X1595Y474D02* +X1671Y474D01* +X1593Y473D02* +X1673Y473D01* +X1591Y472D02* +X1675Y472D01* +X1589Y471D02* +X1677Y471D01* +X1587Y470D02* +X1629Y470D01* +X1637Y470D02* +X1679Y470D01* +X1585Y469D02* +X1625Y469D01* +X1641Y469D02* +X1681Y469D01* +X1583Y468D02* +X1622Y468D01* +X1643Y468D02* +X1683Y468D01* +X1581Y467D02* +X1620Y467D01* +X1645Y467D02* +X1685Y467D01* +X1579Y466D02* +X1618Y466D01* +X1647Y466D02* +X1687Y466D01* +X1577Y465D02* +X1616Y465D01* +X1649Y465D02* +X1689Y465D01* +X1575Y464D02* +X1614Y464D01* +X1651Y464D02* +X1690Y464D01* +X1573Y463D02* +X1612Y463D01* +X1653Y463D02* +X1692Y463D01* +X1572Y462D02* +X1611Y462D01* +X1655Y462D02* +X1693Y462D01* +X1571Y461D02* +X1609Y461D01* +X1657Y461D02* +X1694Y461D01* +X1570Y460D02* +X1607Y460D01* +X1659Y460D02* +X1695Y460D01* +X1569Y459D02* +X1605Y459D01* +X1661Y459D02* +X1696Y459D01* +X1569Y458D02* +X1603Y458D01* +X1663Y458D02* +X1697Y458D01* +X1568Y457D02* +X1601Y457D01* +X1665Y457D02* +X1697Y457D01* +X1567Y456D02* +X1599Y456D01* +X1667Y456D02* +X1698Y456D01* +X1567Y455D02* +X1597Y455D01* +X1669Y455D02* +X1699Y455D01* +X1566Y454D02* +X1595Y454D01* +X1671Y454D02* +X1699Y454D01* +X1566Y453D02* +X1593Y453D01* +X1673Y453D02* +X1700Y453D01* +X1565Y452D02* +X1591Y452D01* +X1675Y452D02* +X1700Y452D01* +X1565Y451D02* +X1589Y451D01* +X1677Y451D02* +X1701Y451D01* +X1565Y450D02* +X1587Y450D01* +X1679Y450D02* +X1701Y450D01* +X1564Y449D02* +X1585Y449D01* +X1681Y449D02* +X1701Y449D01* +X1564Y448D02* +X1583Y448D01* +X1682Y448D02* +X1702Y448D01* +X1564Y447D02* +X1582Y447D01* +X1683Y447D02* +X1702Y447D01* +X1564Y446D02* +X1582Y446D01* +X1684Y446D02* +X1702Y446D01* +X1563Y445D02* +X1581Y445D01* +X1685Y445D02* +X1702Y445D01* +X1563Y444D02* +X1581Y444D01* +X1685Y444D02* +X1702Y444D01* +X1563Y443D02* +X1581Y443D01* +X1685Y443D02* +X1702Y443D01* +X1563Y442D02* +X1580Y442D01* +X1685Y442D02* +X1703Y442D01* +X1563Y441D02* +X1580Y441D01* +X1685Y441D02* +X1703Y441D01* +X1563Y440D02* +X1580Y440D01* +X1685Y440D02* +X1703Y440D01* +X1563Y439D02* +X1580Y439D01* +X1685Y439D02* +X1703Y439D01* +X1563Y438D02* +X1580Y438D01* +X1685Y438D02* +X1703Y438D01* +X1563Y437D02* +X1580Y437D01* +X1685Y437D02* +X1703Y437D01* +X1563Y436D02* +X1580Y436D01* +X1685Y436D02* +X1703Y436D01* +X1563Y435D02* +X1581Y435D01* +X1685Y435D02* +X1703Y435D01* +X1563Y434D02* +X1703Y434D01* +X99Y433D02* +X105Y433D01* +X202Y433D02* +X208Y433D01* +X1563Y433D02* +X1703Y433D01* +X98Y432D02* +X106Y432D01* +X200Y432D02* +X209Y432D01* +X1563Y432D02* +X1703Y432D01* +X97Y431D02* +X107Y431D01* +X199Y431D02* +X210Y431D01* +X1563Y431D02* +X1703Y431D01* +X96Y430D02* +X108Y430D01* +X199Y430D02* +X211Y430D01* +X1563Y430D02* +X1703Y430D01* +X95Y429D02* +X109Y429D01* +X198Y429D02* +X211Y429D01* +X1563Y429D02* +X1703Y429D01* +X95Y428D02* +X109Y428D01* +X198Y428D02* +X212Y428D01* +X1563Y428D02* +X1703Y428D01* +X95Y427D02* +X109Y427D01* +X198Y427D02* +X212Y427D01* +X1563Y427D02* +X1703Y427D01* +X95Y426D02* +X109Y426D01* +X198Y426D02* +X212Y426D01* +X1563Y426D02* +X1703Y426D01* +X95Y425D02* +X109Y425D01* +X198Y425D02* +X212Y425D01* +X1563Y425D02* +X1703Y425D01* +X95Y424D02* +X109Y424D01* +X198Y424D02* +X212Y424D01* +X1563Y424D02* +X1703Y424D01* +X95Y423D02* +X109Y423D01* +X198Y423D02* +X212Y423D01* +X1563Y423D02* +X1703Y423D01* +X95Y422D02* +X109Y422D01* +X198Y422D02* +X212Y422D01* +X1563Y422D02* +X1703Y422D01* +X95Y421D02* +X109Y421D01* +X198Y421D02* +X212Y421D01* +X1563Y421D02* +X1703Y421D01* +X95Y420D02* +X109Y420D01* +X198Y420D02* +X212Y420D01* +X1563Y420D02* +X1703Y420D01* +X95Y419D02* +X109Y419D01* +X198Y419D02* +X212Y419D01* +X1563Y419D02* +X1703Y419D01* +X95Y418D02* +X109Y418D01* +X198Y418D02* +X212Y418D01* +X1563Y418D02* +X1703Y418D01* +X95Y417D02* +X109Y417D01* +X198Y417D02* +X212Y417D01* +X1563Y417D02* +X1703Y417D01* +X95Y416D02* +X109Y416D01* +X198Y416D02* +X212Y416D01* +X1563Y416D02* +X1580Y416D01* +X1685Y416D02* +X1703Y416D01* +X95Y415D02* +X109Y415D01* +X198Y415D02* +X212Y415D01* +X1563Y415D02* +X1580Y415D01* +X1685Y415D02* +X1703Y415D01* +X95Y414D02* +X109Y414D01* +X198Y414D02* +X212Y414D01* +X1563Y414D02* +X1580Y414D01* +X1685Y414D02* +X1703Y414D01* +X95Y413D02* +X109Y413D01* +X198Y413D02* +X212Y413D01* +X1563Y413D02* +X1580Y413D01* +X1685Y413D02* +X1703Y413D01* +X95Y412D02* +X109Y412D01* +X198Y412D02* +X212Y412D01* +X1563Y412D02* +X1580Y412D01* +X1685Y412D02* +X1703Y412D01* +X95Y411D02* +X109Y411D01* +X198Y411D02* +X212Y411D01* +X1563Y411D02* +X1580Y411D01* +X1685Y411D02* +X1703Y411D01* +X95Y410D02* +X109Y410D01* +X198Y410D02* +X212Y410D01* +X1563Y410D02* +X1580Y410D01* +X1685Y410D02* +X1703Y410D01* +X95Y409D02* +X109Y409D01* +X198Y409D02* +X212Y409D01* +X1563Y409D02* +X1580Y409D01* +X1685Y409D02* +X1703Y409D01* +X95Y408D02* +X109Y408D01* +X198Y408D02* +X212Y408D01* +X1563Y408D02* +X1580Y408D01* +X1685Y408D02* +X1703Y408D01* +X95Y407D02* +X109Y407D01* +X198Y407D02* +X212Y407D01* +X1563Y407D02* +X1580Y407D01* +X1685Y407D02* +X1702Y407D01* +X95Y406D02* +X109Y406D01* +X198Y406D02* +X212Y406D01* +X1563Y406D02* +X1580Y406D01* +X1686Y406D02* +X1702Y406D01* +X95Y405D02* +X109Y405D01* +X198Y405D02* +X212Y405D01* +X1564Y405D02* +X1580Y405D01* +X1686Y405D02* +X1702Y405D01* +X95Y404D02* +X109Y404D01* +X198Y404D02* +X212Y404D01* +X1564Y404D02* +X1580Y404D01* +X1686Y404D02* +X1702Y404D01* +X95Y403D02* +X109Y403D01* +X198Y403D02* +X212Y403D01* +X1565Y403D02* +X1579Y403D01* +X1687Y403D02* +X1701Y403D01* +X95Y402D02* +X109Y402D01* +X198Y402D02* +X212Y402D01* +X1565Y402D02* +X1578Y402D01* +X1688Y402D02* +X1700Y402D01* +X95Y401D02* +X109Y401D01* +X198Y401D02* +X212Y401D01* +X1567Y401D02* +X1577Y401D01* +X1689Y401D02* +X1699Y401D01* +X95Y400D02* +X109Y400D01* +X198Y400D02* +X212Y400D01* +X1568Y400D02* +X1576Y400D01* +X1690Y400D02* +X1698Y400D01* +X95Y399D02* +X109Y399D01* +X198Y399D02* +X212Y399D01* +X1571Y399D02* +X1573Y399D01* +X1693Y399D02* +X1695Y399D01* +X95Y398D02* +X109Y398D01* +X198Y398D02* +X212Y398D01* +X95Y397D02* +X109Y397D01* +X198Y397D02* +X212Y397D01* +X95Y396D02* +X109Y396D01* +X197Y396D02* +X212Y396D01* +X95Y395D02* +X110Y395D01* +X197Y395D02* +X212Y395D01* +X95Y394D02* +X110Y394D01* +X197Y394D02* +X212Y394D01* +X95Y393D02* +X111Y393D01* +X196Y393D02* +X211Y393D01* +X96Y392D02* +X112Y392D01* +X195Y392D02* +X211Y392D01* +X96Y391D02* +X114Y391D01* +X193Y391D02* +X211Y391D01* +X96Y390D02* +X116Y390D01* +X191Y390D02* +X211Y390D01* +X97Y389D02* +X118Y389D01* +X189Y389D02* +X210Y389D01* +X97Y388D02* +X120Y388D01* +X187Y388D02* +X210Y388D01* +X98Y387D02* +X122Y387D01* +X185Y387D02* +X209Y387D01* +X98Y386D02* +X124Y386D01* +X183Y386D02* +X209Y386D01* +X99Y385D02* +X126Y385D01* +X181Y385D02* +X208Y385D01* +X100Y384D02* +X128Y384D01* +X179Y384D02* +X207Y384D01* +X101Y383D02* +X130Y383D01* +X177Y383D02* +X207Y383D01* +X101Y382D02* +X132Y382D01* +X175Y382D02* +X206Y382D01* +X102Y381D02* +X134Y381D01* +X173Y381D02* +X205Y381D01* +X104Y380D02* +X136Y380D01* +X171Y380D02* +X204Y380D01* +X105Y379D02* +X138Y379D01* +X169Y379D02* +X202Y379D01* +X107Y378D02* +X140Y378D01* +X167Y378D02* +X201Y378D01* +X108Y377D02* +X141Y377D01* +X165Y377D02* +X199Y377D01* +X110Y376D02* +X143Y376D01* +X163Y376D02* +X197Y376D01* +X112Y375D02* +X146Y375D01* +X161Y375D02* +X195Y375D01* +X114Y374D02* +X149Y374D01* +X157Y374D02* +X193Y374D01* +X116Y373D02* +X191Y373D01* +X118Y372D02* +X189Y372D01* +X120Y371D02* +X187Y371D01* +X122Y370D02* +X185Y370D01* +X124Y369D02* +X183Y369D01* +X126Y368D02* +X181Y368D01* +X128Y367D02* +X179Y367D01* +X130Y366D02* +X177Y366D01* +X132Y365D02* +X174Y365D01* +X134Y364D02* +X172Y364D01* +X136Y363D02* +X170Y363D01* +X138Y362D02* +X168Y362D01* +X141Y361D02* +X166Y361D01* +X144Y360D02* +X163Y360D01* +X148Y359D02* +X159Y359D01* +X1569Y358D02* +X1702Y358D01* +X1567Y357D02* +X1703Y357D01* +X1566Y356D02* +X1703Y356D01* +X1565Y355D02* +X1703Y355D01* +X1565Y354D02* +X1703Y354D01* +X1564Y353D02* +X1703Y353D01* +X1564Y352D02* +X1703Y352D01* +X1563Y351D02* +X1703Y351D01* +X1563Y350D02* +X1703Y350D01* +X1563Y349D02* +X1703Y349D01* +X1563Y348D02* +X1703Y348D01* +X1564Y347D02* +X1703Y347D01* +X1564Y346D02* +X1703Y346D01* +X1564Y345D02* +X1703Y345D01* +X1565Y344D02* +X1703Y344D01* +X1566Y343D02* +X1703Y343D01* +X1567Y342D02* +X1703Y342D01* +X1569Y341D02* +X1703Y341D01* +X1678Y340D02* +X1703Y340D01* +X1677Y339D02* +X1703Y339D01* +X1675Y338D02* +X1703Y338D01* +X1674Y337D02* +X1703Y337D01* +X1672Y336D02* +X1703Y336D01* +X1671Y335D02* +X1702Y335D01* +X1670Y334D02* +X1700Y334D01* +X1668Y333D02* +X1699Y333D01* +X1667Y332D02* +X1697Y332D01* +X1665Y331D02* +X1696Y331D01* +X1664Y330D02* +X1694Y330D01* +X1662Y329D02* +X1693Y329D01* +X1661Y328D02* +X1692Y328D01* +X1660Y327D02* +X1690Y327D01* +X1658Y326D02* +X1689Y326D01* +X1657Y325D02* +X1687Y325D01* +X1655Y324D02* +X1686Y324D01* +X1654Y323D02* +X1684Y323D01* +X1645Y322D02* +X1683Y322D01* +X1643Y321D02* +X1682Y321D01* +X1642Y320D02* +X1680Y320D01* +X1641Y319D02* +X1679Y319D01* +X1641Y318D02* +X1677Y318D01* +X1640Y317D02* +X1676Y317D01* +X1640Y316D02* +X1674Y316D01* +X1640Y315D02* +X1673Y315D01* +X1640Y314D02* +X1672Y314D01* +X1640Y313D02* +X1672Y313D01* +X1640Y312D02* +X1673Y312D01* +X1640Y311D02* +X1675Y311D01* +X1640Y310D02* +X1676Y310D01* +X1641Y309D02* +X1678Y309D01* +X1641Y308D02* +X1679Y308D01* +X1642Y307D02* +X1681Y307D01* +X1644Y306D02* +X1682Y306D01* +X1646Y305D02* +X1683Y305D01* +X1654Y304D02* +X1685Y304D01* +X1655Y303D02* +X1686Y303D01* +X1657Y302D02* +X1688Y302D01* +X1658Y301D02* +X1689Y301D01* +X1660Y300D02* +X1691Y300D01* +X1661Y299D02* +X1692Y299D01* +X1663Y298D02* +X1693Y298D01* +X1664Y297D02* +X1695Y297D01* +X1665Y296D02* +X1696Y296D01* +X1667Y295D02* +X1698Y295D01* +X1668Y294D02* +X1699Y294D01* +X1670Y293D02* +X1700Y293D01* +X1671Y292D02* +X1702Y292D01* +X1673Y291D02* +X1703Y291D01* +X1674Y290D02* +X1703Y290D01* +X1675Y289D02* +X1703Y289D01* +X1677Y288D02* +X1703Y288D01* +X1571Y287D02* +X1703Y287D01* +X1568Y286D02* +X1703Y286D01* +X1567Y285D02* +X1703Y285D01* +X1566Y284D02* +X1703Y284D01* +X1565Y283D02* +X1703Y283D01* +X1564Y282D02* +X1703Y282D01* +X1564Y281D02* +X1703Y281D01* +X1563Y280D02* +X1703Y280D01* +X1563Y279D02* +X1703Y279D01* +X1563Y278D02* +X1703Y278D01* +X1563Y277D02* +X1703Y277D01* +X1563Y276D02* +X1703Y276D01* +X1564Y275D02* +X1703Y275D01* +X1564Y274D02* +X1703Y274D01* +X1565Y273D02* +X1703Y273D01* +X1565Y272D02* +X1703Y272D01* +X1567Y271D02* +X1703Y271D01* +X1568Y270D02* +X1703Y270D01* +X1571Y269D02* +X1702Y269D01* +D02* +G04 End of Copper0* +M02* \ No newline at end of file diff --git a/tests/gerber_files/detector_copper_top.gbr b/tests/gerber_files/detector_copper_top.gbr new file mode 100644 index 0000000..52b2e2a --- /dev/null +++ b/tests/gerber_files/detector_copper_top.gbr @@ -0,0 +1,71 @@ +G04 MADE WITH FRITZING* +G04 WWW.FRITZING.ORG* +G04 DOUBLE SIDED* +G04 HOLES PLATED* +G04 CONTOUR ON CENTER OF CONTOUR VECTOR* +%ASAXBY*% +%FSLAX23Y23*% +%MOIN*% +%OFA0B0*% +%SFA1.0B1.0*% +%ADD10C,0.075000*% +%ADD11C,0.099055*% +%ADD12C,0.078740*% +%ADD13R,0.075000X0.075000*% +%ADD14C,0.024000*% +%ADD15C,0.020000*% +%LNCOPPER1*% +G90* +G70* +G54D10* +X1149Y872D03* +X1349Y872D03* +X749Y722D03* +X749Y522D03* +X1149Y522D03* +X1449Y522D03* +X1149Y422D03* +X1449Y422D03* +X1149Y322D03* +X1449Y322D03* +X1149Y222D03* +X1449Y222D03* +X949Y472D03* +X949Y72D03* +G54D11* +X749Y972D03* +X599Y972D03* +X349Y322D03* +X349Y472D03* +X349Y672D03* +X349Y822D03* +G54D10* +X699Y122D03* +X699Y322D03* +G54D12* +X699Y222D03* +X949Y972D03* +X749Y622D03* +X1049Y222D03* +X1249Y872D03* +G54D13* +X1149Y872D03* +X1149Y522D03* +G54D14* +X952Y946D02* +X1045Y249D01* +G54D15* +X776Y695D02* +X721Y695D01* +X721Y750D01* +X776Y750D01* +X776Y695D01* +D02* +X671Y150D02* +X726Y150D01* +X726Y95D01* +X671Y95D01* +X671Y150D01* +D02* +G04 End of Copper1* +M02* \ No newline at end of file diff --git a/tests/gerber_files/detector_drill.txt b/tests/gerber_files/detector_drill.txt new file mode 100644 index 0000000..c4945b8 --- /dev/null +++ b/tests/gerber_files/detector_drill.txt @@ -0,0 +1,46 @@ +; NON-PLATED HOLES START AT T1 +; THROUGH (PLATED) HOLES START AT T100 +M48 +INCH +T1C0.125984 +T100C0.031496 +T101C0.035000 +T102C0.059055 +% +T1 +X001488Y010223 +X001488Y001223 +X016488Y001223 +X016488Y010223 +T100 +X009488Y009723 +X007488Y006223 +X012488Y008723 +X010488Y002223 +X006988Y002223 +T101 +X014488Y004223 +X006988Y003223 +X013488Y008723 +X011488Y008723 +X007488Y005223 +X014488Y003223 +X014488Y002223 +X011488Y005223 +X009488Y000723 +X011488Y004223 +X006988Y001223 +X009488Y004723 +X007488Y007223 +X011488Y003223 +X014488Y005223 +X011488Y002223 +T102 +X003488Y008223 +X003488Y004723 +X007488Y009723 +X003488Y006723 +X005988Y009723 +X003488Y003223 +T00 +M30 diff --git a/tests/test_tcl_shell.py b/tests/test_tcl_shell.py new file mode 100644 index 0000000..51138b1 --- /dev/null +++ b/tests/test_tcl_shell.py @@ -0,0 +1,155 @@ +import sys +import unittest +from PyQt4 import QtGui +from FlatCAMApp import App +from FlatCAMObj import FlatCAMGerber, FlatCAMGeometry, FlatCAMCNCjob, FlatCAMExcellon +from ObjectUI import GerberObjectUI, GeometryObjectUI +from time import sleep +import os +import tempfile + +class TclShellCommandTest(unittest.TestCase): + + 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' + excellon_name = "excellon" + gerber_top_name = "top" + gerber_bottom_name = "bottom" + gerber_cutout_name = "cutout" + engraver_diameter = 0.3 + cutout_diameter = 3 + drill_diameter = 0.8 + + def setUp(self): + self.app = QtGui.QApplication(sys.argv) + + # Create App, keep app defaults (do not load + # user-defined defaults). + self.fc = App(user_defaults=False) + + def tearDown(self): + del self.fc + del self.app + + def test_set_get_units(self): + + self.fc.exec_command_test('set_sys units IN') + self.fc.exec_command_test('new') + units=self.fc.exec_command_test('get_sys units') + self.assertEquals(units, "IN") + + self.fc.exec_command_test('set_sys units MM') + self.fc.exec_command_test('new') + units=self.fc.exec_command_test('get_sys units') + self.assertEquals(units, "MM") + + def test_gerber_flow(self): + + # open gerber files top, bottom and cutout + + self.fc.exec_command_test('set_sys units MM') + self.fc.exec_command_test('new') + + self.fc.exec_command_test('open_gerber %s/%s -outname %s' % (self.gerber_files, self.copper_top_filename, self.gerber_top_name)) + gerber_top_obj = self.fc.collection.get_by_name(self.gerber_top_name) + self.assertTrue(isinstance(gerber_top_obj, FlatCAMGerber), + "Expected FlatCAMGerber, instead, %s is %s" % + (self.gerber_top_name, type(gerber_top_obj))) + + self.fc.exec_command_test('open_gerber %s/%s -outname %s' % (self.gerber_files, self.copper_bottom_filename, self.gerber_bottom_name)) + gerber_bottom_obj = self.fc.collection.get_by_name(self.gerber_bottom_name) + self.assertTrue(isinstance(gerber_bottom_obj, FlatCAMGerber), + "Expected FlatCAMGerber, instead, %s is %s" % + (self.gerber_bottom_name, type(gerber_bottom_obj))) + + self.fc.exec_command_test('open_gerber %s/%s -outname %s' % (self.gerber_files, self.cutout_filename, self.gerber_cutout_name)) + gerber_cutout_obj = self.fc.collection.get_by_name(self.gerber_cutout_name) + self.assertTrue(isinstance(gerber_cutout_obj, FlatCAMGerber), + "Expected FlatCAMGerber, instead, %s is %s" % + (self.gerber_cutout_name, type(gerber_cutout_obj))) + + # exteriors delete and join geometries for top layer + self.fc.exec_command_test('isolate %s -dia %f' % (self.gerber_cutout_name, self.engraver_diameter)) + self.fc.exec_command_test('exteriors %s -outname %s' % (self.gerber_cutout_name + '_iso', self.gerber_cutout_name + '_iso_exterior')) + self.fc.exec_command_test('delete %s' % (self.gerber_cutout_name + '_iso')) + obj = self.fc.collection.get_by_name(self.gerber_cutout_name + '_iso_exterior') + self.assertTrue(isinstance(obj, FlatCAMGeometry), + "Expected FlatCAMGeometry, instead, %s is %s" % + (self.gerber_cutout_name + '_iso_exterior', type(obj))) + + # mirror bottom gerbers + self.fc.exec_command_test('mirror %s -box %s -axis X' % (self.gerber_bottom_name, self.gerber_cutout_name)) + self.fc.exec_command_test('mirror %s -box %s -axis X' % (self.gerber_cutout_name, self.gerber_cutout_name)) + + # exteriors delete and join geometries for bottom layer + self.fc.exec_command_test('isolate %s -dia %f -outname %s' % (self.gerber_cutout_name, self.engraver_diameter, self.gerber_cutout_name + '_bottom_iso')) + self.fc.exec_command_test('exteriors %s -outname %s' % (self.gerber_cutout_name + '_bottom_iso', self.gerber_cutout_name + '_bottom_iso_exterior')) + self.fc.exec_command_test('delete %s' % (self.gerber_cutout_name + '_bottom_iso')) + obj = self.fc.collection.get_by_name(self.gerber_cutout_name + '_bottom_iso_exterior') + self.assertTrue(isinstance(obj, FlatCAMGeometry), + "Expected FlatCAMGeometry, instead, %s is %s" % + (self.gerber_cutout_name + '_bottom_iso_exterior', type(obj))) + + # at this stage we should have 5 objects + names = self.fc.collection.get_names() + self.assertEqual(len(names), 5, + "Expected 5 objects, found %d" % len(names)) + + # isolate traces + self.fc.exec_command_test('isolate %s -dia %f' % (self.gerber_top_name, self.engraver_diameter)) + self.fc.exec_command_test('isolate %s -dia %f' % (self.gerber_bottom_name, self.engraver_diameter)) + + # join isolated geometries for top and bottom + self.fc.exec_command_test('join_geometries %s %s %s' % (self.gerber_top_name + '_join_iso', self.gerber_top_name + '_iso', self.gerber_cutout_name + '_iso_exterior')) + self.fc.exec_command_test('join_geometries %s %s %s' % (self.gerber_bottom_name + '_join_iso', self.gerber_bottom_name + '_iso', self.gerber_cutout_name + '_bottom_iso_exterior')) + + # at this stage we should have 9 objects + names = self.fc.collection.get_names() + self.assertEqual(len(names), 9, + "Expected 9 objects, found %d" % len(names)) + + # clean unused isolations + self.fc.exec_command_test('delete %s' % (self.gerber_bottom_name + '_iso')) + self.fc.exec_command_test('delete %s' % (self.gerber_top_name + '_iso')) + self.fc.exec_command_test('delete %s' % (self.gerber_cutout_name + '_iso_exterior')) + self.fc.exec_command_test('delete %s' % (self.gerber_cutout_name + '_bottom_iso_exterior')) + + # at this stage we should have 5 objects again + names = self.fc.collection.get_names() + self.assertEqual(len(names), 5, + "Expected 5 objects, found %d" % len(names)) + + # geocutout bottom test (it cuts to same object) + self.fc.exec_command_test('isolate %s -dia %f -outname %s' % (self.gerber_cutout_name, self.cutout_diameter, self.gerber_cutout_name + '_bottom_iso')) + self.fc.exec_command_test('exteriors %s -outname %s' % (self.gerber_cutout_name + '_bottom_iso', self.gerber_cutout_name + '_bottom_iso_exterior')) + self.fc.exec_command_test('delete %s' % (self.gerber_cutout_name + '_bottom_iso')) + obj = self.fc.collection.get_by_name(self.gerber_cutout_name + '_bottom_iso_exterior') + self.assertTrue(isinstance(obj, FlatCAMGeometry), + "Expected FlatCAMGeometry, instead, %s is %s" % + (self.gerber_cutout_name + '_bottom_iso_exterior', type(obj))) + self.fc.exec_command_test('geocutout %s -dia %f -gapsize 0.3 -gaps 4' % (self.gerber_cutout_name + '_bottom_iso_exterior', self.cutout_diameter)) + + # at this stage we should have 6 objects + names = self.fc.collection.get_names() + self.assertEqual(len(names), 6, + "Expected 6 objects, found %d" % len(names)) + + # TODO: tests for tcl + + def test_excellon_flow(self): + + + self.fc.exec_command_test('set_sys units MM') + self.fc.exec_command_test('open_excellon %s/%s -outname %s' % (self.gerber_files, self.excellon_filename, self.excellon_name)) + excellon_obj = self.fc.collection.get_by_name(self.excellon_name) + self.assertTrue(isinstance(excellon_obj, FlatCAMExcellon), + "Expected FlatCAMExcellon, instead, %s is %s" % + (self.excellon_name, type(excellon_obj))) + + # 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 From 4df46df19b13ba364dacbefa6d0c1a1c96bcc1ae Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Fri, 11 Mar 2016 20:32:48 +0100 Subject: [PATCH 063/134] remove line fix crazzy selfness ;)... --- FlatCAMApp.py | 2 +- tests/test_tcl_shell.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 7729cb6..9f2fb81 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -679,7 +679,7 @@ class App(QtCore.QObject): :return: output if there was any """ - return self.exec_command_test(self, text, False) + return self.exec_command_test(text, False) def exec_command_test(self, text, reraise=True): """ diff --git a/tests/test_tcl_shell.py b/tests/test_tcl_shell.py index 51138b1..3d75f6a 100644 --- a/tests/test_tcl_shell.py +++ b/tests/test_tcl_shell.py @@ -141,7 +141,6 @@ class TclShellCommandTest(unittest.TestCase): def test_excellon_flow(self): - self.fc.exec_command_test('set_sys units MM') self.fc.exec_command_test('open_excellon %s/%s -outname %s' % (self.gerber_files, self.excellon_filename, self.excellon_name)) excellon_obj = self.fc.collection.get_by_name(self.excellon_name) From cd6700152c0b83f3b909583695d6aa2f0b55c911 Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Wed, 16 Mar 2016 18:57:43 +0100 Subject: [PATCH 064/134] draft for reimplementation of tcl commands to separated files/modules --- FlatCAMApp.py | 7 +- tclCommands/TclCommand.py | 180 +++++++++++++++++++++++++++++ tclCommands/TclCommandExteriors.py | 64 ++++++++++ tclCommands/TclCommandInteriors.py | 63 ++++++++++ tclCommands/__init__.py | 46 ++++++++ 5 files changed, 359 insertions(+), 1 deletion(-) create mode 100644 tclCommands/TclCommand.py create mode 100644 tclCommands/TclCommandExteriors.py create mode 100644 tclCommands/TclCommandInteriors.py create mode 100644 tclCommands/__init__.py diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 9f2fb81..5764f33 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -25,7 +25,7 @@ from FlatCAMDraw import FlatCAMDraw from FlatCAMProcess import * from MeasurementTool import Measurement from DblSidedTool import DblSidedTool - +import tclCommands ######################################## ## App ## @@ -660,6 +660,8 @@ class App(QtCore.QObject): if not isinstance(unknownException, self.TclErrorException): self.raiseTclError("Unknown error: %s" % str(unknownException)) + else: + raise unknownException def raiseTclError(self, text): """ @@ -3547,6 +3549,9 @@ class App(QtCore.QObject): } } + #import/overwrite tcl commands as objects of TclCommand descendants + tclCommands.register_all_commands(self, commands) + # Add commands to the tcl interpreter for cmd in commands: self.tcl.createcommand(cmd, commands[cmd]['fcn']) diff --git a/tclCommands/TclCommand.py b/tclCommands/TclCommand.py new file mode 100644 index 0000000..ccc29eb --- /dev/null +++ b/tclCommands/TclCommand.py @@ -0,0 +1,180 @@ +import sys, inspect, pkgutil +import re +import FlatCAMApp + + +class TclCommand(object): + + app=None + + # array of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon) + aliases = None + + # dictionary of types from Tcl command: args = {'name': str}, this is for value without optionname + arg_names = {'name': str} + + # dictionary of types from Tcl command: types = {'outname': str} , this is for options like -optionname value + option_types = {} + + # array of mandatory options for current Tcl command: required = {'name','outname'} + required = ['name'] + + # structured help for current command + help = { + 'main': "undefined help.", + 'args': { + 'argumentname': 'undefined help.', + 'optionname': 'undefined help.' + }, + 'examples' : [] + } + + def __init__(self, app): + self.app=app + + def get_decorated_help(self): + """ + Decorate help for TCL console output. + + :return: decorated help from structue + """ + + def get_decorated_command(alias): + command_string = [] + for key, value in reversed(self.help['args'].items()): + command_string.append(get_decorated_argument(key, value, True)) + return "> " + alias + " " + " ".join(command_string) + + def get_decorated_argument(key, value, in_command=False): + option_symbol = '' + if key in self.arg_names: + type=self.arg_names[key] + in_command_name = "<" + str(type.__name__) + ">" + else: + option_symbol = '-' + type=self.option_types[key] + in_command_name = option_symbol + key + " <" + str(type.__name__) + ">" + if in_command: + if key in self.required: + return in_command_name + else: + return '[' + in_command_name + "]" + else: + if key in self.required: + return "\t" + option_symbol + key + " <" + str(type.__name__) + ">: " + value + else: + return "\t[" + option_symbol + key + " <" + str(type.__name__) + ">: " + value+"]" + + def get_decorated_example(example): + example_string = '' + return "todo" + example_string + + help_string=[self.help['main']] + for alias in self.aliases: + help_string.append(get_decorated_command(alias)) + + for key, value in reversed(self.help['args'].items()): + help_string.append(get_decorated_argument(key, value)) + + for example in self.help['examples']: + help_string.append(get_decorated_example(example)) + + return "\n".join(help_string) + + def parse_arguments(self, args): + """ + Pre-processes arguments to detect '-keyword value' pairs into dictionary + and standalone parameters into list. + + This is copy from FlatCAMApp.setup_shell().h() just for accesibility, original should be removed after all commands will be converted + """ + + options = {} + arguments = [] + n = len(args) + name = None + for i in range(n): + match = re.search(r'^-([a-zA-Z].*)', args[i]) + if match: + assert name is None + name = match.group(1) + continue + + if name is None: + arguments.append(args[i]) + else: + options[name] = args[i] + name = None + + return arguments, options + + def check_args(self, args): + """ + Check arguments and options for right types + + :param args: arguments from tcl to check + :return: + """ + + arguments, options = self.parse_arguments(args) + + named_args={} + unnamed_args=[] + + # check arguments + idx=0 + arg_names_reversed=self.arg_names.items() + for argument in arguments: + if len(self.arg_names) > idx: + key, type = arg_names_reversed[len(self.arg_names)-idx-1] + try: + named_args[key] = type(argument) + except Exception, e: + self.app.raiseTclError("Cannot cast named argument '%s' to type %s." % (key, type)) + else: + unnamed_args.append(argument) + idx += 1 + + # check otions + for key in options: + if key not in self.option_types: + self.app.raiseTclError('Unknown parameter: %s' % key) + try: + named_args[key] = self.option_types[key](options[key]) + except Exception, e: + self.app.raiseTclError("Cannot cast argument '-%s' to type %s." % (key, self.option_types[key])) + + # check required arguments + for key in self.required: + if key not in named_args: + self.app.raiseTclError("Missing required argument '%s'." % (key)) + + return named_args, unnamed_args + + def execute_wrapper(self, *args): + """ + Command which is called by tcl console when current commands aliases are hit. + Main catch(except) is implemented here. + This method should be reimplemented only when initial checking sequence differs + + :param args: arguments passed from tcl command console + :return: None, output text or exception + """ + try: + args, unnamed_args = self.check_args(args) + return self.execute(args, unnamed_args) + except Exception as unknown: + self.app.raiseTclUnknownError(unknown) + + def execute(self, args, unnamed_args): + """ + Direct execute of command, this method should be implemented in each descendant. + No main catch should be implemented here. + + :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, output text or exception + """ + + raise NotImplementedError("Please Implement this method") diff --git a/tclCommands/TclCommandExteriors.py b/tclCommands/TclCommandExteriors.py new file mode 100644 index 0000000..aad0fa2 --- /dev/null +++ b/tclCommands/TclCommandExteriors.py @@ -0,0 +1,64 @@ +from ObjectCollection import * +import TclCommand + + +class TclCommandExteriors(TclCommand.TclCommand): + """ + Tcl shell command to get exteriors of polygons + """ + + # array of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon) + aliases = ['exteriors','ext'] + + # dictionary of types from Tcl command: args = {'name': str}, this is for value without optionname + arg_names = {'name': str,'name2': str} + + # dictionary of types from Tcl command: types = {'outname': str} , this is for options like -optionname value + option_types = {'outname': str} + + # array of mandatory options for current Tcl command: required = {'name','outname'} + required = ['name'] + + # structured help for current command + help = { + 'main': "Get exteriors of polygons.", + 'args': { + 'name': 'Name of the source Geometry object.', + '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 + """ + + name = args['name'] + + if 'outname' in args: + outname = args['outname'] + else: + outname = name + "_exteriors" + + try: + obj = self.app.collection.get_by_name(name) + except: + self.app.raiseTclError("Could not retrieve object: %s" % name) + + if obj is None: + self.app.raiseTclError("Object not found: %s" % name) + + if not isinstance(obj, Geometry): + self.app.raiseTclError('Expected Geometry, got %s %s.' % (name, type(obj))) + + def geo_init(geo_obj, app_obj): + geo_obj.solid_geometry = obj_exteriors + + obj_exteriors = obj.get_exteriors() + self.app.new_object('geometry', outname, geo_init) \ No newline at end of file diff --git a/tclCommands/TclCommandInteriors.py b/tclCommands/TclCommandInteriors.py new file mode 100644 index 0000000..f1c6559 --- /dev/null +++ b/tclCommands/TclCommandInteriors.py @@ -0,0 +1,63 @@ +from ObjectCollection import * +import TclCommand + +class TclCommandInteriors(TclCommand.TclCommand): + """ + Tcl shell command to get interiors of polygons + """ + + # array of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon) + aliases = ['interiors'] + + # dictionary of types from Tcl command: args = {'name': str}, this is for value without optionname + arg_names = {'name': str} + + # dictionary of types from Tcl command: types = {'outname': str} , this is for options like -optionname value + option_types = {'outname': str} + + # array of mandatory options for current Tcl command: required = {'name','outname'} + required = ['name'] + + # structured help for current command + help = { + 'main': "Get interiors of polygons.", + 'args': { + 'name': 'Name of the source Geometry object.', + '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 + """ + + name = args['name'] + + if 'outname' in args: + outname = args['outname'] + else: + outname = name + "_interiors" + + try: + obj = self.app.collection.get_by_name(name) + except: + self.app.raiseTclError("Could not retrieve object: %s" % name) + + if obj is None: + self.app.raiseTclError("Object not found: %s" % name) + + if not isinstance(obj, Geometry): + self.app.raiseTclError('Expected Geometry, got %s %s.' % (name, type(obj))) + + def geo_init(geo_obj, app_obj): + geo_obj.solid_geometry = obj_exteriors + + obj_exteriors = obj.get_interiors() + self.app.new_object('geometry', outname, geo_init) \ No newline at end of file diff --git a/tclCommands/__init__.py b/tclCommands/__init__.py new file mode 100644 index 0000000..43cb0a6 --- /dev/null +++ b/tclCommands/__init__.py @@ -0,0 +1,46 @@ +import pkgutil +import inspect +import sys + +# allowed command modules +import tclCommands.TclCommandExteriors +import tclCommands.TclCommandInteriors + + +__all__=[] + +for loader, name, is_pkg in pkgutil.walk_packages(__path__): + module = loader.find_module(name).load_module(name) + __all__.append(name) + + +def register_all_commands(app, commands): + """ + Static method which register all known commands. + + Command should be for now in directory tclCommands and module should start with TCLCommand + Class have to follow same name as module. + + we need import all modules in top section: + import tclCommands.TclCommandExteriors + at this stage we can include only wanted commands with this, autoloading may be implemented in future + I have no enought knowledge about python's anatomy. Would be nice to include all classes which are descendant etc. + + :param app: FlatCAMApp + :param commands: array of commands which should be modified + :return: None + """ + + tcl_modules = {k: v for k, v in sys.modules.items() if k.startswith('tclCommands.TclCommand')} + + for key, module in tcl_modules.items(): + if key != 'tclCommands.TclCommand': + classname = key.split('.')[1] + class_ = getattr(module, classname) + commandInstance=class_(app) + + for alias in commandInstance.aliases: + commands[alias]={ + 'fcn': commandInstance.execute_wrapper, + 'help': commandInstance.get_decorated_help() + } \ No newline at end of file From 2e51c1e9cd00aa404d426fe291da96c8423b2296 Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Thu, 17 Mar 2016 10:54:01 +0100 Subject: [PATCH 065/134] hide showing 'None' if command end sucessfully --- FlatCAMApp.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 5764f33..263580f 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -698,7 +698,8 @@ class App(QtCore.QObject): try: result = self.tcl.eval(str(text)) - self.shell.append_output(result + '\n') + if result!='None': + self.shell.append_output(result + '\n') except Tkinter.TclError, e: #this will display more precise answer if something in TCL shell fail result = self.tcl.eval("set errorInfo") From 78854f7fe04e6eb98627fe710fa082253ade7121 Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Thu, 17 Mar 2016 12:14:12 +0100 Subject: [PATCH 066/134] fix ordering in naming arguments and help add commands TclCommandAddPolygon(add_poly, add_polygon) and TclCommandAddPolyline(add_polyline) implement add_polyline in camlib.py --- camlib.py | 21 +++++++++ tclCommands/TclCommand.py | 52 ++++++++++++---------- tclCommands/TclCommandAddPolygon.py | 65 ++++++++++++++++++++++++++++ tclCommands/TclCommandAddPolyline.py | 65 ++++++++++++++++++++++++++++ tclCommands/TclCommandExteriors.py | 22 ++++++---- tclCommands/TclCommandInteriors.py | 22 ++++++---- tclCommands/__init__.py | 2 + 7 files changed, 209 insertions(+), 40 deletions(-) create mode 100644 tclCommands/TclCommandAddPolygon.py create mode 100644 tclCommands/TclCommandAddPolyline.py diff --git a/camlib.py b/camlib.py index f576ed9..5a8487a 100644 --- a/camlib.py +++ b/camlib.py @@ -136,6 +136,27 @@ class Geometry(object): log.error("Failed to run union on polygons.") raise + def add_polyline(self, points): + """ + Adds a polyline to the object (by union) + + :param points: The vertices of the polyline. + :return: None + """ + if self.solid_geometry is None: + self.solid_geometry = [] + + if type(self.solid_geometry) is list: + self.solid_geometry.append(LineString(points)) + return + + try: + self.solid_geometry = self.solid_geometry.union(LineString(points)) + except: + #print "Failed to run union on polygons." + log.error("Failed to run union on polylines.") + raise + def subtract_polygon(self, points): """ Subtract polygon from the given object. This only operates on the paths in the original geometry, i.e. it converts polygons into paths. diff --git a/tclCommands/TclCommand.py b/tclCommands/TclCommand.py index ccc29eb..eb8e222 100644 --- a/tclCommands/TclCommand.py +++ b/tclCommands/TclCommand.py @@ -1,31 +1,33 @@ import sys, inspect, pkgutil import re import FlatCAMApp - +import collections class TclCommand(object): app=None # array of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon) - aliases = None + aliases = [] - # dictionary of types from Tcl command: args = {'name': str}, this is for value without optionname - arg_names = {'name': str} + # dictionary of types from Tcl command, needs to be ordered + arg_names = collections.OrderedDict([ + ('name', str) + ]) - # dictionary of types from Tcl command: types = {'outname': str} , this is for options like -optionname value - option_types = {} + # dictionary of types from Tcl command, needs to be ordered , this is for options like -optionname value + option_types = collections.OrderedDict([]) # array of mandatory options for current Tcl command: required = {'name','outname'} required = ['name'] - # structured help for current command + # structured help for current command, args needs to be ordered help = { 'main': "undefined help.", - 'args': { - 'argumentname': 'undefined help.', - 'optionname': 'undefined help.' - }, + 'args': collections.OrderedDict([ + ('argumentname', 'undefined help.'), + ('optionname', 'undefined help.') + ]), 'examples' : [] } @@ -41,7 +43,7 @@ class TclCommand(object): def get_decorated_command(alias): command_string = [] - for key, value in reversed(self.help['args'].items()): + for key, value in self.help['args'].items(): command_string.append(get_decorated_argument(key, value, True)) return "> " + alias + " " + " ".join(command_string) @@ -49,11 +51,18 @@ class TclCommand(object): option_symbol = '' if key in self.arg_names: type=self.arg_names[key] - in_command_name = "<" + str(type.__name__) + ">" - else: + type_name=str(type.__name__) + in_command_name = "<" + type_name + ">" + elif key in self.option_types: option_symbol = '-' type=self.option_types[key] - in_command_name = option_symbol + key + " <" + str(type.__name__) + ">" + type_name=str(type.__name__) + in_command_name = option_symbol + key + " <" + type_name + ">" + else: + option_symbol = '' + type_name='?' + in_command_name = option_symbol + key + " <" + type_name + ">" + if in_command: if key in self.required: return in_command_name @@ -61,19 +70,18 @@ class TclCommand(object): return '[' + in_command_name + "]" else: if key in self.required: - return "\t" + option_symbol + key + " <" + str(type.__name__) + ">: " + value + return "\t" + option_symbol + key + " <" + type_name + ">: " + value else: - return "\t[" + option_symbol + key + " <" + str(type.__name__) + ">: " + value+"]" + return "\t[" + option_symbol + key + " <" + type_name + ">: " + value+"]" def get_decorated_example(example): - example_string = '' - return "todo" + example_string + return "> "+example help_string=[self.help['main']] for alias in self.aliases: help_string.append(get_decorated_command(alias)) - for key, value in reversed(self.help['args'].items()): + for key, value in self.help['args'].items(): help_string.append(get_decorated_argument(key, value)) for example in self.help['examples']: @@ -123,10 +131,10 @@ class TclCommand(object): # check arguments idx=0 - arg_names_reversed=self.arg_names.items() + arg_names_items=self.arg_names.items() for argument in arguments: if len(self.arg_names) > idx: - key, type = arg_names_reversed[len(self.arg_names)-idx-1] + key, type = arg_names_items[idx] try: named_args[key] = type(argument) except Exception, e: diff --git a/tclCommands/TclCommandAddPolygon.py b/tclCommands/TclCommandAddPolygon.py new file mode 100644 index 0000000..b5effcd --- /dev/null +++ b/tclCommands/TclCommandAddPolygon.py @@ -0,0 +1,65 @@ +from ObjectCollection import * +import TclCommand + + +class TclCommandAddPolygon(TclCommand.TclCommand): + """ + Tcl shell command to create a polygon in the given Geometry object + """ + + # array of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon) + aliases = ['add_polygon','add_poly'] + + # dictionary of types from Tcl command, needs to be ordered + arg_names = collections.OrderedDict([ + ('name', str) + ]) + + # dictionary of types from Tcl command, needs to be ordered , this is for options like -optionname value + option_types = collections.OrderedDict([]) + + # array of mandatory options for current Tcl command: required = {'name','outname'} + required = ['name'] + + # structured help for current command, args needs to be ordered + help = { + 'main': "Creates a polygon in the given Geometry object.", + 'args': collections.OrderedDict([ + ('name', 'Name of the Geometry object to which to append the polygon.'), + ('xi, yi', 'Coordinates of points in the polygon.') + ]), + 'examples':[ + 'add_polygon [x3 y3 [...]]' + ] + } + + 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 + """ + + name = args['name'] + + try: + obj = self.app.collection.get_by_name(name) + except: + self.app.raiseTclError("Could not retrieve object: %s" % name) + + if obj is None: + self.app.raiseTclError("Object not found: %s" % name) + + if not isinstance(obj, Geometry): + self.app.raiseTclError('Expected Geometry, got %s %s.' % (name, type(obj))) + + if len(unnamed_args) % 2 != 0: + self.app.raiseTclError("Incomplete coordinates.") + + points = [[float(unnamed_args[2*i]), float(unnamed_args[2*i+1])] for i in range(len(unnamed_args)/2)] + + obj.add_polygon(points) + obj.plot() \ No newline at end of file diff --git a/tclCommands/TclCommandAddPolyline.py b/tclCommands/TclCommandAddPolyline.py new file mode 100644 index 0000000..157f6e1 --- /dev/null +++ b/tclCommands/TclCommandAddPolyline.py @@ -0,0 +1,65 @@ +from ObjectCollection import * +import TclCommand + + +class TclCommandAddPolyline(TclCommand.TclCommand): + """ + Tcl shell command to create a polyline in the given Geometry object + """ + + # array of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon) + aliases = ['add_polyline'] + + # dictionary of types from Tcl command, needs to be ordered + arg_names = collections.OrderedDict([ + ('name', str) + ]) + + # dictionary of types from Tcl command, needs to be ordered , this is for options like -optionname value + option_types = collections.OrderedDict([]) + + # array of mandatory options for current Tcl command: required = {'name','outname'} + required = ['name'] + + # structured help for current command, args needs to be ordered + help = { + 'main': "Creates a polyline in the given Geometry object.", + 'args': collections.OrderedDict([ + ('name', 'Name of the Geometry object to which to append the polyline.'), + ('xi, yi', 'Coordinates of points in the polyline.') + ]), + 'examples':[ + 'add_polyline [x3 y3 [...]]' + ] + } + + 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 + """ + + name = args['name'] + + try: + obj = self.app.collection.get_by_name(name) + except: + self.app.raiseTclError("Could not retrieve object: %s" % name) + + if obj is None: + self.app.raiseTclError("Object not found: %s" % name) + + if not isinstance(obj, Geometry): + self.app.raiseTclError('Expected Geometry, got %s %s.' % (name, type(obj))) + + if len(unnamed_args) % 2 != 0: + self.app.raiseTclError("Incomplete coordinates.") + + points = [[float(unnamed_args[2*i]), float(unnamed_args[2*i+1])] for i in range(len(unnamed_args)/2)] + + obj.add_polyline(points) + obj.plot() \ No newline at end of file diff --git a/tclCommands/TclCommandExteriors.py b/tclCommands/TclCommandExteriors.py index aad0fa2..d445cd5 100644 --- a/tclCommands/TclCommandExteriors.py +++ b/tclCommands/TclCommandExteriors.py @@ -10,22 +10,26 @@ class TclCommandExteriors(TclCommand.TclCommand): # array of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon) aliases = ['exteriors','ext'] - # dictionary of types from Tcl command: args = {'name': str}, this is for value without optionname - arg_names = {'name': str,'name2': str} + # dictionary of types from Tcl command, needs to be ordered + arg_names = collections.OrderedDict([ + ('name', str) + ]) - # dictionary of types from Tcl command: types = {'outname': str} , this is for options like -optionname value - option_types = {'outname': str} + # dictionary of types from Tcl command, needs to be ordered , this is for options like -optionname value + option_types = collections.OrderedDict([ + ('outname', str) + ]) # array of mandatory options for current Tcl command: required = {'name','outname'} required = ['name'] - # structured help for current command + # structured help for current command, args needs to be ordered help = { 'main': "Get exteriors of polygons.", - 'args': { - 'name': 'Name of the source Geometry object.', - 'outname': 'Name of the resulting Geometry object.' - }, + 'args': collections.OrderedDict([ + ('name', 'Name of the source Geometry object.'), + ('outname', 'Name of the resulting Geometry object.') + ]), 'examples':[] } diff --git a/tclCommands/TclCommandInteriors.py b/tclCommands/TclCommandInteriors.py index f1c6559..ef67ce9 100644 --- a/tclCommands/TclCommandInteriors.py +++ b/tclCommands/TclCommandInteriors.py @@ -9,22 +9,26 @@ class TclCommandInteriors(TclCommand.TclCommand): # array of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon) aliases = ['interiors'] - # dictionary of types from Tcl command: args = {'name': str}, this is for value without optionname - arg_names = {'name': str} + # dictionary of types from Tcl command, needs to be ordered + arg_names = collections.OrderedDict([ + ('name', str) + ]) - # dictionary of types from Tcl command: types = {'outname': str} , this is for options like -optionname value - option_types = {'outname': str} + # dictionary of types from Tcl command, needs to be ordered , this is for options like -optionname value + option_types = collections.OrderedDict([ + ('outname', str) + ]) # array of mandatory options for current Tcl command: required = {'name','outname'} required = ['name'] - # structured help for current command + # structured help for current command, args needs to be ordered help = { 'main': "Get interiors of polygons.", - 'args': { - 'name': 'Name of the source Geometry object.', - 'outname': 'Name of the resulting Geometry object.' - }, + 'args': collections.OrderedDict([ + ('name', 'Name of the source Geometry object.'), + ('outname', 'Name of the resulting Geometry object.') + ]), 'examples':[] } diff --git a/tclCommands/__init__.py b/tclCommands/__init__.py index 43cb0a6..45d0ffc 100644 --- a/tclCommands/__init__.py +++ b/tclCommands/__init__.py @@ -5,6 +5,8 @@ import sys # allowed command modules import tclCommands.TclCommandExteriors import tclCommands.TclCommandInteriors +import tclCommands.TclCommandAddPolygon +import tclCommands.TclCommandAddPolyline __all__=[] From a6f150a01d6228289408e1dd4db5c879da006455 Mon Sep 17 00:00:00 2001 From: Juan Pablo Caram Date: Thu, 17 Mar 2016 17:33:34 -0400 Subject: [PATCH 067/134] Blocking in shell functions. See #196. --- FlatCAMApp.py | 69 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 711a6d6..c9b4bdd 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -2031,6 +2031,71 @@ class App(QtCore.QObject): return a, kwa + from contextlib import contextmanager + @contextmanager + def wait_signal(signal, timeout=10000): + """Block loop until signal emitted, or timeout (ms) elapses.""" + loop = QtCore.QEventLoop() + signal.connect(loop.quit) + + status = {'timed_out': False} + + def report_quit(): + status['timed_out'] = True + loop.quit() + + yield + + if timeout is not None: + QtCore.QTimer.singleShot(timeout, report_quit) + + loop.exec_() + + if status['timed_out']: + raise Exception('Timed out!') + + def wait_signal2(signal, timeout=10000): + """Block loop until signal emitted, or timeout (ms) elapses.""" + loop = QtCore.QEventLoop() + signal.connect(loop.quit) + status = {'timed_out': False} + + def report_quit(): + status['timed_out'] = True + loop.quit() + + if timeout is not None: + QtCore.QTimer.singleShot(timeout, report_quit) + loop.exec_() + + if status['timed_out']: + raise Exception('Timed out!') + + def mytest(*args): + to = int(args[0]) + + try: + for rec in self.recent: + if rec['kind'] == 'gerber': + self.open_gerber(str(rec['filename'])) + break + + basename = self.collection.get_names()[0] + isolate(basename, '-passes', '10', '-combine', '1') + iso = self.collection.get_by_name(basename + "_iso") + + with wait_signal(self.new_object_available, to): + iso.generatecncjob() + # iso.generatecncjob() + # wait_signal2(self.new_object_available, to) + + return str(self.collection.get_names()) + + except Exception as e: + return str(e) + + return str(self.collection.get_names()) + def import_svg(filename, *args): a, kwa = h(*args) types = {'outname': str} @@ -3016,6 +3081,10 @@ class App(QtCore.QObject): return "ERROR: No such system parameter." commands = { + 'mytest': { + 'fcn': mytest, + 'help': "Test function. Only for testing." + }, 'help': { 'fcn': shelp, 'help': "Shows list of commands." From 980638630d53ead63f9c1952be74eb9ccaa2743c Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Sat, 19 Mar 2016 15:13:07 +0100 Subject: [PATCH 068/134] cleanups implement TclCommand.TclCommandSignaled as proof of concept (not usefull) bypass using threads within obj.generatecncjob(use_thread = False, **args) reimplement some more shell commands to OOP style --- FlatCAMApp.py | 102 +++++++------- FlatCAMObj.py | 31 +++-- tclCommands/TclCommand.py | 190 +++++++++++++++++++++------ tclCommands/TclCommandAddPolygon.py | 20 ++- tclCommands/TclCommandAddPolyline.py | 18 +-- tclCommands/TclCommandCncjob.py | 86 ++++++++++++ tclCommands/TclCommandExportGcode.py | 79 +++++++++++ tclCommands/TclCommandExteriors.py | 18 +-- tclCommands/TclCommandInteriors.py | 17 +-- tclCommands/__init__.py | 28 ++-- 10 files changed, 427 insertions(+), 162 deletions(-) create mode 100644 tclCommands/TclCommandCncjob.py create mode 100644 tclCommands/TclCommandExportGcode.py diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 0090ad0..840cf42 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -651,7 +651,7 @@ class App(QtCore.QObject): pass - def raiseTclUnknownError(self, unknownException): + def raise_tcl_unknown_error(self, unknownException): """ raise Exception if is different type than TclErrorException :param unknownException: @@ -659,11 +659,11 @@ class App(QtCore.QObject): """ if not isinstance(unknownException, self.TclErrorException): - self.raiseTclError("Unknown error: %s" % str(unknownException)) + self.raise_tcl_error("Unknown error: %s" % str(unknownException)) else: raise unknownException - def raiseTclError(self, text): + def raise_tcl_error(self, text): """ this method pass exception from python into TCL as error, so we get stacktrace and reason :param text: text of error @@ -2274,20 +2274,20 @@ class App(QtCore.QObject): # 8 - 2*left + 2*right +2*top + 2*bottom if name is None: - self.raiseTclError('Argument name is missing.') + self.raise_tcl_error('Argument name is missing.') for key in kwa: if key not in types: - self.raiseTclError('Unknown parameter: %s' % key) + self.raise_tcl_error('Unknown parameter: %s' % key) try: kwa[key] = types[key](kwa[key]) except Exception, e: - self.raiseTclError("Cannot cast argument '%s' to type %s." % (key, str(types[key]))) + self.raise_tcl_error("Cannot cast argument '%s' to type %s." % (key, str(types[key]))) try: obj = self.collection.get_by_name(str(name)) except: - self.raiseTclError("Could not retrieve object: %s" % name) + self.raise_tcl_error("Could not retrieve object: %s" % name) # Get min and max data for each object as we just cut rectangles across X or Y xmin, ymin, xmax, ymax = obj.bounds() @@ -2337,7 +2337,7 @@ class App(QtCore.QObject): ymax + gapsize) except Exception as unknown: - self.raiseTclUnknownError(unknown) + self.raise_tcl_unknown_error(unknown) def mirror(name, *args): a, kwa = h(*args) @@ -2624,26 +2624,26 @@ class App(QtCore.QObject): } if name is None: - self.raiseTclError('Argument name is missing.') + self.raise_tcl_error('Argument name is missing.') for key in kwa: if key not in types: - self.raiseTclError('Unknown parameter: %s' % key) + self.raise_tcl_error('Unknown parameter: %s' % key) try: kwa[key] = types[key](kwa[key]) except Exception, e: - self.raiseTclError("Cannot cast argument '%s' to type %s." % (key, str(types[key]))) + self.raise_tcl_error("Cannot cast argument '%s' to type %s." % (key, str(types[key]))) try: obj = self.collection.get_by_name(str(name)) except: - self.raiseTclError("Could not retrieve object: %s" % name) + self.raise_tcl_error("Could not retrieve object: %s" % name) if obj is None: - self.raiseTclError('Object not found: %s' % name) + self.raise_tcl_error('Object not found: %s' % name) if not isinstance(obj, FlatCAMExcellon): - self.raiseTclError('Only Excellon objects can be drilled, got %s %s.' % (name, type(obj))) + self.raise_tcl_error('Only Excellon objects can be drilled, got %s %s.' % (name, type(obj))) try: # Get the tools from the list @@ -2663,10 +2663,10 @@ class App(QtCore.QObject): obj.app.new_object("cncjob", job_name, job_init) except Exception, e: - self.raiseTclError("Operation failed: %s" % str(e)) + self.raise_tcl_error("Operation failed: %s" % str(e)) except Exception as unknown: - self.raiseTclUnknownError(unknown) + self.raise_tcl_unknown_error(unknown) def millholes(name=None, *args): @@ -2684,43 +2684,43 @@ class App(QtCore.QObject): 'outname': str} if name is None: - self.raiseTclError('Argument name is missing.') + self.raise_tcl_error('Argument name is missing.') for key in kwa: if key not in types: - self.raiseTclError('Unknown parameter: %s' % key) + self.raise_tcl_error('Unknown parameter: %s' % key) try: kwa[key] = types[key](kwa[key]) except Exception, e: - self.raiseTclError("Cannot cast argument '%s' to type %s." % (key, types[key])) + self.raise_tcl_error("Cannot cast argument '%s' to type %s." % (key, types[key])) try: if 'tools' in kwa: kwa['tools'] = [x.strip() for x in kwa['tools'].split(",")] except Exception as e: - self.raiseTclError("Bad tools: %s" % str(e)) + self.raise_tcl_error("Bad tools: %s" % str(e)) try: obj = self.collection.get_by_name(str(name)) except: - self.raiseTclError("Could not retrieve object: %s" % name) + self.raise_tcl_error("Could not retrieve object: %s" % name) if obj is None: - self.raiseTclError("Object not found: %s" % name) + self.raise_tcl_error("Object not found: %s" % name) if not isinstance(obj, FlatCAMExcellon): - self.raiseTclError('Only Excellon objects can be mill drilled, got %s %s.' % (name, type(obj))) + self.raise_tcl_error('Only Excellon objects can be mill drilled, got %s %s.' % (name, type(obj))) try: success, msg = obj.generate_milling(**kwa) except Exception as e: - self.raiseTclError("Operation failed: %s" % str(e)) + self.raise_tcl_error("Operation failed: %s" % str(e)) if not success: - self.raiseTclError(msg) + self.raise_tcl_error(msg) except Exception as unknown: - self.raiseTclUnknownError(unknown) + self.raise_tcl_unknown_error(unknown) def exteriors(name=None, *args): ''' @@ -2735,26 +2735,26 @@ class App(QtCore.QObject): types = {'outname': str} if name is None: - self.raiseTclError('Argument name is missing.') + self.raise_tcl_error('Argument name is missing.') for key in kwa: if key not in types: - self.raiseTclError('Unknown parameter: %s' % key) + self.raise_tcl_error('Unknown parameter: %s' % key) try: kwa[key] = types[key](kwa[key]) except Exception, e: - self.raiseTclError("Cannot cast argument '%s' to type %s." % (key, types[key])) + self.raise_tcl_error("Cannot cast argument '%s' to type %s." % (key, types[key])) try: obj = self.collection.get_by_name(str(name)) except: - self.raiseTclError("Could not retrieve object: %s" % name) + self.raise_tcl_error("Could not retrieve object: %s" % name) if obj is None: - self.raiseTclError("Object not found: %s" % name) + self.raise_tcl_error("Object not found: %s" % name) if not isinstance(obj, Geometry): - self.raiseTclError('Expected Geometry, got %s %s.' % (name, type(obj))) + self.raise_tcl_error('Expected Geometry, got %s %s.' % (name, type(obj))) def geo_init(geo_obj, app_obj): geo_obj.solid_geometry = obj_exteriors @@ -2768,10 +2768,10 @@ class App(QtCore.QObject): obj_exteriors = obj.get_exteriors() self.new_object('geometry', outname, geo_init) except Exception as e: - self.raiseTclError("Failed: %s" % str(e)) + self.raise_tcl_error("Failed: %s" % str(e)) except Exception as unknown: - self.raiseTclUnknownError(unknown) + self.raise_tcl_unknown_error(unknown) def interiors(name=None, *args): ''' @@ -2787,25 +2787,25 @@ class App(QtCore.QObject): for key in kwa: if key not in types: - self.raiseTclError('Unknown parameter: %s' % key) + self.raise_tcl_error('Unknown parameter: %s' % key) try: kwa[key] = types[key](kwa[key]) except Exception, e: - self.raiseTclError("Cannot cast argument '%s' to type %s." % (key, types[key])) + self.raise_tcl_error("Cannot cast argument '%s' to type %s." % (key, types[key])) if name is None: - self.raiseTclError('Argument name is missing.') + self.raise_tcl_error('Argument name is missing.') try: obj = self.collection.get_by_name(str(name)) except: - self.raiseTclError("Could not retrieve object: %s" % name) + self.raise_tcl_error("Could not retrieve object: %s" % name) if obj is None: - self.raiseTclError("Object not found: %s" % name) + self.raise_tcl_error("Object not found: %s" % name) if not isinstance(obj, Geometry): - self.raiseTclError('Expected Geometry, got %s %s.' % (name, type(obj))) + self.raise_tcl_error('Expected Geometry, got %s %s.' % (name, type(obj))) def geo_init(geo_obj, app_obj): geo_obj.solid_geometry = obj_interiors @@ -2819,10 +2819,10 @@ class App(QtCore.QObject): obj_interiors = obj.get_interiors() self.new_object('geometry', outname, geo_init) except Exception as e: - self.raiseTclError("Failed: %s" % str(e)) + self.raise_tcl_error("Failed: %s" % str(e)) except Exception as unknown: - self.raiseTclUnknownError(unknown) + self.raise_tcl_unknown_error(unknown) def isolate(name=None, *args): ''' @@ -2840,29 +2840,29 @@ class App(QtCore.QObject): for key in kwa: if key not in types: - self.raiseTclError('Unknown parameter: %s' % key) + self.raise_tcl_error('Unknown parameter: %s' % key) try: kwa[key] = types[key](kwa[key]) except Exception, e: - self.raiseTclError("Cannot cast argument '%s' to type %s." % (key, types[key])) + self.raise_tcl_error("Cannot cast argument '%s' to type %s." % (key, types[key])) try: obj = self.collection.get_by_name(str(name)) except: - self.raiseTclError("Could not retrieve object: %s" % name) + self.raise_tcl_error("Could not retrieve object: %s" % name) if obj is None: - self.raiseTclError("Object not found: %s" % name) + self.raise_tcl_error("Object not found: %s" % name) assert isinstance(obj, FlatCAMGerber), \ "Expected a FlatCAMGerber, got %s" % type(obj) if not isinstance(obj, FlatCAMGerber): - self.raiseTclError('Expected FlatCAMGerber, got %s %s.' % (name, type(obj))) + self.raise_tcl_error('Expected FlatCAMGerber, got %s %s.' % (name, type(obj))) try: obj.isolate(**kwa) except Exception, e: - self.raiseTclError("Operation failed: %s" % str(e)) + self.raise_tcl_error("Operation failed: %s" % str(e)) return 'Ok' @@ -3253,11 +3253,11 @@ class App(QtCore.QObject): Test it like this: if name is None: - self.raiseTclError('Argument name is missing.') + self.raise_tcl_error('Argument name is missing.') - When error ocurre, always use raiseTclError, never return "sometext" on error, + When error ocurre, always use raise_tcl_error, never return "sometext" on error, otherwise we will miss it and processing will silently continue. - Method raiseTclError pass error into TCL interpreter, then raise python exception, + Method raise_tcl_error pass error into TCL interpreter, then raise python exception, which is catched in exec_command and displayed in TCL shell console with red background. Error in console is displayed with TCL trace. diff --git a/FlatCAMObj.py b/FlatCAMObj.py index b8315fc..b475692 100644 --- a/FlatCAMObj.py +++ b/FlatCAMObj.py @@ -1040,6 +1040,10 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob): self.app.inform.emit("Saved to: " + filename) + def get_gcode(self, preamble='', postamble=''): + #we need this to beable get_gcode separatelly for shell command export_code + return preamble + '\n' + self.gcode + "\n" + postamble + def on_plot_cb_click(self, *args): if self.muted_ui: return @@ -1243,7 +1247,8 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): outname=None, spindlespeed=None, multidepth=None, - depthperpass=None): + depthperpass=None, + use_thread=True): """ Creates a CNCJob out of this Geometry object. The actual work is done by the target FlatCAMCNCjob object's @@ -1304,18 +1309,22 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): app_obj.progress.emit(80) - # To be run in separate thread - def job_thread(app_obj): - with self.app.proc_container.new("Generating CNC Job."): - app_obj.new_object("cncjob", outname, job_init) - app_obj.inform.emit("CNCjob created: %s" % outname) - app_obj.progress.emit(100) - # Create a promise with the name - self.app.collection.promise(outname) + if use_thread: + # To be run in separate thread + def job_thread(app_obj): + with self.app.proc_container.new("Generating CNC Job."): + app_obj.new_object("cncjob", outname, job_init) + app_obj.inform.emit("CNCjob created: %s" % outname) + app_obj.progress.emit(100) - # Send to worker - self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]}) + # Create a promise with the name + self.app.collection.promise(outname) + + # Send to worker + self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]}) + else: + self.app.new_object("cncjob", outname, job_init) def on_plot_cb_click(self, *args): # TODO: args not needed if self.muted_ui: diff --git a/tclCommands/TclCommand.py b/tclCommands/TclCommand.py index eb8e222..7f8a7e8 100644 --- a/tclCommands/TclCommand.py +++ b/tclCommands/TclCommand.py @@ -1,83 +1,108 @@ -import sys, inspect, pkgutil import re import FlatCAMApp +import abc import collections +from PyQt4 import QtCore +from contextlib import contextmanager + class TclCommand(object): - app=None + # FlatCAMApp + app = None + + # logger + log = None # array of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon) aliases = [] # dictionary of types from Tcl command, needs to be ordered + # OrderedDict should be like collections.OrderedDict([(key,value),(key2,value2)]) arg_names = collections.OrderedDict([ ('name', str) ]) # dictionary of types from Tcl command, needs to be ordered , this is for options like -optionname value - option_types = collections.OrderedDict([]) + # OrderedDict should be like collections.OrderedDict([(key,value),(key2,value2)]) + option_types = collections.OrderedDict() # array of mandatory options for current Tcl command: required = {'name','outname'} required = ['name'] # structured help for current command, args needs to be ordered + # OrderedDict should be like collections.OrderedDict([(key,value),(key2,value2)]) help = { 'main': "undefined help.", 'args': collections.OrderedDict([ ('argumentname', 'undefined help.'), ('optionname', 'undefined help.') ]), - 'examples' : [] + 'examples': [] } def __init__(self, app): - self.app=app + self.app = app + if self.app is None: + raise TypeError('Expected app to be FlatCAMApp instance.') + if not isinstance(self.app, FlatCAMApp.App): + raise TypeError('Expected FlatCAMApp, got %s.' % type(app)) + self.log = self.app.log + + def raise_tcl_error(self, text): + """ + this method pass exception from python into TCL as error, so we get stacktrace and reason + this is only redirect to self.app.raise_tcl_error + :param text: text of error + :return: none + """ + + self.app.raise_tcl_error(text) def get_decorated_help(self): """ Decorate help for TCL console output. - :return: decorated help from structue + :return: decorated help from structure """ - def get_decorated_command(alias): + def get_decorated_command(alias_name): command_string = [] - for key, value in self.help['args'].items(): - command_string.append(get_decorated_argument(key, value, True)) - return "> " + alias + " " + " ".join(command_string) + for arg_key, arg_type in self.help['args'].items(): + command_string.append(get_decorated_argument(arg_key, arg_type, True)) + return "> " + alias_name + " " + " ".join(command_string) - def get_decorated_argument(key, value, in_command=False): + def get_decorated_argument(help_key, help_text, in_command=False): option_symbol = '' - if key in self.arg_names: - type=self.arg_names[key] - type_name=str(type.__name__) + if help_key in self.arg_names: + arg_type = self.arg_names[help_key] + type_name = str(arg_type.__name__) in_command_name = "<" + type_name + ">" - elif key in self.option_types: + elif help_key in self.option_types: option_symbol = '-' - type=self.option_types[key] - type_name=str(type.__name__) - in_command_name = option_symbol + key + " <" + type_name + ">" + arg_type = self.option_types[help_key] + type_name = str(arg_type.__name__) + in_command_name = option_symbol + help_key + " <" + type_name + ">" else: option_symbol = '' - type_name='?' - in_command_name = option_symbol + key + " <" + type_name + ">" + type_name = '?' + in_command_name = option_symbol + help_key + " <" + type_name + ">" if in_command: - if key in self.required: + if help_key in self.required: return in_command_name else: return '[' + in_command_name + "]" else: - if key in self.required: - return "\t" + option_symbol + key + " <" + type_name + ">: " + value + if help_key in self.required: + return "\t" + option_symbol + help_key + " <" + type_name + ">: " + help_text else: - return "\t[" + option_symbol + key + " <" + type_name + ">: " + value+"]" + return "\t[" + option_symbol + help_key + " <" + type_name + ">: " + help_text + "]" - def get_decorated_example(example): - return "> "+example + def get_decorated_example(example_item): + return "> "+example_item - help_string=[self.help['main']] + help_string = [self.help['main']] for alias in self.aliases: help_string.append(get_decorated_command(alias)) @@ -89,12 +114,17 @@ class TclCommand(object): return "\n".join(help_string) - def parse_arguments(self, args): + @staticmethod + def parse_arguments(args): """ Pre-processes arguments to detect '-keyword value' pairs into dictionary and standalone parameters into list. - This is copy from FlatCAMApp.setup_shell().h() just for accesibility, original should be removed after all commands will be converted + This is copy from FlatCAMApp.setup_shell().h() just for accessibility, + original should be removed after all commands will be converted + + :param args: arguments from tcl to parse + :return: arguments, options """ options = {} @@ -121,41 +151,43 @@ class TclCommand(object): Check arguments and options for right types :param args: arguments from tcl to check - :return: + :return: named_args, unnamed_args """ arguments, options = self.parse_arguments(args) - named_args={} - unnamed_args=[] + named_args = {} + unnamed_args = [] # check arguments - idx=0 - arg_names_items=self.arg_names.items() + idx = 0 + arg_names_items = self.arg_names.items() for argument in arguments: if len(self.arg_names) > idx: - key, type = arg_names_items[idx] + key, arg_type = arg_names_items[idx] try: - named_args[key] = type(argument) + named_args[key] = arg_type(argument) except Exception, e: - self.app.raiseTclError("Cannot cast named argument '%s' to type %s." % (key, type)) + self.raise_tcl_error("Cannot cast named argument '%s' to type %s with exception '%s'." + % (key, arg_type, str(e))) else: unnamed_args.append(argument) idx += 1 - # check otions + # check options for key in options: if key not in self.option_types: - self.app.raiseTclError('Unknown parameter: %s' % key) + self.raise_tcl_error('Unknown parameter: %s' % key) try: named_args[key] = self.option_types[key](options[key]) except Exception, e: - self.app.raiseTclError("Cannot cast argument '-%s' to type %s." % (key, self.option_types[key])) + self.raise_tcl_error("Cannot cast argument '-%s' to type '%s' with exception '%s'." + % (key, self.option_types[key], str(e))) # check required arguments for key in self.required: if key not in named_args: - self.app.raiseTclError("Missing required argument '%s'." % (key)) + self.raise_tcl_error("Missing required argument '%s'." % key) return named_args, unnamed_args @@ -168,12 +200,16 @@ class TclCommand(object): :param args: arguments passed from tcl command console :return: None, output text or exception """ + try: + self.log.debug("TCL command '%s' executed." % str(self.__class__)) args, unnamed_args = self.check_args(args) return self.execute(args, unnamed_args) except Exception as unknown: - self.app.raiseTclUnknownError(unknown) + self.log.error("TCL command '%s' failed." % str(self)) + self.app.raise_tcl_unknown_error(unknown) + @abc.abstractmethod def execute(self, args, unnamed_args): """ Direct execute of command, this method should be implemented in each descendant. @@ -186,3 +222,73 @@ class TclCommand(object): """ raise NotImplementedError("Please Implement this method") + + +class TclCommandSignaled(TclCommand): + """ + !!! I left it here only for demonstration !!! + Go to TclCommandCncjob and into class definition put + class TclCommandCncjob(TclCommand.TclCommandSignaled): + also change + obj.generatecncjob(use_thread = False, **args) + to + obj.generatecncjob(use_thread = True, **args) + + + This class is child of TclCommand and is used for commands which create new objects + it handles all neccessary stuff about blocking and passing exeptions + """ + + # default timeout for operation is 30 sec, but it can be much more + default_timeout = 30000 + + + def execute_wrapper(self, *args): + """ + Command which is called by tcl console when current commands aliases are hit. + Main catch(except) is implemented here. + This method should be reimplemented only when initial checking sequence differs + + :param args: arguments passed from tcl command console + :return: None, output text or exception + """ + + @contextmanager + def wait_signal(signal, timeout=30000): + """Block loop until signal emitted, or timeout (ms) elapses.""" + loop = QtCore.QEventLoop() + signal.connect(loop.quit) + + status = {'timed_out': False} + + def report_quit(): + status['timed_out'] = True + loop.quit() + + yield + + if timeout is not None: + QtCore.QTimer.singleShot(timeout, report_quit) + + loop.exec_() + + if status['timed_out']: + self.app.raise_tcl_unknown_error('Operation timed out!') + + try: + self.log.debug("TCL command '%s' executed." % str(self.__class__)) + args, unnamed_args = self.check_args(args) + if 'timeout' in args: + passed_timeout=args['timeout'] + del args['timeout'] + else: + passed_timeout=self.default_timeout + with wait_signal(self.app.new_object_available, passed_timeout): + # every TclCommandNewObject ancestor support timeout as parameter, + # but it does not mean anything for child itself + # when operation will be really long is good to set it higher then defqault 30s + return self.execute(args, unnamed_args) + + except Exception as unknown: + self.log.error("TCL command '%s' failed." % str(self)) + self.app.raise_tcl_unknown_error(unknown) \ No newline at end of file diff --git a/tclCommands/TclCommandAddPolygon.py b/tclCommands/TclCommandAddPolygon.py index b5effcd..6d2c2af 100644 --- a/tclCommands/TclCommandAddPolygon.py +++ b/tclCommands/TclCommandAddPolygon.py @@ -8,7 +8,7 @@ class TclCommandAddPolygon(TclCommand.TclCommand): """ # array of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon) - aliases = ['add_polygon','add_poly'] + aliases = ['add_polygon', 'add_poly'] # dictionary of types from Tcl command, needs to be ordered arg_names = collections.OrderedDict([ @@ -16,7 +16,7 @@ class TclCommandAddPolygon(TclCommand.TclCommand): ]) # dictionary of types from Tcl command, needs to be ordered , this is for options like -optionname value - option_types = collections.OrderedDict([]) + option_types = collections.OrderedDict() # array of mandatory options for current Tcl command: required = {'name','outname'} required = ['name'] @@ -28,7 +28,7 @@ class TclCommandAddPolygon(TclCommand.TclCommand): ('name', 'Name of the Geometry object to which to append the polygon.'), ('xi, yi', 'Coordinates of points in the polygon.') ]), - 'examples':[ + 'examples': [ 'add_polygon [x3 y3 [...]]' ] } @@ -45,21 +45,17 @@ class TclCommandAddPolygon(TclCommand.TclCommand): name = args['name'] - try: - obj = self.app.collection.get_by_name(name) - except: - self.app.raiseTclError("Could not retrieve object: %s" % name) - + obj = self.app.collection.get_by_name(name) if obj is None: - self.app.raiseTclError("Object not found: %s" % name) + self.raise_tcl_error("Object not found: %s" % name) if not isinstance(obj, Geometry): - self.app.raiseTclError('Expected Geometry, got %s %s.' % (name, type(obj))) + self.raise_tcl_error('Expected Geometry, got %s %s.' % (name, type(obj))) if len(unnamed_args) % 2 != 0: - self.app.raiseTclError("Incomplete coordinates.") + self.raise_tcl_error("Incomplete coordinates.") points = [[float(unnamed_args[2*i]), float(unnamed_args[2*i+1])] for i in range(len(unnamed_args)/2)] obj.add_polygon(points) - obj.plot() \ No newline at end of file + obj.plot() diff --git a/tclCommands/TclCommandAddPolyline.py b/tclCommands/TclCommandAddPolyline.py index 157f6e1..57b8fe0 100644 --- a/tclCommands/TclCommandAddPolyline.py +++ b/tclCommands/TclCommandAddPolyline.py @@ -16,7 +16,7 @@ class TclCommandAddPolyline(TclCommand.TclCommand): ]) # dictionary of types from Tcl command, needs to be ordered , this is for options like -optionname value - option_types = collections.OrderedDict([]) + option_types = collections.OrderedDict() # array of mandatory options for current Tcl command: required = {'name','outname'} required = ['name'] @@ -28,7 +28,7 @@ class TclCommandAddPolyline(TclCommand.TclCommand): ('name', 'Name of the Geometry object to which to append the polyline.'), ('xi, yi', 'Coordinates of points in the polyline.') ]), - 'examples':[ + 'examples': [ 'add_polyline [x3 y3 [...]]' ] } @@ -45,21 +45,17 @@ class TclCommandAddPolyline(TclCommand.TclCommand): name = args['name'] - try: - obj = self.app.collection.get_by_name(name) - except: - self.app.raiseTclError("Could not retrieve object: %s" % name) - + obj = self.app.collection.get_by_name(name) if obj is None: - self.app.raiseTclError("Object not found: %s" % name) + self.raise_tcl_error("Object not found: %s" % name) if not isinstance(obj, Geometry): - self.app.raiseTclError('Expected Geometry, got %s %s.' % (name, type(obj))) + self.raise_tcl_error('Expected Geometry, got %s %s.' % (name, type(obj))) if len(unnamed_args) % 2 != 0: - self.app.raiseTclError("Incomplete coordinates.") + self.raise_tcl_error("Incomplete coordinates.") points = [[float(unnamed_args[2*i]), float(unnamed_args[2*i+1])] for i in range(len(unnamed_args)/2)] obj.add_polyline(points) - obj.plot() \ No newline at end of file + obj.plot() diff --git a/tclCommands/TclCommandCncjob.py b/tclCommands/TclCommandCncjob.py new file mode 100644 index 0000000..17a677e --- /dev/null +++ b/tclCommands/TclCommandCncjob.py @@ -0,0 +1,86 @@ +from ObjectCollection import * +import TclCommand + + +class TclCommandCncjob(TclCommand.TclCommand): + """ + Tcl shell command to Generates a CNC Job from a Geometry Object. + + example: + set_sys units MM + new + open_gerber tests/gerber_files/simple1.gbr -outname margin + isolate margin -dia 3 + cncjob margin_iso + """ + + # array of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon) + aliases = ['cncjob'] + + # dictionary of types from Tcl command, needs to be ordered + arg_names = collections.OrderedDict([ + ('name', str) + ]) + + # dictionary of types from Tcl command, needs to be ordered , this is for options like -optionname value + option_types = collections.OrderedDict([ + ('z_cut',float), + ('z_move',float), + ('feedrate',float), + ('tooldia',float), + ('spindlespeed',int), + ('multidepth',bool), + ('depthperpass',float), + ('outname',str) + ]) + + # array of mandatory options for current Tcl command: required = {'name','outname'} + required = ['name'] + + # structured help for current command, args needs to be ordered + help = { + 'main': "Generates a CNC Job from a Geometry Object.", + 'args': collections.OrderedDict([ + ('name', 'Name of the source object.'), + ('z_cut', 'Z-axis cutting position.'), + ('z_move', 'Z-axis moving position.'), + ('feedrate', 'Moving speed when cutting.'), + ('tooldia', 'Tool diameter to show on screen.'), + ('spindlespeed', 'Speed of the spindle in rpm (example: 4000).'), + ('multidepth', 'Use or not multidepth cnccut.'), + ('depthperpass', 'Height of one layer for multidepth.'), + ('outname', 'Name of the resulting Geometry object.'), + ('timeout', 'Max wait for job timeout before error.') + ]), + '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 + """ + + name = args['name'] + + if 'outname' not in args: + args['outname'] = name + "_cnc" + + if 'timeout' in args: + timeout = args['timeout'] + else: + timeout = 10000 + + obj = self.app.collection.get_by_name(name) + if obj is None: + self.raise_tcl_error("Object not found: %s" % name) + + if not isinstance(obj, FlatCAMGeometry): + self.raise_tcl_error('Expected FlatCAMGeometry, got %s %s.' % (name, type(obj))) + + del args['name'] + obj.generatecncjob(use_thread = False, **args) \ No newline at end of file diff --git a/tclCommands/TclCommandExportGcode.py b/tclCommands/TclCommandExportGcode.py new file mode 100644 index 0000000..520f6ec --- /dev/null +++ b/tclCommands/TclCommandExportGcode.py @@ -0,0 +1,79 @@ +from ObjectCollection import * +import TclCommand + + +class TclCommandExportGcode(TclCommand.TclCommand): + """ + Tcl shell command to export gcode as tcl output for "set X [export_gcode ...]" + + Requires name to be available. It might still be in the + making at the time this function is called, so check for + promises and send to background if there are promises. + + + this export may be catched by tcl and past as preable to another export_gcode or write_gcode + this can be used to join GCODES + + example: + set_sys units MM + new + open_gerber tests/gerber_files/simple1.gbr -outname margin + isolate margin -dia 3 + cncjob margin_iso + cncjob margin_iso + set EXPORT [export_gcode margin_iso_cnc] + write_gcode margin_iso_cnc_1 /tmp/file.gcode ${EXPORT} + + """ + + # array of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon) + aliases = ['export_gcode'] + + # dictionary of types from Tcl command, needs to be ordered + arg_names = collections.OrderedDict([ + ('name', str), + ('preamble', str), + ('postamble', str) + ]) + + # dictionary of types from Tcl command, needs to be ordered , this is for options like -optionname value + option_types = collections.OrderedDict() + + # array of mandatory options for current Tcl command: required = {'name','outname'} + required = ['name'] + + # structured help for current command, args needs to be ordered + help = { + 'main': "Export gcode into console output.", + 'args': collections.OrderedDict([ + ('name', 'Name of the source Geometry object.'), + ('preamble', 'Prepend GCODE.'), + ('postamble', 'Append GCODE.') + ]), + '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 + """ + + name = args['name'] + + obj = self.app.collection.get_by_name(name) + if obj is None: + self.raise_tcl_error("Object not found: %s" % name) + + if not isinstance(obj, CNCjob): + self.raise_tcl_error('Expected CNCjob, got %s %s.' % (name, type(obj))) + + if self.app.collection.has_promises(): + self.raise_tcl_error('!!!Promises exists, but should not here!!!') + + del args['name'] + return obj.get_gcode(**args) diff --git a/tclCommands/TclCommandExteriors.py b/tclCommands/TclCommandExteriors.py index d445cd5..16f2fee 100644 --- a/tclCommands/TclCommandExteriors.py +++ b/tclCommands/TclCommandExteriors.py @@ -8,7 +8,7 @@ class TclCommandExteriors(TclCommand.TclCommand): """ # array of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon) - aliases = ['exteriors','ext'] + aliases = ['exteriors', 'ext'] # dictionary of types from Tcl command, needs to be ordered arg_names = collections.OrderedDict([ @@ -30,7 +30,7 @@ class TclCommandExteriors(TclCommand.TclCommand): ('name', 'Name of the source Geometry object.'), ('outname', 'Name of the resulting Geometry object.') ]), - 'examples':[] + 'examples': [] } def execute(self, args, unnamed_args): @@ -50,19 +50,15 @@ class TclCommandExteriors(TclCommand.TclCommand): else: outname = name + "_exteriors" - try: - obj = self.app.collection.get_by_name(name) - except: - self.app.raiseTclError("Could not retrieve object: %s" % name) - + obj = self.app.collection.get_by_name(name) if obj is None: - self.app.raiseTclError("Object not found: %s" % name) + self.raise_tcl_error("Object not found: %s" % name) if not isinstance(obj, Geometry): - self.app.raiseTclError('Expected Geometry, got %s %s.' % (name, type(obj))) + self.raise_tcl_error('Expected Geometry, got %s %s.' % (name, type(obj))) - def geo_init(geo_obj, app_obj): + def geo_init(geo_obj): geo_obj.solid_geometry = obj_exteriors obj_exteriors = obj.get_exteriors() - self.app.new_object('geometry', outname, geo_init) \ No newline at end of file + self.app.new_object('geometry', outname, geo_init) diff --git a/tclCommands/TclCommandInteriors.py b/tclCommands/TclCommandInteriors.py index ef67ce9..2314be3 100644 --- a/tclCommands/TclCommandInteriors.py +++ b/tclCommands/TclCommandInteriors.py @@ -1,6 +1,7 @@ from ObjectCollection import * import TclCommand + class TclCommandInteriors(TclCommand.TclCommand): """ Tcl shell command to get interiors of polygons @@ -29,7 +30,7 @@ class TclCommandInteriors(TclCommand.TclCommand): ('name', 'Name of the source Geometry object.'), ('outname', 'Name of the resulting Geometry object.') ]), - 'examples':[] + 'examples': [] } def execute(self, args, unnamed_args): @@ -49,19 +50,15 @@ class TclCommandInteriors(TclCommand.TclCommand): else: outname = name + "_interiors" - try: - obj = self.app.collection.get_by_name(name) - except: - self.app.raiseTclError("Could not retrieve object: %s" % name) - + obj = self.app.collection.get_by_name(name) if obj is None: - self.app.raiseTclError("Object not found: %s" % name) + self.raise_tcl_error("Object not found: %s" % name) if not isinstance(obj, Geometry): - self.app.raiseTclError('Expected Geometry, got %s %s.' % (name, type(obj))) + self.raise_tcl_error('Expected Geometry, got %s %s.' % (name, type(obj))) - def geo_init(geo_obj, app_obj): + def geo_init(geo_obj): geo_obj.solid_geometry = obj_exteriors obj_exteriors = obj.get_interiors() - self.app.new_object('geometry', outname, geo_init) \ No newline at end of file + self.app.new_object('geometry', outname, geo_init) diff --git a/tclCommands/__init__.py b/tclCommands/__init__.py index 45d0ffc..3055dbc 100644 --- a/tclCommands/__init__.py +++ b/tclCommands/__init__.py @@ -1,5 +1,4 @@ import pkgutil -import inspect import sys # allowed command modules @@ -7,9 +6,10 @@ import tclCommands.TclCommandExteriors import tclCommands.TclCommandInteriors import tclCommands.TclCommandAddPolygon import tclCommands.TclCommandAddPolyline +import tclCommands.TclCommandExportGcode +import tclCommands.TclCommandCncjob - -__all__=[] +__all__ = [] for loader, name, is_pkg in pkgutil.walk_packages(__path__): module = loader.find_module(name).load_module(name) @@ -25,8 +25,8 @@ def register_all_commands(app, commands): we need import all modules in top section: import tclCommands.TclCommandExteriors - at this stage we can include only wanted commands with this, autoloading may be implemented in future - I have no enought knowledge about python's anatomy. Would be nice to include all classes which are descendant etc. + at this stage we can include only wanted commands with this, auto loading may be implemented in future + I have no enough knowledge about python's anatomy. Would be nice to include all classes which are descendant etc. :param app: FlatCAMApp :param commands: array of commands which should be modified @@ -35,14 +35,14 @@ def register_all_commands(app, commands): tcl_modules = {k: v for k, v in sys.modules.items() if k.startswith('tclCommands.TclCommand')} - for key, module in tcl_modules.items(): + for key, mod in tcl_modules.items(): if key != 'tclCommands.TclCommand': - classname = key.split('.')[1] - class_ = getattr(module, classname) - commandInstance=class_(app) + class_name = key.split('.')[1] + class_type = getattr(mod, class_name) + command_instance = class_type(app) - for alias in commandInstance.aliases: - commands[alias]={ - 'fcn': commandInstance.execute_wrapper, - 'help': commandInstance.get_decorated_help() - } \ No newline at end of file + for alias in command_instance.aliases: + commands[alias] = { + 'fcn': command_instance.execute_wrapper, + 'help': command_instance.get_decorated_help() + } From a5ff8c574a07e4662f32c8e345b5e20ce0fb689d Mon Sep 17 00:00:00 2001 From: grbd Date: Mon, 21 Mar 2016 11:38:14 +0000 Subject: [PATCH 069/134] Added initial svg export functionality --- FlatCAMApp.py | 75 ++++++++++++++++++++++++++++++++++++++++++++++++++- FlatCAMGUI.py | 4 +++ camlib.py | 17 ++++++++++++ 3 files changed, 95 insertions(+), 1 deletion(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index e5b9489..3069eaf 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -25,7 +25,7 @@ from FlatCAMDraw import FlatCAMDraw from FlatCAMProcess import * from MeasurementTool import Measurement from DblSidedTool import DblSidedTool - +from xml.dom.minidom import parseString as parse_xml_string ######################################## ## App ## @@ -451,6 +451,7 @@ class App(QtCore.QObject): self.ui.menufileopengcode.triggered.connect(self.on_fileopengcode) self.ui.menufileopenproject.triggered.connect(self.on_file_openproject) self.ui.menufileimportsvg.triggered.connect(self.on_file_importsvg) + self.ui.menufileexportsvg.triggered.connect(self.on_file_exportsvg) self.ui.menufilesaveproject.triggered.connect(self.on_file_saveproject) self.ui.menufilesaveprojectas.triggered.connect(self.on_file_saveprojectas) self.ui.menufilesaveprojectcopy.triggered.connect(lambda: self.on_file_saveprojectas(make_copy=True)) @@ -1577,6 +1578,42 @@ class App(QtCore.QObject): # thread safe. The new_project() self.open_project(filename) + def on_file_exportsvg(self): + """ + Callback for menu item File->Export SVG. + + :return: None + """ + self.report_usage("on_file_exportsvg") + App.log.debug("on_file_exportsvg()") + + obj = self.collection.get_active() + if obj is None: + self.inform.emit("WARNING: No object selected.") + msg = "Please Select a Geometry object to export" + msgbox = QtGui.QMessageBox() + msgbox.setInformativeText(msg) + msgbox.setStandardButtons(QtGui.QMessageBox.Ok) + msgbox.setDefaultButton(QtGui.QMessageBox.Ok) + msgbox.exec_() + return + + name = self.collection.get_active().options["name"] + + try: + filename = QtGui.QFileDialog.getSaveFileName(caption="Export SVG", + directory=self.get_last_folder(), filter="*.svg") + except TypeError: + filename = QtGui.QFileDialog.getSaveFileName(caption="Export SVG") + + filename = str(filename) + + if str(filename) == "": + self.inform.emit("Export SVG cancelled.") + return + else: + self.export_svg(name, filename) + def on_file_importsvg(self): """ Callback for menu item File->Import SVG. @@ -1661,6 +1698,31 @@ class App(QtCore.QObject): else: self.inform.emit("Project copy saved to: " + self.project_filename) + + def export_svg(self, obj_name, filename, outname=None): + """ + Exports a Geometry Object to a SVG File + + :param filename: Path to the SVG file to save to. + :param outname: + :return: + """ + self.log.debug("export_svg()") + + try: + obj = self.collection.get_by_name(str(obj_name)) + except: + return "Could not retrieve object: %s" % obj_name + + # TODO needs size of board / dpi information + + with self.proc_container.new("Exporting SVG") as proc: + svg_elem = obj.export_svg() + svg_elem = "" + svg_elem + "" + doc = parse_xml_string(svg_elem) + with open(filename, 'w') as fp: + fp.write(doc.toprettyxml()) + def import_svg(self, filename, outname=None): """ Adds a new Geometry Object to the projects and populates @@ -2109,6 +2171,10 @@ class App(QtCore.QObject): return str(self.collection.get_names()) + def export_svg(name, filename): + + self.export_svg(str(name), str(filename)) + def import_svg(filename, *args): a, kwa = h(*args) types = {'outname': str} @@ -3225,6 +3291,13 @@ class App(QtCore.QObject): "> import_svg " + " filename: Path to the file to import." }, + 'export_svg': { + 'fcn': export_svg, + 'help': "Export a Geometry Object as a SVG File\n" + + "> export_svg \n" + + " name: Name of the geometry object to export.\n" + + " filename: Path to the file to export." + }, 'open_gerber': { 'fcn': open_gerber, 'help': "Opens a Gerber file.\n" diff --git a/FlatCAMGUI.py b/FlatCAMGUI.py index 8bb2445..3c01d12 100644 --- a/FlatCAMGUI.py +++ b/FlatCAMGUI.py @@ -48,6 +48,10 @@ class FlatCAMGUI(QtGui.QMainWindow): self.menufileimportsvg = QtGui.QAction(QtGui.QIcon('share/folder16.png'), 'Import &SVG ...', self) self.menufile.addAction(self.menufileimportsvg) + # Export SVG ... + self.menufileexportsvg = QtGui.QAction(QtGui.QIcon('share/folder16.png'), 'Export &SVG ...', self) + self.menufile.addAction(self.menufileexportsvg) + # Save Project self.menufilesaveproject = QtGui.QAction(QtGui.QIcon('share/floppy16.png'), '&Save Project', self) self.menufile.addAction(self.menufilesaveproject) diff --git a/camlib.py b/camlib.py index f576ed9..8bbd49c 100644 --- a/camlib.py +++ b/camlib.py @@ -869,6 +869,14 @@ class Geometry(object): """ self.solid_geometry = [cascaded_union(self.solid_geometry)] + def export_svg(self): + """ + Exports the Gemoetry Object as a SVG Element + + :return: SVG Element + """ + svg_elem = self.solid_geometry.svg() + return svg_elem class ApertureMacro: """ @@ -3313,6 +3321,15 @@ class CNCjob(Geometry): self.create_geometry() + def export_svg(self): + """ + Exports the CNC Job as a SVG Element + + :return: SVG Element + """ + self.solid_geometry = cascaded_union([geo['geom'] for geo in self.gcode_parsed]) + svg_elem = self.solid_geometry.svg() + return svg_elem # def get_bounds(geometry_set): # xmin = Inf From b272329384f5aa49a87ddbd04792abc9444201cd Mon Sep 17 00:00:00 2001 From: grbd Date: Mon, 21 Mar 2016 17:25:46 +0000 Subject: [PATCH 070/134] Initial scaling fixes for svg export --- FlatCAMApp.py | 10 +++++++--- camlib.py | 4 ++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 3069eaf..f6f74d4 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -1714,11 +1714,15 @@ class App(QtCore.QObject): except: return "Could not retrieve object: %s" % obj_name - # TODO needs size of board / dpi information + # TODO needs size of board determining + # TODO needs seperate colours for CNCPath Export with self.proc_container.new("Exporting SVG") as proc: - svg_elem = obj.export_svg() - svg_elem = "" + svg_elem + "" + svg_header = ' Date: Mon, 21 Mar 2016 19:34:33 +0000 Subject: [PATCH 071/134] Fixed the scaling issues with the svg export --- FlatCAMApp.py | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index f6f74d4..fb60028 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -1714,15 +1714,32 @@ class App(QtCore.QObject): except: return "Could not retrieve object: %s" % obj_name - # TODO needs size of board determining # TODO needs seperate colours for CNCPath Export + # The line thickness is only affected by the scaling factor not the tool size + # Use the tool size to determine the scaling factor for line thickness with self.proc_container.new("Exporting SVG") as proc: + exported_svg = obj.export_svg() + + # Determine bounding area for svg export + svgwidth = obj.solid_geometry.bounds[2] - obj.solid_geometry.bounds[0] + svgheight = obj.solid_geometry.bounds[3] - obj.solid_geometry.bounds[1] + minx = obj.solid_geometry.bounds[0] + miny = obj.solid_geometry.bounds[1] - svgheight + + svgwidth = str(svgwidth) + svgheight = str(svgheight) + minx = str(minx) + miny = str(miny) + uom = obj.units.lower() + svg_header = '' svg_header += '' svg_footer = ' ' - svg_elem = svg_header + obj.export_svg() + svg_footer + svg_elem = svg_header + exported_svg + svg_footer doc = parse_xml_string(svg_elem) with open(filename, 'w') as fp: fp.write(doc.toprettyxml()) From 532a821c765d163185aaed1f16c6e7ec4820df06 Mon Sep 17 00:00:00 2001 From: grbd Date: Mon, 21 Mar 2016 21:46:29 +0000 Subject: [PATCH 072/134] Fixed the colors with svg exports from cnc jobs for Visicut --- camlib.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/camlib.py b/camlib.py index c9f2f7f..bc42b5d 100644 --- a/camlib.py +++ b/camlib.py @@ -3327,8 +3327,28 @@ class CNCjob(Geometry): :return: SVG Element """ + + # This appears to match up distance wise with inkscape + scale = self.options['tooldia'] / 2 + if scale == 0: + scale = 0.05 + + cuts = [] + travels = [] + for g in self.gcode_parsed: + if g['kind'][0] == 'C': cuts.append(g) + if g['kind'][0] == 'T': travels.append(g) + + # Used to determine board size self.solid_geometry = cascaded_union([geo['geom'] for geo in self.gcode_parsed]) - svg_elem = self.solid_geometry.svg(scale_factor=0.05) + + # Seperate the travels from the cuts for laser cutting under Visicut + travelsgeom = cascaded_union([geo['geom'] for geo in travels]) + cutsgeom = cascaded_union([geo['geom'] for geo in cuts]) + + svg_elem = travelsgeom.svg(scale_factor=scale, stroke_color="#F0E24D") + svg_elem += cutsgeom.svg(scale_factor=scale, stroke_color="#5E6CFF") + return svg_elem # def get_bounds(geometry_set): From 10e9fa74c3226dc076c8c2de00f79344bf0959bf Mon Sep 17 00:00:00 2001 From: grbd Date: Tue, 22 Mar 2016 02:25:07 +0000 Subject: [PATCH 073/134] Added some additional checks for the types when exporting, and additional comments --- FlatCAMApp.py | 23 ++++++++++++++++++----- camlib.py | 9 +++++++-- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index fb60028..76c7287 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -1598,6 +1598,18 @@ class App(QtCore.QObject): msgbox.exec_() return + # Check for more compatible types and add as required + # Excellon not yet supported, there seems to be a list within the Polygon Geometry that shapely's svg export doesn't like + + if (not isinstance(obj, FlatCAMGeometry) and not isinstance(obj, FlatCAMGerber) and not isinstance(obj, FlatCAMCNCjob)): + msg = "ERROR: Only Geometry, Gerber and CNCJob objects can be used." + msgbox = QtGui.QMessageBox() + msgbox.setInformativeText(msg) + msgbox.setStandardButtons(QtGui.QMessageBox.Ok) + msgbox.setDefaultButton(QtGui.QMessageBox.Ok) + msgbox.exec_() + return + name = self.collection.get_active().options["name"] try: @@ -1714,10 +1726,6 @@ class App(QtCore.QObject): except: return "Could not retrieve object: %s" % obj_name - # TODO needs seperate colours for CNCPath Export - # The line thickness is only affected by the scaling factor not the tool size - # Use the tool size to determine the scaling factor for line thickness - with self.proc_container.new("Exporting SVG") as proc: exported_svg = obj.export_svg() @@ -1727,12 +1735,15 @@ class App(QtCore.QObject): minx = obj.solid_geometry.bounds[0] miny = obj.solid_geometry.bounds[1] - svgheight + # Convert everything to strings for use in the xml doc svgwidth = str(svgwidth) svgheight = str(svgheight) minx = str(minx) miny = str(miny) uom = obj.units.lower() - + + # Add a SVG Header and footer to the svg output from shapely + # The transform flips the Y Axis so that everything renders properly within svg apps such as inkscape svg_header = ' Date: Tue, 22 Mar 2016 09:54:57 +0000 Subject: [PATCH 074/134] This adds a bunch of fixes when exporting svg's from geom's or cncjobs generated from drill files, also adds support for exporting drill files directly as svg's, and should capture any objects that use list within the solid_geometry attribute --- FlatCAMApp.py | 20 +++++++++++++------- camlib.py | 22 +++++++++++++++++----- 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 76c7287..e0dac23 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -1599,9 +1599,8 @@ class App(QtCore.QObject): return # Check for more compatible types and add as required - # Excellon not yet supported, there seems to be a list within the Polygon Geometry that shapely's svg export doesn't like - - if (not isinstance(obj, FlatCAMGeometry) and not isinstance(obj, FlatCAMGerber) and not isinstance(obj, FlatCAMCNCjob)): + if (not isinstance(obj, FlatCAMGeometry) and not isinstance(obj, FlatCAMGerber) and not isinstance(obj, FlatCAMCNCjob) + and not isinstance(obj, FlatCAMExcellon)): msg = "ERROR: Only Geometry, Gerber and CNCJob objects can be used." msgbox = QtGui.QMessageBox() msgbox.setInformativeText(msg) @@ -1729,11 +1728,18 @@ class App(QtCore.QObject): with self.proc_container.new("Exporting SVG") as proc: exported_svg = obj.export_svg() + # Sometimes obj.solid_geometry can be a list instead of a Shapely class + # Make sure we see it as a Shapely Geometry class + geom = obj.solid_geometry + if type(obj.solid_geometry) is list: + geom = [cascaded_union(obj.solid_geometry)][0] + + # Determine bounding area for svg export - svgwidth = obj.solid_geometry.bounds[2] - obj.solid_geometry.bounds[0] - svgheight = obj.solid_geometry.bounds[3] - obj.solid_geometry.bounds[1] - minx = obj.solid_geometry.bounds[0] - miny = obj.solid_geometry.bounds[1] - svgheight + svgwidth = geom.bounds[2] - geom.bounds[0] + svgheight = geom.bounds[3] - geom.bounds[1] + minx = geom.bounds[0] + miny = geom.bounds[1] - svgheight # Convert everything to strings for use in the xml doc svgwidth = str(svgwidth) diff --git a/camlib.py b/camlib.py index e59b18c..9cd11e9 100644 --- a/camlib.py +++ b/camlib.py @@ -875,7 +875,14 @@ class Geometry(object): :return: SVG Element """ - svg_elem = self.solid_geometry.svg(scale_factor=0.05) + # Sometimes self.solid_geometry can be a list instead of a Shapely class + # Make sure we see it as a Shapely Geometry class + geom = self.solid_geometry + if type(self.solid_geometry) is list: + geom = [cascaded_union(self.solid_geometry)][0] + + # Convert to a SVG + svg_elem = geom.svg(scale_factor=0.05) return svg_elem class ApertureMacro: @@ -3345,14 +3352,19 @@ class CNCjob(Geometry): self.solid_geometry = cascaded_union([geo['geom'] for geo in self.gcode_parsed]) # Convert the cuts and travels into single geometry objects we can render as svg xml - travelsgeom = cascaded_union([geo['geom'] for geo in travels]) - cutsgeom = cascaded_union([geo['geom'] for geo in cuts]) + if travels: + travelsgeom = cascaded_union([geo['geom'] for geo in travels]) + if cuts: + cutsgeom = cascaded_union([geo['geom'] for geo in cuts]) # Render the SVG Xml # The scale factor affects the size of the lines, and the stroke color adds different formatting for each set # It's better to have the travels sitting underneath the cuts for visicut - svg_elem = travelsgeom.svg(scale_factor=scale, stroke_color="#F0E24D") - svg_elem += cutsgeom.svg(scale_factor=scale, stroke_color="#5E6CFF") + svg_elem = "" + if travels: + svg_elem = travelsgeom.svg(scale_factor=scale, stroke_color="#F0E24D") + if cuts: + svg_elem += cutsgeom.svg(scale_factor=scale, stroke_color="#5E6CFF") return svg_elem From ee43d8b920b1d0db22e80e43a5822762a9299000 Mon Sep 17 00:00:00 2001 From: grbd Date: Tue, 22 Mar 2016 18:56:04 +0000 Subject: [PATCH 075/134] Additional fixes for export size and flattening the geometry list --- FlatCAMApp.py | 19 +++++++------------ camlib.py | 4 +--- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index e0dac23..2f54058 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -1730,22 +1730,17 @@ class App(QtCore.QObject): # Sometimes obj.solid_geometry can be a list instead of a Shapely class # Make sure we see it as a Shapely Geometry class - geom = obj.solid_geometry - if type(obj.solid_geometry) is list: - geom = [cascaded_union(obj.solid_geometry)][0] - + geom = cascaded_union(obj.flatten()) # Determine bounding area for svg export - svgwidth = geom.bounds[2] - geom.bounds[0] - svgheight = geom.bounds[3] - geom.bounds[1] - minx = geom.bounds[0] - miny = geom.bounds[1] - svgheight + bounds = obj.bounds() + size = obj.size() # Convert everything to strings for use in the xml doc - svgwidth = str(svgwidth) - svgheight = str(svgheight) - minx = str(minx) - miny = str(miny) + svgwidth = str(size[0]) + svgheight = str(size[1]) + minx = str(bounds[0]) + miny = str(bounds[1] - size[1]) uom = obj.units.lower() # Add a SVG Header and footer to the svg output from shapely diff --git a/camlib.py b/camlib.py index 9cd11e9..2ae8471 100644 --- a/camlib.py +++ b/camlib.py @@ -877,9 +877,7 @@ class Geometry(object): """ # Sometimes self.solid_geometry can be a list instead of a Shapely class # Make sure we see it as a Shapely Geometry class - geom = self.solid_geometry - if type(self.solid_geometry) is list: - geom = [cascaded_union(self.solid_geometry)][0] + geom = cascaded_union(self.flatten()) # Convert to a SVG svg_elem = geom.svg(scale_factor=0.05) From 039a2dd4dc6f7fa7b26346d34887029883998396 Mon Sep 17 00:00:00 2001 From: grbd Date: Tue, 22 Mar 2016 23:22:02 +0000 Subject: [PATCH 076/134] Made scale_factor optional for cli, added more comments, removed redundant code --- FlatCAMApp.py | 24 ++++++++++++++---------- camlib.py | 40 ++++++++++++++++++++++++++++------------ 2 files changed, 42 insertions(+), 22 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 2f54058..6593d9a 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -1710,7 +1710,7 @@ class App(QtCore.QObject): self.inform.emit("Project copy saved to: " + self.project_filename) - def export_svg(self, obj_name, filename, outname=None): + def export_svg(self, obj_name, filename, scale_factor=0.00): """ Exports a Geometry Object to a SVG File @@ -1726,11 +1726,7 @@ class App(QtCore.QObject): return "Could not retrieve object: %s" % obj_name with self.proc_container.new("Exporting SVG") as proc: - exported_svg = obj.export_svg() - - # Sometimes obj.solid_geometry can be a list instead of a Shapely class - # Make sure we see it as a Shapely Geometry class - geom = cascaded_union(obj.flatten()) + exported_svg = obj.export_svg(scale_factor=scale_factor) # Determine bounding area for svg export bounds = obj.bounds() @@ -2206,9 +2202,16 @@ class App(QtCore.QObject): return str(self.collection.get_names()) - def export_svg(name, filename): + def export_svg(name, filename, *args): + a, kwa = h(*args) + types = {'scale_factor': float} - self.export_svg(str(name), str(filename)) + for key in kwa: + if key not in types: + return 'Unknown parameter: %s' % key + kwa[key] = types[key](kwa[key]) + + self.export_svg(str(name), str(filename), **kwa) def import_svg(filename, *args): a, kwa = h(*args) @@ -3329,9 +3332,10 @@ class App(QtCore.QObject): 'export_svg': { 'fcn': export_svg, 'help': "Export a Geometry Object as a SVG File\n" + - "> export_svg \n" + + "> export_svg [-scale_factor <0.0 (float)>]\n" + " name: Name of the geometry object to export.\n" + - " filename: Path to the file to export." + " filename: Path to the file to export.\n" + + " scale_factor: Multiplication factor used for scaling line widths during export." }, 'open_gerber': { 'fcn': open_gerber, diff --git a/camlib.py b/camlib.py index 2ae8471..23bf1bc 100644 --- a/camlib.py +++ b/camlib.py @@ -869,18 +869,25 @@ class Geometry(object): """ self.solid_geometry = [cascaded_union(self.solid_geometry)] - def export_svg(self): + def export_svg(self, scale_factor=0.00): """ Exports the Gemoetry Object as a SVG Element :return: SVG Element """ - # Sometimes self.solid_geometry can be a list instead of a Shapely class - # Make sure we see it as a Shapely Geometry class + # Make sure we see a Shapely Geometry class and not a list geom = cascaded_union(self.flatten()) + # scale_factor is a multiplication factor for the SVG stroke-width used within shapely's svg export + + # If 0 or less which is invalid then default to 0.05 + # This value appears to work for zooming, and getting the output svg line width + # to match that viewed on screen with FlatCam + if scale_factor <= 0: + scale_factor = 0.05 + # Convert to a SVG - svg_elem = geom.svg(scale_factor=0.05) + svg_elem = geom.svg(scale_factor=scale_factor) return svg_elem class ApertureMacro: @@ -3326,17 +3333,26 @@ class CNCjob(Geometry): self.create_geometry() - def export_svg(self): + def export_svg(self, scale_factor=0.00): """ Exports the CNC Job as a SVG Element - :return: SVG Element + :scale_factor: float + :return: SVG Element string """ + # scale_factor is a multiplication factor for the SVG stroke-width used within shapely's svg export + # If not specified then try and use the tool diameter + # This way what is on screen will match what is outputed for the svg + # This is quite a useful feature for svg's used with visicut - # This appears to match up distance wise with inkscape - scale = self.options['tooldia'] / 2 - if scale == 0: - scale = 0.05 + if scale_factor <= 0: + scale_factor = self.options['tooldia'] / 2 + + # If still 0 then defailt to 0.05 + # This value appears to work for zooming, and getting the output svg line width + # to match that viewed on screen with FlatCam + if scale_factor == 0: + scale_factor = 0.05 # Seperate the list of cuts and travels into 2 distinct lists # This way we can add different formatting / colors to both @@ -3360,9 +3376,9 @@ class CNCjob(Geometry): # It's better to have the travels sitting underneath the cuts for visicut svg_elem = "" if travels: - svg_elem = travelsgeom.svg(scale_factor=scale, stroke_color="#F0E24D") + svg_elem = travelsgeom.svg(scale_factor=scale_factor, stroke_color="#F0E24D") if cuts: - svg_elem += cutsgeom.svg(scale_factor=scale, stroke_color="#5E6CFF") + svg_elem += cutsgeom.svg(scale_factor=scale_factor, stroke_color="#5E6CFF") return svg_elem From 790f53dd55e26e80ee56541073a892d4fe4fe391 Mon Sep 17 00:00:00 2001 From: Juan Pablo Caram Date: Wed, 23 Mar 2016 11:06:48 -0400 Subject: [PATCH 077/134] Blocking in shell functions. Test for exception handling. See #196. --- FlatCAMApp.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index e5b9489..6626cac 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -2107,7 +2107,27 @@ class App(QtCore.QObject): except Exception as e: return str(e) - return str(self.collection.get_names()) + def mytest2(*args): + to = int(args[0]) + + try: + for rec in self.recent: + if rec['kind'] == 'gerber': + self.open_gerber(str(rec['filename'])) + break + + basename = self.collection.get_names()[0] + isolate(basename, '-passes', '10', '-combine', '1') + iso = self.collection.get_by_name(basename + "_iso") + + with wait_signal(self.new_object_available, to): + 1/0 # Force exception + iso.generatecncjob() + + return str(self.collection.get_names()) + + except Exception as e: + return str(e) def import_svg(filename, *args): a, kwa = h(*args) @@ -3215,6 +3235,10 @@ class App(QtCore.QObject): 'fcn': mytest, 'help': "Test function. Only for testing." }, + 'mytest2': { + 'fcn': mytest2, + 'help': "Test function. Only for testing." + }, 'help': { 'fcn': shelp, 'help': "Shows list of commands." From 95676f21e2d973c0486e55f87beaede434f15764 Mon Sep 17 00:00:00 2001 From: Juan Pablo Caram Date: Wed, 23 Mar 2016 14:58:53 -0400 Subject: [PATCH 078/134] Blocking in shell functions. Correctly report exceptions in threads. See #196. --- FlatCAMApp.py | 99 +++++++++++++++++++++++++++++++++++---------------- 1 file changed, 69 insertions(+), 30 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 6626cac..82ec8dd 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -2059,30 +2059,40 @@ class App(QtCore.QObject): yield + oeh = sys.excepthook + ex = [] + def exceptHook(type_, value, traceback): + ex.append(value) + oeh(type_, value, traceback) + sys.excepthook = exceptHook + if timeout is not None: QtCore.QTimer.singleShot(timeout, report_quit) loop.exec_() + sys.excepthook = oeh + if ex: + self.raiseTclError(str(ex[0])) if status['timed_out']: raise Exception('Timed out!') - def wait_signal2(signal, timeout=10000): - """Block loop until signal emitted, or timeout (ms) elapses.""" - loop = QtCore.QEventLoop() - signal.connect(loop.quit) - status = {'timed_out': False} - - def report_quit(): - status['timed_out'] = True - loop.quit() - - if timeout is not None: - QtCore.QTimer.singleShot(timeout, report_quit) - loop.exec_() - - if status['timed_out']: - raise Exception('Timed out!') + # def wait_signal2(signal, timeout=10000): + # """Block loop until signal emitted, or timeout (ms) elapses.""" + # loop = QtCore.QEventLoop() + # signal.connect(loop.quit) + # status = {'timed_out': False} + # + # def report_quit(): + # status['timed_out'] = True + # loop.quit() + # + # if timeout is not None: + # QtCore.QTimer.singleShot(timeout, report_quit) + # loop.exec_() + # + # if status['timed_out']: + # raise Exception('Timed out!') def mytest(*args): to = int(args[0]) @@ -2110,24 +2120,45 @@ class App(QtCore.QObject): def mytest2(*args): to = int(args[0]) - try: - for rec in self.recent: - if rec['kind'] == 'gerber': - self.open_gerber(str(rec['filename'])) - break + for rec in self.recent: + if rec['kind'] == 'gerber': + self.open_gerber(str(rec['filename'])) + break - basename = self.collection.get_names()[0] - isolate(basename, '-passes', '10', '-combine', '1') - iso = self.collection.get_by_name(basename + "_iso") + basename = self.collection.get_names()[0] + isolate(basename, '-passes', '10', '-combine', '1') + iso = self.collection.get_by_name(basename + "_iso") - with wait_signal(self.new_object_available, to): - 1/0 # Force exception - iso.generatecncjob() + with wait_signal(self.new_object_available, to): + 1/0 # Force exception + iso.generatecncjob() - return str(self.collection.get_names()) + return str(self.collection.get_names()) - except Exception as e: - return str(e) + def mytest3(*args): + to = int(args[0]) + + def sometask(*args): + time.sleep(2) + self.inform.emit("mytest3") + + with wait_signal(self.inform, to): + self.worker_task.emit({'fcn': sometask, 'params': []}) + + return "mytest3 done" + + def mytest4(*args): + to = int(args[0]) + + def sometask(*args): + time.sleep(2) + 1/0 # Force exception + self.inform.emit("mytest4") + + with wait_signal(self.inform, to): + self.worker_task.emit({'fcn': sometask, 'params': []}) + + return "mytest3 done" def import_svg(filename, *args): a, kwa = h(*args) @@ -3239,6 +3270,14 @@ class App(QtCore.QObject): 'fcn': mytest2, 'help': "Test function. Only for testing." }, + 'mytest3': { + 'fcn': mytest3, + 'help': "Test function. Only for testing." + }, + 'mytest4': { + 'fcn': mytest4, + 'help': "Test function. Only for testing." + }, 'help': { 'fcn': shelp, 'help': "Shows list of commands." From b0575a1c34f0c40eb480a884914f679b37e4dc6d Mon Sep 17 00:00:00 2001 From: Juan Pablo Caram Date: Thu, 24 Mar 2016 15:44:22 -0400 Subject: [PATCH 079/134] Tidying up imports. --- FlatCAMApp.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 5ca2e90..538bd14 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -10,6 +10,7 @@ import os import Tkinter from PyQt4 import QtCore import time # Just used for debugging. Double check before removing. +from xml.dom.minidom import parseString as parse_xml_string ######################################## ## Imports part of FlatCAM ## @@ -25,7 +26,7 @@ from FlatCAMDraw import FlatCAMDraw from FlatCAMProcess import * from MeasurementTool import Measurement from DblSidedTool import DblSidedTool -from xml.dom.minidom import parseString as parse_xml_string + ######################################## ## App ## From a5207294448ce9ff3e9bd811402536479ae26d97 Mon Sep 17 00:00:00 2001 From: Juan Pablo Caram Date: Thu, 24 Mar 2016 16:06:44 -0400 Subject: [PATCH 080/134] Complete implementation of blocking mechanism waiting for signal. See #196. --- FlatCAMApp.py | 32 ++++++++++++++++++++++++++------ FlatCAMWorker.py | 17 ++++++++++------- 2 files changed, 36 insertions(+), 13 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 538bd14..5933523 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -11,6 +11,7 @@ import Tkinter from PyQt4 import QtCore import time # Just used for debugging. Double check before removing. from xml.dom.minidom import parseString as parse_xml_string +from contextlib import contextmanager ######################################## ## Imports part of FlatCAM ## @@ -106,6 +107,10 @@ class App(QtCore.QObject): message = QtCore.pyqtSignal(str, str, str) + # Emitted when an unhandled exception happens + # in the worker task. + thread_exception = QtCore.pyqtSignal(object) + def __init__(self, user_defaults=True, post_gui=None): """ Starts the application. @@ -2138,13 +2143,22 @@ class App(QtCore.QObject): return a, kwa - from contextlib import contextmanager @contextmanager def wait_signal(signal, timeout=10000): - """Block loop until signal emitted, or timeout (ms) elapses.""" + """ + Block loop until signal emitted, timeout (ms) elapses + or unhandled exception happens in a thread. + + :param signal: Signal to wait for. + """ loop = QtCore.QEventLoop() + + # Normal termination signal.connect(loop.quit) + # Termination by exception in thread + self.thread_exception.connect(loop.quit) + status = {'timed_out': False} def report_quit(): @@ -2153,17 +2167,23 @@ class App(QtCore.QObject): yield + # Temporarily change how exceptions are managed. oeh = sys.excepthook ex = [] - def exceptHook(type_, value, traceback): - ex.append(value) - oeh(type_, value, traceback) - sys.excepthook = exceptHook + def except_hook(type_, value, traceback_): + ex.append(value) + oeh(type_, value, traceback_) + sys.excepthook = except_hook + + # Terminate on timeout if timeout is not None: QtCore.QTimer.singleShot(timeout, report_quit) + #### Block #### loop.exec_() + + # Restore exception management sys.excepthook = oeh if ex: self.raiseTclError(str(ex[0])) diff --git a/FlatCAMWorker.py b/FlatCAMWorker.py index 528171b..e97d9d1 100644 --- a/FlatCAMWorker.py +++ b/FlatCAMWorker.py @@ -14,24 +14,27 @@ class Worker(QtCore.QObject): self.name = name def run(self): - # FlatCAMApp.App.log.debug("Worker Started!") + self.app.log.debug("Worker Started!") # Tasks are queued in the event listener. self.app.worker_task.connect(self.do_worker_task) def do_worker_task(self, task): - # FlatCAMApp.App.log.debug("Running task: %s" % str(task)) + self.app.log.debug("Running task: %s" % str(task)) # 'worker_name' property of task allows to target # specific worker. - if 'worker_name' in task and task['worker_name'] == self.name: - task['fcn'](*task['params']) - return + if ('worker_name' in task and task['worker_name'] == self.name) or \ + ('worker_name' not in task and self.name is None): + + try: + task['fcn'](*task['params']) + except Exception as e: + self.app.thread_exception.emit(e) + raise e - if 'worker_name' not in task and self.name is None: - task['fcn'](*task['params']) return # FlatCAMApp.App.log.debug("Task ignored.") From e96ee1af29a791972897135003d119172a1d0ebf Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Thu, 24 Mar 2016 23:06:44 +0100 Subject: [PATCH 081/134] merge new pull requests from FlatCAM->master implement executing of tasks inside worker thread cleanups, reimplement Isolate/New/OpenGerber as OOP style Shell commands disable edit during shell execution, show some progress add ability for breakpoints in other threads and only if available add X11 safe flag, not sure what happen on windows --- FlatCAM.py | 5 + FlatCAMApp.py | 244 ++++++++++++++++++++++++--- FlatCAMGUI.py | 4 + FlatCAMShell.py | 2 +- FlatCAMWorker.py | 28 ++- camlib.py | 68 ++++++++ tclCommands/TclCommand.py | 53 +++++- tclCommands/TclCommandAddPolygon.py | 2 +- tclCommands/TclCommandAddPolyline.py | 2 +- tclCommands/TclCommandCncjob.py | 7 +- tclCommands/TclCommandExportGcode.py | 2 +- tclCommands/TclCommandExteriors.py | 4 +- tclCommands/TclCommandInteriors.py | 4 +- tclCommands/TclCommandIsolate.py | 79 +++++++++ tclCommands/TclCommandNew.py | 40 +++++ tclCommands/TclCommandOpenGerber.py | 95 +++++++++++ tclCommands/__init__.py | 10 +- termwidget.py | 28 ++- tests/test_tcl_shell.py | 24 ++- 19 files changed, 651 insertions(+), 50 deletions(-) create mode 100644 tclCommands/TclCommandIsolate.py create mode 100644 tclCommands/TclCommandNew.py create mode 100644 tclCommands/TclCommandOpenGerber.py diff --git a/FlatCAM.py b/FlatCAM.py index 1c1b1f7..92ed2e1 100644 --- a/FlatCAM.py +++ b/FlatCAM.py @@ -1,5 +1,6 @@ import sys from PyQt4 import QtGui +from PyQt4 import QtCore from FlatCAMApp import App def debug_trace(): @@ -10,6 +11,10 @@ def debug_trace(): #set_trace() debug_trace() + +# all X11 calling should be thread safe otherwise we have strenght issues +QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_X11InitThreads) + app = QtGui.QApplication(sys.argv) fc = App() sys.exit(app.exec_()) \ No newline at end of file diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 840cf42..dd917bb 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -10,6 +10,7 @@ import os import Tkinter from PyQt4 import QtCore import time # Just used for debugging. Double check before removing. +from contextlib import contextmanager ######################################## ## Imports part of FlatCAM ## @@ -25,6 +26,7 @@ from FlatCAMDraw import FlatCAMDraw from FlatCAMProcess import * from MeasurementTool import Measurement from DblSidedTool import DblSidedTool +from xml.dom.minidom import parseString as parse_xml_string import tclCommands ######################################## @@ -103,6 +105,9 @@ class App(QtCore.QObject): # and is ready to be used. new_object_available = QtCore.pyqtSignal(object) + # Emmited when shell command is finished(one command only) + shell_command_finished = QtCore.pyqtSignal(object) + message = QtCore.pyqtSignal(str, str, str) def __init__(self, user_defaults=True, post_gui=None): @@ -451,6 +456,7 @@ class App(QtCore.QObject): self.ui.menufileopengcode.triggered.connect(self.on_fileopengcode) self.ui.menufileopenproject.triggered.connect(self.on_file_openproject) self.ui.menufileimportsvg.triggered.connect(self.on_file_importsvg) + self.ui.menufileexportsvg.triggered.connect(self.on_file_exportsvg) self.ui.menufilesaveproject.triggered.connect(self.on_file_saveproject) self.ui.menufilesaveprojectas.triggered.connect(self.on_file_saveprojectas) self.ui.menufilesaveprojectcopy.triggered.connect(lambda: self.on_file_saveprojectas(make_copy=True)) @@ -523,8 +529,8 @@ class App(QtCore.QObject): self.shell.resize(*self.defaults["shell_shape"]) self.shell.append_output("FlatCAM %s\n(c) 2014-2015 Juan Pablo Caram\n\n" % self.version) self.shell.append_output("Type help to get started.\n\n") - self.tcl = Tkinter.Tcl() - self.setup_shell() + + self.init_tcl() if self.cmd_line_shellfile: try: @@ -542,6 +548,17 @@ class App(QtCore.QObject): App.log.debug("END of constructor. Releasing control.") + def init_tcl(self): + if hasattr(self,'tcl'): + # self.tcl = None + # TODO we need to clean non default variables and procedures here + # new object cannot be used here as it will not remember values created for next passes, + # because tcl was execudted in old instance of TCL + pass + else: + self.tcl = Tkinter.Tcl() + self.setup_shell() + def defaults_read_form(self): for option in self.defaults_form_fields: self.defaults[option] = self.defaults_form_fields[option].get_value() @@ -676,12 +693,16 @@ class App(QtCore.QObject): def exec_command(self, text): """ Handles input from the shell. See FlatCAMApp.setup_shell for shell commands. + Also handles execution in separated threads :param text: :return: output if there was any """ - return self.exec_command_test(text, False) + self.report_usage('exec_command') + + result = self.exec_command_test(text, False) + return result def exec_command_test(self, text, reraise=True): """ @@ -692,11 +713,10 @@ class App(QtCore.QObject): :return: output if there was any """ - self.report_usage('exec_command') - text = str(text) try: + self.shell.open_proccessing() result = self.tcl.eval(str(text)) if result!='None': self.shell.append_output(result + '\n') @@ -708,6 +728,9 @@ class App(QtCore.QObject): #show error in console and just return or in test raise exception if reraise: raise e + finally: + self.shell.close_proccessing() + pass return result """ @@ -1491,6 +1514,9 @@ class App(QtCore.QObject): self.plotcanvas.clear() + # tcl needs to be reinitialized, otherwise old shell variables etc remains + self.init_tcl() + self.collection.delete_all() self.setup_component_editor() @@ -1612,6 +1638,53 @@ class App(QtCore.QObject): # thread safe. The new_project() self.open_project(filename) + def on_file_exportsvg(self): + """ + Callback for menu item File->Export SVG. + + :return: None + """ + self.report_usage("on_file_exportsvg") + App.log.debug("on_file_exportsvg()") + + obj = self.collection.get_active() + if obj is None: + self.inform.emit("WARNING: No object selected.") + msg = "Please Select a Geometry object to export" + msgbox = QtGui.QMessageBox() + msgbox.setInformativeText(msg) + msgbox.setStandardButtons(QtGui.QMessageBox.Ok) + msgbox.setDefaultButton(QtGui.QMessageBox.Ok) + msgbox.exec_() + return + + # Check for more compatible types and add as required + if (not isinstance(obj, FlatCAMGeometry) and not isinstance(obj, FlatCAMGerber) and not isinstance(obj, FlatCAMCNCjob) + and not isinstance(obj, FlatCAMExcellon)): + msg = "ERROR: Only Geometry, Gerber and CNCJob objects can be used." + msgbox = QtGui.QMessageBox() + msgbox.setInformativeText(msg) + msgbox.setStandardButtons(QtGui.QMessageBox.Ok) + msgbox.setDefaultButton(QtGui.QMessageBox.Ok) + msgbox.exec_() + return + + name = self.collection.get_active().options["name"] + + try: + filename = QtGui.QFileDialog.getSaveFileName(caption="Export SVG", + directory=self.get_last_folder(), filter="*.svg") + except TypeError: + filename = QtGui.QFileDialog.getSaveFileName(caption="Export SVG") + + filename = str(filename) + + if str(filename) == "": + self.inform.emit("Export SVG cancelled.") + return + else: + self.export_svg(name, filename) + def on_file_importsvg(self): """ Callback for menu item File->Import SVG. @@ -1696,6 +1769,51 @@ class App(QtCore.QObject): else: self.inform.emit("Project copy saved to: " + self.project_filename) + + def export_svg(self, obj_name, filename, scale_factor=0.00): + """ + Exports a Geometry Object to a SVG File + + :param filename: Path to the SVG file to save to. + :param outname: + :return: + """ + self.log.debug("export_svg()") + + try: + obj = self.collection.get_by_name(str(obj_name)) + except: + return "Could not retrieve object: %s" % obj_name + + with self.proc_container.new("Exporting SVG") as proc: + exported_svg = obj.export_svg(scale_factor=scale_factor) + + # Determine bounding area for svg export + bounds = obj.bounds() + size = obj.size() + + # Convert everything to strings for use in the xml doc + svgwidth = str(size[0]) + svgheight = str(size[1]) + minx = str(bounds[0]) + miny = str(bounds[1] - size[1]) + uom = obj.units.lower() + + # Add a SVG Header and footer to the svg output from shapely + # The transform flips the Y Axis so that everything renders properly within svg apps such as inkscape + svg_header = '' + svg_header += '' + svg_footer = ' ' + svg_elem = svg_header + exported_svg + svg_footer + + # Parse the xml through a xml parser just to add line feeds and to make it look more pretty for the output + doc = parse_xml_string(svg_elem) + with open(filename, 'w') as fp: + fp.write(doc.toprettyxml()) + def import_svg(self, filename, outname=None): """ Adds a new Geometry Object to the projects and populates @@ -2079,7 +2197,7 @@ class App(QtCore.QObject): return a, kwa - from contextlib import contextmanager + @contextmanager def wait_signal(signal, timeout=10000): """Block loop until signal emitted, or timeout (ms) elapses.""" @@ -2094,30 +2212,40 @@ class App(QtCore.QObject): yield + oeh = sys.excepthook + ex = [] + def exceptHook(type_, value, traceback): + ex.append(value) + oeh(type_, value, traceback) + sys.excepthook = exceptHook + if timeout is not None: QtCore.QTimer.singleShot(timeout, report_quit) loop.exec_() + sys.excepthook = oeh + if ex: + self.raiseTclError(str(ex[0])) if status['timed_out']: raise Exception('Timed out!') - def wait_signal2(signal, timeout=10000): - """Block loop until signal emitted, or timeout (ms) elapses.""" - loop = QtCore.QEventLoop() - signal.connect(loop.quit) - status = {'timed_out': False} - - def report_quit(): - status['timed_out'] = True - loop.quit() - - if timeout is not None: - QtCore.QTimer.singleShot(timeout, report_quit) - loop.exec_() - - if status['timed_out']: - raise Exception('Timed out!') + # def wait_signal2(signal, timeout=10000): + # """Block loop until signal emitted, or timeout (ms) elapses.""" + # loop = QtCore.QEventLoop() + # signal.connect(loop.quit) + # status = {'timed_out': False} + # + # def report_quit(): + # status['timed_out'] = True + # loop.quit() + # + # if timeout is not None: + # QtCore.QTimer.singleShot(timeout, report_quit) + # loop.exec_() + # + # if status['timed_out']: + # raise Exception('Timed out!') def mytest(*args): to = int(args[0]) @@ -2142,8 +2270,60 @@ class App(QtCore.QObject): except Exception as e: return str(e) + def mytest2(*args): + to = int(args[0]) + + for rec in self.recent: + if rec['kind'] == 'gerber': + self.open_gerber(str(rec['filename'])) + break + + basename = self.collection.get_names()[0] + isolate(basename, '-passes', '10', '-combine', '1') + iso = self.collection.get_by_name(basename + "_iso") + + with wait_signal(self.new_object_available, to): + 1/0 # Force exception + iso.generatecncjob() + return str(self.collection.get_names()) + def mytest3(*args): + to = int(args[0]) + + def sometask(*args): + time.sleep(2) + self.inform.emit("mytest3") + + with wait_signal(self.inform, to): + self.worker_task.emit({'fcn': sometask, 'params': []}) + + return "mytest3 done" + + def mytest4(*args): + to = int(args[0]) + + def sometask(*args): + time.sleep(2) + 1/0 # Force exception + self.inform.emit("mytest4") + + with wait_signal(self.inform, to): + self.worker_task.emit({'fcn': sometask, 'params': []}) + + return "mytest3 done" + + def export_svg(name, filename, *args): + a, kwa = h(*args) + types = {'scale_factor': float} + + for key in kwa: + if key not in types: + return 'Unknown parameter: %s' % key + kwa[key] = types[key](kwa[key]) + + self.export_svg(str(name), str(filename), **kwa) + def import_svg(filename, *args): a, kwa = h(*args) types = {'outname': str} @@ -3274,6 +3454,18 @@ class App(QtCore.QObject): 'fcn': mytest, 'help': "Test function. Only for testing." }, + 'mytest2': { + 'fcn': mytest2, + 'help': "Test function. Only for testing." + }, + 'mytest3': { + 'fcn': mytest3, + 'help': "Test function. Only for testing." + }, + 'mytest4': { + 'fcn': mytest4, + 'help': "Test function. Only for testing." + }, 'help': { 'fcn': shelp, 'help': "Shows list of commands." @@ -3284,6 +3476,14 @@ class App(QtCore.QObject): "> import_svg " + " filename: Path to the file to import." }, + 'export_svg': { + 'fcn': export_svg, + 'help': "Export a Geometry Object as a SVG File\n" + + "> export_svg [-scale_factor <0.0 (float)>]\n" + + " name: Name of the geometry object to export.\n" + + " filename: Path to the file to export.\n" + + " scale_factor: Multiplication factor used for scaling line widths during export." + }, 'open_gerber': { 'fcn': open_gerber, 'help': "Opens a Gerber file.\n" diff --git a/FlatCAMGUI.py b/FlatCAMGUI.py index 8bb2445..3c01d12 100644 --- a/FlatCAMGUI.py +++ b/FlatCAMGUI.py @@ -48,6 +48,10 @@ class FlatCAMGUI(QtGui.QMainWindow): self.menufileimportsvg = QtGui.QAction(QtGui.QIcon('share/folder16.png'), 'Import &SVG ...', self) self.menufile.addAction(self.menufileimportsvg) + # Export SVG ... + self.menufileexportsvg = QtGui.QAction(QtGui.QIcon('share/folder16.png'), 'Export &SVG ...', self) + self.menufile.addAction(self.menufileexportsvg) + # Save Project self.menufilesaveproject = QtGui.QAction(QtGui.QIcon('share/floppy16.png'), '&Save Project', self) self.menufile.addAction(self.menufilesaveproject) diff --git a/FlatCAMShell.py b/FlatCAMShell.py index 695d7a9..c85e86e 100644 --- a/FlatCAMShell.py +++ b/FlatCAMShell.py @@ -22,4 +22,4 @@ class FCShell(termwidget.TermWidget): return True def child_exec_command(self, text): - self._sysShell.exec_command(text) + self._sysShell.exec_command(text) \ No newline at end of file diff --git a/FlatCAMWorker.py b/FlatCAMWorker.py index 528171b..8e51a7f 100644 --- a/FlatCAMWorker.py +++ b/FlatCAMWorker.py @@ -1,6 +1,4 @@ from PyQt4 import QtCore -#import FlatCAMApp - class Worker(QtCore.QObject): """ @@ -8,12 +6,33 @@ class Worker(QtCore.QObject): in a single independent thread. """ + # avoid multiple tests for debug availability + pydef_failed = False + def __init__(self, app, name=None): super(Worker, self).__init__() self.app = app self.name = name + def allow_debug(self): + """ + allow debuging/breakpoints in this threads + should work from PyCharm and PyDev + :return: + """ + + if not self.pydef_failed: + try: + import pydevd + pydevd.settrace(suspend=False, trace_only_current_thread=True) + except ImportError: + pass + def run(self): + + # allow debuging/breakpoints in this threads + #pydevd.settrace(suspend=False, trace_only_current_thread=True) + # FlatCAMApp.App.log.debug("Worker Started!") self.app.log.debug("Worker Started!") @@ -21,9 +40,12 @@ class Worker(QtCore.QObject): self.app.worker_task.connect(self.do_worker_task) def do_worker_task(self, task): + # FlatCAMApp.App.log.debug("Running task: %s" % str(task)) self.app.log.debug("Running task: %s" % str(task)) + self.allow_debug() + # 'worker_name' property of task allows to target # specific worker. if 'worker_name' in task and task['worker_name'] == self.name: @@ -35,4 +57,4 @@ class Worker(QtCore.QObject): return # FlatCAMApp.App.log.debug("Task ignored.") - self.app.log.debug("Task ignored.") \ No newline at end of file + self.app.log.debug("Task ignored.") diff --git a/camlib.py b/camlib.py index 5a8487a..2a717ea 100644 --- a/camlib.py +++ b/camlib.py @@ -890,6 +890,26 @@ class Geometry(object): """ self.solid_geometry = [cascaded_union(self.solid_geometry)] + def export_svg(self, scale_factor=0.00): + """ + Exports the Gemoetry Object as a SVG Element + + :return: SVG Element + """ + # Make sure we see a Shapely Geometry class and not a list + geom = cascaded_union(self.flatten()) + + # scale_factor is a multiplication factor for the SVG stroke-width used within shapely's svg export + + # If 0 or less which is invalid then default to 0.05 + # This value appears to work for zooming, and getting the output svg line width + # to match that viewed on screen with FlatCam + if scale_factor <= 0: + scale_factor = 0.05 + + # Convert to a SVG + svg_elem = geom.svg(scale_factor=scale_factor) + return svg_elem class ApertureMacro: """ @@ -3334,6 +3354,54 @@ class CNCjob(Geometry): self.create_geometry() + def export_svg(self, scale_factor=0.00): + """ + Exports the CNC Job as a SVG Element + + :scale_factor: float + :return: SVG Element string + """ + # scale_factor is a multiplication factor for the SVG stroke-width used within shapely's svg export + # If not specified then try and use the tool diameter + # This way what is on screen will match what is outputed for the svg + # This is quite a useful feature for svg's used with visicut + + if scale_factor <= 0: + scale_factor = self.options['tooldia'] / 2 + + # If still 0 then defailt to 0.05 + # This value appears to work for zooming, and getting the output svg line width + # to match that viewed on screen with FlatCam + if scale_factor == 0: + scale_factor = 0.05 + + # Seperate the list of cuts and travels into 2 distinct lists + # This way we can add different formatting / colors to both + cuts = [] + travels = [] + for g in self.gcode_parsed: + if g['kind'][0] == 'C': cuts.append(g) + if g['kind'][0] == 'T': travels.append(g) + + # Used to determine the overall board size + self.solid_geometry = cascaded_union([geo['geom'] for geo in self.gcode_parsed]) + + # Convert the cuts and travels into single geometry objects we can render as svg xml + if travels: + travelsgeom = cascaded_union([geo['geom'] for geo in travels]) + if cuts: + cutsgeom = cascaded_union([geo['geom'] for geo in cuts]) + + # Render the SVG Xml + # The scale factor affects the size of the lines, and the stroke color adds different formatting for each set + # It's better to have the travels sitting underneath the cuts for visicut + svg_elem = "" + if travels: + svg_elem = travelsgeom.svg(scale_factor=scale_factor, stroke_color="#F0E24D") + if cuts: + svg_elem += cutsgeom.svg(scale_factor=scale_factor, stroke_color="#5E6CFF") + + return svg_elem # def get_bounds(geometry_set): # xmin = Inf diff --git a/tclCommands/TclCommand.py b/tclCommands/TclCommand.py index 7f8a7e8..a446a15 100644 --- a/tclCommands/TclCommand.py +++ b/tclCommands/TclCommand.py @@ -1,3 +1,4 @@ +import sys import re import FlatCAMApp import abc @@ -41,6 +42,9 @@ class TclCommand(object): 'examples': [] } + # original incoming arguments into command + original_args = None + def __init__(self, app): self.app = app if self.app is None: @@ -59,6 +63,18 @@ class TclCommand(object): self.app.raise_tcl_error(text) + def get_current_command(self): + """ + get current command, we are not able to get it from TCL we have to reconstruct it + :return: current command + """ + command_string = [] + command_string.append(self.aliases[0]) + if self.original_args is not None: + for arg in self.original_args: + command_string.append(arg) + return " ".join(command_string) + def get_decorated_help(self): """ Decorate help for TCL console output. @@ -176,7 +192,7 @@ class TclCommand(object): # check options for key in options: - if key not in self.option_types: + if key not in self.option_types and key is not 'timeout': self.raise_tcl_error('Unknown parameter: %s' % key) try: named_args[key] = self.option_types[key](options[key]) @@ -201,8 +217,11 @@ class TclCommand(object): :return: None, output text or exception """ + #self.worker_task.emit({'fcn': self.exec_command_test, 'params': [text, False]}) + try: self.log.debug("TCL command '%s' executed." % str(self.__class__)) + self.original_args=args args, unnamed_args = self.check_args(args) return self.execute(args, unnamed_args) except Exception as unknown: @@ -239,9 +258,15 @@ class TclCommandSignaled(TclCommand): it handles all neccessary stuff about blocking and passing exeptions """ - # default timeout for operation is 30 sec, but it can be much more - default_timeout = 30000 + # default timeout for operation is 300 sec, but it can be much more + default_timeout = 300000 + output = None + + def execute_call(self, args, unnamed_args): + + self.output = self.execute(args, unnamed_args) + self.app.shell_command_finished.emit(self) def execute_wrapper(self, *args): """ @@ -254,7 +279,7 @@ class TclCommandSignaled(TclCommand): """ @contextmanager - def wait_signal(signal, timeout=30000): + def wait_signal(signal, timeout=300000): """Block loop until signal emitted, or timeout (ms) elapses.""" loop = QtCore.QEventLoop() signal.connect(loop.quit) @@ -267,27 +292,43 @@ class TclCommandSignaled(TclCommand): yield + oeh = sys.excepthook + ex = [] + def exceptHook(type_, value, traceback): + ex.append(value) + oeh(type_, value, traceback) + sys.excepthook = exceptHook + if timeout is not None: QtCore.QTimer.singleShot(timeout, report_quit) loop.exec_() + sys.excepthook = oeh + if ex: + self.raise_tcl_error(str(ex[0])) + if status['timed_out']: self.app.raise_tcl_unknown_error('Operation timed out!') try: self.log.debug("TCL command '%s' executed." % str(self.__class__)) + self.original_args=args args, unnamed_args = self.check_args(args) if 'timeout' in args: passed_timeout=args['timeout'] del args['timeout'] else: passed_timeout=self.default_timeout - with wait_signal(self.app.new_object_available, passed_timeout): + + # set detail for processing, it will be there until next open or close + self.app.shell.open_proccessing(self.get_current_command()) + + with wait_signal(self.app.shell_command_finished, passed_timeout): # every TclCommandNewObject ancestor support timeout as parameter, # but it does not mean anything for child itself # when operation will be really long is good to set it higher then defqault 30s - return self.execute(args, unnamed_args) + self.app.worker_task.emit({'fcn': self.execute_call, 'params': [args, unnamed_args]}) except Exception as unknown: self.log.error("TCL command '%s' failed." % str(self)) diff --git a/tclCommands/TclCommandAddPolygon.py b/tclCommands/TclCommandAddPolygon.py index 6d2c2af..c9e3507 100644 --- a/tclCommands/TclCommandAddPolygon.py +++ b/tclCommands/TclCommandAddPolygon.py @@ -2,7 +2,7 @@ from ObjectCollection import * import TclCommand -class TclCommandAddPolygon(TclCommand.TclCommand): +class TclCommandAddPolygon(TclCommand.TclCommandSignaled): """ Tcl shell command to create a polygon in the given Geometry object """ diff --git a/tclCommands/TclCommandAddPolyline.py b/tclCommands/TclCommandAddPolyline.py index 57b8fe0..3c99476 100644 --- a/tclCommands/TclCommandAddPolyline.py +++ b/tclCommands/TclCommandAddPolyline.py @@ -2,7 +2,7 @@ from ObjectCollection import * import TclCommand -class TclCommandAddPolyline(TclCommand.TclCommand): +class TclCommandAddPolyline(TclCommand.TclCommandSignaled): """ Tcl shell command to create a polyline in the given Geometry object """ diff --git a/tclCommands/TclCommandCncjob.py b/tclCommands/TclCommandCncjob.py index 17a677e..e088d0e 100644 --- a/tclCommands/TclCommandCncjob.py +++ b/tclCommands/TclCommandCncjob.py @@ -2,7 +2,7 @@ from ObjectCollection import * import TclCommand -class TclCommandCncjob(TclCommand.TclCommand): +class TclCommandCncjob(TclCommand.TclCommandSignaled): """ Tcl shell command to Generates a CNC Job from a Geometry Object. @@ -70,11 +70,6 @@ class TclCommandCncjob(TclCommand.TclCommand): if 'outname' not in args: args['outname'] = name + "_cnc" - if 'timeout' in args: - timeout = args['timeout'] - else: - timeout = 10000 - obj = self.app.collection.get_by_name(name) if obj is None: self.raise_tcl_error("Object not found: %s" % name) diff --git a/tclCommands/TclCommandExportGcode.py b/tclCommands/TclCommandExportGcode.py index 520f6ec..feecd87 100644 --- a/tclCommands/TclCommandExportGcode.py +++ b/tclCommands/TclCommandExportGcode.py @@ -2,7 +2,7 @@ from ObjectCollection import * import TclCommand -class TclCommandExportGcode(TclCommand.TclCommand): +class TclCommandExportGcode(TclCommand.TclCommandSignaled): """ Tcl shell command to export gcode as tcl output for "set X [export_gcode ...]" diff --git a/tclCommands/TclCommandExteriors.py b/tclCommands/TclCommandExteriors.py index 16f2fee..ac69e7c 100644 --- a/tclCommands/TclCommandExteriors.py +++ b/tclCommands/TclCommandExteriors.py @@ -2,7 +2,7 @@ from ObjectCollection import * import TclCommand -class TclCommandExteriors(TclCommand.TclCommand): +class TclCommandExteriors(TclCommand.TclCommandSignaled): """ Tcl shell command to get exteriors of polygons """ @@ -57,7 +57,7 @@ class TclCommandExteriors(TclCommand.TclCommand): if not isinstance(obj, Geometry): self.raise_tcl_error('Expected Geometry, got %s %s.' % (name, type(obj))) - def geo_init(geo_obj): + def geo_init(geo_obj, app_obj): geo_obj.solid_geometry = obj_exteriors obj_exteriors = obj.get_exteriors() diff --git a/tclCommands/TclCommandInteriors.py b/tclCommands/TclCommandInteriors.py index 2314be3..61bfe9f 100644 --- a/tclCommands/TclCommandInteriors.py +++ b/tclCommands/TclCommandInteriors.py @@ -2,7 +2,7 @@ from ObjectCollection import * import TclCommand -class TclCommandInteriors(TclCommand.TclCommand): +class TclCommandInteriors(TclCommand.TclCommandSignaled): """ Tcl shell command to get interiors of polygons """ @@ -57,7 +57,7 @@ class TclCommandInteriors(TclCommand.TclCommand): if not isinstance(obj, Geometry): self.raise_tcl_error('Expected Geometry, got %s %s.' % (name, type(obj))) - def geo_init(geo_obj): + def geo_init(geo_obj, app_obj): geo_obj.solid_geometry = obj_exteriors obj_exteriors = obj.get_interiors() diff --git a/tclCommands/TclCommandIsolate.py b/tclCommands/TclCommandIsolate.py new file mode 100644 index 0000000..8c51f21 --- /dev/null +++ b/tclCommands/TclCommandIsolate.py @@ -0,0 +1,79 @@ +from ObjectCollection import * +import TclCommand + + +class TclCommandIsolate(TclCommand.TclCommandSignaled): + """ + Tcl shell command to Creates isolation routing geometry for the given Gerber. + + example: + set_sys units MM + new + open_gerber tests/gerber_files/simple1.gbr -outname margin + isolate margin -dia 3 + cncjob margin_iso + """ + + # array of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon) + aliases = ['isolate'] + + # dictionary of types from Tcl command, needs to be ordered + arg_names = collections.OrderedDict([ + ('name', str) + ]) + + # dictionary of types from Tcl command, needs to be ordered , this is for options like -optionname value + option_types = collections.OrderedDict([ + ('dia',float), + ('passes',int), + ('overlap',float), + ('combine',int), + ('outname',str) + ]) + + # array of mandatory options for current Tcl command: required = {'name','outname'} + required = ['name'] + + # structured help for current command, args needs to be ordered + help = { + 'main': "Creates isolation routing geometry for the given Gerber.", + 'args': collections.OrderedDict([ + ('name', 'Name of the source object.'), + ('dia', 'Tool diameter.'), + ('passes', 'Passes of tool width.'), + ('overlap', 'Fraction of tool diameter to overlap passes.'), + ('combine', 'Combine all passes into one geometry.'), + ('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 + """ + + name = args['name'] + + if 'outname' not in args: + args['outname'] = name + "_iso" + + if 'timeout' in args: + timeout = args['timeout'] + else: + timeout = 10000 + + obj = self.app.collection.get_by_name(name) + if obj is None: + self.raise_tcl_error("Object not found: %s" % name) + + if not isinstance(obj, FlatCAMGerber): + self.raise_tcl_error('Expected FlatCAMGerber, got %s %s.' % (name, type(obj))) + + del args['name'] + obj.isolate(**args) diff --git a/tclCommands/TclCommandNew.py b/tclCommands/TclCommandNew.py new file mode 100644 index 0000000..db3fe57 --- /dev/null +++ b/tclCommands/TclCommandNew.py @@ -0,0 +1,40 @@ +from ObjectCollection import * +from PyQt4 import QtCore +import TclCommand + + +class TclCommandNew(TclCommand.TclCommand): + """ + Tcl shell command to starts a new project. Clears objects from memory + """ + + # array of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon) + aliases = ['new'] + + # dictionary of types from Tcl command, needs to be ordered + arg_names = collections.OrderedDict() + + # dictionary of types from Tcl command, needs to be ordered , this is for options like -optionname value + option_types = collections.OrderedDict() + + # array of mandatory options for current Tcl command: required = {'name','outname'} + required = [] + + # structured help for current command, args needs to be ordered + help = { + 'main': "Starts a new project. Clears objects from memory.", + 'args': collections.OrderedDict(), + '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 + """ + + self.app.on_file_new() diff --git a/tclCommands/TclCommandOpenGerber.py b/tclCommands/TclCommandOpenGerber.py new file mode 100644 index 0000000..a951d8f --- /dev/null +++ b/tclCommands/TclCommandOpenGerber.py @@ -0,0 +1,95 @@ +from ObjectCollection import * +import TclCommand + + +class TclCommandOpenGerber(TclCommand.TclCommandSignaled): + """ + Tcl shell command to opens a Gerber file + """ + + # array of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon) + aliases = ['open_gerber'] + + # 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([ + ('follow', 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': "Opens a Gerber file.", + 'args': collections.OrderedDict([ + ('filename', 'Path to file to open.'), + ('follow', 'N If 1, does not create polygons, just follows the gerber path.'), + ('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(gerber_obj, app_obj): + + if not isinstance(gerber_obj, Geometry): + self.raise_tcl_error('Expected FlatCAMGerber, got %s %s.' % (outname, type(gerber_obj))) + + # Opening the file happens here + self.app.progress.emit(30) + try: + gerber_obj.parse_file(filename, follow=follow) + + except IOError: + app_obj.inform.emit("[error] Failed to open file: %s " % filename) + app_obj.progress.emit(0) + self.raise_tcl_error('Failed to open file: %s' % filename) + + except ParseError, e: + app_obj.inform.emit("[error] Failed to parse file: %s, %s " % (filename, str(e))) + app_obj.progress.emit(0) + self.log.error(str(e)) + raise + + # Further parsing + app_obj.progress.emit(70) + + filename = args['filename'] + + if 'outname' in args: + outname = args['outname'] + else: + outname = filename.split('/')[-1].split('\\')[-1] + + follow = None + if 'follow' in args: + follow = args['follow'] + + with self.app.proc_container.new("Opening Gerber"): + + # Object creation + self.app.new_object("gerber", outname, obj_init) + + # Register recent file + self.app.file_opened.emit("gerber", filename) + + self.app.progress.emit(100) + + # GUI feedback + self.app.inform.emit("Opened: " + filename) diff --git a/tclCommands/__init__.py b/tclCommands/__init__.py index 3055dbc..af67a9c 100644 --- a/tclCommands/__init__.py +++ b/tclCommands/__init__.py @@ -2,12 +2,16 @@ import pkgutil import sys # allowed command modules -import tclCommands.TclCommandExteriors -import tclCommands.TclCommandInteriors import tclCommands.TclCommandAddPolygon import tclCommands.TclCommandAddPolyline -import tclCommands.TclCommandExportGcode import tclCommands.TclCommandCncjob +import tclCommands.TclCommandExportGcode +import tclCommands.TclCommandExteriors +import tclCommands.TclCommandInteriors +import tclCommands.TclCommandIsolate +import tclCommands.TclCommandNew +import tclCommands.TclCommandOpenGerber + __all__ = [] diff --git a/termwidget.py b/termwidget.py index d6309fd..94bbb80 100644 --- a/termwidget.py +++ b/termwidget.py @@ -4,7 +4,7 @@ Shows intput and output text. Allows to enter commands. Supports history. """ import cgi - +from PyQt4 import QtCore from PyQt4.QtCore import pyqtSignal from PyQt4.QtGui import QColor, QKeySequence, QLineEdit, QPalette, \ QSizePolicy, QTextCursor, QTextEdit, \ @@ -113,6 +113,32 @@ class TermWidget(QWidget): self._edit.setFocus() + def open_proccessing(self, detail=None): + """ + Open processing and disable using shell commands again until all commands are finished + :return: + """ + + self._edit.setTextColor(QtCore.Qt.white) + self._edit.setTextBackgroundColor(QtCore.Qt.darkGreen) + if detail is None: + self._edit.setPlainText("...proccessing...") + else: + self._edit.setPlainText("...proccessing... [%s]" % detail) + + self._edit.setDisabled(True) + + def close_proccessing(self): + """ + Close processing and enable using shell commands again + :return: + """ + + self._edit.setTextColor(QtCore.Qt.black) + self._edit.setTextBackgroundColor(QtCore.Qt.white) + self._edit.setPlainText('') + self._edit.setDisabled(False) + def _append_to_browser(self, style, text): """ Convert text to HTML for inserting it to browser diff --git a/tests/test_tcl_shell.py b/tests/test_tcl_shell.py index 3d75f6a..526354f 100644 --- a/tests/test_tcl_shell.py +++ b/tests/test_tcl_shell.py @@ -8,7 +8,7 @@ from time import sleep import os import tempfile -class TclShellCommandTest(unittest.TestCase): +class TclShellTest(unittest.TestCase): gerber_files = 'tests/gerber_files' copper_bottom_filename = 'detector_copper_bottom.gbr' @@ -30,12 +30,21 @@ class TclShellCommandTest(unittest.TestCase): # user-defined defaults). self.fc = App(user_defaults=False) + self.fc.shell.show() + pass + def tearDown(self): + self.app.closeAllWindows() + del self.fc del self.app + pass def test_set_get_units(self): + self.fc.exec_command_test('set_sys units MM') + self.fc.exec_command_test('new') + self.fc.exec_command_test('set_sys units IN') self.fc.exec_command_test('new') units=self.fc.exec_command_test('get_sys units') @@ -46,6 +55,7 @@ class TclShellCommandTest(unittest.TestCase): units=self.fc.exec_command_test('get_sys units') self.assertEquals(units, "MM") + def test_gerber_flow(self): # open gerber files top, bottom and cutout @@ -139,9 +149,21 @@ class TclShellCommandTest(unittest.TestCase): # TODO: tests for tcl + def test_open_gerber(self): + + self.fc.exec_command_test('set_sys units MM') + self.fc.exec_command_test('new') + + self.fc.exec_command_test('open_gerber %s/%s -outname %s' % (self.gerber_files, self.copper_top_filename, self.gerber_top_name)) + gerber_top_obj = self.fc.collection.get_by_name(self.gerber_top_name) + self.assertTrue(isinstance(gerber_top_obj, FlatCAMGerber), + "Expected FlatCAMGerber, instead, %s is %s" % + (self.gerber_top_name, type(gerber_top_obj))) + def test_excellon_flow(self): self.fc.exec_command_test('set_sys units MM') + self.fc.exec_command_test('new') self.fc.exec_command_test('open_excellon %s/%s -outname %s' % (self.gerber_files, self.excellon_filename, self.excellon_name)) excellon_obj = self.fc.collection.get_by_name(self.excellon_name) self.assertTrue(isinstance(excellon_obj, FlatCAMExcellon), From cac2f74be2788c71fdb76088415d326b70c07bb7 Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Thu, 24 Mar 2016 23:23:27 +0100 Subject: [PATCH 082/134] fix pydevd_failed typo and it was not reset to True --- FlatCAMWorker.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/FlatCAMWorker.py b/FlatCAMWorker.py index 8e51a7f..90edaa7 100644 --- a/FlatCAMWorker.py +++ b/FlatCAMWorker.py @@ -7,7 +7,7 @@ class Worker(QtCore.QObject): """ # avoid multiple tests for debug availability - pydef_failed = False + pydevd_failed = False def __init__(self, app, name=None): super(Worker, self).__init__() @@ -21,12 +21,12 @@ class Worker(QtCore.QObject): :return: """ - if not self.pydef_failed: + if not self.pydevd_failed: try: import pydevd pydevd.settrace(suspend=False, trace_only_current_thread=True) except ImportError: - pass + self.pydevd_failed=True def run(self): From 2082446ab04b6e173d491cd7efe4984039fd6915 Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Fri, 25 Mar 2016 00:59:02 +0100 Subject: [PATCH 083/134] tweak signal handling --- FlatCAMApp.py | 4 ++++ FlatCAMWorker.py | 22 ++++++++++++++++----- tclCommands/TclCommand.py | 41 ++++++++++++++++++++++++++++++--------- tests/test_tcl_shell.py | 26 ++++++++++++++++--------- 4 files changed, 70 insertions(+), 23 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index dd917bb..0ed5b89 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -108,6 +108,10 @@ class App(QtCore.QObject): # Emmited when shell command is finished(one command only) shell_command_finished = QtCore.pyqtSignal(object) + # Emitted when an unhandled exception happens + # in the worker task. + thread_exception = QtCore.pyqtSignal(object) + message = QtCore.pyqtSignal(str, str, str) def __init__(self, user_defaults=True, post_gui=None): diff --git a/FlatCAMWorker.py b/FlatCAMWorker.py index 90edaa7..a1f49a8 100644 --- a/FlatCAMWorker.py +++ b/FlatCAMWorker.py @@ -48,12 +48,24 @@ class Worker(QtCore.QObject): # 'worker_name' property of task allows to target # specific worker. - if 'worker_name' in task and task['worker_name'] == self.name: - task['fcn'](*task['params']) - return + #if 'worker_name' in task and task['worker_name'] == self.name: + # task['fcn'](*task['params']) + # return + + #if 'worker_name' not in task and self.name is None: + # task['fcn'](*task['params']) + # return + + + if ('worker_name' in task and task['worker_name'] == self.name) or \ + ('worker_name' not in task and self.name is None): + + try: + task['fcn'](*task['params']) + except Exception as e: + self.app.thread_exception.emit(e) + raise e - if 'worker_name' not in task and self.name is None: - task['fcn'](*task['params']) return # FlatCAMApp.App.log.debug("Task ignored.") diff --git a/tclCommands/TclCommand.py b/tclCommands/TclCommand.py index a446a15..f713b31 100644 --- a/tclCommands/TclCommand.py +++ b/tclCommands/TclCommand.py @@ -258,15 +258,17 @@ class TclCommandSignaled(TclCommand): it handles all neccessary stuff about blocking and passing exeptions """ - # default timeout for operation is 300 sec, but it can be much more - default_timeout = 300000 + # default timeout for operation is 10 sec, but it can be much more + default_timeout = 10000 output = None def execute_call(self, args, unnamed_args): - self.output = self.execute(args, unnamed_args) - self.app.shell_command_finished.emit(self) + try: + self.output = self.execute(args, unnamed_args) + finally: + self.app.shell_command_finished.emit(self) def execute_wrapper(self, *args): """ @@ -279,11 +281,16 @@ class TclCommandSignaled(TclCommand): """ @contextmanager - def wait_signal(signal, timeout=300000): + def wait_signal(signal, timeout=10000): """Block loop until signal emitted, or timeout (ms) elapses.""" loop = QtCore.QEventLoop() + + # Normal termination signal.connect(loop.quit) + # Termination by exception in thread + self.app.thread_exception.connect(loop.quit) + status = {'timed_out': False} def report_quit(): @@ -292,18 +299,23 @@ class TclCommandSignaled(TclCommand): yield + # Temporarily change how exceptions are managed. oeh = sys.excepthook ex = [] - def exceptHook(type_, value, traceback): - ex.append(value) - oeh(type_, value, traceback) - sys.excepthook = exceptHook + def except_hook(type_, value, traceback_): + ex.append(value) + oeh(type_, value, traceback_) + sys.excepthook = except_hook + + # Terminate on timeout if timeout is not None: QtCore.QTimer.singleShot(timeout, report_quit) + # Block loop.exec_() + # Restore exception management sys.excepthook = oeh if ex: self.raise_tcl_error(str(ex[0])) @@ -324,12 +336,23 @@ class TclCommandSignaled(TclCommand): # set detail for processing, it will be there until next open or close self.app.shell.open_proccessing(self.get_current_command()) + self.output = None + + def handle_finished(obj): + self.app.shell_command_finished.disconnect(handle_finished) + # TODO: handle output + pass + + self.app.shell_command_finished.connect(handle_finished) + with wait_signal(self.app.shell_command_finished, passed_timeout): # every TclCommandNewObject ancestor support timeout as parameter, # but it does not mean anything for child itself # when operation will be really long is good to set it higher then defqault 30s self.app.worker_task.emit({'fcn': self.execute_call, 'params': [args, unnamed_args]}) + return self.output + except Exception as unknown: self.log.error("TCL command '%s' failed." % str(self)) self.app.raise_tcl_unknown_error(unknown) \ No newline at end of file diff --git a/tests/test_tcl_shell.py b/tests/test_tcl_shell.py index 526354f..ecf60f5 100644 --- a/tests/test_tcl_shell.py +++ b/tests/test_tcl_shell.py @@ -1,6 +1,8 @@ import sys import unittest from PyQt4 import QtGui +from PyQt4.QtCore import QThread + from FlatCAMApp import App from FlatCAMObj import FlatCAMGerber, FlatCAMGeometry, FlatCAMCNCjob, FlatCAMExcellon from ObjectUI import GerberObjectUI, GeometryObjectUI @@ -10,6 +12,8 @@ import tempfile class TclShellTest(unittest.TestCase): + setup = False + gerber_files = 'tests/gerber_files' copper_bottom_filename = 'detector_copper_bottom.gbr' copper_top_filename = 'detector_copper_top.gbr' @@ -24,20 +28,23 @@ class TclShellTest(unittest.TestCase): drill_diameter = 0.8 def setUp(self): - self.app = QtGui.QApplication(sys.argv) - # Create App, keep app defaults (do not load - # user-defined defaults). - self.fc = App(user_defaults=False) + if not self.setup: + self.setup=True + self.app = QtGui.QApplication(sys.argv) - self.fc.shell.show() + # Create App, keep app defaults (do not load + # user-defined defaults). + self.fc = App(user_defaults=False) + + self.fc.shell.show() pass def tearDown(self): - self.app.closeAllWindows() - - del self.fc - del self.app + #self.fc.tcl=None + #self.app.closeAllWindows() + #del self.fc + #del self.app pass def test_set_get_units(self): @@ -60,6 +67,7 @@ class TclShellTest(unittest.TestCase): # open gerber files top, bottom and cutout + self.fc.exec_command_test('set_sys units MM') self.fc.exec_command_test('new') From f61aa397d4b577bcbd5e2517ff6c0138756793a1 Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Fri, 25 Mar 2016 11:12:43 +0100 Subject: [PATCH 084/134] fix test hanging for shell --- tests/test_tcl_shell.py | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/tests/test_tcl_shell.py b/tests/test_tcl_shell.py index ecf60f5..5813c23 100644 --- a/tests/test_tcl_shell.py +++ b/tests/test_tcl_shell.py @@ -12,8 +12,6 @@ import tempfile class TclShellTest(unittest.TestCase): - setup = False - gerber_files = 'tests/gerber_files' copper_bottom_filename = 'detector_copper_bottom.gbr' copper_top_filename = 'detector_copper_top.gbr' @@ -27,24 +25,22 @@ class TclShellTest(unittest.TestCase): cutout_diameter = 3 drill_diameter = 0.8 - def setUp(self): + @classmethod + def setUpClass(self): - if not self.setup: - self.setup=True - self.app = QtGui.QApplication(sys.argv) + self.setup=True + self.app = QtGui.QApplication(sys.argv) + # Create App, keep app defaults (do not load + # user-defined defaults). + self.fc = App(user_defaults=False) + self.fc.shell.show() - # Create App, keep app defaults (do not load - # user-defined defaults). - self.fc = App(user_defaults=False) - - self.fc.shell.show() - pass - - def tearDown(self): - #self.fc.tcl=None - #self.app.closeAllWindows() - #del self.fc - #del self.app + @classmethod + def tearDownClass(self): + self.fc.tcl=None + self.app.closeAllWindows() + del self.fc + del self.app pass def test_set_get_units(self): From 4a57e437fc4cf4ef03768d8623f242391d10443b Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Fri, 25 Mar 2016 12:16:54 +0100 Subject: [PATCH 085/134] Implement shell window as dockable --- FlatCAMApp.py | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 5933523..0a4873a 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -476,7 +476,7 @@ class App(QtCore.QObject): self.ui.menuviewdisableall.triggered.connect(self.disable_plots) self.ui.menuviewdisableother.triggered.connect(lambda: self.disable_plots(except_current=True)) self.ui.menuviewenable.triggered.connect(self.enable_all_plots) - self.ui.menutoolshell.triggered.connect(lambda: self.shell.show()) + self.ui.menutoolshell.triggered.connect(self.on_toggle_shell) self.ui.menuhelp_about.triggered.connect(self.on_about) self.ui.menuhelp_home.triggered.connect(lambda: webbrowser.open(self.app_url)) self.ui.menuhelp_manual.triggered.connect(lambda: webbrowser.open(self.manual_url)) @@ -490,7 +490,7 @@ class App(QtCore.QObject): self.ui.editgeo_btn.triggered.connect(self.edit_geometry) self.ui.updategeo_btn.triggered.connect(self.editor2geometry) self.ui.delete_btn.triggered.connect(self.on_delete) - self.ui.shell_btn.triggered.connect(lambda: self.shell.show()) + self.ui.shell_btn.triggered.connect(self.on_toggle_shell) # Object list self.collection.view.activated.connect(self.on_row_activated) # Options @@ -525,14 +525,25 @@ class App(QtCore.QObject): self.shell = FCShell(self) self.shell.setWindowIcon(self.ui.app_icon) self.shell.setWindowTitle("FlatCAM Shell") - if self.defaults["shell_at_startup"]: - self.shell.show() self.shell.resize(*self.defaults["shell_shape"]) self.shell.append_output("FlatCAM %s\n(c) 2014-2015 Juan Pablo Caram\n\n" % self.version) self.shell.append_output("Type help to get started.\n\n") self.tcl = Tkinter.Tcl() self.setup_shell() + + self.ui.shell_dock = QtGui.QDockWidget("FlatCAM TCL Shell") + self.ui.shell_dock.setWidget(self.shell) + self.ui.shell_dock.setAllowedAreas(QtCore.Qt.AllDockWidgetAreas) + self.ui.shell_dock.setFeatures(QtGui.QDockWidget.DockWidgetMovable | + QtGui.QDockWidget.DockWidgetFloatable | QtGui.QDockWidget.DockWidgetClosable) + self.ui.addDockWidget(QtCore.Qt.BottomDockWidgetArea, self.ui.shell_dock) + + if self.defaults["shell_at_startup"]: + self.ui.shell_dock.show() + else: + self.ui.shell_dock.hide() + if self.cmd_line_shellfile: try: with open(self.cmd_line_shellfile, "r") as myfile: @@ -1009,6 +1020,16 @@ class App(QtCore.QObject): if not silent: self.inform.emit("Defaults saved.") + def on_toggle_shell(self): + """ + toggle shell if is visible close it if closed open it + :return: + """ + if self.ui.shell_dock.isVisible(): + self.ui.shell_dock.hide() + else: + self.ui.shell_dock.show() + def on_edit_join(self): """ Callback for Edit->Join. Joins the selected geometry objects into From 5ec25ebea6e10b19d20ba36bdbdbfab0b476fec7 Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Fri, 25 Mar 2016 12:24:57 +0100 Subject: [PATCH 086/134] remove blank line --- FlatCAMApp.py | 1 - 1 file changed, 1 deletion(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 0a4873a..c510b74 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -531,7 +531,6 @@ class App(QtCore.QObject): self.tcl = Tkinter.Tcl() self.setup_shell() - self.ui.shell_dock = QtGui.QDockWidget("FlatCAM TCL Shell") self.ui.shell_dock.setWidget(self.shell) self.ui.shell_dock.setAllowedAreas(QtCore.Qt.AllDockWidgetAreas) From 80d6c657d5bd7ef5e45cdf312934a314a384190f Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Fri, 25 Mar 2016 13:56:18 +0100 Subject: [PATCH 087/134] merge changes from master merge dockable shell --- FlatCAMApp.py | 61 ++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 48 insertions(+), 13 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 0ed5b89..d17bbbc 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -10,6 +10,7 @@ import os import Tkinter from PyQt4 import QtCore import time # Just used for debugging. Double check before removing. +from xml.dom.minidom import parseString as parse_xml_string from contextlib import contextmanager ######################################## @@ -26,7 +27,6 @@ from FlatCAMDraw import FlatCAMDraw from FlatCAMProcess import * from MeasurementTool import Measurement from DblSidedTool import DblSidedTool -from xml.dom.minidom import parseString as parse_xml_string import tclCommands ######################################## @@ -105,6 +105,8 @@ class App(QtCore.QObject): # and is ready to be used. new_object_available = QtCore.pyqtSignal(object) + message = QtCore.pyqtSignal(str, str, str) + # Emmited when shell command is finished(one command only) shell_command_finished = QtCore.pyqtSignal(object) @@ -112,8 +114,6 @@ class App(QtCore.QObject): # in the worker task. thread_exception = QtCore.pyqtSignal(object) - message = QtCore.pyqtSignal(str, str, str) - def __init__(self, user_defaults=True, post_gui=None): """ Starts the application. @@ -479,7 +479,7 @@ class App(QtCore.QObject): self.ui.menuviewdisableall.triggered.connect(self.disable_plots) self.ui.menuviewdisableother.triggered.connect(lambda: self.disable_plots(except_current=True)) self.ui.menuviewenable.triggered.connect(self.enable_all_plots) - self.ui.menutoolshell.triggered.connect(lambda: self.shell.show()) + self.ui.menutoolshell.triggered.connect(self.on_toggle_shell) self.ui.menuhelp_about.triggered.connect(self.on_about) self.ui.menuhelp_home.triggered.connect(lambda: webbrowser.open(self.app_url)) self.ui.menuhelp_manual.triggered.connect(lambda: webbrowser.open(self.manual_url)) @@ -493,7 +493,7 @@ class App(QtCore.QObject): self.ui.editgeo_btn.triggered.connect(self.edit_geometry) self.ui.updategeo_btn.triggered.connect(self.editor2geometry) self.ui.delete_btn.triggered.connect(self.on_delete) - self.ui.shell_btn.triggered.connect(lambda: self.shell.show()) + self.ui.shell_btn.triggered.connect(self.on_toggle_shell) # Object list self.collection.view.activated.connect(self.on_row_activated) # Options @@ -528,14 +528,24 @@ class App(QtCore.QObject): self.shell = FCShell(self) self.shell.setWindowIcon(self.ui.app_icon) self.shell.setWindowTitle("FlatCAM Shell") - if self.defaults["shell_at_startup"]: - self.shell.show() self.shell.resize(*self.defaults["shell_shape"]) self.shell.append_output("FlatCAM %s\n(c) 2014-2015 Juan Pablo Caram\n\n" % self.version) self.shell.append_output("Type help to get started.\n\n") self.init_tcl() + self.ui.shell_dock = QtGui.QDockWidget("FlatCAM TCL Shell") + self.ui.shell_dock.setWidget(self.shell) + self.ui.shell_dock.setAllowedAreas(QtCore.Qt.AllDockWidgetAreas) + self.ui.shell_dock.setFeatures(QtGui.QDockWidget.DockWidgetMovable | + QtGui.QDockWidget.DockWidgetFloatable | QtGui.QDockWidget.DockWidgetClosable) + self.ui.addDockWidget(QtCore.Qt.BottomDockWidgetArea, self.ui.shell_dock) + + if self.defaults["shell_at_startup"]: + self.ui.shell_dock.show() + else: + self.ui.shell_dock.hide() + if self.cmd_line_shellfile: try: with open(self.cmd_line_shellfile, "r") as myfile: @@ -1064,6 +1074,16 @@ class App(QtCore.QObject): if not silent: self.inform.emit("Defaults saved.") + def on_toggle_shell(self): + """ + toggle shell if is visible close it if closed open it + :return: + """ + if self.ui.shell_dock.isVisible(): + self.ui.shell_dock.hide() + else: + self.ui.shell_dock.show() + def on_edit_join(self): """ Callback for Edit->Join. Joins the selected geometry objects into @@ -2201,13 +2221,22 @@ class App(QtCore.QObject): return a, kwa - @contextmanager def wait_signal(signal, timeout=10000): - """Block loop until signal emitted, or timeout (ms) elapses.""" + """ + Block loop until signal emitted, timeout (ms) elapses + or unhandled exception happens in a thread. + + :param signal: Signal to wait for. + """ loop = QtCore.QEventLoop() + + # Normal termination signal.connect(loop.quit) + # Termination by exception in thread + self.thread_exception.connect(loop.quit) + status = {'timed_out': False} def report_quit(): @@ -2216,17 +2245,23 @@ class App(QtCore.QObject): yield + # Temporarily change how exceptions are managed. oeh = sys.excepthook ex = [] - def exceptHook(type_, value, traceback): - ex.append(value) - oeh(type_, value, traceback) - sys.excepthook = exceptHook + def except_hook(type_, value, traceback_): + ex.append(value) + oeh(type_, value, traceback_) + sys.excepthook = except_hook + + # Terminate on timeout if timeout is not None: QtCore.QTimer.singleShot(timeout, report_quit) + #### Block #### loop.exec_() + + # Restore exception management sys.excepthook = oeh if ex: self.raiseTclError(str(ex[0])) From 05a9e05c97c3cab7f44d7c3e334864b362ac2c53 Mon Sep 17 00:00:00 2001 From: Juan Pablo Caram Date: Fri, 25 Mar 2016 21:28:02 -0400 Subject: [PATCH 088/134] Removed background highlighting in shell. --- termwidget.py | 61 +++++++++++++++++++-------------------------------- 1 file changed, 22 insertions(+), 39 deletions(-) diff --git a/termwidget.py b/termwidget.py index d6309fd..36487b6 100644 --- a/termwidget.py +++ b/termwidget.py @@ -19,13 +19,13 @@ class _ExpandableTextEdit(QTextEdit): historyNext = pyqtSignal() historyPrev = pyqtSignal() - def __init__(self, termWidget, *args): + def __init__(self, termwidget, *args): QTextEdit.__init__(self, *args) self.setStyleSheet("font: 9pt \"Courier\";") self._fittedHeight = 1 self.textChanged.connect(self._fit_to_document) self._fit_to_document() - self._termWidget = termWidget + self._termWidget = termwidget def sizeHint(self): """ @@ -39,10 +39,10 @@ class _ExpandableTextEdit(QTextEdit): """ Update widget height to fit all text """ - documentSize = self.document().size().toSize() - self._fittedHeight = documentSize.height() + (self.height() - self.viewport().height()) + documentsize = self.document().size().toSize() + self._fittedHeight = documentsize.height() + (self.height() - self.viewport().height()) self.setMaximumHeight(self._fittedHeight) - self.updateGeometry(); + self.updateGeometry() def keyPressEvent(self, event): """ @@ -55,26 +55,26 @@ class _ExpandableTextEdit(QTextEdit): return elif event.matches(QKeySequence.MoveToNextLine): text = self.toPlainText() - cursorPos = self.textCursor().position() - textBeforeEnd = text[cursorPos:] + cursor_pos = self.textCursor().position() + textBeforeEnd = text[cursor_pos:] # if len(textBeforeEnd.splitlines()) <= 1: if len(textBeforeEnd.split('\n')) <= 1: self.historyNext.emit() return elif event.matches(QKeySequence.MoveToPreviousLine): text = self.toPlainText() - cursorPos = self.textCursor().position() - textBeforeStart = text[:cursorPos] + cursor_pos = self.textCursor().position() + text_before_start = text[:cursor_pos] # lineCount = len(textBeforeStart.splitlines()) - lineCount = len(textBeforeStart.split('\n')) - if len(textBeforeStart) > 0 and \ - (textBeforeStart[-1] == '\n' or textBeforeStart[-1] == '\r'): - lineCount += 1 - if lineCount <= 1: + line_count = len(text_before_start.split('\n')) + if len(text_before_start) > 0 and \ + (text_before_start[-1] == '\n' or text_before_start[-1] == '\r'): + line_count += 1 + if line_count <= 1: self.historyPrev.emit() return elif event.matches(QKeySequence.MoveToNextPage) or \ - event.matches(QKeySequence.MoveToPreviousPage): + event.matches(QKeySequence.MoveToPreviousPage): return self._termWidget.browser().keyPressEvent(event) QTextEdit.keyPressEvent(self, event) @@ -94,8 +94,9 @@ class TermWidget(QWidget): self._browser = QTextEdit(self) self._browser.setStyleSheet("font: 9pt \"Courier\";") self._browser.setReadOnly(True) - self._browser.document().setDefaultStyleSheet(self._browser.document().defaultStyleSheet() + - "span {white-space:pre;}") + self._browser.document().setDefaultStyleSheet( + self._browser.document().defaultStyleSheet() + + "span {white-space:pre;}") self._edit = _ExpandableTextEdit(self, self) self._edit.historyNext.connect(self._on_history_next) @@ -120,30 +121,12 @@ class TermWidget(QWidget): assert style in ('in', 'out', 'err') text = cgi.escape(text) - text = text.replace('\n', '
') - if style != 'out': - def_bg = self._browser.palette().color(QPalette.Base) - h, s, v, a = def_bg.getHsvF() - - if style == 'in': - if v > 0.5: # white background - v = v - (v / 8) # make darker - else: - v = v + ((1 - v) / 4) # make ligher - else: # err - if v < 0.5: - v = v + ((1 - v) / 4) # make ligher - - if h == -1: # make red - h = 0 - s = .4 - else: - h = h + ((1 - h) * 0.5) # make more red - - bg = QColor.fromHsvF(h, s, v).name() - text = '%s' % (str(bg), text) + if style == 'in': + text = '%s' % text + elif style == 'err': + text = '%s' % text else: text = '%s' % text # without span
is ignored!!! From b1f2b680e30dff243f258d0891c8b3916d6871b5 Mon Sep 17 00:00:00 2001 From: Juan Pablo Caram Date: Fri, 25 Mar 2016 22:00:05 -0400 Subject: [PATCH 089/134] Fixes #198 --- termwidget.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/termwidget.py b/termwidget.py index 36487b6..b2e4fdb 100644 --- a/termwidget.py +++ b/termwidget.py @@ -79,6 +79,10 @@ class _ExpandableTextEdit(QTextEdit): QTextEdit.keyPressEvent(self, event) + def insertFromMimeData(self, mime_data): + # Paste only plain text. + self.insertPlainText(mime_data.text()) + class TermWidget(QWidget): """ From b333d136b5fd114b516a9a6f0e70babe9dd16196 Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Thu, 31 Mar 2016 17:16:14 +0200 Subject: [PATCH 090/134] merge changes from master cleanups and prepare for pull request --- FlatCAM.py | 2 +- FlatCAMShell.py | 2 +- FlatCAMWorker.py | 12 +------- termwidget.py | 80 ++++++++++++++++++++---------------------------- 4 files changed, 36 insertions(+), 60 deletions(-) diff --git a/FlatCAM.py b/FlatCAM.py index 92ed2e1..1cb60c9 100644 --- a/FlatCAM.py +++ b/FlatCAM.py @@ -12,7 +12,7 @@ def debug_trace(): debug_trace() -# all X11 calling should be thread safe otherwise we have strenght issues +# all X11 calling should be thread safe otherwise we have strange issues QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_X11InitThreads) app = QtGui.QApplication(sys.argv) diff --git a/FlatCAMShell.py b/FlatCAMShell.py index c85e86e..695d7a9 100644 --- a/FlatCAMShell.py +++ b/FlatCAMShell.py @@ -22,4 +22,4 @@ class FCShell(termwidget.TermWidget): return True def child_exec_command(self, text): - self._sysShell.exec_command(text) \ No newline at end of file + self._sysShell.exec_command(text) diff --git a/FlatCAMWorker.py b/FlatCAMWorker.py index a1f49a8..cbe9de9 100644 --- a/FlatCAMWorker.py +++ b/FlatCAMWorker.py @@ -1,5 +1,6 @@ from PyQt4 import QtCore + class Worker(QtCore.QObject): """ Implements a queue of tasks to be carried out in order @@ -46,17 +47,6 @@ class Worker(QtCore.QObject): self.allow_debug() - # 'worker_name' property of task allows to target - # specific worker. - #if 'worker_name' in task and task['worker_name'] == self.name: - # task['fcn'](*task['params']) - # return - - #if 'worker_name' not in task and self.name is None: - # task['fcn'](*task['params']) - # return - - if ('worker_name' in task and task['worker_name'] == self.name) or \ ('worker_name' not in task and self.name is None): diff --git a/termwidget.py b/termwidget.py index 94bbb80..538cc16 100644 --- a/termwidget.py +++ b/termwidget.py @@ -4,8 +4,7 @@ Shows intput and output text. Allows to enter commands. Supports history. """ import cgi -from PyQt4 import QtCore -from PyQt4.QtCore import pyqtSignal +from PyQt4.QtCore import pyqtSignal, Qt from PyQt4.QtGui import QColor, QKeySequence, QLineEdit, QPalette, \ QSizePolicy, QTextCursor, QTextEdit, \ QVBoxLayout, QWidget @@ -19,13 +18,13 @@ class _ExpandableTextEdit(QTextEdit): historyNext = pyqtSignal() historyPrev = pyqtSignal() - def __init__(self, termWidget, *args): + def __init__(self, termwidget, *args): QTextEdit.__init__(self, *args) self.setStyleSheet("font: 9pt \"Courier\";") self._fittedHeight = 1 self.textChanged.connect(self._fit_to_document) self._fit_to_document() - self._termWidget = termWidget + self._termWidget = termwidget def sizeHint(self): """ @@ -39,10 +38,10 @@ class _ExpandableTextEdit(QTextEdit): """ Update widget height to fit all text """ - documentSize = self.document().size().toSize() - self._fittedHeight = documentSize.height() + (self.height() - self.viewport().height()) + documentsize = self.document().size().toSize() + self._fittedHeight = documentsize.height() + (self.height() - self.viewport().height()) self.setMaximumHeight(self._fittedHeight) - self.updateGeometry(); + self.updateGeometry() def keyPressEvent(self, event): """ @@ -55,30 +54,33 @@ class _ExpandableTextEdit(QTextEdit): return elif event.matches(QKeySequence.MoveToNextLine): text = self.toPlainText() - cursorPos = self.textCursor().position() - textBeforeEnd = text[cursorPos:] + cursor_pos = self.textCursor().position() + textBeforeEnd = text[cursor_pos:] # if len(textBeforeEnd.splitlines()) <= 1: if len(textBeforeEnd.split('\n')) <= 1: self.historyNext.emit() return elif event.matches(QKeySequence.MoveToPreviousLine): text = self.toPlainText() - cursorPos = self.textCursor().position() - textBeforeStart = text[:cursorPos] + cursor_pos = self.textCursor().position() + text_before_start = text[:cursor_pos] # lineCount = len(textBeforeStart.splitlines()) - lineCount = len(textBeforeStart.split('\n')) - if len(textBeforeStart) > 0 and \ - (textBeforeStart[-1] == '\n' or textBeforeStart[-1] == '\r'): - lineCount += 1 - if lineCount <= 1: + line_count = len(text_before_start.split('\n')) + if len(text_before_start) > 0 and \ + (text_before_start[-1] == '\n' or text_before_start[-1] == '\r'): + line_count += 1 + if line_count <= 1: self.historyPrev.emit() return elif event.matches(QKeySequence.MoveToNextPage) or \ - event.matches(QKeySequence.MoveToPreviousPage): + event.matches(QKeySequence.MoveToPreviousPage): return self._termWidget.browser().keyPressEvent(event) QTextEdit.keyPressEvent(self, event) + def insertFromMimeData(self, mime_data): + # Paste only plain text. + self.insertPlainText(mime_data.text()) class TermWidget(QWidget): """ @@ -94,8 +96,9 @@ class TermWidget(QWidget): self._browser = QTextEdit(self) self._browser.setStyleSheet("font: 9pt \"Courier\";") self._browser.setReadOnly(True) - self._browser.document().setDefaultStyleSheet(self._browser.document().defaultStyleSheet() + - "span {white-space:pre;}") + self._browser.document().setDefaultStyleSheet( + self._browser.document().defaultStyleSheet() + + "span {white-space:pre;}") self._edit = _ExpandableTextEdit(self, self) self._edit.historyNext.connect(self._on_history_next) @@ -116,11 +119,13 @@ class TermWidget(QWidget): def open_proccessing(self, detail=None): """ Open processing and disable using shell commands again until all commands are finished - :return: + + :param detail: text detail about what is currently called from TCL to python + :return: None """ - self._edit.setTextColor(QtCore.Qt.white) - self._edit.setTextBackgroundColor(QtCore.Qt.darkGreen) + self._edit.setTextColor(Qt.white) + self._edit.setTextBackgroundColor(Qt.darkGreen) if detail is None: self._edit.setPlainText("...proccessing...") else: @@ -134,8 +139,8 @@ class TermWidget(QWidget): :return: """ - self._edit.setTextColor(QtCore.Qt.black) - self._edit.setTextBackgroundColor(QtCore.Qt.white) + self._edit.setTextColor(Qt.black) + self._edit.setTextBackgroundColor(Qt.white) self._edit.setPlainText('') self._edit.setDisabled(False) @@ -146,30 +151,12 @@ class TermWidget(QWidget): assert style in ('in', 'out', 'err') text = cgi.escape(text) - text = text.replace('\n', '
') - if style != 'out': - def_bg = self._browser.palette().color(QPalette.Base) - h, s, v, a = def_bg.getHsvF() - - if style == 'in': - if v > 0.5: # white background - v = v - (v / 8) # make darker - else: - v = v + ((1 - v) / 4) # make ligher - else: # err - if v < 0.5: - v = v + ((1 - v) / 4) # make ligher - - if h == -1: # make red - h = 0 - s = .4 - else: - h = h + ((1 - h) * 0.5) # make more red - - bg = QColor.fromHsvF(h, s, v).name() - text = '%s' % (str(bg), text) + if style == 'in': + text = '%s' % text + elif style == 'err': + text = '%s' % text else: text = '%s' % text # without span
is ignored!!! @@ -264,4 +251,3 @@ class TermWidget(QWidget): self._historyIndex -= 1 self._edit.setPlainText(self._history[self._historyIndex]) self._edit.moveCursor(QTextCursor.End) - From e941e55a4abda91fe7b3fe9c52115afbc4749ab6 Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Thu, 31 Mar 2016 17:29:11 +0200 Subject: [PATCH 091/134] show ui.shell_dock instead of shell during tests --- tests/test_tcl_shell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_tcl_shell.py b/tests/test_tcl_shell.py index 5813c23..d36f30e 100644 --- a/tests/test_tcl_shell.py +++ b/tests/test_tcl_shell.py @@ -33,7 +33,7 @@ class TclShellTest(unittest.TestCase): # Create App, keep app defaults (do not load # user-defined defaults). self.fc = App(user_defaults=False) - self.fc.shell.show() + self.fc.ui.shell_dock.show() @classmethod def tearDownClass(self): From a4845d150e5e031e373b8670bb6572c977f12b48 Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Sun, 3 Apr 2016 10:43:06 +0200 Subject: [PATCH 092/134] add important comment --- FlatCAMApp.py | 1 + 1 file changed, 1 insertion(+) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index d31a704..32b501e 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -685,6 +685,7 @@ class App(QtCore.QObject): def raise_tcl_unknown_error(self, unknownException): """ raise Exception if is different type than TclErrorException + this is here mainly to show unknown errors inside TCL shell console :param unknownException: :return: """ From b98954dccd383f178b1a6b58e4e491281e4045c1 Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Sun, 3 Apr 2016 14:20:50 +0200 Subject: [PATCH 093/134] fix error handling in signaled commands, error gets info about different scoup instead of true error more detaild error print including python trace when more complex unknown error reinplement drillcncjob fix camlib problem with all drills("all" was already there) but it crashes on tools without points, when no tools "all" is as default add timeout to all helps if command is signaled --- FlatCAMApp.py | 28 +++++++++- camlib.py | 36 +++++++------ tclCommands/TclCommand.py | 57 ++++++++++++++++---- tclCommands/TclCommandCncjob.py | 3 +- tclCommands/TclCommandDrillcncjob.py | 81 ++++++++++++++++++++++++++++ tclCommands/__init__.py | 4 +- 6 files changed, 177 insertions(+), 32 deletions(-) create mode 100644 tclCommands/TclCommandDrillcncjob.py diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 32b501e..020ec59 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -1,4 +1,4 @@ -import sys +import sys, traceback import urllib import getopt import random @@ -695,6 +695,30 @@ class App(QtCore.QObject): else: raise unknownException + def display_tcl_error(self, error, error_info=None): + """ + escape bracket [ with \ otherwise there is error + "ERROR: missing close-bracket" instead of real error + :param error: it may be text or exception + :return: None + """ + + if isinstance(error, Exception): + exc_type, exc_value, exc_traceback = error_info + trc=traceback.format_list(traceback.extract_tb(exc_traceback)) + trc_formated=[] + for a in reversed(trc): + trc_formated.append(a.replace(" ", " > ").replace("\n","")) + text="%s\nPython traceback: %s\n%s" % (exc_value, + exc_type, + "\n".join(trc_formated)) + else: + text=error + + text = text.replace('[', '\\[').replace('"','\\"') + + self.tcl.eval('return -code error "%s"' % text) + def raise_tcl_error(self, text): """ this method pass exception from python into TCL as error, so we get stacktrace and reason @@ -702,7 +726,7 @@ class App(QtCore.QObject): :return: raise exception """ - self.tcl.eval('return -code error "%s"' % text) + self.display_tcl_error(text) raise self.TclErrorException(text) def exec_command(self, text): diff --git a/camlib.py b/camlib.py index 2a717ea..0d1bd38 100644 --- a/camlib.py +++ b/camlib.py @@ -2818,24 +2818,26 @@ class CNCjob(Geometry): for tool in tools: - # Tool change sequence (optional) - if toolchange: - gcode += "G00 Z%.4f\n" % toolchangez - gcode += "T%d\n" % int(tool) # Indicate tool slot (for automatic tool changer) - gcode += "M5\n" # Spindle Stop - gcode += "M6\n" # Tool change - gcode += "(MSG, Change to tool dia=%.4f)\n" % exobj.tools[tool]["C"] - gcode += "M0\n" # Temporary machine stop - if self.spindlespeed is not None: - gcode += "M03 S%d\n" % int(self.spindlespeed) # Spindle start with configured speed - else: - gcode += "M03\n" # Spindle start + # only if tool have some points, otherwise thre may be error and this part is useless + if "tool" in points: + # Tool change sequence (optional) + if toolchange: + gcode += "G00 Z%.4f\n" % toolchangez + gcode += "T%d\n" % int(tool) # Indicate tool slot (for automatic tool changer) + gcode += "M5\n" # Spindle Stop + gcode += "M6\n" # Tool change + gcode += "(MSG, Change to tool dia=%.4f)\n" % exobj.tools[tool]["C"] + gcode += "M0\n" # Temporary machine stop + if self.spindlespeed is not None: + gcode += "M03 S%d\n" % int(self.spindlespeed) # Spindle start with configured speed + else: + gcode += "M03\n" # Spindle start - # Drillling! - for point in points[tool]: - x, y = point.coords.xy - gcode += t % (x[0], y[0]) - gcode += down + up + # Drillling! + for point in points[tool]: + x, y = point.coords.xy + gcode += t % (x[0], y[0]) + gcode += down + up gcode += t % (0, 0) gcode += "M05\n" # Spindle stop diff --git a/tclCommands/TclCommand.py b/tclCommands/TclCommand.py index f713b31..57e4a9d 100644 --- a/tclCommands/TclCommand.py +++ b/tclCommands/TclCommand.py @@ -125,6 +125,10 @@ class TclCommand(object): for key, value in self.help['args'].items(): help_string.append(get_decorated_argument(key, value)) + # timeout is unique for signaled commands (this is not best oop practice, but much easier for now) + if isinstance(self, TclCommandSignaled): + help_string.append("\t[-timeout : Max wait for job timeout before error.]") + for example in self.help['examples']: help_string.append(get_decorated_example(example)) @@ -192,10 +196,13 @@ class TclCommand(object): # check options for key in options: - if key not in self.option_types and key is not 'timeout': + if key not in self.option_types and key != 'timeout': self.raise_tcl_error('Unknown parameter: %s' % key) try: - named_args[key] = self.option_types[key](options[key]) + if key != 'timeout': + named_args[key] = self.option_types[key](options[key]) + else: + named_args[key] = int(options[key]) except Exception, e: self.raise_tcl_error("Cannot cast argument '-%s' to type '%s' with exception '%s'." % (key, self.option_types[key], str(e))) @@ -207,6 +214,31 @@ class TclCommand(object): return named_args, unnamed_args + + def raise_tcl_unknown_error(self, unknownException): + """ + raise Exception if is different type than TclErrorException + this is here mainly to show unknown errors inside TCL shell console + :param unknownException: + :return: + """ + + #if not isinstance(unknownException, self.TclErrorException): + # self.raise_tcl_error("Unknown error: %s" % str(unknownException)) + #else: + raise unknownException + + def raise_tcl_error(self, text): + """ + this method pass exception from python into TCL as error, so we get stacktrace and reason + :param text: text of error + :return: raise exception + """ + + # becouse of signaling we cannot call error to TCL from here but when task is finished + # also nonsiglaned arwe handled here to better exception handling and diplay after command is finished + raise self.app.TclErrorException(text) + def execute_wrapper(self, *args): """ Command which is called by tcl console when current commands aliases are hit. @@ -225,8 +257,10 @@ class TclCommand(object): args, unnamed_args = self.check_args(args) return self.execute(args, unnamed_args) except Exception as unknown: + error_info=sys.exc_info() self.log.error("TCL command '%s' failed." % str(self)) - self.app.raise_tcl_unknown_error(unknown) + self.app.display_tcl_error(unknown, error_info) + self.raise_tcl_unknown_error(unknown) @abc.abstractmethod def execute(self, args, unnamed_args): @@ -242,7 +276,6 @@ class TclCommand(object): raise NotImplementedError("Please Implement this method") - class TclCommandSignaled(TclCommand): """ !!! I left it here only for demonstration !!! @@ -266,7 +299,13 @@ class TclCommandSignaled(TclCommand): def execute_call(self, args, unnamed_args): try: + self.output = None + self.error=None + self.error_info=None self.output = self.execute(args, unnamed_args) + except Exception as unknown: + self.error_info = sys.exc_info() + self.error=unknown finally: self.app.shell_command_finished.emit(self) @@ -336,12 +375,12 @@ class TclCommandSignaled(TclCommand): # set detail for processing, it will be there until next open or close self.app.shell.open_proccessing(self.get_current_command()) - self.output = None - def handle_finished(obj): self.app.shell_command_finished.disconnect(handle_finished) - # TODO: handle output - pass + if self.error is not None: + self.log.error("TCL command '%s' failed." % str(self)) + self.app.display_tcl_error(self.error, self.error_info) + self.raise_tcl_unknown_error(self.error) self.app.shell_command_finished.connect(handle_finished) @@ -355,4 +394,4 @@ class TclCommandSignaled(TclCommand): except Exception as unknown: self.log.error("TCL command '%s' failed." % str(self)) - self.app.raise_tcl_unknown_error(unknown) \ No newline at end of file + self.raise_tcl_unknown_error(unknown) \ No newline at end of file diff --git a/tclCommands/TclCommandCncjob.py b/tclCommands/TclCommandCncjob.py index e088d0e..e6d84de 100644 --- a/tclCommands/TclCommandCncjob.py +++ b/tclCommands/TclCommandCncjob.py @@ -49,8 +49,7 @@ class TclCommandCncjob(TclCommand.TclCommandSignaled): ('spindlespeed', 'Speed of the spindle in rpm (example: 4000).'), ('multidepth', 'Use or not multidepth cnccut.'), ('depthperpass', 'Height of one layer for multidepth.'), - ('outname', 'Name of the resulting Geometry object.'), - ('timeout', 'Max wait for job timeout before error.') + ('outname', 'Name of the resulting Geometry object.') ]), 'examples': [] } diff --git a/tclCommands/TclCommandDrillcncjob.py b/tclCommands/TclCommandDrillcncjob.py new file mode 100644 index 0000000..1744196 --- /dev/null +++ b/tclCommands/TclCommandDrillcncjob.py @@ -0,0 +1,81 @@ +from ObjectCollection import * +import TclCommand + + +class TclCommandDrillcncjob(TclCommand.TclCommandSignaled): + """ + Tcl shell command to Generates a Drill CNC Job from a Excellon Object. + """ + + # array of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon) + aliases = ['drillcncjob'] + + # dictionary of types from Tcl command, needs to be ordered + arg_names = collections.OrderedDict([ + ('name', str) + ]) + + # dictionary of types from Tcl command, needs to be ordered , this is for options like -optionname value + option_types = collections.OrderedDict([ + ('tools',str), + ('drillz',float), + ('travelz',float), + ('feedrate',float), + ('spindlespeed',int), + ('toolchange',bool), + ('outname',str) + ]) + + # array of mandatory options for current Tcl command: required = {'name','outname'} + required = ['name'] + + # structured help for current command, args needs to be ordered + help = { + 'main': "Generates a Drill CNC Job from a Excellon Object.", + 'args': collections.OrderedDict([ + ('name', 'Name of the source object.'), + ('tools', 'Comma separated indexes of tools (example: 1,3 or 2) or select all if not specified.'), + ('drillz', 'Drill depth into material (example: -2.0).'), + ('travelz', 'Travel distance above material (example: 2.0).'), + ('feedrate', 'Drilling feed rate.'), + ('spindlespeed', 'Speed of the spindle in rpm (example: 4000).'), + ('toolchange', 'Enable tool changes (example: 1).'), + ('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 + """ + + name = args['name'] + + if 'outname' not in args: + args['outname'] = name + "_cnc" + + obj = self.app.collection.get_by_name(name) + if obj is None: + self.raise_tcl_error("Object not found: %s" % name) + + if not isinstance(obj, FlatCAMExcellon): + self.raise_tcl_error('Expected FlatCAMExcellon, got %s %s.' % (name, type(obj))) + + def job_init(job_obj, app): + job_obj.z_cut = args["drillz"] + job_obj.z_move = args["travelz"] + job_obj.feedrate = args["feedrate"] + job_obj.spindlespeed = args["spindlespeed"] if "spindlespeed" in args else None + toolchange = True if "toolchange" in args and args["toolchange"] == 1 else False + tools = args["tools"] if "tools" in args else 'all' + job_obj.generate_from_excellon_by_tool(obj, tools, toolchange) + job_obj.gcode_parse() + job_obj.create_geometry() + + self.app.new_object("cncjob", name, job_init) diff --git a/tclCommands/__init__.py b/tclCommands/__init__.py index af67a9c..2f73301 100644 --- a/tclCommands/__init__.py +++ b/tclCommands/__init__.py @@ -1,10 +1,11 @@ import pkgutil import sys -# allowed command modules +# allowed command modules (please append them alphabetically ordered) import tclCommands.TclCommandAddPolygon import tclCommands.TclCommandAddPolyline import tclCommands.TclCommandCncjob +import tclCommands.TclCommandDrillcncjob import tclCommands.TclCommandExportGcode import tclCommands.TclCommandExteriors import tclCommands.TclCommandInteriors @@ -19,7 +20,6 @@ for loader, name, is_pkg in pkgutil.walk_packages(__path__): module = loader.find_module(name).load_module(name) __all__.append(name) - def register_all_commands(app, commands): """ Static method which register all known commands. From c2cdaaf45234029ae43cc4a1b438258fad6decac Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Sun, 3 Apr 2016 14:37:40 +0200 Subject: [PATCH 094/134] fix display also for nonsignaled exceptions in execute_wrapper --- tclCommands/TclCommand.py | 2 ++ tclCommands/TclCommandDrillcncjob.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/tclCommands/TclCommand.py b/tclCommands/TclCommand.py index 57e4a9d..24f8295 100644 --- a/tclCommands/TclCommand.py +++ b/tclCommands/TclCommand.py @@ -393,5 +393,7 @@ class TclCommandSignaled(TclCommand): return self.output except Exception as unknown: + 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/TclCommandDrillcncjob.py b/tclCommands/TclCommandDrillcncjob.py index 1744196..f65931f 100644 --- a/tclCommands/TclCommandDrillcncjob.py +++ b/tclCommands/TclCommandDrillcncjob.py @@ -39,7 +39,7 @@ class TclCommandDrillcncjob(TclCommand.TclCommandSignaled): ('travelz', 'Travel distance above material (example: 2.0).'), ('feedrate', 'Drilling feed rate.'), ('spindlespeed', 'Speed of the spindle in rpm (example: 4000).'), - ('toolchange', 'Enable tool changes (example: 1).'), + ('toolchange', 'Enable tool changes (example: True).'), ('outname', 'Name of the resulting Geometry object.') ]), 'examples': [] From 5bd6432ead16b96dfe40a15134765d37b5364d45 Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Wed, 6 Apr 2016 11:20:53 +0200 Subject: [PATCH 095/134] solve message in special tcl keywords used in wrong context as "unknown" --- FlatCAMApp.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 020ec59..b2ee974 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -762,6 +762,11 @@ class App(QtCore.QObject): except Tkinter.TclError, e: #this will display more precise answer if something in TCL shell fail result = self.tcl.eval("set errorInfo") + + # solve message in special tcl keywords used in wrong context as "unknown" + if e.message == 'invalid command name ""': + result=result.replace('""','"%s"' % text.replace("\n","")) + self.log.error("Exec command Exception: %s" % (result + '\n')) self.shell.append_error('ERROR: ' + result + '\n') #show error in console and just return or in test raise exception From 4c20040fbe792cf247f2412e841d5e3aa1ecd99c Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Sat, 9 Apr 2016 12:48:32 +0200 Subject: [PATCH 096/134] fix errors in tool selection --- camlib.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/camlib.py b/camlib.py index 0d1bd38..7c0cd11 100644 --- a/camlib.py +++ b/camlib.py @@ -2777,7 +2777,7 @@ class CNCjob(Geometry): # so we actually are sorting the tools by diameter sorted_tools = sorted(exobj.tools.items(), key = lambda x: x[1]) if tools == "all": - tools = str([i[0] for i in sorted_tools]) # we get a string of ordered tools + tools = [i[0] for i in sorted_tools] # we get a array of ordered tools log.debug("Tools 'all' and sorted are: %s" % str(tools)) else: selected_tools = [x.strip() for x in tools.split(",")] # we strip spaces and also separate the tools by ',' @@ -2819,7 +2819,7 @@ class CNCjob(Geometry): for tool in tools: # only if tool have some points, otherwise thre may be error and this part is useless - if "tool" in points: + if tool in points: # Tool change sequence (optional) if toolchange: gcode += "G00 Z%.4f\n" % toolchangez From fae9875dd896762c156dc18b1e38985f136ec639 Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Sun, 10 Apr 2016 11:09:26 +0200 Subject: [PATCH 097/134] remove unknown workaround --- FlatCAMApp.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index b2ee974..020ec59 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -762,11 +762,6 @@ class App(QtCore.QObject): except Tkinter.TclError, e: #this will display more precise answer if something in TCL shell fail result = self.tcl.eval("set errorInfo") - - # solve message in special tcl keywords used in wrong context as "unknown" - if e.message == 'invalid command name ""': - result=result.replace('""','"%s"' % text.replace("\n","")) - self.log.error("Exec command Exception: %s" % (result + '\n')) self.shell.append_error('ERROR: ' + result + '\n') #show error in console and just return or in test raise exception From 26a8b7347b2474ae5bb287091ddff2d2aa8db873 Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Sun, 10 Apr 2016 11:10:25 +0200 Subject: [PATCH 098/134] change default timeout fix outname bug in drillcncjob --- tclCommands/TclCommand.py | 4 ++-- tclCommands/TclCommandDrillcncjob.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tclCommands/TclCommand.py b/tclCommands/TclCommand.py index 24f8295..bc7cd2c 100644 --- a/tclCommands/TclCommand.py +++ b/tclCommands/TclCommand.py @@ -291,8 +291,8 @@ class TclCommandSignaled(TclCommand): it handles all neccessary stuff about blocking and passing exeptions """ - # default timeout for operation is 10 sec, but it can be much more - default_timeout = 10000 + # default timeout for operation is 300000 sec, but it can be much more + default_timeout = 300000 output = None diff --git a/tclCommands/TclCommandDrillcncjob.py b/tclCommands/TclCommandDrillcncjob.py index f65931f..783b659 100644 --- a/tclCommands/TclCommandDrillcncjob.py +++ b/tclCommands/TclCommandDrillcncjob.py @@ -78,4 +78,4 @@ class TclCommandDrillcncjob(TclCommand.TclCommandSignaled): job_obj.gcode_parse() job_obj.create_geometry() - self.app.new_object("cncjob", name, job_init) + self.app.new_object("cncjob", args['outname'], job_init) From e236a60be90e9ca9bdc4be90ca6c4731dcdf8265 Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Sun, 10 Apr 2016 15:14:18 +0200 Subject: [PATCH 099/134] implement system values background_timeout and verbose_error_level implement correct error level handling based on verbose_error_level , fix double print of tcl error and do not wrap unknown exceptions into TCL known --- FlatCAMApp.py | 27 +++++++++++++++++++-------- tclCommands/TclCommand.py | 13 ++++--------- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 020ec59..872a6e4 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -286,6 +286,8 @@ class App(QtCore.QObject): "cncjob_tooldia": 0.016, "cncjob_prepend": "", "cncjob_append": "", + "background_timeout": 300000, #default value is 5 minutes + "verbose_error_level": 0, # shell verbosity 0 = default(python trace only for unknown errors), 1 = show trace(show trace allways), 2 = (For the future). # Persistence "last_folder": None, @@ -679,7 +681,6 @@ class App(QtCore.QObject): """ this exception is deffined here, to be able catch it if we sucessfully handle all errors from shell command """ - pass def raise_tcl_unknown_error(self, unknownException): @@ -704,14 +705,24 @@ class App(QtCore.QObject): """ if isinstance(error, Exception): + exc_type, exc_value, exc_traceback = error_info - trc=traceback.format_list(traceback.extract_tb(exc_traceback)) - trc_formated=[] - for a in reversed(trc): - trc_formated.append(a.replace(" ", " > ").replace("\n","")) - text="%s\nPython traceback: %s\n%s" % (exc_value, - exc_type, - "\n".join(trc_formated)) + if not isinstance(error, self.TclErrorException): + show_trace = 1 + else: + show_trace = int(self.defaults['verbose_error_level']) + + if show_trace > 0: + trc=traceback.format_list(traceback.extract_tb(exc_traceback)) + trc_formated=[] + for a in reversed(trc): + trc_formated.append(a.replace(" ", " > ").replace("\n","")) + text="%s\nPython traceback: %s\n%s" % (exc_value, + exc_type, + "\n".join(trc_formated)) + + else: + text="%s" % error else: text=error diff --git a/tclCommands/TclCommand.py b/tclCommands/TclCommand.py index bc7cd2c..b93ec75 100644 --- a/tclCommands/TclCommand.py +++ b/tclCommands/TclCommand.py @@ -291,9 +291,6 @@ class TclCommandSignaled(TclCommand): it handles all neccessary stuff about blocking and passing exeptions """ - # default timeout for operation is 300000 sec, but it can be much more - default_timeout = 300000 - output = None def execute_call(self, args, unnamed_args): @@ -320,7 +317,7 @@ class TclCommandSignaled(TclCommand): """ @contextmanager - def wait_signal(signal, timeout=10000): + def wait_signal(signal, timeout=300000): """Block loop until signal emitted, or timeout (ms) elapses.""" loop = QtCore.QEventLoop() @@ -357,10 +354,10 @@ class TclCommandSignaled(TclCommand): # Restore exception management sys.excepthook = oeh if ex: - self.raise_tcl_error(str(ex[0])) + raise ex[0] if status['timed_out']: - self.app.raise_tcl_unknown_error('Operation timed out!') + self.app.raise_tcl_unknown_error("Operation timed outed! Consider increasing option '-timeout ' for command or 'set_sys background_timeout '.") try: self.log.debug("TCL command '%s' executed." % str(self.__class__)) @@ -370,7 +367,7 @@ class TclCommandSignaled(TclCommand): passed_timeout=args['timeout'] del args['timeout'] else: - passed_timeout=self.default_timeout + passed_timeout= self.app.defaults['background_timeout'] # set detail for processing, it will be there until next open or close self.app.shell.open_proccessing(self.get_current_command()) @@ -378,8 +375,6 @@ class TclCommandSignaled(TclCommand): def handle_finished(obj): self.app.shell_command_finished.disconnect(handle_finished) if self.error is not None: - self.log.error("TCL command '%s' failed." % str(self)) - self.app.display_tcl_error(self.error, self.error_info) self.raise_tcl_unknown_error(self.error) self.app.shell_command_finished.connect(handle_finished) From a0bd34de451826ba0648a35eeb505fa55bf9c50c Mon Sep 17 00:00:00 2001 From: Juan Pablo Caram Date: Sun, 10 Apr 2016 15:44:17 -0400 Subject: [PATCH 100/134] Fixes #198 --- descartes/patch.py | 66 ---------------------------------------------- 1 file changed, 66 deletions(-) delete mode 100644 descartes/patch.py diff --git a/descartes/patch.py b/descartes/patch.py deleted file mode 100644 index 34686f7..0000000 --- a/descartes/patch.py +++ /dev/null @@ -1,66 +0,0 @@ -"""Paths and patches""" - -from matplotlib.patches import PathPatch -from matplotlib.path import Path -from numpy import asarray, concatenate, ones - - -class Polygon(object): - # Adapt Shapely or GeoJSON/geo_interface polygons to a common interface - def __init__(self, context): - if hasattr(context, 'interiors'): - self.context = context - else: - self.context = getattr(context, '__geo_interface__', context) - @property - def geom_type(self): - return (getattr(self.context, 'geom_type', None) - or self.context['type']) - @property - def exterior(self): - return (getattr(self.context, 'exterior', None) - or self.context['coordinates'][0]) - @property - def interiors(self): - value = getattr(self.context, 'interiors', None) - if value is None: - value = self.context['coordinates'][1:] - return value - - -def PolygonPath(polygon): - """Constructs a compound matplotlib path from a Shapely or GeoJSON-like - geometric object""" - this = Polygon(polygon) - assert this.geom_type == 'Polygon' - def coding(ob): - # The codes will be all "LINETO" commands, except for "MOVETO"s at the - # beginning of each subpath - n = len(getattr(ob, 'coords', None) or ob) - vals = ones(n, dtype=Path.code_type) * Path.LINETO - vals[0] = Path.MOVETO - return vals - vertices = concatenate( - [asarray(this.exterior)] - + [asarray(r) for r in this.interiors]) - codes = concatenate( - [coding(this.exterior)] - + [coding(r) for r in this.interiors]) - return Path(vertices, codes) - - -def PolygonPatch(polygon, **kwargs): - """Constructs a matplotlib patch from a geometric object - - The `polygon` may be a Shapely or GeoJSON-like object with or without holes. - The `kwargs` are those supported by the matplotlib.patches.Polygon class - constructor. Returns an instance of matplotlib.patches.PathPatch. - - Example (using Shapely Point and a matplotlib axes): - - >>> b = Point(0, 0).buffer(1.0) - >>> patch = PolygonPatch(b, fc='blue', ec='blue', alpha=0.5) - >>> axis.add_patch(patch) - - """ - return PathPatch(PolygonPath(polygon), **kwargs) From bac9f29d0889a0154f0cc08093e96687eec7f34f Mon Sep 17 00:00:00 2001 From: Juan Pablo Caram Date: Sun, 10 Apr 2016 15:48:25 -0400 Subject: [PATCH 101/134] Recovered patch.py --- descartes/patch.py | 66 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 descartes/patch.py diff --git a/descartes/patch.py b/descartes/patch.py new file mode 100644 index 0000000..34686f7 --- /dev/null +++ b/descartes/patch.py @@ -0,0 +1,66 @@ +"""Paths and patches""" + +from matplotlib.patches import PathPatch +from matplotlib.path import Path +from numpy import asarray, concatenate, ones + + +class Polygon(object): + # Adapt Shapely or GeoJSON/geo_interface polygons to a common interface + def __init__(self, context): + if hasattr(context, 'interiors'): + self.context = context + else: + self.context = getattr(context, '__geo_interface__', context) + @property + def geom_type(self): + return (getattr(self.context, 'geom_type', None) + or self.context['type']) + @property + def exterior(self): + return (getattr(self.context, 'exterior', None) + or self.context['coordinates'][0]) + @property + def interiors(self): + value = getattr(self.context, 'interiors', None) + if value is None: + value = self.context['coordinates'][1:] + return value + + +def PolygonPath(polygon): + """Constructs a compound matplotlib path from a Shapely or GeoJSON-like + geometric object""" + this = Polygon(polygon) + assert this.geom_type == 'Polygon' + def coding(ob): + # The codes will be all "LINETO" commands, except for "MOVETO"s at the + # beginning of each subpath + n = len(getattr(ob, 'coords', None) or ob) + vals = ones(n, dtype=Path.code_type) * Path.LINETO + vals[0] = Path.MOVETO + return vals + vertices = concatenate( + [asarray(this.exterior)] + + [asarray(r) for r in this.interiors]) + codes = concatenate( + [coding(this.exterior)] + + [coding(r) for r in this.interiors]) + return Path(vertices, codes) + + +def PolygonPatch(polygon, **kwargs): + """Constructs a matplotlib patch from a geometric object + + The `polygon` may be a Shapely or GeoJSON-like object with or without holes. + The `kwargs` are those supported by the matplotlib.patches.Polygon class + constructor. Returns an instance of matplotlib.patches.PathPatch. + + Example (using Shapely Point and a matplotlib axes): + + >>> b = Point(0, 0).buffer(1.0) + >>> patch = PolygonPatch(b, fc='blue', ec='blue', alpha=0.5) + >>> axis.add_patch(patch) + + """ + return PathPatch(PolygonPath(polygon), **kwargs) From 7112ac5caf115abc4961a2eeafb129cfb5d7280b Mon Sep 17 00:00:00 2001 From: Juan Pablo Caram Date: Sun, 10 Apr 2016 16:02:38 -0400 Subject: [PATCH 102/134] Recovered patch.py --- FlatCAM.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/FlatCAM.py b/FlatCAM.py index 1cb60c9..46cfc30 100644 --- a/FlatCAM.py +++ b/FlatCAM.py @@ -1,10 +1,13 @@ import sys from PyQt4 import QtGui -from PyQt4 import QtCore from FlatCAMApp import App + def debug_trace(): - '''Set a tracepoint in the Python debugger that works with Qt''' + """ + Set a tracepoint in the Python debugger that works with Qt + :return: None + """ from PyQt4.QtCore import pyqtRemoveInputHook #from pdb import set_trace pyqtRemoveInputHook() @@ -12,9 +15,10 @@ def debug_trace(): debug_trace() -# all X11 calling should be thread safe otherwise we have strange issues -QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_X11InitThreads) +# All X11 calling should be thread safe otherwise we have strange issues +# QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_X11InitThreads) +# NOTE: Never talk to the GUI from threads! This is why I commented the above. app = QtGui.QApplication(sys.argv) fc = App() -sys.exit(app.exec_()) \ No newline at end of file +sys.exit(app.exec_()) From 3717169105a89eb9bd481367cc942d09e553efcb Mon Sep 17 00:00:00 2001 From: Juan Pablo Caram Date: Sun, 10 Apr 2016 16:23:04 -0400 Subject: [PATCH 103/134] Default excellon milling tool dia. Fixes #160. --- FlatCAMApp.py | 2 ++ FlatCAMGUI.py | 17 +++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 872a6e4..6c0701b 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -231,6 +231,7 @@ class App(QtCore.QObject): "excellon_feedrate": self.defaults_form.excellon_group.feedrate_entry, "excellon_spindlespeed": self.defaults_form.excellon_group.spindlespeed_entry, "excellon_toolchangez": self.defaults_form.excellon_group.toolchangez_entry, + "excellon_tooldia": self.defaults_form.excellon_group.tooldia_entry, "geometry_plot": self.defaults_form.geometry_group.plot_cb, "geometry_cutz": self.defaults_form.geometry_group.cutz_entry, "geometry_travelz": self.defaults_form.geometry_group.travelz_entry, @@ -360,6 +361,7 @@ class App(QtCore.QObject): "excellon_feedrate": self.options_form.excellon_group.feedrate_entry, "excellon_spindlespeed": self.options_form.excellon_group.spindlespeed_entry, "excellon_toolchangez": self.options_form.excellon_group.toolchangez_entry, + "excellon_tooldia": self.options_form.excellon_group.tooldia_entry, "geometry_plot": self.options_form.geometry_group.plot_cb, "geometry_cutz": self.options_form.geometry_group.cutz_entry, "geometry_travelz": self.options_form.geometry_group.travelz_entry, diff --git a/FlatCAMGUI.py b/FlatCAMGUI.py index 3c01d12..2ec95ac 100644 --- a/FlatCAMGUI.py +++ b/FlatCAMGUI.py @@ -607,6 +607,23 @@ class ExcellonOptionsGroupUI(OptionsGroupUI): self.spindlespeed_entry = IntEntry(allow_empty=True) grid1.addWidget(self.spindlespeed_entry, 4, 1) + #### Milling Holes #### + self.mill_hole_label = QtGui.QLabel('Mill Holes') + self.mill_hole_label.setToolTip( + "Create Geometry for milling holes." + ) + self.layout.addWidget(self.mill_hole_label) + + grid1 = QtGui.QGridLayout() + self.layout.addLayout(grid1) + tdlabel = QtGui.QLabel('Tool dia:') + tdlabel.setToolTip( + "Diameter of the cutting tool." + ) + grid1.addWidget(tdlabel, 0, 0) + self.tooldia_entry = LengthEntry() + grid1.addWidget(self.tooldia_entry, 0, 1) + class GeometryOptionsGroupUI(OptionsGroupUI): def __init__(self, parent=None): From d28858ff38f419a6c9a173a8a9b398928beb4e58 Mon Sep 17 00:00:00 2001 From: Juan Pablo Caram Date: Sun, 10 Apr 2016 16:43:03 -0400 Subject: [PATCH 104/134] Fast vertical movement above board. Fixes #141. --- camlib.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/camlib.py b/camlib.py index 7c0cd11..d66a71f 100644 --- a/camlib.py +++ b/camlib.py @@ -2782,7 +2782,9 @@ class CNCjob(Geometry): else: selected_tools = [x.strip() for x in tools.split(",")] # we strip spaces and also separate the tools by ',' selected_tools = filter(lambda i: i in selected_tools, selected_tools) - tools = [i for i,j in sorted_tools for k in selected_tools if i == k] # create a sorted list of selected tools from the sorted_tools list + + # Create a sorted list of selected tools from the sorted_tools list + tools = [i for i, j in sorted_tools for k in selected_tools if i == k] log.debug("Tools selected and sorted are: %s" % str(tools)) # Points (Group by tool) @@ -2800,7 +2802,8 @@ class CNCjob(Geometry): # Basic G-Code macros t = "G00 " + CNCjob.defaults["coordinate_format"] + "\n" down = "G01 Z%.4f\n" % self.z_cut - up = "G01 Z%.4f\n" % self.z_move + up = "G00 Z%.4f\n" % self.z_move + up_to_zero = "G01 Z0\n" # Initialization gcode = self.unitcode[self.units.upper()] + "\n" @@ -2810,7 +2813,8 @@ class CNCjob(Geometry): gcode += "G00 Z%.4f\n" % self.z_move # Move to travel height if self.spindlespeed is not None: - gcode += "M03 S%d\n" % int(self.spindlespeed) # Spindle start with configured speed + # Spindle start with configured speed + gcode += "M03 S%d\n" % int(self.spindlespeed) else: gcode += "M03\n" # Spindle start @@ -2818,7 +2822,7 @@ class CNCjob(Geometry): for tool in tools: - # only if tool have some points, otherwise thre may be error and this part is useless + # Only if tool has points. if tool in points: # Tool change sequence (optional) if toolchange: @@ -2829,7 +2833,8 @@ class CNCjob(Geometry): gcode += "(MSG, Change to tool dia=%.4f)\n" % exobj.tools[tool]["C"] gcode += "M0\n" # Temporary machine stop if self.spindlespeed is not None: - gcode += "M03 S%d\n" % int(self.spindlespeed) # Spindle start with configured speed + # Spindle start with configured speed + gcode += "M03 S%d\n" % int(self.spindlespeed) else: gcode += "M03\n" # Spindle start @@ -2837,7 +2842,7 @@ class CNCjob(Geometry): for point in points[tool]: x, y = point.coords.xy gcode += t % (x[0], y[0]) - gcode += down + up + gcode += down + up_to_zero + up gcode += t % (0, 0) gcode += "M05\n" # Spindle stop From 8a67a3cce192eae8609fc587de2b9b3c56370930 Mon Sep 17 00:00:00 2001 From: sopak Date: Mon, 11 Apr 2016 13:14:45 +0200 Subject: [PATCH 105/134] reimplement command import_svg --- tclCommands/TclCommandImportSvg.py | 71 ++++++++++++++++++++++++++++++ tclCommands/__init__.py | 1 + 2 files changed, 72 insertions(+) create mode 100644 tclCommands/TclCommandImportSvg.py diff --git a/tclCommands/TclCommandImportSvg.py b/tclCommands/TclCommandImportSvg.py new file mode 100644 index 0000000..b767e0b --- /dev/null +++ b/tclCommands/TclCommandImportSvg.py @@ -0,0 +1,71 @@ +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([ + ('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.'), + ('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, 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] + + with self.app.proc_container.new("Opening Gerber"): + + # Object creation + self.app.new_object("gerber", outname, obj_init) + + # Register recent file + self.file_opened.emit("svg", filename) + + # GUI feedback + self.inform.emit("Opened: " + filename) + diff --git a/tclCommands/__init__.py b/tclCommands/__init__.py index 2f73301..0885d14 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 From db1504470640f87afb82908f301ad0fc7d8fff6b Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Tue, 12 Apr 2016 13:09:41 +0200 Subject: [PATCH 106/134] fix exception thrown when new project is issued from shell(probbly from guy too) because of excellon_tooldia introduced, but no default values and no in array dimensions on change units defaults added for excellon_tooldia background_timeout and verbose_error_level in self.options.update({}) --- FlatCAMApp.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 6c0701b..f9ca0a4 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -274,6 +274,7 @@ class App(QtCore.QObject): "excellon_feedrate": 3.0, "excellon_spindlespeed": None, "excellon_toolchangez": 1.0, + "excellon_tooldia": 0.016, "geometry_plot": True, "geometry_cutz": -0.002, "geometry_travelz": 0.1, @@ -403,6 +404,7 @@ class App(QtCore.QObject): "excellon_feedrate": 3.0, "excellon_spindlespeed": None, "excellon_toolchangez": 1.0, + "excellon_tooldia": 0.016, "geometry_plot": True, "geometry_cutz": -0.002, "geometry_travelz": 0.1, @@ -415,7 +417,9 @@ class App(QtCore.QObject): "cncjob_plot": True, "cncjob_tooldia": 0.016, "cncjob_prepend": "", - "cncjob_append": "" + "cncjob_append": "", + "background_timeout": 300000, #default value is 5 minutes + "verbose_error_level": 0, # shell verbosity 0 = default(python trace only for unknown errors), 1 = show trace(show trace allways), 2 = (For the future). }) self.options.update(self.defaults) # Copy app defaults to project options #self.options_write_form() @@ -1285,7 +1289,7 @@ class App(QtCore.QObject): # Options to scale dimensions = ['gerber_isotooldia', 'gerber_cutoutmargin', 'gerber_cutoutgapsize', 'gerber_noncoppermargin', 'gerber_bboxmargin', 'excellon_drillz', - 'excellon_travelz', 'excellon_feedrate', 'excellon_toolchangez', 'cncjob_tooldia', + 'excellon_travelz', 'excellon_feedrate', 'excellon_toolchangez', 'excellon_tooldia', 'cncjob_tooldia', 'geometry_cutz', 'geometry_travelz', 'geometry_feedrate', 'geometry_cnctooldia', 'geometry_painttooldia', 'geometry_paintoverlap', 'geometry_paintmargin'] From 96419921e5c21ba57a4a7e325a527596484d675d Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Tue, 12 Apr 2016 19:44:56 +0200 Subject: [PATCH 107/134] small fix if error happens inside thread execution, then pass correct error_info to display command imort_svg was using self instead self.app wrong object Fix in svgparse for rotate regexp and division by zero problem. Linestring need at least 2 points within very small arcs. In svg rect x and y are optional , they are 0 by default. Ignore transformation for unknown kind. Strip spaces for ptliststr In parse_svg_point_list to avoid parsing errors. --- svgparse.py | 71 ++++++++++++++++++------------ tclCommands/TclCommand.py | 6 ++- tclCommands/TclCommandImportSvg.py | 8 ++-- 3 files changed, 52 insertions(+), 33 deletions(-) diff --git a/svgparse.py b/svgparse.py index f2ecb52..24f0462 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,10 +87,15 @@ 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) - frac = 1.0 / steps + + # solve error when step is below 1, + # it may cause other problems, but LineString needs at least two points + if steps == 0: + steps = 1 # print length, steps, frac for i in range(steps): + frac = 1.0 / steps point = component.point(i * frac) x, y = point.real, point.imag if len(points) == 0 or points[-1] != (x, y): @@ -117,9 +123,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 +318,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 +361,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 +450,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'))?\)' 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 b93ec75..470358f 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 index b767e0b..79bea26 100644 --- a/tclCommands/TclCommandImportSvg.py +++ b/tclCommands/TclCommandImportSvg.py @@ -58,14 +58,14 @@ class TclCommandImportSvg(TclCommand.TclCommandSignaled): else: outname = filename.split('/')[-1].split('\\')[-1] - with self.app.proc_container.new("Opening Gerber"): + with self.app.proc_container.new("Import SVG"): # Object creation - self.app.new_object("gerber", outname, obj_init) + self.app.new_object("geometry", outname, obj_init) # Register recent file - self.file_opened.emit("svg", filename) + self.app.file_opened.emit("svg", filename) # GUI feedback - self.inform.emit("Opened: " + filename) + self.app.inform.emit("Opened: " + filename) From 5c80f2b6d35658d42edfbe64493bfb6c897803be Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Tue, 12 Apr 2016 20:27:53 +0200 Subject: [PATCH 108/134] implemenmt basic test for import_svg --- tests/svg/7segment_9,9.svg | 34 +++ tests/svg/Arduino Nano3_pcb.svg | 468 ++++++++++++++++++++++++++++++++ tests/svg/usb_connector.svg | 77 ++++++ tests/test_tcl_shell.py | 41 ++- 4 files changed, 619 insertions(+), 1 deletion(-) create mode 100644 tests/svg/7segment_9,9.svg create mode 100644 tests/svg/Arduino Nano3_pcb.svg create mode 100644 tests/svg/usb_connector.svg diff --git a/tests/svg/7segment_9,9.svg b/tests/svg/7segment_9,9.svg new file mode 100644 index 0000000..ffe7c65 --- /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 0000000..f1f3b0c --- /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 0000000..25db707 --- /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 d36f30e..18b15f7 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,13 @@ import tempfile class TclShellTest(unittest.TestCase): + svg_files = 'tests/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' + geometry_name = "geometry" excellon_name = "excellon" gerber_top_name = "top" gerber_bottom_name = "bottom" @@ -177,4 +181,39 @@ 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)) + + excellon_obj = self.fc.collection.get_by_name(svg_file) + self.assertTrue(isinstance(excellon_obj, FlatCAMGeometry), + "Expected FlatCAMGeometry, instead, %s is %s" % + (self.excellon_name, type(excellon_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)) + + excellon_obj = self.fc.collection.get_by_name(outname) + self.assertTrue(isinstance(excellon_obj, FlatCAMGeometry), + "Expected FlatCAMGeometry, instead, %s is %s" % + (self.excellon_name, type(excellon_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))) From cd57af18bc358cd2d2c487c8da93d14668f04911 Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Tue, 12 Apr 2016 21:35:04 +0200 Subject: [PATCH 109/134] add option type (new object will be gerber or geometry) add tests for import_svg as gerber and geometry fix obj names in test_ import_svg --- tclCommands/TclCommandImportSvg.py | 14 +++++++++-- tests/test_tcl_shell.py | 40 +++++++++++++++++++++++++----- 2 files changed, 46 insertions(+), 8 deletions(-) diff --git a/tclCommands/TclCommandImportSvg.py b/tclCommands/TclCommandImportSvg.py index 79bea26..6f25567 100644 --- a/tclCommands/TclCommandImportSvg.py +++ b/tclCommands/TclCommandImportSvg.py @@ -17,6 +17,7 @@ class TclCommandImportSvg(TclCommand.TclCommandSignaled): # 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) ]) @@ -28,6 +29,7 @@ class TclCommandImportSvg(TclCommand.TclCommandSignaled): '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': [] @@ -47,7 +49,7 @@ class TclCommandImportSvg(TclCommand.TclCommandSignaled): def obj_init(geo_obj, app_obj): if not isinstance(geo_obj, Geometry): - self.raise_tcl_error('Expected Geometry, got %s %s.' % (outname, type(geo_obj))) + self.raise_tcl_error('Expected Geometry or Gerber, got %s %s.' % (outname, type(geo_obj))) geo_obj.import_svg(filename) @@ -58,10 +60,18 @@ class TclCommandImportSvg(TclCommand.TclCommandSignaled): 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 gebe 'geopmetry' or 'gerber' only, got '%s'." % obj_type) + with self.app.proc_container.new("Import SVG"): # Object creation - self.app.new_object("geometry", outname, obj_init) + self.app.new_object(obj_type, outname, obj_init) # Register recent file self.app.file_opened.emit("svg", filename) diff --git a/tests/test_tcl_shell.py b/tests/test_tcl_shell.py index 18b15f7..cc415f7 100644 --- a/tests/test_tcl_shell.py +++ b/tests/test_tcl_shell.py @@ -15,11 +15,13 @@ 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" @@ -200,20 +202,46 @@ class TclShellTest(unittest.TestCase): # import without outname self.fc.exec_command_test('import_svg "%s/%s"' % (self.svg_files, svg_file)) - excellon_obj = self.fc.collection.get_by_name(svg_file) - self.assertTrue(isinstance(excellon_obj, FlatCAMGeometry), + obj = self.fc.collection.get_by_name(svg_file) + self.assertTrue(isinstance(obj, FlatCAMGeometry), "Expected FlatCAMGeometry, instead, %s is %s" % - (self.excellon_name, type(excellon_obj))) + (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)) - excellon_obj = self.fc.collection.get_by_name(outname) - self.assertTrue(isinstance(excellon_obj, FlatCAMGeometry), + obj = self.fc.collection.get_by_name(outname) + self.assertTrue(isinstance(obj, FlatCAMGeometry), "Expected FlatCAMGeometry, instead, %s is %s" % - (self.excellon_name, type(excellon_obj))) + (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))) From be76b464adf9efa458afaf8cf1f6cbc7d94eaac1 Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Mon, 18 Apr 2016 20:36:41 +0200 Subject: [PATCH 110/134] fix typo error and cleaning --- svgparse.py | 5 +++-- tclCommands/TclCommandImportSvg.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/svgparse.py b/svgparse.py index 24f0462..544f0ba 100644 --- a/svgparse.py +++ b/svgparse.py @@ -93,9 +93,10 @@ def path2shapely(path, res=1.0): if steps == 0: steps = 1 + frac = 1.0 / steps + # print length, steps, frac for i in range(steps): - frac = 1.0 / steps point = component.point(i * frac) x, y = point.real, point.imag if len(points) == 0 or points[-1] != (x, y): @@ -450,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/TclCommandImportSvg.py b/tclCommands/TclCommandImportSvg.py index 6f25567..51cc190 100644 --- a/tclCommands/TclCommandImportSvg.py +++ b/tclCommands/TclCommandImportSvg.py @@ -66,7 +66,7 @@ class TclCommandImportSvg(TclCommand.TclCommandSignaled): obj_type = 'geometry' if obj_type != "geometry" and obj_type != "gerber": - self.raise_tcl_error("Option type can gebe 'geopmetry' or 'gerber' only, got '%s'." % obj_type) + self.raise_tcl_error("Option type can be 'geopmetry' or 'gerber' only, got '%s'." % obj_type) with self.app.proc_container.new("Import SVG"): From 05f88af917c609a5c7f9ead5d06d0e6a707c234d Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Sun, 24 Apr 2016 01:24:54 +0200 Subject: [PATCH 111/134] separate tcl tests into smaller chunks implement collection of tcl command tests --- tests/tclCommands/__init__.py | 17 ++++ .../tclCommands/test_TclCommandAddPolygon.py | 18 ++++ .../tclCommands/test_TclCommandAddPolyline.py | 18 ++++ tests/tclCommands/test_TclCommandImportSvg.py | 60 +++++++++++++ tests/tclCommands/test_TclCommandNew.py | 48 +++++++++++ .../tclCommands/test_TclCommandNewGeometry.py | 14 ++++ .../test_TclCommandPaintPolygon.py | 25 ++++++ tests/test_tcl_shell.py | 84 +++---------------- 8 files changed, 211 insertions(+), 73 deletions(-) create mode 100644 tests/tclCommands/__init__.py create mode 100644 tests/tclCommands/test_TclCommandAddPolygon.py create mode 100644 tests/tclCommands/test_TclCommandAddPolyline.py create mode 100644 tests/tclCommands/test_TclCommandImportSvg.py create mode 100644 tests/tclCommands/test_TclCommandNew.py create mode 100644 tests/tclCommands/test_TclCommandNewGeometry.py create mode 100644 tests/tclCommands/test_TclCommandPaintPolygon.py diff --git a/tests/tclCommands/__init__.py b/tests/tclCommands/__init__.py new file mode 100644 index 0000000..6842402 --- /dev/null +++ b/tests/tclCommands/__init__.py @@ -0,0 +1,17 @@ +import pkgutil +import sys + +# allowed command tests (please append them alphabetically ordered) +from test_TclCommandAddPolygon import * +from test_TclCommandAddPolyline import * +# from test_TclCommandCncjob import * +# from test_TclCommandDrillcncjob import * +# from test_TclCommandExportGcode import * +# from test_TclCommandExteriors import * +from test_TclCommandImportSvg import * +# from test_TclCommandInteriors import * +# from test_TclCommandIsolate import * +from test_TclCommandNew import * +from test_TclCommandNewGeometry import * +# from test_TclCommandOpenGerber import * +from test_TclCommandPaintPolygon import * diff --git a/tests/tclCommands/test_TclCommandAddPolygon.py b/tests/tclCommands/test_TclCommandAddPolygon.py new file mode 100644 index 0000000..e2099ad --- /dev/null +++ b/tests/tclCommands/test_TclCommandAddPolygon.py @@ -0,0 +1,18 @@ +from FlatCAMObj import FlatCAMGeometry + + +def test_add_polygon(self): + """ + Test add polygon into geometry + :param self: + :return: + """ + + self.fc.exec_command_test('new_geometry "%s"' % self.geometry_name) + geometry_obj = self.fc.collection.get_by_name(self.geometry_name) + self.assertTrue(isinstance(geometry_obj, FlatCAMGeometry), "Expected FlatCAMGeometry, instead, %s is %s" + % (self.geometry_name, type(geometry_obj))) + + points = '0 0 20 0 10 10 0 10' + + self.fc.exec_command_test('add_polygon "%s" %s' % (self.geometry_name, points)) diff --git a/tests/tclCommands/test_TclCommandAddPolyline.py b/tests/tclCommands/test_TclCommandAddPolyline.py new file mode 100644 index 0000000..69c0577 --- /dev/null +++ b/tests/tclCommands/test_TclCommandAddPolyline.py @@ -0,0 +1,18 @@ +from FlatCAMObj import FlatCAMGeometry + + +def test_add_polyline(self): + """ + Test add polyline into geometry + :param self: + :return: + """ + + self.fc.exec_command_test('new_geometry "%s"' % self.geometry_name) + geometry_obj = self.fc.collection.get_by_name(self.geometry_name) + self.assertTrue(isinstance(geometry_obj, FlatCAMGeometry), "Expected FlatCAMGeometry, instead, %s is %s" + % (self.geometry_name, type(geometry_obj))) + + points = '0 0 20 0 10 10 0 10 33 33' + + self.fc.exec_command_test('add_polyline "%s" %s' % (self.geometry_name, points)) diff --git a/tests/tclCommands/test_TclCommandImportSvg.py b/tests/tclCommands/test_TclCommandImportSvg.py new file mode 100644 index 0000000..3db2590 --- /dev/null +++ b/tests/tclCommands/test_TclCommandImportSvg.py @@ -0,0 +1,60 @@ +from os import listdir + +from FlatCAMObj import FlatCAMGerber, FlatCAMGeometry + + +def test_import_svg(self): + """ + Test all SVG files inside svg directory. + Problematic SVG files shold be put there as test reference. + :param self: + :return: + """ + + 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('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('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))) diff --git a/tests/tclCommands/test_TclCommandNew.py b/tests/tclCommands/test_TclCommandNew.py new file mode 100644 index 0000000..07eba0b --- /dev/null +++ b/tests/tclCommands/test_TclCommandNew.py @@ -0,0 +1,48 @@ +from FlatCAMObj import FlatCAMGeometry + + +def test_new(self): + """ + Test new project + :param self: + :return: + """ + + self.fc.exec_command_test('new_geometry "%s"' % self.geometry_name) + geometry_obj = self.fc.collection.get_by_name(self.geometry_name) + self.assertTrue(isinstance(geometry_obj, FlatCAMGeometry), "Expected FlatCAMGeometry, instead, %s is %s" + % (self.geometry_name, type(geometry_obj))) + + self.fc.exec_command_test('proc testproc {} { puts "testresult" }') + + result = self.fc.exec_command_test('testproc') + + self.assertEqual(result, "testresult",'testproc should return "testresult"') + + self.fc.exec_command_test('set_sys units MM') + self.fc.exec_command_test('new') + + # object should not exists anymore + geometry_obj = self.fc.collection.get_by_name(self.geometry_name) + self.assertIsNone(geometry_obj, "Expected object to be None, instead, %s is %s" + % (self.geometry_name, type(geometry_obj))) + + # TODO after new it should delete all procedures and variables, we need to make sure "testproc" does not exists + + # Test it again with same names + + self.fc.exec_command_test('set_sys units MM') + self.fc.exec_command_test('new') + + self.fc.exec_command_test('new_geometry "%s"' % self.geometry_name) + geometry_obj = self.fc.collection.get_by_name(self.geometry_name) + self.assertTrue(isinstance(geometry_obj, FlatCAMGeometry), "Expected FlatCAMGeometry, instead, %s is %s" + % (self.geometry_name, type(geometry_obj))) + + self.fc.exec_command_test('set_sys units MM') + self.fc.exec_command_test('new') + + # object should not exists anymore + geometry_obj = self.fc.collection.get_by_name(self.geometry_name) + self.assertIsNone(geometry_obj, "Expected object to be None, instead, %s is %s" + % (self.geometry_name, type(geometry_obj))) diff --git a/tests/tclCommands/test_TclCommandNewGeometry.py b/tests/tclCommands/test_TclCommandNewGeometry.py new file mode 100644 index 0000000..72e069c --- /dev/null +++ b/tests/tclCommands/test_TclCommandNewGeometry.py @@ -0,0 +1,14 @@ +from FlatCAMObj import FlatCAMGeometry + + +def test_new_geometry(self): + """ + Test create new geometry + :param self: + :return: + """ + + self.fc.exec_command_test('new_geometry "%s"' % self.geometry_name) + geometry_obj = self.fc.collection.get_by_name(self.geometry_name) + self.assertTrue(isinstance(geometry_obj, FlatCAMGeometry), "Expected FlatCAMGeometry, instead, %s is %s" + % (self.geometry_name, type(geometry_obj))) diff --git a/tests/tclCommands/test_TclCommandPaintPolygon.py b/tests/tclCommands/test_TclCommandPaintPolygon.py new file mode 100644 index 0000000..cc0c561 --- /dev/null +++ b/tests/tclCommands/test_TclCommandPaintPolygon.py @@ -0,0 +1,25 @@ +from FlatCAMObj import FlatCAMGeometry + + +def test_paint_polygon(self): + """ + Test create paint polygon geometry + :param self: + :return: + """ + + self.fc.exec_command_test('new_geometry "%s"' % self.geometry_name) + geometry_obj = self.fc.collection.get_by_name(self.geometry_name) + self.assertTrue(isinstance(geometry_obj, FlatCAMGeometry), "Expected FlatCAMGeometry, instead, %s is %s" + % (self.geometry_name, type(geometry_obj))) + + points = '0 0 20 0 10 10 0 10' + + self.fc.exec_command_test('add_polygon "%s" %s' % (self.geometry_name, points)) + + # TODO rename to paint_polygon in future oop command implementation + self.fc.exec_command_test('paint_poly "%s" 5 5 2 0.5' % (self.geometry_name)) + geometry_obj = self.fc.collection.get_by_name(self.geometry_name+'_paint') + # TODO uncoment check after oop implementation, because of threading inside paint poly + #self.assertTrue(isinstance(geometry_obj, FlatCAMGeometry), "Expected FlatCAMGeometry, instead, %s is %s" + # % (self.geometry_name+'_paint', type(geometry_obj))) diff --git a/tests/test_tcl_shell.py b/tests/test_tcl_shell.py index cc415f7..3838fff 100644 --- a/tests/test_tcl_shell.py +++ b/tests/test_tcl_shell.py @@ -31,6 +31,10 @@ class TclShellTest(unittest.TestCase): cutout_diameter = 3 drill_diameter = 0.8 + # load test methods to split huge test file into smaller pieces + # reason for this is reuse one test window only, + from tests.tclCommands import * + @classmethod def setUpClass(self): @@ -41,6 +45,10 @@ class TclShellTest(unittest.TestCase): self.fc = App(user_defaults=False) self.fc.ui.shell_dock.show() + def setUp(self): + self.fc.exec_command_test('set_sys units MM') + self.fc.exec_command_test('new') + @classmethod def tearDownClass(self): self.fc.tcl=None @@ -65,14 +73,10 @@ class TclShellTest(unittest.TestCase): self.assertEquals(units, "MM") - def test_gerber_flow(self): + def aatest_gerber_flow(self): # open gerber files top, bottom and cutout - - self.fc.exec_command_test('set_sys units MM') - self.fc.exec_command_test('new') - self.fc.exec_command_test('open_gerber %s/%s -outname %s' % (self.gerber_files, self.copper_top_filename, self.gerber_top_name)) gerber_top_obj = self.fc.collection.get_by_name(self.gerber_top_name) self.assertTrue(isinstance(gerber_top_obj, FlatCAMGerber), @@ -159,10 +163,7 @@ class TclShellTest(unittest.TestCase): # TODO: tests for tcl - def test_open_gerber(self): - - self.fc.exec_command_test('set_sys units MM') - self.fc.exec_command_test('new') + def aatest_open_gerber(self): self.fc.exec_command_test('open_gerber %s/%s -outname %s' % (self.gerber_files, self.copper_top_filename, self.gerber_top_name)) gerber_top_obj = self.fc.collection.get_by_name(self.gerber_top_name) @@ -170,10 +171,8 @@ class TclShellTest(unittest.TestCase): "Expected FlatCAMGerber, instead, %s is %s" % (self.gerber_top_name, type(gerber_top_obj))) - def test_excellon_flow(self): + def aatest_excellon_flow(self): - self.fc.exec_command_test('set_sys units MM') - self.fc.exec_command_test('new') self.fc.exec_command_test('open_excellon %s/%s -outname %s' % (self.gerber_files, self.excellon_filename, self.excellon_name)) excellon_obj = self.fc.collection.get_by_name(self.excellon_name) self.assertTrue(isinstance(excellon_obj, FlatCAMExcellon), @@ -184,64 +183,3 @@ class TclShellTest(unittest.TestCase): self.fc.exec_command_test('mirror %s -box %s -axis X' % (self.excellon_name, self.gerber_cutout_name)) # 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))) From acb70c0cc3d59094beb05f5febb1def7c53b72c0 Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Sun, 24 Apr 2016 21:24:56 +0200 Subject: [PATCH 112/134] implement test for test_TclCommandOpenGerber --- tests/tclCommands/__init__.py | 2 +- .../tclCommands/test_TclCommandOpenGerber.py | 25 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 tests/tclCommands/test_TclCommandOpenGerber.py diff --git a/tests/tclCommands/__init__.py b/tests/tclCommands/__init__.py index 6842402..0b91f03 100644 --- a/tests/tclCommands/__init__.py +++ b/tests/tclCommands/__init__.py @@ -13,5 +13,5 @@ from test_TclCommandImportSvg import * # from test_TclCommandIsolate import * from test_TclCommandNew import * from test_TclCommandNewGeometry import * -# from test_TclCommandOpenGerber import * +from test_TclCommandOpenGerber import * from test_TclCommandPaintPolygon import * diff --git a/tests/tclCommands/test_TclCommandOpenGerber.py b/tests/tclCommands/test_TclCommandOpenGerber.py new file mode 100644 index 0000000..1510021 --- /dev/null +++ b/tests/tclCommands/test_TclCommandOpenGerber.py @@ -0,0 +1,25 @@ +from FlatCAMObj import FlatCAMGerber + + +def test_open_gerber(self): + """ + Test open gerber project + :param self: + :return: + """ + + self.fc.exec_command_test('open_gerber %s/%s -outname %s' + % (self.gerber_files, self.copper_top_filename, self.gerber_top_name)) + gerber_top_obj = self.fc.collection.get_by_name(self.gerber_top_name) + self.assertTrue(isinstance(gerber_top_obj, FlatCAMGerber), "Expected FlatCAMGerber, instead, %s is %s" + % (self.gerber_top_name, type(gerber_top_obj))) + + self.fc.exec_command_test('open_gerber %s/%s -outname %s' + % (self.gerber_files, self.copper_bottom_filename, self.gerber_bottom_name)) + gerber_bottom_obj = self.fc.collection.get_by_name(self.gerber_bottom_name) + self.assertTrue(isinstance(gerber_bottom_obj, FlatCAMGerber), "Expected FlatCAMGerber, instead, %s is %s" + % (self.gerber_bottom_name, type(gerber_bottom_obj))) + + #just read with original name + self.fc.exec_command_test('open_gerber %s/%s' + % (self.gerber_files, self.copper_top_filename)) From 23dc2059f06e1f8e3abc771f1aef7f762979fb45 Mon Sep 17 00:00:00 2001 From: sopak Date: Sun, 24 Apr 2016 22:05:07 +0200 Subject: [PATCH 113/134] implement test_TclCommandIsolate --- tests/tclCommands/__init__.py | 2 +- tests/tclCommands/test_TclCommandIsolate.py | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 tests/tclCommands/test_TclCommandIsolate.py diff --git a/tests/tclCommands/__init__.py b/tests/tclCommands/__init__.py index 0b91f03..b60be8a 100644 --- a/tests/tclCommands/__init__.py +++ b/tests/tclCommands/__init__.py @@ -10,7 +10,7 @@ from test_TclCommandAddPolyline import * # from test_TclCommandExteriors import * from test_TclCommandImportSvg import * # from test_TclCommandInteriors import * -# from test_TclCommandIsolate import * +from test_TclCommandIsolate import * from test_TclCommandNew import * from test_TclCommandNewGeometry import * from test_TclCommandOpenGerber import * diff --git a/tests/tclCommands/test_TclCommandIsolate.py b/tests/tclCommands/test_TclCommandIsolate.py new file mode 100644 index 0000000..3823d61 --- /dev/null +++ b/tests/tclCommands/test_TclCommandIsolate.py @@ -0,0 +1,18 @@ +from FlatCAMObj import FlatCAMGerber + + +def test_isolate(self): + """ + Test isolate gerber + :param self: + :return: + """ + + self.fc.exec_command_test('open_gerber %s/%s -outname %s' + % (self.gerber_files, self.copper_top_filename, self.gerber_top_name)) + gerber_top_obj = self.fc.collection.get_by_name(self.gerber_top_name) + self.assertTrue(isinstance(gerber_top_obj, FlatCAMGerber), "Expected FlatCAMGerber, instead, %s is %s" + % (self.gerber_top_name, type(gerber_top_obj))) + + # isolate traces + self.fc.exec_command_test('isolate %s -dia %f' % (self.gerber_top_name, self.engraver_diameter)) From 56ba233fd66c3b56112b6c7af4d7cb696cef0a02 Mon Sep 17 00:00:00 2001 From: sopak Date: Sun, 24 Apr 2016 22:44:28 +0200 Subject: [PATCH 114/134] implement test_TclCommandExteriors implement test_TclCommandInteriors --- tests/tclCommands/__init__.py | 4 ++-- tests/tclCommands/test_TclCommandExteriors.py | 24 +++++++++++++++++++ tests/tclCommands/test_TclCommandInteriors.py | 24 +++++++++++++++++++ 3 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 tests/tclCommands/test_TclCommandExteriors.py create mode 100644 tests/tclCommands/test_TclCommandInteriors.py diff --git a/tests/tclCommands/__init__.py b/tests/tclCommands/__init__.py index b60be8a..0829032 100644 --- a/tests/tclCommands/__init__.py +++ b/tests/tclCommands/__init__.py @@ -7,9 +7,9 @@ from test_TclCommandAddPolyline import * # from test_TclCommandCncjob import * # from test_TclCommandDrillcncjob import * # from test_TclCommandExportGcode import * -# from test_TclCommandExteriors import * +from test_TclCommandExteriors import * from test_TclCommandImportSvg import * -# from test_TclCommandInteriors import * +from test_TclCommandInteriors import * from test_TclCommandIsolate import * from test_TclCommandNew import * from test_TclCommandNewGeometry import * diff --git a/tests/tclCommands/test_TclCommandExteriors.py b/tests/tclCommands/test_TclCommandExteriors.py new file mode 100644 index 0000000..da47be9 --- /dev/null +++ b/tests/tclCommands/test_TclCommandExteriors.py @@ -0,0 +1,24 @@ +from FlatCAMObj import FlatCAMGerber, FlatCAMGeometry + + +def test_exteriors(self): + """ + Test exteriors + :param self: + :return: + """ + + self.fc.exec_command_test('open_gerber %s/%s -outname %s' + % (self.gerber_files, self.cutout_filename, self.gerber_cutout_name)) + gerber_cutout_obj = self.fc.collection.get_by_name(self.gerber_cutout_name) + self.assertTrue(isinstance(gerber_cutout_obj, FlatCAMGerber), "Expected FlatCAMGerber, instead, %s is %s" + % (self.gerber_cutout_name, type(gerber_cutout_obj))) + + # exteriors interiors and delete isolated traces + self.fc.exec_command_test('isolate %s -dia %f' % (self.gerber_cutout_name, self.engraver_diameter)) + self.fc.exec_command_test('exteriors %s -outname %s' + % (self.gerber_cutout_name + '_iso', self.gerber_cutout_name + '_iso_exterior')) + self.fc.exec_command_test('delete %s' % (self.gerber_cutout_name + '_iso')) + obj = self.fc.collection.get_by_name(self.gerber_cutout_name + '_iso_exterior') + self.assertTrue(isinstance(obj, FlatCAMGeometry), "Expected FlatCAMGeometry, instead, %s is %s" + % (self.gerber_cutout_name + '_iso_exterior', type(obj))) diff --git a/tests/tclCommands/test_TclCommandInteriors.py b/tests/tclCommands/test_TclCommandInteriors.py new file mode 100644 index 0000000..c58c380 --- /dev/null +++ b/tests/tclCommands/test_TclCommandInteriors.py @@ -0,0 +1,24 @@ +from FlatCAMObj import FlatCAMGerber, FlatCAMGeometry + + +def test_interiors(self): + """ + Test interiors + :param self: + :return: + """ + + self.fc.exec_command_test('open_gerber %s/%s -outname %s' + % (self.gerber_files, self.cutout_filename, self.gerber_cutout_name)) + gerber_cutout_obj = self.fc.collection.get_by_name(self.gerber_cutout_name) + self.assertTrue(isinstance(gerber_cutout_obj, FlatCAMGerber), "Expected FlatCAMGerber, instead, %s is %s" + % (self.gerber_cutout_name, type(gerber_cutout_obj))) + + # interiors and delete isolated traces + self.fc.exec_command_test('isolate %s -dia %f' % (self.gerber_cutout_name, self.engraver_diameter)) + self.fc.exec_command_test('interiors %s -outname %s' + % (self.gerber_cutout_name + '_iso', self.gerber_cutout_name + '_iso_interior')) + self.fc.exec_command_test('delete %s' % (self.gerber_cutout_name + '_iso')) + obj = self.fc.collection.get_by_name(self.gerber_cutout_name + '_iso_interior') + self.assertTrue(isinstance(obj, FlatCAMGeometry), "Expected FlatCAMGeometry, instead, %s is %s" + % (self.gerber_cutout_name + '_iso_interior', type(obj))) From 7d465f081414cded2c125adee1f2cee524bf6b3f Mon Sep 17 00:00:00 2001 From: sopak Date: Mon, 25 Apr 2016 00:36:58 +0200 Subject: [PATCH 115/134] implement tests for TCL commands --- tests/tclCommands/__init__.py | 7 ++-- tests/tclCommands/test_TclCommandCncjob.py | 17 ++++++++++ .../tclCommands/test_TclCommandDrillcncjob.py | 18 ++++++++++ .../tclCommands/test_TclCommandExportGcode.py | 33 +++++++++++++++++++ tests/tclCommands/test_TclCommandIsolate.py | 5 ++- .../test_TclCommandOpenExcellon.py | 15 +++++++++ .../tclCommands/test_TclCommandOpenGerber.py | 2 +- 7 files changed, 92 insertions(+), 5 deletions(-) create mode 100644 tests/tclCommands/test_TclCommandCncjob.py create mode 100644 tests/tclCommands/test_TclCommandDrillcncjob.py create mode 100644 tests/tclCommands/test_TclCommandExportGcode.py create mode 100644 tests/tclCommands/test_TclCommandOpenExcellon.py diff --git a/tests/tclCommands/__init__.py b/tests/tclCommands/__init__.py index 0829032..2d1ed5c 100644 --- a/tests/tclCommands/__init__.py +++ b/tests/tclCommands/__init__.py @@ -4,14 +4,15 @@ import sys # allowed command tests (please append them alphabetically ordered) from test_TclCommandAddPolygon import * from test_TclCommandAddPolyline import * -# from test_TclCommandCncjob import * -# from test_TclCommandDrillcncjob import * -# from test_TclCommandExportGcode import * +from test_TclCommandCncjob import * +from test_TclCommandDrillcncjob import * +from test_TclCommandExportGcode import * from test_TclCommandExteriors import * from test_TclCommandImportSvg import * from test_TclCommandInteriors import * from test_TclCommandIsolate import * from test_TclCommandNew import * from test_TclCommandNewGeometry import * +from test_TclCommandOpenExcellon import * from test_TclCommandOpenGerber import * from test_TclCommandPaintPolygon import * diff --git a/tests/tclCommands/test_TclCommandCncjob.py b/tests/tclCommands/test_TclCommandCncjob.py new file mode 100644 index 0000000..cdd8e79 --- /dev/null +++ b/tests/tclCommands/test_TclCommandCncjob.py @@ -0,0 +1,17 @@ +from FlatCAMObj import FlatCAMGerber, FlatCAMGeometry, FlatCAMObj +from test_TclCommandIsolate import * + +def test_cncjob(self): + """ + Test cncjob + :param self: + :return: + """ + + # reuse isolate tests + test_isolate(self) + + self.fc.exec_command_test('cncjob %s_iso -tooldia 0.5 -z_cut 0.05 -z_move 3 -feedrate 300' % self.gerber_top_name) + cam_top_obj = self.fc.collection.get_by_name(self.gerber_top_name + '_iso_cnc') + self.assertTrue(isinstance(cam_top_obj, FlatCAMObj), "Expected FlatCAMObj, instead, %s is %s" + % (self.gerber_top_name + '_iso_cnc', type(cam_top_obj))) \ No newline at end of file diff --git a/tests/tclCommands/test_TclCommandDrillcncjob.py b/tests/tclCommands/test_TclCommandDrillcncjob.py new file mode 100644 index 0000000..78326d2 --- /dev/null +++ b/tests/tclCommands/test_TclCommandDrillcncjob.py @@ -0,0 +1,18 @@ +from FlatCAMObj import FlatCAMObj +from test_TclCommandOpenExcellon import * + + +def test_drillcncjob(self): + """ + Test cncjob + :param self: + :return: + """ + # reuse open excellontests + test_open_excellon(self) + + self.fc.exec_command_test('drillcncjob %s -tools all -drillz 0.5 -travelz 3 -feedrate 300' + % self.excellon_name) + cam_top_obj = self.fc.collection.get_by_name(self.excellon_name + '_cnc') + self.assertTrue(isinstance(cam_top_obj, FlatCAMObj), "Expected FlatCAMObj, instead, %s is %s" + % (self.excellon_name + '_cnc', type(cam_top_obj))) diff --git a/tests/tclCommands/test_TclCommandExportGcode.py b/tests/tclCommands/test_TclCommandExportGcode.py new file mode 100644 index 0000000..102e6e3 --- /dev/null +++ b/tests/tclCommands/test_TclCommandExportGcode.py @@ -0,0 +1,33 @@ +import os +import tempfile + +from test_TclCommandCncjob import * +from test_TclCommandDrillcncjob import * + + +def test_export_gcodecncjob(self): + """ + Test cncjob + :param self: + :return: + """ + + # reuse tests + test_cncjob(self) + test_drillcncjob(self) + + with tempfile.NamedTemporaryFile(prefix='unittest.', suffix="." + self.excellon_name + '.gcode', delete=True)\ + as tmp_file: + output_filename = tmp_file.name + self.fc.exec_command_test('write_gcode "%s" "%s"' % (self.excellon_name + '_cnc', output_filename)) + self.assertTrue(os.path.isfile(output_filename)) + os.remove(output_filename) + + with tempfile.NamedTemporaryFile(prefix='unittest.', suffix="." + self.gerber_top_name + '.gcode', delete=True)\ + as tmp_file: + output_filename = tmp_file.name + self.fc.exec_command_test('write_gcode "%s" "%s"' % (self.gerber_top_name + '_iso_cnc', output_filename)) + self.assertTrue(os.path.isfile(output_filename)) + os.remove(output_filename) + + # TODO check what is inside files , it should be same every time \ No newline at end of file diff --git a/tests/tclCommands/test_TclCommandIsolate.py b/tests/tclCommands/test_TclCommandIsolate.py index 3823d61..e61aa40 100644 --- a/tests/tclCommands/test_TclCommandIsolate.py +++ b/tests/tclCommands/test_TclCommandIsolate.py @@ -1,4 +1,4 @@ -from FlatCAMObj import FlatCAMGerber +from FlatCAMObj import FlatCAMGerber, FlatCAMGeometry def test_isolate(self): @@ -16,3 +16,6 @@ def test_isolate(self): # isolate traces self.fc.exec_command_test('isolate %s -dia %f' % (self.gerber_top_name, self.engraver_diameter)) + geometry_top_obj = self.fc.collection.get_by_name(self.gerber_top_name+'_iso') + self.assertTrue(isinstance(geometry_top_obj, FlatCAMGeometry), "Expected FlatCAMGeometry, instead, %s is %s" + % (self.gerber_top_name+'_iso', type(geometry_top_obj))) \ No newline at end of file diff --git a/tests/tclCommands/test_TclCommandOpenExcellon.py b/tests/tclCommands/test_TclCommandOpenExcellon.py new file mode 100644 index 0000000..7570ea5 --- /dev/null +++ b/tests/tclCommands/test_TclCommandOpenExcellon.py @@ -0,0 +1,15 @@ +from FlatCAMObj import FlatCAMExcellon + + +def test_open_excellon(self): + """ + Test open excellon file + :param self: + :return: + """ + + self.fc.exec_command_test('open_excellon %s/%s -outname %s' + % (self.gerber_files, self.excellon_filename, self.excellon_name)) + excellon_obj = self.fc.collection.get_by_name(self.excellon_name) + self.assertTrue(isinstance(excellon_obj, FlatCAMExcellon), "Expected FlatCAMExcellon, instead, %s is %s" + % (self.excellon_name, type(excellon_obj))) diff --git a/tests/tclCommands/test_TclCommandOpenGerber.py b/tests/tclCommands/test_TclCommandOpenGerber.py index 1510021..71d50b0 100644 --- a/tests/tclCommands/test_TclCommandOpenGerber.py +++ b/tests/tclCommands/test_TclCommandOpenGerber.py @@ -3,7 +3,7 @@ from FlatCAMObj import FlatCAMGerber def test_open_gerber(self): """ - Test open gerber project + Test open gerber file :param self: :return: """ From f9260daa1770de08acf3430ef004c8645761a9dd Mon Sep 17 00:00:00 2001 From: sopak Date: Mon, 25 Apr 2016 00:40:14 +0200 Subject: [PATCH 116/134] remove forgotten aa debug prefix names --- tests/test_tcl_shell.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_tcl_shell.py b/tests/test_tcl_shell.py index 3838fff..be1f81d 100644 --- a/tests/test_tcl_shell.py +++ b/tests/test_tcl_shell.py @@ -73,7 +73,7 @@ class TclShellTest(unittest.TestCase): self.assertEquals(units, "MM") - def aatest_gerber_flow(self): + def test_gerber_flow(self): # open gerber files top, bottom and cutout @@ -163,7 +163,7 @@ class TclShellTest(unittest.TestCase): # TODO: tests for tcl - def aatest_open_gerber(self): + def test_open_gerber(self): self.fc.exec_command_test('open_gerber %s/%s -outname %s' % (self.gerber_files, self.copper_top_filename, self.gerber_top_name)) gerber_top_obj = self.fc.collection.get_by_name(self.gerber_top_name) @@ -171,7 +171,7 @@ class TclShellTest(unittest.TestCase): "Expected FlatCAMGerber, instead, %s is %s" % (self.gerber_top_name, type(gerber_top_obj))) - def aatest_excellon_flow(self): + def test_excellon_flow(self): self.fc.exec_command_test('open_excellon %s/%s -outname %s' % (self.gerber_files, self.excellon_filename, self.excellon_name)) excellon_obj = self.fc.collection.get_by_name(self.excellon_name) From 18d53155734a08833ea1597f1b2fc7e989dae112 Mon Sep 17 00:00:00 2001 From: jpcgt Date: Mon, 23 May 2016 13:21:10 +0000 Subject: [PATCH 117/134] cirkuix.pyc deleted online with Bitbucket --- cirkuix.pyc | Bin 9442 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 cirkuix.pyc diff --git a/cirkuix.pyc b/cirkuix.pyc deleted file mode 100644 index 3732da81826408c773fe186eeac5fdb771a77407..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9442 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 From a3dbaff258ce6be2055c1f8328f9aef4e262fb6a Mon Sep 17 00:00:00 2001 From: jpcgt Date: Mon, 23 May 2016 13:21:25 +0000 Subject: [PATCH 118/134] camlib.pyc deleted online with Bitbucket --- camlib.pyc | Bin 85665 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 camlib.pyc diff --git a/camlib.pyc b/camlib.pyc deleted file mode 100644 index 7f788a0a00238d15c88118a9d928ad5804d46f5d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 85665 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 Date: Fri, 3 Jun 2016 17:15:35 -0400 Subject: [PATCH 119/134] Cleaned up tests for shell. Added test steps for gerber flow. --- FlatCAMApp.py | 1 + FlatCAMObj.py | 2 +- tclCommands/__init__.py | 9 ++-- tests/test_gerber_flow.py | 46 ++++++++++++++++++- .../__init__.py | 0 .../test_TclCommandAddPolygon.py | 0 .../test_TclCommandAddPolyline.py | 0 .../test_TclCommandCncjob.py | 0 .../test_TclCommandDrillcncjob.py | 0 .../test_TclCommandExportGcode.py | 0 .../test_TclCommandExteriors.py | 0 .../test_TclCommandImportSvg.py | 0 .../test_TclCommandInteriors.py | 0 .../test_TclCommandIsolate.py | 0 .../test_TclCommandNew.py | 0 .../test_TclCommandNewGeometry.py | 0 .../test_TclCommandOpenExcellon.py | 0 .../test_TclCommandOpenGerber.py | 0 .../test_TclCommandPaintPolygon.py | 0 tests/test_tcl_shell.py | 26 ++++++----- 20 files changed, 65 insertions(+), 19 deletions(-) rename tests/{tclCommands => test_tclCommands}/__init__.py (100%) rename tests/{tclCommands => test_tclCommands}/test_TclCommandAddPolygon.py (100%) rename tests/{tclCommands => test_tclCommands}/test_TclCommandAddPolyline.py (100%) rename tests/{tclCommands => test_tclCommands}/test_TclCommandCncjob.py (100%) rename tests/{tclCommands => test_tclCommands}/test_TclCommandDrillcncjob.py (100%) rename tests/{tclCommands => test_tclCommands}/test_TclCommandExportGcode.py (100%) rename tests/{tclCommands => test_tclCommands}/test_TclCommandExteriors.py (100%) rename tests/{tclCommands => test_tclCommands}/test_TclCommandImportSvg.py (100%) rename tests/{tclCommands => test_tclCommands}/test_TclCommandInteriors.py (100%) rename tests/{tclCommands => test_tclCommands}/test_TclCommandIsolate.py (100%) rename tests/{tclCommands => test_tclCommands}/test_TclCommandNew.py (100%) rename tests/{tclCommands => test_tclCommands}/test_TclCommandNewGeometry.py (100%) rename tests/{tclCommands => test_tclCommands}/test_TclCommandOpenExcellon.py (100%) rename tests/{tclCommands => test_tclCommands}/test_TclCommandOpenGerber.py (100%) rename tests/{tclCommands => test_tclCommands}/test_TclCommandPaintPolygon.py (100%) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index f9ca0a4..c239118 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -29,6 +29,7 @@ from MeasurementTool import Measurement from DblSidedTool import DblSidedTool import tclCommands + ######################################## ## App ## ######################################## diff --git a/FlatCAMObj.py b/FlatCAMObj.py index b475692..722e9c4 100644 --- a/FlatCAMObj.py +++ b/FlatCAMObj.py @@ -327,7 +327,7 @@ class FlatCAMGerber(FlatCAMObj, Gerber): "isotooldia": self.ui.iso_tool_dia_entry, "isopasses": self.ui.iso_width_entry, "isooverlap": self.ui.iso_overlap_entry, - "combine_passes":self.ui.combine_passes_cb, + "combine_passes": self.ui.combine_passes_cb, "cutouttooldia": self.ui.cutout_tooldia_entry, "cutoutmargin": self.ui.cutout_margin_entry, "cutoutgapsize": self.ui.cutout_gap_entry, diff --git a/tclCommands/__init__.py b/tclCommands/__init__.py index 0885d14..f02b02d 100644 --- a/tclCommands/__init__.py +++ b/tclCommands/__init__.py @@ -21,15 +21,16 @@ for loader, name, is_pkg in pkgutil.walk_packages(__path__): module = loader.find_module(name).load_module(name) __all__.append(name) + def register_all_commands(app, commands): """ Static method which register all known commands. - Command should be for now in directory tclCommands and module should start with TCLCommand + Command should be for now in directory test_tclCommands and module should start with TCLCommand Class have to follow same name as module. we need import all modules in top section: - import tclCommands.TclCommandExteriors + import test_tclCommands.TclCommandExteriors at this stage we can include only wanted commands with this, auto loading may be implemented in future I have no enough knowledge about python's anatomy. Would be nice to include all classes which are descendant etc. @@ -38,10 +39,10 @@ def register_all_commands(app, commands): :return: None """ - tcl_modules = {k: v for k, v in sys.modules.items() if k.startswith('tclCommands.TclCommand')} + tcl_modules = {k: v for k, v in sys.modules.items() if k.startswith('test_tclCommands.TclCommand')} for key, mod in tcl_modules.items(): - if key != 'tclCommands.TclCommand': + if key != 'test_tclCommands.TclCommand': class_name = key.split('.')[1] class_type = getattr(mod, class_name) command_instance = class_type(app) diff --git a/tests/test_gerber_flow.py b/tests/test_gerber_flow.py index 9eac0ef..23ef1c1 100644 --- a/tests/test_gerber_flow.py +++ b/tests/test_gerber_flow.py @@ -1,13 +1,14 @@ import sys import unittest from PyQt4 import QtGui -from FlatCAMApp import App +from FlatCAMApp import App, tclCommands from FlatCAMObj import FlatCAMGerber, FlatCAMGeometry, FlatCAMCNCjob from ObjectUI import GerberObjectUI, GeometryObjectUI from time import sleep import os import tempfile + class GerberFlowTestCase(unittest.TestCase): filename = 'simple1.gbr' @@ -51,6 +52,36 @@ class GerberFlowTestCase(unittest.TestCase): "Expected FlatCAMGerber, instead, %s is %s" % (gerber_name, type(gerber_obj))) + #---------------------------------------- + # Object's GUI matches Object's options + #---------------------------------------- + # TODO: Open GUI with double-click on object. + # Opens the Object's GUI, populates it. + gerber_obj.build_ui() + for option, value in gerber_obj.options.iteritems(): + try: + form_field = gerber_obj.form_fields[option] + except KeyError: + print ("**********************************************************\n" + "* WARNING: Option '{}' has no form field\n" + "**********************************************************" + "".format(option)) + continue + self.assertEqual(value, form_field.get_value(), + "Option '{}' == {} but form has {}".format( + option, value, form_field.get_value() + )) + + #-------------------------------------------------- + # Changes in the GUI should be read in when + # running any process. Changing something here. + #-------------------------------------------------- + + form_field = gerber_obj.form_fields['isotooldia'] + value = form_field.get_value() + form_field.set_value(value * 1.1) # Increase by 10% + print "'isotooldia' == {}".format(value) + #-------------------------------------------------- # Create isolation routing using default values # and by clicking on the button. @@ -58,11 +89,22 @@ class GerberFlowTestCase(unittest.TestCase): # Get the object's GUI and click on "Generate Geometry" under # "Isolation Routing" assert isinstance(gerber_obj, FlatCAMGerber) # Just for the IDE - gerber_obj.build_ui() # Open the object's UI. + # Changed: UI has been build already + #gerber_obj.build_ui() # Open the object's UI. ui = gerber_obj.ui assert isinstance(ui, GerberObjectUI) ui.generate_iso_button.click() # Click + #--------------------------------------------- + # Check that GUI has been read in. + #--------------------------------------------- + value = gerber_obj.options['isotooldia'] + form_value = form_field.get_value() + self.assertEqual(value, form_value, + "Form value for '{}' == {} was not read into options" + "which has {}".format('isotooldia', form_value, value)) + print "'isotooldia' == {}".format(value) + #--------------------------------------------- # Check that only 1 object has been created. #--------------------------------------------- diff --git a/tests/tclCommands/__init__.py b/tests/test_tclCommands/__init__.py similarity index 100% rename from tests/tclCommands/__init__.py rename to tests/test_tclCommands/__init__.py diff --git a/tests/tclCommands/test_TclCommandAddPolygon.py b/tests/test_tclCommands/test_TclCommandAddPolygon.py similarity index 100% rename from tests/tclCommands/test_TclCommandAddPolygon.py rename to tests/test_tclCommands/test_TclCommandAddPolygon.py diff --git a/tests/tclCommands/test_TclCommandAddPolyline.py b/tests/test_tclCommands/test_TclCommandAddPolyline.py similarity index 100% rename from tests/tclCommands/test_TclCommandAddPolyline.py rename to tests/test_tclCommands/test_TclCommandAddPolyline.py diff --git a/tests/tclCommands/test_TclCommandCncjob.py b/tests/test_tclCommands/test_TclCommandCncjob.py similarity index 100% rename from tests/tclCommands/test_TclCommandCncjob.py rename to tests/test_tclCommands/test_TclCommandCncjob.py diff --git a/tests/tclCommands/test_TclCommandDrillcncjob.py b/tests/test_tclCommands/test_TclCommandDrillcncjob.py similarity index 100% rename from tests/tclCommands/test_TclCommandDrillcncjob.py rename to tests/test_tclCommands/test_TclCommandDrillcncjob.py diff --git a/tests/tclCommands/test_TclCommandExportGcode.py b/tests/test_tclCommands/test_TclCommandExportGcode.py similarity index 100% rename from tests/tclCommands/test_TclCommandExportGcode.py rename to tests/test_tclCommands/test_TclCommandExportGcode.py diff --git a/tests/tclCommands/test_TclCommandExteriors.py b/tests/test_tclCommands/test_TclCommandExteriors.py similarity index 100% rename from tests/tclCommands/test_TclCommandExteriors.py rename to tests/test_tclCommands/test_TclCommandExteriors.py diff --git a/tests/tclCommands/test_TclCommandImportSvg.py b/tests/test_tclCommands/test_TclCommandImportSvg.py similarity index 100% rename from tests/tclCommands/test_TclCommandImportSvg.py rename to tests/test_tclCommands/test_TclCommandImportSvg.py diff --git a/tests/tclCommands/test_TclCommandInteriors.py b/tests/test_tclCommands/test_TclCommandInteriors.py similarity index 100% rename from tests/tclCommands/test_TclCommandInteriors.py rename to tests/test_tclCommands/test_TclCommandInteriors.py diff --git a/tests/tclCommands/test_TclCommandIsolate.py b/tests/test_tclCommands/test_TclCommandIsolate.py similarity index 100% rename from tests/tclCommands/test_TclCommandIsolate.py rename to tests/test_tclCommands/test_TclCommandIsolate.py diff --git a/tests/tclCommands/test_TclCommandNew.py b/tests/test_tclCommands/test_TclCommandNew.py similarity index 100% rename from tests/tclCommands/test_TclCommandNew.py rename to tests/test_tclCommands/test_TclCommandNew.py diff --git a/tests/tclCommands/test_TclCommandNewGeometry.py b/tests/test_tclCommands/test_TclCommandNewGeometry.py similarity index 100% rename from tests/tclCommands/test_TclCommandNewGeometry.py rename to tests/test_tclCommands/test_TclCommandNewGeometry.py diff --git a/tests/tclCommands/test_TclCommandOpenExcellon.py b/tests/test_tclCommands/test_TclCommandOpenExcellon.py similarity index 100% rename from tests/tclCommands/test_TclCommandOpenExcellon.py rename to tests/test_tclCommands/test_TclCommandOpenExcellon.py diff --git a/tests/tclCommands/test_TclCommandOpenGerber.py b/tests/test_tclCommands/test_TclCommandOpenGerber.py similarity index 100% rename from tests/tclCommands/test_TclCommandOpenGerber.py rename to tests/test_tclCommands/test_TclCommandOpenGerber.py diff --git a/tests/tclCommands/test_TclCommandPaintPolygon.py b/tests/test_tclCommands/test_TclCommandPaintPolygon.py similarity index 100% rename from tests/tclCommands/test_TclCommandPaintPolygon.py rename to tests/test_tclCommands/test_TclCommandPaintPolygon.py diff --git a/tests/test_tcl_shell.py b/tests/test_tcl_shell.py index be1f81d..510a491 100644 --- a/tests/test_tcl_shell.py +++ b/tests/test_tcl_shell.py @@ -12,6 +12,7 @@ from time import sleep import os import tempfile + class TclShellTest(unittest.TestCase): svg_files = 'tests/svg' @@ -33,28 +34,30 @@ class TclShellTest(unittest.TestCase): # load test methods to split huge test file into smaller pieces # reason for this is reuse one test window only, - from tests.tclCommands import * + + # CANNOT DO THIS HERE!!! + #from tests.test_tclCommands import * @classmethod - def setUpClass(self): + def setUpClass(cls): - self.setup=True - self.app = QtGui.QApplication(sys.argv) + cls.setup = True + cls.app = QtGui.QApplication(sys.argv) # Create App, keep app defaults (do not load # user-defined defaults). - self.fc = App(user_defaults=False) - self.fc.ui.shell_dock.show() + cls.fc = App(user_defaults=False) + cls.fc.ui.shell_dock.show() def setUp(self): self.fc.exec_command_test('set_sys units MM') self.fc.exec_command_test('new') @classmethod - def tearDownClass(self): - self.fc.tcl=None - self.app.closeAllWindows() - del self.fc - del self.app + def tearDownClass(cls): + cls.fc.tcl = None + cls.app.closeAllWindows() + del cls.fc + del cls.app pass def test_set_get_units(self): @@ -72,7 +75,6 @@ class TclShellTest(unittest.TestCase): units=self.fc.exec_command_test('get_sys units') self.assertEquals(units, "MM") - def test_gerber_flow(self): # open gerber files top, bottom and cutout From 6136afe84c38602b88f6cf0137bcab9fc81b5a85 Mon Sep 17 00:00:00 2001 From: Juan Pablo Caram Date: Fri, 3 Jun 2016 22:19:47 -0400 Subject: [PATCH 120/134] Added dwell (G4) post processing option to gcode. --- FlatCAMApp.py | 13 ++++++++--- FlatCAMGUI.py | 20 +++++++++++++++++ FlatCAMObj.py | 61 +++++++++++++++++++++++++++++++++++++++++++++------ ObjectUI.py | 22 +++++++++++++++++++ camlib.py | 12 ++++++---- 5 files changed, 114 insertions(+), 14 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index c239118..028fbfd 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -245,7 +245,9 @@ class App(QtCore.QObject): "cncjob_plot": self.defaults_form.cncjob_group.plot_cb, "cncjob_tooldia": self.defaults_form.cncjob_group.tooldia_entry, "cncjob_prepend": self.defaults_form.cncjob_group.prepend_text, - "cncjob_append": self.defaults_form.cncjob_group.append_text + "cncjob_append": self.defaults_form.cncjob_group.append_text, + "cncjob_dwell": self.defaults_form.cncjob_group.dwell_cb, + "cncjob_dwelltime": self.defaults_form.cncjob_group.dwelltime_cb } self.defaults = LoudDict() @@ -289,8 +291,13 @@ class App(QtCore.QObject): "cncjob_tooldia": 0.016, "cncjob_prepend": "", "cncjob_append": "", - "background_timeout": 300000, #default value is 5 minutes - "verbose_error_level": 0, # shell verbosity 0 = default(python trace only for unknown errors), 1 = show trace(show trace allways), 2 = (For the future). + "cncjob_dwell": True, + "cncjob_dwelltime": 1, + "background_timeout": 300000, # Default value is 5 minutes + "verbose_error_level": 0, # Shell verbosity 0 = default + # (python trace only for unknown errors), + # 1 = show trace(show trace allways), + # 2 = (For the future). # Persistence "last_folder": None, diff --git a/FlatCAMGUI.py b/FlatCAMGUI.py index 2ec95ac..9f22faa 100644 --- a/FlatCAMGUI.py +++ b/FlatCAMGUI.py @@ -806,6 +806,26 @@ class CNCJobOptionsGroupUI(OptionsGroupUI): self.append_text = FCTextArea() self.layout.addWidget(self.append_text) + # Dwell + grid1 = QtGui.QGridLayout() + self.layout.addLayout(grid1) + + dwelllabel = QtGui.QLabel('Dwell:') + dwelllabel.setToolTip( + "Pause to allow the spindle to reach its\n" + "speed before cutting." + ) + dwelltime = QtGui.QLabel('Duration [sec.]:') + dwelltime.setToolTip( + "Number of second to dwell." + ) + self.dwell_cb = FCCheckBox() + self.dwelltime_cb = FCEntry() + grid1.addWidget(dwelllabel, 0, 0) + grid1.addWidget(self.dwell_cb, 0, 1) + grid1.addWidget(dwelltime, 1, 0) + grid1.addWidget(self.dwelltime_cb, 1, 1) + class GlobalOptionsUI(QtGui.QWidget): """ diff --git a/FlatCAMObj.py b/FlatCAMObj.py index 722e9c4..199c63f 100644 --- a/FlatCAMObj.py +++ b/FlatCAMObj.py @@ -1,3 +1,4 @@ +from cStringIO import StringIO from PyQt4 import QtCore from copy import copy from ObjectUI import * @@ -981,7 +982,9 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob): "plot": True, "tooldia": 0.4 / 25.4, # 0.4mm in inches "append": "", - "prepend": "" + "prepend": "", + "dwell": False, + "dwelltime": 1 }) # Attributes to be included in serialization @@ -1001,7 +1004,9 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob): "plot": self.ui.plot_cb, "tooldia": self.ui.tooldia_entry, "append": self.ui.append_text, - "prepend": self.ui.prepend_text + "prepend": self.ui.prepend_text, + "dwell": self.ui.dwell_cb, + "dwelltime": self.ui.dwelltime_entry }) self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click) @@ -1019,6 +1024,8 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob): def on_exportgcode_button_click(self, *args): self.app.report_usage("cncjob_on_exportgcode_button") + self.read_form() + try: filename = QtGui.QFileDialog.getSaveFileName(caption="Export G-Code ...", directory=self.app.defaults["last_folder"]) @@ -1030,10 +1037,51 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob): self.export_gcode(filename, preamble=preamble, postamble=postamble) + def dwell_generator(self, lines): + + log.debug("dwell_generator()...") + + m3m4re = re.compile(r'^\s*[mM]0[34]') + g4re = re.compile(r'^\s*[gG]4\s+([\d\.\+\-e]+)') + bufline = None + + for line in lines: + # If the buffer contains a G4, yield that. + # If current line is a G4, discard it. + if bufline is not None: + yield bufline + bufline = None + + if not g4re.search(line): + yield line + + continue + + # If start spindle, buffer a G4. + if m3m4re.search(line): + log.debug("Found M03/4") + bufline = "G4 {}\n".format(self.options['dwelltime']) + + yield line + + raise StopIteration + def export_gcode(self, filename, preamble='', postamble=''): - f = open(filename, 'w') - f.write(preamble + '\n' + self.gcode + "\n" + postamble) - f.close() + + lines = StringIO(self.gcode) + + if self.options['dwell']: + log.debug("Will add G04!") + lines = self.dwell_generator(lines) + + with open(filename, 'w') as f: + f.write(preamble + "\n") + + for line in lines: + + f.write(line) + + f.write(postamble) # Just for adding it to the recent files list. self.app.file_opened.emit("cncjob", filename) @@ -1309,8 +1357,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): app_obj.progress.emit(80) - - if use_thread: + if use_thread: # To be run in separate thread def job_thread(app_obj): with self.app.proc_container.new("Generating CNC Job."): diff --git a/ObjectUI.py b/ObjectUI.py index f54f1c3..61b4b00 100644 --- a/ObjectUI.py +++ b/ObjectUI.py @@ -163,7 +163,9 @@ class CNCObjectUI(ObjectUI): ) self.custom_box.addWidget(self.updateplot_button) + ################## ## Export G-Code + ################## self.export_gcode_label = QtGui.QLabel("Export G-Code:") self.export_gcode_label.setToolTip( "Export and save G-Code to\n" @@ -194,6 +196,26 @@ class CNCObjectUI(ObjectUI): self.append_text = FCTextArea() self.custom_box.addWidget(self.append_text) + # Dwell + grid1 = QtGui.QGridLayout() + self.custom_box.addLayout(grid1) + + dwelllabel = QtGui.QLabel('Dwell:') + dwelllabel.setToolTip( + "Pause to allow the spindle to reach its\n" + "speed before cutting." + ) + dwelltime = QtGui.QLabel('Duration [sec.]:') + dwelltime.setToolTip( + "Number of second to dwell." + ) + self.dwell_cb = FCCheckBox() + self.dwelltime_entry = FCEntry() + grid1.addWidget(dwelllabel, 0, 0) + grid1.addWidget(self.dwell_cb, 0, 1) + grid1.addWidget(dwelltime, 1, 0) + grid1.addWidget(self.dwelltime_entry, 1, 1) + # GO Button self.export_gcode_button = QtGui.QPushButton('Export G-Code') self.export_gcode_button.setToolTip( diff --git a/camlib.py b/camlib.py index d66a71f..9b4f02f 100644 --- a/camlib.py +++ b/camlib.py @@ -2720,7 +2720,8 @@ class CNCjob(Geometry): self.feedrate = feedrate self.tooldia = tooldia self.unitcode = {"IN": "G20", "MM": "G21"} - self.pausecode = "G04 P1" + # TODO: G04 Does not exist. It's G4 and now we are handling in postprocessing. + #self.pausecode = "G04 P1" self.feedminutecode = "G94" self.absolutecode = "G90" self.gcode = "" @@ -2818,7 +2819,7 @@ class CNCjob(Geometry): else: gcode += "M03\n" # Spindle start - gcode += self.pausecode + "\n" + #gcode += self.pausecode + "\n" for tool in tools: @@ -2915,7 +2916,7 @@ class CNCjob(Geometry): self.gcode += "M03 S%d\n" % int(self.spindlespeed) # Spindle start with configured speed else: self.gcode += "M03\n" # Spindle start - self.gcode += self.pausecode + "\n" + #self.gcode += self.pausecode + "\n" ## Iterate over geometry paths getting the nearest each time. log.debug("Starting G-Code...") @@ -3031,6 +3032,8 @@ class CNCjob(Geometry): :param gtext: A single string with g-code """ + log.debug("pre_parse()") + # Units: G20-inches, G21-mm units_re = re.compile(r'^G2([01])') @@ -3097,7 +3100,8 @@ class CNCjob(Geometry): # TODO: Merge into single parser? gobjs = self.pre_parse(self.gcode) - + + log.debug("gcode_parse(): pre_parse() done.") # Last known instruction current = {'X': 0.0, 'Y': 0.0, 'Z': 0.0, 'G': 0} From d1442a49008d1a3a7030b6b32898f11e7795b09b Mon Sep 17 00:00:00 2001 From: Juan Pablo Caram Date: Sat, 4 Jun 2016 16:45:52 -0400 Subject: [PATCH 121/134] Cleaned up G-code parser. Fixed dwell command. Fixes #184. --- FlatCAMObj.py | 10 +++++- camlib.py | 84 ++++++++++++++------------------------------------- 2 files changed, 32 insertions(+), 62 deletions(-) diff --git a/FlatCAMObj.py b/FlatCAMObj.py index 199c63f..a3d7610 100644 --- a/FlatCAMObj.py +++ b/FlatCAMObj.py @@ -1038,6 +1038,11 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob): self.export_gcode(filename, preamble=preamble, postamble=postamble) def dwell_generator(self, lines): + """ + Inserts "G4 P..." instructions after spindle-start + instructions (M03 or M04). + + """ log.debug("dwell_generator()...") @@ -1060,7 +1065,7 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob): # If start spindle, buffer a G4. if m3m4re.search(line): log.debug("Found M03/4") - bufline = "G4 {}\n".format(self.options['dwelltime']) + bufline = "G4 P{}\n".format(self.options['dwelltime']) yield line @@ -1070,10 +1075,13 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob): lines = StringIO(self.gcode) + ## Post processing + # Dwell? if self.options['dwell']: log.debug("Will add G04!") lines = self.dwell_generator(lines) + ## Write with open(filename, 'w') as f: f.write(preamble + "\n") diff --git a/camlib.py b/camlib.py index 9b4f02f..d29367d 100644 --- a/camlib.py +++ b/camlib.py @@ -9,6 +9,7 @@ #from scipy import optimize #import traceback +from cStringIO import StringIO from numpy import arctan2, Inf, array, sqrt, pi, ceil, sin, cos, dot, float32, \ transpose from numpy.linalg import solve, norm @@ -3024,67 +3025,25 @@ class CNCjob(Geometry): self.gcode += "G00 X0Y0\n" self.gcode += "M05\n" # Spindle stop - def pre_parse(self, gtext): + @staticmethod + def codes_split(gline): """ - Separates parts of the G-Code text into a list of dictionaries. - Used by ``self.gcode_parse()``. + Parses a line of G-Code such as "G01 X1234 Y987" into + a dictionary: {'G': 1.0, 'X': 1234.0, 'Y': 987.0} - :param gtext: A single string with g-code + :param gline: G-Code line string + :return: Dictionary with parsed line. """ - log.debug("pre_parse()") + command = {} - # Units: G20-inches, G21-mm - units_re = re.compile(r'^G2([01])') + match = re.search(r'^\s*([A-Z])\s*([\+\-\.\d\s]+)', gline) + while match: + command[match.group(1)] = float(match.group(2).replace(" ", "")) + gline = gline[match.end():] + match = re.search(r'^\s*([A-Z])\s*([\+\-\.\d\s]+)', gline) - # TODO: This has to be re-done - gcmds = [] - lines = gtext.split("\n") # TODO: This is probably a lot of work! - for line in lines: - # Clean up - line = line.strip() - - # Remove comments - # NOTE: Limited to 1 bracket pair - op = line.find("(") - cl = line.find(")") - #if op > -1 and cl > op: - if cl > op > -1: - #comment = line[op+1:cl] - line = line[:op] + line[(cl+1):] - - # Units - match = units_re.match(line) - if match: - self.units = {'0': "IN", '1': "MM"}[match.group(1)] - - # Parse GCode - # 0 4 12 - # G01 X-0.007 Y-0.057 - # --> codes_idx = [0, 4, 12] - codes = "NMGXYZIJFPST" - codes_idx = [] - i = 0 - for ch in line: - if ch in codes: - codes_idx.append(i) - i += 1 - n_codes = len(codes_idx) - if n_codes == 0: - continue - - # Separate codes in line - parts = [] - for p in range(n_codes - 1): - parts.append(line[codes_idx[p]:codes_idx[p+1]].strip()) - parts.append(line[codes_idx[-1]:].strip()) - - # Separate codes from values - cmds = {} - for part in parts: - cmds[part[0]] = float(part[1:]) - gcmds.append(cmds) - return gcmds + return command def gcode_parse(self): """ @@ -3097,11 +3056,7 @@ class CNCjob(Geometry): # Results go here geometry = [] - - # TODO: Merge into single parser? - gobjs = self.pre_parse(self.gcode) - log.debug("gcode_parse(): pre_parse() done.") # Last known instruction current = {'X': 0.0, 'Y': 0.0, 'Z': 0.0, 'G': 0} @@ -3110,7 +3065,14 @@ class CNCjob(Geometry): path = [(0, 0)] # Process every instruction - for gobj in gobjs: + for line in StringIO(self.gcode): + + gobj = self.codes_split(line) + + ## Units + if 'G' in gobj and (gobj['G'] == 20.0 or gobj['G'] == 21.0): + self.units = {20.0: "IN", 21.0: "MM"}[gobj['G']] + continue ## Changing height if 'Z' in gobj: @@ -3154,7 +3116,7 @@ class CNCjob(Geometry): center = [gobj['I'] + current['X'], gobj['J'] + current['Y']] radius = sqrt(gobj['I']**2 + gobj['J']**2) start = arctan2(-gobj['J'], -gobj['I']) - stop = arctan2(-center[1]+y, -center[0]+x) + stop = arctan2(-center[1] + y, -center[0] + x) path += arc(center, radius, start, stop, arcdir[current['G']], self.steps_per_circ) From eb18b7fd3f8b92d7b4daf5aff4b97900f945ad1c Mon Sep 17 00:00:00 2001 From: Juan Pablo Caram Date: Sat, 4 Jun 2016 17:54:07 -0400 Subject: [PATCH 122/134] Fixes #157. --- camlib.py | 89 +++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 70 insertions(+), 19 deletions(-) diff --git a/camlib.py b/camlib.py index d29367d..29c26e6 100644 --- a/camlib.py +++ b/camlib.py @@ -1773,7 +1773,10 @@ class Gerber (Geometry): ## --- BUFFERED --- if making_region: - geo = Polygon(path) + if follow: + geo = Polygon() + else: + geo = Polygon(path) else: if last_path_aperture is None: log.warning("No aperture defined for curent path. (%d)" % line_num) @@ -1783,7 +1786,9 @@ class Gerber (Geometry): geo = LineString(path) else: geo = LineString(path).buffer(width / 2) - if not geo.is_empty: poly_buffer.append(geo) + + if not geo.is_empty: + poly_buffer.append(geo) path = [[current_x, current_y]] # Start new path @@ -1795,17 +1800,26 @@ class Gerber (Geometry): if len(path) > 1: # --- Buffered ---- width = self.apertures[last_path_aperture]["size"] - geo = LineString(path).buffer(width / 2) - if not geo.is_empty: poly_buffer.append(geo) + + if follow: + geo = LineString(path) + else: + geo = LineString(path).buffer(width / 2) + + if not geo.is_empty: + poly_buffer.append(geo) # Reset path starting point path = [[current_x, current_y]] # --- BUFFERED --- # Draw the flash + if follow: + continue flash = Gerber.create_flash_geometry(Point([current_x, current_y]), self.apertures[current_aperture]) - if not flash.is_empty: poly_buffer.append(flash) + if not flash.is_empty: + poly_buffer.append(flash) continue @@ -1858,8 +1872,13 @@ class Gerber (Geometry): # --- BUFFERED --- width = self.apertures[last_path_aperture]["size"] - buffered = LineString(path).buffer(width / 2) - if not buffered.is_empty: poly_buffer.append(buffered) + + if follow: + buffered = LineString(path) + else: + buffered = LineString(path).buffer(width / 2) + if not buffered.is_empty: + poly_buffer.append(buffered) current_x = x current_y = y @@ -1972,9 +1991,12 @@ class Gerber (Geometry): log.debug("Bare op-code %d." % current_operation_code) # flash = Gerber.create_flash_geometry(Point(path[-1]), # self.apertures[current_aperture]) + if follow: + continue flash = Gerber.create_flash_geometry(Point(current_x, current_y), self.apertures[current_aperture]) - if not flash.is_empty: poly_buffer.append(flash) + if not flash.is_empty: + poly_buffer.append(flash) except IndexError: log.warning("Line %d: %s -> Nothing there to flash!" % (line_num, gline)) @@ -1996,8 +2018,13 @@ class Gerber (Geometry): ## --- Buffered --- width = self.apertures[last_path_aperture]["size"] - geo = LineString(path).buffer(width/2) - if not geo.is_empty: poly_buffer.append(geo) + + if follow: + geo = LineString(path) + else: + geo = LineString(path).buffer(width/2) + if not geo.is_empty: + poly_buffer.append(geo) path = [path[-1]] @@ -2024,10 +2051,15 @@ class Gerber (Geometry): # "aperture": last_path_aperture}) # --- Buffered --- - region = Polygon(path) + if follow: + region = Polygon() + else: + region = Polygon(path) if not region.is_valid: - region = region.buffer(0) - if not region.is_empty: poly_buffer.append(region) + if not follow: + region = region.buffer(0) + if not region.is_empty: + poly_buffer.append(region) path = [[current_x, current_y]] # Start new path continue @@ -2060,8 +2092,14 @@ class Gerber (Geometry): if len(path) > 1: # --- Buffered ---- width = self.apertures[last_path_aperture]["size"] - geo = LineString(path).buffer(width / 2) - if not geo.is_empty: poly_buffer.append(geo) + + if follow: + geo = LineString(path) + else: + geo = LineString(path).buffer(width / 2) + if not geo.is_empty: + poly_buffer.append(geo) + path = [path[-1]] continue @@ -2076,8 +2114,13 @@ class Gerber (Geometry): # --- Buffered ---- width = self.apertures[last_path_aperture]["size"] - geo = LineString(path).buffer(width / 2) - if not geo.is_empty: poly_buffer.append(geo) + + if follow: + geo = LineString(path) + else: + geo = LineString(path).buffer(width / 2) + if not geo.is_empty: + poly_buffer.append(geo) path = [path[-1]] @@ -2148,10 +2191,18 @@ class Gerber (Geometry): ## --- Buffered --- width = self.apertures[last_path_aperture]["size"] - geo = LineString(path).buffer(width / 2) - if not geo.is_empty: poly_buffer.append(geo) + if follow: + geo = LineString(path) + else: + geo = LineString(path).buffer(width / 2) + if not geo.is_empty: + poly_buffer.append(geo) # --- Apply buffer --- + if follow: + self.solid_geometry = poly_buffer + return + log.warn("Joining %d polygons." % len(poly_buffer)) if self.use_buffer_for_union: log.debug("Union by buffer...") From 9f138bdcc2049fa9d47dc6b85f7ec9c882bf6652 Mon Sep 17 00:00:00 2001 From: Juan Pablo Caram Date: Sat, 4 Jun 2016 23:01:36 -0400 Subject: [PATCH 123/134] Fixes #119. --- DblSidedTool.py | 6 ++-- camlib.py | 79 +++++++++++++++++++++++++++++++------------------ 2 files changed, 54 insertions(+), 31 deletions(-) diff --git a/DblSidedTool.py b/DblSidedTool.py index d9356b5..c9cdbd9 100644 --- a/DblSidedTool.py +++ b/DblSidedTool.py @@ -162,8 +162,10 @@ class DblSidedTool(FlatCAMTool): # For now, lets limit to Gerbers and Excellons. # assert isinstance(gerb, FlatCAMGerber) - if not isinstance(fcobj, FlatCAMGerber) and not isinstance(fcobj, FlatCAMExcellon): - self.info("ERROR: Only Gerber and Excellon objects can be mirrored.") + if not isinstance(fcobj, FlatCAMGerber) and \ + not isinstance(fcobj, FlatCAMExcellon) and \ + not isinstance(fcobj, FlatCAMGeometry): + self.info("ERROR: Only Gerber, Excellon and Geometry objects can be mirrored.") return axis = self.mirror_axis.get_value() diff --git a/camlib.py b/camlib.py index 29c26e6..89d99af 100644 --- a/camlib.py +++ b/camlib.py @@ -912,6 +912,27 @@ class Geometry(object): svg_elem = geom.svg(scale_factor=scale_factor) return svg_elem + def mirror(self, axis, point): + """ + Mirrors the object around a specified axis passign through + the given point. + + :param axis: "X" or "Y" indicates around which axis to mirror. + :type axis: str + :param point: [x, y] point belonging to the mirror axis. + :type point: list + :return: None + """ + + px, py = point + xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis] + + ## solid_geometry ??? + # It's a cascaded union of objects. + self.solid_geometry = affinity.scale(self.solid_geometry, + xscale, yscale, origin=(px, py)) + + class ApertureMacro: """ Syntax of aperture macros. @@ -1496,35 +1517,35 @@ class Gerber (Geometry): ## Solid geometry self.solid_geometry = affinity.translate(self.solid_geometry, xoff=dx, yoff=dy) - def mirror(self, axis, point): - """ - Mirrors the object around a specified axis passign through - the given point. What is affected: - - * ``buffered_paths`` - * ``flash_geometry`` - * ``solid_geometry`` - * ``regions`` - - NOTE: - Does not modify the data used to create these elements. If these - are recreated, the scaling will be lost. This behavior was modified - because of the complexity reached in this class. - - :param axis: "X" or "Y" indicates around which axis to mirror. - :type axis: str - :param point: [x, y] point belonging to the mirror axis. - :type point: list - :return: None - """ - - px, py = point - xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis] - - ## solid_geometry ??? - # It's a cascaded union of objects. - self.solid_geometry = affinity.scale(self.solid_geometry, - xscale, yscale, origin=(px, py)) + # def mirror(self, axis, point): + # """ + # Mirrors the object around a specified axis passign through + # the given point. What is affected: + # + # * ``buffered_paths`` + # * ``flash_geometry`` + # * ``solid_geometry`` + # * ``regions`` + # + # NOTE: + # Does not modify the data used to create these elements. If these + # are recreated, the scaling will be lost. This behavior was modified + # because of the complexity reached in this class. + # + # :param axis: "X" or "Y" indicates around which axis to mirror. + # :type axis: str + # :param point: [x, y] point belonging to the mirror axis. + # :type point: list + # :return: None + # """ + # + # px, py = point + # xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis] + # + # ## solid_geometry ??? + # # It's a cascaded union of objects. + # self.solid_geometry = affinity.scale(self.solid_geometry, + # xscale, yscale, origin=(px, py)) def aperture_parse(self, apertureId, apertureType, apParameters): """ From ee8e9f8f4b7dcced3b0ca2f7ed6461276c2d42ee Mon Sep 17 00:00:00 2001 From: Juan Pablo Caram Date: Sat, 4 Jun 2016 23:04:22 -0400 Subject: [PATCH 124/134] Support for mirroring Geometry Objects from the shell. See #119. --- FlatCAMApp.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 028fbfd..346cd80 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -2631,8 +2631,10 @@ class App(QtCore.QObject): if obj is None: return "Object not found: %s" % name - if not isinstance(obj, FlatCAMGerber) and not isinstance(obj, FlatCAMExcellon): - return "ERROR: Only Gerber and Excellon objects can be mirrored." + if not isinstance(obj, FlatCAMGerber) and \ + not isinstance(obj, FlatCAMExcellon) and \ + not isinstance(obj, FlatCAMGeometry): + return "ERROR: Only Gerber, Excellon and Geometry objects can be mirrored." # Axis try: @@ -2692,7 +2694,6 @@ class App(QtCore.QObject): return 'Unknown parameter: %s' % key kwa[key] = types[key](kwa[key]) - if 'columns' not in kwa or 'rows' not in kwa: return "ERROR: Specify -columns and -rows" From 856d126546ea5c1ca2bc81120c27edddecc5404b Mon Sep 17 00:00:00 2001 From: "Zheng, Lei" Date: Thu, 9 Jun 2016 15:55:17 +0800 Subject: [PATCH 125/134] Added indent to json in save_project This is to make the saved project file more version control frendly --- FlatCAMApp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 346cd80..dafc544 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -4124,7 +4124,7 @@ class App(QtCore.QObject): return # Write - json.dump(d, f, default=to_dict) + json.dump(d, f, default=to_dict, indent=2, sort_keys=True) # try: # json.dump(d, f, default=to_dict) # except Exception, e: From c5f4b9474a257344971206f7f5959252b382c7ed Mon Sep 17 00:00:00 2001 From: "Zheng, Lei" Date: Sat, 11 Jun 2016 06:07:28 +0800 Subject: [PATCH 126/134] Toggle plot by pressing SPACE key in project panel --- GUIElements.py | 3 +++ ObjectCollection.py | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/GUIElements.py b/GUIElements.py index ba240ad..3d9ca18 100644 --- a/GUIElements.py +++ b/GUIElements.py @@ -199,6 +199,9 @@ class FCCheckBox(QtGui.QCheckBox): def set_value(self, val): self.setChecked(val) + def toggle(self): + self.set_value(not self.get_value()) + class FCTextArea(QtGui.QPlainTextEdit): def __init__(self, parent=None): diff --git a/ObjectCollection.py b/ObjectCollection.py index 727358d..e423e5d 100644 --- a/ObjectCollection.py +++ b/ObjectCollection.py @@ -82,6 +82,11 @@ class ObjectCollection(QtCore.QAbstractListModel): # Delete via the application to # ensure cleanup of the GUI self.get_active().app.on_delete() + return + + if key == QtCore.Qt.Key_Space: + self.get_active().ui.plot_cb.toggle() + return def on_mouse_down(self, event): FlatCAMApp.App.log.debug("Mouse button pressed on list") From 66901041d943c68b6c4a757f451a60d08ca6d5ee Mon Sep 17 00:00:00 2001 From: Juan Pablo Caram Date: Sat, 11 Jun 2016 19:50:19 -0400 Subject: [PATCH 127/134] Fixed errors that I introduced in last commit. --- tclCommands/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tclCommands/__init__.py b/tclCommands/__init__.py index f02b02d..47e65b4 100644 --- a/tclCommands/__init__.py +++ b/tclCommands/__init__.py @@ -26,11 +26,11 @@ def register_all_commands(app, commands): """ Static method which register all known commands. - Command should be for now in directory test_tclCommands and module should start with TCLCommand + Command should be for now in directory tclCommands and module should start with TCLCommand Class have to follow same name as module. we need import all modules in top section: - import test_tclCommands.TclCommandExteriors + import tclCommands.TclCommandExteriors at this stage we can include only wanted commands with this, auto loading may be implemented in future I have no enough knowledge about python's anatomy. Would be nice to include all classes which are descendant etc. @@ -39,10 +39,10 @@ def register_all_commands(app, commands): :return: None """ - tcl_modules = {k: v for k, v in sys.modules.items() if k.startswith('test_tclCommands.TclCommand')} + tcl_modules = {k: v for k, v in sys.modules.items() if k.startswith('tclCommands.TclCommand')} for key, mod in tcl_modules.items(): - if key != 'test_tclCommands.TclCommand': + if key != 'tclCommands.TclCommand': class_name = key.split('.')[1] class_type = getattr(mod, class_name) command_instance = class_type(app) From f9cbd78cd152b5c932f9c4fe5c8094f932f5c360 Mon Sep 17 00:00:00 2001 From: Juan Pablo Caram Date: Sat, 11 Jun 2016 21:33:38 -0400 Subject: [PATCH 128/134] Show messages and errors in TCL shell. Better exception handling and reporting when opening files. --- FlatCAMApp.py | 97 +++++++++++++++++++++++++++++++++++++++------------ camlib.py | 19 +++++----- 2 files changed, 85 insertions(+), 31 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index dafc544..fcdfc56 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -691,16 +691,35 @@ class App(QtCore.QObject): else: self.defaults['stats'][resource] = 1 + # TODO: This shouldn't be here. class TclErrorException(Exception): """ this exception is deffined here, to be able catch it if we sucessfully handle all errors from shell command """ pass + def shell_message(self, msg, show=False, error=False): + """ + Shows a message on the FlatCAM Shell + + :param msg: Message to display. + :param show: Opens the shell. + :param error: Shows the message as an error. + :return: None + """ + if show: + self.ui.shell_dock.show() + + if error: + self.shell.append_error(msg + "\n") + else: + self.shell.append_output(msg + "\n") + def raise_tcl_unknown_error(self, unknownException): """ - raise Exception if is different type than TclErrorException - this is here mainly to show unknown errors inside TCL shell console + Raise exception if is different type than TclErrorException + this is here mainly to show unknown errors inside TCL shell console. + :param unknownException: :return: """ @@ -727,20 +746,20 @@ class App(QtCore.QObject): show_trace = int(self.defaults['verbose_error_level']) if show_trace > 0: - trc=traceback.format_list(traceback.extract_tb(exc_traceback)) - trc_formated=[] + trc = traceback.format_list(traceback.extract_tb(exc_traceback)) + trc_formated = [] for a in reversed(trc): - trc_formated.append(a.replace(" ", " > ").replace("\n","")) - text="%s\nPython traceback: %s\n%s" % (exc_value, + trc_formated.append(a.replace(" ", " > ").replace("\n", "")) + text = "%s\nPython traceback: %s\n%s" % (exc_value, exc_type, "\n".join(trc_formated)) else: - text="%s" % error + text = "%s" % error else: - text=error + text = error - text = text.replace('[', '\\[').replace('"','\\"') + text = text.replace('[', '\\[').replace('"', '\\"') self.tcl.eval('return -code error "%s"' % text) @@ -782,7 +801,7 @@ class App(QtCore.QObject): try: self.shell.open_proccessing() result = self.tcl.eval(str(text)) - if result!='None': + if result != 'None': self.shell.append_output(result + '\n') except Tkinter.TclError, e: #this will display more precise answer if something in TCL shell fail @@ -836,16 +855,27 @@ class App(QtCore.QObject): def info(self, msg): """ - Writes on the status bar. + Informs the user. Normally on the status bar, optionally + also on the shell. :param msg: Text to write. + :param toshell: Forward the :return: None """ + + # Type of message in brackets at the begining of the message. match = re.search("\[([^\]]+)\](.*)", msg) if match: - self.ui.fcinfo.set_status(QtCore.QString(match.group(2)), level=match.group(1)) + level = match.group(1) + msg_ = match.group(2) + self.ui.fcinfo.set_status(QtCore.QString(msg_), level=level) + + error = level == "error" or level == "warning" + self.shell_message(msg, error=error, show=True) + else: self.ui.fcinfo.set_status(QtCore.QString(msg), level="info") + self.shell_message(msg) def load_defaults(self): """ @@ -1953,6 +1983,17 @@ class App(QtCore.QObject): self.log.error(str(e)) raise + except: + msg = "[error] An internal error has ocurred. See shell.\n" + msg += traceback.format_exc() + app_obj.inform.emit(msg) + raise + + if gerber_obj.is_empty(): + app_obj.inform.emit("[error] No geometry found in file: " + filename) + self.collection.set_active(gerber_obj.options["name"]) + self.collection.delete_active() + # Further parsing self.progress.emit(70) # TODO: Note the mixture of self and app_obj used here @@ -1997,18 +2038,31 @@ class App(QtCore.QObject): try: excellon_obj.parse_file(filename) + except IOError: app_obj.inform.emit("[error] Cannot open file: " + filename) self.progress.emit(0) # TODO: self and app_bjj mixed raise IOError("Cannot open file: " + filename) + except: + msg = "[error] An internal error has ocurred. See shell.\n" + msg += traceback.format_exc() + app_obj.inform.emit(msg) + raise + try: excellon_obj.create_geometry() - except Exception as e: - app_obj.inform.emit("[error] Failed to create geometry after parsing: " + filename) - self.progress.emit(0) - raise e + except: + msg = "[error] An internal error has ocurred. See shell.\n" + msg += traceback.format_exc() + app_obj.inform.emit(msg) + raise + + if excellon_obj.is_empty(): + app_obj.inform.emit("[error] No geometry found in file: " + filename) + self.collection.set_active(excellon_obj.options["name"]) + self.collection.delete_active() #self.progress.emit(70) with self.proc_container.new("Opening Excellon."): @@ -2479,8 +2533,8 @@ class App(QtCore.QObject): return "Could not retrieve object: %s" % name def geo_init_me(geo_obj, app_obj): - margin = kwa['margin']+kwa['dia']/2 - gap_size = kwa['dia']+kwa['gapsize'] + margin = kwa['margin'] + kwa['dia'] / 2 + gap_size = kwa['dia'] + kwa['gapsize'] minx, miny, maxx, maxy = obj.bounds() minx -= margin maxx += margin @@ -2519,9 +2573,8 @@ class App(QtCore.QObject): return 'Ok' - def geocutout(name=None, *args): - ''' + """ TCL shell command - see help section Subtract gaps from geometry, this will not create new object @@ -2529,7 +2582,7 @@ class App(QtCore.QObject): :param name: name of object :param args: array of arguments :return: "Ok" if completed without errors - ''' + """ try: a, kwa = h(*args) @@ -2569,7 +2622,7 @@ class App(QtCore.QObject): lenghty = (ymax - ymin) gapsize = kwa['gapsize'] + kwa['dia'] / 2 - if kwa['gaps'] == '8' or kwa['gaps']=='2lr': + if kwa['gaps'] == '8' or kwa['gaps'] == '2lr': subtract_rectangle(name, xmin - gapsize, diff --git a/camlib.py b/camlib.py index 89d99af..2e7a82d 100644 --- a/camlib.py +++ b/camlib.py @@ -158,6 +158,16 @@ class Geometry(object): log.error("Failed to run union on polylines.") raise + def is_empty(self): + + if isinstance(self.solid_geometry, BaseGeometry): + return self.solid_geometry.is_empty + + if isinstance(self.solid_geometry, list): + return len(self.solid_geometry) == 0 + + raise Exception("self.solid_geometry is neither BaseGeometry or list.") + def subtract_polygon(self, points): """ Subtract polygon from the given object. This only operates on the paths in the original geometry, i.e. it converts polygons into paths. @@ -390,15 +400,6 @@ class Geometry(object): """ return self.solid_geometry.buffer(offset) - def is_empty(self): - if self.solid_geometry is None: - return True - - if type(self.solid_geometry) is list and len(self.solid_geometry) == 0: - return True - - return False - def import_svg(self, filename, flip=True): """ Imports shapes from an SVG file into the object's geometry. From 28bb476a5ca310ce069f972678445be56dbae3a3 Mon Sep 17 00:00:00 2001 From: Juan Pablo Caram Date: Sat, 11 Jun 2016 21:55:53 -0400 Subject: [PATCH 129/134] Fixes #202 --- FlatCAMApp.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index fcdfc56..62757ef 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -1875,13 +1875,11 @@ class App(QtCore.QObject): else: self.inform.emit("Project copy saved to: " + self.project_filename) - def export_svg(self, obj_name, filename, scale_factor=0.00): """ - Exports a Geometry Object to a SVG File + Exports a Geometry Object to an SVG file. :param filename: Path to the SVG file to save to. - :param outname: :return: """ @@ -1890,6 +1888,7 @@ class App(QtCore.QObject): try: obj = self.collection.get_by_name(str(obj_name)) except: + # TODO: The return behavior has not been established... should raise exception? return "Could not retrieve object: %s" % obj_name with self.proc_container.new("Exporting SVG") as proc: @@ -1907,8 +1906,10 @@ class App(QtCore.QObject): uom = obj.units.lower() # Add a SVG Header and footer to the svg output from shapely - # The transform flips the Y Axis so that everything renders properly within svg apps such as inkscape - svg_header = '' @@ -1916,7 +1917,8 @@ class App(QtCore.QObject): svg_footer = ' ' svg_elem = svg_header + exported_svg + svg_footer - # Parse the xml through a xml parser just to add line feeds and to make it look more pretty for the output + # Parse the xml through a xml parser just to add line feeds + # and to make it look more pretty for the output doc = parse_xml_string(svg_elem) with open(filename, 'w') as fp: fp.write(doc.toprettyxml()) @@ -2038,7 +2040,7 @@ class App(QtCore.QObject): try: excellon_obj.parse_file(filename) - + except IOError: app_obj.inform.emit("[error] Cannot open file: " + filename) self.progress.emit(0) # TODO: self and app_bjj mixed @@ -2994,7 +2996,6 @@ class App(QtCore.QObject): except Exception as unknown: self.raise_tcl_unknown_error(unknown) - def millholes(name=None, *args): ''' TCL shell command - see help section @@ -3035,10 +3036,13 @@ class App(QtCore.QObject): self.raise_tcl_error("Object not found: %s" % name) if not isinstance(obj, FlatCAMExcellon): - self.raise_tcl_error('Only Excellon objects can be mill drilled, got %s %s.' % (name, type(obj))) + self.raise_tcl_error('Only Excellon objects can be mill-drilled, got %s %s.' % (name, type(obj))) try: - success, msg = obj.generate_milling(**kwa) + # This runs in the background: Block until done. + with wait_signal(self.new_object_available): + success, msg = obj.generate_milling(**kwa) + except Exception as e: self.raise_tcl_error("Operation failed: %s" % str(e)) From 01c2feca98539c8545c93df1e1911f5e3c27a035 Mon Sep 17 00:00:00 2001 From: Juan Pablo Caram Date: Sat, 2 Jul 2016 16:47:15 -0400 Subject: [PATCH 130/134] Added (passing) test for Excellon flow. --- tests/excellon_files/case1.drl | 125 +++++++++++++++++++++++++ tests/test_excellon_flow.py | 163 +++++++++++++++++++++++++++++++++ tests/test_gerber_flow.py | 13 ++- 3 files changed, 299 insertions(+), 2 deletions(-) create mode 100644 tests/excellon_files/case1.drl create mode 100644 tests/test_excellon_flow.py diff --git a/tests/excellon_files/case1.drl b/tests/excellon_files/case1.drl new file mode 100644 index 0000000..95b89ca --- /dev/null +++ b/tests/excellon_files/case1.drl @@ -0,0 +1,125 @@ +M48 +INCH +T01C0.0200 +T02C0.0800 +T03C0.0600 +T04C0.0300 +T05C0.0650 +T06C0.0450 +T07C0.0400 +T08C0.1181 +T09C0.0500 +% +T01 +X-018204Y+015551 +X-025842Y+015551 +T02 +X-000118Y+020629 +X-000118Y+016889 +X+012401Y+020629 +X+012401Y+016889 +X-010170Y+002440 +X-010110Y+011470 +X+018503Y+026574 +T03 +X+013060Y+010438 +X+013110Y+000000 +X-049015Y+002165 +X+018378Y+010433 +X+018317Y+000000 +X-049015Y+010039 +X-041141Y-000629 +X-041181Y+012992 +X-056496Y+012992 +X-056496Y-000590 +T04 +X-037560Y+030490 +X-036560Y+030490 +X-035560Y+030490 +X-034560Y+030490 +X-033560Y+030490 +X-032560Y+030490 +X-031560Y+030490 +X-030560Y+030490 +X-029560Y+030490 +X-028560Y+030490 +X-027560Y+030490 +X-026560Y+030490 +X-025560Y+030490 +X-024560Y+030490 +X-024560Y+036490 +X-025560Y+036490 +X-026560Y+036490 +X-027560Y+036490 +X-028560Y+036490 +X-029560Y+036490 +X-030560Y+036490 +X-031560Y+036490 +X-032560Y+036490 +X-033560Y+036490 +X-034560Y+036490 +X-035560Y+036490 +X-036560Y+036490 +X-037560Y+036490 +X-014590Y+030810 +X-013590Y+030810 +X-012590Y+030810 +X-011590Y+030810 +X-011590Y+033810 +X-012590Y+033810 +X-013590Y+033810 +X-014590Y+033810 +X-021260Y+034680 +X-020010Y+034680 +X-008390Y+035840 +X-008390Y+034590 +X-008440Y+031870 +X-008440Y+030620 +T05 +X-022504Y+019291 +X-020354Y+019291 +X-018204Y+019291 +X-030142Y+019291 +X-027992Y+019291 +X-025842Y+019291 +X-012779Y+019291 +X-010629Y+019291 +X-008479Y+019291 +T06 +X-028080Y+028230 +X-030080Y+028230 +X-034616Y+024409 +X-039616Y+024409 +X-045364Y+023346 +X-045364Y+018346 +X-045364Y+030157 +X-045364Y+025157 +X-008604Y+026983 +X-013604Y+026983 +X-016844Y+034107 +X-016844Y+029107 +T07 +X-041655Y+026456 +X-040655Y+026456 +X-039655Y+026456 +X-041640Y+022047 +X-040640Y+022047 +X-039640Y+022047 +X-049760Y+029430 +X-048760Y+029430 +X-047760Y+029430 +X-019220Y+037380 +X-020220Y+037380 +X-021220Y+037380 +T08 +X-024212Y+007751 +X-024212Y+004011 +X-035629Y+007874 +X-035629Y+004133 +T09 +X+007086Y+030708 +X+007086Y+032874 +X-000787Y+031889 +X-000787Y+035826 +X-000787Y+027952 +M30 diff --git a/tests/test_excellon_flow.py b/tests/test_excellon_flow.py new file mode 100644 index 0000000..234dff9 --- /dev/null +++ b/tests/test_excellon_flow.py @@ -0,0 +1,163 @@ +import unittest +from PyQt4 import QtGui +import sys +from FlatCAMApp import App +from FlatCAMObj import FlatCAMExcellon, FlatCAMCNCjob +from ObjectUI import ExcellonObjectUI +import tempfile +import os +from time import sleep + + +class ExcellonFlowTestCase(unittest.TestCase): + """ + This is a top-level test covering the Excellon-to-GCode + generation workflow. + + THIS IS A REQUIRED TEST FOR ANY UPDATES. + + """ + + filename = 'case1.drl' + + def setUp(self): + self.app = QtGui.QApplication(sys.argv) + + # Create App, keep app defaults (do not load + # user-defined defaults). + self.fc = App(user_defaults=False) + + self.fc.open_excellon('tests/excellon_files/' + self.filename) + + def tearDown(self): + del self.fc + del self.app + + def test_flow(self): + # Names of available objects. + names = self.fc.collection.get_names() + print names + + #-------------------------------------- + # Total of 1 objects. + #-------------------------------------- + self.assertEquals(len(names), 1, + "Expected 1 object, found %d" % len(names)) + + #-------------------------------------- + # Object's name matches the file name. + #-------------------------------------- + self.assertEquals(names[0], self.filename, + "Expected name == %s, got %s" % (self.filename, names[0])) + + #--------------------------------------- + # Get object by that name, make sure it's a FlatCAMExcellon. + #--------------------------------------- + excellon_name = names[0] + excellon_obj = self.fc.collection.get_by_name(excellon_name) + self.assertTrue(isinstance(excellon_obj, FlatCAMExcellon), + "Expected FlatCAMExcellon, instead, %s is %s" % + (excellon_name, type(excellon_obj))) + + #---------------------------------------- + # Object's GUI matches Object's options + #---------------------------------------- + # TODO: Open GUI with double-click on object. + # Opens the Object's GUI, populates it. + excellon_obj.build_ui() + for option, value in excellon_obj.options.iteritems(): + try: + form_field = excellon_obj.form_fields[option] + except KeyError: + print ("**********************************************************\n" + "* WARNING: Option '{}' has no form field\n" + "**********************************************************" + "".format(option)) + continue + self.assertEqual(value, form_field.get_value(), + "Option '{}' == {} but form has {}".format( + option, value, form_field.get_value() + )) + + #-------------------------------------------------- + # Changes in the GUI should be read in when + # running any process. Changing something here. + #-------------------------------------------------- + + form_field = excellon_obj.form_fields['feedrate'] + value = form_field.get_value() + form_field.set_value(value * 1.1) # Increase by 10% + print "'feedrate' == {}".format(value) + + #-------------------------------------------------- + # Create GCode using all tools. + #-------------------------------------------------- + + assert isinstance(excellon_obj, FlatCAMExcellon) # Just for the IDE + ui = excellon_obj.ui + assert isinstance(ui, ExcellonObjectUI) + ui.tools_table.selectAll() # Select All + ui.generate_cnc_button.click() # Click + + # Work is done in a separate thread and results are + # passed via events to the main event loop which is + # not running. Run only for pending events. + # + # I'm not sure why, but running it only once does + # not catch the new object. Might be a timing issue. + # http://pyqt.sourceforge.net/Docs/PyQt4/qeventloop.html#details + for _ in range(2): + sleep(0.1) + self.app.processEvents() + + #--------------------------------------------- + # Check that GUI has been read in. + #--------------------------------------------- + + value = excellon_obj.options['feedrate'] + form_value = form_field.get_value() + self.assertEqual(value, form_value, + "Form value for '{}' == {} was not read into options" + "which has {}".format('feedrate', form_value, value)) + print "'feedrate' == {}".format(value) + + #--------------------------------------------- + # Check that only 1 object has been created. + #--------------------------------------------- + + names = self.fc.collection.get_names() + self.assertEqual(len(names), 2, + "Expected 2 objects, found %d" % len(names)) + + #------------------------------------------------------- + # Make sure the CNCJob Object has the correct name + #------------------------------------------------------- + + cncjob_name = excellon_name + "_cnc" + self.assertTrue(cncjob_name in names, + "Object named %s not found." % cncjob_name) + + #------------------------------------------------------- + # Get the object make sure it's a cncjob object + #------------------------------------------------------- + + cncjob_obj = self.fc.collection.get_by_name(cncjob_name) + self.assertTrue(isinstance(cncjob_obj, FlatCAMCNCjob), + "Expected a FlatCAMCNCjob, got %s" % type(cncjob_obj)) + + #----------------------------------------- + # Export G-Code, check output + #----------------------------------------- + assert isinstance(cncjob_obj, FlatCAMCNCjob) # For IDE + + # get system temporary file(try create it and delete) + with tempfile.NamedTemporaryFile(prefix='unittest.', + suffix="." + cncjob_name + '.gcode', + delete=True) as tmp_file: + output_filename = tmp_file.name + + cncjob_obj.export_gcode(output_filename) + self.assertTrue(os.path.isfile(output_filename)) + os.remove(output_filename) + + print names diff --git a/tests/test_gerber_flow.py b/tests/test_gerber_flow.py index 23ef1c1..a832150 100644 --- a/tests/test_gerber_flow.py +++ b/tests/test_gerber_flow.py @@ -10,6 +10,13 @@ import tempfile class GerberFlowTestCase(unittest.TestCase): + """ + This is a top-level test covering the Gerber-to-GCode + generation workflow. + + THIS IS A REQUIRED TEST FOR ANY UPDATES. + + """ filename = 'simple1.gbr' @@ -172,10 +179,12 @@ class GerberFlowTestCase(unittest.TestCase): assert isinstance(cnc_obj, FlatCAMCNCjob) output_filename = "" # get system temporary file(try create it and delete also) - with tempfile.NamedTemporaryFile(prefix='unittest.', suffix="." + cnc_name + '.gcode', delete=True) as tmp_file: + with tempfile.NamedTemporaryFile(prefix='unittest.', + suffix="." + cnc_name + '.gcode', + delete=True) as tmp_file: output_filename = tmp_file.name cnc_obj.export_gcode(output_filename) self.assertTrue(os.path.isfile(output_filename)) os.remove(output_filename) - print names \ No newline at end of file + print names From b4017cfec2ee67857ff8ddbe7c7d930cea57ec04 Mon Sep 17 00:00:00 2001 From: Juan Pablo Caram Date: Sat, 2 Jul 2016 17:36:19 -0400 Subject: [PATCH 131/134] Update instead of setting options when reading project. Fixes #204. --- FlatCAMObj.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/FlatCAMObj.py b/FlatCAMObj.py index a3d7610..8860bcb 100644 --- a/FlatCAMObj.py +++ b/FlatCAMObj.py @@ -49,6 +49,23 @@ class FlatCAMObj(QtCore.QObject): # self.ui.offset_button.clicked.connect(self.on_offset_button_click) # self.ui.scale_button.clicked.connect(self.on_scale_button_click) + def from_dict(self, d): + """ + This supersedes ``from_dict`` in derived classes. Derived classes + must inherit from FlatCAMObj first, then from derivatives of Geometry. + + ``self.options`` is only updated, not overwritten. This ensures that + options set by the app do not vanish when reading the objects + from a project file. + """ + + for attr in self.ser_attrs: + + if attr == 'options': + self.options.update(d[attr]) + else: + setattr(self, attr, d[attr]) + def on_options_change(self, key): self.emit(QtCore.SIGNAL("optionChanged"), key) From 8b10967a59a6de05f8377efa9f5e03245ef026aa Mon Sep 17 00:00:00 2001 From: Juan Pablo Caram Date: Mon, 4 Jul 2016 17:30:32 -0400 Subject: [PATCH 132/134] Added SVG-to-GCode flow test. --- tests/test_svg_flow.py | 129 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 tests/test_svg_flow.py diff --git a/tests/test_svg_flow.py b/tests/test_svg_flow.py new file mode 100644 index 0000000..c3b4322 --- /dev/null +++ b/tests/test_svg_flow.py @@ -0,0 +1,129 @@ +import sys +import unittest +from PyQt4 import QtGui +from FlatCAMApp import App +from FlatCAMObj import FlatCAMGeometry, FlatCAMCNCjob +from ObjectUI import GerberObjectUI, GeometryObjectUI +from time import sleep +import os +import tempfile + + +class SVGFlowTestCase(unittest.TestCase): + + def setUp(self): + self.app = QtGui.QApplication(sys.argv) + + # Create App, keep app defaults (do not load + # user-defined defaults). + self.fc = App(user_defaults=False) + + self.filename = 'drawing.svg' + + def tearDown(self): + del self.fc + del self.app + + def test_flow(self): + + self.fc.import_svg('tests/svg/' + self.filename) + + names = self.fc.collection.get_names() + print names + + #-------------------------------------- + # Total of 1 objects. + #-------------------------------------- + self.assertEquals(len(names), 1, + "Expected 1 object, found %d" % len(names)) + + #-------------------------------------- + # Object's name matches the file name. + #-------------------------------------- + self.assertEquals(names[0], self.filename, + "Expected name == %s, got %s" % (self.filename, names[0])) + + #--------------------------------------- + # Get object by that name, make sure it's a FlatCAMGerber. + #--------------------------------------- + geo_name = names[0] + geo_obj = self.fc.collection.get_by_name(geo_name) + self.assertTrue(isinstance(geo_obj, FlatCAMGeometry), + "Expected FlatCAMGeometry, instead, %s is %s" % + (geo_name, type(geo_obj))) + + #---------------------------------------- + # Object's GUI matches Object's options + #---------------------------------------- + # TODO: Open GUI with double-click on object. + # Opens the Object's GUI, populates it. + geo_obj.build_ui() + for option, value in geo_obj.options.iteritems(): + try: + form_field = geo_obj.form_fields[option] + except KeyError: + print ("**********************************************************\n" + "* WARNING: Option '{}' has no form field\n" + "**********************************************************" + "".format(option)) + continue + self.assertEqual(value, form_field.get_value(), + "Option '{}' == {} but form has {}".format( + option, value, form_field.get_value() + )) + + #------------------------------------ + # Open the UI, make CNCObject + #------------------------------------ + geo_obj.build_ui() + ui = geo_obj.ui + assert isinstance(ui, GeometryObjectUI) # Just for the IDE + ui.generate_cnc_button.click() # Click + + # Work is done in a separate thread and results are + # passed via events to the main event loop which is + # not running. Run only for pending events. + # + # I'm not sure why, but running it only once does + # not catch the new object. Might be a timing issue. + # http://pyqt.sourceforge.net/Docs/PyQt4/qeventloop.html#details + for _ in range(2): + sleep(0.1) + self.app.processEvents() + + #--------------------------------------------- + # Check that only 1 object has been created. + #--------------------------------------------- + names = self.fc.collection.get_names() + self.assertEqual(len(names), 2, + "Expected 2 objects, found %d" % len(names)) + + #------------------------------------------------------- + # Make sure the CNC Job Object has the correct name + #------------------------------------------------------- + cnc_name = geo_name + "_cnc" + self.assertTrue(cnc_name in names, + "Object named %s not found." % geo_name) + + #------------------------------------------------------- + # Get the object make sure it's a CNC Job object + #------------------------------------------------------- + cnc_obj = self.fc.collection.get_by_name(cnc_name) + self.assertTrue(isinstance(cnc_obj, FlatCAMCNCjob), + "Expected a FlatCAMCNCJob, got %s" % type(geo_obj)) + + #----------------------------------------- + # Export G-Code, check output + #----------------------------------------- + assert isinstance(cnc_obj, FlatCAMCNCjob) + output_filename = "" + # get system temporary file(try create it and delete also) + with tempfile.NamedTemporaryFile(prefix='unittest.', + suffix="." + cnc_name + '.gcode', + delete=True) as tmp_file: + output_filename = tmp_file.name + cnc_obj.export_gcode(output_filename) + self.assertTrue(os.path.isfile(output_filename)) + os.remove(output_filename) + + print names From 3660b4fe8122ef039c798cc2cf3a775704e2b01d Mon Sep 17 00:00:00 2001 From: jpcgt Date: Wed, 6 Jul 2016 00:35:02 +0000 Subject: [PATCH 133/134] README.md edited online with Bitbucket --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5b156d1..c150ae4 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ FlatCAM: 2D Computer-Aided PCB Manufacturing ============================================ -(c) 2014-2015 Juan Pablo Caram +(c) 2014-2016 Juan Pablo Caram FlatCAM is a program for preparing CNC jobs for making PCBs on a CNC router. Among other things, it can take a Gerber file generated by your favorite PCB -CAD program, and create G-Code for Isolation routing. +CAD program, and create G-Code for Isolation routing. \ No newline at end of file From 12ee70e06a8c2ce35941454af1f1678a42a42d49 Mon Sep 17 00:00:00 2001 From: Denvi Date: Mon, 11 Jul 2016 19:05:33 +0500 Subject: [PATCH 134/134] Merge remote master --- camlib.pyc | Bin 0 -> 83358 bytes descartes/__init__.pyc | Bin 284 -> 267 bytes descartes/patch.pyc | Bin 3192 -> 3039 bytes 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 camlib.pyc diff --git a/camlib.pyc b/camlib.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c42a4ee71933b50936c49e4f93845428235d31ad GIT binary patch literal 83358 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 P;Zv@4