diff --git a/CHANGELOG.md b/CHANGELOG.md index 17c69d6d..fa899dec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ CHANGELOG for FlatCAM beta - fixed the multi-color feature in the Gerber object UI - fixed the marking of apertures in Gerber object UI and in Extract plugin - minor changes in `cncjob` Tcl command +- updated the `offset` and `scale` Tcl commands to work on a selection of objects 31.02.2022 diff --git a/tclCommands/TclCommandJoinExcellon.py b/tclCommands/TclCommandJoinExcellon.py index 96ab7512..473ceab8 100644 --- a/tclCommands/TclCommandJoinExcellon.py +++ b/tclCommands/TclCommandJoinExcellon.py @@ -57,6 +57,10 @@ class TclCommandJoinExcellon(TclCommand): outname = args['outname'] if 'outname' in args else "joined_exc" obj_names = unnamed_args + if not obj_names: + self.app.log.error("Missing objects to be joined. Exiting.") + return "fail" + objs = [] for obj_n in obj_names: obj = self.app.collection.get_by_name(str(obj_n)) diff --git a/tclCommands/TclCommandJoinGeometry.py b/tclCommands/TclCommandJoinGeometry.py index 55f5100f..5c59a7eb 100644 --- a/tclCommands/TclCommandJoinGeometry.py +++ b/tclCommands/TclCommandJoinGeometry.py @@ -53,6 +53,9 @@ class TclCommandJoinGeometry(TclCommand): outname = args['outname'] if 'outname' in args else "joined_geo" obj_names = unnamed_args + if not obj_names: + self.app.log.error("Missing objects to be joined. Exiting.") + return "fail" objs = [] for obj_n in obj_names: diff --git a/tclCommands/TclCommandOffset.py b/tclCommands/TclCommandOffset.py index 91fd46fc..8a97480c 100644 --- a/tclCommands/TclCommandOffset.py +++ b/tclCommands/TclCommandOffset.py @@ -14,13 +14,11 @@ class TclCommandOffset(TclCommand): # List of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon) aliases = ['offset'] - description = '%s %s' % ("--", "Will offset the geometry of a named object. Does not create a new object.") + description = '%s %s' % ("--", "Will offset the geometry of named objects. Does not create a new object.") # Dictionary of types from Tcl command, needs to be ordered arg_names = collections.OrderedDict([ - ('name', str), - ('x', float), - ('y', float) + ]) # Dictionary of types from Tcl command, needs to be ordered , this is for options like -optionname value @@ -30,17 +28,19 @@ class TclCommandOffset(TclCommand): ]) # array of mandatory options for current Tcl command: required = {'name','outname'} - required = ['name'] + required = [] # structured help for current command, args needs to be ordered help = { - 'main': "Changes the position of the object on X and/or Y axis.", + 'main': "Changes the position of the named object(s) on X and/or Y axis.\n" + "The names of the objects to be offset will be entered after the command,\n" + "separated by spaces. See the example below.\n" + "WARNING: if the name of an object has spaces, enclose the name with quotes.", 'args': collections.OrderedDict([ - ('name', 'Name of the object to offset. Required.'), ('x', 'Offset distance in the X axis. If it is not used it will be assumed to be 0.0'), ('y', 'Offset distance in the Y axis. If it is not used it will be assumed to be 0.0') ]), - 'examples': ['offset my_geometry -x 1.2 -y -0.3', 'offset my_geometry -x 1.0'] + 'examples': ['offset my_object_1 "my object_1" -x 1.2 -y -0.3', 'offset my_object -x 1.0'] } def execute(self, args, unnamed_args): @@ -51,13 +51,24 @@ class TclCommandOffset(TclCommand): :return: """ - name = args['name'] off_x = args['x'] if 'x' in args else 0.0 off_y = args['y'] if 'y' in args else 0.0 x, y = float(off_x), float(off_y) if (x, y) == (0.0, 0.0): + self.app.log.warning("Offset by 0.0. Nothing to be done.") return - self.app.collection.get_by_name(name).offset((x, y)) + obj_names = unnamed_args + if not obj_names: + self.app.log.error("Missing objects to be offset. Exiting.") + return "fail" + + for name in obj_names: + obj = self.app.collection.get_by_name(str(name)) + if obj is None or obj == '': + self.app.log.error("Object not found: %s" % name) + return "fail" + + obj.offset((x, y)) diff --git a/tclCommands/TclCommandScale.py b/tclCommands/TclCommandScale.py index 7a7061d0..dc13a47e 100644 --- a/tclCommands/TclCommandScale.py +++ b/tclCommands/TclCommandScale.py @@ -25,16 +25,16 @@ class TclCommandScale(TclCommand): # List of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon) aliases = ['scale'] - description = '%s %s' % ("--", "Will scale the geometry of a named object. Does not create a new object.") + description = '%s %s' % ("--", "Will scale the geometry of named objects. Does not create a new object.") # Dictionary of types from Tcl command, needs to be ordered arg_names = collections.OrderedDict([ - ('name', str), - ('factor', float) + ]) # Dictionary of types from Tcl command, needs to be ordered , this is for options like -optionname value option_types = collections.OrderedDict([ + ('factor', float), ('x', float), ('y', float), ('origin', str) @@ -45,24 +45,26 @@ class TclCommandScale(TclCommand): # structured help for current command, args needs to be ordered help = { - 'main': "Resizes the object by a factor on X axis and a factor on Y axis, having as scale origin the point ", + 'main': "Resizes objects by a factor on X axis and a factor on Y axis, having a specified scale origin\n" + "The names of the objects to be scaled will be entered after the command,\n" + "separated by spaces. See the example below.\n" + "WARNING: if the name of an object has spaces, enclose the name with quotes.", 'args': collections.OrderedDict([ - ('name', 'Name of the object (Gerber, Geometry or Excellon) to be resized. Required.'), ('factor', 'Fraction by which to scale on both axis.'), ('x', 'Fraction by which to scale on X axis. If "factor" is used then this parameter is ignored'), ('y', 'Fraction by which to scale on Y axis. If "factor" is used then this parameter is ignored'), ('origin', 'Reference used for scale.\n' 'The reference point can be:\n' '- "origin" which means point (0, 0)\n' - '- "min_bounds" which means the lower left point of the bounding box\n' - '- "center" which means the center point of the bounding box of the object.\n' - '- a tuple in format (x,y) with the X and Y coordinates separated by a comma. NO SPACES ALLOWED') + '- "min_bounds" which means the lower left point of the bounding box made for all objects\n' + '- "center" which means the center point of the bounding box made for all objects.\n' + '- a point in format (x,y) with the X and Y coordinates separated by a comma. NO SPACES ALLOWED') ]), - 'examples': ['scale my_geometry 4.2', - 'scale my_geo -x 3.1 -y 2.8', - 'scale my_geo 1.2 -origin min_bounds', - 'scale my_geometry -x 2 -origin 3.0,2.1'] + 'examples': ['scale my_obj_1 "my obj_2" -factor 4.2', + 'scale my_obj -x 3.1 -y 2.8', + 'scale my_obj -factor 1.2 -origin min_bounds', + 'scale my_object -x 2 -origin 3.0,2.1'] } def execute(self, args, unnamed_args): @@ -72,59 +74,87 @@ class TclCommandScale(TclCommand): :param unnamed_args: :return: """ + if 'x' not in args and 'y' not in args and 'factor' not in args: + self.app.log.warning('%s' % "Expected -x -y or -factor ") + self.raise_tcl_error('%s' % "Expected -x -y or -factor ") + return 'fail' - name = args['name'] - try: - obj_to_scale = self.app.collection.get_by_name(name) - except Exception as e: - self.app.log.error("TclCommandCopperScale.execute() --> %s" % str(e)) - self.raise_tcl_error("%s: %s" % (_("Could not retrieve object"), name)) - return "Could not retrieve object: %s" % name + obj_names = unnamed_args + if not obj_names: + self.app.log.error("Missing objects to be offset. Exiting.") + return "fail" - if 'origin' not in args: - xmin, ymin, xmax, ymax = obj_to_scale.bounds() - c_x = xmin + (xmax - xmin) / 2 - c_y = ymin + (ymax - ymin) / 2 - point = (c_x, c_y) - else: - if args['origin'] == 'origin': - point = (0, 0) - elif args['origin'] == 'min_bounds': - xmin, ymin, xmax, ymax = obj_to_scale.bounds() - point = (xmin, ymin) - elif args['origin'] == 'center': - xmin, ymin, xmax, ymax = obj_to_scale.bounds() + # calculate the bounds + minx_lst = [] + miny_lst = [] + maxx_lst = [] + maxy_lst = [] + for name in obj_names: + obj = self.app.collection.get_by_name(str(name)) + if obj is None or obj == '': + self.app.log.error("Object not found: %s" % name) + return "fail" + a, b, c, d = obj.bounds() + minx_lst.append(a) + miny_lst.append(b) + maxx_lst.append(c) + maxy_lst.append(d) + xmin = min(minx_lst) + ymin = min(miny_lst) + xmax = max(maxx_lst) + ymax = max(maxy_lst) + + for name in obj_names: + try: + obj_to_scale = self.app.collection.get_by_name(name) + except Exception as e: + self.app.log.error("TclCommandCopperScale.execute() --> %s" % str(e)) + self.app.log.error("Could not retrieve object: %s" % name) + self.raise_tcl_error("%s: %s" % (_("Could not retrieve object"), name)) + return "fail" + + if obj_to_scale is None or obj_to_scale == '': + self.app.log.error("Object not found: %s" % name) + return "fail" + + if 'origin' not in args: c_x = xmin + (xmax - xmin) / 2 c_y = ymin + (ymax - ymin) / 2 point = (c_x, c_y) else: - try: - point = eval(args['origin']) - if not isinstance(point, tuple): - raise Exception - except Exception as e: - self.raise_tcl_error('%s\n%s' % (_("Expected -origin or " - "-origin or " - "-origin
or " - "- origin 3.0,4.2."), str(e))) - return 'fail' + if args['origin'] == 'origin': + point = (0, 0) + elif args['origin'] == 'min_bounds': + point = (xmin, ymin) + elif args['origin'] == 'center': + c_x = xmin + (xmax - xmin) / 2 + c_y = ymin + (ymax - ymin) / 2 + point = (c_x, c_y) + else: + try: + point = eval(str(args['origin'])) + if not isinstance(point, tuple): + self.app.log.error("The -origin value is not a tuple in format e.g 3.32,4.5") + return "fail" + except Exception as e: + self.raise_tcl_error('%s\n%s' % (_("Expected -origin or " + "-origin or " + "-origin
or " + "- origin 3.0,4.2."), str(e))) + return 'fail' - if 'factor' in args: - factor = float(args['factor']) - obj_to_scale.scale(factor, point=point) - return + if 'factor' in args: + factor = float(args['factor']) + obj_to_scale.scale(factor, point=point) + continue - if 'x' not in args and 'y' not in args: - self.raise_tcl_error('%s' % _("Expected -x -y .")) - return 'fail' - - if 'x' in args and 'y' not in args: - f_x = float(args['x']) - obj_to_scale.scale(f_x, 0, point=point) - elif 'x' not in args and 'y' in args: - f_y = float(args['y']) - obj_to_scale.scale(0, f_y, point=point) - elif 'x' in args and 'y' in args: - f_x = float(args['x']) - f_y = float(args['y']) - obj_to_scale.scale(f_x, f_y, point=point) + if 'x' in args and 'y' not in args: + f_x = float(args['x']) + obj_to_scale.scale(f_x, 0, point=point) + elif 'x' not in args and 'y' in args: + f_y = float(args['y']) + obj_to_scale.scale(0, f_y, point=point) + elif 'x' in args and 'y' in args: + f_x = float(args['x']) + f_y = float(args['y']) + obj_to_scale.scale(f_x, f_y, point=point)