Browse Source

- Gerber Editor: added Add Pad Array tool
- Gerber Editor: in Add Pad Array tool, if the pad is not circular type, for circular array the pad will be rotated to match the array angle

Marius Stanciu 6 years ago
parent
commit
cc6ff98529
3 changed files with 412 additions and 51 deletions
  1. 2 0
      README.md
  2. 386 43
      flatcamEditors/FlatCAMGrbEditor.py
  3. 24 8
      flatcamGUI/FlatCAMGUI.py

+ 2 - 0
README.md

@@ -15,6 +15,8 @@ CAD program, and create G-Code for Isolation routing.
 - Gerber Editor: added Transformation Tool and Rotation key shortcut
 - in all Editors, manually deactivating a button in the editor toolbar will automatically select the 'Select' button
 - fixed Excellon Editor selection: when a tool is selected in Tools Table, all the drills belonging to that tool are selected. When a drill is selected on canvas, the associated tool will be selected without automatically selecting all other drills with same tool
+- Gerber Editor: added Add Pad Array tool
+- Gerber Editor: in Add Pad Array tool, if the pad is not circular type, for circular array the pad will be rotated to match the array angle
 
 10.04.2019
 

+ 386 - 43
flatcamEditors/FlatCAMGrbEditor.py

@@ -109,42 +109,223 @@ class FCPad(FCShapeTool):
 
 class FCPadArray(FCShapeTool):
     """
-    Resulting type: Polygon
+    Resulting type: MultiPolygon
     """
 
     def __init__(self, draw_app):
         DrawTool.__init__(self, draw_app)
-        self.name = 'pad_array'
+        self.name = 'array'
+        self.draw_app = draw_app
 
-        self.start_msg = _("Click on 1st corner ...")
+        self.storage_obj = self.draw_app.storage_dict[self.draw_app.last_aperture_selected]['solid_geometry']
+        self.radius = float(self.draw_app.storage_dict[self.draw_app.last_aperture_selected]['size']) / 2
 
-    def click(self, point):
-        self.points.append(point)
+        # if those cause KeyError exception it means that the aperture type is not 'R'. Only 'R' type has those keys
+        try:
+            self.half_width = float(self.draw_app.storage_dict[self.draw_app.last_aperture_selected]['width']) / 2
+        except KeyError:
+            pass
+        try:
+            self.half_height = float(self.draw_app.storage_dict[self.draw_app.last_aperture_selected]['height']) / 2
+        except KeyError:
+            pass
 
-        if len(self.points) == 1:
-            return "Click on opposite corner to complete ..."
+        self.draw_app.array_frame.show()
+
+        self.selected_size = None
+        self.pad_axis = 'X'
+        self.pad_array = 'linear'
+        self.pad_array_size = None
+        self.pad_pitch = None
+        self.pad_linear_angle = None
+
+        self.pad_angle = None
+        self.pad_direction = None
+        self.pad_radius = None
+
+        self.origin = None
+        self.destination = None
+        self.flag_for_circ_array = None
+
+        self.last_dx = 0
+        self.last_dy = 0
+
+        self.pt = []
+
+        self.draw_app.app.inform.emit(self.start_msg)
+
+        try:
+            self.selected_size = self.draw_app.tool2tooldia[self.draw_app.last_aperture_selected]
+        except KeyError:
+            self.draw_app.app.inform.emit(_("[WARNING_NOTCL] To add an Pad Array first select a tool in Tool Table"))
+            return
+
+        geo = self.utility_geometry(data=(self.draw_app.snap_x, self.draw_app.snap_y), static=True)
+
+        if isinstance(geo, DrawToolShape) and geo.geo is not None:
+            self.draw_app.draw_utility_geometry(geo=geo)
 
-        if len(self.points) == 2:
+        self.draw_app.app.inform.emit(_("Click on target location ..."))
+
+        # Switch notebook to Selected page
+        self.draw_app.app.ui.notebook.setCurrentWidget(self.draw_app.app.ui.selected_tab)
+
+    def click(self, point):
+
+        if self.pad_array == 'Linear':
             self.make()
