فهرست منبع

- Buffer sub-tool in Transform Tool: added the possibility to apply a factor effectively scaling the aperture size thus the copper features sizes
- in Transform Tool adjusted the GUI

Marius Stanciu 6 سال پیش
والد
کامیت
c0ec3b6546
8فایلهای تغییر یافته به همراه267 افزوده شده و 98 حذف شده
  1. 2 0
      FlatCAMApp.py
  2. 2 2
      FlatCAMObj.py
  3. 5 0
      README.md
  4. 17 10
      camlib.py
  5. 18 2
      flatcamGUI/PreferencesUI.py
  6. 17 6
      flatcamParsers/ParseExcellon.py
  7. 130 56
      flatcamParsers/ParseGerber.py
  8. 76 22
      flatcamTools/ToolTransform.py

+ 2 - 0
FlatCAMApp.py

@@ -843,6 +843,7 @@ class App(QtCore.QObject):
             "tools_transform_mirror_reference": False,
             "tools_transform_mirror_point": (0, 0),
             "tools_transform_buffer_dis": 0.0,
+            "tools_transform_buffer_factor": 100.0,
             "tools_transform_buffer_corner": True,
 
             # SolderPaste Tool
@@ -1465,6 +1466,7 @@ class App(QtCore.QObject):
             "tools_transform_mirror_reference": self.ui.tools_defaults_form.tools_transform_group.mirror_reference_cb,
             "tools_transform_mirror_point": self.ui.tools_defaults_form.tools_transform_group.flip_ref_entry,
             "tools_transform_buffer_dis": self.ui.tools_defaults_form.tools_transform_group.buffer_entry,
+            "tools_transform_buffer_factor": self.ui.tools_defaults_form.tools_transform_group.buffer_factor_entry,
             "tools_transform_buffer_corner": self.ui.tools_defaults_form.tools_transform_group.buffer_rounded_cb,
 
             # SolderPaste Dispensing Tool

+ 2 - 2
FlatCAMObj.py

@@ -2215,8 +2215,8 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
         Gerber.skew(self, angle_x=angle_x, angle_y=angle_y, point=point)
         self.replotApertures.emit()
 
-    def buffer(self, distance, join):
-        Gerber.buffer(self, distance=distance, join=join)
+    def buffer(self, distance, join, factor=None):
+        Gerber.buffer(self, distance=distance, join=join, factor=factor)
         self.replotApertures.emit()
 
     def serialize(self):

+ 5 - 0
README.md

@@ -9,6 +9,11 @@ CAD program, and create G-Code for Isolation routing.
 
 =================================================
 
+30.12.2019
+
+- Buffer sub-tool in Transform Tool: added the possibility to apply a factor effectively scaling the aperture size thus the copper features sizes
+- in Transform Tool adjusted the GUI
+
 29.12.2019
 
 - the Apply button text in Preferences is now made red when changes were made and require to be applied

+ 17 - 10
camlib.py

@@ -2118,11 +2118,11 @@ class Geometry(object):
         #     self.solid_geometry = affinity.skew(self.solid_geometry, angle_x, angle_y,
         #                                         origin=(px, py))
 
-    def buffer(self, distance, join):
+    def buffer(self, distance, join, factor):
         """
 
-        :param distance:
-        :param join:
+        :param distance: if 'factor' is True then distance is the factor
+        :param factor: True or False (None)
         :return:
         """
 
@@ -2145,7 +2145,10 @@ class Geometry(object):
                         self.app.proc_container.update_view_text(' %d%%' % disp_number)
                         self.old_disp_number = disp_number
 
-                    return obj.buffer(distance, resolution=self.geo_steps_per_circle, join_style=join)
+                    if factor is None:
+                        return obj.buffer(distance, resolution=self.geo_steps_per_circle, join_style=join)
+                    else:
+                        return affinity.scale(obj, xfact=distance, yfact=distance, origin='center')
                 except AttributeError:
                     return obj
 
