From 52a0efb62e7d913361613487c4584e13d28b358e Mon Sep 17 00:00:00 2001 From: Marius Stanciu Date: Tue, 1 Feb 2022 16:48:42 +0200 Subject: [PATCH] - upgraded the `skew` Tcl command to work on a selection of objects and added the ability to skew not only in degrees but also by distances --- CHANGELOG.md | 3 +- app_Main.py | 6 +- defaults.py | 8 +- tclCommands/TclCommandScale.py | 7 +- tclCommands/TclCommandSkew.py | 175 +++++++++++++++++++++++++++++---- 5 files changed, 166 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 92a045c5..726a8a57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,8 @@ CHANGELOG for FlatCAM beta - updated the `offset` and `scale` Tcl commands to work on a selection of objects - minor changed in camlib.py regarding the self.check_zcut() method setting the self.z_cut to 'fail' - upgraded the `mirror` Tcl command to work on a selection of objects and also fixed a plot issue - +- upgraded the `skew` Tcl command to work on a selection of objects and added the ability to skew not only in degrees but also by distances +- 31.02.2022 - fixed all the `laser` preprocessors to work correctly and the resulting GCode to be plotted diff --git a/app_Main.py b/app_Main.py index 874ce3d8..64d7d2b8 100644 --- a/app_Main.py +++ b/app_Main.py @@ -659,21 +659,21 @@ class App(QtCore.QObject): 'Toolchange_Manual', 'Toolchange_Probe_MACH3', 'True', 'Users', 'all', 'auto', 'axis', - 'axisoffset', 'box', 'center_x', 'center_y', 'columns', 'combine', 'connect', + 'axisoffset', 'box', 'center_x', 'center_y', 'center', 'columns', 'combine', 'connect', 'contour', 'default', 'depthperpass', 'dia', 'diatol', 'dist', 'drilled_dias', 'drillz', 'dpp', 'dwelltime', 'extracut_length', 'endxy', 'endz', 'f', 'factor', 'feedrate', 'feedrate_z', 'gridoffsety', 'gridx', 'gridy', 'has_offset', 'holes', 'hpgl', 'iso_type', 'join', 'keep_scripts', 'las_min_pwr', 'las_power', 'margin', 'marlin', 'method', - 'milled_dias', 'minoffset', 'name', 'offset', 'opt_type', 'order', + 'milled_dias', 'minoffset', 'min_bounds', 'name', 'offset', 'opt_type', 'order', 'outname', 'overlap', 'obj_name', 'passes', 'postamble', 'pp', 'ppname_e', 'ppname_g', 'preamble', 'radius', 'ref', 'rest', 'rows', 'shellvar_', 'scale_factor', 'spacing_columns', 'spacing_rows', 'spindlespeed', 'startz', 'startxy', 'toolchange_xy', 'toolchangez', 'travelz', 'tooldia', 'use_threads', 'value', - 'x', 'x0', 'x1', 'y', 'y0', 'y1', 'z_cut', 'z_move' + 'x', 'x0', 'x1', 'x_dist', 'y', 'y0', 'y1', 'y_dist', 'z_cut', 'z_move' ] self.tcl_keywords = [ diff --git a/defaults.py b/defaults.py index 0fc8609f..640eea11 100644 --- a/defaults.py +++ b/defaults.py @@ -801,17 +801,19 @@ class FlatCAMDefaults: 'Repetier, Roland_MDX_20, Roland_MDX_540,' 'Toolchange_Manual, Toolchange_Probe_MACH3, True, ' 'Users, all, auto, axis, axisoffset, ' - 'box, center_x, center_y, columns, combine, connect, contour, default, ' + 'box, center_x, center_y, center, columns, combine, connect, contour, default, ' 'depthperpass, dia, diatol, dist, drilled_dias, drillz, dpp, dwelltime, ' 'endxy, endz, extracut_length, f, factor, feedrate, ' 'feedrate_z, gridoffsety, gridx, gridy, has_offset, ' 'holes, hpgl, iso_type, join, ' 'las_min_pwr, las_power, keep_scripts, margin, marlin, method, milled_dias, ' - 'minoffset, name, offset, opt_type, order, outname, overlap, obj_name' + 'minoffset, min_bounds, name, offset, opt_type, order, ' + 'outname, overlap, obj_name, ' 'passes, postamble, pp, ppname_e, ppname_g, preamble, radius, ref, rest, ' 'rows, shellvar_, scale_factor, spacing_columns, spacing_rows, spindlespeed, ' 'startz, startxy, toolchange_xy, toolchangez, ' - 'tooldia, travelz, use_threads, value, x, x0, x1, y, y0, y1, z_cut, ' + 'tooldia, travelz, use_threads, value, ' + 'x, x0, x1, x_dist, y, y0, y1, y_dist, z_cut, ' 'z_move', "script_autocompleter": True, "script_text": "", diff --git a/tclCommands/TclCommandScale.py b/tclCommands/TclCommandScale.py index dc13a47e..053a0a1d 100644 --- a/tclCommands/TclCommandScale.py +++ b/tclCommands/TclCommandScale.py @@ -1,7 +1,6 @@ from tclCommands.TclCommand import TclCommand import collections -import logging import gettext import appTranslation as fcTranslate @@ -11,8 +10,6 @@ fcTranslate.apply_language('strings') if '_' not in builtins.__dict__: _ = gettext.gettext -log = logging.getLogger('base') - class TclCommandScale(TclCommand): """ @@ -74,14 +71,14 @@ class TclCommandScale(TclCommand): :param unnamed_args: :return: """ - if 'x' not in args and 'y' not in args and 'factor' not in args: + if ('x' not in args or 'X' not in args) and ('y' not in args or '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' obj_names = unnamed_args if not obj_names: - self.app.log.error("Missing objects to be offset. Exiting.") + self.app.log.error("Missing objects to be scale. Exiting.") return "fail" # calculate the bounds diff --git a/tclCommands/TclCommandSkew.py b/tclCommands/TclCommandSkew.py index b181d9a2..1eae6844 100644 --- a/tclCommands/TclCommandSkew.py +++ b/tclCommands/TclCommandSkew.py @@ -1,6 +1,15 @@ from tclCommands.TclCommand import TclCommand import collections +import math + +import gettext +import appTranslation as fcTranslate +import builtins + +fcTranslate.apply_language('strings') +if '_' not in builtins.__dict__: + _ = gettext.gettext class TclCommandSkew(TclCommand): @@ -18,28 +27,45 @@ class TclCommandSkew(TclCommand): # 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([ ('x', float), - ('y', float) + ('y', float), + ('x_dist', float), + ('y_dist', float), + ('origin', str) ]) # 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': "Shear/Skew an object along x and y dimensions. The reference point is the left corner of " - "the bounding box of the object.", + 'main': "Shear/Skew an object along x and y dimensions, having a specified skew origin" + "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 deformed (skewed). Required.'), ('x', 'Angle in degrees by which to skew on the X axis. If it is not used it will be assumed to be 0.0'), - ('y', 'Angle in degrees by which to skew on the Y axis. If it is not used it will be assumed to be 0.0') + ('y', 'Angle in degrees by which to skew on the Y axis. If it is not used it will be assumed to be 0.0'), + ('x_dist', 'Distance to skew on the X axis. If it is not used it will be assumed to be 0.0'), + ('y_dist', 'Distance to skew on the Y axis. If it is not used it will be assumed to be 0.0\n' + 'WARNING: You either use the (x_dist, y_dist) pair or the (x, y). They can not be mixed.'), + + ('origin', 'Reference used for skew.\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 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': ['skew my_geometry -x 10.2 -y 3.5', 'skew my_geo -x 3.0'] + 'examples': ['skew my_obj1 "my obj_2" -x 10.2 -y 3.5', + 'skew my_obj -x 3.0 -origin 3.0,2.1', + 'skew my_obj -x 1.0 -origin min_bounds', + 'skew my_obj1 "my obj2" -x_dist 3.0 -y_dist 1.0 -origin 3.0,2.1'] } def execute(self, args, unnamed_args): @@ -49,23 +75,130 @@ class TclCommandSkew(TclCommand): :param unnamed_args: :return: """ + if ('x' in args or 'X' in args or 'y' in args or 'Y' in args) and ('x_dist' in args or 'y_dist' in args): + self.app.log.error( + '%s' % "You either use the (x_dist, y_dist) pair or the (x, y). They can not be mixed.") + return 'fail' - name = args['name'] + obj_names = unnamed_args + if not obj_names: + self.app.log.error("Missing objects to be offset. Exiting.") + return "fail" - if 'x' in args: + use_angles = False + use_distances = False + + try: angle_x = float(args['x']) - else: - angle_x = 0.0 + use_angles = True + except Exception: + try: + angle_x = float(args['X']) + use_angles = True + except Exception: + angle_x = 0.0 - if 'y' in args: + try: angle_y = float(args['y']) - else: - angle_y = 0.0 + use_angles = True + except Exception: + try: + angle_y = float(args['Y']) + use_angles = True + except Exception: + angle_y = 0.0 - if angle_x == 0.0 and angle_y == 0.0: - # nothing to be done - return + try: + dist_x = float(args['x_dist']) + use_distances = True + except Exception: + dist_x = 0.0 - obj_to_skew = self.app.collection.get_by_name(name) - xmin, ymin, xmax, ymax = obj_to_skew.bounds() - obj_to_skew.skew(angle_x, angle_y, point=(xmin, ymin)) + try: + dist_y = float(args['y_dist']) + use_distances = True + except Exception: + dist_y = 0.0 + + if use_angles is True: + if angle_x == 0.0 and angle_y == 0.0: + # nothing to be done + return + + if use_distances is True: + if dist_x == 0.0 and dist_y == 0.0: + # nothing to be done + return + + obj_names = unnamed_args + if not obj_names: + self.app.log.error("Missing objects to be skew. Exiting.") + return "fail" + + # 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_skew = self.app.collection.get_by_name(name) + except Exception as e: + self.app.log.error("TclCommandCopperSkew.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_skew is None or obj_to_skew == '': + self.app.log.error("Object not found: %s" % name) + return "fail" + + if 'origin' not in args: + ref_point = (0, 0) + else: + if args['origin'] == 'origin': + ref_point = (0, 0) + elif args['origin'] == 'min_bounds': + ref_point = (xmin, ymin) + elif args['origin'] == 'center': + c_x = xmin + (xmax - xmin) / 2 + c_y = ymin + (ymax - ymin) / 2 + ref_point = (c_x, c_y) + else: + try: + ref_point = eval(str(args['origin'])) + if not isinstance(ref_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 use_distances: + # determination of angle_x + height = ymax - ymin + angle_x = math.degrees(math.atan(dist_x/height)) + + # determination of angle_y + width = xmax - xmin + angle_y = math.degrees(math.atan(dist_y/width)) + + obj_to_skew.skew(angle_x, angle_y, point=ref_point)