-            return "Done."
+            return
+        else:
+            if self.flag_for_circ_array is None:
+                self.draw_app.in_action = True
+                self.pt.append(point)
 
-        return ""
+                self.flag_for_circ_array = True
+                self.set_origin(point)
+                self.draw_app.app.inform.emit(_("Click on the Pad Circular Array Start position"))
+            else:
+                self.destination = point
+                self.make()
+                self.flag_for_circ_array = None
+                return
 
-    def utility_geometry(self, data=None):
-        if len(self.points) == 1:
-            p1 = self.points[0]
-            p2 = data
-            return DrawToolUtilityShape(LinearRing([p1, (p2[0], p1[1]), p2, (p1[0], p2[1])]))
+    def set_origin(self, origin):
+        self.origin = origin
 
-        return None
+    def utility_geometry(self, data=None, static=None):
+        self.pad_axis = self.draw_app.pad_axis_radio.get_value()
+        self.pad_direction = self.draw_app.pad_direction_radio.get_value()
+        self.pad_array = self.draw_app.array_type_combo.get_value()
+        try:
+            self.pad_array_size = int(self.draw_app.pad_array_size_entry.get_value())
+            try:
+                self.pad_pitch = float(self.draw_app.pad_pitch_entry.get_value())
+                self.pad_linear_angle = float(self.draw_app.linear_angle_spinner.get_value())
+                self.pad_angle = float(self.draw_app.pad_angle_entry.get_value())
+            except TypeError:
+                self.draw_app.app.inform.emit(
+                    _("[ERROR_NOTCL] The value is not Float. Check for comma instead of dot separator."))
+                return
+        except Exception as e:
+            self.draw_app.app.inform.emit(_("[ERROR_NOTCL] The value is mistyped. Check the value."))
+            return
+
+        if self.pad_array == 'Linear':
+            if data[0] is None and data[1] is None:
+                dx = self.draw_app.x
+                dy = self.draw_app.y
+            else:
+                dx = data[0]
+                dy = data[1]
+
+            geo_list = []
+            geo = None
+            self.points = [dx, dy]
+
+            for item in range(self.pad_array_size):
+                if self.pad_axis == 'X':
+                    geo = self.util_shape(((dx + (self.pad_pitch * item)), dy))
+                if self.pad_axis == 'Y':
+                    geo = self.util_shape((dx, (dy + (self.pad_pitch * item))))
+                if self.pad_axis == 'A':
+                    x_adj = self.pad_pitch * math.cos(math.radians(self.pad_linear_angle))
+                    y_adj = self.pad_pitch * math.sin(math.radians(self.pad_linear_angle))
+                    geo = self.util_shape(
+                        ((dx + (x_adj * item)), (dy + (y_adj * item)))
+                    )
+
+                if static is None or static is False:
+                    geo_list.append(affinity.translate(geo, xoff=(dx - self.last_dx), yoff=(dy - self.last_dy)))
+                else:
+                    geo_list.append(geo)
+            # self.origin = data
+
+            self.last_dx = dx
+            self.last_dy = dy
+            return DrawToolUtilityShape(geo_list)
+        else:
+            if data[0] is None and data[1] is None:
+                cdx = self.draw_app.x
+                cdy = self.draw_app.y
+            else:
+                cdx = data[0]
+                cdy = data[1]
+
+            if len(self.pt) > 0:
+                temp_points = [x for x in self.pt]
+                temp_points.append([cdx, cdy])
+                return DrawToolUtilityShape(LineString(temp_points))
+
+    def util_shape(self, point):
+        if point[0] is None and point[1] is None:
+            point_x = self.draw_app.x
+            point_y = self.draw_app.y
+        else:
+            point_x = point[0]
+            point_y = point[1]
+
+        ap_type = self.draw_app.storage_dict[self.draw_app.last_aperture_selected]['type']
+        if ap_type == 'C':
+            center = Point([point_x, point_y])
+            return center.buffer(self.radius)
+        elif ap_type == 'R':
+            p1 = (point_x - self.half_width, point_y - self.half_height)
+            p2 = (point_x + self.half_width, point_y - self.half_height)
+            p3 = (point_x + self.half_width, point_y + self.half_height)
+            p4 = (point_x - self.half_width, point_y + self.half_height)
+            return Polygon([p1, p2, p3, p4, p1])
+        else:
+            self.draw_app.app.inform.emit(_("Incompatible aperture type. Select an aperture with type 'C' or 'R'."))
+            return None
 
     def make(self):
