diff --git a/CHANGELOG.md b/CHANGELOG.md index 9145df1a..9c867fbf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ CHANGELOG for FlatCAM beta ================================================= +3.01.2021 + +- fixed the Tcl Commands: new_geometry, new_gerber and new_excellon to create correct objects. New Geometry object created with the new_geometry Tcl command accept now the usage of add_circle/polygon/polyline/rectangle Tcl commands +- updated the autocompleter list for the Tcl commands with missing Tcl commands names +- added three new Tcl Commands: add_aperture (adds an aperture to a Gerber object), add_drill and add_slot (add a drill/slot to an Excellon object) + 2.01.2021 - removed the 'machinist setting' and allow all over the app the usages of both negative and positive values (where it is the case) diff --git a/appObjects/AppObject.py b/appObjects/AppObject.py index 56674984..a45e6529 100644 --- a/appObjects/AppObject.py +++ b/appObjects/AppObject.py @@ -395,14 +395,16 @@ class AppObject(QtCore.QObject): """ self.app.on_zoom_fit() - def new_excellon_object(self): + def new_excellon_object(self, new_name=None): """ Creates a new, blank Excellon object. - :return: None + :param new_name: new name for the new Geometry object + :type new_name: str + :return: None """ - outname = 'new_exc' + outname = 'new_exc' if new_name is None else new_name def obj_init(new_obj, app_obj): new_obj.tools = {} @@ -411,13 +413,16 @@ class AppObject(QtCore.QObject): self.new_object('excellon', outname, obj_init, plot=False) - def new_geometry_object(self): + def new_geometry_object(self, new_name=None): """ Creates a new, blank and single-tool Geometry object. - :return: None + :param new_name: new name for the new Geometry object + :type new_name: str + :return: None """ - outname = 'new_geo' + + outname = 'new_geo' if new_name is None else new_name def initialize(new_obj, app): new_obj.multitool = True @@ -450,13 +455,17 @@ class AppObject(QtCore.QObject): self.new_object('geometry', outname, initialize, plot=False) - def new_gerber_object(self): + def new_gerber_object(self, new_name=None): """ Creates a new, blank Gerber object. - :return: None + :param new_name: new name for the new Geometry object + :type new_name: str + :return: None """ + outname = 'new_geo' if new_name is None else new_name + def initialize(new_obj, app): new_obj.multitool = False new_obj.source_file = '' @@ -474,15 +483,19 @@ class AppObject(QtCore.QObject): except KeyError: pass - self.new_object('gerber', 'new_grb', initialize, plot=False) + self.new_object('gerber', outname, initialize, plot=False) - def new_script_object(self): + def new_script_object(self, new_name=None): """ Creates a new, blank TCL Script object. - :return: None + :param new_name: new name for the new Geometry object + :type new_name: str + :return: None """ + outname = 'new_script' if new_name is None else new_name + # commands_list = "# AddCircle, AddPolygon, AddPolyline, AddRectangle, AlignDrill, " \ # "AlignDrillGrid, Bbox, Bounds, ClearShell, CopperClear,\n" \ # "# Cncjob, Cutout, Delete, Drillcncjob, ExportDXF, ExportExcellon, ExportGcode,\n" \ @@ -505,17 +518,20 @@ class AppObject(QtCore.QObject): def initialize(new_obj, app): new_obj.source_file = deepcopy(new_source_file) - outname = 'new_script' self.new_object('script', outname, initialize, plot=False) - def new_document_object(self): + def new_document_object(self, new_name=None): """ Creates a new, blank Document object. - :return: None + :param new_name: new name for the new Geometry object + :type new_name: str + :return: None """ + outname = 'new_document' if new_name is None else new_name + def initialize(new_obj, app): new_obj.source_file = "" - self.new_object('document', 'new_document', initialize, plot=False) + self.new_object('document', outname, initialize, plot=False) diff --git a/appTools/ToolShell.py b/appTools/ToolShell.py index 3e99ca77..be22b61b 100644 --- a/appTools/ToolShell.py +++ b/appTools/ToolShell.py @@ -552,7 +552,7 @@ class FCShell(TermWidget): """ self.display_tcl_error(text) - raise self.TclErrorException(text) + # raise self.TclErrorException(text) class TclErrorException(Exception): """ diff --git a/app_Main.py b/app_Main.py index cb39f99f..d1bbc5b2 100644 --- a/app_Main.py +++ b/app_Main.py @@ -604,7 +604,8 @@ class App(QtCore.QObject): # ####################################### Auto-complete KEYWORDS ############################################ # ######################## Setup after the Defaults class was instantiated ################################## # ########################################################################################################### - self.tcl_commands_list = ['add_circle', 'add_poly', 'add_polygon', 'add_polyline', 'add_rectangle', + self.tcl_commands_list = ['add_aperture', 'add_circle', 'add_drill', 'add_poly', 'add_polygon', 'add_polyline', + 'add_rectangle', 'add_rect', 'add_slot', 'aligndrill', 'aligndrillgrid', 'bbox', 'clear', 'cncjob', 'cutout', 'del', 'drillcncjob', 'export_dxf', 'edxf', 'export_excellon', 'export_exc', @@ -613,7 +614,8 @@ class App(QtCore.QObject): 'interiors', 'isolate', 'join_excellon', 'join_geometry', 'list_sys', 'milld', 'mills', 'milldrills', 'millslots', 'mirror', 'ncc', - 'ncr', 'new', 'new_geometry', 'non_copper_regions', 'offset', + 'ncr', 'new', 'new_geometry', 'new_gerber', 'new_excellon', 'non_copper_regions', + 'offset', 'open_dxf', 'open_excellon', 'open_gcode', 'open_gerber', 'open_project', 'open_svg', 'options', 'origin', 'paint', 'panelize', 'plot_all', 'plot_objects', 'plot_status', 'quit_flatcam', diff --git a/tclCommands/TclCommand.py b/tclCommands/TclCommand.py index 80f19dd9..2ab164ec 100644 --- a/tclCommands/TclCommand.py +++ b/tclCommands/TclCommand.py @@ -264,17 +264,17 @@ class TclCommand(object): raise unknown_exception - 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 - """ - - # because of signaling we cannot call error to TCL from here but when task - # is finished also non-signaled are handled here to better exception - # handling and displayed after command is finished - raise self.app.shell.TclErrorException(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 + # :return: raise exception + # """ + # + # # because of signaling we cannot call error to TCL from here but when task + # # is finished also non-signaled are handled here to better exception + # # handling and displayed after command is finished + # raise self.app.shell.TclErrorException(text) def execute_wrapper(self, *args): """ @@ -310,7 +310,7 @@ class TclCommand(object): :return: None, output text or exception """ - raise NotImplementedError("Please Implement this method") + return "Not Implemented Error -> Incorrect call." class TclCommandSignaled(TclCommand): @@ -330,7 +330,7 @@ class TclCommandSignaled(TclCommand): @abc.abstractmethod def execute(self, args, unnamed_args): - raise NotImplementedError("Please Implement this method") + return "Not Implemented Error -> Incorrect call." output = None diff --git a/tclCommands/TclCommandAddAperture.py b/tclCommands/TclCommandAddAperture.py new file mode 100644 index 00000000..d157eeba --- /dev/null +++ b/tclCommands/TclCommandAddAperture.py @@ -0,0 +1,117 @@ +from tclCommands.TclCommand import * +import math + + +class TclCommandAddAperture(TclCommandSignaled): + """ + Tcl shell command to add a rectange to the given Geometry object. + """ + + # array of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon) + aliases = ['add_aperture'] + + description = '%s %s' % ("--", "Adds an aperture in the given Gerber object, if it does not exist already.") + + # Dictionary of types from Tcl command, needs to be ordered. + # For positional arguments + arg_names = collections.OrderedDict([ + ('name', str) + ]) + + # Dictionary of types from Tcl command, needs to be ordered. + # For options like -optionname value + option_types = collections.OrderedDict([ + ('apid', int), + ('type', str), + ('size', float), + ('width', float), + ('height', float) + ]) + + # 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 an aperture in the given Gerber object.", + 'args': collections.OrderedDict([ + ('name', 'Name of the Gerber object in which to add the aperture.'), + ('apid', 'Aperture ID. If not used then it will add one automatically.'), + ('type', "Aperture Type. It can be letter 'C' or 'R' or 'O'. " + "If not used then it will use 'C' by default."), + ('size', "The aperture size. Used only if aperture type is 'C'."), + ('width', "Aperture width. Used only if aperture type is 'R' or 'O'."), + ('height', "Aperture height. Used only if aperture type is 'R' or 'O'.") + ]), + 'examples': ["add_aperture gerber_name -apid 10 -type 'C' -size 1.0"] + } + + 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 + """ + + if unnamed_args: + self.raise_tcl_error( + "Too many arguments. Correct format: %s" % + '["add_aperture gerber_name -apid -type -size -width -height"]') + + name = args['name'] + try: + obj = self.app.collection.get_by_name(str(name)) + except Exception: + return "Could not retrieve object: %s" % name + if obj is None: + return "Object not found: %s" % name + if obj.kind != 'gerber': + return 'Expected Gerber, got %s %s.' % (name, type(obj)) + + obj_apertures_keys = list(obj.apertures.keys()) + if 'apid' in args: + new_apid = args['apid'] + else: + ap_list = [int(ap) for ap in obj_apertures_keys] + max_ap = max(ap_list) + # we can't have aperture ID's between 0 an 10 + new_apid = 10 if max_ap == 0 else (max_ap + 1) + + if str(new_apid) in obj_apertures_keys: + return "The aperture is already used. Try another 'apid' parameter value." + + if 'type' in args: + try: + # if it's a string using the quotes + new_type = eval(args['type']) + except NameError: + # if it's a string without quotes + new_type = args['type'] + else: + new_type = 'C' + if new_type == 'C': + new_size = args['size'] if 'size' in args else 0.6 + obj.apertures[str(new_apid)] = { + 'type': new_type, + 'size': new_size, + 'geometry': [] + } + elif new_type in ['R', 'O']: + new_width = args['width'] if 'width' in args else 0.0 + new_height = args['height'] if 'height' in args else 0.0 + new_size = math.sqrt(new_width ** 2 + new_height ** 2) if new_width > 0 and new_height > 0 else 0 + + obj.apertures[str(new_apid)] = { + 'type': new_type, + 'size': new_size, + 'width': new_width, + 'height': new_height, + 'geometry': [] + } + else: + return 'The supported aperture types are: C=circular, R=rectangular, O=oblong' + + print(obj.apertures) \ No newline at end of file diff --git a/tclCommands/TclCommandAddDrill.py b/tclCommands/TclCommandAddDrill.py new file mode 100644 index 00000000..2acfddab --- /dev/null +++ b/tclCommands/TclCommandAddDrill.py @@ -0,0 +1,123 @@ +from tclCommands.TclCommand import * +from copy import deepcopy +from shapely.geometry import Point + + +class TclCommandAddDrill(TclCommandSignaled): + """ + Tcl shell command to add a rectange to the given Geometry object. + """ + + # array of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon) + aliases = ['add_drill'] + + description = '%s %s' % ("--", "Adds a drill in the given Excellon object, if it does not exist already.") + + # Dictionary of types from Tcl command, needs to be ordered. + # For positional arguments + arg_names = collections.OrderedDict([ + ('name', str) + ]) + + # Dictionary of types from Tcl command, needs to be ordered. + # For options like -optionname value + option_types = collections.OrderedDict([ + ('dia', float), + ('x', float), + ('y', float), + ]) + + # 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': "Adds a drill hole in the given Excellon object.", + 'args': collections.OrderedDict([ + ('name', 'Name of the Excellon object in which to add the drill hole.'), + ('dia', 'The tool diameter.'), + ('x', "X coordinate for the drill hole. If not used then the assumed X value is 0.0."), + ('y', "Y coordinate for the drill hole. If not used then the assumed Y value is 0.0."), + ]), + 'examples': ["add_drill excellon_name -dia 0.8 -x 13.3 -y 6.2"] + } + + 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 + """ + + if unnamed_args: + self.raise_tcl_error( + "Too many arguments. Correct format: %s" % + '["add_drill excellon_name -dia -x -y"]') + + name = args['name'] + try: + obj = self.app.collection.get_by_name(str(name)) + except Exception: + return "Could not retrieve object: %s" % name + if obj is None: + return "Object not found: %s" % name + if obj.kind != 'excellon': + return 'Expected Excellon, got %s %s.' % (name, type(obj)) + + if 'dia' not in args: + return "Failed. The -dia parameter is missing and it is required." + + new_dia = args['dia'] + drill_x = 0.0 if 'x' not in args else args['x'] + drill_y = 0.0 if 'y' not in args else args['y'] + + # create a dict with the keys the tool dimater value truncated to the nr of decimals use by the app and the + # value is the tool number + dia_tool_dict = { + self.app.dec_format(diam['tooldia'], self.app.decimals): tool for tool, diam in list(obj.tools.items()) + } + + new_data = {} + kind = 'excellon' + for option in self.app.options: + if option.find(kind + "_") == 0: + oname = option[len(kind) + 1:] + new_data[oname] = self.app.options[option] + + if option.find('tools_drill_') == 0: + new_data[option] = self.app.options[option] + + drill_point = Point((drill_x, drill_y)) + + if not dia_tool_dict: + obj.tools[1] = { + 'tooldia': new_dia, + 'drills': [drill_point], + 'slots': [], + 'data': deepcopy(new_data) + } + elif new_dia not in list(dia_tool_dict.keys()): + new_tool = max(list(obj.tools.keys())) + 1 + obj.tools[new_tool] = { + 'tooldia': new_dia, + 'drills': [drill_point], + 'slots': [], + 'data': deepcopy(new_data) + } + + # sort the updated tools dict + new_tools = {} + tools_sorted = sorted(obj.tools.items(), key=lambda x: x[1]['tooldia']) + for idx, tool in enumerate(tools_sorted, start=1): + new_tools[idx] = tool[1] + obj.tools = deepcopy(new_tools) + elif new_dia in list(dia_tool_dict.keys()): + if drill_point not in obj.tools[dia_tool_dict[new_dia]]['drills']: + obj.tools[dia_tool_dict[new_dia]]['drills'].append(drill_point) + else: + return "Drill with the given coordinates is already in the object." + + obj.create_geometry() diff --git a/tclCommands/TclCommandAddRectangle.py b/tclCommands/TclCommandAddRectangle.py index 2cbdb0d0..4ab2c005 100644 --- a/tclCommands/TclCommandAddRectangle.py +++ b/tclCommands/TclCommandAddRectangle.py @@ -7,7 +7,7 @@ class TclCommandAddRectangle(TclCommandSignaled): """ # array of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon) - aliases = ['add_rectangle'] + aliases = ['add_rect', 'add_rectangle'] description = '%s %s' % ("--", "Creates a rectangle in the given Geometry object.") diff --git a/tclCommands/TclCommandAddSlot.py b/tclCommands/TclCommandAddSlot.py new file mode 100644 index 00000000..4f43e8ab --- /dev/null +++ b/tclCommands/TclCommandAddSlot.py @@ -0,0 +1,136 @@ +from tclCommands.TclCommand import * +from copy import deepcopy +from shapely.geometry import Point + + +class TclCommandAddSlot(TclCommandSignaled): + """ + Tcl shell command to add a rectange to the given Geometry object. + """ + + # array of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon) + aliases = ['add_slot'] + + description = '%s %s' % ("--", "Adds a slot in the given Excellon object, if it does not exist already.") + + # Dictionary of types from Tcl command, needs to be ordered. + # For positional arguments + arg_names = collections.OrderedDict([ + ('name', str) + ]) + + # Dictionary of types from Tcl command, needs to be ordered. + # For options like -optionname value + option_types = collections.OrderedDict([ + ('dia', float), + ('startx', float), + ('starty', float), + ('stopx', float), + ('stopy', float), + ]) + + # 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': "Adds a slot hole in the given Excellon object.", + 'args': collections.OrderedDict([ + ('name', 'Name of the Excellon object in which to add the slot.'), + ('dia', 'The tool diameter. Required.'), + ('startx', "The X coordinate for the slot start point. Required."), + ('starty', "The Y coordinate for the slot stop point. Required."), + ('stopx', "The X coordinate for the slot start point. Required."), + ('stopy', "The Y coordinate for the slot stop point. Required."), + ]), + 'examples': ["add_slot excellon_name -dia 0.8 -startx 1 -starty 1 -stopx 1.3 -stopy 1"] + } + + 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 + """ + + if unnamed_args: + self.raise_tcl_error( + "Too many arguments. Correct format: %s" % + '["add_drill excellon_name -dia -x -y"]') + + name = args['name'] + try: + obj = self.app.collection.get_by_name(str(name)) + except Exception: + return "Could not retrieve object: %s" % name + if obj is None: + return "Object not found: %s" % name + if obj.kind != 'excellon': + return 'Expected Excellon, got %s %s.' % (name, type(obj)) + + if 'dia' not in args: + return "Failed. The -dia parameter is missing and it is required." + if 'startx' not in args: + return "Failed. The -startx parameter is missing and it is required." + if 'starty' not in args: + return "Failed. The -starty parameter is missing and it is required." + if 'stopx' not in args: + return "Failed. The -stopx parameter is missing and it is required." + if 'stopy' not in args: + return "Failed. The -stopy parameter is missing and it is required." + + new_dia = args['dia'] + slot_start_x = args['startx'] + slot_start_y = args['starty'] + slot_stop_x = args['stopx'] + slot_stop_y = args['stopy'] + + dia_tool_dict = {diam['tooldia']: tool for tool, diam in list(obj.tools.items())} + + new_data = {} + kind = 'excellon' + for option in self.app.options: + if option.find(kind + "_") == 0: + oname = option[len(kind) + 1:] + new_data[oname] = self.app.options[option] + + if option.find('tools_drill_') == 0: + new_data[option] = self.app.options[option] + + new_slot = ( + Point(slot_start_x, slot_start_y), + Point(slot_stop_x, slot_stop_y) + ) + + if not dia_tool_dict: + obj.tools[1] = { + 'tooldia': new_dia, + 'drills': [], + 'slots': [new_slot], + 'data': deepcopy(new_data) + } + elif new_dia not in list(dia_tool_dict.keys()): + new_tool = max(list(obj.tools.keys())) + 1 + obj.tools[new_tool] = { + 'tooldia': new_dia, + 'drills': [], + 'slots': [new_slot], + 'data': deepcopy(new_data) + } + + # sort the updated tools dict + new_tools = {} + tools_sorted = sorted(obj.tools.items(), key=lambda x: x[1]['tooldia']) + for idx, tool in enumerate(tools_sorted, start=1): + new_tools[idx] = tool[1] + obj.tools = deepcopy(new_tools) + elif new_dia in list(dia_tool_dict.keys()): + if new_slot not in obj.tools[dia_tool_dict[new_dia]]['slots']: + obj.tools[dia_tool_dict[new_dia]]['slots'].append(new_slot) + else: + return "Slot with the given coordinates is already in the object." + + obj.create_geometry() diff --git a/tclCommands/TclCommandNewExcellon.py b/tclCommands/TclCommandNewExcellon.py index c5072ffb..ed77e575 100644 --- a/tclCommands/TclCommandNewExcellon.py +++ b/tclCommands/TclCommandNewExcellon.py @@ -58,4 +58,4 @@ class TclCommandNewExcellon(TclCommandSignaled): name = args['name'] else: name = 'new_exc' - self.app.app_obj.new_object('excellon', name, lambda x, y: None, plot=False) + self.app.app_obj.new_excellon_object(new_name=name) diff --git a/tclCommands/TclCommandNewGeometry.py b/tclCommands/TclCommandNewGeometry.py index 08c3c7ce..53f2ecb7 100644 --- a/tclCommands/TclCommandNewGeometry.py +++ b/tclCommands/TclCommandNewGeometry.py @@ -52,4 +52,4 @@ class TclCommandNewGeometry(TclCommandSignaled): else: name = 'new_geo' - self.app.app_obj.new_object('geometry', str(name), lambda x, y: None, plot=False) + self.app.app_obj.new_geometry_object(new_name=name) diff --git a/tclCommands/TclCommandNewGerber.py b/tclCommands/TclCommandNewGerber.py index 0b2e44fb..0d6c80a3 100644 --- a/tclCommands/TclCommandNewGerber.py +++ b/tclCommands/TclCommandNewGerber.py @@ -59,21 +59,4 @@ class TclCommandNewGerber(TclCommandSignaled): else: name = 'new_grb' - def initialize(grb_obj, app_obj): - grb_obj.multitool = False - grb_obj.source_file = [] - grb_obj.multigeo = False - grb_obj.follow = False - grb_obj.apertures = {} - grb_obj.solid_geometry = [] - grb_obj.follow_geometry = [] - - try: - grb_obj.options['xmin'] = 0 - grb_obj.options['ymin'] = 0 - grb_obj.options['xmax'] = 0 - grb_obj.options['ymax'] = 0 - except KeyError: - pass - - self.app.app_obj.new_object('gerber', name, initialize, plot=False) + self.app.app_obj.new_gerber_object(new_name=name) diff --git a/tclCommands/__init__.py b/tclCommands/__init__.py index 30971044..b1d04fd6 100644 --- a/tclCommands/__init__.py +++ b/tclCommands/__init__.py @@ -2,10 +2,13 @@ import pkgutil import sys # allowed command modules (please append them alphabetically ordered) +import tclCommands.TclCommandAddAperture import tclCommands.TclCommandAddCircle +import tclCommands.TclCommandAddDrill import tclCommands.TclCommandAddPolygon import tclCommands.TclCommandAddPolyline import tclCommands.TclCommandAddRectangle +import tclCommands.TclCommandAddSlot import tclCommands.TclCommandAlignDrill import tclCommands.TclCommandAlignDrillGrid import tclCommands.TclCommandBbox