@@ -2155,20 +2158,23 @@ class Geometry(object):
                     # variables to display the percentage of work done
                     self.geo_len = 0
                     try:
-                        for __ in self.tools[tool]['solid_geometry']:
-                            self.geo_len += 1
+                        self.geo_len += len(self.tools[tool]['solid_geometry'])
                     except TypeError:
-                        self.geo_len = 1
+                        self.geo_len += 1
                     self.old_disp_number = 0
                     self.el_count = 0
 
-                    self.tools[tool]['solid_geometry'] = buffer_geom(self.tools[tool]['solid_geometry'])
+                    res = buffer_geom(self.tools[tool]['solid_geometry'])
+                    try:
+                        __ = iter(res)
+                        self.tools[tool]['solid_geometry'] = res
+                    except TypeError:
+                        self.tools[tool]['solid_geometry'] = [res]
 
             # variables to display the percentage of work done
             self.geo_len = 0
             try:
-                for __ in self.solid_geometry:
-                    self.geo_len += 1
+                self.geo_len = len(self.solid_geometry)
             except TypeError:
                 self.geo_len = 1
             self.old_disp_number = 0
@@ -2182,6 +2188,7 @@ class Geometry(object):
 
         self.app.proc_container.new_text = ''
 
+
 class AttrDict(dict):
     def __init__(self, *args, **kwargs):
         super(AttrDict, self).__init__(*args, **kwargs)

+ 18 - 2
flatcamGUI/PreferencesUI.py

@@ -6416,6 +6416,23 @@ class ToolsTransformPrefGroupUI(OptionsGroupUI):
         grid0.addWidget(self.buffer_label, 17, 0)
         grid0.addWidget(self.buffer_entry, 17, 1)
 
+        self.buffer_factor_label = QtWidgets.QLabel('%s:' % _("Factor"))
+        self.buffer_factor_label.setToolTip(
+            _("A positive value will create the effect of dilation,\n"
+              "while a negative value will create the effect of erosion.\n"
+              "Each geometry element of the object will be increased\n"
+              "or decreased by the 'factor'.")
+        )
+
+        self.buffer_factor_entry = FCDoubleSpinner(suffix='%')
+        self.buffer_factor_entry.set_range(-100.0000, 1000.0000)
+        self.buffer_factor_entry.set_precision(self.decimals)
+        self.buffer_factor_entry.setWrapping(True)
+        self.buffer_factor_entry.setSingleStep(1)
+
+        grid0.addWidget(self.buffer_factor_label, 18, 0)
+        grid0.addWidget(self.buffer_factor_entry, 18, 1)
+
         self.buffer_rounded_cb = FCCheckBox()
         self.buffer_rounded_cb.setText('%s' % _("Rounded"))
         self.buffer_rounded_cb.setToolTip(
@@ -6425,9 +6442,8 @@ class ToolsTransformPrefGroupUI(OptionsGroupUI):
               "of the buffered shape.")
         )
 
-        grid0.addWidget(self.buffer_rounded_cb, 18, 0, 1, 2)
+        grid0.addWidget(self.buffer_rounded_cb, 19, 0, 1, 2)
 
-        grid0.addWidget(QtWidgets.QLabel(''), 19, 0, 1, 2)
 
         self.layout.addStretch()
 

+ 17 - 6
flatcamParsers/ParseExcellon.py

@@ -1459,11 +1459,11 @@ class Excellon(Geometry):
         self.create_geometry()
         self.app.proc_container.new_text = ''
 
-    def buffer(self, distance, join):
+    def buffer(self, distance, join, factor):
         """
 
-        :param distance:
-        :param join:
+        :param distance: if 'factor' is True then distance is the factor
+        :param factor: True or False (None)
         :return:
         """
         log.debug("flatcamParsers.ParseExcellon.Excellon.buffer()")
@@ -1479,13 +1479,24 @@ class Excellon(Geometry):
                 return new_obj
             else:
                 try:
-                    return obj.buffer(distance, resolution=self.geo_steps_per_circle)
+                    if factor is None:
+                        return obj.buffer(distance, resolution=self.geo_steps_per_circle)
+                    else:
+                        return affinity.scale(obj, xfact=distance, yfact=distance, origin='center')
                 except AttributeError:
                     return obj
 
         # buffer solid_geometry
         for tool, tool_dict in list(self.tools.items()):
-            self.tools[tool]['solid_geometry'] = buffer_geom(tool_dict['solid_geometry'])
-            self.tools[tool]['C'] += distance
+            res = buffer_geom(tool_dict['solid_geometry'])
+            try:
+                __ = iter(res)
+                self.tools[tool]['solid_geometry'] = res
+            except TypeError:
+                self.tools[tool]['solid_geometry'] = [res]
+            if factor is None:
+                self.tools[tool]['C'] += distance
+            else:
+                self.tools[tool]['C'] *= distance
 
         self.create_geometry()

+ 130 - 56
flatcamParsers/ParseGerber.py

@@ -2188,14 +2188,14 @@ class Gerber(Geometry):
         except Exception as e:
             log.debug('camlib.Gerber.rotate() Exception --> %s' % str(e))
             return 'fail'
-        self.app.inform.emit('[success] %s' %
-                             _("Gerber Rotate done."))
+        self.app.inform.emit('[success] %s' % _("Gerber Rotate done."))
         self.app.proc_container.new_text = ''
 
-    def buffer(self, distance, join):
+    def buffer(self, distance, join, factor=None):
         """
 
-        :param distance:
+        :param distance: if 'factor' is True then distance is the factor
+        :param factor: True or False (None)
         :return:
         """
         log.debug("parseGerber.Gerber.buffer()")
@@ -2206,69 +2206,143 @@ class Gerber(Geometry):
         # variables to display the percentage of work done
         self.geo_len = 0
         try:
-            for __ in self.solid_geometry:
-                self.geo_len += 1
-        except TypeError:
+            self.geo_len = len(self.solid_geometry)
+        except (TypeError, ValueError):
             self.geo_len = 1
 
         self.old_disp_number = 0
         self.el_count = 0
 
-        def buffer_geom(obj):
-            if type(obj) is list:
-                new_obj = []
-                for g in obj:
-                    new_obj.append(buffer_geom(g))
-                return new_obj
-            else:
-                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:
+            def buffer_geom(obj):
+                if type(obj) is list:
+                    new_obj = []
+                    for g in obj:
+                        new_obj.append(buffer_geom(g))
+                    return new_obj
+                else:
+                    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=self.steps_per_circle, join_style=join)
-                except AttributeError:
-                    return obj
+                        return obj.buffer(distance, resolution=int(self.steps_per_circle), join_style=join)
 
-        self.solid_geometry = buffer_geom(self.solid_geometry)
+                    except AttributeError:
+                        return obj
 
-        # we need to buffer the geometry stored in the Gerber apertures, too
-        try:
-            for apid in self.apertures:
-                new_geometry = list()
-                if 'geometry' in self.apertures[apid]:
-                    for geo_el in self.apertures[apid]['geometry']:
-                        new_geo_el = dict()
-                        if 'solid' in geo_el:
-                            new_geo_el['solid'] = buffer_geom(geo_el['solid'])
-                        if 'follow' in geo_el:
-                            new_geo_el['follow'] = buffer_geom(geo_el['follow'])
-                        if 'clear' in geo_el:
-                            new_geo_el['clear'] = buffer_geom(geo_el['clear'])
-                        new_geometry.append(new_geo_el)
+            res = buffer_geom(self.solid_geometry)
+            try:
+                __ = iter(res)
+                self.solid_geometry = res
+            except TypeError:
+                self.solid_geometry = [res]
 
