From d0641458e4354a7b0e2ced745926b11926bdb58c Mon Sep 17 00:00:00 2001 From: Marius Stanciu Date: Tue, 12 Feb 2019 04:00:11 +0200 Subject: [PATCH] - 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 --- FlatCAMApp.py | 4 +- FlatCAMEditor.py | 9 +- FlatCAMGUI.py | 73 +++--- FlatCAMObj.py | 69 +++++ ObjectUI.py | 8 +- README.md | 6 + camlib.py | 408 +++++++++++++++-------------- flatcamTools/ToolCalculators.py | 5 + flatcamTools/ToolCutOut.py | 5 + flatcamTools/ToolDblSided.py | 5 + flatcamTools/ToolFilm.py | 5 + flatcamTools/ToolImage.py | 5 + flatcamTools/ToolNonCopperClear.py | 5 + flatcamTools/ToolPaint.py | 5 + flatcamTools/ToolPanelize.py | 5 + flatcamTools/ToolProperties.py | 5 + flatcamTools/ToolTransform.py | 5 + 17 files changed, 391 insertions(+), 236 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 2fb7ff10..9f770020 100644 --- a/FlatCAMApp.py +++ b/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, diff --git a/FlatCAMEditor.py b/FlatCAMEditor.py index 3b94c51c..7e4d4b49 100644 --- a/FlatCAMEditor.py +++ b/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: diff --git a/FlatCAMGUI.py b/FlatCAMGUI.py index bf5e1d60..a68649ef 100644 --- a/FlatCAMGUI.py +++ b/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('Gcode: ') @@ -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() diff --git a/FlatCAMObj.py b/FlatCAMObj.py index 9a9c437f..7a186199 100644 --- a/FlatCAMObj.py +++ b/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 diff --git a/ObjectUI.py b/ObjectUI.py index bad4575e..00116f40 100644 --- a/ObjectUI.py +++ b/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) diff --git a/README.md b/README.md index 743b92f6..534ce0f1 100644 --- a/README.md +++ b/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 diff --git a/camlib.py b/camlib.py index fda02701..8d020053 100644 --- a/camlib.py +++ b/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): diff --git a/flatcamTools/ToolCalculators.py b/flatcamTools/ToolCalculators.py index e74393ed..98e03354 100644 --- a/flatcamTools/ToolCalculators.py +++ b/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): diff --git a/flatcamTools/ToolCutOut.py b/flatcamTools/ToolCutOut.py index 6176808d..9a2e4a9c 100644 --- a/flatcamTools/ToolCutOut.py +++ b/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): diff --git a/flatcamTools/ToolDblSided.py b/flatcamTools/ToolDblSided.py index 3223616e..f7828618 100644 --- a/flatcamTools/ToolDblSided.py +++ b/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): diff --git a/flatcamTools/ToolFilm.py b/flatcamTools/ToolFilm.py index 759a1580..1c508233 100644 --- a/flatcamTools/ToolFilm.py +++ b/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): diff --git a/flatcamTools/ToolImage.py b/flatcamTools/ToolImage.py index 4e4d92c8..e03a915f 100644 --- a/flatcamTools/ToolImage.py +++ b/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): diff --git a/flatcamTools/ToolNonCopperClear.py b/flatcamTools/ToolNonCopperClear.py index e8f7f041..e739f60c 100644 --- a/flatcamTools/ToolNonCopperClear.py +++ b/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") diff --git a/flatcamTools/ToolPaint.py b/flatcamTools/ToolPaint.py index a0aa5bc5..945c6030 100644 --- a/flatcamTools/ToolPaint.py +++ b/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): diff --git a/flatcamTools/ToolPanelize.py b/flatcamTools/ToolPanelize.py index 11549d08..5ae48515 100644 --- a/flatcamTools/ToolPanelize.py +++ b/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): diff --git a/flatcamTools/ToolProperties.py b/flatcamTools/ToolProperties.py index 1cfb8fd7..ffdbe136 100644 --- a/flatcamTools/ToolProperties.py +++ b/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() diff --git a/flatcamTools/ToolTransform.py b/flatcamTools/ToolTransform.py index 3652e2e6..77c6561e 100644 --- a/flatcamTools/ToolTransform.py +++ b/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):