-        p1 = self.points[0]
-        p2 = self.points[1]
-        # self.geometry = LinearRing([p1, (p2[0], p1[1]), p2, (p1[0], p2[1])])
-        self.geometry = DrawToolShape(Polygon([p1, (p2[0], p1[1]), p2, (p1[0], p2[1])]))
+        self.geometry = []
+        geo = None
+
+        self.draw_app.current_storage = self.storage_obj
+
+        if self.pad_array == 'Linear':
+            for item in range(self.pad_array_size):
+                if self.pad_axis == 'X':
+                    geo = self.util_shape(((self.points[0] + (self.pad_pitch * item)), self.points[1]))
+                if self.pad_axis == 'Y':
+                    geo = self.util_shape((self.points[0], (self.points[1] + (self.pad_pitch * item))))
+                if self.pad_axis == 'A':
+                    x_adj = self.pad_pitch * math.cos(math.radians(self.pad_linear_angle))
+                    y_adj = self.pad_pitch * math.sin(math.radians(self.pad_linear_angle))
+                    geo = self.util_shape(
+                        ((self.points[0] + (x_adj * item)), (self.points[1] + (y_adj * item)))
+                    )
+
+                self.geometry.append(DrawToolShape(geo))
+        else:
+            if (self.pad_angle * self.pad_array_size) > 360:
+                self.draw_app.app.inform.emit(_("[WARNING_NOTCL] Too many Pads for the selected spacing angle."))
+                return
+
+            radius = distance(self.destination, self.origin)
+            initial_angle = math.asin((self.destination[1] - self.origin[1]) / radius)
+            for i in range(self.pad_array_size):
+                angle_radians = math.radians(self.pad_angle * i)
+                if self.pad_direction == 'CW':
+                    x = self.origin[0] + radius * math.cos(-angle_radians + initial_angle)
+                    y = self.origin[1] + radius * math.sin(-angle_radians + initial_angle)
+                else:
+                    x = self.origin[0] + radius * math.cos(angle_radians + initial_angle)
+                    y = self.origin[1] + radius * math.sin(angle_radians + initial_angle)
+
+                geo = self.util_shape((x, y))
+                if self.pad_direction == 'CW':
+                    geo = affinity.rotate(geo, angle=(math.pi - angle_radians), use_radians=True)
+                else:
+                    geo = affinity.rotate(geo, angle=angle_radians, use_radians=True)
+
+                self.geometry.append(DrawToolShape(geo))
         self.complete = True
-        self.draw_app.app.inform.emit(_("[success] Done. Rectangle completed."))
+        self.draw_app.app.inform.emit(_("[success] Done. Pad Array added."))
+        self.draw_app.in_action = True
+        self.draw_app.array_frame.hide()
+        return
 
 
 class FCRegion(FCShapeTool):
@@ -322,8 +503,7 @@ class FCApertureMove(FCShapeTool):
 
         for index in self.draw_app.apertures_table.selectedIndexes():
             row = index.row()
-            # on column 1 in tool tables we hold the diameters, and we retrieve them as strings
-            # therefore below we convert to float
+            # on column 1 in tool tables we hold the aperture codes, and we retrieve them as strings
             aperture_on_row = self.draw_app.apertures_table.item(row, 1).text()
             self.selected_apertures.append(aperture_on_row)
 
@@ -756,6 +936,133 @@ class FlatCAMGrbEditor(QtCore.QObject):
         self.scale_button = QtWidgets.QPushButton(_("Scale"))
         hlay_scale.addWidget(self.scale_button)
 
