diff --git a/CHANGELOG.md b/CHANGELOG.md index 52a28b4d..7bdf5b2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ CHANGELOG for FlatCAM beta - NCC Tool optimization - optimized the Tool edit method - NCC Tool - allow no tool at NCC Tool start (the Preferences have no tool) - NCC Tool - optimized tool reset code +- NCC Tool - fixed the non-rest copper clearing to work as expected: each tool in the tool table will make it's own copper clearing without interference from the rest of the tools 11.06.2020 diff --git a/appTools/ToolNCC.py b/appTools/ToolNCC.py index 859a9ad1..3a89cc1a 100644 --- a/appTools/ToolNCC.py +++ b/appTools/ToolNCC.py @@ -525,7 +525,7 @@ class NonCopperClear(AppTool, Gerber): 'offset': 'Path', 'offset_value': 0.0, 'type': 'Iso', - 'tool_type': self.ui.tool_type_radio.get_value(), + 'tool_type': self.app.defaults["tools_ncctool_type"], 'data': deepcopy(self.default_data), 'solid_geometry': [] } @@ -578,42 +578,27 @@ class NonCopperClear(AppTool, Gerber): for tooluid_key, tooluid_value in self.ncc_tools.items(): if float('%.*f' % (self.decimals, tooluid_value['tooldia'])) == tool_sorted: tool_id += 1 + id_ = QtWidgets.QTableWidgetItem('%d' % int(tool_id)) id_.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled) row_no = tool_id - 1 self.ui.tools_table.setItem(row_no, 0, id_) # Tool name/id - # Make sure that the drill diameter when in MM is with no more than 2 decimals - # There are no drill bits in MM with more than 2 decimals diameter - # For INCH the decimals should be no more than 4. There are no drills under 10mils + # Make sure that the drill diameter when in MM is with no more than self.decimals decimals dia = QtWidgets.QTableWidgetItem('%.*f' % (self.decimals, tooluid_value['tooldia'])) - dia.setFlags(QtCore.Qt.ItemIsEnabled) + self.ui.tools_table.setItem(row_no, 1, dia) # Diameter tool_type_item = FCComboBox() tool_type_item.addItems(self.tool_type_item_options) - - # tool_type_item.setStyleSheet('background-color: rgb(255,255,255)') idx = tool_type_item.findText(tooluid_value['tool_type']) tool_type_item.setCurrentIndex(idx) - - tool_uid_item = QtWidgets.QTableWidgetItem(str(int(tooluid_key))) - - # operation_type = FCComboBox() - # operation_type.addItems(['iso_op', 'clear_op']) - # - # # operation_type.setStyleSheet('background-color: rgb(255,255,255)') - # op_idx = operation_type.findText(tooluid_value['operation']) - # operation_type.setCurrentIndex(op_idx) - - self.ui.tools_table.setItem(row_no, 1, dia) # Diameter self.ui.tools_table.setCellWidget(row_no, 2, tool_type_item) + tool_uid_item = QtWidgets.QTableWidgetItem(str(int(tooluid_key))) # ## REMEMBER: THIS COLUMN IS HIDDEN IN OBJECTUI.PY # ## self.ui.tools_table.setItem(row_no, 3, tool_uid_item) # Tool unique ID - # self.ui.tools_table.setCellWidget(row_no, 4, operation_type) - # make the diameter column editable for row in range(tool_id): self.ui.tools_table.item(row, 1).setFlags( @@ -925,7 +910,7 @@ class NonCopperClear(AppTool, Gerber): 'offset': 'Path', 'offset_value': 0.0, 'type': 'Iso', - 'tool_type': self.ui.tool_type_radio.get_value(), + 'tool_type': self.app.defaults["tools_ncctool_type"], 'data': deepcopy(self.default_data), 'solid_geometry': [] } @@ -957,22 +942,21 @@ class NonCopperClear(AppTool, Gerber): # identify the tool that was edited and get it's tooluid if new_tool_dia not in tool_dias: - self.ncc_tools[editeduid]['tooldia'] = new_tool_dia + self.ncc_tools[editeduid]['tooldia'] = deepcopy(float('%.*f' % (self.decimals, new_tool_dia))) self.app.inform.emit('[success] %s' % _("Tool from Tool Table was edited.")) self.blockSignals(False) self.build_ui() return - else: - # identify the old tool_dia and restore the text in tool table - for k, v in self.ncc_tools.items(): - if k == editeduid: - old_tool_dia = v['tooldia'] - restore_dia_item = self.ui.tools_table.item(edited_row, 1) - restore_dia_item.setText(str(old_tool_dia)) - break - self.app.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled. " - "New diameter value is already in the Tool Table.")) + # identify the old tool_dia and restore the text in tool table + for k, v in self.ncc_tools.items(): + if k == editeduid: + old_tool_dia = v['tooldia'] + restore_dia_item = self.ui.tools_table.item(edited_row, 1) + restore_dia_item.setText(str(old_tool_dia)) + break + + self.app.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled. New diameter value is already in the Tool Table.")) self.blockSignals(False) self.build_ui() @@ -1039,13 +1023,20 @@ class NonCopperClear(AppTool, Gerber): :return: None """ - # init values for the next usage - self.set_tool_ui() + self.app.defaults.report_usage("on_ncc_click") - self.app.defaults.report_usage("on_paint_button_click") + self.first_click = False + self.cursor_pos = None + self.mouse_is_dragging = False + + prog_plot = True if self.app.defaults["tools_ncc_plotting"] == 'progressive' else False + if prog_plot: + self.temp_shapes.clear(update=True) + + self.sel_rect = [] self.grb_circle_steps = int(self.app.defaults["gerber_circle_steps"]) - self.obj_name = self.object_combo.currentText() + self.obj_name = self.ui.object_combo.currentText() # Get source object. try: @@ -1102,7 +1093,8 @@ class NonCopperClear(AppTool, Gerber): self.clear_copper(ncc_obj=self.ncc_obj, ncctooldia=self.ncc_dia_list, isotooldia=self.iso_dia_list, - outname=self.o_name) + outname=self.o_name, + tools_storage=self.ncc_tools) elif self.select_method == _("Area Selection"): self.app.inform.emit('[WARNING_NOTCL] %s' % _("Click the start point of the area.")) @@ -1493,7 +1485,7 @@ class NonCopperClear(AppTool, Gerber): :param has_offset: :param ncc_offset: :param ncc_margin: - :param bounding_box: + :param bounding_box: only this area is kept :param tools_storage: :return: """ @@ -1521,13 +1513,11 @@ class NonCopperClear(AppTool, Gerber): self.app.inform.emit('[success] %s ...' % _("Buffering finished")) empty = self.get_ncc_empty_area(target=sol_geo, boundary=bounding_box) - if empty == 'fail': + if empty == 'fail' or empty.is_empty: + msg = '[ERROR_NOTCL] %s' % _("Could not get the extent of the area to be non copper cleared.") + self.app.inform.emit(msg) return 'fail' - if empty.is_empty: - self.app.inform.emit('[ERROR_NOTCL] %s' % - _("Could not get the extent of the area to be non copper cleared.")) - return 'fail' elif ncc_obj.kind == 'gerber' and isotooldia: isolated_geo = [] @@ -1550,7 +1540,7 @@ class NonCopperClear(AppTool, Gerber): else: isolated_geo = self.generate_envelope(tool_iso / 2, 0) - if isolated_geo == 'fail': + if isolated_geo == 'fail' or isolated_geo.is_empty: self.app.inform.emit('[ERROR_NOTCL] %s %s' % (_("Isolation geometry could not be generated."), str(tool_iso))) continue @@ -1611,7 +1601,6 @@ class NonCopperClear(AppTool, Gerber): warning_flag += 1 break - current_uid = 0 for k, v in tools_storage.items(): if float('%.*f' % (self.decimals, v['tooldia'])) == float('%.*f' % (self.decimals, tool_iso)): @@ -1620,8 +1609,8 @@ class NonCopperClear(AppTool, Gerber): # and then reset the temporary list that stored that solid_geometry v['solid_geometry'] = deepcopy(new_geometry) v['data']['name'] = name + geo_obj.tools[current_uid] = dict(tools_storage[current_uid]) break - geo_obj.tools[current_uid] = dict(tools_storage[current_uid]) sol_geo = cascaded_union(isolated_geo) if has_offset is True: @@ -1630,13 +1619,11 @@ class NonCopperClear(AppTool, Gerber): self.app.inform.emit('[success] %s ...' % _("Buffering finished")) empty = self.get_ncc_empty_area(target=sol_geo, boundary=bounding_box) - if empty == 'fail': + if empty == 'fail' or empty.is_empty: + msg = '[ERROR_NOTCL] %s' % _("Could not get the extent of the area to be non copper cleared.") + self.app.inform.emit(msg) return 'fail' - if empty.is_empty: - self.app.inform.emit('[ERROR_NOTCL] %s' % - _("Isolation geometry is broken. Margin is less than isolation tool diameter.")) - return 'fail' elif ncc_obj.kind == 'geometry': sol_geo = cascaded_union(ncc_obj.solid_geometry) if has_offset is True: @@ -1644,12 +1631,9 @@ class NonCopperClear(AppTool, Gerber): sol_geo = sol_geo.buffer(distance=ncc_offset) self.app.inform.emit('[success] %s ...' % _("Buffering finished")) empty = self.get_ncc_empty_area(target=sol_geo, boundary=bounding_box) - if empty == 'fail': - return 'fail' - - if empty.is_empty: - self.app.inform.emit('[ERROR_NOTCL] %s' % - _("Could not get the extent of the area to be non copper cleared.")) + if empty == 'fail' or empty.is_empty: + msg = '[ERROR_NOTCL] %s' % _("Could not get the extent of the area to be non copper cleared.") + self.app.inform.emit(msg) return 'fail' else: self.app.inform.emit('[ERROR_NOTCL] %s' % _('The selected object is not suitable for copper clearing.')) @@ -1822,17 +1806,8 @@ class NonCopperClear(AppTool, Gerber): else: pass - cleared_geo = [] - cleared = MultiPolygon() # Already cleared area app_obj.poly_not_cleared = False # flag for polygons not cleared - # Generate area for each tool - offset = sum(sorted_clear_tools) - current_uid = int(1) - # try: - # tool = eval(self.app.defaults["tools_ncctools"])[0] - # except TypeError: - # tool = eval(self.app.defaults["tools_ncctools"]) if ncc_select == _("Reference Object"): bbox_geo, bbox_kind = self.calculate_bounding_box( @@ -1860,6 +1835,9 @@ class NonCopperClear(AppTool, Gerber): ) app_obj.proc_container.update_view_text(' %d%%' % 0) + # store here the geometry generated by clear operation + cleared_geo = [] + tool_uid = 0 # find the current tool_uid for k, v in self.ncc_tools.items(): if float('%.*f' % (self.decimals, v['tooldia'])) == float('%.*f' % (self.decimals, tool)): @@ -1875,25 +1853,17 @@ class NonCopperClear(AppTool, Gerber): has_offset = self.ncc_tools[tool_uid]["data"]["tools_ncc_offset_choice"] ncc_offset = float(self.ncc_tools[tool_uid]["data"]["tools_ncc_offset_value"]) - # Get remaining tools offset - offset -= (tool - 1e-12) # Bounding box for current tool bbox = self.apply_margin_to_bounding_box(bbox=bbox_geo, box_kind=bbox_kind, ncc_select=ncc_select, ncc_margin=ncc_margin) # Area to clear - empty, warning_flag = self.get_tool_empty_area(name=name, ncc_obj=ncc_obj, geo_obj=geo_obj, + area, warning_flag = self.get_tool_empty_area(name=name, ncc_obj=ncc_obj, geo_obj=geo_obj, isotooldia=isotooldia, ncc_margin=ncc_margin, has_offset=has_offset, ncc_offset=ncc_offset, tools_storage=tools_storage, bounding_box=bbox) - area = empty.buffer(-offset) - try: - area = area.difference(cleared) - except Exception: - continue - # Transform area to MultiPolygon if isinstance(area, Polygon): area = MultiPolygon([area]) @@ -1904,7 +1874,6 @@ class NonCopperClear(AppTool, Gerber): old_disp_number = 0 log.warning("Total number of polygons to be cleared. %s" % str(geo_len)) - cleared_geo[:] = [] if area.geoms: if len(area.geoms) > 0: pol_nr = 0 @@ -1966,12 +1935,6 @@ class NonCopperClear(AppTool, Gerber): # check if there is a geometry at all in the cleared geometry if cleared_geo: - cleared = empty.buffer(-offset * (1 + ncc_overlap)) # Overall cleared area - cleared = cleared.buffer(-tool / 1.999999).buffer(tool / 1.999999) - - # clean-up cleared geo - cleared = cleared.buffer(0) - # find the tooluid associated with the current tool_dia so we know where to add the tool # solid_geometry for k, v in tools_storage.items(): @@ -1983,10 +1946,11 @@ class NonCopperClear(AppTool, Gerber): # and then reset the temporary list that stored that solid_geometry v['solid_geometry'] = deepcopy(cleared_geo) v['data']['name'] = name + geo_obj.tools[current_uid] = dict(tools_storage[current_uid]) break - geo_obj.tools[current_uid] = dict(tools_storage[current_uid]) else: log.debug("There are no geometries in the cleared polygon.") + # clean the progressive plotted shapes if it was used if self.app.defaults["tools_ncc_plotting"] == 'progressive': self.temp_shapes.clear(update=True) @@ -1997,6 +1961,14 @@ class NonCopperClear(AppTool, Gerber): try: # if the solid_geometry (type=list) is empty if not uid_val['solid_geometry']: + msg = '%s %s: %s %s: %s' % ( + _("Could not use the tool for copper clear."), + _("Tool"), + str(uid), + _("with diameter"), + str(uid_val['tooldia'])) + self.app.inform.emit(msg) + log.debug("Empty geometry for tool: %s with diameter: %s" % (str(uid), str(uid_val['tooldia']))) tools_storage.pop(uid, None) except KeyError: tools_storage.pop(uid, None) @@ -2004,7 +1976,6 @@ class NonCopperClear(AppTool, Gerber): geo_obj.options["cnctooldia"] = str(tool) geo_obj.multigeo = True - geo_obj.tools.clear() geo_obj.tools = dict(tools_storage) # test if at least one tool has solid_geometry. If no tool has solid_geometry we raise an Exception @@ -2013,10 +1984,10 @@ class NonCopperClear(AppTool, Gerber): if geo_obj.tools[tid]['solid_geometry']: has_solid_geo += 1 if has_solid_geo == 0: - app_obj.inform.emit('[ERROR] %s' % - _("There is no NCC Geometry in the file.\n" - "Usually it means that the tool diameter is too big for the painted geometry.\n" - "Change the painting parameters and try again.")) + msg = '[ERROR] %s' % _("There is no NCC Geometry in the file.\n" + "Usually it means that the tool diameter is too big for the painted geometry.\n" + "Change the painting parameters and try again.") + app_obj.inform.emit(msg) return 'fail' # check to see if geo_obj.tools is empty diff --git a/camlib.py b/camlib.py index 77a98c38..b78f2f0f 100644 --- a/camlib.py +++ b/camlib.py @@ -1339,16 +1339,16 @@ class Geometry(object): valid cuts. Finalizes by cutting around the inside edge of the polygon. - :param polygon_to_clear: Shapely.geometry.Polygon - :param steps_per_circle: how many linear segments to use to approximate a circle - :param tooldia: Diameter of the tool - :param seedpoint: Shapely.geometry.Point or None - :param overlap: Tool fraction overlap bewteen passes - :param connect: Connect disjoint segment to minumize tool lifts - :param contour: Cut countour inside the polygon. - :return: List of toolpaths covering polygon. - :rtype: FlatCAMRTreeStorage | None - :param prog_plot: boolean; if True use the progressive plotting + :param polygon_to_clear: Shapely.geometry.Polygon + :param steps_per_circle: how many linear segments to use to approximate a circle + :param tooldia: Diameter of the tool + :param seedpoint: Shapely.geometry.Point or None + :param overlap: Tool fraction overlap bewteen passes + :param connect: Connect disjoint segment to minumize tool lifts + :param contour: Cut countour inside the polygon. + :param prog_plot: boolean; if True use the progressive plotting + :return: List of toolpaths covering polygon. + :rtype: FlatCAMRTreeStorage | None """ # log.debug("camlib.clear_polygon2()") @@ -1368,7 +1368,7 @@ class Geometry(object): path_margin = polygon_to_clear.buffer(-tooldia / 2, int(steps_per_circle)) if path_margin.is_empty or path_margin is None: - return + return None # Estimate good seedpoint if not provided. if seedpoint is None: @@ -1411,12 +1411,12 @@ class Geometry(object): # Clean inside edges (contours) of the original polygon if contour: - outer_edges = [ - x.exterior for x in autolist(polygon_to_clear.buffer(-tooldia / 2, int(steps_per_circle))) - ] + buffered_poly = autolist(polygon_to_clear.buffer(-tooldia / 2, int(steps_per_circle))) + outer_edges = [x.exterior for x in buffered_poly] + inner_edges = [] # Over resulting polygons - for x in autolist(polygon_to_clear.buffer(-tooldia / 2, int(steps_per_circle))): + for x in buffered_poly: for y in x.interiors: # Over interiors of each polygon inner_edges.append(y) # geoms += outer_edges + inner_edges @@ -1450,14 +1450,14 @@ class Geometry(object): This algorithm draws horizontal lines inside the polygon. - :param polygon: The polygon being painted. - :type polygon: shapely.geometry.Polygon - :param tooldia: Tool diameter. - :param steps_per_circle: how many linear segments to use to approximate a circle - :param overlap: Tool path overlap percentage. - :param connect: Connect lines to avoid tool lifts. - :param contour: Paint around the edges. - :param prog_plot: boolean; if to use the progressive plotting + :param polygon: The polygon being painted. + :type polygon: shapely.geometry.Polygon + :param tooldia: Tool diameter. + :param steps_per_circle: how many linear segments to use to approximate a circle + :param overlap: Tool path overlap percentage. + :param connect: Connect lines to avoid tool lifts. + :param contour: Paint around the edges. + :param prog_plot: boolean; if to use the progressive plotting :return: """ @@ -1570,12 +1570,14 @@ class Geometry(object): try: for line in lines_trimmed: if isinstance(line, LineString) or isinstance(line, LinearRing): - geoms.insert(line) + if not line.is_empty: + geoms.insert(line) else: log.debug("camlib.Geometry.clear_polygon3(). Not a line: %s" % str(type(line))) except TypeError: # in case lines_trimmed are not iterable (Linestring, LinearRing) - geoms.insert(lines_trimmed) + if not lines_trimmed.is_empty: + geoms.insert(lines_trimmed) # Add margin (contour) to storage if contour: @@ -1633,7 +1635,7 @@ class Geometry(object): """ # log.debug("camlib.fill_with_lines()") - if not isinstance(line, LineString) and not isinstance(line, MultiLineString): + if not isinstance(line, LineString): log.debug("camlib.Geometry.fill_with_lines() --> Not a LineString/MultiLineString but %s" % str(type(line))) return None