Просмотр исходного кода

- whenever a FlatCAM tool is activated, if the notebook side is hidden it will be unhidden
- reactivated the Voronoi classed
- added a new parameter named Offset in the Excellon tool table - work in progress

Marius Stanciu 7 лет назад
Родитель
Сommit
d0641458e4

+ 3 - 1
FlatCAMApp.py

@@ -93,7 +93,7 @@ class App(QtCore.QObject):
 
     # Version
     version = 8.909
-    version_date = "2019/02/12"
+    version_date = "2019/02/13"
     beta = True
 
     # current date now
@@ -365,6 +365,7 @@ class App(QtCore.QObject):
             "excellon_startz": self.excellon_defaults_form.excellon_opt_group.estartz_entry,
             "excellon_endz": self.excellon_defaults_form.excellon_opt_group.eendz_entry,
             "excellon_tooldia": self.excellon_defaults_form.excellon_opt_group.tooldia_entry,
+            "excellon_offset": self.excellon_defaults_form.excellon_opt_group.offset_entry,
             "excellon_slot_tooldia": self.excellon_defaults_form.excellon_opt_group.slot_tooldia_entry,
             "excellon_gcode_type": self.excellon_defaults_form.excellon_opt_group.excellon_gcode_type_radio,
 
@@ -556,6 +557,7 @@ class App(QtCore.QObject):
             "excellon_toolchangez": 1.0,
             "excellon_toolchangexy": "0.0, 0.0",
             "excellon_tooldia": 0.016,
+            "excellon_offset": 0.0,
             "excellon_slot_tooldia": 0.016,
             "excellon_startz": None,
             "excellon_endz": 2.0,

+ 8 - 1
FlatCAMEditor.py

@@ -3532,6 +3532,7 @@ class FlatCAMExcEditor(QtCore.QObject):
         self.new_drills = []
         self.new_tools = {}
         self.new_slots = {}
+        self.new_tool_offset = {}
 
         # dictionary to store the tool_row and diameters in Tool_table
         # it will be updated everytime self.build_ui() is called
@@ -3872,7 +3873,7 @@ class FlatCAMExcEditor(QtCore.QObject):
         horizontal_header.setSectionResizeMode(3, QtWidgets.QHeaderView.ResizeToContents)
         # horizontal_header.setStretchLastSection(True)
 
-        self.tools_table_exc.setSortingEnabled(True)
+        # self.tools_table_exc.setSortingEnabled(True)
         # sort by tool diameter
         self.tools_table_exc.sortItems(1)
 
@@ -3949,6 +3950,7 @@ class FlatCAMExcEditor(QtCore.QObject):
     def on_tool_delete(self, dia=None):
         self.is_modified = True
         deleted_tool_dia_list = []
+        deleted_tool_offset_list = []
 
         try:
             if dia is None or dia is False:
@@ -3984,6 +3986,8 @@ class FlatCAMExcEditor(QtCore.QObject):
             if flag_del:
                 for tool_to_be_deleted in flag_del:
                     self.tool2tooldia.pop(tool_to_be_deleted, None)
+                    self.exc_obj.tool_offset.pop(tool_to_be_deleted, None)
+
                     # delete also the drills from points_edit dict just in case we add the tool again, we don't want to show the
                     # number of drills from before was deleter
                     self.points_edit[deleted_tool_dia] = []
@@ -4315,6 +4319,8 @@ class FlatCAMExcEditor(QtCore.QObject):
         if self.exc_obj.slots:
             self.new_slots = self.exc_obj.slots
 
+        self.new_tool_offset = self.exc_obj.tool_offset
+
         # reset the tool table
         self.tools_table_exc.clear()
         self.tools_table_exc.setHorizontalHeaderLabels(['#', 'Diameter', 'D', 'S'])
@@ -4364,6 +4370,7 @@ class FlatCAMExcEditor(QtCore.QObject):
             excellon_obj.drills = self.new_drills
             excellon_obj.tools = self.new_tools
             excellon_obj.slots = self.new_slots
+            excellon_obj.tool_offset = self.new_tool_offset
             excellon_obj.options['name'] = outname
 
             try:

+ 41 - 32
FlatCAMGUI.py

@@ -3296,14 +3296,23 @@ class ExcellonOptPrefGroupUI(OptionsGroupUI):
         self.cutz_entry = LengthEntry()
         grid2.addWidget(self.cutz_entry, 0, 1)
 
+        offsetlabel = QtWidgets.QLabel('Offset:')
+        offsetlabel.setToolTip(
+            "Some drill bits (the larger ones) need to drill deeper\n"
+            "to create the desired exit hole diameter due of the tip shape.\n"
+            "The value here can compensate the Cut Z parameter.")
+        grid2.addWidget(offsetlabel, 1, 0)
+        self.offset_entry = LengthEntry()
+        grid2.addWidget(self.offset_entry, 1, 1)
+
         travelzlabel = QtWidgets.QLabel('Travel Z:')
         travelzlabel.setToolTip(
             "Tool height when travelling\n"
             "across the XY plane."
         )
-        grid2.addWidget(travelzlabel, 1, 0)
+        grid2.addWidget(travelzlabel, 2, 0)
         self.travelz_entry = LengthEntry()
-        grid2.addWidget(self.travelz_entry, 1, 1)
+        grid2.addWidget(self.travelz_entry, 2, 1)
 
         # Tool change:
         toolchlabel = QtWidgets.QLabel("Tool change:")
@@ -3312,51 +3321,51 @@ class ExcellonOptPrefGroupUI(OptionsGroupUI):
             "in G-Code (Pause for tool change)."
         )
         self.toolchange_cb = FCCheckBox()
-        grid2.addWidget(toolchlabel, 2, 0)
-        grid2.addWidget(self.toolchange_cb, 2, 1)
+        grid2.addWidget(toolchlabel, 3, 0)
+        grid2.addWidget(self.toolchange_cb, 3, 1)
 
         toolchangezlabel = QtWidgets.QLabel('Toolchange Z:')
         toolchangezlabel.setToolTip(
             "Toolchange Z position."
         )
-        grid2.addWidget(toolchangezlabel, 3, 0)
+        grid2.addWidget(toolchangezlabel, 4, 0)
         self.toolchangez_entry = LengthEntry()
-        grid2.addWidget(self.toolchangez_entry, 3, 1)
+        grid2.addWidget(self.toolchangez_entry, 4, 1)
 
         toolchange_xy_label = QtWidgets.QLabel('Toolchange X,Y:')
         toolchange_xy_label.setToolTip(
             "Toolchange X,Y position."
         )
-        grid2.addWidget(toolchange_xy_label, 4, 0)
+        grid2.addWidget(toolchange_xy_label, 5, 0)
         self.toolchangexy_entry = FCEntry()
-        grid2.addWidget(self.toolchangexy_entry, 4, 1)
+        grid2.addWidget(self.toolchangexy_entry, 5, 1)
 
         startzlabel = QtWidgets.QLabel('Start move Z:')
         startzlabel.setToolTip(
             "Height of the tool just after start.\n"
             "Delete the value if you don't need this feature."
         )
-        grid2.addWidget(startzlabel, 5, 0)
+        grid2.addWidget(startzlabel, 6, 0)
         self.estartz_entry = FloatEntry()
-        grid2.addWidget(self.estartz_entry, 5, 1)
+        grid2.addWidget(self.estartz_entry, 6, 1)
 
         endzlabel = QtWidgets.QLabel('End move Z:')
         endzlabel.setToolTip(
             "Height of the tool after\n"
             "the last move at the end of the job."
         )
-        grid2.addWidget(endzlabel, 6, 0)
+        grid2.addWidget(endzlabel, 7, 0)
         self.eendz_entry = LengthEntry()
-        grid2.addWidget(self.eendz_entry, 6, 1)
+        grid2.addWidget(self.eendz_entry, 7, 1)
 
         frlabel = QtWidgets.QLabel('Feedrate:')
         frlabel.setToolTip(
             "Tool speed while drilling\n"
             "(in units per minute)."
         )
-        grid2.addWidget(frlabel, 7, 0)
+        grid2.addWidget(frlabel, 8, 0)
         self.feedrate_entry = LengthEntry()
-        grid2.addWidget(self.feedrate_entry, 7, 1)
+        grid2.addWidget(self.feedrate_entry, 8, 1)
 
         fr_rapid_label = QtWidgets.QLabel('Feedrate Rapids:')
         fr_rapid_label.setToolTip(
@@ -3364,9 +3373,9 @@ class ExcellonOptPrefGroupUI(OptionsGroupUI):
             "with rapid move\n"
             "(in units per minute)."
         )
-        grid2.addWidget(fr_rapid_label, 8, 0)
+        grid2.addWidget(fr_rapid_label, 9, 0)
         self.feedrate_rapid_entry = LengthEntry()
-        grid2.addWidget(self.feedrate_rapid_entry, 8, 1)
+        grid2.addWidget(self.feedrate_rapid_entry, 9, 1)
 
         # Spindle speed
         spdlabel = QtWidgets.QLabel('Spindle speed:')
@@ -3374,9 +3383,9 @@ class ExcellonOptPrefGroupUI(OptionsGroupUI):
             "Speed of the spindle\n"
             "in RPM (optional)"
         )
-        grid2.addWidget(spdlabel, 9, 0)
+        grid2.addWidget(spdlabel, 10, 0)
         self.spindlespeed_entry = IntEntry(allow_empty=True)
-        grid2.addWidget(self.spindlespeed_entry, 9, 1)
+        grid2.addWidget(self.spindlespeed_entry, 10, 1)
 
         # Dwell
         dwelllabel = QtWidgets.QLabel('Dwell:')
@@ -3390,10 +3399,10 @@ class ExcellonOptPrefGroupUI(OptionsGroupUI):
         )
         self.dwell_cb = FCCheckBox()
         self.dwelltime_entry = FCEntry()
-        grid2.addWidget(dwelllabel, 10, 0)
-        grid2.addWidget(self.dwell_cb, 10, 1)
-        grid2.addWidget(dwelltime, 11, 0)
-        grid2.addWidget(self.dwelltime_entry, 11, 1)
+        grid2.addWidget(dwelllabel, 11, 0)
+        grid2.addWidget(self.dwell_cb, 11, 1)
+        grid2.addWidget(dwelltime, 12, 0)
+        grid2.addWidget(self.dwelltime_entry, 12, 1)
 
         self.ois_dwell_exc = OptionalInputSection(self.dwell_cb, [self.dwelltime_entry])
 
