diff --git a/CHANGELOG.md b/CHANGELOG.md index cce56528..ff7b4b4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ CHANGELOG for FlatCAM beta ================================================= +21.01.2022 + +- added a new Tcl command: `buffer` which will buffer the geometry of an object or will scale individually each geometry sub element +- fixed the buffer() method for the Excellon objects (the resulting tool diameters were calculated less than the what was expected) + 19.01.2022 - updated the header of the postprocessos with 'laser' to show essential informations like some of them do not move on the Z axis diff --git a/appParsers/ParseExcellon.py b/appParsers/ParseExcellon.py index d2b7ae82..06061556 100644 --- a/appParsers/ParseExcellon.py +++ b/appParsers/ParseExcellon.py @@ -9,7 +9,7 @@ from camlib import Geometry, grace import shapely.affinity as affinity -from shapely.geometry import Point, LineString +from shapely.geometry import Point, LineString, LinearRing, MultiLineString, MultiPolygon import numpy as np import re @@ -1559,13 +1559,14 @@ class Excellon(Geometry): self.create_geometry() self.app.proc_container.new_text = '' - def buffer(self, distance, join, factor): + def buffer(self, distance, join, factor, only_exterior=False): """ - :param distance: if 'factor' is True then distance is the factor - :param factor: True or False (None) - :param join: The type of line joint used by the shapely buffer method: round, square, bevel - :return: None + :param distance: if 'factor' is True then distance is the factor + :param factor: True or False (None) + :param join: The type of line joint used by the shapely buffer method: round, square, bevel + :param only_exterior: Bool. If True, the LineStrings are buffered only on the outside + :return: None """ self.app.log.debug("appParsers.ParseExcellon.Excellon.buffer()") @@ -1573,19 +1574,22 @@ class Excellon(Geometry): return def buffer_geom(obj): + new_obj = [] try: - new_obj = [] - for g in obj: + work_geo = obj.geoms if isinstance(obj, (MultiPolygon, MultiLineString)) else obj + for g in work_geo: new_obj.append(buffer_geom(g)) - return new_obj except TypeError: try: if factor is None: - return obj.buffer(distance, resolution=self.excellon_circle_steps) + new_obj = obj.buffer(distance, resolution=self.excellon_circle_steps) + if isinstance(obj, (LinearRing, LineString)) and only_exterior is True: + new_obj = new_obj.exterior else: - return affinity.scale(obj, xfact=distance, yfact=distance, origin='center') + new_obj = affinity.scale(obj, xfact=distance, yfact=distance, origin='center') except AttributeError: - return obj + new_obj = obj + return new_obj # buffer solid_geometry for tool, tool_dict in list(self.tools.items()): @@ -1596,8 +1600,8 @@ class Excellon(Geometry): except TypeError: self.tools[tool]['solid_geometry'] = [res] if factor is None: - self.tools[tool]['tooldia'] += distance + self.tools[tool]['tooldia'] += (distance * 2) else: - self.tools[tool]['tooldia'] *= distance + self.tools[tool]['tooldia'] *= (distance * 2) self.create_geometry() diff --git a/appParsers/ParseGerber.py b/appParsers/ParseGerber.py index a4bb76a1..433c70eb 100644 --- a/appParsers/ParseGerber.py +++ b/appParsers/ParseGerber.py @@ -8,6 +8,7 @@ from copy import deepcopy from shapely.ops import unary_union, linemerge import shapely.affinity as affinity from shapely.geometry import box as shply_box +from shapely.geometry import LinearRing, MultiLineString from lxml import etree as ET import ezdxf @@ -2488,13 +2489,14 @@ class Gerber(Geometry): self.app.inform.emit('[success] %s' % _("Done.")) self.app.proc_container.new_text = '' - def buffer(self, distance, join=2, factor=None): + def buffer(self, distance, join=2, factor=None, only_exterior=False): """ - :param distance: If 'factor' is True then distance is the factor - :param join: The type of joining used by the Shapely buffer method. Can be: round, square and bevel - :param factor: True or False (None) - :return: + :param distance: If 'factor' is True then distance is the factor + :param join: The type of joining used by the Shapely buffer method. Can be: round, square and bevel + :param factor: True or False (None) + :param only_exterior: Bool. If True, the LineStrings are buffered only on the outside + :return: None """ self.app.log.debug("parseGerber.Gerber.buffer()") @@ -2513,23 +2515,20 @@ class Gerber(Geometry): if factor is None: def buffer_geom(obj): - if type(obj) is list: - new_obj = [] - for g in obj: + new_obj = [] + try: + work_geo = obj.geoms if isinstance(obj, (MultiPolygon, MultiLineString)) else obj + for g in work_geo: new_obj.append(buffer_geom(g)) - return new_obj - else: + except TypeError: try: - self.el_count += 1 - disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100])) - if self.old_disp_number < disp_number <= 100: - self.app.proc_container.update_view_text(' %d%%' % disp_number) - self.old_disp_number = disp_number - - return obj.buffer(distance, resolution=int(self.steps_per_circle), join_style=join) - + new_obj = obj.buffer(distance, resolution=self.steps_per_circle, join_style=join) + if isinstance(obj, (LinearRing, LineString)) and only_exterior is True: + new_obj = new_obj.exterior except AttributeError: - return obj + new_obj = obj + + return new_obj self.solid_geometry = flatten_shapely_geometry(buffer_geom(self.solid_geometry)) diff --git a/app_Main.py b/app_Main.py index f71ac666..6ac3c9f2 100644 --- a/app_Main.py +++ b/app_Main.py @@ -623,7 +623,7 @@ class App(QtCore.QObject): # ########################################################################################################### 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', + 'aligndrill', 'aligndrillgrid', 'bbox', 'buffer', 'clear', 'cncjob', 'cutout', 'del', 'drillcncjob', 'export_dxf', 'edxf', 'export_excellon', 'export_exc', 'export_gcode', 'export_gerber', 'export_svg', 'ext', 'exteriors', 'follow', @@ -653,9 +653,9 @@ class App(QtCore.QObject): 'axisoffset', 'box', 'center_x', 'center_y', 'columns', 'combine', 'connect', 'contour', 'default', 'depthperpass', 'dia', 'diatol', 'dist', 'drilled_dias', 'drillz', 'dpp', - 'dwelltime', 'extracut_length', 'endxy', 'enz', 'f', 'feedrate', + 'dwelltime', 'extracut_length', 'endxy', 'enz', 'f', 'factor', 'feedrate', 'feedrate_z', 'GRBL_11', 'GRBL_laser', 'gridoffsety', 'gridx', 'gridy', - 'has_offset', 'holes', 'hpgl', 'iso_type', 'margin', 'marlin', 'method', + 'has_offset', 'holes', 'hpgl', 'iso_type', 'join', 'margin', 'marlin', 'method', 'milled_dias', 'minoffset', 'name', 'offset', 'opt_type', 'order', 'outname', 'overlap', 'obj_name', 'passes', 'postamble', 'pp', 'ppname_e', 'ppname_g', 'preamble', 'radius', 'ref', 'rest', 'rows', 'shellvar_', 'scale_factor', diff --git a/camlib.py b/camlib.py index 993371f6..62ac10b3 100644 --- a/camlib.py +++ b/camlib.py @@ -2604,12 +2604,13 @@ class Geometry(object): # self.solid_geometry = affinity.skew(self.solid_geometry, angle_x, angle_y, # origin=(px, py)) - def buffer(self, distance, join, factor): + def buffer(self, distance, join, factor, only_exterior=False): """ - :param distance: if 'factor' is True then distance is the factor - :param join: The kind of join used by the shapely buffer method: round, square or bevel - :param factor: True or False (None) + :param distance: if 'factor' is True then distance is the scale factor for each geometric element + :param join: The kind of join used by the shapely buffer method: round, square or bevel + :param factor: True or False (None) + :param only_exterior: Bool. If True, the LineStrings are buffered only on the outside :return: """ @@ -2619,58 +2620,35 @@ class Geometry(object): return def buffer_geom(obj): - if type(obj) is list: - new_obj = [] - for g in obj: + new_obj = [] + try: + work_geo = obj.geoms if isinstance(obj, (MultiPolygon, MultiLineString)) else obj + for g in work_geo: new_obj.append(buffer_geom(g)) - return new_obj - else: + except TypeError: try: - self.el_count += 1 - disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100])) - if self.old_disp_number < disp_number <= 100: - self.app.proc_container.update_view_text(' %d%%' % disp_number) - self.old_disp_number = disp_number - - if factor is None: - return obj.buffer(distance, resolution=self.geo_steps_per_circle, join_style=join) + if factor is None or factor is False or factor == 0: + new_obj = obj.buffer(distance, resolution=self.geo_steps_per_circle, join_style=join) + if isinstance(obj, (LinearRing, LineString)) and only_exterior is True: + new_obj = new_obj.exterior else: - return affinity.scale(obj, xfact=distance, yfact=distance, origin='center') + new_obj = affinity.scale(obj, xfact=distance, yfact=distance, origin='center') except AttributeError: - return obj + new_obj = obj + + return new_obj try: if self.multigeo is True: for tool in self.tools: - # variables to display the percentage of work done - self.geo_len = 0 - try: - self.geo_len += len(self.tools[tool]['solid_geometry']) - except TypeError: - self.geo_len += 1 - self.old_disp_number = 0 - self.el_count = 0 - res = buffer_geom(self.tools[tool]['solid_geometry']) self.tools[tool]['solid_geometry'] = flatten_shapely_geometry(res) - # variables to display the percentage of work done - self.geo_len = 0 - try: - self.geo_len = len(self.solid_geometry) - except TypeError: - self.geo_len = 1 - self.old_disp_number = 0 - self.el_count = 0 - self.solid_geometry = buffer_geom(self.solid_geometry) - self.app.inform.emit('[success] %s...' % _('Object was buffered')) except AttributeError: self.app.inform.emit('[ERROR_NOTCL] %s %s' % (_("Failed."), _("No object is selected."))) - self.app.proc_container.new_text = '' - class AttrDict(dict): def __init__(self, *args, **kwargs): diff --git a/tclCommands/TclCommandBuffer.py b/tclCommands/TclCommandBuffer.py new file mode 100644 index 00000000..ea07bab8 --- /dev/null +++ b/tclCommands/TclCommandBuffer.py @@ -0,0 +1,112 @@ +from tclCommands.TclCommand import TclCommand + +import collections +import logging + +import gettext +import appTranslation as fcTranslate +import builtins + +fcTranslate.apply_language('strings') +if '_' not in builtins.__dict__: + _ = gettext.gettext + +log = logging.getLogger('base') + + +class TclCommandBuffer(TclCommand): + """ + Tcl shell command to buffer the object by a distance or to scale each geometric element using the center of + its individual bounding box as a reference. + + example: + # buffer each geometric element at the distance 4.2 in the my_geo Geometry obj + buffer my_geo -distance 4.2 + # scale each geo element by a factor of 4.2 in the my_geo Geometry obj and the join is 2 (square) + buffer my_geo -distance 4.2 -factor True -join 2 + """ + + # List of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon) + aliases = ['buffer'] + + description = '%s %s' % ("--", "Will buffer the geometry of a named object. Does not create a new object.") + + # 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([ + ('dist', float), + ('join', str), + ('factor', str) + ]) + + # array of mandatory options for current Tcl command: required = {'name','outname'} + required = ['name', 'distance'] + + # structured help for current command, args needs to be ordered + help = { + 'main': "Works only on Geometry objects.\n" + "Buffer the object by a distance or to scale each geometric element using \n" + "the center of its individual bounding box as a reference.\n" + "If 'factor' is True(1) then 'dist' is the scale factor for each geometric element.", + 'args': collections.OrderedDict([ + ('name', 'Name of the Geometry object to be buffered. Required.'), + ('dist', 'Distance to which to buffer each geometric element.'), + ('join', 'How two lines join and make a corner: round (1), square (2) or bevel (3). Default is: round'), + ('factor', "If 'factor' is True(1) then 'distance' parameter\n" + "is the scale factor for each geometric element") + + ]), + 'examples': [ + '# buffer each geometric element at the distance 4.2 in the my_geo Geometry obj' + 'buffer my_geo -dist 4.2 ', + '# scale each geo element by a factor of 4.2 in the my_geo Geometry obj', + 'buffer my_geo -dist 4.2 -factor True', + 'buffer my_geo -dist 4.2 -factor 1', + '# scale each geo element by a factor of 4.2 in the my_geo Geometry obj and the join is 2 (square)', + 'buffer my_geo -distance 4.2 -factor True -join 2' + ] + } + + def execute(self, args, unnamed_args): + """ + + :param args: + :param unnamed_args: + :return: + """ + + name = args['name'] + try: + obj_to_buff = self.app.collection.get_by_name(name) + except Exception as e: + self.app.log.error("TclCommandCopperBuffer.execute() --> %s" % str(e)) + self.raise_tcl_error("%s: %s" % (_("Could not retrieve object"), name)) + return "Could not retrieve object: %s" % name + + if obj_to_buff.kind not in ['geometry', 'excellon', 'gerber']: + self.app.log.error("%s: %s %s" % ("The object", name, "can be only of type Geometry, Gerber or Excellon.")) + self.raise_tcl_error( + "%s: %s %s" % ("The object", name, "can be only of type Geometry, Gerber or Excellon.")) + return "%s: %s %s" % ("The object", name, "can be only of type Geometry, Gerber or Excellon.") + + if 'dist' not in args: + self.raise_tcl_error('%s' % _("Expected -dist ")) + return 'fail' + + distance = args['dist'] + if 'join' not in args: + join = 1 + else: + if args['join'] in ['square', 'Square', '2', 2]: + join = 2 + elif args['join'] in ['bevel', 'Bevel', '3', 3]: + join = 3 + else: + join = 1 + factor = bool(eval(str(args['factor']).capitalize())) if 'factor' in args else None + + obj_to_buff.buffer(distance, join, factor, only_exterior=True) diff --git a/tclCommands/TclCommandScale.py b/tclCommands/TclCommandScale.py index c1c69a5c..7a7061d0 100644 --- a/tclCommands/TclCommandScale.py +++ b/tclCommands/TclCommandScale.py @@ -77,7 +77,7 @@ class TclCommandScale(TclCommand): try: obj_to_scale = self.app.collection.get_by_name(name) except Exception as e: - log.error("TclCommandCopperClear.execute() --> %s" % str(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 diff --git a/tclCommands/__init__.py b/tclCommands/__init__.py index f7845495..f7274cc8 100644 --- a/tclCommands/__init__.py +++ b/tclCommands/__init__.py @@ -13,6 +13,7 @@ import tclCommands.TclCommandAlignDrill import tclCommands.TclCommandAlignDrillGrid import tclCommands.TclCommandBbox import tclCommands.TclCommandBounds +import tclCommands.TclCommandBuffer import tclCommands.TclCommandClearShell import tclCommands.TclCommandCncjob import tclCommands.TclCommandCopperClear