-                self.apertures[apid]['geometry'] = deepcopy(new_geometry)
+            # we need to buffer the geometry stored in the Gerber apertures, too
+            try:
+                for apid in self.apertures:
+                    new_geometry = list()
+                    if 'geometry' in self.apertures[apid]:
+                        for geo_el in self.apertures[apid]['geometry']:
+                            new_geo_el = dict()
+                            if 'solid' in geo_el:
+                                new_geo_el['solid'] = buffer_geom(geo_el['solid'])
+                            if 'follow' in geo_el:
+                                new_geo_el['follow'] = geo_el['follow']
+                            if 'clear' in geo_el:
+                                new_geo_el['clear'] = buffer_geom(geo_el['clear'])
+                            new_geometry.append(new_geo_el)
+
+                    self.apertures[apid]['geometry'] = deepcopy(new_geometry)
 
-                try:
-                    if str(self.apertures[apid]['type']) == 'R' or str(self.apertures[apid]['type']) == 'O':
-                        self.apertures[apid]['width'] += (distance * 2)
-                        self.apertures[apid]['height'] += (distance * 2)
-                    elif str(self.apertures[apid]['type']) == 'P':
-                        self.apertures[apid]['diam'] += (distance * 2)
-                        self.apertures[apid]['nVertices'] += (distance * 2)
-                except KeyError:
-                    pass
+                    try:
+                        if str(self.apertures[apid]['type']) == 'R' or str(self.apertures[apid]['type']) == 'O':
+                            self.apertures[apid]['width'] += (distance * 2)
+                            self.apertures[apid]['height'] += (distance * 2)
+                        elif str(self.apertures[apid]['type']) == 'P':
+                            self.apertures[apid]['diam'] += (distance * 2)
+                            self.apertures[apid]['nVertices'] += (distance * 2)
+                    except KeyError:
+                        pass
 
-                try:
-                    if self.apertures[apid]['size'] is not None:
-                        self.apertures[apid]['size'] = float(self.apertures[apid]['size'] + (distance * 2))
-                except KeyError:
-                    pass
-        except Exception as e:
-            log.debug('camlib.Gerber.buffer() Exception --> %s' % str(e))
-            return 'fail'
+                    try:
+                        if self.apertures[apid]['size'] is not None:
+                            self.apertures[apid]['size'] = float(self.apertures[apid]['size'] + (distance * 2))
+                    except KeyError:
+                        pass
+            except Exception as e:
+                log.debug('camlib.Gerber.buffer() Exception --> %s' % str(e))
+                return 'fail'
+        else:
+            try:
+                for apid in self.apertures:
+                    try:
+                        if str(self.apertures[apid]['type']) == 'R' or str(self.apertures[apid]['type']) == 'O':
+                            self.apertures[apid]['width'] *= distance
+                            self.apertures[apid]['height'] *= distance
+                        elif str(self.apertures[apid]['type']) == 'P':
+                            self.apertures[apid]['diam'] *= distance
+                            self.apertures[apid]['nVertices'] *= distance
+                    except KeyError:
+                        pass
+
+                    try:
+                        if self.apertures[apid]['size'] is not None:
+                            self.apertures[apid]['size'] = float(self.apertures[apid]['size']) * distance
+                    except KeyError:
+                        pass
+
+                    new_geometry = list()
+                    if 'geometry' in self.apertures[apid]:
+                        for geo_el in self.apertures[apid]['geometry']:
+                            new_geo_el = dict()
+                            if 'follow' in geo_el:
+                                new_geo_el['follow'] = geo_el['follow']
+                                size = float(self.apertures[apid]['size'])
+                                if isinstance(new_geo_el['follow'], Point):
+                                    if str(self.apertures[apid]['type']) == 'C':
+                                        new_geo_el['solid'] = geo_el['follow'].buffer(
+                                            size / 1.9999,
+                                            resolution=int(self.steps_per_circle)
+                                        )
+                                    elif str(self.apertures[apid]['type']) == 'R':
+                                        width = self.apertures[apid]['width']
+                                        height = self.apertures[apid]['height']
+                                        minx = new_geo_el['follow'].x - width / 2
+                                        maxx = new_geo_el['follow'].x + width / 2
+                                        miny = new_geo_el['follow'].y - height / 2
+                                        maxy = new_geo_el['follow'].y + height / 2
+
+                                        geo_p = shply_box(minx, miny, maxx, maxy)
+                                        new_geo_el['solid'] = geo_p
+                                    else:
+                                        log.debug("flatcamParsers.ParseGerber.Gerber.buffer() --> "
+                                                  "ap type not supported")
+                                else:
+                                    new_geo_el['solid'] = geo_el['follow'].buffer(
+                                        size/1.9999,
+                                        resolution=int(self.steps_per_circle)
+                                    )
+                            if 'clear' in geo_el:
+                                new_geo_el['clear'] = geo_el['clear']
+                            new_geometry.append(new_geo_el)
+
+                    self.apertures[apid]['geometry'] = deepcopy(new_geometry)
+            except Exception as e:
+                log.debug('camlib.Gerber.buffer() Exception --> %s' % str(e))
+                return 'fail'
+
+            # make the new solid_geometry
+            new_solid_geo = list()
+            for apid in self.apertures:
+                if 'geometry' in self.apertures[apid]:
+                    new_solid_geo += [geo_el['solid'] for geo_el in self.apertures[apid]['geometry']]
+
+            self.solid_geometry = MultiPolygon(new_solid_geo)
+            self.solid_geometry = self.solid_geometry.buffer(0.000001)
+            self.solid_geometry = self.solid_geometry.buffer(-0.000001)
 
         self.app.inform.emit('[success] %s' % _("Gerber Buffer done."))
         self.app.proc_container.new_text = ''

+ 76 - 22
flatcamTools/ToolTransform.py

@@ -89,7 +89,10 @@ class ToolTransform(FlatCAMTool):
         grid0.addWidget(self.rotate_entry, 1, 1)
         grid0.addWidget(self.rotate_button, 1, 2)
 
-        grid0.addWidget(QtWidgets.QLabel(''), 2, 0)
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 2, 0, 1, 3)
 
         # ## Skew Title
         skew_title_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.skewName)
@@ -139,7 +142,10 @@ class ToolTransform(FlatCAMTool):
         grid0.addWidget(self.skewy_entry, 5, 1)
         grid0.addWidget(self.skewy_button, 5, 2)
 
-        grid0.addWidget(QtWidgets.QLabel(''), 6, 0)
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 6, 0, 1, 3)
 
         # ## Scale Title
         scale_title_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.scaleName)
@@ -208,7 +214,11 @@ class ToolTransform(FlatCAMTool):
 
         grid0.addWidget(self.scale_link_cb, 10, 0)
         grid0.addWidget(self.scale_zero_ref_cb, 10, 1)
-        grid0.addWidget(QtWidgets.QLabel(''), 11, 0)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 11, 0, 1, 3)
 
         # ## Offset Title
         offset_title_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.offsetName)
@@ -256,7 +266,10 @@ class ToolTransform(FlatCAMTool):
         grid0.addWidget(self.offy_entry, 14, 1)
         grid0.addWidget(self.offy_button, 14, 2)
 
-        grid0.addWidget(QtWidgets.QLabel(''), 15, 0, 1, 3)
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 15, 0, 1, 3)
 
         # ## Flip Title
         flip_title_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.flipName)
@@ -323,7 +336,10 @@ class ToolTransform(FlatCAMTool):
 
         grid0.addWidget(self.flip_ref_button, 20, 0, 1, 3)
 
-        grid0.addWidget(QtWidgets.QLabel(''), 21, 0, 1, 3)
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 21, 0, 1, 3)
 
         # ## Buffer Title
         buffer_title_label = QtWidgets.QLabel("<font size=3><b>%s</b></font>" % self.bufferName)
