Przeglądaj źródła

Merged in beta_8.905 (pull request #130)

Beta 8.905
Marius Stanciu 7 lat temu
rodzic
commit
13cae5d0be

+ 1 - 0
FlatCAM.py

@@ -6,6 +6,7 @@ from FlatCAMApp import App
 from multiprocessing import freeze_support
 import VisPyPatches
 
+
 if sys.platform == "win32":
     # cx_freeze 'module win32' workaround
     import OpenGL.platform.win32

Plik diff jest za duży
+ 424 - 314
FlatCAMApp.py


+ 120 - 16
FlatCAMEditor.py

@@ -291,7 +291,7 @@ class TextInputTool(FlatCAMTool):
                     font_name=self.font_name,
                     font_size=font_to_geo_size,
                     font_type=font_to_geo_type,
-                    units=self.app.general_options_form.general_group.units_radio.get_value().upper())
+                    units=self.app.general_options_form.general_app_group.units_radio.get_value().upper())
 
     def font_family(self, font):
         self.text_input_entry.selectAll()
@@ -613,6 +613,7 @@ class FCCircle(FCShapeTool):
         radius = distance(p1, p2)
         self.geometry = DrawToolShape(Point(p1).buffer(radius, int(self.steps_per_circ / 4)))
         self.complete = True
+        self.draw_app.app.inform.emit("[success]Done. Adding Circle completed.")
 
 
 class FCArc(FCShapeTool):
@@ -794,6 +795,7 @@ class FCArc(FCShapeTool):
             self.geometry = DrawToolShape(LineString(arc(center, radius, startangle, stopangle,
                                                          self.direction, self.steps_per_circ)))
         self.complete = True
+        self.draw_app.app.inform.emit("[success]Done. Arc completed.")
 
 
 class FCRectangle(FCShapeTool):
@@ -831,6 +833,7 @@ class FCRectangle(FCShapeTool):
         # 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.complete = True
+        self.draw_app.app.inform.emit("[success]Done. Rectangle completed.")
 
 
 class FCPolygon(FCShapeTool):
@@ -869,6 +872,7 @@ class FCPolygon(FCShapeTool):
         self.geometry = DrawToolShape(Polygon(self.points))
         self.draw_app.in_action = False
         self.complete = True
+        self.draw_app.app.inform.emit("[success]Done. Polygon completed.")
 
     def on_key(self, key):
         if key == 'backspace':
@@ -885,6 +889,7 @@ class FCPath(FCPolygon):
         self.geometry = DrawToolShape(LineString(self.points))
         self.draw_app.in_action = False
         self.complete = True
+        self.draw_app.app.inform.emit("[success]Done. Path completed.")
 
     def utility_geometry(self, data=None):
         if len(self.points) > 0:
@@ -1143,6 +1148,7 @@ class FCMove(FCShapeTool):
         self.start_msg = "Click on reference point."
 
     def set_origin(self, origin):
+        self.draw_app.app.inform.emit("Click on destination point.")
         self.origin = origin
 
     def click(self, point):
@@ -1173,6 +1179,7 @@ class FCMove(FCShapeTool):
         #     self.draw_app.set_selected(g)
 
         self.complete = True
+        self.draw_app.app.inform.emit("[success]Done. Geometry(s) Move completed.")
 
     def utility_geometry(self, data=None):
         """
@@ -1208,6 +1215,7 @@ class FCCopy(FCMove):
         self.geometry = [DrawToolShape(affinity.translate(geom.geo, xoff=dx, yoff=dy))
                          for geom in self.draw_app.get_selected()]
         self.complete = True
+        self.draw_app.app.inform.emit("[success]Done. Geometry(s) Copy completed.")
 
 
 class FCText(FCShapeTool):
@@ -1241,6 +1249,7 @@ class FCText(FCShapeTool):
         self.text_gui.text_path = []
         self.text_gui.hide_tool()
         self.complete = True
+        self.draw_app.app.inform.emit("[success]Done. Adding Text completed.")
 
     def utility_geometry(self, data=None):
         """
@@ -1281,6 +1290,7 @@ class FCBuffer(FCShapeTool):
         self.draw_app.buffer(buffer_distance, join_style)
         self.app.ui.notebook.setTabText(2, "Tools")
         self.disactivate()
+        self.draw_app.app.inform.emit("[success]Done. Buffer Tool completed.")
 
     def on_buffer_int(self):
         buffer_distance = self.buff_tool.buffer_distance_entry.get_value()
@@ -1290,6 +1300,7 @@ class FCBuffer(FCShapeTool):
         self.draw_app.buffer_int(buffer_distance, join_style)
         self.app.ui.notebook.setTabText(2, "Tools")
         self.disactivate()
+        self.draw_app.app.inform.emit("[success]Done. Buffer Int Tool completed.")
 
     def on_buffer_ext(self):
         buffer_distance = self.buff_tool.buffer_distance_entry.get_value()
@@ -1299,6 +1310,7 @@ class FCBuffer(FCShapeTool):
         self.draw_app.buffer_ext(buffer_distance, join_style)
         self.app.ui.notebook.setTabText(2, "Tools")
         self.disactivate()
+        self.draw_app.app.inform.emit("[success]Done. Buffer Ext Tool completed.")
 
     def activate(self):
         self.buff_tool.buffer_button.clicked.disconnect()
@@ -1360,6 +1372,7 @@ class FCRotate(FCShapeTool):
         # Delete old
         self.draw_app.delete_selected()
         self.complete = True
+        self.draw_app.app.inform.emit("[success]Done. Geometry rotate completed.")
 
         # MS: automatically select the Select Tool after finishing the action but is not working yet :(
         #self.draw_app.select_tool("select")
@@ -1901,6 +1914,9 @@ class FlatCAMGeoEditor(QtCore.QObject):
         self.app.ui.geo_cutpath_btn.triggered.connect(self.cutpath)
         self.app.ui.geo_delete_btn.triggered.connect(self.on_delete_btn)
 