@@ -3403,10 +3412,10 @@ class ExcellonOptPrefGroupUI(OptionsGroupUI):
             "The postprocessor file that dictates\n"
             "gcode output."
         )
-        grid2.addWidget(pp_excellon_label, 12, 0)
+        grid2.addWidget(pp_excellon_label, 13, 0)
         self.pp_excellon_name_cb = FCComboBox()
         self.pp_excellon_name_cb.setFocusPolicy(Qt.StrongFocus)
-        grid2.addWidget(self.pp_excellon_name_cb, 12, 1)
+        grid2.addWidget(self.pp_excellon_name_cb, 13, 1)
 
         # Probe depth
         self.pdepth_label = QtWidgets.QLabel("Probe Z depth:")
@@ -3414,18 +3423,18 @@ class ExcellonOptPrefGroupUI(OptionsGroupUI):
             "The maximum depth that the probe is allowed\n"
             "to probe. Negative value, in current units."
         )
-        grid2.addWidget(self.pdepth_label, 13, 0)
+        grid2.addWidget(self.pdepth_label, 14, 0)
         self.pdepth_entry = FCEntry()
-        grid2.addWidget(self.pdepth_entry, 13, 1)
+        grid2.addWidget(self.pdepth_entry, 14, 1)
 
         # Probe feedrate
         self.feedrate_probe_label = QtWidgets.QLabel("Feedrate Probe:")
         self.feedrate_probe_label.setToolTip(
             "The feedrate used while the probe is probing."
         )
-        grid2.addWidget(self.feedrate_probe_label, 14, 0)
+        grid2.addWidget(self.feedrate_probe_label, 15, 0)
         self.feedrate_probe_entry = FCEntry()
-        grid2.addWidget(self.feedrate_probe_entry, 14, 1)
+        grid2.addWidget(self.feedrate_probe_entry, 15, 1)
 
         fplungelabel = QtWidgets.QLabel('Fast Plunge:')
         fplungelabel.setToolTip(
@@ -3435,8 +3444,8 @@ class ExcellonOptPrefGroupUI(OptionsGroupUI):
             "WARNING: the move is done at Toolchange X,Y coords."
         )
         self.fplunge_cb = FCCheckBox()
-        grid2.addWidget(fplungelabel, 15, 0)
-        grid2.addWidget(self.fplunge_cb, 15, 1)
+        grid2.addWidget(fplungelabel, 16, 0)
+        grid2.addWidget(self.fplunge_cb, 16, 1)
 
         #### Choose what to use for Gcode creation: Drills, Slots or Both
         excellon_gcode_type_label = QtWidgets.QLabel('<b>Gcode:    </b>')
@@ -3449,8 +3458,8 @@ class ExcellonOptPrefGroupUI(OptionsGroupUI):
         self.excellon_gcode_type_radio = RadioSet([{'label': 'Drills', 'value': 'drills'},
                                           {'label': 'Slots', 'value': 'slots'},
                                           {'label': 'Both', 'value': 'both'}])
-        grid2.addWidget(excellon_gcode_type_label, 16, 0)
-        grid2.addWidget(self.excellon_gcode_type_radio, 16, 1)
+        grid2.addWidget(excellon_gcode_type_label, 17, 0)
+        grid2.addWidget(self.excellon_gcode_type_radio, 17, 1)
 
         # until I decide to implement this feature those remain disabled
         excellon_gcode_type_label.hide()

+ 69 - 0
FlatCAMObj.py

@@ -865,6 +865,9 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
         # TODO: Document this.
         self.tool_cbs = {}
 
+        # dict to hold the tool number as key and tool offset as value
+        self.tool_offset ={}
+
         # Attributes to be included in serialization
         # Always append to it because it carries contents
         # from predecessors.
@@ -1058,6 +1061,12 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
     def build_ui(self):
         FlatCAMObj.build_ui(self)
 
+        try:
+            # if connected, disconnect the signal from the slot on item_changed as it creates issues
+            self.ui.tools_table.itemChanged.disconnect()
+        except:
+            pass
+
         n = len(self.tools)
         # we have (n+2) rows because there are 'n' tools, each a row, plus the last 2 rows for totals.
         self.ui.tools_table.setRowCount(n + 2)
@@ -1116,9 +1125,20 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
                 slot_count = QtWidgets.QTableWidgetItem('')
             slot_count.setFlags(QtCore.Qt.ItemIsEnabled)
 
+            try:
+                if self.units == 'MM':
+                    t_offset = self.tool_offset[float('%.2f' % float(self.tools[tool_no]['C']))]
+                else:
+                    t_offset = self.tool_offset[float('%.3f' % float(self.tools[tool_no]['C']))]
+            except KeyError:
+                    t_offset = self.app.defaults['excellon_offset']
+            tool_offset_item = QtWidgets.QTableWidgetItem('%s' % str(t_offset))
+
             self.ui.tools_table.setItem(self.tool_row, 1, dia)  # Diameter
             self.ui.tools_table.setItem(self.tool_row, 2, drill_count)  # Number of drills per tool
             self.ui.tools_table.setItem(self.tool_row, 3, slot_count)  # Number of drills per tool
+            self.ui.tools_table.setItem(self.tool_row, 4, tool_offset_item)  # Tool offset
+
             self.tool_row += 1
 
         # add a last row with the Total number of drills
@@ -1210,6 +1230,9 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
             self.ui.slot_tooldia_entry.show()
             self.ui.generate_milling_slots_button.show()
 