@@ -343,13 +359,11 @@ class ToolTransform(FlatCAMTool):
         self.buffer_entry.setWrapping(True)
         self.buffer_entry.set_range(-9999.9999, 9999.9999)
 
-        # self.rotate_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
-
         self.buffer_button = FCButton()
-        self.buffer_button.set_value(_("Buffer"))
+        self.buffer_button.set_value(_("Buffer D"))
         self.buffer_button.setToolTip(
             _("Create the buffer effect on each geometry,\n"
-              "element from the selected object.")
+              "element from the selected object, using the distance.")
         )
         self.buffer_button.setMinimumWidth(90)
 
@@ -357,8 +371,33 @@ class ToolTransform(FlatCAMTool):
         grid0.addWidget(self.buffer_entry, 23, 1)
         grid0.addWidget(self.buffer_button, 23, 2)
 
-        self.buffer_rounded_cb = FCCheckBox()
-        self.buffer_rounded_cb.setText('%s' % _("Rounded"))
+        self.buffer_factor_label = QtWidgets.QLabel('%s:' % _("Factor"))
+        self.buffer_factor_label.setToolTip(
+            _("A positive value will create the effect of dilation,\n"
+              "while a negative value will create the effect of erosion.\n"
+              "Each geometry element of the object will be increased\n"
+              "or decreased by the 'factor'.")
+        )
+
+        self.buffer_factor_entry = FCDoubleSpinner(suffix='%')
+        self.buffer_factor_entry.set_range(-100.0000, 1000.0000)
+        self.buffer_factor_entry.set_precision(self.decimals)
+        self.buffer_factor_entry.setWrapping(True)
+        self.buffer_factor_entry.setSingleStep(1)
+
+        self.buffer_factor_button = FCButton()
+        self.buffer_factor_button.set_value(_("Buffer F"))
+        self.buffer_factor_button.setToolTip(
+            _("Create the buffer effect on each geometry,\n"
+              "element from the selected object, using the factor.")
+        )
+        self.buffer_factor_button.setMinimumWidth(90)
+
+        grid0.addWidget(self.buffer_factor_label, 24, 0)
+        grid0.addWidget(self.buffer_factor_entry, 24, 1)
+        grid0.addWidget(self.buffer_factor_button, 24, 2)
+
+        self.buffer_rounded_cb = FCCheckBox('%s' % _("Rounded"))
         self.buffer_rounded_cb.setToolTip(
             _("If checked then the buffer will surround the buffered shape,\n"
               "every corner will be rounded.\n"
@@ -366,9 +405,9 @@ class ToolTransform(FlatCAMTool):
               "of the buffered shape.")
         )
 
-        grid0.addWidget(self.buffer_rounded_cb, 24, 0, 1, 3)
+        grid0.addWidget(self.buffer_rounded_cb, 25, 0, 1, 3)
 
-        grid0.addWidget(QtWidgets.QLabel(''), 25, 0, 1, 3)
+        grid0.addWidget(QtWidgets.QLabel(''), 26, 0, 1, 3)
 
         self.transform_lay.addStretch()
 
@@ -383,7 +422,8 @@ class ToolTransform(FlatCAMTool):
         self.flipx_button.clicked.connect(self.on_flipx)
         self.flipy_button.clicked.connect(self.on_flipy)
         self.flip_ref_button.clicked.connect(self.on_flip_add_coords)
-        self.buffer_button.clicked.connect(self.on_buffer)
+        self.buffer_button.clicked.connect(self.on_buffer_by_distance)
+        self.buffer_factor_button.clicked.connect(self.on_buffer_by_factor)
 
         # self.rotate_entry.returnPressed.connect(self.on_rotate)
         # self.skewx_entry.returnPressed.connect(self.on_skewx)
