diff --git a/README.md b/README.md index 1bb62a18..eac5dcc6 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,9 @@ CAD program, and create G-Code for Isolation routing. - added ability to turn on/of the grid snapping and to jump to a location while in CutOut Tool manual gap adding action - made PlotCanvas class inherit from VisPy Canvas instead of creating an instance of it (work of JP) - fixed selection by dragging a selection shape in Geometry Editor +- modified the Paint Tool. Now the Single Polygon and Area/Reference Object painting works with multiple tools too. The tools have to be selected in the Tool Table. +- remade the TclCommand Paint to work in the new configuration of the the app (the painting functions are now in their own tool, Paint Tool) +- fixed a bug in the Properties Tool 23.08.2019 diff --git a/flatcamTools/ToolPaint.py b/flatcamTools/ToolPaint.py index 7d45ed51..937be49a 100644 --- a/flatcamTools/ToolPaint.py +++ b/flatcamTools/ToolPaint.py @@ -232,10 +232,10 @@ class ToolPaint(FlatCAMTool, Gerber): # Method methodlabel = QtWidgets.QLabel('%s:' % _('Method')) methodlabel.setToolTip( - _("Algorithm for non-copper clearing:
" - "Standard: Fixed step inwards.
" - "Seed-based: Outwards from seed.
" - "Line-based: Parallel lines.") + _("Algorithm for painting:\n" + "- Standard: Fixed step inwards.\n" + "- Seed-based: Outwards from seed.\n" + "- Line-based: Parallel lines.") ) grid3.addWidget(methodlabel, 3, 0) self.paintmethod_combo = RadioSet([ @@ -473,14 +473,14 @@ class ToolPaint(FlatCAMTool, Gerber): self.rest_cb.set_value(False) self.rest_cb.setDisabled(True) # delete all tools except first row / tool for single polygon painting - list_to_del = list(range(1, self.tools_table.rowCount())) - if list_to_del: - self.on_tool_delete(rows_to_delete=list_to_del) - # disable addTool and delTool - self.addtool_entry.setDisabled(True) - self.addtool_btn.setDisabled(True) - self.deltool_btn.setDisabled(True) - self.tools_table.setContextMenuPolicy(Qt.NoContextMenu) + # list_to_del = list(range(1, self.tools_table.rowCount())) + # if list_to_del: + # self.on_tool_delete(rows_to_delete=list_to_del) + # # disable addTool and delTool + # self.addtool_entry.setDisabled(True) + # self.addtool_btn.setDisabled(True) + # self.deltool_btn.setDisabled(True) + # self.tools_table.setContextMenuPolicy(Qt.NoContextMenu) if self.selectmethod_combo.get_value() == 'area': # disable rest-machining for single polygon painting self.rest_cb.set_value(False) @@ -941,8 +941,28 @@ class ToolPaint(FlatCAMTool, Gerber): o_name = '%s_multitool_paint' % self.obj_name + # use the selected tools in the tool table; get diameters + tooldia_list = list() + if self.tools_table.selectedItems(): + for x in self.tools_table.selectedItems(): + try: + tooldia = float(self.tools_table.item(x.row(), 1).text()) + except ValueError: + # try to convert comma to decimal point. if it's still not working error message and return + try: + tooldia = float(self.tools_table.item(x.row(), 1).text().replace(',', '.')) + except ValueError: + self.app.inform.emit(_("[ERROR_NOTCL] Wrong Tool Dia value format entered, " + "use a number.")) + continue + tooldia_list.append(tooldia) + else: + self.app.inform.emit(_("[ERROR_NOTCL] No selected tools in Tool Table.")) + return + if select_method == "all": self.paint_poly_all(self.paint_obj, + tooldia=tooldia_list, outname=o_name, overlap=overlap, connect=connect, @@ -952,7 +972,7 @@ class ToolPaint(FlatCAMTool, Gerber): self.app.inform.emit(_("[WARNING_NOTCL] Click inside the desired polygon.")) # use the first tool in the tool table; get the diameter - tooldia = float('%.4f' % float(self.tools_table.item(0, 1).text())) + # tooldia = float('%.4f' % float(self.tools_table.item(0, 1).text())) # To be called after clicking on the plot. def doit(event): @@ -967,12 +987,14 @@ class ToolPaint(FlatCAMTool, Gerber): self.paint_poly(self.paint_obj, inside_pt=[pos[0], pos[1]], - tooldia=tooldia, + tooldia=tooldia_list, overlap=overlap, connect=connect, contour=contour) self.app.plotcanvas.vis_connect('mouse_press', self.app.on_mouse_click_over_plot) + self.app.plotcanvas.vis_connect('mouse_release', self.app.on_mouse_click_release_over_plot) + self.app.plotcanvas.vis_disconnect('mouse_release', self.app.on_mouse_click_release_over_plot) self.app.plotcanvas.vis_disconnect('mouse_press', self.app.on_mouse_click_over_plot) self.app.plotcanvas.vis_connect('mouse_press', doit) @@ -980,7 +1002,7 @@ class ToolPaint(FlatCAMTool, Gerber): self.app.inform.emit(_("[WARNING_NOTCL] Click the start point of the paint area.")) # use the first tool in the tool table; get the diameter - tooldia = float('%.4f' % float(self.tools_table.item(0, 1).text())) + # tooldia = float('%.4f' % float(self.tools_table.item(0, 1).text())) # To be called after clicking on the plot. def on_mouse_release(event): @@ -1024,6 +1046,7 @@ class ToolPaint(FlatCAMTool, Gerber): self.sel_rect = cascaded_union(self.sel_rect) self.paint_poly_area(obj=self.paint_obj, + tooldia=tooldia_list, sel_obj= self.sel_rect, outname=o_name, overlap=overlap, @@ -1047,6 +1070,7 @@ class ToolPaint(FlatCAMTool, Gerber): self.sel_rect = cascaded_union(self.sel_rect) self.paint_poly_area(obj=self.paint_obj, + tooldia=tooldia_list, sel_obj=self.sel_rect, outname=o_name, overlap=overlap, @@ -1093,32 +1117,27 @@ class ToolPaint(FlatCAMTool, Gerber): self.app.inform.emit(_("[ERROR_NOTCL] Could not retrieve object: %s") % self.obj_name) return "Could not retrieve object: %s" % self.obj_name - geo = self.bound_obj.solid_geometry - try: - if isinstance(geo, MultiPolygon): - env_obj = geo.convex_hull - elif (isinstance(geo, MultiPolygon) and len(geo) == 1) or \ - (isinstance(geo, list) and len(geo) == 1) and isinstance(geo[0], Polygon): - env_obj = cascaded_union(self.bound_obj.solid_geometry) - else: - env_obj = cascaded_union(self.bound_obj.solid_geometry) - env_obj = env_obj.convex_hull - sel_rect = env_obj.buffer(distance=0.0000001, join_style=base.JOIN_STYLE.mitre) - except Exception as e: - log.debug("ToolPaint.on_paint_button_click() --> %s" % str(e)) - self.app.inform.emit(_("[ERROR_NOTCL] No object available.")) - return + self.paint_poly_ref(obj=self.paint_obj, + sel_obj=self.bound_obj, + tooldia=tooldia_list, + overlap=overlap, + outname=o_name, + connect=connect, + contour=contour) - self.paint_poly_area(obj=self.paint_obj, - sel_obj=sel_rect, - outname=o_name, - overlap=overlap, - connect=connect, - contour=contour) - - def paint_poly(self, obj, inside_pt, tooldia, overlap, outname=None, connect=True, contour=True): + def paint_poly(self, obj, + inside_pt=None, + tooldia=None, + overlap=None, + order=None, + margin=None, + method=None, + outname=None, + connect=None, + contour=None, + tools_storage=None): """ - Paints a polygon selected by clicking on its interior. + Paints a polygon selected by clicking on its interior or by having a point coordinates given Note: * The margin is taken directly from the form. @@ -1126,27 +1145,35 @@ class ToolPaint(FlatCAMTool, Gerber): :param inside_pt: [x, y] :param tooldia: Diameter of the painting tool :param overlap: Overlap of the tool between passes. + :param order: if the tools are ordered and how + :param margin: a border around painting area :param outname: Name of the resulting Geometry Object. :param connect: Connect lines to avoid tool lifts. :param contour: Paint around the edges. + :param method: choice out of 'seed', 'normal', 'lines' + :param tools_storage: whether to use the current tools_storage self.paints_tools or a different one. + Usage of the different one is related to when this function is called from a TcL command. :return: None """ # Which polygon. # poly = find_polygon(self.solid_geometry, inside_pt) poly = self.find_polygon(point=inside_pt, geoset=obj.solid_geometry) - paint_method = self.paintmethod_combo.get_value() + paint_method = method if method is None else self.paintmethod_combo.get_value() - try: - paint_margin = float(self.paintmargin_entry.get_value()) - except ValueError: - # try to convert comma to decimal point. if it's still not working error message and return + if margin is not None: + paint_margin = margin + else: try: - paint_margin = float(self.paintmargin_entry.get_value().replace(',', '.')) + paint_margin = float(self.paintmargin_entry.get_value()) except ValueError: - self.app.inform.emit(_("[ERROR_NOTCL] Wrong value format entered, " - "use a number.")) - return + # try to convert comma to decimal point. if it's still not working error message and return + try: + paint_margin = float(self.paintmargin_entry.get_value().replace(',', '.')) + except ValueError: + self.app.inform.emit(_("[ERROR_NOTCL] Wrong value format entered, " + "use a number.")) + return # No polygon? if poly is None: @@ -1156,41 +1183,72 @@ class ToolPaint(FlatCAMTool, Gerber): proc = self.app.proc_container.new(_("Painting polygon.")) - name = outname if outname else self.obj_name + "_paint" + name = outname if outname is not None else self.obj_name + "_paint" + + over = overlap if overlap is not None else float(self.app.defaults["tools_paintoverlap"]) + conn = connect if connect is not None else self.app.defaults["tools_pathconnect"] + cont = contour if contour is not None else self.app.defaults["tools_paintcontour"] + order = order if order is not None else self.order_radio.get_value() + + sorted_tools = [] + if tooldia is not None: + try: + sorted_tools = [float(eval(dia)) for dia in tooldia.split(",") if dia != ''] + except AttributeError: + if not isinstance(tooldia, list): + sorted_tools = [float(tooldia)] + else: + sorted_tools = tooldia + else: + for row in range(self.tools_table.rowCount()): + sorted_tools.append(float(self.tools_table.item(row, 1).text())) + + if tools_storage is not None: + tools_storage = tools_storage + else: + tools_storage = self.paint_tools # Initializes the new geometry object def gen_paintarea(geo_obj, app_obj): - assert isinstance(geo_obj, FlatCAMGeometry), \ - "Initializer expected a FlatCAMGeometry, got %s" % type(geo_obj) + # assert isinstance(geo_obj, FlatCAMGeometry), \ + # "Initializer expected a FlatCAMGeometry, got %s" % type(geo_obj) # assert isinstance(app_obj, App) - def paint_p(polyg): + tool_dia = None + if order == 'fwd': + sorted_tools.sort(reverse=False) + elif order == 'rev': + sorted_tools.sort(reverse=True) + else: + pass + + def paint_p(polyg, tooldia): if paint_method == "seed": # Type(cp) == FlatCAMRTreeStorage | None cpoly = self.clear_polygon2(polyg, tooldia=tooldia, steps_per_circle=self.app.defaults["geometry_circle_steps"], - overlap=overlap, - contour=contour, - connect=connect) + overlap=over, + contour=cont, + connect=conn) elif paint_method == "lines": # Type(cp) == FlatCAMRTreeStorage | None cpoly = self.clear_polygon3(polyg, tooldia=tooldia, steps_per_circle=self.app.defaults["geometry_circle_steps"], - overlap=overlap, - contour=contour, - connect=connect) + overlap=over, + contour=cont, + connect=conn) else: # Type(cp) == FlatCAMRTreeStorage | None cpoly = self.clear_polygon(polyg, tooldia=tooldia, steps_per_circle=self.app.defaults["geometry_circle_steps"], - overlap=overlap, - contour=contour, - connect=connect) + overlap=over, + contour=cont, + connect=conn) if cpoly is not None: geo_obj.solid_geometry += list(cpoly.get_objects()) @@ -1199,8 +1257,6 @@ class ToolPaint(FlatCAMTool, Gerber): self.app.inform.emit(_('[ERROR_NOTCL] Geometry could not be painted completely')) return None - geo_obj.solid_geometry = [] - try: a, b, c, d = poly.bounds geo_obj.options['xmin'] = a @@ -1211,39 +1267,78 @@ class ToolPaint(FlatCAMTool, Gerber): log.debug("ToolPaint.paint_poly.gen_paintarea() bounds error --> %s" % str(e)) return - try: - poly_buf = poly.buffer(-paint_margin) - if isinstance(poly_buf, MultiPolygon): - cp = [] - for pp in poly_buf: - cp.append(paint_p(pp)) - else: - cp = paint_p(poly_buf) - except Exception as e: - log.debug("Could not Paint the polygons. %s" % str(e)) - self.app.inform.emit( - _("[ERROR] Could not do Paint. Try a different combination of parameters. " - "Or a different strategy of paint\n%s") % str(e)) + total_geometry = [] + current_uid = int(1) + + geo_obj.solid_geometry = [] + + for tool_dia in sorted_tools: + # 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(): + if float('%.4f' % v['tooldia']) == float('%.4f' % tool_dia): + current_uid = int(k) + break + + try: + poly_buf = poly.buffer(-paint_margin) + if isinstance(poly_buf, MultiPolygon): + cp = [] + for pp in poly_buf: + cp.append(paint_p(pp, tooldia=tool_dia)) + else: + cp = paint_p(poly_buf, tooldia=tool_dia) + + if cp is not None: + if isinstance(cp, list): + for x in cp: + total_geometry += list(x.get_objects()) + else: + total_geometry = list(cp.get_objects()) + except Exception as e: + log.debug("Could not Paint the polygons. %s" % str(e)) + self.app.inform.emit( + _("[ERROR] Could not do Paint. Try a different combination of parameters. " + "Or a different strategy of paint\n%s") % str(e)) + return + + # add the solid_geometry to the current too in self.paint_tools (tools_storage) + # dictionary and then reset the temporary list that stored that solid_geometry + tools_storage[current_uid]['solid_geometry'] = deepcopy(total_geometry) + + tools_storage[current_uid]['data']['name'] = name + total_geometry[:] = [] + + # delete tools with empty geometry + keys_to_delete = [] + # look for keys in the tools_storage dict that have 'solid_geometry' values empty + for uid in tools_storage: + # if the solid_geometry (type=list) is empty + if not tools_storage[uid]['solid_geometry']: + keys_to_delete.append(uid) + + # actual delete of keys from the tools_storage dict + for k in keys_to_delete: + tools_storage.pop(k, None) + + geo_obj.options["cnctooldia"] = str(tool_dia) + # this turn on the FlatCAMCNCJob plot for multiple tools + geo_obj.multigeo = True + geo_obj.multitool = 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 + has_solid_geo = 0 + for tooluid in geo_obj.tools: + if geo_obj.tools[tooluid]['solid_geometry']: + has_solid_geo += 1 + if has_solid_geo == 0: + self.app.inform.emit(_("[ERROR] There is no Painting 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.")) return - if cp is not None: - if isinstance(cp, list): - for x in cp: - geo_obj.solid_geometry += list(x.get_objects()) - else: - geo_obj.solid_geometry = list(cp.get_objects()) - - geo_obj.options["cnctooldia"] = str(tooldia) - # this turn on the FlatCAMCNCJob plot for multiple tools - geo_obj.multigeo = False - geo_obj.multitool = True - - current_uid = int(self.tools_table.item(0, 3).text()) - for k, v in self.paint_tools.items(): - if k == current_uid: - v['data']['name'] = name - - geo_obj.tools = dict(self.paint_tools) + self.app.inform.emit(_("[success] Paint Single Done.")) # Experimental... # print("Indexing...", end=' ') @@ -1278,36 +1373,73 @@ class ToolPaint(FlatCAMTool, Gerber): # Background self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]}) - def paint_poly_all(self, obj, overlap, outname=None, connect=True, contour=True): + def paint_poly_all(self, obj, + tooldia=None, + overlap=None, + order=None, + margin=None, + method=None, + outname=None, + connect=None, + contour=None, + tools_storage=None): """ Paints all polygons in this object. :param obj: painted object - :param overlap: - :param outname: + :param tooldia: a tuple or single element made out of diameters of the tools to be used + :param overlap: value by which the paths will overlap + :param order: if the tools are ordered and how + :param margin: a border around painting area + :param outname: name of the resulting object :param connect: Connect lines to avoid tool lifts. :param contour: Paint around the edges. + :param method: choice out of 'seed', 'normal', 'lines' + :param tools_storage: whether to use the current tools_storage self.paints_tools or a different one. + Usage of the different one is related to when this function is called from a TcL command. :return: """ - paint_method = self.paintmethod_combo.get_value() + paint_method = method if method is None else self.paintmethod_combo.get_value() - try: - paint_margin = float(self.paintmargin_entry.get_value()) - except ValueError: - # try to convert comma to decimal point. if it's still not working error message and return + if margin is not None: + paint_margin = margin + else: try: - paint_margin = float(self.paintmargin_entry.get_value().replace(',', '.')) + paint_margin = float(self.paintmargin_entry.get_value()) except ValueError: - self.app.inform.emit(_("[ERROR_NOTCL] Wrong value format entered, " - "use a number.")) - return + # try to convert comma to decimal point. if it's still not working error message and return + try: + paint_margin = float(self.paintmargin_entry.get_value().replace(',', '.')) + except ValueError: + self.app.inform.emit(_("[ERROR_NOTCL] Wrong value format entered, " + "use a number.")) + return proc = self.app.proc_container.new(_("Painting polygon...")) - name = outname if outname else self.obj_name + "_paint" - over = overlap - conn = connect - cont = contour + name = outname if outname is not None else self.obj_name + "_paint" + over = overlap if overlap is not None else float(self.app.defaults["tools_paintoverlap"]) + conn = connect if connect is not None else self.app.defaults["tools_pathconnect"] + cont = contour if contour is not None else self.app.defaults["tools_paintcontour"] + order = order if order is not None else self.order_radio.get_value() + + sorted_tools = [] + if tooldia is not None: + try: + sorted_tools = [float(eval(dia)) for dia in tooldia.split(",") if dia != ''] + except AttributeError: + if not isinstance(tooldia, list): + sorted_tools = [float(tooldia)] + else: + sorted_tools = tooldia + else: + for row in range(self.tools_table.rowCount()): + sorted_tools.append(float(self.tools_table.item(row, 1).text())) + + if tools_storage is not None: + tools_storage = tools_storage + else: + tools_storage = self.paint_tools # This is a recursive generator of individual Polygons. # Note: Double check correct implementation. Might exit # early if it finds something that is not a Polygon? @@ -1355,15 +1487,10 @@ class ToolPaint(FlatCAMTool, Gerber): # Initializes the new geometry object def gen_paintarea(geo_obj, app_obj): - assert isinstance(geo_obj, FlatCAMGeometry), \ - "Initializer expected a FlatCAMGeometry, got %s" % type(geo_obj) + # assert isinstance(geo_obj, FlatCAMGeometry), \ + # "Initializer expected a FlatCAMGeometry, got %s" % type(geo_obj) + tool_dia = None - - sorted_tools = [] - for row in range(self.tools_table.rowCount()): - sorted_tools.append(float(self.tools_table.item(row, 1).text())) - - order = self.order_radio.get_value() if order == 'fwd': sorted_tools.sort(reverse=False) elif order == 'rev': @@ -1383,10 +1510,12 @@ class ToolPaint(FlatCAMTool, Gerber): total_geometry = [] current_uid = int(1) + geo_obj.solid_geometry = [] for tool_dia in sorted_tools: + # find the tooluid associated with the current tool_dia so we know where to add the tool solid_geometry - for k, v in self.paint_tools.items(): + for k, v in tools_storage.items(): if float('%.4f' % v['tooldia']) == float('%.4f' % tool_dia): current_uid = int(k) break @@ -1434,19 +1563,31 @@ class ToolPaint(FlatCAMTool, Gerber): "Or a different Method of paint\n%s") % str(e)) return - # add the solid_geometry to the current too in self.paint_tools dictionary and then reset the - # temporary list that stored that solid_geometry - self.paint_tools[current_uid]['solid_geometry'] = deepcopy(total_geometry) + # add the solid_geometry to the current too in self.paint_tools (tools_storage) + # dictionary and then reset the temporary list that stored that solid_geometry + tools_storage[current_uid]['solid_geometry'] = deepcopy(total_geometry) - self.paint_tools[current_uid]['data']['name'] = name + tools_storage[current_uid]['data']['name'] = name total_geometry[:] = [] + # delete tools with empty geometry + keys_to_delete = [] + # look for keys in the tools_storage dict that have 'solid_geometry' values empty + for uid in tools_storage: + # if the solid_geometry (type=list) is empty + if not tools_storage[uid]['solid_geometry']: + keys_to_delete.append(uid) + + # actual delete of keys from the tools_storage dict + for k in keys_to_delete: + tools_storage.pop(k, None) + geo_obj.options["cnctooldia"] = str(tool_dia) # this turn on the FlatCAMCNCJob plot for multiple tools geo_obj.multigeo = True geo_obj.multitool = True geo_obj.tools.clear() - geo_obj.tools = dict(self.paint_tools) + 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 has_solid_geo = 0 @@ -1467,13 +1608,10 @@ class ToolPaint(FlatCAMTool, Gerber): # Initializes the new geometry object def gen_paintarea_rest_machining(geo_obj, app_obj): - assert isinstance(geo_obj, FlatCAMGeometry), \ - "Initializer expected a FlatCAMGeometry, got %s" % type(geo_obj) + # assert isinstance(geo_obj, FlatCAMGeometry), \ + # "Initializer expected a FlatCAMGeometry, got %s" % type(geo_obj) tool_dia = None - sorted_tools = [] - for row in range(self.tools_table.rowCount()): - sorted_tools.append(float(self.tools_table.item(row, 1).text())) sorted_tools.sort(reverse=True) cleared_geo = [] @@ -1526,16 +1664,16 @@ class ToolPaint(FlatCAMTool, Gerber): return # find the tooluid associated with the current tool_dia so we know where to add the tool solid_geometry - for k, v in self.paint_tools.items(): + for k, v in tools_storage.items(): if float('%.4f' % v['tooldia']) == float('%.4f' % tool_dia): current_uid = int(k) break - # add the solid_geometry to the current too in self.paint_tools dictionary and then reset the - # temporary list that stored that solid_geometry - self.paint_tools[current_uid]['solid_geometry'] = deepcopy(cleared_geo) + # add the solid_geometry to the current too in self.paint_tools (or tools_storage) dictionary and + # then reset the temporary list that stored that solid_geometry + tools_storage[current_uid]['solid_geometry'] = deepcopy(cleared_geo) - self.paint_tools[current_uid]['data']['name'] = name + tools_storage[current_uid]['data']['name'] = name cleared_geo[:] = [] geo_obj.options["cnctooldia"] = str(tool_dia) @@ -1543,7 +1681,7 @@ class ToolPaint(FlatCAMTool, Gerber): geo_obj.multigeo = True geo_obj.multitool = True geo_obj.tools.clear() - geo_obj.tools = dict(self.paint_tools) + 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 has_solid_geo = 0 @@ -1584,36 +1722,74 @@ class ToolPaint(FlatCAMTool, Gerber): # Background self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]}) - def paint_poly_area(self, obj, sel_obj, overlap, outname=None, connect=True, contour=True): + def paint_poly_area(self, obj, sel_obj, + tooldia=None, + overlap=None, + order=None, + margin=None, + method=None, + outname=None, + connect=None, + contour=None, + tools_storage=None): """ Paints all polygons in this object that are within the sel_obj object :param obj: painted object :param sel_obj: paint only what is inside this object bounds - :param overlap: - :param outname: + :param tooldia: a tuple or single element made out of diameters of the tools to be used + :param overlap: value by which the paths will overlap + :param order: if the tools are ordered and how + :param margin: a border around painting area + :param outname: name of the resulting object :param connect: Connect lines to avoid tool lifts. :param contour: Paint around the edges. + :param method: choice out of 'seed', 'normal', 'lines' + :param tools_storage: whether to use the current tools_storage self.paints_tools or a different one. + Usage of the different one is related to when this function is called from a TcL command. :return: """ - paint_method = self.paintmethod_combo.get_value() + paint_method = method if method is None else self.paintmethod_combo.get_value() - try: - paint_margin = float(self.paintmargin_entry.get_value()) - except ValueError: - # try to convert comma to decimal point. if it's still not working error message and return + if margin is not None: + paint_margin = margin + else: try: - paint_margin = float(self.paintmargin_entry.get_value().replace(',', '.')) + paint_margin = float(self.paintmargin_entry.get_value()) except ValueError: - self.app.inform.emit(_("[ERROR_NOTCL] Wrong value format entered, " - "use a number.")) - return + # try to convert comma to decimal point. if it's still not working error message and return + try: + paint_margin = float(self.paintmargin_entry.get_value().replace(',', '.')) + except ValueError: + self.app.inform.emit(_("[ERROR_NOTCL] Wrong value format entered, " + "use a number.")) + return proc = self.app.proc_container.new(_("Painting polygon...")) - name = outname if outname else self.obj_name + "_paint" - over = overlap - conn = connect - cont = contour + name = outname if outname is not None else self.obj_name + "_paint" + + over = overlap if overlap is not None else float(self.app.defaults["tools_paintoverlap"]) + conn = connect if connect is not None else self.app.defaults["tools_pathconnect"] + cont = contour if contour is not None else self.app.defaults["tools_paintcontour"] + order = order if order is not None else self.order_radio.get_value() + + sorted_tools = [] + if tooldia is not None: + try: + sorted_tools = [float(eval(dia)) for dia in tooldia.split(",") if dia != ''] + except AttributeError: + if not isinstance(tooldia, list): + sorted_tools = [float(tooldia)] + else: + sorted_tools = tooldia + else: + for row in range(self.tools_table.rowCount()): + sorted_tools.append(float(self.tools_table.item(row, 1).text())) + + if tools_storage is not None: + tools_storage = tools_storage + else: + tools_storage = self.paint_tools def recurse(geometry, reset=True): """ @@ -1648,15 +1824,9 @@ class ToolPaint(FlatCAMTool, Gerber): # Initializes the new geometry object def gen_paintarea(geo_obj, app_obj): - assert isinstance(geo_obj, FlatCAMGeometry), \ - "Initializer expected a FlatCAMGeometry, got %s" % type(geo_obj) + # assert isinstance(geo_obj, FlatCAMGeometry), \ + # "Initializer expected a FlatCAMGeometry, got %s" % type(geo_obj) tool_dia = None - - sorted_tools = [] - for row in range(self.tools_table.rowCount()): - sorted_tools.append(float(self.tools_table.item(row, 1).text())) - - order = self.order_radio.get_value() if order == 'fwd': sorted_tools.sort(reverse=False) elif order == 'rev': @@ -1664,6 +1834,7 @@ class ToolPaint(FlatCAMTool, Gerber): else: pass + # this is were heavy lifting is done and creating the geometry to be painted geo_to_paint = [] if not isinstance(obj.solid_geometry, list): target_geo = [obj.solid_geometry] @@ -1686,10 +1857,12 @@ class ToolPaint(FlatCAMTool, Gerber): total_geometry = [] current_uid = int(1) + geo_obj.solid_geometry = [] for tool_dia in sorted_tools: + # find the tooluid associated with the current tool_dia so we know where to add the tool solid_geometry - for k, v in self.paint_tools.items(): + for k, v in tools_storage.items(): if float('%.4f' % v['tooldia']) == float('%.4f' % tool_dia): current_uid = int(k) break @@ -1737,19 +1910,31 @@ class ToolPaint(FlatCAMTool, Gerber): "Or a different Method of paint\n%s") % str(e)) return - # add the solid_geometry to the current too in self.paint_tools dictionary and then reset the - # temporary list that stored that solid_geometry - self.paint_tools[current_uid]['solid_geometry'] = deepcopy(total_geometry) + # add the solid_geometry to the current too in self.paint_tools (tools_storage) + # dictionary and then reset the temporary list that stored that solid_geometry + tools_storage[current_uid]['solid_geometry'] = deepcopy(total_geometry) - self.paint_tools[current_uid]['data']['name'] = name + tools_storage[current_uid]['data']['name'] = name total_geometry[:] = [] + # delete tools with empty geometry + keys_to_delete = [] + # look for keys in the tools_storage dict that have 'solid_geometry' values empty + for uid in tools_storage: + # if the solid_geometry (type=list) is empty + if not tools_storage[uid]['solid_geometry']: + keys_to_delete.append(uid) + + # actual delete of keys from the tools_storage dict + for k in keys_to_delete: + tools_storage.pop(k, None) + geo_obj.options["cnctooldia"] = str(tool_dia) # this turn on the FlatCAMCNCJob plot for multiple tools geo_obj.multigeo = True geo_obj.multitool = True geo_obj.tools.clear() - geo_obj.tools = dict(self.paint_tools) + 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 has_solid_geo = 0 @@ -1766,7 +1951,7 @@ class ToolPaint(FlatCAMTool, Gerber): # print("Indexing...", end=' ') # geo_obj.make_index() - self.app.inform.emit(_("[success] Paint All Done.")) + self.app.inform.emit(_("[success] Paint Area Done.")) # Initializes the new geometry object def gen_paintarea_rest_machining(geo_obj, app_obj): @@ -1774,9 +1959,6 @@ class ToolPaint(FlatCAMTool, Gerber): "Initializer expected a FlatCAMGeometry, got %s" % type(geo_obj) tool_dia = None - sorted_tools = [] - for row in range(self.tools_table.rowCount()): - sorted_tools.append(float(self.tools_table.item(row, 1).text())) sorted_tools.sort(reverse=True) cleared_geo = [] @@ -1829,16 +2011,16 @@ class ToolPaint(FlatCAMTool, Gerber): return # find the tooluid associated with the current tool_dia so we know where to add the tool solid_geometry - for k, v in self.paint_tools.items(): + for k, v in tools_storage.items(): if float('%.4f' % v['tooldia']) == float('%.4f' % tool_dia): current_uid = int(k) break - # add the solid_geometry to the current too in self.paint_tools dictionary and then reset the - # temporary list that stored that solid_geometry - self.paint_tools[current_uid]['solid_geometry'] = deepcopy(cleared_geo) + # add the solid_geometry to the current too in self.paint_tools (or tools_storage) dictionary and + # then reset the temporary list that stored that solid_geometry + tools_storage[current_uid]['solid_geometry'] = deepcopy(cleared_geo) - self.paint_tools[current_uid]['data']['name'] = name + tools_storage[current_uid]['data']['name'] = name cleared_geo[:] = [] geo_obj.options["cnctooldia"] = str(tool_dia) @@ -1887,6 +2069,61 @@ class ToolPaint(FlatCAMTool, Gerber): # Background self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]}) + def paint_poly_ref(self, obj, sel_obj, + tooldia=None, + overlap=None, + order=None, + margin=None, + method=None, + outname=None, + connect=None, + contour=None, + tools_storage=None): + """ + Paints all polygons in this object that are within the sel_obj object + + :param obj: painted object + :param sel_obj: paint only what is inside this object bounds + :param tooldia: a tuple or single element made out of diameters of the tools to be used + :param overlap: value by which the paths will overlap + :param order: if the tools are ordered and how + :param margin: a border around painting area + :param outname: name of the resulting object + :param connect: Connect lines to avoid tool lifts. + :param contour: Paint around the edges. + :param method: choice out of 'seed', 'normal', 'lines' + :param tools_storage: whether to use the current tools_storage self.paints_tools or a different one. + Usage of the different one is related to when this function is called from a TcL command. + :return: + """ + geo = sel_obj.solid_geometry + try: + if isinstance(geo, MultiPolygon): + env_obj = geo.convex_hull + elif (isinstance(geo, MultiPolygon) and len(geo) == 1) or \ + (isinstance(geo, list) and len(geo) == 1) and isinstance(geo[0], Polygon): + env_obj = cascaded_union(self.bound_obj.solid_geometry) + else: + env_obj = cascaded_union(self.bound_obj.solid_geometry) + env_obj = env_obj.convex_hull + sel_rect = env_obj.buffer(distance=0.0000001, join_style=base.JOIN_STYLE.mitre) + except Exception as e: + log.debug("ToolPaint.on_paint_button_click() --> %s" % str(e)) + self.app.inform.emit(_("[ERROR_NOTCL] No object available.")) + return + + self.paint_poly_area(obj=obj, + sel_obj=sel_rect, + tooldia=tooldia, + overlap=overlap, + order=order, + margin=margin, + method=method, + outname=outname, + connect=connect, + contour=contour, + tools_storage=tools_storage) + @staticmethod def paint_bounds(geometry): def bounds_rec(o): diff --git a/flatcamTools/ToolProperties.py b/flatcamTools/ToolProperties.py index 40ca330f..d1517cc6 100644 --- a/flatcamTools/ToolProperties.py +++ b/flatcamTools/ToolProperties.py @@ -175,10 +175,10 @@ class Properties(FlatCAMTool): env_obj = geo.convex_hull elif (isinstance(geo, MultiPolygon) and len(geo) == 1) or \ (isinstance(geo, list) and len(geo) == 1) and isinstance(geo[0], Polygon): - env_obj = cascaded_union(self.bound_obj.solid_geometry) + env_obj = cascaded_union(obj.solid_geometry) env_obj = env_obj.convex_hull else: - env_obj = cascaded_union(self.bound_obj.solid_geometry) + env_obj = cascaded_union(obj.solid_geometry) env_obj = env_obj.convex_hull area_chull = env_obj.area diff --git a/tclCommands/TclCommand.py b/tclCommands/TclCommand.py index 5e69a2c4..b50b380c 100644 --- a/tclCommands/TclCommand.py +++ b/tclCommands/TclCommand.py @@ -202,7 +202,6 @@ class TclCommand(object): """ arguments, options = self.parse_arguments(args) - named_args = {} unnamed_args = [] @@ -274,7 +273,7 @@ class TclCommand(object): :return: None, output text or exception """ - #self.worker_task.emit({'fcn': self.exec_command_test, 'params': [text, False]}) + # self.worker_task.emit({'fcn': self.exec_command_test, 'params': [text, False]}) try: self.log.debug("TCL command '%s' executed." % str(self.__class__)) @@ -283,7 +282,7 @@ class TclCommand(object): return self.execute(args, unnamed_args) except Exception as unknown: error_info = sys.exc_info() - self.log.error("TCL command '%s' failed." % str(self)) + self.log.error("TCL command '%s' failed. Error text: %s" % (str(self), str(unknown))) self.app.display_tcl_error(unknown, error_info) self.raise_tcl_unknown_error(unknown) diff --git a/tclCommands/TclCommandPaint.py b/tclCommands/TclCommandPaint.py index 8a118290..8ea70516 100644 --- a/tclCommands/TclCommandPaint.py +++ b/tclCommands/TclCommandPaint.py @@ -1,8 +1,16 @@ from ObjectCollection import * -from tclCommands.TclCommand import TclCommandSignaled +from tclCommands.TclCommand import TclCommand + +import gettext +import FlatCAMTranslation as fcTranslate +import builtins + +fcTranslate.apply_language('strings') +if '_' not in builtins.__dict__: + _ = gettext.gettext -class TclCommandPaint(TclCommandSignaled): +class TclCommandPaint(TclCommand): """ Paint the interior of polygons """ @@ -13,32 +21,53 @@ class TclCommandPaint(TclCommandSignaled): # dictionary of types from Tcl command, needs to be ordered arg_names = collections.OrderedDict([ ('name', str), - ('tooldia', float), - ('overlap', float) ]) # dictionary of types from Tcl command, needs to be ordered , this is for options like -optionname value option_types = collections.OrderedDict([ - ('outname', str), + ('tooldia', str), + ('overlap', float), + ('order', str), + ('margin', float), + ('method', str), + ('connect', bool), + ('contour', bool), + ('all', bool), + ('single', bool), + ('ref', bool), + ('box', str), ('x', float), - ('y', float) + ('y', float), + ('outname', str), ]) # array of mandatory options for current Tcl command: required = {'name','outname'} - required = ['name', 'tooldia', 'overlap'] + required = ['name'] # structured help for current command, args needs to be ordered help = { 'main': "Paint polygons", 'args': collections.OrderedDict([ - ('name', 'Name of the source Geometry object.'), - ('tooldia', 'Diameter of the tool to be used.'), - ('overlap', 'Fraction of the tool diameter to overlap cuts.'), - ('outname', 'Name of the resulting Geometry object.'), - ('all', 'Paint all polygons in the object.'), - ('x', 'X value of coordinate for the selection of a single polygon.'), - ('y', 'Y value of coordinate for the selection of a single polygon.') + ('name', 'Name of the source Geometry object. String.'), + ('tooldia', 'Diameter of the tool to be used. Can be a comma separated list of diameters. No space is ' + 'allowed between tool diameters. E.g: correct: 0.5,1 / incorrect: 0.5, 1'), + ('overlap', 'Fraction of the tool diameter to overlap cuts. Float number.'), + ('order', 'Can have the values: "no", "fwd" and "rev". String.' + 'It is useful when there are multiple tools in tooldia parameter.' + '"no" -> the order used is the one provided.' + '"fwd" -> tools are ordered from smallest to biggest.' + '"rev" -> tools are ordered from biggest to smallest.'), + ('method', 'Algorithm for painting. Can be: "standard", "seed" or "lines".'), + ('connect', 'Draw lines to minimize tool lifts. True or False'), + ('contour', 'Cut around the perimeter of the painting. True or False'), + ('all', 'Paint all polygons in the object. True or False'), + ('single', 'Paint a single polygon specified by "x" and "y" parameters. True or False'), + ('ref', 'Paint all polygons within a specified object with the name in "box" parameter. True or False'), + ('box', 'name of the object to be used as paint reference when selecting "ref"" True. String.'), + ('x', 'X value of coordinate for the selection of a single polygon. Float number.'), + ('y', 'Y value of coordinate for the selection of a single polygon. Float number.'), + ('outname', 'Name of the resulting Geometry object. String.'), ]), 'examples': [] } @@ -54,31 +83,177 @@ class TclCommandPaint(TclCommandSignaled): """ name = args['name'] - tooldia = args['tooldia'] - overlap = args['overlap'] + + if 'tooldia' in args: + tooldia = str(args['tooldia']) + else: + tooldia = float(self.app.defaults["tools_paintoverlap"]) + + if 'overlap' in args: + overlap = float(args['overlap']) + else: + overlap = float(self.app.defaults["tools_paintoverlap"]) + + if 'order' in args: + order = args['order'] + else: + order = str(self.app.defaults["tools_paintorder"]) + + if 'margin' in args: + margin = float(args['margin']) + else: + margin = float(self.app.defaults["tools_paintmargin"]) + + if 'method' in args: + method = args['method'] + else: + method = str(self.app.defaults["tools_paintmethod"]) + + if 'connect' in args: + connect = eval(str(args['connect']).capitalize()) + else: + connect = eval(str(self.app.defaults["tools_pathconnect"])) + + if 'contour' in args: + contour = eval(str(args['contour']).capitalize()) + else: + contour = eval(str(self.app.defaults["tools_paintcontour"])) if 'outname' in args: outname = args['outname'] else: outname = name + "_paint" - obj = self.app.collection.get_by_name(name) + # Get source object. + try: + obj = self.app.collection.get_by_name(str(name)) + except Exception as e: + log.debug("TclCommandPaint.execute() --> %s" % str(e)) + self.raise_tcl_error("%s: %s" % (_("Could not retrieve object"), name)) + return "Could not retrieve object: %s" % name + + try: + tools = [float(eval(dia)) for dia in tooldia.split(",") if dia != ''] + except AttributeError: + tools = [float(tooldia)] + # store here the default data for Geometry Data + default_data = {} + default_data.update({ + "name": '_paint', + "plot": self.app.defaults["geometry_plot"], + "cutz": self.app.defaults["geometry_cutz"], + "vtipdia": 0.1, + "vtipangle": 30, + "travelz": self.app.defaults["geometry_travelz"], + "feedrate": self.app.defaults["geometry_feedrate"], + "feedrate_z": self.app.defaults["geometry_feedrate_z"], + "feedrate_rapid": self.app.defaults["geometry_feedrate_rapid"], + "dwell": self.app.defaults["geometry_dwell"], + "dwelltime": self.app.defaults["geometry_dwelltime"], + "multidepth": self.app.defaults["geometry_multidepth"], + "ppname_g": self.app.defaults["geometry_ppname_g"], + "depthperpass": self.app.defaults["geometry_depthperpass"], + "extracut": self.app.defaults["geometry_extracut"], + "toolchange": self.app.defaults["geometry_toolchange"], + "toolchangez": self.app.defaults["geometry_toolchangez"], + "endz": self.app.defaults["geometry_endz"], + "spindlespeed": self.app.defaults["geometry_spindlespeed"], + "toolchangexy": self.app.defaults["geometry_toolchangexy"], + "startz": self.app.defaults["geometry_startz"], + + "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"] + }) + paint_tools = dict() + + tooluid = 0 + for tool in tools: + tooluid += 1 + paint_tools.update({ + int(tooluid): { + 'tooldia': float('%.4f' % tool), + 'offset': 'Path', + 'offset_value': 0.0, + 'type': 'Iso', + 'tool_type': 'C1', + 'data': dict(default_data), + 'solid_geometry': [] + } + }) + if obj is None: - self.raise_tcl_error("Object not found: %s" % name) + return "Object not found: %s" % name - if not isinstance(obj, Geometry): - self.raise_tcl_error('Expected Geometry, got %s %s.' % (name, type(obj))) - - if 'all' in args and args['all']: - obj.paint_poly_all(tooldia, overlap, outname) + # Paint all polygons in the painted object + if 'all' in args and args['all'] is True: + self.app.paint_tool.paint_poly_all(obj=obj, + tooldia=tooldia, + overlap=overlap, + order=order, + margin=margin, + method=method, + outname=outname, + connect=connect, + contour=contour, + tools_storage=paint_tools) return - if 'x' not in args or 'y' not in args: - self.raise_tcl_error('Expected -all 1 or -x and -y .') + # Paint single polygon in the painted object + elif 'single' in args and args['single'] is True: + if 'x' not in args or 'y' not in args: + self.raise_tcl_error('%s' % _("Expected -x and -y .")) + else: + x = args['x'] + y = args['y'] - x = args['x'] - y = args['y'] + self.app.paint_tool.paint_poly(obj=obj, + inside_pt=[x, y], + tooldia=tooldia, + overlap=overlap, + order=order, + margin=margin, + method=method, + outname=outname, + connect=connect, + contour=contour, + tools_storage=paint_tools) + return - obj.paint_poly_single_click([x, y], tooldia, overlap, outname) + # Paint all polygons found within the box object from the the painted object + elif 'ref' in args and args['ref'] is True: + if 'box' not in args: + self.raise_tcl_error('%s' % _("Expected -box .")) + else: + box_name = args['box'] + # Get box source object. + try: + box_obj = self.app.collection.get_by_name(str(box_name)) + except Exception as e: + log.debug("TclCommandPaint.execute() --> %s" % str(e)) + self.raise_tcl_error("%s: %s" % (_("Could not retrieve box object"), name)) + return "Could not retrieve object: %s" % name + self.app.paint_tool.paint_poly_ref(obj=obj, + sel_obj=box_obj, + tooldia=tooldia, + overlap=overlap, + order=order, + margin=margin, + method=method, + outname=outname, + connect=connect, + contour=contour, + tools_storage=paint_tools) + return + + else: + self.raise_tcl_error("%s:" % _("There was none of the following args: 'ref', 'single', 'all'.\n" + "Paint failed.")) + return "There was none of the following args: 'ref', 'single', 'all'.\n" \ + "Paint failed."