+        # we reactivate the signals after the after the tool adding as we don't need to see the tool been populated
+        self.ui.tools_table.itemChanged.connect(self.on_tool_offset_edit)
+
     def set_ui(self, ui):
         """
         Configures the user interface for this object.
@@ -1254,6 +1277,16 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
         # Fill form fields
         self.to_form()
 
+        # initialize the dict that holds the tools offset
+        t_default_offset = self.app.defaults["excellon_offset"]
+        if not self.tool_offset:
+            for value in self.tools.values():
+                if self.units == 'MM':
+                    dia = float('%.2f' % float(value['C']))
+                else:
+                    dia = float('%.3f' % float(value['C']))
+                self.tool_offset[dia] = t_default_offset
+
         assert isinstance(self.ui, ExcellonObjectUI), \
             "Expected a ExcellonObjectUI, got %s" % type(self.ui)
         self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click)
@@ -1264,6 +1297,42 @@ class FlatCAMExcellon(FlatCAMObj, Excellon):
 
         self.ui.pp_excellon_name_cb.activated.connect(self.on_pp_changed)
 
+    def on_tool_offset_edit(self):
+        # if connected, disconnect the signal from the slot on item_changed as it creates issues
+        self.ui.tools_table.itemChanged.disconnect()
+        # self.tools_table_exc.selectionModel().currentChanged.disconnect()
+
+        self.is_modified = True
+
+        row_of_item_changed = self.ui.tools_table.currentRow()
+        if self.units == 'MM':
+            dia = float('%.2f' % float(self.ui.tools_table.item(row_of_item_changed, 1).text()))
+        else:
+            dia = float('%.3f' % float(self.ui.tools_table.item(row_of_item_changed, 1).text()))
+
+        current_table_offset_edited = None
+        if self.ui.tools_table.currentItem() is not None:
+            try:
+                current_table_offset_edited = float(self.ui.tools_table.currentItem().text())
+            except ValueError:
+                # try to convert comma to decimal point. if it's still not working error message and return
+                try:
+                    current_table_offset_edited = float(self.ui.tools_table.currentItem().text().replace(',', '.'))
+                    self.ui.tools_table.currentItem().setText(
+                        self.ui.tools_table.currentItem().text().replace(',', '.'))
+                except ValueError:
+                    self.app.inform.emit("[ERROR_NOTCL]Wrong value format entered, "
+                                         "use a number.")
+                    self.ui.tools_table.currentItem().setText(str(self.tool_offset[dia]))
+                    return
+
+        self.tool_offset[dia] = current_table_offset_edited
+
+        print(self.tool_offset)
+
+        # we reactivate the signals after the after the tool editing
+        self.ui.tools_table.itemChanged.connect(self.on_tool_offset_edit)
+
     def get_selected_tools_list(self):
         """
         Returns the keys to the self.tools dictionary corresponding

+ 6 - 2
ObjectUI.py

@@ -419,8 +419,8 @@ class ExcellonObjectUI(ObjectUI):
         self.tools_table = FCTable()
         self.tools_box.addWidget(self.tools_table)
 