@@ -392,7 +432,7 @@ class ToolTransform(FlatCAMTool):
         # self.scaley_entry.returnPressed.connect(self.on_scaley)
         # self.offx_entry.returnPressed.connect(self.on_offx)
         # self.offy_entry.returnPressed.connect(self.on_offy)
-        # self.buffer_entry.returnPressed.connect(self.on_buffer)
+        # self.buffer_entry.returnPressed.connect(self.on_buffer_by_distance)
 
     def run(self, toggle=True):
         self.app.report_usage("ToolTransform()")
@@ -486,6 +526,11 @@ class ToolTransform(FlatCAMTool):
         else:
             self.buffer_entry.set_value(0.0)
 
+        if self.app.defaults["tools_transform_buffer_factor"]:
+            self.buffer_factor_entry.set_value(self.app.defaults["tools_transform_buffer_factor"])
+        else:
+            self.buffer_factor_entry.set_value(100.0)
+
         if self.app.defaults["tools_transform_buffer_corner"]:
             self.buffer_rounded_cb.set_value(self.app.defaults["tools_transform_buffer_corner"])
         else:
@@ -589,13 +634,23 @@ class ToolTransform(FlatCAMTool):
         self.app.worker_task.emit({'fcn': self.on_offset, 'params': [axis, value]})
         return
 
-    def on_buffer(self):
+    def on_buffer_by_distance(self):
         value = self.buffer_entry.get_value()
         join = 1 if self.buffer_rounded_cb.get_value() else 2
 
         self.app.worker_task.emit({'fcn': self.on_buffer_action, 'params': [value, join]})
         return
 
+    def on_buffer_by_factor(self):
+        value = self.buffer_factor_entry.get_value() / 100.0
+        join = 1 if self.buffer_rounded_cb.get_value() else 2
+
+        # tell the buffer method to use the factor
+        factor = True
+
+        self.app.worker_task.emit({'fcn': self.on_buffer_action, 'params': [value, join, factor]})
+        return
+
     def on_rotate_action(self, num):
         obj_list = self.app.collection.get_selected()
         xminlist = []
@@ -604,8 +659,7 @@ class ToolTransform(FlatCAMTool):
         ymaxlist = []
 
         if not obj_list:
-            self.app.inform.emit('[WARNING_NOTCL] %s' %
-                                 _("No object selected. Please Select an object to rotate!"))
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("No object selected. Please Select an object to rotate!"))
             return
         else:
             with self.app.proc_container.new(_("Appying Rotate")):
@@ -874,7 +928,7 @@ class ToolTransform(FlatCAMTool):
                                          (_("Due of"), str(e),  _("action was not executed.")))
                     return
 
-    def on_buffer_action(self, value, join):
+    def on_buffer_action(self, value, join, factor=None):
         obj_list = self.app.collection.get_selected()
 
         if not obj_list:
@@ -887,17 +941,17 @@ class ToolTransform(FlatCAMTool):
                         if isinstance(sel_obj, FlatCAMCNCjob):
                             self.app.inform.emit(_("CNCJob objects can't be buffered."))
                         elif sel_obj.kind.lower() == 'gerber':
-                            sel_obj.buffer(value, join)
+                            sel_obj.buffer(value, join, factor)
                             sel_obj.source_file = self.app.export_gerber(obj_name=sel_obj.options['name'],
                                                                          filename=None, local_use=sel_obj,
                                                                          use_thread=False)
                         elif sel_obj.kind.lower() == 'excellon':
-                            sel_obj.buffer(value, join)
+                            sel_obj.buffer(value, join, factor)
                             sel_obj.source_file = self.app.export_excellon(obj_name=sel_obj.options['name'],
                                                                            filename=None, local_use=sel_obj,
                                                                            use_thread=False)
                         elif sel_obj.kind.lower() == 'geometry':
-                            sel_obj.buffer(value, join)
+                            sel_obj.buffer(value, join, factor)
 
                         self.app.object_changed.emit(sel_obj)
                         sel_obj.plot()