+        self.app.ui.geo_move_menuitem.triggered.connect(self.on_move)
+        self.app.ui.geo_cornersnap_menuitem.triggered.connect(self.on_corner_snap)
+
         ## Toolbar events and properties
         self.tools = {
             "select": {"button": self.app.ui.geo_select_btn,
@@ -2002,7 +2018,8 @@ class FlatCAMGeoEditor(QtCore.QObject):
             try:
                 self.options[option] = float(entry.text())
             except Exception as e:
-                log.debug(str(e))
+                log.debug("FlatCAMGeoEditor.__init__().entry2option() --> %s" % str(e))
+                return
 
         self.app.ui.grid_gap_x_entry.setValidator(QtGui.QDoubleValidator())
         self.app.ui.grid_gap_x_entry.textChanged.connect(
@@ -2565,6 +2582,15 @@ class FlatCAMGeoEditor(QtCore.QObject):
             self.on_tool_select('rotate')
             self.active_tool.set_origin(self.snap(self.x, self.y))
 
+        if event.key == '1':
+            self.app.on_zoom_fit(None)
+
+        if event.key == '2':
+            self.app.plotcanvas.zoom(1 / self.app.defaults['zoom_ratio'], [self.snap_x, self.snap_y])
+
+        if event.key == '3':
+            self.app.plotcanvas.zoom(self.app.defaults['zoom_ratio'], [self.snap_x, self.snap_y])
+
         # Arc Tool
         if event.key.name == 'A':
             self.select_tool('arc')
@@ -2580,6 +2606,22 @@ class FlatCAMGeoEditor(QtCore.QObject):
             self.active_tool.set_origin(self.snap(self.x, self.y))
             self.app.inform.emit("Click on target point.")
 
+        # Substract Tool
+        if event.key.name == 'E':
+            if self.get_selected() is not None:
+                self.intersection()
+            else:
+                msg = "Please select geometry items \n" \
+                      "on which to perform Intersection Tool."
+
+                messagebox =QtWidgets.QMessageBox()
+                messagebox.setText(msg)
+                messagebox.setWindowTitle("Warning")
+                messagebox.setWindowIcon(QtGui.QIcon('share/warning.png'))
+                messagebox.setStandardButtons(QtWidgets.QMessageBox.Ok)
+                messagebox.setDefaultButton(QtWidgets.QMessageBox.Ok)
+                messagebox.exec_()
+
         # Grid Snap
         if event.key.name == 'G':
             self.app.ui.grid_snap_btn.trigger()
@@ -2596,14 +2638,11 @@ class FlatCAMGeoEditor(QtCore.QObject):
 
         # Corner Snap
         if event.key.name == 'K':
-            self.app.ui.corner_snap_btn.trigger()
+            self.on_corner_snap()
 
         # Move
         if event.key.name == 'M':
-            self.app.ui.geo_move_btn.setChecked(True)
-            self.on_tool_select('move')
-            self.active_tool.set_origin(self.snap(self.x, self.y))
-            self.app.inform.emit("Click on target point.")
+            self.on_move_click()
 
         # Polygon Tool
         if event.key.name == 'N':
@@ -2621,14 +2660,42 @@ class FlatCAMGeoEditor(QtCore.QObject):
         if event.key.name == 'R':
             self.select_tool('rectangle')
 
-        # Select Tool
+        # Substract Tool
         if event.key.name == 'S':
-            self.select_tool('select')
+            if self.get_selected() is not None:
+                self.subtract()
+            else:
+                msg = "Please select geometry items \n" \
+                      "on which to perform Substraction Tool."
+
+                messagebox =QtWidgets.QMessageBox()
+                messagebox.setText(msg)
+                messagebox.setWindowTitle("Warning")
+                messagebox.setWindowIcon(QtGui.QIcon('share/warning.png'))
+                messagebox.setStandardButtons(QtWidgets.QMessageBox.Ok)
+                messagebox.setDefaultButton(QtWidgets.QMessageBox.Ok)
+                messagebox.exec_()
 
         # Add Text Tool
         if event.key.name == 'T':
             self.select_tool('text')
 
+        # Substract Tool
+        if event.key.name == 'U':
+            if self.get_selected() is not None:
+                self.union()
+            else:
+                msg = "Please select geometry items \n" \
+                      "on which to perform union."
+
+                messagebox =QtWidgets.QMessageBox()
+                messagebox.setText(msg)
+                messagebox.setWindowTitle("Warning")
+                messagebox.setWindowIcon(QtGui.QIcon('share/warning.png'))
+                messagebox.setStandardButtons(QtWidgets.QMessageBox.Ok)
+                messagebox.setDefaultButton(QtWidgets.QMessageBox.Ok)
+                messagebox.exec_()
+
         # Cut Action Tool
         if event.key.name == 'X':
             if self.get_selected() is not None:
@@ -2661,11 +2728,15 @@ class FlatCAMGeoEditor(QtCore.QObject):
     def on_shortcut_list(self):
         msg = '''<b>Shortcut list in Geometry Editor</b><br>
 <br>
+<b>1:</b>       Zoom Fit<br>
+<b>2:</b>       Zoom Out<br>
+<b>3:</b>       Zoom In<br>
 <b>A:</b>       Add an 'Arc'<br>
 <b>B:</b>       Add a Buffer Geo<br>
 <b>C:</b>       Copy Geo Item<br>
+<b>E:</b>       Intersection Tool<br>
 <b>G:</b>       Grid Snap On/Off<br>
-<b>G:</b>       Paint Tool<br>
+<b>I:</b>       Paint Tool<br>
 <b>K:</b>       Corner Snap On/Off<br>
 <b>M:</b>       Move Geo Item<br>
 <br>
@@ -2673,8 +2744,9 @@ class FlatCAMGeoEditor(QtCore.QObject):
 <b>O:</b>       Add a 'Circle'<br>
 <b>P:</b>       Add a 'Path'<br>
 <b>R:</b>       Add an 'Rectangle'<br>
-<b>S:</b>       Select Tool Active<br>
+<b>S:</b>       Substraction Tool<br>
 <b>T:</b>       Add Text Geometry<br>
+<b>U:</b>       Union Tool<br>
 <br>
 <b>X:</b>       Cut Path<br>
 <br>
@@ -2682,7 +2754,7 @@ class FlatCAMGeoEditor(QtCore.QObject):
 <br>
 <b>Space:</b>   Rotate selected Geometry<br>
 <b>Enter:</b>   Finish Current Action<br>
-<b>Escape:</b>  Abort Current Action<br>
+<b>Escape:</b>  Select Tool (Exit any other Tool)<br>
 <b>Delete:</b>  Delete Obj'''
 
         helpbox =QtWidgets.QMessageBox()
@@ -2718,6 +2790,17 @@ class FlatCAMGeoEditor(QtCore.QObject):
         if shape in self.selected:
             self.selected.remove(shape)  # TODO: Check performance
 
+    def on_move(self):
+        self.app.ui.geo_move_btn.setChecked(True)
+        self.on_tool_select('move')
+
+    def on_move_click(self):
+        self.on_move()
+        self.active_tool.set_origin(self.snap(self.x, self.y))
+
+    def on_corner_snap(self):
+        self.app.ui.corner_snap_btn.trigger()
+
     def get_selected(self):
         """
         Returns list of shapes that are selected in the editor.
@@ -2950,7 +3033,13 @@ class FlatCAMGeoEditor(QtCore.QObject):
 
         shapes = self.get_selected()
 
-        results = shapes[0].geo
+        try:
+            results = shapes[0].geo
+        except Exception as e:
+            log.debug("FlatCAMGeoEditor.intersection() --> %s" % str(e))
+            self.app.inform.emit("[warning_notcl]A selection of at least 2 geo items is required to do Intersection.")
+            self.select_tool('select')
+            return
 
         for shape in shapes[1:]:
             results = results.intersection(shape.geo)
@@ -3638,7 +3727,7 @@ class FlatCAMExcEditor(QtCore.QObject):
         self.move_timer.setSingleShot(True)
 
         ## Current application units in Upper Case
-        self.units = self.app.general_options_form.general_group.units_radio.get_value().upper()
+        self.units = self.app.general_options_form.general_app_group.units_radio.get_value().upper()
 
         self.key = None  # Currently pressed key
         self.modifiers = None
@@ -3712,7 +3801,7 @@ class FlatCAMExcEditor(QtCore.QObject):
 
     def set_ui(self):
         # updated units
-        self.units = self.app.general_options_form.general_group.units_radio.get_value().upper()
+        self.units = self.app.general_options_form.general_app_group.units_radio.get_value().upper()
 
         self.olddia_newdia.clear()
         self.tool2tooldia.clear()
@@ -3752,7 +3841,7 @@ class FlatCAMExcEditor(QtCore.QObject):
             pass
 
         # updated units
-        self.units = self.app.general_options_form.general_group.units_radio.get_value().upper()
+        self.units = self.app.general_options_form.general_app_group.units_radio.get_value().upper()
 
         # make a new name for the new Excellon object (the one with edited content)
         self.edited_obj_name = self.exc_obj.options['name']
@@ -4744,6 +4833,18 @@ class FlatCAMExcEditor(QtCore.QObject):
                 self.app.inform.emit("[warning_notcl]Cancelled. Nothing selected to delete.")
             return
 
+        if event.key == '1':
+            self.launched_from_shortcuts = True
+            self.app.on_zoom_fit(None)
+
+        if event.key == '2':
+            self.launched_from_shortcuts = True
+            self.app.plotcanvas.zoom(1 / self.app.defaults['zoom_ratio'], [self.snap_x, self.snap_y])
+
+        if event.key == '3':
+            self.launched_from_shortcuts = True
+            self.app.plotcanvas.zoom(self.app.defaults['zoom_ratio'], [self.snap_x, self.snap_y])
+
         # Add Array of Drill Hole Tool
         if event.key.name == 'A':
             self.launched_from_shortcuts = True
@@ -4828,6 +4929,9 @@ class FlatCAMExcEditor(QtCore.QObject):
     def on_shortcut_list(self):
         msg = '''<b>Shortcut list in Geometry Editor</b><br>
 <br>
+<b>1:</b>       Zoom Fit<br>
+<b>2:</b>       Zoom Out<br>
+<b>3:</b>       Zoom In<br>
 <b>A:</b>       Add an 'Drill Array'<br>
 <b>C:</b>       Copy Drill Hole<br>
 <b>D:</b>       Add an Drill Hole<br>

Plik diff jest za duży
+ 370 - 338
FlatCAMGUI.py


+ 185 - 35
FlatCAMObj.py

@@ -366,6 +366,7 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
 
         if grb_final.solid_geometry is None:
             grb_final.solid_geometry = []
+
         if type(grb_final.solid_geometry) is not list:
             grb_final.solid_geometry = [grb_final.solid_geometry]
 
@@ -380,10 +381,11 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
             # Expand lists
             if type(grb) is list:
                 FlatCAMGerber.merge(grb, grb_final)
+            else:   # If not list, just append
+                for geos in grb.solid_geometry:
+                    grb_final.solid_geometry.append(geos)
 
-            # If not list, just append
-            else:
-                grb_final.solid_geometry.append(grb.solid_geometry)
+        grb_final.solid_geometry = MultiPolygon(grb_final.solid_geometry)
 
     def __init__(self, name):
         Gerber.__init__(self, steps_per_circle=self.app.defaults["gerber_circle_steps"])
@@ -402,9 +404,6 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
             "isooverlap": 0.15,
             "milling_type": "cl",
             "combine_passes": True,
-            "ncctools": "1.0, 0.5",
-            "nccoverlap": 0.4,
-            "nccmargin": 1,
             "noncoppermargin": 0.0,
             "noncopperrounded": False,
             "bboxmargin": 0.0,
@@ -755,9 +754,6 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
         factor = Gerber.convert_units(self, units)
 
         self.options['isotooldia'] *= factor
-        self.options['cutoutmargin'] *= factor
-        self.options['cutoutgapsize'] *= factor
-        self.options['noncoppermargin'] *= factor
         self.options['bboxmargin'] *= factor
 
     def plot(self, **kwargs):
@@ -899,14 +895,21 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
         :return: None
         """
 
+        # flag to signal that we need to reorder the tools dictionary and drills and slots lists
+        flag_order = False
+
         try:
             flattened_list = list(itertools.chain(*exc_list))
         except TypeError:
             flattened_list = exc_list
 
         # this dict will hold the unique tool diameters found in the exc_list objects as the dict keys and the dict
-        # values will be list of Shapely Points
-        custom_dict = {}
+        # values will be list of Shapely Points; for drills
+        custom_dict_drills = {}
+
+        # this dict will hold the unique tool diameters found in the exc_list objects as the dict keys and the dict
+        # values will be list of Shapely Points; for slots
+        custom_dict_slots = {}
 
         for exc in flattened_list:
             # copy options of the current excellon obj to the final excellon obj
@@ -920,21 +923,29 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
             for drill in exc.drills:
                 exc_tool_dia = float('%.3f' % exc.tools[drill['tool']]['C'])
 
-                if exc_tool_dia not in custom_dict:
-                    custom_dict[exc_tool_dia] = [drill['point']]
+                if exc_tool_dia not in custom_dict_drills:
+                    custom_dict_drills[exc_tool_dia] = [drill['point']]
+                else:
+                    custom_dict_drills[exc_tool_dia].append(drill['point'])
+
+            for slot in exc.slots:
+                exc_tool_dia = float('%.3f' % exc.tools[slot['tool']]['C'])
+
+                if exc_tool_dia not in custom_dict_slots:
+                    custom_dict_slots[exc_tool_dia] = [[slot['start'], slot['stop']]]
                 else:
-                    custom_dict[exc_tool_dia].append(drill['point'])
+                    custom_dict_slots[exc_tool_dia].append([slot['start'], slot['stop']])
 
-                # add the zeros and units to the exc_final object
+            # add the zeros and units to the exc_final object
             exc_final.zeros = exc.zeros
             exc_final.units = exc.units
 
         # variable to make tool_name for the tools
         current_tool = 0
-
         # Here we add data to the exc_final object
-        # the tools diameter are now the keys in the drill_dia dict and the values are the Shapely Points
-        for tool_dia in custom_dict:
+        # the tools diameter are now the keys in the drill_dia dict and the values are the Shapely Points in case of
+        # drills
+        for tool_dia in custom_dict_drills:
             # we create a tool name for each key in the drill_dia dict (the key is a unique drill diameter)
             current_tool += 1
 
@@ -943,7 +954,7 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
             exc_final.tools[tool_name] = spec
 
             # rebuild the drills list of dict's that belong to the exc_final object
-            for point in custom_dict[tool_dia]:
+            for point in custom_dict_drills[tool_dia]:
                 exc_final.drills.append(
                     {
                         "point": point,
@@ -951,6 +962,94 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
                     }
                 )
 
+        # Here we add data to the exc_final object
+        # the tools diameter are now the keys in the drill_dia dict and the values are a list ([start, stop])
+        # of two Shapely Points in case of slots
+        for tool_dia in custom_dict_slots:
+            # we create a tool name for each key in the slot_dia dict (the key is a unique slot diameter)
+            # but only if there are no drills
+            if not exc_final.tools:
+                current_tool += 1
+                tool_name = str(current_tool)
+                spec = {"C": float(tool_dia)}
+                exc_final.tools[tool_name] = spec
+            else:
+                dia_list = []
+                for v in exc_final.tools.values():
+                    dia_list.append(float(v["C"]))
+
+                if tool_dia not in dia_list:
+                    flag_order = True
+
+                    current_tool = len(dia_list) + 1
+                    tool_name = str(current_tool)
+                    spec = {"C": float(tool_dia)}
+                    exc_final.tools[tool_name] = spec
+
+                else:
+                    for k, v in exc_final.tools.items():
+                        if v["C"] == tool_dia:
+                            current_tool = int(k)
+                            break
+
+            # rebuild the slots list of dict's that belong to the exc_final object
+            for point in custom_dict_slots[tool_dia]:
+                exc_final.slots.append(
+                    {
+                        "start": point[0],
+                        "stop": point[1],
+                        "tool": str(current_tool)
+                    }
+                )
+
+        # flag_order == True means that there was an slot diameter not in the tools and we also have drills
+        # and the new tool was added to self.tools therefore we need to reorder the tools and drills and slots
+        current_tool = 0
+        if flag_order is True:
+            dia_list = []
+            temp_drills = []
+            temp_slots = []
+            temp_tools = {}
+            for v in exc_final.tools.values():
+                dia_list.append(float(v["C"]))
+            dia_list.sort()
+            for ordered_dia in dia_list:
+                current_tool += 1
+                tool_name_temp = str(current_tool)
+                spec_temp = {"C": float(ordered_dia)}
+                temp_tools[tool_name_temp] = spec_temp
+
+                for drill in exc_final.drills:
+                    exc_tool_dia = float('%.3f' % exc_final.tools[drill['tool']]['C'])
+                    if exc_tool_dia == ordered_dia:
+                        temp_drills.append(
+                            {
+                                "point": drill["point"],
+                                "tool": str(current_tool)
+                            }
+                        )
+
+                for slot in exc_final.slots:
+                    slot_tool_dia = float('%.3f' % exc_final.tools[slot['tool']]['C'])
+                    if slot_tool_dia == ordered_dia:
+                        temp_slots.append(
+                            {
+                                "start": slot["start"],
+                                "stop": slot["stop"],
+                                "tool": str(current_tool)
+                            }
+                        )
+
+            # delete the exc_final tools, drills and slots
+            exc_final.tools = dict()
+            exc_final.drills[:] = []
+            exc_final.slots[:] = []
+
+            # update the exc_final tools, drills and slots with the ordered values
+            exc_final.tools = temp_tools
+            exc_final.drills[:] = temp_drills
+            exc_final.slots[:] = temp_slots
+
         # create the geometry for the exc_final object
         exc_final.create_geometry()
 
@@ -1192,7 +1291,7 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
         """
 
         excellon_code = ''
-        units = self.app.general_options_form.general_group.units_radio.get_value().upper()
+        units = self.app.general_options_form.general_app_group.units_radio.get_value().upper()
 
         # store here if the file has slots, return 1 if any slots, 0 if only drills
         has_slots = 0
@@ -1252,7 +1351,7 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
         """
 
         excellon_code = ''
-        units = self.app.general_options_form.general_group.units_radio.get_value().upper()
+        units = self.app.general_options_form.general_app_group.units_radio.get_value().upper()
 
         # store here if the file has slots, return 1 if any slots, 0 if only drills
         has_slots = 0
@@ -2759,7 +2858,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
             self.ui.geo_tools_table.setCurrentItem(self.ui.geo_tools_table.item(row, 0))
 
     def export_dxf(self):
-        units = self.app.general_options_form.general_group.units_radio.get_value().upper()
+        units = self.app.general_options_form.general_app_group.units_radio.get_value().upper()
         dwg = None
         try:
             dwg = ezdxf.new('R2010')
@@ -2859,7 +2958,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
         else:
             self.app.inform.emit("[error_notcl] Failed. No tool selected in the tool table ...")
 
-    def mtool_gen_cncjob(self, use_thread=True):
+    def mtool_gen_cncjob(self, segx=None, segy=None, use_thread=True):
         """
         Creates a multi-tool CNCJob out of this Geometry object.
         The actual work is done by the target FlatCAMCNCjob object's
@@ -2883,6 +2982,9 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
         # use the name of the first tool selected in self.geo_tools_table which has the diameter passed as tool_dia
         outname = "%s_%s" % (self.options["name"], 'cnc')
 
+        segx = segx if segx is not None else float(self.app.defaults['geometry_segx'])
+        segy = segy if segy is not None else float(self.app.defaults['geometry_segy'])
+
         # Object initialization function for app.new_object()
         # RUNNING ON SEPARATE THREAD!
         def job_init_single_geometry(job_obj, app_obj):
@@ -2901,6 +3003,8 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
             # job_obj.create_geometry()
 
             job_obj.options['Tools_in_use'] = self.get_selected_tools_table_items()
+            job_obj.segx = segx
+            job_obj.segy = segy
 
             for tooluid_key in self.sel_tools:
                 tool_cnt += 1
@@ -3242,6 +3346,8 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
                        toolchange=None, toolchangez=None, toolchangexy=None,
                        extracut=None, startz=None, endz=None,
                        ppname_g=None,
+                       segx=None,
+                       segy=None,
                        use_thread=True):
         """
         Only used for TCL Command.
@@ -3273,6 +3379,9 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
         multidepth = multidepth if multidepth is not None else self.options["multidepth"]
         depthperpass = depthperpass if depthperpass is not None else self.options["depthperpass"]
 
+        segx = segx if segx is not None else float(self.app.defaults['geometry_segx'])
+        segy = segy if segy is not None else float(self.app.defaults['geometry_segy'])
+
         extracut = extracut if extracut is not None else self.options["extracut"]
         startz = startz if startz is not None else self.options["startz"]
         endz = endz if endz is not None else self.options["endz"]
@@ -3307,6 +3416,9 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
             job_obj.options['type'] = 'Geometry'
             job_obj.options['tool_dia'] = tooldia
 
+            job_obj.segx = segx
+            job_obj.segy = segy
+
             # TODO: The tolerance should not be hard coded. Just for testing.
             job_obj.generate_from_geometry_2(self, tooldia=tooldia, offset=offset, tolerance=0.0005,
                                              z_cut=z_cut, z_move=z_move,
@@ -3358,8 +3470,20 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
         :rtype: None
         """
 
+        try:
+            xfactor = float(xfactor)
+        except:
+            self.app.inform.emit("[error_notcl] Scale factor has to be a number: integer or float.")
+            return
+
         if yfactor is None:
             yfactor = xfactor
+        else:
+            try:
+                yfactor = float(yfactor)
+            except:
+                self.app.inform.emit("[error_notcl] Scale factor has to be a number: integer or float.")
+                return
 
         if point is None:
             px = 0
@@ -3367,16 +3491,33 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
         else:
             px, py = point
 
-        if type(self.solid_geometry) == list:
-            geo_list =  self.flatten(self.solid_geometry)
-            self.solid_geometry = []
-            # for g in geo_list:
-            #     self.solid_geometry.append(affinity.scale(g, xfactor, yfactor, origin=(px, py)))
-            self.solid_geometry = [affinity.scale(g, xfactor, yfactor, origin=(px, py))
-                                   for g in geo_list]
+        # if type(self.solid_geometry) == list:
+        #     geo_list =  self.flatten(self.solid_geometry)
+        #     self.solid_geometry = []
+        #     # for g in geo_list:
+        #     #     self.solid_geometry.append(affinity.scale(g, xfactor, yfactor, origin=(px, py)))
+        #     self.solid_geometry = [affinity.scale(g, xfactor, yfactor, origin=(px, py))
+        #                            for g in geo_list]
+        # else:
+        #     self.solid_geometry = affinity.scale(self.solid_geometry, xfactor, yfactor,
+        #                                          origin=(px, py))
+        # self.app.inform.emit("[success]Geometry Scale done.")
+
+        def scale_recursion(geom):
+            if type(geom) == list:
+                geoms=list()
+                for local_geom in geom:
+                    geoms.append(scale_recursion(local_geom))
+                return geoms
+            else:
+                return  affinity.scale(geom, xfactor, yfactor, origin=(px, py))
+
+        if self.multigeo is True:
+            for tool in self.tools:
+                self.tools[tool]['solid_geometry'] = scale_recursion(self.tools[tool]['solid_geometry'])
         else:
-            self.solid_geometry = affinity.scale(self.solid_geometry, xfactor, yfactor,
-                                                 origin=(px, py))
+            self.solid_geometry=scale_recursion(self.solid_geometry)
+        self.app.inform.emit("[success]Geometry Scale done.")
 
     def offset(self, vect):
         """
@@ -3388,7 +3529,12 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
         :rtype: None
         """
 
-        dx, dy = vect
+        try:
+            dx, dy = vect
+        except TypeError:
+            self.app.inform.emit("[error_notcl]An (x,y) pair of values are needed. "
+                                 "Probable you entered only one value in the Offset field.")
+            return
 
         def translate_recursion(geom):
             if type(geom) == list:
@@ -3404,6 +3550,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry):
                 self.tools[tool]['solid_geometry'] = translate_recursion(self.tools[tool]['solid_geometry'])
         else:
             self.solid_geometry=translate_recursion(self.solid_geometry)
+        self.app.inform.emit("[success]Geometry Offset done.")
 
     def convert_units(self, units):
         self.ui_disconnect()
@@ -4042,13 +4189,16 @@ class FlatCAMCNCjob(FlatCAMObj, CNCjob):
         cw_row = cw_index.row()
 
         self.shapes.clear(update=True)
+
         for tooluid_key in self.cnc_tools:
             tooldia = float('%.4f' % float(self.cnc_tools[tooluid_key]['tooldia']))
             gcode_parsed = self.cnc_tools[tooluid_key]['gcode_parsed']
             # tool_uid = int(self.ui.cnc_tools_table.item(cw_row, 3).text())
 
-            if self.ui.cnc_tools_table.cellWidget((tooluid_key - 1), 6).isChecked():
-                self.plot2(tooldia=tooldia, obj=self, visible=True, gcode_parsed=gcode_parsed)
+            for r in range(self.ui.cnc_tools_table.rowCount()):
+                if int(self.ui.cnc_tools_table.item(r, 5).text()) == int(tooluid_key):
+                    if self.ui.cnc_tools_table.cellWidget(r, 6).isChecked():
+                        self.plot2(tooldia=tooldia, obj=self, visible=True, gcode_parsed=gcode_parsed)
 
         self.shapes.redraw()
 

+ 7 - 2
FlatCAMTool.py

@@ -33,7 +33,7 @@ class FlatCAMTool(QtWidgets.QWidget):
 
         self.menuAction = None
 
-    def install(self, icon=None, separator=None, **kwargs):
+    def install(self, icon=None, separator=None, shortcut=None, **kwargs):
         before = None
 
         # 'pos' is the menu where the Action has to be installed
@@ -54,8 +54,13 @@ class FlatCAMTool(QtWidgets.QWidget):
         # if provided, add an icon to this Action
         if icon is not None:
             self.menuAction.setIcon(icon)
+
         # set the text name of the Action, which will be displayed in the menu
-        self.menuAction.setText(self.toolName)
+        if shortcut is None:
+            self.menuAction.setText(self.toolName)
+        else:
+            self.menuAction.setText(self.toolName + '\t%s' % shortcut)
+
         # add a ToolTip to the new Action
         # self.menuAction.setToolTip(self.toolTip) # currently not available
 

+ 78 - 6
ObjectCollection.py

@@ -12,6 +12,7 @@ import inspect  # TODO: Remove
 import FlatCAMApp
 from PyQt5 import QtGui, QtCore, QtWidgets
 from PyQt5.QtCore import Qt
+import webbrowser
 
 
 class KeySensitiveListView(QtWidgets.QTreeView):
@@ -262,6 +263,9 @@ class ObjectCollection(QtCore.QAbstractItemModel):
             if key == QtCore.Qt.Key_G:
                 self.app.on_fileopengerber()
 
+            if key == QtCore.Qt.Key_N:
+                self.app.on_file_new_click()
+
             if key == QtCore.Qt.Key_M:
                 self.app.measurement_tool.run()
             if key == QtCore.Qt.Key_O:
@@ -272,6 +276,11 @@ class ObjectCollection(QtCore.QAbstractItemModel):
             return
         elif modifiers == QtCore.Qt.ShiftModifier:
 
+            # Copy Object Name
+            # Copy Object Name
+            if key == QtCore.Qt.Key_C:
+                self.app.on_copy_name()
+
             # Toggle axis
             if key == QtCore.Qt.Key_G:
                 if self.toggle_axis is False:
@@ -286,12 +295,65 @@ class ObjectCollection(QtCore.QAbstractItemModel):
                     self.appplotcanvas.redraw()
                     self.app.toggle_axis = False
 
+            # Open Preferences Window
+            if key == QtCore.Qt.Key_P:
+                self.app.on_preferences()
+                return
+
             # Rotate Object by 90 degree CCW
             if key == QtCore.Qt.Key_R:
                 self.app.on_rotate(silent=True, preset=-90)
                 return
 
+            # Run a Script
+            if key == QtCore.Qt.Key_S:
+                self.app.on_filerunscript()
+                return
+
+            # Toggle Workspace
+            if key == QtCore.Qt.Key_W:
+                self.app.on_workspace_menu()
+                return
+
+            # Skew on X axis
+            if key == QtCore.Qt.Key_X:
+                self.app.on_skewx()
+                return
+
+            # Skew on Y axis
+            if key == QtCore.Qt.Key_Y:
+                self.app.on_skewy()
+                return
+        elif modifiers == QtCore.Qt.AltModifier:
+            # 2-Sided PCB Tool
+            if key == QtCore.Qt.Key_D:
+                self.app.dblsidedtool.run()
+                return
+
+            # Non-Copper Clear Tool
+            if key == QtCore.Qt.Key_N:
+                self.app.ncclear_tool.run()
+                return
+
+            # Transformation Tool
+            if key == QtCore.Qt.Key_R:
+                self.app.transform_tool.run()
+                return
+
+            # Cutout Tool
+            if key == QtCore.Qt.Key_U:
+                self.app.cutout_tool.run()
+                return
+
         else:
+            # Open Manual
+            if key == QtCore.Qt.Key_F1:
+                webbrowser.open(self.app.manual_url)
+
+            # Open Video Help
+            if key == QtCore.Qt.Key_F2:
+                webbrowser.open(self.app.video_url)
+
             # Zoom Fit
             if key == QtCore.Qt.Key_1:
                 self.app.on_zoom_fit(None)
@@ -316,10 +378,6 @@ class ObjectCollection(QtCore.QAbstractItemModel):
                     select.ui.plot_cb.toggle()
                 self.app.delete_selection_shape()
 
-            # Copy Object Name
-            if key == QtCore.Qt.Key_C:
-                self.app.on_copy_name()
-
             # Copy Object Name
             if key == QtCore.Qt.Key_E:
                 self.app.object2editor()
@@ -332,6 +390,10 @@ class ObjectCollection(QtCore.QAbstractItemModel):
             if key == QtCore.Qt.Key_J:
                 self.app.on_jump_to()
 
+            # New Excellon
+            if key == QtCore.Qt.Key_L:
+                self.app.new_excellon_object()
+
             # Move tool toggle
             if key == QtCore.Qt.Key_M:
                 self.app.move_tool.toggle()
@@ -340,12 +402,22 @@ class ObjectCollection(QtCore.QAbstractItemModel):
             if key == QtCore.Qt.Key_N:
                 self.app.on_new_geometry()
 
+            # Set Origin
+            if key == QtCore.Qt.Key_O:
+                self.app.on_set_origin()
+                return
+
+            # Set Origin
+            if key == QtCore.Qt.Key_P:
+                self.app.properties_tool.run()
+                return
+
             # Change Units
             if key == QtCore.Qt.Key_Q:
                 if self.app.options["units"] == 'MM':
-                    self.app.general_options_form.general_group.units_radio.set_value("IN")
+                    self.app.general_options_form.general_app_group.units_radio.set_value("IN")
                 else:
-                    self.app.general_options_form.general_group.units_radio.set_value("MM")
+                    self.app.general_options_form.general_app_group.units_radio.set_value("MM")
                 self.app.on_toggle_units()
 
             # Rotate Object by 90 degree CW

+ 3 - 2
ObjectUI.py

@@ -970,8 +970,9 @@ class GeometryObjectUI(ObjectUI):
         # Spindlespeed
         spdlabel = QtWidgets.QLabel('Spindle speed:')
         spdlabel.setToolTip(
-            "Speed of the spindle\n"
-            "in RPM (optional)"
+            "Speed of the spindle in RPM (optional).\n"
+            "If LASER postprocessor is used,\n"
+            "this value is the power of laser."
         )
         self.grid3.addWidget(spdlabel, 14, 0)
         self.cncspindlespeed_entry = IntEntry(allow_empty=True)

+ 2 - 2
PlotCanvas.py

@@ -65,7 +65,7 @@ class PlotCanvas(QtCore.QObject):
         self.draw_workspace()
 
         # if self.app.defaults['global_workspace'] is True:
-        #     if self.app.general_options_form.general_group.units_radio.get_value().upper() == 'MM':
+        #     if self.app.general_options_form.general_app_group.units_radio.get_value().upper() == 'MM':
         #         self.wkspace_t = Line(pos=)
 
         self.shape_collections = []
@@ -92,7 +92,7 @@ class PlotCanvas(QtCore.QObject):
         a3p_mm = np.array([(0, 0), (297, 0), (297, 420), (0, 420)])
         a3l_mm = np.array([(0, 0), (420, 0), (420, 297), (0, 297)])
 
-        if self.app.general_options_form.general_group.units_radio.get_value().upper() == 'MM':
+        if self.app.general_options_form.general_app_group.units_radio.get_value().upper() == 'MM':
             if self.app.defaults['global_workspaceT'] == 'A4P':
                 a = a4p_mm
             elif self.app.defaults['global_workspaceT'] == 'A4L':

+ 43 - 0
README.md

@@ -9,6 +9,49 @@ CAD program, and create G-Code for Isolation routing.
 
 =================================================
 
+29.01.2019
+
+- fixed issue in Tool Calculators when a float value was entered starting only with the dot.
+- added protection for entering incorrect values in Offset and Scale fields for Gerber and Geometry objects (in Selected Tab)
+- added more shortcut keys in the Geometry Editor and in Excellon Editor; activated also the zoom (fit, in, out) shortcut keys ('1' , '2', '3') for the editors
+- disabled the context menu in tools table on Paint Tool in case that the painting method is single.
+- added protection when trying to do Intersection in Geometry Editor without having selected Geometry items.
+- fixed the scale, mirror, rotate, skew functions to work with Geometry Objects of multi-geometry type.
+- added a GUI for Excellon Search time for OR-TOOLS path optimization in Edit -> Preferences -> Excellon General -> Optimization Time
+- more changes in Edit -> Preferences -> Geometry, Gerber and in CNCJob
+- added new option for Cutout Tool Freeform Gaps in Edit -> Preferences -> Tools
+- fixed Freeform Cutout gaps issue (it was double than the value set)
+- added protection so the Cutout (either Freeform or Rectangular) cannot be done on a multigeo Geometry
+- added 2Sided Tool default values in Edit -> Preferences -> Tools
+- optimized the FlatCAMCNCJob.on_plot_cb_click_table() plot function and solved a bug regarding having tools numbers not in sync with the cnc tool table
+
+28.01.2018
+
+- fixed the FlatCAMGerber.merge() function
+- added a new menu entry for the Gerber Join function: Edit -> Conversions -> "Join Gerber(s) to Gerber" allowing joining Gerber objects into a final Gerber object
+- moved Paint Tool defaults from Geometry section to the Tools section in Edit -> Preferences
+- added key shortcuts for Open Manual = F1 and for Open Online VideoHelp = F2
+
+27.01.2018
+
+- added more key shortcuts into the application; they are now displayed in the GUI menu's
+- reorganized the Edit -> Preferences -> Global
+- redesigned the messagebox that is showed when quiting ot creating a New Project: now it has an option ('Cancel') to abort the process returning to the app
+- added options for trace segmentation that can be useful for auto-levelling (code snippet from Lei Zheng from a rejected pull request on FlatCAM https://bitbucket.org/realthunder/ )
+- added shortcut key 'L' for creating 'New Excellon' 
+- added shortcut key combo 'SHIFT+S' for Running a Script.
+- modified grbl_laser postprocessor file so it includes a Sxxxx command on the line with M03 (laser active) whenever a value is enter in the Spindlespeed entry field
+- remade the EDIT -> PREFERENCES window, the Excellon and Gerber sections. Created a new section named TOOLS
+
+26.01.2019
+
+- fixed grbl_11 postprocessor in linear_code() function
+- added icons to the Project Tab context menu
+- added new entries to the Canvas context menu (Copy, Delete, Edit/Save, Move, New Excellon, New Geometry, New Project)
+- fixed grbl_laser postprocessor file
+- updated function for copy of an Excellon object for the case when the object has slots
+- updated FlatCAMExcellon.merge() function to work in case some (or all) of the merged objects have slots  
+
 25.01.2019
 
 - deleted junk folders

+ 107 - 8
camlib.py

@@ -1354,11 +1354,17 @@ class Geometry(object):
                 return affinity.scale(obj, xscale, yscale, origin=(px,py))
 
         try:
-            self.solid_geometry = mirror_geom(self.solid_geometry)
+            if self.multigeo is True:
+                for tool in self.tools:
+                    self.tools[tool]['solid_geometry'] = mirror_geom(self.tools[tool]['solid_geometry'])
+            else:
+                self.solid_geometry = mirror_geom(self.solid_geometry)
             self.app.inform.emit('[success]Object was mirrored ...')
         except AttributeError:
             self.app.inform.emit("[error_notcl] Failed to mirror. No object selected")
 
+
+
     def rotate(self, angle, point):
         """
         Rotate an object by an angle (in degrees) around the provided coordinates.
@@ -1388,7 +1394,11 @@ class Geometry(object):
                 return affinity.rotate(obj, angle, origin=(px, py))
 
         try:
-            self.solid_geometry = rotate_geom(self.solid_geometry)
+            if self.multigeo is True:
+                for tool in self.tools:
+                    self.tools[tool]['solid_geometry'] = rotate_geom(self.tools[tool]['solid_geometry'])
+            else:
+                self.solid_geometry = rotate_geom(self.solid_geometry)
             self.app.inform.emit('[success]Object was rotated ...')
         except AttributeError:
             self.app.inform.emit("[error_notcl] Failed to rotate. No object selected")
@@ -1420,7 +1430,11 @@ class Geometry(object):
                 return affinity.skew(obj, angle_x, angle_y, origin=(px, py))
 
         try:
-            self.solid_geometry = skew_geom(self.solid_geometry)
+            if self.multigeo is True:
+                for tool in self.tools:
+                    self.tools[tool]['solid_geometry'] = skew_geom(self.tools[tool]['solid_geometry'])
+            else:
+                self.solid_geometry = skew_geom(self.solid_geometry)
             self.app.inform.emit('[success]Object was skewed ...')
         except AttributeError:
             self.app.inform.emit("[error_notcl] Failed to skew. No object selected")
@@ -2965,7 +2979,7 @@ class Gerber (Geometry):
             return 0, 0, 0, 0
 
         def bounds_rec(obj):
-            if type(obj) is list:
+            if type(obj) is list and type(obj) is not MultiPolygon:
                 minx = Inf
                 miny = Inf
                 maxx = -Inf
@@ -2980,7 +2994,12 @@ class Gerber (Geometry):
                             maxx = max(maxx, maxx_)
                             maxy = max(maxy, maxy_)
                     else:
-                        minx_, miny_, maxx_, maxy_ = bounds_rec(k)
+                        try:
+                            minx_, miny_, maxx_, maxy_ = bounds_rec(k)
+                        except Exception as e:
+                            log.debug("camlib.Geometry.bounds() --> %s" % str(e))
+                            return
+
                         minx = min(minx, minx_)
                         miny = min(miny, miny_)
                         maxx = max(maxx, maxx_)
@@ -3013,8 +3032,20 @@ class Gerber (Geometry):
         :rtype : None
         """
 
+        try:
+            xfactor = float(xfactor)
+        except:
+            self.app.inform.emit("[error_notcl] Scale factor has to be a number: integer or float.")
+            return
+
         if yfactor is None:
             yfactor = xfactor
+        else:
+            try:
+                yfactor = float(yfactor)
+            except:
+                self.app.inform.emit("[error_notcl] Scale factor has to be a number: integer or float.")
+                return
 
         if point is None:
             px = 0
@@ -3033,6 +3064,7 @@ class Gerber (Geometry):
                                              yfactor, origin=(px, py))
 
         self.solid_geometry = scale_geom(self.solid_geometry)
+        self.app.inform.emit("[success]Gerber Scale done.")
 
         ## solid_geometry ???
         #  It's a cascaded union of objects.
@@ -3061,8 +3093,12 @@ class Gerber (Geometry):
         :type vect: tuple
         :return: None
         """
-
-        dx, dy = vect
+        try:
+            dx, dy = vect
+        except TypeError:
+            self.app.inform.emit("[error_notcl]An (x,y) pair of values are needed. "
+                                 "Probable you entered only one value in the Offset field.")
+            return
 
         def offset_geom(obj):
             if type(obj) is list:
@@ -3076,6 +3112,7 @@ class Gerber (Geometry):
         ## Solid geometry
         # self.solid_geometry = affinity.translate(self.solid_geometry, xoff=dx, yoff=dy)
         self.solid_geometry = offset_geom(self.solid_geometry)
+        self.app.inform.emit("[success]Gerber Offset done.")
 
     def mirror(self, axis, point):
         """
@@ -4332,6 +4369,8 @@ class CNCjob(Geometry):
                  spindlespeed=None, dwell=True, dwelltime=1000,
                  toolchangez=0.787402,
                  endz=2.0,
+                 segx=None,
+                 segy=None,
                  steps_per_circle=None):
 
         # Used when parsing G-code arcs
@@ -4376,6 +4415,9 @@ class CNCjob(Geometry):
         self.dwell = dwell
         self.dwelltime = dwelltime
 
+        self.segx = float(segx) if segx is not None else 0.0
+        self.segy = float(segy) if segy is not None else 0.0
+
         self.input_geometry_bounds = None
 
         # Attributes to be included in serialization
@@ -5456,6 +5498,61 @@ class CNCjob(Geometry):
         self.solid_geometry = cascaded_union([geo['geom'] for geo in self.gcode_parsed])
         return self.solid_geometry
 
+    # code snippet added by Lei Zheng in a rejected pull request on FlatCAM https://bitbucket.org/realthunder/
+    def segment(self, coords):
+        """
+        break long linear lines to make it more auto level friendly
+        """
+
+        if len(coords) < 2 or self.segx <= 0 and self.segy <= 0:
+            return list(coords)
+
+        path = [coords[0]]
+
+        # break the line in either x or y dimension only
+        def linebreak_single(line, dim, dmax):
+            if dmax <= 0:
+                return None
+
+            if line[1][dim] > line[0][dim]:
+                sign = 1.0
+                d = line[1][dim] - line[0][dim]
+            else:
+                sign = -1.0
+                d = line[0][dim] - line[1][dim]
+            if d > dmax:
+                # make sure we don't make any new lines too short
+                if d > dmax * 2:
+                    dd = dmax
+                else:
+                    dd = d / 2
+                other = dim ^ 1
+                return (line[0][dim] + dd * sign, line[0][other] + \
+                        dd * (line[1][other] - line[0][other]) / d)
+            return None
+
+        # recursively breaks down a given line until it is within the
+        # required step size
+        def linebreak(line):
+            pt_new = linebreak_single(line, 0, self.segx)
+            if pt_new is None:
+                pt_new2 = linebreak_single(line, 1, self.segy)
+            else:
+                pt_new2 = linebreak_single((line[0], pt_new), 1, self.segy)
+            if pt_new2 is not None:
+                pt_new = pt_new2[::-1]
+
+            if pt_new is None:
+                path.append(line[1])
+            else:
+                path.append(pt_new)
+                linebreak((pt_new, line[1]))
+
+        for pt in coords[1:]:
+            linebreak((path[-1], pt))
+
+        return path
+
     def linear2gcode(self, linear, tolerance=0, down=True, up=True,
                      z_cut=None, z_move=None, zdownrate=None,
                      feedrate=None, feedrate_z=None, feedrate_rapid=None, cont=False):
@@ -5500,7 +5597,9 @@ class CNCjob(Geometry):
 
         gcode = ""
 
-        path = list(target_linear.coords)
+        # path = list(target_linear.coords)
+        path = self.segment(target_linear.coords)
+
         p = self.pp_geometry
 
         # Move fast to 1st point

+ 4 - 1
flatcamTools/ToolCalculators.py

@@ -141,6 +141,9 @@ class ToolCalculator(FlatCAMTool):
         FlatCAMTool.run(self)
         self.app.ui.notebook.setTabText(2, "Calc. Tool")
 
+    def install(self, icon=None, separator=None, **kwargs):
+        FlatCAMTool.install(self, icon, separator, shortcut='ALT+C', **kwargs)
+
     def on_calculate_tool_dia(self):
         # Calculation:
         # Manufacturer gives total angle of the the tip but we need only half of it
@@ -154,7 +157,7 @@ class ToolCalculator(FlatCAMTool):
             tip_diameter = float(self.tipDia_entry.get_value())
             half_tip_angle = float(self.tipAngle_entry.get_value()) / 2
             cut_depth = float(self.cutDepth_entry.get_value())
-        except TypeError:
+        except:
             return
 
         tool_diameter = tip_diameter + (2 * cut_depth * math.tan(math.radians(half_tip_angle)))

+ 39 - 10
flatcamTools/ToolCutout.py

@@ -9,7 +9,7 @@ from FlatCAMObj import FlatCAMGeometry, FlatCAMExcellon, FlatCAMGerber
 
 class ToolCutout(FlatCAMTool):
 
-    toolName = "Cutout PCB Tool"
+    toolName = "Cutout PCB"
 
     def __init__(self, app):
         FlatCAMTool.__init__(self, app)
@@ -101,10 +101,9 @@ class ToolCutout(FlatCAMTool):
         # 8     - 2*left + 2*right +2*top + 2*bottom
 
         # Gaps
-        self.gaps = FCEntry()
-        self.gaps_label = QtWidgets.QLabel("Type of gaps:   ")
-        self.gaps_label.setToolTip(
-            "Number of gaps used for the cutout.\n"
+        gaps_ff_label = QtWidgets.QLabel('Gaps FF:      ')
+        gaps_ff_label.setToolTip(
+            "Number of gaps used for the FreeForm cutout.\n"
             "There can be maximum 8 bridges/gaps.\n"
             "The choices are:\n"
             "- lr    - left + right\n"
@@ -114,7 +113,13 @@ class ToolCutout(FlatCAMTool):
             "- 2tb  - 2*top + 2*bottom\n"
             "- 8     - 2*left + 2*right +2*top + 2*bottom"
         )
-        form_layout_2.addRow(self.gaps_label, self.gaps)
+
+        self.gaps = FCComboBox()
+        gaps_items = ['LR', 'TB', '4', '2LR', '2TB', '8']
+        for it in gaps_items:
+            self.gaps.addItem(it)
+            self.gaps.setStyleSheet('background-color: rgb(255,255,255)')
+        form_layout_2.addRow(gaps_ff_label, self.gaps)
 
         ## Buttons
         hlay = QtWidgets.QHBoxLayout()
@@ -146,8 +151,8 @@ class ToolCutout(FlatCAMTool):
             "- one gap Left / one gap Right\n"
             "- one gap on each of the 4 sides."
         )
-        self.gaps_rect_radio = RadioSet([{'label': 'T/B', 'value': 'tb'},
-                                    {'label': 'L/R', 'value': 'lr'},
+        self.gaps_rect_radio = RadioSet([{'label': '2(T/B)', 'value': 'tb'},
+                                    {'label': '2(L/R)', 'value': 'lr'},
                                     {'label': '4', 'value': '4'}])
         form_layout_3.addRow(gapslabel_rect, self.gaps_rect_radio)
 
@@ -186,8 +191,19 @@ class ToolCutout(FlatCAMTool):
 
     def run(self):
         FlatCAMTool.run(self)
+        self.set_ui()
         self.app.ui.notebook.setTabText(2, "Cutout Tool")
 
+    def install(self, icon=None, separator=None, **kwargs):
+        FlatCAMTool.install(self, icon, separator, shortcut='ALT+U', **kwargs)
+
+    def set_ui(self):
+        self.dia.set_value(float(self.app.defaults["tools_cutouttooldia"]))
+        self.margin.set_value(float(self.app.defaults["tools_cutoutmargin"]))
+        self.gapsize.set_value(float(self.app.defaults["tools_cutoutgapsize"]))
+        self.gaps.set_value(4)
+        self.gaps_rect_radio.set_value(str(self.app.defaults["tools_gaps_rect"]))
+
     def on_freeform_cutout(self):
 
         def subtract_rectangle(obj_, x0, y0, x1, y1):
@@ -204,7 +220,8 @@ class ToolCutout(FlatCAMTool):
             return "Could not retrieve object: %s" % name
 
         if cutout_obj is None:
-            self.app.inform.emit("[error_notcl]Object not found: %s" % cutout_obj)
+            self.app.inform.emit("[error_notcl]There is no object selected for Cutout.\nSelect one and try again.")
+            return
 
         try:
             dia = float(self.dia.get_value())
@@ -236,6 +253,12 @@ class ToolCutout(FlatCAMTool):
                                  "Fill in a correct value and retry. ")
             return
 
+        if cutout_obj.multigeo is True:
+            self.app.inform.emit("[error]Cutout operation cannot be done on a multi-geo Geometry.\n"
+                                 "Optionally, this Multi-geo Geometry can be converted to Single-geo Geometry,\n"
+                                 "and after that perform Cutout.")
+            return
+
         # Get min and max data for each object as we just cut rectangles across X or Y
         xmin, ymin, xmax, ymax = cutout_obj.bounds()
         px = 0.5 * (xmin + xmax) + margin
@@ -243,7 +266,7 @@ class ToolCutout(FlatCAMTool):
         lenghtx = (xmax - xmin) + (margin * 2)
         lenghty = (ymax - ymin) + (margin * 2)
 
-        gapsize = gapsize + (dia / 2)
+        gapsize = gapsize / 2 + (dia / 2)
 
         if isinstance(cutout_obj,FlatCAMGeometry):
             # rename the obj name so it can be identified as cutout
@@ -346,6 +369,12 @@ class ToolCutout(FlatCAMTool):
             self.app.inform.emit("[error_notcl]Tool Diameter is zero value. Change it to a positive integer.")
             return "Tool Diameter is zero value. Change it to a positive integer."
 
+        if cutout_obj.multigeo is True:
+            self.app.inform.emit("[error]Cutout operation cannot be done on a multi-geo Geometry.\n"
+                                 "Optionally, this Multi-geo Geometry can be converted to Single-geo Geometry,\n"
+                                 "and after that perform Cutout.")
+            return
+
         def geo_init(geo_obj, app_obj):
             real_margin = margin + (dia / 2)
             real_gap_size = gapsize + dia

+ 22 - 14
flatcamTools/ToolDblSided.py

@@ -8,7 +8,7 @@ from PyQt5 import QtCore
 
 class DblSidedTool(FlatCAMTool):
 
-    toolName = "Double-Sided PCB Tool"
+    toolName = "2-Sided PCB"
 
     def __init__(self, app):
         FlatCAMTool.__init__(self, app)
@@ -249,10 +249,26 @@ class DblSidedTool(FlatCAMTool):
 
         self.drill_values = ""
 
+        self.set_ui()
+
+    def install(self, icon=None, separator=None, **kwargs):
+        FlatCAMTool.install(self, icon, separator, shortcut='ALT+D', **kwargs)
+
+    def run(self):
+        FlatCAMTool.run(self)
+
+        self.app.ui.notebook.setTabText(2, "2-Sided Tool")
+        self.reset_fields()
+        self.set_ui()
+
+    def set_ui(self):
         ## Initialize form
-        self.mirror_axis.set_value('X')
-        self.axis_location.set_value('point')
-        self.drill_dia.set_value(1)
+        self.point_entry.set_value("")
+        self.alignment_holes.set_value("")
+
+        self.mirror_axis.set_value(self.app.defaults["tools_2sided_mirror_axis"])
+        self.axis_location.set_value(self.app.defaults["tools_2sided_axis_loc"])
+        self.drill_dia.set_value(self.app.defaults["tools_2sided_drilldia"])
 
     def on_combo_box_type(self):
         obj_type = self.box_combo_type.currentIndex()
@@ -454,14 +470,6 @@ class DblSidedTool(FlatCAMTool):
 
 
         self.drill_values = ""
-        self.point_entry.set_value("")
-        self.alignment_holes.set_value("")
-        ## Initialize form
-        self.mirror_axis.set_value('X')
-        self.axis_location.set_value('point')
-        self.drill_dia.set_value(1)
 
-    def run(self):
-        FlatCAMTool.run(self)
-        self.app.ui.notebook.setTabText(2, "2-Sided Tool")
-        self.reset_fields()
+
+

+ 4 - 1
flatcamTools/ToolFilm.py

@@ -6,7 +6,7 @@ from PyQt5 import QtGui, QtCore, QtWidgets
 
 class Film(FlatCAMTool):
 
-    toolName = "Film PCB Tool"
+    toolName = "Film PCB"
 
     def __init__(self, app):
         FlatCAMTool.__init__(self, app)
@@ -154,6 +154,9 @@ class Film(FlatCAMTool):
         FlatCAMTool.run(self)
         self.app.ui.notebook.setTabText(2, "Film Tool")
 
+    def install(self, icon=None, separator=None, **kwargs):
+        FlatCAMTool.install(self, icon, separator, shortcut='ALT+L', **kwargs)
+
     def on_film_creation(self):
         try:
             name = self.tf_object_combo.currentText()

+ 7 - 4
flatcamTools/ToolMeasurement.py

@@ -7,12 +7,12 @@ from math import sqrt
 
 class Measurement(FlatCAMTool):
 
-    toolName = "Measurement Tool"
+    toolName = "Measurement"
 
     def __init__(self, app):
         FlatCAMTool.__init__(self, app)
 
-        self.units = self.app.general_options_form.general_group.units_radio.get_value().lower()
+        self.units = self.app.general_options_form.general_app_group.units_radio.get_value().lower()
 
         ## Title
         title_label = QtWidgets.QLabel("<font size=4><b>%s</b></font><br>" % self.toolName)
@@ -165,10 +165,13 @@ class Measurement(FlatCAMTool):
 
         # Switch notebook to tool page
         self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
-        self.units = self.app.general_options_form.general_group.units_radio.get_value().lower()
+        self.units = self.app.general_options_form.general_app_group.units_radio.get_value().lower()
         self.show()
         self.app.ui.notebook.setTabText(2, "Meas. Tool")
 
+    def install(self, icon=None, separator=None, **kwargs):
+        FlatCAMTool.install(self, icon, separator, shortcut='CTRL+M', **kwargs)
+
     def on_key_release_meas(self, event):
         if event.key == 'escape':
             # abort the measurement action
@@ -217,7 +220,7 @@ class Measurement(FlatCAMTool):
         else:
             # ENABLE the Measuring TOOL
             self.active = True
-            self.units = self.app.general_options_form.general_group.units_radio.get_value().lower()
+            self.units = self.app.general_options_form.general_app_group.units_radio.get_value().lower()
 
             # we disconnect the mouse/key handlers from wherever the measurement tool was called
             if self.app.call_source == 'app':

+ 1 - 1
flatcamTools/ToolMove.py

@@ -31,7 +31,7 @@ class ToolMove(FlatCAMTool):
         self.sel_shapes = ShapeCollection(parent=self.app.plotcanvas.vispy_canvas.view.scene, layers=1)
 
     def install(self, icon=None, separator=None, **kwargs):
-        FlatCAMTool.install(self, icon, separator, **kwargs)
+        FlatCAMTool.install(self, icon, separator, shortcut='M', **kwargs)
 
     def run(self):
         if self.app.tool_tab_locked is True:

+ 33 - 31
flatcamTools/ToolNonCopperClear.py

@@ -7,7 +7,7 @@ import time
 
 class NonCopperClear(FlatCAMTool, Gerber):
 
-    toolName = "Non-Copper Clearing Tool"
+    toolName = "Non-Copper Clearing"
 
     def __init__(self, app):
         self.app = app
@@ -234,7 +234,7 @@ class NonCopperClear(FlatCAMTool, Gerber):
         self.generate_ncc_button.clicked.connect(self.on_ncc)
 
     def install(self, icon=None, separator=None, **kwargs):
-        FlatCAMTool.install(self, icon, separator, **kwargs)
+        FlatCAMTool.install(self, icon, separator, shortcut='ALT+N', **kwargs)
 
     def run(self):
         FlatCAMTool.run(self)
@@ -244,12 +244,12 @@ class NonCopperClear(FlatCAMTool, Gerber):
         self.app.ui.notebook.setTabText(2, "NCC Tool")
 
     def set_ui(self):
-        self.ncc_overlap_entry.set_value(self.app.defaults["gerber_nccoverlap"])
-        self.ncc_margin_entry.set_value(self.app.defaults["gerber_nccmargin"])
-        self.ncc_method_radio.set_value(self.app.defaults["gerber_nccmethod"])
-        self.ncc_connect_cb.set_value(self.app.defaults["gerber_nccconnect"])
-        self.ncc_contour_cb.set_value(self.app.defaults["gerber_ncccontour"])
-        self.ncc_rest_cb.set_value(self.app.defaults["gerber_nccrest"])
+        self.ncc_overlap_entry.set_value(self.app.defaults["tools_nccoverlap"])
+        self.ncc_margin_entry.set_value(self.app.defaults["tools_nccmargin"])
+        self.ncc_method_radio.set_value(self.app.defaults["tools_nccmethod"])
+        self.ncc_connect_cb.set_value(self.app.defaults["tools_nccconnect"])
+        self.ncc_contour_cb.set_value(self.app.defaults["tools_ncccontour"])
+        self.ncc_rest_cb.set_value(self.app.defaults["tools_nccrest"])
 
         self.tools_table.setupContextMenu()
         self.tools_table.addContextMenu(
@@ -263,7 +263,6 @@ class NonCopperClear(FlatCAMTool, Gerber):
         self.default_data.update({
             "name": '_ncc',
             "plot": self.app.defaults["geometry_plot"],
-            "tooldia": self.app.defaults["geometry_painttooldia"],
             "cutz": self.app.defaults["geometry_cutz"],
             "vtipdia": 0.1,
             "vtipangle": 30,
@@ -283,24 +282,27 @@ class NonCopperClear(FlatCAMTool, Gerber):
             "spindlespeed": self.app.defaults["geometry_spindlespeed"],
             "toolchangexy": self.app.defaults["geometry_toolchangexy"],
             "startz": self.app.defaults["geometry_startz"],
-            "paintmargin": self.app.defaults["geometry_paintmargin"],
-            "paintmethod": self.app.defaults["geometry_paintmethod"],
-            "selectmethod": self.app.defaults["geometry_selectmethod"],
-            "pathconnect": self.app.defaults["geometry_pathconnect"],
-            "paintcontour": self.app.defaults["geometry_paintcontour"],
-            "paintoverlap": self.app.defaults["geometry_paintoverlap"],
-            "nccoverlap": self.app.defaults["gerber_nccoverlap"],
-            "nccmargin": self.app.defaults["gerber_nccmargin"],
-            "nccmethod": self.app.defaults["gerber_nccmethod"],
-            "nccconnect": self.app.defaults["gerber_nccconnect"],
-            "ncccontour": self.app.defaults["gerber_ncccontour"],
-            "nccrest": self.app.defaults["gerber_nccrest"]
+
+            "tooldia": self.app.defaults["tools_painttooldia"],
+            "paintmargin": self.app.defaults["tools_paintmargin"],
+            "paintmethod": self.app.defaults["tools_paintmethod"],
+            "selectmethod": self.app.defaults["tools_selectmethod"],
+            "pathconnect": self.app.defaults["tools_pathconnect"],
+            "paintcontour": self.app.defaults["tools_paintcontour"],
+            "paintoverlap": self.app.defaults["tools_paintoverlap"],
+
+            "nccoverlap": self.app.defaults["tools_nccoverlap"],
+            "nccmargin": self.app.defaults["tools_nccmargin"],
+            "nccmethod": self.app.defaults["tools_nccmethod"],
+            "nccconnect": self.app.defaults["tools_nccconnect"],
+            "ncccontour": self.app.defaults["tools_ncccontour"],
+            "nccrest": self.app.defaults["tools_nccrest"]
         })
 
         try:
-            dias = [float(eval(dia)) for dia in self.app.defaults["gerber_ncctools"].split(",")]
+            dias = [float(eval(dia)) for dia in self.app.defaults["tools_ncctools"].split(",")]
         except:
-            log.error("At least one tool diameter needed. Verify in Edit -> Preferences -> Gerber Object -> NCC Tools.")
+            log.error("At least one tool diameter needed. Verify in Edit -> Preferences -> TOOLS -> NCC Tools.")
             return
 
         self.tooluid = 0
@@ -322,13 +324,13 @@ class NonCopperClear(FlatCAMTool, Gerber):
         self.obj_name = ""
         self.ncc_obj = None
         self.tool_type_item_options = ["C1", "C2", "C3", "C4", "B", "V"]
-        self.units = self.app.general_options_form.general_group.units_radio.get_value().upper()
+        self.units = self.app.general_options_form.general_app_group.units_radio.get_value().upper()
 
     def build_ui(self):
         self.ui_disconnect()
 
         # updated units
-        self.units = self.app.general_options_form.general_group.units_radio.get_value().upper()
+        self.units = self.app.general_options_form.general_app_group.units_radio.get_value().upper()
 
         if self.units == "IN":
             self.addtool_entry.set_value(0.039)
@@ -550,22 +552,22 @@ class NonCopperClear(FlatCAMTool, Gerber):
     def on_ncc(self):
 
         over = self.ncc_overlap_entry.get_value()
-        over = over if over else self.app.defaults["gerber_nccoverlap"]
+        over = over if over else self.app.defaults["tools_nccoverlap"]
 
         margin = self.ncc_margin_entry.get_value()
-        margin = margin if margin else self.app.defaults["gerber_nccmargin"]
+        margin = margin if margin else self.app.defaults["tools_nccmargin"]
 
         connect = self.ncc_connect_cb.get_value()
-        connect = connect if connect else self.app.defaults["gerber_nccconnect"]
+        connect = connect if connect else self.app.defaults["tools_nccconnect"]
 
         contour = self.ncc_contour_cb.get_value()
-        contour = contour if contour else self.app.defaults["gerber_ncccontour"]
+        contour = contour if contour else self.app.defaults["tools_ncccontour"]
 
         clearing_method = self.ncc_rest_cb.get_value()
-        clearing_method = clearing_method if clearing_method else self.app.defaults["gerber_nccrest"]
+        clearing_method = clearing_method if clearing_method else self.app.defaults["tools_nccrest"]
 
         pol_method = self.ncc_method_radio.get_value()
-        pol_method = pol_method if pol_method else self.app.defaults["gerber_nccmethod"]
+        pol_method = pol_method if pol_method else self.app.defaults["tools_nccmethod"]
 
         self.obj_name = self.object_combo.currentText()
         # Get source object.

+ 27 - 19
flatcamTools/ToolPaint.py

@@ -5,7 +5,7 @@ from ObjectCollection import *
 
 class ToolPaint(FlatCAMTool, Gerber):
 
-    toolName = "Paint Area Tool"
+    toolName = "Paint Area"
 
     def __init__(self, app):
         self.app = app
@@ -250,7 +250,6 @@ class ToolPaint(FlatCAMTool, Gerber):
         self.default_data.update({
             "name": '_paint',
             "plot": self.app.defaults["geometry_plot"],
-            "tooldia": self.app.defaults["geometry_painttooldia"],
             "cutz": self.app.defaults["geometry_cutz"],
             "vtipdia": 0.1,
             "vtipangle": 30,
@@ -270,12 +269,14 @@ class ToolPaint(FlatCAMTool, Gerber):
             "spindlespeed": self.app.defaults["geometry_spindlespeed"],
             "toolchangexy": self.app.defaults["geometry_toolchangexy"],
             "startz": self.app.defaults["geometry_startz"],
-            "paintmargin": self.app.defaults["geometry_paintmargin"],
-            "paintmethod": self.app.defaults["geometry_paintmethod"],
-            "selectmethod": self.app.defaults["geometry_selectmethod"],
-            "pathconnect": self.app.defaults["geometry_pathconnect"],
-            "paintcontour": self.app.defaults["geometry_paintcontour"],
-            "paintoverlap": self.app.defaults["geometry_paintoverlap"]
+
+            "tooldia": self.app.defaults["tools_painttooldia"],
+            "paintmargin": self.app.defaults["tools_paintmargin"],
+            "paintmethod": self.app.defaults["tools_paintmethod"],
+            "selectmethod": self.app.defaults["tools_selectmethod"],
+            "pathconnect": self.app.defaults["tools_pathconnect"],
+            "paintcontour": self.app.defaults["tools_paintcontour"],
+            "paintoverlap": self.app.defaults["tools_paintoverlap"]
         })
 
         self.tool_type_item_options = ["C1", "C2", "C3", "C4", "B", "V"]
@@ -290,7 +291,7 @@ class ToolPaint(FlatCAMTool, Gerber):
 
 
     def install(self, icon=None, separator=None, **kwargs):
-        FlatCAMTool.install(self, icon, separator, **kwargs)
+        FlatCAMTool.install(self, icon, separator, shortcut='ALT+P', **kwargs)
 
     def run(self):
         FlatCAMTool.run(self)
@@ -311,11 +312,13 @@ class ToolPaint(FlatCAMTool, Gerber):
             self.addtool_entry.setDisabled(True)
             self.addtool_btn.setDisabled(True)
             self.deltool_btn.setDisabled(True)
+            self.tools_table.setContextMenuPolicy(Qt.NoContextMenu)
         else:
             self.rest_cb.setDisabled(False)
             self.addtool_entry.setDisabled(False)
             self.addtool_btn.setDisabled(False)
             self.deltool_btn.setDisabled(False)
+            self.tools_table.setContextMenuPolicy(Qt.ActionsContextMenu)
 
     def set_ui(self):
         ## Init the GUI interface
@@ -327,7 +330,7 @@ class ToolPaint(FlatCAMTool, Gerber):
         self.paintoverlap_entry.set_value(self.default_data["paintoverlap"])
 
         # updated units
-        self.units = self.app.general_options_form.general_group.units_radio.get_value().upper()
+        self.units = self.app.general_options_form.general_app_group.units_radio.get_value().upper()
 
         if self.units == "IN":
             self.addtool_entry.set_value(0.039)
@@ -349,7 +352,6 @@ class ToolPaint(FlatCAMTool, Gerber):
         self.default_data.update({
             "name": '_paint',
             "plot": self.app.defaults["geometry_plot"],
-            "tooldia": self.app.defaults["geometry_painttooldia"],
             "cutz": self.app.defaults["geometry_cutz"],
             "vtipdia": 0.1,
             "vtipangle": 30,
@@ -369,17 +371,23 @@ class ToolPaint(FlatCAMTool, Gerber):
             "spindlespeed": self.app.defaults["geometry_spindlespeed"],
             "toolchangexy": self.app.defaults["geometry_toolchangexy"],
             "startz": self.app.defaults["geometry_startz"],
-            "paintmargin": self.app.defaults["geometry_paintmargin"],
-            "paintmethod": self.app.defaults["geometry_paintmethod"],
-            "selectmethod": self.app.defaults["geometry_selectmethod"],
-            "pathconnect": self.app.defaults["geometry_pathconnect"],
-            "paintcontour": self.app.defaults["geometry_paintcontour"],
-            "paintoverlap": self.app.defaults["geometry_paintoverlap"]
+
+            "tooldia": self.app.defaults["tools_painttooldia"],
+            "paintmargin": self.app.defaults["tools_paintmargin"],
+            "paintmethod": self.app.defaults["tools_paintmethod"],
+            "selectmethod": self.app.defaults["tools_selectmethod"],
+            "pathconnect": self.app.defaults["tools_pathconnect"],
+            "paintcontour": self.app.defaults["tools_paintcontour"],
+            "paintoverlap": self.app.defaults["tools_paintoverlap"]
         })
 
         # call on self.on_tool_add() counts as an call to self.build_ui()
         # through this, we add a initial row / tool in the tool_table
-        self.on_tool_add(self.app.defaults["geometry_painttooldia"], muted=True)
+        self.on_tool_add(self.app.defaults["tools_painttooldia"], muted=True)
+
+        # if the Paint Method is "Single" disable the tool table context menu
+        if  self.default_data["selectmethod"] == "single":
+            self.tools_table.setContextMenuPolicy(Qt.NoContextMenu)
 
     def build_ui(self):
 
@@ -390,7 +398,7 @@ class ToolPaint(FlatCAMTool, Gerber):
             pass
 
         # updated units
-        self.units = self.app.general_options_form.general_group.units_radio.get_value().upper()
+        self.units = self.app.general_options_form.general_app_group.units_radio.get_value().upper()
 
         sorted_tools = []
         for k, v in self.paint_tools.items():

+ 4 - 1
flatcamTools/ToolPanelize.py

@@ -6,7 +6,7 @@ import time
 
 class Panelize(FlatCAMTool):
 
-    toolName = "Panelize PCB Tool"
+    toolName = "Panelize PCB"
 
     def __init__(self, app):
         super(Panelize, self).__init__(self)
@@ -197,6 +197,9 @@ class Panelize(FlatCAMTool):
         FlatCAMTool.run(self)
         self.app.ui.notebook.setTabText(2, "Panel. Tool")
 
+    def install(self, icon=None, separator=None, **kwargs):
+        FlatCAMTool.install(self, icon, separator, shortcut='ALT+Z', **kwargs)
+
     def on_panelize(self):
         name = self.object_combo.currentText()
 

+ 6 - 3
flatcamTools/ToolProperties.py

@@ -53,6 +53,9 @@ class Properties(FlatCAMTool):
         FlatCAMTool.run(self)
         self.properties()
 
+    def install(self, icon=None, separator=None, **kwargs):
+        FlatCAMTool.install(self, icon, separator, shortcut='P', **kwargs)
+
     def properties(self):
         obj_list = self.app.collection.get_selected()
         if not obj_list:
@@ -86,10 +89,10 @@ class Properties(FlatCAMTool):
         width = abs(ymax - ymin)
 
         self.addChild(dims, ['Length:', '%.4f %s' % (
-            length, self.app.general_options_form.general_group.units_radio.get_value().lower())], True)
+            length, self.app.general_options_form.general_app_group.units_radio.get_value().lower())], True)
         self.addChild(dims, ['Width:', '%.4f %s' % (
-            width, self.app.general_options_form.general_group.units_radio.get_value().lower())], True)
-        if self.app.general_options_form.general_group.units_radio.get_value().lower() == 'mm':
+            width, self.app.general_options_form.general_app_group.units_radio.get_value().lower())], True)
+        if self.app.general_options_form.general_app_group.units_radio.get_value().lower() == 'mm':
             area = (length * width) / 100
             self.addChild(dims, ['Box Area:', '%.4f %s' % (area, 'cm2')], True)
         else:

+ 3 - 0
flatcamTools/ToolTransform.py

@@ -370,6 +370,9 @@ class ToolTransform(FlatCAMTool):
         FlatCAMTool.run(self)
         self.app.ui.notebook.setTabText(2, "Transform Tool")
 
+    def install(self, icon=None, separator=None, **kwargs):
+        FlatCAMTool.install(self, icon, separator, shortcut='ALT+R', **kwargs)
+
     def on_rotate(self):
         try:
             value = float(self.rotate_entry.get_value())

+ 2 - 1
postprocessors/grbl_11.py

@@ -105,7 +105,8 @@ M0""".format(toolchangez=self.coordinate_format%(p.coords_decimals, toolchangez)
         return ('G00 ' + self.position_code(p)).format(**p)
 
     def linear_code(self, p):
-        return ('G01 ' + self.position_code(p)).format(**p) + " " + self.feedrate_code(p)
+        return ('G01 ' + self.position_code(p)).format(**p) + \
+               ' F' + str(self.feedrate_format %(p.fr_decimals, p.feedrate))
 
     def end_code(self, p):
         coords_xy = p['toolchange_xy']

+ 7 - 6
postprocessors/grbl_laser.py

@@ -36,7 +36,10 @@ class grbl_laser(FlatCAMPostProc):
         return 'M05'
 
     def down_code(self, p):
-        return 'M03'
+        if p.spindlespeed:
+            return 'M03 S%d' % p.spindlespeed
+        else:
+            return 'M03'
 
     def toolchange_code(self, p):
         return ''
@@ -52,7 +55,8 @@ class grbl_laser(FlatCAMPostProc):
         return ('G00 ' + self.position_code(p)).format(**p)
 
     def linear_code(self, p):
-        return ('G01 ' + self.position_code(p)).format(**p) + " " + self.feedrate_code(p)
+        return ('G01 ' + self.position_code(p)).format(**p) + \
+               ' F' + str(self.feedrate_format %(p.fr_decimals, p.feedrate))
 
     def end_code(self, p):
         gcode = ('G00 Z' + self.feedrate_format %(p.fr_decimals, p.endz) + "\n")
@@ -66,10 +70,7 @@ class grbl_laser(FlatCAMPostProc):
         return 'G01 F' + str(self.feedrate_format %(p.fr_decimals, p.feedrate_z))
 
     def spindle_code(self, p):
-        if p.spindlespeed:
-            return 'S%d' % p.spindlespeed
-        else:
-            return ''
+        return ''
 
     def dwell_code(self, p):
         return ''

BIN
share/toggle_units16.png


BIN
share/toggle_units32.png


Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików