diff --git a/FlatCAMApp.py b/FlatCAMApp.py index ee6dcf03..ecca8b7e 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -5694,13 +5694,12 @@ class App(QtCore.QObject): self.plotcanvas.vispy_canvas.view.camera.pan_button_setting = self.defaults['global_pan_button'] self.pos_canvas = self.plotcanvas.vispy_canvas.translate_coords(event.pos) + self.pos = (self.pos_canvas[0], self.pos_canvas[1]) + self.app_cursor.enabled = False - if self.grid_status() == True: + if self.grid_status(): self.pos = self.geo_editor.snap(self.pos_canvas[0], self.pos_canvas[1]) self.app_cursor.enabled = True - else: - self.pos = (self.pos_canvas[0], self.pos_canvas[1]) - self.app_cursor.enabled = False try: modifiers = QtWidgets.QApplication.keyboardModifiers() @@ -5753,7 +5752,7 @@ class App(QtCore.QObject): if self.rel_point1 is not None: try: # May fail in case mouse not within axes pos_canvas = self.plotcanvas.vispy_canvas.translate_coords(event.pos) - if self.grid_status(): + if self.grid_status() == True: pos = self.geo_editor.snap(pos_canvas[0], pos_canvas[1]) self.app_cursor.enabled = True # Update cursor @@ -6157,6 +6156,12 @@ class App(QtCore.QObject): face_color = kwargs['face_color'] else: face_color = self.defaults['global_sel_fill'] + + if 'face_alpha' in kwargs: + face_alpha = kwargs['face_alpha'] + else: + face_alpha = 0.3 + x0, y0 = old_coords x1, y1 = coords pt1 = (x0, y0) @@ -6166,7 +6171,7 @@ class App(QtCore.QObject): sel_rect = Polygon([pt1, pt2, pt3, pt4]) color_t = Color(face_color) - color_t.alpha = 0.3 + color_t.alpha = face_alpha self.move_tool.sel_shapes.add(sel_rect, color=color, face_color=color_t, update=True, layer=0, tolerance=None) diff --git a/README.md b/README.md index 67f7ae51..36acccaa 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ CAD program, and create G-Code for Isolation routing. 9.09.2018 - added Exception handing for the case when the user is trying to save & overwrite a file already opened in another file +- finished added 'Area' type of Paint in Paint Tool 7.09.2019 diff --git a/flatcamTools/ToolPaint.py b/flatcamTools/ToolPaint.py index 3eb64b64..64650eee 100644 --- a/flatcamTools/ToolPaint.py +++ b/flatcamTools/ToolPaint.py @@ -269,6 +269,9 @@ class ToolPaint(FlatCAMTool, Gerber): self.units = '' self.paint_tools = {} self.tooluid = 0 + self.first_click = False + self.cursor_pos = None + # store here the default data for Geometry Data self.default_data = {} self.default_data.update({ @@ -828,29 +831,61 @@ class ToolPaint(FlatCAMTool, Gerber): def on_mouse_press(event): # do paint single only for left mouse clicks if event.button == 1: - self.app.inform.emit(_("Painting selected area...")) - self.app.plotcanvas.vis_disconnect('mouse_press', doit) + if not self.first_click: + self.first_click = True + self.app.inform.emit(_("[WARNING_NOTCL] Click the end point of the paint area.")) - pos = self.app.plotcanvas.vispy_canvas.translate_coords(event.pos) - if self.app.grid_status(): - pos = self.app.geo_editor.snap(pos[0], pos[1]) + self.cursor_pos = self.app.plotcanvas.vispy_canvas.translate_coords(event.pos) + if self.app.grid_status(): + self.cursor_pos = self.app.geo_editor.snap(self.cursor_pos[0], self.cursor_pos[1]) + else: + self.app.inform.emit(_("Done.")) + self.first_click = False + self.app.delete_selection_shape() - self.paint_poly(self.paint_obj, - inside_pt=[pos[0], pos[1]], - tooldia=tooldia, - overlap=overlap, - connect=connect, - contour=contour) + curr_pos = self.app.plotcanvas.vispy_canvas.translate_coords(event.pos) + if self.app.grid_status(): + curr_pos = self.app.geo_editor.snap(curr_pos[0], curr_pos[1]) - # to be called after second click on plot - def on_mouse_click_release(event): - self.app.plotcanvas.vis_connect('mouse_press', self.app.on_mouse_click_over_plot) - self.app.plotcanvas.vis_connect('mouse_move', self.app.on_mouse_move_over_plot) - self.app.plotcanvas.vis_connect('mouse_release', self.app.on_mouse_click_release_over_plot) + x0, y0 = self.cursor_pos[0], self.cursor_pos[1] + x1, y1 = curr_pos[0], curr_pos[1] + pt1 = (x0, y0) + pt2 = (x1, y0) + pt3 = (x1, y1) + pt4 = (x0, y1) + sel_rect = Polygon([pt1, pt2, pt3, pt4]) + + self.paint_poly_area(obj=self.paint_obj, + sel_obj= sel_rect, + outname=o_name, + overlap=overlap, + connect=connect, + contour=contour) + + self.app.plotcanvas.vis_disconnect('mouse_press', on_mouse_press) + self.app.plotcanvas.vis_disconnect('mouse_move', on_mouse_move) + + self.app.plotcanvas.vis_connect('mouse_press', self.app.on_mouse_click_over_plot) + self.app.plotcanvas.vis_connect('mouse_move', self.app.on_mouse_move_over_plot) + self.app.plotcanvas.vis_connect('mouse_release', self.app.on_mouse_click_release_over_plot) # called on mouse move def on_mouse_move(event): - pass + curr_pos = self.app.plotcanvas.vispy_canvas.translate_coords(event.pos) + self.app.app_cursor.enabled = False + + if self.app.grid_status(): + self.app.app_cursor.enabled = True + # Update cursor + curr_pos = self.app.geo_editor.snap(curr_pos[0], curr_pos[1]) + self.app.app_cursor.set_data(np.asarray([(curr_pos[0], curr_pos[1])]), + symbol='++', edge_color='black', size=20) + + if self.first_click: + self.app.delete_selection_shape() + self.app.draw_moving_selection_shape(old_coords=(self.cursor_pos[0], self.cursor_pos[1]), + coords=(curr_pos[0], curr_pos[1]), + face_alpha=0.0) self.app.plotcanvas.vis_disconnect('mouse_press', self.app.on_mouse_click_over_plot) self.app.plotcanvas.vis_disconnect('mouse_move', self.app.on_mouse_move_over_plot) @@ -858,7 +893,6 @@ class ToolPaint(FlatCAMTool, Gerber): self.app.plotcanvas.vis_connect('mouse_press', on_mouse_press) self.app.plotcanvas.vis_connect('mouse_move', on_mouse_move) - self.app.plotcanvas.vis_connect('mouse_release', on_mouse_click_release) def paint_poly(self, obj, inside_pt, tooldia, overlap, outname=None, connect=True, contour=True): """ @@ -1321,5 +1355,323 @@ 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): + """ + 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 connect: Connect lines to avoid tool lifts. + :param contour: Paint around the edges. + :return: + """ + paint_method = 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 + 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 + + def recurse(geometry, reset=True): + """ + Creates a list of non-iterable linear geometry objects. + Results are placed in self.flat_geometry + + :param geometry: Shapely type or list or list of list of such. + :param reset: Clears the contents of self.flat_geometry. + """ + + if geometry is None: + return + + if reset: + self.flat_geometry = [] + + # ## If iterable, expand recursively. + try: + for geo in geometry: + if geo is not None: + recurse(geometry=geo, reset=False) + + # ## Not iterable, do the actual indexing and add. + except TypeError: + if isinstance(geometry, LinearRing): + g = Polygon(geometry) + self.flat_geometry.append(g) + else: + self.flat_geometry.append(geometry) + + return self.flat_geometry + + # 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) + 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) + + geo_to_paint = [] + for poly in obj.solid_geometry: + new_pol = poly.intersection(sel_obj) + geo_to_paint.append(new_pol) + + try: + a, b, c, d = self.paint_bounds(geo_to_paint) + geo_obj.options['xmin'] = a + geo_obj.options['ymin'] = b + geo_obj.options['xmax'] = c + geo_obj.options['ymax'] = d + except Exception as e: + log.debug("ToolPaint.paint_poly.gen_paintarea() bounds error --> %s" % str(e)) + return + + 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(): + if float('%.4f' % v['tooldia']) == float('%.4f' % tool_dia): + current_uid = int(k) + break + + for geo in recurse(geo_to_paint): + try: + # Polygons are the only really paintable geometries, lines in theory have no area to be painted + if not isinstance(geo, Polygon): + continue + poly_buf = geo.buffer(-paint_margin) + + if paint_method == "seed": + # Type(cp) == FlatCAMRTreeStorage | None + cp = self.clear_polygon2(poly_buf, + tooldia=tool_dia, + steps_per_circle=self.app.defaults["geometry_circle_steps"], + overlap=over, + contour=cont, + connect=conn) + + elif paint_method == "lines": + # Type(cp) == FlatCAMRTreeStorage | None + cp = self.clear_polygon3(poly_buf, + tooldia=tool_dia, + steps_per_circle=self.app.defaults["geometry_circle_steps"], + overlap=over, + contour=cont, + connect=conn) + + else: + # Type(cp) == FlatCAMRTreeStorage | None + cp = self.clear_polygon(poly_buf, + tooldia=tool_dia, + steps_per_circle=self.app.defaults["geometry_circle_steps"], + overlap=over, + contour=cont, + connect=conn) + + if cp is not None: + 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 All. Try a different combination of parameters. " + "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) + + self.paint_tools[current_uid]['data']['name'] = name + total_geometry[:] = [] + + 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) + + # 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 + + # Experimental... + # print("Indexing...", end=' ') + # geo_obj.make_index() + + self.app.inform.emit(_("[success] Paint All Done.")) + + # 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) + + 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 = [] + current_uid = int(1) + geo_obj.solid_geometry = [] + + try: + a, b, c, d = obj.bounds() + geo_obj.options['xmin'] = a + geo_obj.options['ymin'] = b + geo_obj.options['xmax'] = c + geo_obj.options['ymax'] = d + except Exception as e: + log.debug("ToolPaint.paint_poly.gen_paintarea() bounds error --> %s" % str(e)) + return + + for tool_dia in sorted_tools: + for geo in recurse(obj.solid_geometry): + try: + geo = Polygon(geo) if not isinstance(geo, Polygon) else geo + poly_buf = geo.buffer(-paint_margin) + cp = None + + if paint_method == "standard": + # Type(cp) == FlatCAMRTreeStorage | None + cp = self.clear_polygon(poly_buf, tooldia=tool_dia, + steps_per_circle=self.app.defaults["geometry_circle_steps"], + overlap=over, contour=cont, connect=conn) + + elif paint_method == "seed": + # Type(cp) == FlatCAMRTreeStorage | None + cp = self.clear_polygon2(poly_buf, tooldia=tool_dia, + steps_per_circle=self.app.defaults["geometry_circle_steps"], + overlap=over, contour=cont, connect=conn) + + elif paint_method == "lines": + # Type(cp) == FlatCAMRTreeStorage | None + cp = self.clear_polygon3(poly_buf, tooldia=tool_dia, + steps_per_circle=self.app.defaults["geometry_circle_steps"], + overlap=over, contour=cont, connect=conn) + + if cp is not None: + cleared_geo += 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 All. Try a different combination of parameters. " + "Or a different Method of paint\n%s") % str(e)) + 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(): + 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) + + self.paint_tools[current_uid]['data']['name'] = name + cleared_geo[:] = [] + + 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) + + # 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_NOTCL] 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 + + # Experimental... + # print("Indexing...", end=' ') + # geo_obj.make_index() + + self.app.inform.emit(_("[success] Paint All with Rest-Machining done.")) + + def job_thread(app_obj): + try: + if self.rest_cb.isChecked(): + app_obj.new_object("geometry", name, gen_paintarea_rest_machining) + else: + app_obj.new_object("geometry", name, gen_paintarea) + except Exception as e: + proc.done() + traceback.print_stack() + return + proc.done() + # focus on Selected Tab + self.app.ui.notebook.setCurrentWidget(self.app.ui.selected_tab) + + self.app.inform.emit(_("Polygon Paint started ...")) + + # Promise object with the new name + self.app.collection.promise(name) + + # Background + self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]}) + + @staticmethod + def paint_bounds(geometry): + def bounds_rec(o): + if type(o) is list: + minx = Inf + miny = Inf + maxx = -Inf + maxy = -Inf + + for k in o: + try: + minx_, miny_, maxx_, maxy_ = bounds_rec(k) + except Exception as e: + log.debug("ToolPaint.bounds() --> %s" % str(e)) + return + + minx = min(minx, minx_) + miny = min(miny, miny_) + maxx = max(maxx, maxx_) + maxy = max(maxy, maxy_) + return minx, miny, maxx, maxy + else: + # it's a Shapely object, return it's bounds + return o.bounds + + return bounds_rec(geometry) + def reset_fields(self): self.object_combo.setRootModelIndex(self.app.collection.index(2, 0, QtCore.QModelIndex()))