+        # add a frame and inside add a vertical box layout. Inside this vbox layout I add
+        # all the add Pad array  widgets
+        # this way I can hide/show the frame
+        self.array_frame = QtWidgets.QFrame()
+        self.array_frame.setContentsMargins(0, 0, 0, 0)
+        self.custom_box.addWidget(self.array_frame)
+        self.array_box = QtWidgets.QVBoxLayout()
+        self.array_box.setContentsMargins(0, 0, 0, 0)
+        self.array_frame.setLayout(self.array_box)
+
+        #### Add Pad Array ####
+        self.emptyarray_label = QtWidgets.QLabel('')
+        self.array_box.addWidget(self.emptyarray_label)
+
+        self.padarray_label = QtWidgets.QLabel('<b>%s</b>' % _("Add Pad Array"))
+        self.padarray_label.setToolTip(
+            _("Add an array of pads (linear or circular array)")
+        )
+        self.array_box.addWidget(self.padarray_label)
+
+        self.array_type_combo = FCComboBox()
+        self.array_type_combo.setToolTip(
+           _( "Select the type of pads array to create.\n"
+            "It can be Linear X(Y) or Circular")
+        )
+        self.array_type_combo.addItem(_("Linear"))
+        self.array_type_combo.addItem(_("Circular"))
+
+        self.array_box.addWidget(self.array_type_combo)
+
+        self.array_form = QtWidgets.QFormLayout()
+        self.array_box.addLayout(self.array_form)
+
+        self.pad_array_size_label = QtWidgets.QLabel(_('Nr of pads:'))
+        self.pad_array_size_label.setToolTip(
+            _("Specify how many pads to be in the array.")
+        )
+        self.pad_array_size_label.setFixedWidth(100)
+
+        self.pad_array_size_entry = LengthEntry()
+        self.array_form.addRow(self.pad_array_size_label, self.pad_array_size_entry)
+
+        self.array_linear_frame = QtWidgets.QFrame()
+        self.array_linear_frame.setContentsMargins(0, 0, 0, 0)
+        self.array_box.addWidget(self.array_linear_frame)
+        self.linear_box = QtWidgets.QVBoxLayout()
+        self.linear_box.setContentsMargins(0, 0, 0, 0)
+        self.array_linear_frame.setLayout(self.linear_box)
+
+        self.linear_form = QtWidgets.QFormLayout()
+        self.linear_box.addLayout(self.linear_form)
+
+        self.pad_axis_label = QtWidgets.QLabel(_('Direction:'))
+        self.pad_axis_label.setToolTip(
+            _("Direction on which the linear array is oriented:\n"
+            "- 'X' - horizontal axis \n"
+            "- 'Y' - vertical axis or \n"
+            "- 'Angle' - a custom angle for the array inclination")
+        )
+        self.pad_axis_label.setFixedWidth(100)
+
+        self.pad_axis_radio = RadioSet([{'label': 'X', 'value': 'X'},
+                                          {'label': 'Y', 'value': 'Y'},
+                                          {'label': _('Angle'), 'value': 'A'}])
+        self.pad_axis_radio.set_value('X')
+        self.linear_form.addRow(self.pad_axis_label, self.pad_axis_radio)
+
+        self.pad_pitch_label = QtWidgets.QLabel(_('Pitch:'))
+        self.pad_pitch_label.setToolTip(
+            _("Pitch = Distance between elements of the array.")
+        )
+        self.pad_pitch_label.setFixedWidth(100)
+
+        self.pad_pitch_entry = LengthEntry()
+        self.linear_form.addRow(self.pad_pitch_label, self.pad_pitch_entry)
+
+        self.linear_angle_label = QtWidgets.QLabel(_('Angle:'))
+        self.linear_angle_label.setToolTip(
+           _( "Angle at which the linear array is placed.\n"
+            "The precision is of max 2 decimals.\n"
+            "Min value is: -359.99 degrees.\n"
+            "Max value is:  360.00 degrees.")
+        )
+        self.linear_angle_label.setFixedWidth(100)
+
+        self.linear_angle_spinner = FCDoubleSpinner()
+        self.linear_angle_spinner.set_precision(2)
+        self.linear_angle_spinner.setRange(-359.99, 360.00)
+        self.linear_form.addRow(self.linear_angle_label, self.linear_angle_spinner)
+
+        self.array_circular_frame = QtWidgets.QFrame()
+        self.array_circular_frame.setContentsMargins(0, 0, 0, 0)
+        self.array_box.addWidget(self.array_circular_frame)
+        self.circular_box = QtWidgets.QVBoxLayout()
+        self.circular_box.setContentsMargins(0, 0, 0, 0)
+        self.array_circular_frame.setLayout(self.circular_box)
+
+        self.pad_direction_label = QtWidgets.QLabel(_('Direction:'))
+        self.pad_direction_label.setToolTip(
+           _( "Direction for circular array."
+            "Can be CW = clockwise or CCW = counter clockwise.")
+        )
+        self.pad_direction_label.setFixedWidth(100)
+
+        self.circular_form = QtWidgets.QFormLayout()
+        self.circular_box.addLayout(self.circular_form)
+
+        self.pad_direction_radio = RadioSet([{'label': 'CW', 'value': 'CW'},
+                                               {'label': 'CCW.', 'value': 'CCW'}])
+        self.pad_direction_radio.set_value('CW')
+        self.circular_form.addRow(self.pad_direction_label, self.pad_direction_radio)
+
+        self.pad_angle_label = QtWidgets.QLabel(_('Angle:'))
+        self.pad_angle_label.setToolTip(
+            _("Angle at which each element in circular array is placed.")
+        )
+        self.pad_angle_label.setFixedWidth(100)
+
+        self.pad_angle_entry = LengthEntry()
+        self.circular_form.addRow(self.pad_angle_label, self.pad_angle_entry)
+
+        self.array_circular_frame.hide()
+
+        self.linear_angle_spinner.hide()
+        self.linear_angle_label.hide()
+
+        self.array_frame.hide()
 
         self.custom_box.addStretch()
 