-        self.tools_table.setColumnCount(4)
-        self.tools_table.setHorizontalHeaderLabels(['#', 'Diameter', 'D', 'S'])
+        self.tools_table.setColumnCount(5)
+        self.tools_table.setHorizontalHeaderLabels(['#', 'Diameter', 'D', 'S', 'Offset'])
         self.tools_table.setSortingEnabled(False)
 
         self.tools_table.horizontalHeaderItem(0).setToolTip(
@@ -436,6 +436,10 @@ class ExcellonObjectUI(ObjectUI):
         self.tools_table.horizontalHeaderItem(3).setToolTip(
             "The number of Slot holes. Holes that are created by\n"
             "milling them with an endmill bit.")
+        self.tools_table.horizontalHeaderItem(4).setToolTip(
+            "Some drill bits (the larger ones) need to drill deeper\n"
+            "to create the desired exit hole diameter due of the tip shape.\n"
+            "The value here can compensate the Cut Z parameter.")
 
         self.empty_label = QtWidgets.QLabel('')
         self.tools_box.addWidget(self.empty_label)

+ 6 - 0
README.md

@@ -9,6 +9,12 @@ CAD program, and create G-Code for Isolation routing.
 
 =================================================
 
+12.02.2019
+
+- whenever a FlatCAM tool is activated, if the notebook side is hidden it will be unhidden
+- reactivated the Voronoi classed
+- added a new parameter named Offset in the Excellon tool table - work in progress
+
 10.02.2019
 
 - the SELECTED type of messages are no longer printed to shell from 2 reasons: first, too much spam and second, issue with displaying html

+ 208 - 200
camlib.py

@@ -43,6 +43,8 @@ from rasterio.features import shapes
 
 from xml.dom.minidom import parseString as parse_xml_string
 
+from scipy.spatial import KDTree, Delaunay
+
 from ParseSVG import *
 from ParseDXF import *
 
@@ -6491,206 +6493,212 @@ def parse_gerber_number(strnumber, int_digits, frac_digits, zeros):
     return ret_val
 
 
-# def voronoi(P):
-#     """
-#     Returns a list of all edges of the voronoi diagram for the given input points.
-#     """
-#     delauny = Delaunay(P)
-#     triangles = delauny.points[delauny.vertices]
-#
-#     circum_centers = np.array([triangle_csc(tri) for tri in triangles])
-#     long_lines_endpoints = []
-#
-#     lineIndices = []
-#     for i, triangle in enumerate(triangles):
-#         circum_center = circum_centers[i]
-#         for j, neighbor in enumerate(delauny.neighbors[i]):
-#             if neighbor != -1:
-#                 lineIndices.append((i, neighbor))
-#             else:
-#                 ps = triangle[(j+1)%3] - triangle[(j-1)%3]
-#                 ps = np.array((ps[1], -ps[0]))
-#
-#                 middle = (triangle[(j+1)%3] + triangle[(j-1)%3]) * 0.5
-#                 di = middle - triangle[j]
-#
-#                 ps /= np.linalg.norm(ps)
-#                 di /= np.linalg.norm(di)
-#
-#                 if np.dot(di, ps) < 0.0:
-#                     ps *= -1000.0
-#                 else:
-#                     ps *= 1000.0
-#
-#                 long_lines_endpoints.append(circum_center + ps)
-#                 lineIndices.append((i, len(circum_centers) + len(long_lines_endpoints)-1))
-#
-#     vertices = np.vstack((circum_centers, long_lines_endpoints))
-#
-#     # filter out any duplicate lines
-#     lineIndicesSorted = np.sort(lineIndices) # make (1,2) and (2,1) both (1,2)
-#     lineIndicesTupled = [tuple(row) for row in lineIndicesSorted]
-#     lineIndicesUnique = np.unique(lineIndicesTupled)
-#
-#     return vertices, lineIndicesUnique
-#
-#
-# def triangle_csc(pts):
-#     rows, cols = pts.shape
-#
-#     A = np.bmat([[2 * np.dot(pts, pts.T), np.ones((rows, 1))],
-#                  [np.ones((1, rows)), np.zeros((1, 1))]])
-#
-#     b = np.hstack((np.sum(pts * pts, axis=1), np.ones((1))))
-#     x = np.linalg.solve(A,b)
-#     bary_coords = x[:-1]
-#     return np.sum(pts * np.tile(bary_coords.reshape((pts.shape[0], 1)), (1, pts.shape[1])), axis=0)
-#
-#
-# def voronoi_cell_lines(points, vertices, lineIndices):
-#     """
-#     Returns a mapping from a voronoi cell to its edges.
-#
-#     :param points: shape (m,2)
-#     :param vertices: shape (n,2)
-#     :param lineIndices: shape (o,2)
-#     :rtype: dict point index -> list of shape (n,2) with vertex indices
-#     """
-#     kd = KDTree(points)
-#
-#     cells = collections.defaultdict(list)
-#     for i1, i2 in lineIndices:
-#         v1, v2 = vertices[i1], vertices[i2]
-#         mid = (v1+v2)/2
-#         _, (p1Idx, p2Idx) = kd.query(mid, 2)
-#         cells[p1Idx].append((i1, i2))
-#         cells[p2Idx].append((i1, i2))
-#
-#     return cells
-#
-#
-# def voronoi_edges2polygons(cells):
-#     """
-#     Transforms cell edges into polygons.
-#
-#     :param cells: as returned from voronoi_cell_lines
-#     :rtype: dict point index -> list of vertex indices which form a polygon
-#     """
-#
-#     # first, close the outer cells
-#     for pIdx, lineIndices_ in cells.items():
-#         dangling_lines = []
-#         for i1, i2 in lineIndices_:
-#             connections = filter(lambda (i1_, i2_): (i1, i2) != (i1_, i2_) and (i1 == i1_ or i1 == i2_ or i2 == i1_ or i2 == i2_), lineIndices_)
-#             assert 1 <= len(connections) <= 2
-#             if len(connections) == 1:
-#                 dangling_lines.append((i1, i2))
-#         assert len(dangling_lines) in [0, 2]
-#         if len(dangling_lines) == 2:
-#             (i11, i12), (i21, i22) = dangling_lines
-#
-#             # determine which line ends are unconnected
-#             connected = filter(lambda (i1,i2): (i1,i2) != (i11,i12) and (i1 == i11 or i2 == i11), lineIndices_)
-#             i11Unconnected = len(connected) == 0
-#
-#             connected = filter(lambda (i1,i2): (i1,i2) != (i21,i22) and (i1 == i21 or i2 == i21), lineIndices_)
-#             i21Unconnected = len(connected) == 0
-#
-#             startIdx = i11 if i11Unconnected else i12
-#             endIdx = i21 if i21Unconnected else i22
-#
-#             cells[pIdx].append((startIdx, endIdx))
-#
-#     # then, form polygons by storing vertex indices in (counter-)clockwise order
-#     polys = dict()
-#     for pIdx, lineIndices_ in cells.items():
-#         # get a directed graph which contains both directions and arbitrarily follow one of both
-#         directedGraph = lineIndices_ + [(i2, i1) for (i1, i2) in lineIndices_]
-#         directedGraphMap = collections.defaultdict(list)
-#         for (i1, i2) in directedGraph:
-#             directedGraphMap[i1].append(i2)
-#         orderedEdges = []
-#         currentEdge = directedGraph[0]
-#         while len(orderedEdges) < len(lineIndices_):
-#             i1 = currentEdge[1]
-#             i2 = directedGraphMap[i1][0] if directedGraphMap[i1][0] != currentEdge[0] else directedGraphMap[i1][1]
-#             nextEdge = (i1, i2)
-#             orderedEdges.append(nextEdge)
-#             currentEdge = nextEdge
-#
-#         polys[pIdx] = [i1 for (i1, i2) in orderedEdges]
-#
-#     return polys
-#
-#
-# def voronoi_polygons(points):
-#     """
-#     Returns the voronoi polygon for each input point.
-#
-#     :param points: shape (n,2)
-#     :rtype: list of n polygons where each polygon is an array of vertices
-#     """
-#     vertices, lineIndices = voronoi(points)
-#     cells = voronoi_cell_lines(points, vertices, lineIndices)
-#     polys = voronoi_edges2polygons(cells)
-#     polylist = []
-#     for i in xrange(len(points)):
-#         poly = vertices[np.asarray(polys[i])]
-#         polylist.append(poly)
-#     return polylist
-#
-#
-# class Zprofile:
-#     def __init__(self):
-#
-#         # data contains lists of [x, y, z]
-#         self.data = []
-#
-#         # Computed voronoi polygons (shapely)
-#         self.polygons = []
-#         pass
-#
-#     def plot_polygons(self):
-#         axes = plt.subplot(1, 1, 1)
-#
-#         plt.axis([-0.05, 1.05, -0.05, 1.05])
-#
-#         for poly in self.polygons:
-#             p = PolygonPatch(poly, facecolor=np.random.rand(3, 1), alpha=0.3)
-#             axes.add_patch(p)
-#
-#     def init_from_csv(self, filename):
-#         pass
-#
-#     def init_from_string(self, zpstring):
-#         pass
-#
-#     def init_from_list(self, zplist):
-#         self.data = zplist
-#
-#     def generate_polygons(self):
-#         self.polygons = [Polygon(p) for p in voronoi_polygons(array([[x[0], x[1]] for x in self.data]))]
-#
-#     def normalize(self, origin):
-#         pass
-#
-#     def paste(self, path):
-#         """
-#         Return a list of dictionaries containing the parts of the original
-#         path and their z-axis offset.
-#         """
-#
-#         # At most one region/polygon will contain the path
-#         containing = [i for i in range(len(self.polygons)) if self.polygons[i].contains(path)]
-#
-#         if len(containing) > 0:
-#             return [{"path": path, "z": self.data[containing[0]][2]}]
-#
-#         # All region indexes that intersect with the path
-#         crossing = [i for i in range(len(self.polygons)) if self.polygons[i].intersects(path)]
-#
-#         return [{"path": path.intersection(self.polygons[i]),
-#                  "z": self.data[i][2]} for i in crossing]
+def voronoi(P):
+    """
+    Returns a list of all edges of the voronoi diagram for the given input points.
+    """
+    delauny = Delaunay(P)
+    triangles = delauny.points[delauny.vertices]
+
+    circum_centers = np.array([triangle_csc(tri) for tri in triangles])
+    long_lines_endpoints = []
+
+    lineIndices = []
+    for i, triangle in enumerate(triangles):
+        circum_center = circum_centers[i]
+        for j, neighbor in enumerate(delauny.neighbors[i]):
+            if neighbor != -1:
+                lineIndices.append((i, neighbor))
+            else:
+                ps = triangle[(j+1)%3] - triangle[(j-1)%3]
+                ps = np.array((ps[1], -ps[0]))
+
+                middle = (triangle[(j+1)%3] + triangle[(j-1)%3]) * 0.5
+                di = middle - triangle[j]
+
+                ps /= np.linalg.norm(ps)
+                di /= np.linalg.norm(di)
+
+                if np.dot(di, ps) < 0.0:
+                    ps *= -1000.0
+                else:
+                    ps *= 1000.0
+
+                long_lines_endpoints.append(circum_center + ps)
+                lineIndices.append((i, len(circum_centers) + len(long_lines_endpoints)-1))
+
+    vertices = np.vstack((circum_centers, long_lines_endpoints))
+
+    # filter out any duplicate lines
+    lineIndicesSorted = np.sort(lineIndices) # make (1,2) and (2,1) both (1,2)
+    lineIndicesTupled = [tuple(row) for row in lineIndicesSorted]
+    lineIndicesUnique = np.unique(lineIndicesTupled)
+
+    return vertices, lineIndicesUnique
+
+
+def triangle_csc(pts):
+    rows, cols = pts.shape
+
+    A = np.bmat([[2 * np.dot(pts, pts.T), np.ones((rows, 1))],
+                 [np.ones((1, rows)), np.zeros((1, 1))]])
+
+    b = np.hstack((np.sum(pts * pts, axis=1), np.ones((1))))
+    x = np.linalg.solve(A,b)
+    bary_coords = x[:-1]
+    return np.sum(pts * np.tile(bary_coords.reshape((pts.shape[0], 1)), (1, pts.shape[1])), axis=0)
+
+
+def voronoi_cell_lines(points, vertices, lineIndices):
+    """
+    Returns a mapping from a voronoi cell to its edges.
+
+    :param points: shape (m,2)
+    :param vertices: shape (n,2)
+    :param lineIndices: shape (o,2)
+    :rtype: dict point index -> list of shape (n,2) with vertex indices
+    """
+    kd = KDTree(points)
+
+    cells = collections.defaultdict(list)
+    for i1, i2 in lineIndices:
+        v1, v2 = vertices[i1], vertices[i2]
+        mid = (v1+v2)/2
+        _, (p1Idx, p2Idx) = kd.query(mid, 2)
+        cells[p1Idx].append((i1, i2))
+        cells[p2Idx].append((i1, i2))
+
+    return cells
+
+
+def voronoi_edges2polygons(cells):
+    """
+    Transforms cell edges into polygons.
+
+    :param cells: as returned from voronoi_cell_lines
+    :rtype: dict point index -> list of vertex indices which form a polygon
+    """
+
+    # first, close the outer cells
+    for pIdx, lineIndices_ in cells.items():
+        dangling_lines = []
+        for i1, i2 in lineIndices_:
+            p = (i1, i2)
+            connections = filter(lambda k: p != k and (p[0] == k[0] or p[0] == k[1] or p[1] == k[0] or p[1] == k[1]), lineIndices_)
+            # connections = filter(lambda (i1_, i2_): (i1, i2) != (i1_, i2_) and (i1 == i1_ or i1 == i2_ or i2 == i1_ or i2 == i2_), lineIndices_)
+            assert 1 <= len(connections) <= 2
+            if len(connections) == 1:
+                dangling_lines.append((i1, i2))
+        assert len(dangling_lines) in [0, 2]
+        if len(dangling_lines) == 2:
+            (i11, i12), (i21, i22) = dangling_lines
+            s = (i11, i12)
+            t = (i21, i22)
+
+            # determine which line ends are unconnected
+            connected = filter(lambda k: k != s and (k[0] == s[0] or k[1] == s[0]), lineIndices_)
+            # connected = filter(lambda (i1,i2): (i1,i2) != (i11,i12) and (i1 == i11 or i2 == i11), lineIndices_)
+            i11Unconnected = len(connected) == 0
+
+            connected = filter(lambda k: k != t and (k[0] == t[0] or k[1] == t[0]), lineIndices_)
+            # connected = filter(lambda (i1,i2): (i1,i2) != (i21,i22) and (i1 == i21 or i2 == i21), lineIndices_)
+            i21Unconnected = len(connected) == 0
+
+            startIdx = i11 if i11Unconnected else i12
+            endIdx = i21 if i21Unconnected else i22
+
+            cells[pIdx].append((startIdx, endIdx))
+
+    # then, form polygons by storing vertex indices in (counter-)clockwise order
+    polys = dict()
+    for pIdx, lineIndices_ in cells.items():
+        # get a directed graph which contains both directions and arbitrarily follow one of both
+        directedGraph = lineIndices_ + [(i2, i1) for (i1, i2) in lineIndices_]
+        directedGraphMap = collections.defaultdict(list)
+        for (i1, i2) in directedGraph:
+            directedGraphMap[i1].append(i2)
+        orderedEdges = []
+        currentEdge = directedGraph[0]
+        while len(orderedEdges) < len(lineIndices_):
+            i1 = currentEdge[1]
+            i2 = directedGraphMap[i1][0] if directedGraphMap[i1][0] != currentEdge[0] else directedGraphMap[i1][1]
+            nextEdge = (i1, i2)
+            orderedEdges.append(nextEdge)
+            currentEdge = nextEdge
+
+        polys[pIdx] = [i1 for (i1, i2) in orderedEdges]
+
+    return polys
+
+
+def voronoi_polygons(points):
+    """
+    Returns the voronoi polygon for each input point.
+
+    :param points: shape (n,2)
+    :rtype: list of n polygons where each polygon is an array of vertices
+    """
+    vertices, lineIndices = voronoi(points)
+    cells = voronoi_cell_lines(points, vertices, lineIndices)
+    polys = voronoi_edges2polygons(cells)
+    polylist = []
+    for i in range(len(points)):
+        poly = vertices[np.asarray(polys[i])]
+        polylist.append(poly)
+    return polylist
+
+
+class Zprofile:
+    def __init__(self):
+
+        # data contains lists of [x, y, z]
+        self.data = []
+
+        # Computed voronoi polygons (shapely)
+        self.polygons = []
+        pass
+
+    # def plot_polygons(self):
+    #     axes = plt.subplot(1, 1, 1)
+    #
+    #     plt.axis([-0.05, 1.05, -0.05, 1.05])
+    #
+    #     for poly in self.polygons:
+    #         p = PolygonPatch(poly, facecolor=np.random.rand(3, 1), alpha=0.3)
+    #         axes.add_patch(p)
+
+    def init_from_csv(self, filename):
+        pass
+
+    def init_from_string(self, zpstring):
+        pass
+
+    def init_from_list(self, zplist):
+        self.data = zplist
+
+    def generate_polygons(self):
+        self.polygons = [Polygon(p) for p in voronoi_polygons(array([[x[0], x[1]] for x in self.data]))]
+
+    def normalize(self, origin):
+        pass
+
+    def paste(self, path):
+        """
+        Return a list of dictionaries containing the parts of the original
+        path and their z-axis offset.
+        """
+
+        # At most one region/polygon will contain the path
+        containing = [i for i in range(len(self.polygons)) if self.polygons[i].contains(path)]
+
+        if len(containing) > 0:
+            return [{"path": path, "z": self.data[containing[0]][2]}]
+
+        # All region indexes that intersect with the path
+        crossing = [i for i in range(len(self.polygons)) if self.polygons[i].intersects(path)]
+
+        return [{"path": path.intersection(self.polygons[i]),
+                 "z": self.data[i][2]} for i in crossing]
 
 
 def autolist(obj):

+ 5 - 0
flatcamTools/ToolCalculators.py

@@ -220,6 +220,11 @@ class ToolCalculator(FlatCAMTool):
 
         FlatCAMTool.run(self)
         self.set_tool_ui()
+
+        # if the splitter us hidden, display it
+        if self.app.ui.splitter.sizes()[0] == 0:
+            self.app.ui.splitter.setSizes([1, 1])
+
         self.app.ui.notebook.setTabText(2, "Calc. Tool")
 
     def install(self, icon=None, separator=None, **kwargs):

+ 5 - 0
flatcamTools/ToolCutOut.py

@@ -196,6 +196,11 @@ class ToolCutOut(FlatCAMTool):
 
         FlatCAMTool.run(self)
         self.set_tool_ui()
+
+        # if the splitter us hidden, display it
+        if self.app.ui.splitter.sizes()[0] == 0:
+            self.app.ui.splitter.setSizes([1, 1])
+
         self.app.ui.notebook.setTabText(2, "Cutout Tool")
 
     def install(self, icon=None, separator=None, **kwargs):

+ 5 - 0
flatcamTools/ToolDblSided.py

@@ -259,6 +259,11 @@ class DblSidedTool(FlatCAMTool):
 
         FlatCAMTool.run(self)
         self.set_tool_ui()
+
+        # if the splitter us hidden, display it
+        if self.app.ui.splitter.sizes()[0] == 0:
+            self.app.ui.splitter.setSizes([1, 1])
+
         self.app.ui.notebook.setTabText(2, "2-Sided Tool")
 
     def set_tool_ui(self):

+ 5 - 0
flatcamTools/ToolFilm.py

@@ -161,6 +161,11 @@ class Film(FlatCAMTool):
 
         FlatCAMTool.run(self)
         self.set_tool_ui()
+
+        # if the splitter us hidden, display it
+        if self.app.ui.splitter.sizes()[0] == 0:
+            self.app.ui.splitter.setSizes([1, 1])
+
         self.app.ui.notebook.setTabText(2, "Film Tool")
 
     def install(self, icon=None, separator=None, **kwargs):

+ 5 - 0
flatcamTools/ToolImage.py

@@ -129,6 +129,11 @@ class ToolImage(FlatCAMTool):
 
         FlatCAMTool.run(self)
         self.set_tool_ui()
+
+        # if the splitter us hidden, display it
+        if self.app.ui.splitter.sizes()[0] == 0:
+            self.app.ui.splitter.setSizes([1, 1])
+
         self.app.ui.notebook.setTabText(2, "Image Tool")
 
     def install(self, icon=None, separator=None, **kwargs):

+ 5 - 0
flatcamTools/ToolNonCopperClear.py

@@ -243,6 +243,11 @@ class NonCopperClear(FlatCAMTool, Gerber):
 
         FlatCAMTool.run(self)
         self.set_tool_ui()
+
+        # if the splitter us hidden, display it
+        if self.app.ui.splitter.sizes()[0] == 0:
+            self.app.ui.splitter.setSizes([1, 1])
+
         self.build_ui()
         self.app.ui.notebook.setTabText(2, "NCC Tool")
 

+ 5 - 0
flatcamTools/ToolPaint.py

@@ -299,6 +299,11 @@ class ToolPaint(FlatCAMTool, Gerber):
 
         FlatCAMTool.run(self)
         self.set_tool_ui()
+
+        # if the splitter us hidden, display it
+        if self.app.ui.splitter.sizes()[0] == 0:
+            self.app.ui.splitter.setSizes([1, 1])
+
         self.app.ui.notebook.setTabText(2, "Paint Tool")
 
     def on_radio_selection(self):

+ 5 - 0
flatcamTools/ToolPanelize.py

@@ -184,6 +184,11 @@ class Panelize(FlatCAMTool):
 
         FlatCAMTool.run(self)
         self.set_tool_ui()
+
+        # if the splitter us hidden, display it
+        if self.app.ui.splitter.sizes()[0] == 0:
+            self.app.ui.splitter.setSizes([1, 1])
+
         self.app.ui.notebook.setTabText(2, "Panel. Tool")
 
     def install(self, icon=None, separator=None, **kwargs):

+ 5 - 0
flatcamTools/ToolProperties.py

@@ -47,6 +47,11 @@ class Properties(FlatCAMTool):
         if self.app.tool_tab_locked is True:
             return
         self.set_tool_ui()
+
+        # if the splitter us hidden, display it
+        if self.app.ui.splitter.sizes()[0] == 0:
+            self.app.ui.splitter.setSizes([1, 1])
+
         FlatCAMTool.run(self)
         self.properties()
 

+ 5 - 0
flatcamTools/ToolTransform.py

@@ -360,6 +360,11 @@ class ToolTransform(FlatCAMTool):
 
         FlatCAMTool.run(self)
         self.set_tool_ui()
+
+        # if the splitter us hidden, display it
+        if self.app.ui.splitter.sizes()[0] == 0:
+            self.app.ui.splitter.setSizes([1, 1])
+
         self.app.ui.notebook.setTabText(2, "Transform Tool")
 
     def install(self, icon=None, separator=None, **kwargs):