@@ -765,6 +1072,8 @@ class FlatCAMGrbEditor(QtCore.QObject):
                        "constructor": FCApertureSelect},
             "pad": {"button": self.app.ui.grb_add_pad_btn,
                               "constructor": FCPad},
+            "array": {"button": self.app.ui.add_pad_ar_btn,
+                    "constructor": FCPadArray},
             "track": {"button": self.app.ui.grb_add_track_btn,
                               "constructor": FCTrack},
             "region": {"button": self.app.ui.grb_add_region_btn,
@@ -795,7 +1104,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
         # store here the plot promises, if empty the delayed plot will be activated
         self.grb_plot_promises = []
 
-        # dictionary to store the tool_row and diameters in Tool_table
+        # dictionary to store the tool_row and aperture codes in Tool_table
         # it will be updated everytime self.build_ui() is called
         self.olddia_newdia = {}
 
@@ -887,7 +1196,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
         self.buffer_button.clicked.connect(self.on_buffer)
         self.scale_button.clicked.connect(self.on_scale)
 
-        self.app.ui.delete_drill_btn.triggered.connect(self.on_delete_btn)
+        self.app.ui.aperture_delete_btn.triggered.connect(self.on_delete_btn)
         self.name_entry.returnPressed.connect(self.on_name_activate)
 
         self.aptype_cb.currentIndexChanged[str].connect(self.on_aptype_changed)
@@ -897,6 +1206,8 @@ class FlatCAMGrbEditor(QtCore.QObject):
         self.apertures_table.cellPressed.connect(self.on_row_selected)
 
         self.app.ui.grb_add_pad_menuitem.triggered.connect(self.on_pad_add)
+        self.app.ui.grb_add_pad_array_menuitem.triggered.connect(self.on_pad_add_array)
+
         self.app.ui.grb_add_track_menuitem.triggered.connect(self.on_track_add)
         self.app.ui.grb_add_region_menuitem.triggered.connect(self.on_region_add)
 
@@ -909,6 +1220,9 @@ class FlatCAMGrbEditor(QtCore.QObject):
 
         self.app.ui.grb_move_menuitem.triggered.connect(self.on_move_button)
 
+        self.array_type_combo.currentIndexChanged.connect(self.on_array_type_combo)
+        self.pad_axis_radio.activated_custom.connect(self.on_linear_angle_radio)
+
         # store the status of the editor so the Delete at object level will not work until the edit is finished
         self.editor_active = False
 
@@ -932,7 +1246,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
             sort_temp.append(int(aperture))
         self.sorted_apid = sorted(sort_temp)
 
-        # populate self.intial_table_rows dict with the tool number as keys and tool diameters as values
+        # populate self.intial_table_rows dict with the tool number as keys and aperture codes as values
         for i in range(len(self.sorted_apid)):
             tt_aperture = self.sorted_apid[i]
             self.tool2tooldia[i + 1] = tt_aperture
@@ -942,6 +1256,13 @@ class FlatCAMGrbEditor(QtCore.QObject):
         else:
             self.apsize_entry.set_value(1.00)
 
+        # Init GUI
+        self.pad_array_size_entry.set_value(5)
+        self.pad_pitch_entry.set_value(2.54)
+        self.pad_angle_entry.set_value(12)
+        self.pad_direction_radio.set_value('CW')
+        self.pad_axis_radio.set_value('X')
+
     def build_ui(self):
 
         try:
@@ -1139,8 +1460,8 @@ class FlatCAMGrbEditor(QtCore.QObject):
             self.storage_dict[ap_id]['solid_geometry'] = []
             self.storage_dict[ap_id]['follow_geometry'] = []
 
-            # self.olddia_newdia dict keeps the evidence on current tools diameters as keys and gets updated on values
-            # each time a tool diameter is edited or added
+            # self.olddia_newdia dict keeps the evidence on current aperture codes as keys and gets updated on values
+            # each time a aperture code is edited or added
             self.olddia_newdia[ap_id] = ap_id
         else:
             self.app.inform.emit(_("[WARNING_NOTCL] Aperture already in the aperture table."))
@@ -1150,7 +1471,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
         # we add a new entry in the tool2tooldia dict
         self.tool2tooldia[len(self.olddia_newdia)] = int(ap_id)
 
-        self.app.inform.emit(_("[success] Added new aperture with dia: {apid}").format(apid=str(ap_id)))
+        self.app.inform.emit(_("[success] Added new aperture with code: {apid}").format(apid=str(ap_id)))
 
         self.build_ui()
 
@@ -1164,7 +1485,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
 
     def on_aperture_delete(self, apid=None):
         self.is_modified = True
-        deleted_tool_dia_list = []
+        deleted_apcode_list = []
         deleted_tool_offset_list = []
 
         try:
@@ -1172,37 +1493,38 @@ class FlatCAMGrbEditor(QtCore.QObject):
                 # deleted_tool_dia = float(self.apertures_table.item(self.apertures_table.currentRow(), 1).text())
                 for index in self.apertures_table.selectionModel().selectedRows():
                     row = index.row()
-                    deleted_tool_dia_list.append(self.apertures_table.item(row, 1).text())
+                    deleted_apcode_list.append(self.apertures_table.item(row, 1).text())
             else:
                 if isinstance(apid, list):
                     for dd in apid:
-                        deleted_tool_dia_list.append(dd)
+                        deleted_apcode_list.append(dd)
                 else:
-                    deleted_tool_dia_list.append(apid)
+                    deleted_apcode_list.append(apid)
         except:
             self.app.inform.emit(_("[WARNING_NOTCL] Select a tool in Tool Table"))
             return
 
-        for deleted_tool_dia in deleted_tool_dia_list:
+        for deleted_aperture in deleted_apcode_list:
             # delete the storage used for that tool
-            self.storage_dict.pop(deleted_tool_dia, None)
+            self.storage_dict.pop(deleted_aperture, None)
 
             # I've added this flag_del variable because dictionary don't like
             # having keys deleted while iterating through them
             flag_del = []
             for deleted_tool in self.tool2tooldia:
-                if self.tool2tooldia[deleted_tool] == deleted_tool_dia:
+                if self.tool2tooldia[deleted_tool] == deleted_aperture:
                     flag_del.append(deleted_tool)
 
             if flag_del:
-                for tool_to_be_deleted in flag_del:
+                for aperture_to_be_deleted in flag_del:
                     # delete the tool
-                    self.tool2tooldia.pop(tool_to_be_deleted, None)
+                    self.tool2tooldia.pop(aperture_to_be_deleted, None)
                 flag_del = []
 
-            self.olddia_newdia.pop(deleted_tool_dia, None)
+            self.olddia_newdia.pop(deleted_aperture, None)
 
-            self.app.inform.emit(_("[success] Deleted aperture with code: {del_dia}").format(del_dia=str(deleted_tool_dia)))
+            self.app.inform.emit(_("[success] Deleted aperture with code: {del_dia}").format(
+                del_dia=str(deleted_aperture)))
 
         self.plot_all()
         self.build_ui()
@@ -1232,7 +1554,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
 
         dia_changed = self.tool2tooldia[key_in_tool2tooldia]
 
-        # tool diameter is not used so we create a new tool with the desired diameter
+        # aperture code is not used so we create a new tool with the desired diameter
         if current_table_dia_edited not in self.olddia_newdia.values():
             # update the dict that holds as keys our initial diameters and as values the edited diameters
             self.olddia_newdia[dia_changed] = current_table_dia_edited
@@ -1245,7 +1567,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
 
             self.plot_all()
         else:
-            # tool diameter is already in use so we move the drills from the prior tool to the new tool
+            # aperture code is already in use so we move the pads from the prior tool to the new tool
             factor = current_table_dia_edited / dia_changed
             for shape in self.storage_dict[dia_changed].get_objects():
                 geometry.append(DrawToolShape(
@@ -1432,7 +1754,6 @@ class FlatCAMGrbEditor(QtCore.QObject):
         # we activate this after the initial build as we don't need to see the tool been populated
         self.apertures_table.itemChanged.connect(self.on_tool_edit)
 
-        # build the geometry for each tool-diameter, each drill will be represented by a '+' symbol
         # and then add it to the storage elements (each storage elements is a member of a list
 
         def job_thread(self, apid):
@@ -2143,6 +2464,25 @@ class FlatCAMGrbEditor(QtCore.QObject):
         if shape in self.selected:
             self.selected.remove(shape)
 
+    def on_array_type_combo(self):
+        if self.array_type_combo.currentIndex() == 0:
+            self.array_circular_frame.hide()
+            self.array_linear_frame.show()
+        else:
+            self.delete_utility_geometry()
+            self.array_circular_frame.show()
+            self.array_linear_frame.hide()
+            self.app.inform.emit(_("Click on the circular array Center position"))
+
+    def on_linear_angle_radio(self):
+        val = self.pad_axis_radio.get_value()
+        if val == 'A':
+            self.linear_angle_spinner.show()
+            self.linear_angle_label.show()
+        else:
+            self.linear_angle_spinner.hide()
+            self.linear_angle_label.hide()
+
     def on_copy_button(self):
         self.select_tool('copy')
         return
@@ -2154,6 +2494,9 @@ class FlatCAMGrbEditor(QtCore.QObject):
     def on_pad_add(self):
         self.select_tool('pad')
 
+    def on_pad_add_array(self):
+        self.select_tool('array')
+
     def on_track_add(self):
         self.select_tool('track')
 

+ 24 - 8
flatcamGUI/FlatCAMGUI.py

@@ -462,6 +462,8 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
 
         self.grb_add_pad_menuitem = self.grb_editor_menu.addAction(
             QtGui.QIcon('share/aperture16.png'), _('Add Pad\tP'))
+        self.grb_add_pad_array_menuitem = self.grb_editor_menu.addAction(
+            QtGui.QIcon('share/padarray32.png'), _('Add Pad Array\tA'))
         self.grb_add_track_menuitem = self.grb_editor_menu.addAction(
             QtGui.QIcon('share/track32.png'), _('Add Track\tT'))
         self.grb_add_region_menuitem = self.grb_editor_menu.addAction(QtGui.QIcon('share/rectangle32.png'),
@@ -639,7 +641,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
         self.select_drill_btn = self.exc_edit_toolbar.addAction(QtGui.QIcon('share/pointer32.png'), _("Select"))
         self.add_drill_btn = self.exc_edit_toolbar.addAction(QtGui.QIcon('share/plus16.png'), _('Add Drill Hole'))
         self.add_drill_array_btn = self.exc_edit_toolbar.addAction(
-            QtGui.QIcon('share/addarray16.png'), 'Add Drill Hole Array')
+            QtGui.QIcon('share/addarray16.png'), _('Add Drill Hole Array'))
         self.resize_drill_btn = self.exc_edit_toolbar.addAction(QtGui.QIcon('share/resize16.png'), _('Resize Drill'))
         self.exc_edit_toolbar.addSeparator()
 
@@ -685,6 +687,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
         ### Gerber Editor Toolbar ###
         self.grb_select_btn = self.grb_edit_toolbar.addAction(QtGui.QIcon('share/pointer32.png'), _("Select"))
         self.grb_add_pad_btn = self.grb_edit_toolbar.addAction(QtGui.QIcon('share/aperture32.png'), _("Add Pad"))
+        self.add_pad_ar_btn = self.grb_edit_toolbar.addAction(QtGui.QIcon('share/padarray32.png'), _('Add Pad Array'))
         self.grb_add_track_btn = self.grb_edit_toolbar.addAction(QtGui.QIcon('share/track32.png'), _("Add Track"))
         self.grb_add_region_btn = self.grb_edit_toolbar.addAction(QtGui.QIcon('share/polygon32.png'), _("Add Region"))
         self.grb_edit_toolbar.addSeparator()
@@ -1779,6 +1782,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
         ### Gerber Editor Toolbar ###
         self.grb_select_btn = self.grb_edit_toolbar.addAction(QtGui.QIcon('share/pointer32.png'), _("Select"))
         self.grb_add_pad_btn = self.grb_edit_toolbar.addAction(QtGui.QIcon('share/aperture32.png'), _("Add Pad"))
+        self.add_pad_ar_btn = self.grb_edit_toolbar.addAction(QtGui.QIcon('share/padarray32.png'), _('Add Pad Array'))
         self.grb_add_track_btn = self.grb_edit_toolbar.addAction(QtGui.QIcon('share/track32.png'), _("Add Track"))
         self.grb_add_region_btn = self.grb_edit_toolbar.addAction(QtGui.QIcon('share/polygon32.png'), _("Add Region"))
         self.grb_edit_toolbar.addSeparator()
@@ -2513,6 +2517,24 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                     self.app.on_select_tab('tool')
                     return
 
+                # Add Array of pads
+                if key == QtCore.Qt.Key_A or key == 'A':
+                    self.app.grb_editor.launched_from_shortcuts = True
+                    self.app.inform.emit("Click on target point.")
+                    self.app.ui.add_pad_ar_btn.setChecked(True)
+
+                    self.app.grb_editor.x = self.app.mouse[0]
+                    self.app.grb_editor.y = self.app.mouse[1]
+
+                    self.app.grb_editor.select_tool('array')
+                    return
+
+                # Scale Tool
+                if key == QtCore.Qt.Key_B or key == 'B':
+                    self.app.grb_editor.launched_from_shortcuts = True
+                    self.app.grb_editor.select_tool('buffer')
+                    return
+
                 # Copy
                 if key == QtCore.Qt.Key_C or key == 'C':
                     self.app.grb_editor.launched_from_shortcuts = True
@@ -2526,12 +2548,6 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                         self.app.inform.emit(_("[WARNING_NOTCL] Cancelled. Nothing selected to copy."))
                     return
 
-                # Scale Tool
-                if key == QtCore.Qt.Key_B or key == 'B':
-                    self.app.grb_editor.launched_from_shortcuts = True
-                    self.app.grb_editor.select_tool('buffer')
-                    return
-
                 # Grid Snap
                 if key == QtCore.Qt.Key_G or key == 'G':
                     self.app.grb_editor.launched_from_shortcuts = True
@@ -2570,7 +2586,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
                 if key == QtCore.Qt.Key_P or key == 'P':
                     self.app.grb_editor.launched_from_shortcuts = True
                     self.app.inform.emit(_("Click on target point."))
-                    self.app.ui.grb_add_pad_btn.setChecked(True)
+                    self.app.ui.add_pad_ar_btn.setChecked(True)
 
                     self.app.grb_editor.x = self.app.mouse[0]
                     self.app.grb_editor.y = self.app.mouse[1]