From cc303ce4f2584e4d5dd0510de54b313f1cee8377 Mon Sep 17 00:00:00 2001 From: Marius Stanciu Date: Mon, 23 Dec 2019 15:49:16 +0200 Subject: [PATCH 1/5] - speed up the plotting in OpenGL(3D) graphic mode - spped up the color setting for Gerber object when using the OpenGL(3D) graphic mode --- README.md | 2 ++ flatcamGUI/VisPyVisuals.py | 71 +++++++++++++++++++++----------------- 2 files changed, 41 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index fadfdd62..0ebd5ead 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,8 @@ CAD program, and create G-Code for Isolation routing. - some fixes in the Legacy(2D) graphic mode regarding the possibility of changing the color of the Gerber objects - added a method to darken the outline color for Gerber objects when they have the color set - when Printing as PDF Gerber objects now the rendered color is the print color +- speed up the plotting in OpenGL(3D) graphic mode +- spped up the color setting for Gerber object when using the OpenGL(3D) graphic mode 22.12.2019 diff --git a/flatcamGUI/VisPyVisuals.py b/flatcamGUI/VisPyVisuals.py index b70f4a79..e881e5c9 100644 --- a/flatcamGUI/VisPyVisuals.py +++ b/flatcamGUI/VisPyVisuals.py @@ -45,44 +45,48 @@ def _update_shape_buffers(data, triangulation='glu'): geo, color, face_color, tolerance = data['geometry'], data['color'], data['face_color'], data['tolerance'] if geo is not None and not geo.is_empty: - simple = geo.simplify(tolerance) if tolerance else geo # Simplified shape - pts = [] # Shape line points - tri_pts = [] # Mesh vertices - tri_tris = [] # Mesh faces + simplified_geo = geo.simplify(tolerance) if tolerance else geo # Simplified shape + pts = [] # Shape line points + tri_pts = [] # Mesh vertices + tri_tris = [] # Mesh faces if type(geo) == LineString: # Prepare lines - pts = _linestring_to_segments(list(simple.coords)) + pts = _linestring_to_segments(list(simplified_geo.coords)) elif type(geo) == LinearRing: # Prepare lines - pts = _linearring_to_segments(list(simple.coords)) + pts = _linearring_to_segments(list(simplified_geo.coords)) elif type(geo) == Polygon: # Prepare polygon faces if face_color is not None: if triangulation == 'glu': gt = GLUTess() - tri_tris, tri_pts = gt.triangulate(simple) + tri_tris, tri_pts = gt.triangulate(simplified_geo) else: print("Triangulation type '%s' isn't implemented. Drawing only edges." % triangulation) # Prepare polygon edges if color is not None: - pts = _linearring_to_segments(list(simple.exterior.coords)) - for ints in simple.interiors: + pts = _linearring_to_segments(list(simplified_geo.exterior.coords)) + for ints in simplified_geo.interiors: pts += _linearring_to_segments(list(ints.coords)) # Appending data for mesh if len(tri_pts) > 0 and len(tri_tris) > 0: mesh_tris += tri_tris mesh_vertices += tri_pts - mesh_colors += [Color(face_color).rgba] * (len(tri_tris) // 3) + face_color_rgba = Color(face_color).rgba + # mesh_colors += [face_color_rgba] * (len(tri_tris) // 3) + mesh_colors += [face_color_rgba for __ in range(len(tri_tris) // 3)] # Appending data for line if len(pts) > 0: line_pts += pts - line_colors += [Color(color).rgba] * len(pts) + colo_rgba = Color(color).rgba + # line_colors += [colo_rgba] * len(pts) + line_colors += [colo_rgba for __ in range(len(pts))] # Store buffers data['line_pts'] = line_pts @@ -314,12 +318,27 @@ class ShapeCollectionVisual(CompoundVisual): self.__update() def update_color(self, new_mesh_color=None, new_line_color=None, indexes=None): - if (new_mesh_color is None or new_mesh_color == '') and (new_line_color is None or new_line_color == ''): + if new_mesh_color is None and new_line_color is None: return if not self.data: return + # if a new color is empty string then make it None so it will not be updated + # if a new color is valid then transform it here in a format palatable + mesh_color_rgba = None + line_color_rgba = None + if new_mesh_color: + if new_mesh_color != '': + mesh_color_rgba = Color(new_mesh_color).rgba + else: + new_mesh_color = None + if new_line_color: + if new_line_color != '': + line_color_rgba = Color(new_line_color).rgba + else: + new_line_color = None + mesh_colors = [[] for _ in range(0, len(self._meshes))] # Face colors line_colors = [[] for _ in range(0, len(self._meshes))] # Line colors line_pts = [[] for _ in range(0, len(self._lines))] # Vertices for line @@ -335,13 +354,10 @@ class ShapeCollectionVisual(CompoundVisual): dim_mesh_tris = (len(data['mesh_tris']) // 3) if dim_mesh_tris != 0: try: - mesh_colors[data['layer']] += [Color(new_mesh_color).rgba] * dim_mesh_tris + mesh_colors[data['layer']] += [mesh_color_rgba] * dim_mesh_tris self.data[k]['face_color'] = new_mesh_color - new_temp = list() - for i in range(len(data['mesh_colors'])): - new_temp.append(Color(new_mesh_color).rgba) - data['mesh_colors'] = new_temp + data['mesh_colors'] = [mesh_color_rgba for __ in range(len(data['mesh_colors']))] except Exception as e: print("VisPyVisuals.ShapeCollectionVisual.update_color(). " "Create mesh colors --> Data error. %s" % str(e)) @@ -351,13 +367,10 @@ class ShapeCollectionVisual(CompoundVisual): if dim_line_pts != 0: try: line_pts[data['layer']] += data['line_pts'] - line_colors[data['layer']] += [Color(new_line_color).rgba] * dim_line_pts + line_colors[data['layer']] += [line_color_rgba] * dim_line_pts self.data[k]['color'] = new_line_color - new_temp = list() - for i in range(len(data['line_colors'])): - new_temp.append(Color(new_line_color).rgba) - data['line_colors'] = new_temp + data['line_colors'] = [mesh_color_rgba for __ in range(len(data['line_colors']))] except Exception as e: print("VisPyVisuals.ShapeCollectionVisual.update_color(). " "Create line colors --> Data error. %s" % str(e)) @@ -371,13 +384,10 @@ class ShapeCollectionVisual(CompoundVisual): if new_mesh_color and new_mesh_color != '': if dim_mesh_tris != 0: try: - mesh_colors[data['layer']] += [Color(new_mesh_color).rgba] * dim_mesh_tris + mesh_colors[data['layer']] += [mesh_color_rgba] * dim_mesh_tris self.data[k]['face_color'] = new_mesh_color - new_temp = list() - for i in range(len(data['mesh_colors'])): - new_temp.append(Color(new_mesh_color).rgba) - data['mesh_colors'] = new_temp + data['mesh_colors'] = [mesh_color_rgba for __ in range(len(data['mesh_colors']))] except Exception as e: print("VisPyVisuals.ShapeCollectionVisual.update_color(). " "Create mesh colors --> Data error. %s" % str(e)) @@ -385,13 +395,10 @@ class ShapeCollectionVisual(CompoundVisual): if dim_line_pts != 0: try: line_pts[data['layer']] += data['line_pts'] - line_colors[data['layer']] += [Color(new_line_color).rgba] * dim_line_pts + line_colors[data['layer']] += [line_color_rgba] * dim_line_pts self.data[k]['color'] = new_line_color - new_temp = list() - for i in range(len(data['line_colors'])): - new_temp.append(Color(new_line_color).rgba) - data['line_colors'] = new_temp + data['line_colors'] = [mesh_color_rgba for __ in range(len(data['line_colors']))] except Exception as e: print("VisPyVisuals.ShapeCollectionVisual.update_color(). " "Create line colors --> Data error. %s" % str(e)) From 98e246a5c4b7b65f2f71188e2098d449d77cc9eb Mon Sep 17 00:00:00 2001 From: Marius Stanciu Date: Mon, 23 Dec 2019 17:30:04 +0200 Subject: [PATCH 2/5] - when the selection is changed in the Project Tree the selection shape on canvas is deleted - if an object is selected on Project Tree and it does not have the selection shape drawn, first click on canvas over it will draw the selection shape --- FlatCAMApp.py | 50 ++++++++++++++++++++++++++++++--------------- FlatCAMObj.py | 3 +++ ObjectCollection.py | 5 +++++ README.md | 3 +++ 4 files changed, 44 insertions(+), 17 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 75a77f76..a6c396b9 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -8904,20 +8904,26 @@ class App(QtCore.QObject): # create the selection box around the selected object if self.defaults['global_selection_shape'] is True: self.draw_selection_shape(curr_sel_obj) + curr_sel_obj.selection_shape_drawn = True - elif self.collection.get_active().options['name'] not in objects_under_the_click_list: + elif curr_sel_obj.options['name'] not in objects_under_the_click_list: self.on_objects_selection(False) self.delete_selection_shape() + curr_sel_obj.selection_shape_drawn = False self.collection.set_active(objects_under_the_click_list[0]) curr_sel_obj = self.collection.get_active() - # create the selection box around the selected object if self.defaults['global_selection_shape'] is True: self.draw_selection_shape(curr_sel_obj) + curr_sel_obj.selection_shape_drawn = True self.selected_message(curr_sel_obj=curr_sel_obj) + elif curr_sel_obj.selection_shape_drawn is False: + if self.defaults['global_selection_shape'] is True: + self.draw_selection_shape(curr_sel_obj) + curr_sel_obj.selection_shape_drawn = True else: self.on_objects_selection(False) self.delete_selection_shape() @@ -8932,6 +8938,7 @@ class App(QtCore.QObject): # make active the first element of the overlapped objects list if self.collection.get_active() is None: self.collection.set_active(objects_under_the_click_list[0]) + objects_under_the_click_list[0].selection_shape_drawn = True name_sel_obj = self.collection.get_active().options['name'] # In case that there is a selected object but it is not in the overlapped object list @@ -8949,9 +8956,12 @@ class App(QtCore.QObject): curr_sel_obj = self.collection.get_active() # delete the possible selection box around a possible selected object self.delete_selection_shape() + curr_sel_obj.selection_shape_drawn = False + # create the selection box around the selected object if self.defaults['global_selection_shape'] is True: self.draw_selection_shape(curr_sel_obj) + curr_sel_obj.selection_shape_drawn = True self.selected_message(curr_sel_obj=curr_sel_obj) @@ -8961,6 +8971,9 @@ class App(QtCore.QObject): # delete the possible selection box around a possible selected object self.delete_selection_shape() + for o in self.collection.get_list(): + o.selection_shape_drawn = False + # and as a convenience move the focus to the Project tab because Selected tab is now empty but # only when working on App if self.call_source == 'app': @@ -12365,7 +12378,10 @@ class App(QtCore.QObject): new_color = self.defaults['global_plot_fill'] act_name = self.sender().text().lower() - sel_obj = self.collection.get_active() + sel_obj_list = self.collection.get_selected() + + if not sel_obj_list: + return if act_name == 'red': new_color = '#FF0000' + \ @@ -12397,22 +12413,22 @@ class App(QtCore.QObject): new_color = str(plot_fill_color.name()) + \ str(hex(self.ui.general_defaults_form.general_gui_group.pf_color_alpha_slider.value())[2:]) - if self.is_legacy is False: - new_line_color = color_variant(new_color[:7], 0.7) - sel_obj.fill_color = new_color - sel_obj.outline_color = new_line_color + new_line_color = color_variant(new_color[:7], 0.7) - sel_obj.shapes.redraw( - update_colors=(new_color, new_line_color) - ) - else: - new_line_color = color_variant(new_color[:7], 0.7) + for sel_obj in sel_obj_list: + if self.is_legacy is False: + sel_obj.fill_color = new_color + sel_obj.outline_color = new_line_color - sel_obj.fill_color = new_color - sel_obj.outline_color = new_line_color - sel_obj.shapes.redraw( - update_colors=(new_color, new_line_color) - ) + sel_obj.shapes.redraw( + update_colors=(new_color, new_line_color) + ) + else: + sel_obj.fill_color = new_color + sel_obj.outline_color = new_line_color + sel_obj.shapes.redraw( + update_colors=(new_color, new_line_color) + ) def on_grid_snap_triggered(self, state): if state: diff --git a/FlatCAMObj.py b/FlatCAMObj.py index 44e5aa31..dbd5eb48 100644 --- a/FlatCAMObj.py +++ b/FlatCAMObj.py @@ -128,6 +128,9 @@ class FlatCAMObj(QtCore.QObject): self.isHovering = False self.notHovering = True + # Flag to show if a selection shape is drawn + self.selection_shape_drawn = False + # self.units = 'IN' self.units = self.app.defaults['units'] diff --git a/ObjectCollection.py b/ObjectCollection.py index f134fc32..d2c37096 100644 --- a/ObjectCollection.py +++ b/ObjectCollection.py @@ -787,6 +787,11 @@ class ObjectCollection(QtCore.QAbstractItemModel): # FlatCAMApp.App.log.debug("Current: %s, Previous %s" % (str(current), str(previous))) try: + # delete selection shape + self.app.delete_selection_shape() + for o in self.get_list(): + o.selection_shape_drawn = False + obj = current.indexes()[0].internalPointer().obj self.item_selected.emit(obj.options['name']) diff --git a/README.md b/README.md index 0ebd5ead..eef59695 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,9 @@ CAD program, and create G-Code for Isolation routing. - when Printing as PDF Gerber objects now the rendered color is the print color - speed up the plotting in OpenGL(3D) graphic mode - spped up the color setting for Gerber object when using the OpenGL(3D) graphic mode +- setting color for Gerber objects work on a selection of Gerber objects +- when the selection is changed in the Project Tree the selection shape on canvas is deleted +- if an object is selected on Project Tree and it does not have the selection shape drawn, first click on canvas over it will draw the selection shape 22.12.2019 From fe5c2c7836ff3667c436f3746a390c3289af0b15 Mon Sep 17 00:00:00 2001 From: Marius Stanciu Date: Mon, 23 Dec 2019 17:47:47 +0200 Subject: [PATCH 3/5] - small change --- FlatCAMApp.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index a6c396b9..0df5f314 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -8852,12 +8852,14 @@ class App(QtCore.QObject): # create the selection box around the selected object if self.defaults['global_selection_shape'] is True: self.draw_selection_shape(obj) + obj.selection_shape_drawn = True self.collection.set_active(obj.options['name']) else: if poly_selection.intersects(poly_obj): # create the selection box around the selected object if self.defaults['global_selection_shape'] is True: self.draw_selection_shape(obj) + obj.selection_shape_drawn = True self.collection.set_active(obj.options['name']) except Exception as e: # the Exception here will happen if we try to select on screen and we have an newly (and empty) From b1b140634b85fe277fddeacda72a8030cb2e56c4 Mon Sep 17 00:00:00 2001 From: Marius Stanciu Date: Mon, 23 Dec 2019 22:59:01 +0200 Subject: [PATCH 4/5] - in Tool Transform added a new feature named 'Buffer'. For Geometry and Gerber objects will create (and replace) a geometry at a distance from the original geometry and for Excellon will adjust the Tool diameters --- FlatCAMApp.py | 4 + FlatCAMObj.py | 17 +++- ObjectCollection.py | 5 -- README.md | 3 +- camlib.py | 63 +++++++++++++++ flatcamGUI/PreferencesUI.py | 50 ++++++++++-- flatcamParsers/ParseExcellon.py | 33 +++++++- flatcamParsers/ParseGerber.py | 81 +++++++++++++++++++ flatcamTools/ToolTransform.py | 136 ++++++++++++++++++++++++++++---- 9 files changed, 359 insertions(+), 33 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 0df5f314..2ba31254 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -830,6 +830,8 @@ class App(QtCore.QObject): "tools_transform_offset_y": 0.0, "tools_transform_mirror_reference": False, "tools_transform_mirror_point": (0, 0), + "tools_transform_buffer_dis": 0.0, + "tools_transform_buffer_corner": True, # SolderPaste Tool "tools_solderpaste_tools": "1.0, 0.3", @@ -1432,6 +1434,8 @@ class App(QtCore.QObject): "tools_transform_offset_y": self.ui.tools_defaults_form.tools_transform_group.offy_entry, "tools_transform_mirror_reference": self.ui.tools_defaults_form.tools_transform_group.mirror_reference_cb, "tools_transform_mirror_point": self.ui.tools_defaults_form.tools_transform_group.flip_ref_entry, + "tools_transform_buffer_dis": self.ui.tools_defaults_form.tools_transform_group.buffer_entry, + "tools_transform_buffer_corner": self.ui.tools_defaults_form.tools_transform_group.buffer_rounded_cb, # SolderPaste Dispensing Tool "tools_solderpaste_tools": self.ui.tools_defaults_form.tools_solderpaste_group.nozzle_tool_dia_entry, diff --git a/FlatCAMObj.py b/FlatCAMObj.py index dbd5eb48..4ac69b82 100644 --- a/FlatCAMObj.py +++ b/FlatCAMObj.py @@ -599,7 +599,9 @@ class FlatCAMGerber(FlatCAMObj, Gerber): def __init__(self, name): self.decimals = self.app.decimals - Gerber.__init__(self, steps_per_circle=int(self.app.defaults["gerber_circle_steps"])) + self.circle_steps = int(self.app.defaults["gerber_circle_steps"]) + + Gerber.__init__(self, steps_per_circle=self.circle_steps) FlatCAMObj.__init__(self, name) self.kind = "gerber" @@ -2199,6 +2201,10 @@ class FlatCAMGerber(FlatCAMObj, Gerber): Gerber.skew(self, angle_x=angle_x, angle_y=angle_y, point=point) self.replotApertures.emit() + def buffer(self, distance, join): + Gerber.buffer(self, distance=distance, join=join) + self.replotApertures.emit() + def serialize(self): return { "options": self.options, @@ -2217,7 +2223,9 @@ class FlatCAMExcellon(FlatCAMObj, Excellon): def __init__(self, name): self.decimals = self.app.decimals - Excellon.__init__(self, geo_steps_per_circle=int(self.app.defaults["geometry_circle_steps"])) + self.circle_steps = int(self.app.defaults["geometry_circle_steps"]) + + Excellon.__init__(self, geo_steps_per_circle=self.circle_steps) FlatCAMObj.__init__(self, name) self.kind = "excellon" @@ -3545,8 +3553,11 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): def __init__(self, name): self.decimals = self.app.decimals + + self.circle_steps = int(self.app.defaults["geometry_circle_steps"]) + FlatCAMObj.__init__(self, name) - Geometry.__init__(self, geo_steps_per_circle=int(self.app.defaults["geometry_circle_steps"])) + Geometry.__init__(self, geo_steps_per_circle=self.circle_steps) self.kind = "geometry" diff --git a/ObjectCollection.py b/ObjectCollection.py index d2c37096..f134fc32 100644 --- a/ObjectCollection.py +++ b/ObjectCollection.py @@ -787,11 +787,6 @@ class ObjectCollection(QtCore.QAbstractItemModel): # FlatCAMApp.App.log.debug("Current: %s, Previous %s" % (str(current), str(previous))) try: - # delete selection shape - self.app.delete_selection_shape() - for o in self.get_list(): - o.selection_shape_drawn = False - obj = current.indexes()[0].internalPointer().obj self.item_selected.emit(obj.options['name']) diff --git a/README.md b/README.md index eef59695..b4160bec 100644 --- a/README.md +++ b/README.md @@ -17,8 +17,9 @@ CAD program, and create G-Code for Isolation routing. - speed up the plotting in OpenGL(3D) graphic mode - spped up the color setting for Gerber object when using the OpenGL(3D) graphic mode - setting color for Gerber objects work on a selection of Gerber objects -- when the selection is changed in the Project Tree the selection shape on canvas is deleted +- ~~when the selection is changed in the Project Tree the selection shape on canvas is deleted~~ - if an object is selected on Project Tree and it does not have the selection shape drawn, first click on canvas over it will draw the selection shape +- in Tool Transform added a new feature named 'Buffer'. For Geometry and Gerber objects will create (and replace) a geometry at a distance from the original geometry and for Excellon will adjust the Tool diameters 22.12.2019 diff --git a/camlib.py b/camlib.py index f699ee36..c1751bcb 100644 --- a/camlib.py +++ b/camlib.py @@ -2118,6 +2118,69 @@ class Geometry(object): # self.solid_geometry = affinity.skew(self.solid_geometry, angle_x, angle_y, # origin=(px, py)) + def buffer(self, distance, join): + """ + + :param distance: + :param join: + :return: + """ + + log.debug("camlib.Geometry.buffer()") + + if distance == 0: + return + + def buffer_geom(obj): + if type(obj) is list: + new_obj = [] + for g in obj: + new_obj.append(buffer_geom(g)) + return new_obj + else: + try: + self.el_count += 1 + disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100])) + if self.old_disp_number < disp_number <= 100: + self.app.proc_container.update_view_text(' %d%%' % disp_number) + self.old_disp_number = disp_number + + return obj.buffer(distance, resolution=self.geo_steps_per_circle, join_style=join) + except AttributeError: + return obj + + try: + if self.multigeo is True: + for tool in self.tools: + # variables to display the percentage of work done + self.geo_len = 0 + try: + for __ in self.tools[tool]['solid_geometry']: + self.geo_len += 1 + except TypeError: + self.geo_len = 1 + self.old_disp_number = 0 + self.el_count = 0 + + self.tools[tool]['solid_geometry'] = buffer_geom(self.tools[tool]['solid_geometry']) + + # variables to display the percentage of work done + self.geo_len = 0 + try: + for __ in self.solid_geometry: + self.geo_len += 1 + except TypeError: + self.geo_len = 1 + self.old_disp_number = 0 + self.el_count = 0 + + self.solid_geometry = buffer_geom(self.solid_geometry) + + self.app.inform.emit('[success] %s...' % _('Object was buffered')) + except AttributeError: + self.app.inform.emit('[ERROR_NOTCL] %s' % _("Failed to buffer. No object selected")) + + self.app.proc_container.new_text = '' class AttrDict(dict): def __init__(self, *args, **kwargs): diff --git a/flatcamGUI/PreferencesUI.py b/flatcamGUI/PreferencesUI.py index 6b34a72c..f06e983f 100644 --- a/flatcamGUI/PreferencesUI.py +++ b/flatcamGUI/PreferencesUI.py @@ -5378,7 +5378,7 @@ class ToolsTransformPrefGroupUI(OptionsGroupUI): grid0.addWidget(self.skewy_label, 4, 0) grid0.addWidget(self.skewy_entry, 4, 1) - # ## Scale factor on X axis + # ## Scale scale_title_lbl = QtWidgets.QLabel('%s' % _("Scale")) grid0.addWidget(scale_title_lbl, 5, 0, 1, 2) @@ -5425,7 +5425,7 @@ class ToolsTransformPrefGroupUI(OptionsGroupUI): ) grid0.addWidget(self.reference_cb, 8, 1) - # ## Offset distance on X axis + # ## Offset offset_title_lbl = QtWidgets.QLabel('%s' % _("Offset")) grid0.addWidget(offset_title_lbl, 9, 0, 1, 2) @@ -5454,6 +5454,10 @@ class ToolsTransformPrefGroupUI(OptionsGroupUI): grid0.addWidget(self.offy_label, 11, 0) grid0.addWidget(self.offy_entry, 11, 1) + # ## Mirror + mirror_title_lbl = QtWidgets.QLabel('%s' % _("Mirror")) + grid0.addWidget(mirror_title_lbl, 12, 0, 1, 2) + # ## Mirror (Flip) Reference Point self.mirror_reference_cb = FCCheckBox('%s' % _("Mirror Reference")) self.mirror_reference_cb.setToolTip( @@ -5466,9 +5470,9 @@ class ToolsTransformPrefGroupUI(OptionsGroupUI): "Then click Add button to insert coordinates.\n" "Or enter the coords in format (x, y) in the\n" "Point Entry field and click Flip on X(Y)")) - grid0.addWidget(self.mirror_reference_cb, 12, 0, 1, 2) + grid0.addWidget(self.mirror_reference_cb, 13, 0, 1, 2) - self.flip_ref_label = QtWidgets.QLabel('%s' % _("Mirror Reference point")) + self.flip_ref_label = QtWidgets.QLabel('%s' % _("Mirror Reference point")) self.flip_ref_label.setToolTip( _("Coordinates in format (x, y) used as reference for mirroring.\n" "The 'x' in (x, y) will be used when using Flip on X and\n" @@ -5476,8 +5480,42 @@ class ToolsTransformPrefGroupUI(OptionsGroupUI): ) self.flip_ref_entry = EvalEntry2("(0, 0)") - grid0.addWidget(self.flip_ref_label, 13, 0, 1, 2) - grid0.addWidget(self.flip_ref_entry, 14, 0, 1, 2) + grid0.addWidget(self.flip_ref_label, 14, 0, 1, 2) + grid0.addWidget(self.flip_ref_entry, 15, 0, 1, 2) + + # ## Buffer + buffer_title_lbl = QtWidgets.QLabel('%s' % _("Buffer")) + grid0.addWidget(buffer_title_lbl, 16, 0, 1, 2) + + self.buffer_label = QtWidgets.QLabel('%s:' % _("Distance")) + self.buffer_label.setToolTip( + _("A positive value will create the effect of dilation,\n" + "while a negative value will create the effect of erosion.\n" + "Each geometry element of the object will be increased\n" + "or decreased with the 'distance'.") + ) + + self.buffer_entry = FCDoubleSpinner() + self.buffer_entry.set_precision(self.decimals) + self.buffer_entry.setSingleStep(0.1) + self.buffer_entry.setWrapping(True) + self.buffer_entry.set_range(-9999.9999, 9999.9999) + + grid0.addWidget(self.buffer_label, 17, 0) + grid0.addWidget(self.buffer_entry, 17, 1) + + self.buffer_rounded_cb = FCCheckBox() + self.buffer_rounded_cb.setText('%s' % _("Rounded")) + self.buffer_rounded_cb.setToolTip( + _("If checked then the buffer will surround the buffered shape,\n" + "every corner will be rounded.\n" + "If not checked then the buffer will follow the exact geometry\n" + "of the buffered shape.") + ) + + grid0.addWidget(self.buffer_rounded_cb, 18, 0, 1, 2) + + grid0.addWidget(QtWidgets.QLabel(''), 19, 0, 1, 2) self.layout.addStretch() diff --git a/flatcamParsers/ParseExcellon.py b/flatcamParsers/ParseExcellon.py index 0b5677dc..894851c6 100644 --- a/flatcamParsers/ParseExcellon.py +++ b/flatcamParsers/ParseExcellon.py @@ -1457,4 +1457,35 @@ class Excellon(Geometry): slot['start'] = affinity.rotate(slot['start'], angle, origin=(px, py)) self.create_geometry() - self.app.proc_container.new_text = '' \ No newline at end of file + self.app.proc_container.new_text = '' + + def buffer(self, distance, join): + """ + + :param distance: + :param join: + :return: + """ + log.debug("flatcamParsers.ParseExcellon.Excellon.buffer()") + + if distance == 0: + return + + def buffer_geom(obj): + if type(obj) is list: + new_obj = [] + for g in obj: + new_obj.append(buffer_geom(g)) + return new_obj + else: + try: + return obj.buffer(distance, resolution=self.geo_steps_per_circle) + except AttributeError: + return obj + + # buffer solid_geometry + for tool, tool_dict in list(self.tools.items()): + self.tools[tool]['solid_geometry'] = buffer_geom(tool_dict['solid_geometry']) + self.tools[tool]['C'] += distance + + self.create_geometry() diff --git a/flatcamParsers/ParseGerber.py b/flatcamParsers/ParseGerber.py index b9bc504d..881b49b6 100644 --- a/flatcamParsers/ParseGerber.py +++ b/flatcamParsers/ParseGerber.py @@ -2169,6 +2169,87 @@ class Gerber(Geometry): _("Gerber Rotate done.")) self.app.proc_container.new_text = '' + def buffer(self, distance, join): + """ + + :param distance: + :return: + """ + log.debug("parseGerber.Gerber.buffer()") + + if distance == 0: + return + + # variables to display the percentage of work done + self.geo_len = 0 + try: + for __ in self.solid_geometry: + self.geo_len += 1 + except TypeError: + self.geo_len = 1 + + self.old_disp_number = 0 + self.el_count = 0 + + def buffer_geom(obj): + if type(obj) is list: + new_obj = [] + for g in obj: + new_obj.append(buffer_geom(g)) + return new_obj + else: + try: + self.el_count += 1 + disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100])) + if self.old_disp_number < disp_number <= 100: + self.app.proc_container.update_view_text(' %d%%' % disp_number) + self.old_disp_number = disp_number + + return obj.buffer(distance, resolution=self.steps_per_circle, join_style=join) + except AttributeError: + return obj + + self.solid_geometry = buffer_geom(self.solid_geometry) + + # we need to buffer the geometry stored in the Gerber apertures, too + try: + for apid in self.apertures: + new_geometry = list() + if 'geometry' in self.apertures[apid]: + for geo_el in self.apertures[apid]['geometry']: + new_geo_el = dict() + if 'solid' in geo_el: + new_geo_el['solid'] = buffer_geom(geo_el['solid']) + if 'follow' in geo_el: + new_geo_el['follow'] = buffer_geom(geo_el['follow']) + if 'clear' in geo_el: + new_geo_el['clear'] = buffer_geom(geo_el['clear']) + new_geometry.append(new_geo_el) + + self.apertures[apid]['geometry'] = deepcopy(new_geometry) + + try: + if str(self.apertures[apid]['type']) == 'R' or str(self.apertures[apid]['type']) == 'O': + self.apertures[apid]['width'] += (distance * 2) + self.apertures[apid]['height'] += (distance * 2) + elif str(self.apertures[apid]['type']) == 'P': + self.apertures[apid]['diam'] += (distance * 2) + self.apertures[apid]['nVertices'] += (distance * 2) + except KeyError: + pass + + try: + if self.apertures[apid]['size'] is not None: + self.apertures[apid]['size'] = float(self.apertures[apid]['size'] + (distance * 2)) + except KeyError: + pass + except Exception as e: + log.debug('camlib.Gerber.buffer() Exception --> %s' % str(e)) + return 'fail' + + self.app.inform.emit('[success] %s' % _("Gerber Buffer done.")) + self.app.proc_container.new_text = '' + def parse_gerber_number(strnumber, int_digits, frac_digits, zeros): """ diff --git a/flatcamTools/ToolTransform.py b/flatcamTools/ToolTransform.py index 0b83c5c6..566372c2 100644 --- a/flatcamTools/ToolTransform.py +++ b/flatcamTools/ToolTransform.py @@ -27,6 +27,7 @@ class ToolTransform(FlatCAMTool): scaleName = _("Scale") flipName = _("Mirror (Flip)") offsetName = _("Offset") + bufferName = _("Buffer") def __init__(self, app): FlatCAMTool.__init__(self, app) @@ -255,11 +256,11 @@ class ToolTransform(FlatCAMTool): grid0.addWidget(self.offy_entry, 14, 1) grid0.addWidget(self.offy_button, 14, 2) - grid0.addWidget(QtWidgets.QLabel('')) + grid0.addWidget(QtWidgets.QLabel(''), 15, 0, 1, 3) # ## Flip Title flip_title_label = QtWidgets.QLabel("%s" % self.flipName) - self.transform_lay.addWidget(flip_title_label) + grid0.addWidget(flip_title_label, 16, 0, 1, 3) self.flipx_button = FCButton() self.flipx_button.set_value(_("Flip on X")) @@ -274,7 +275,7 @@ class ToolTransform(FlatCAMTool): ) hlay0 = QtWidgets.QHBoxLayout() - self.transform_lay.addLayout(hlay0) + grid0.addLayout(hlay0, 17, 0, 1, 3) hlay0.addWidget(self.flipx_button) hlay0.addWidget(self.flipy_button) @@ -293,7 +294,7 @@ class ToolTransform(FlatCAMTool): "Or enter the coords in format (x, y) in the\n" "Point Entry field and click Flip on X(Y)")) - self.transform_lay.addWidget(self.flip_ref_cb) + grid0.addWidget(self.flip_ref_cb, 18, 0, 1, 3) self.flip_ref_label = QtWidgets.QLabel('%s:' % _("Ref. Point")) self.flip_ref_label.setToolTip( @@ -315,12 +316,60 @@ class ToolTransform(FlatCAMTool): self.ois_flip = OptionalInputSection(self.flip_ref_cb, [self.flip_ref_entry, self.flip_ref_button], logic=True) hlay1 = QtWidgets.QHBoxLayout() - self.transform_lay.addLayout(hlay1) + grid0.addLayout(hlay1, 19, 0, 1, 3) hlay1.addWidget(self.flip_ref_label) hlay1.addWidget(self.flip_ref_entry) - self.transform_lay.addWidget(self.flip_ref_button) + grid0.addWidget(self.flip_ref_button, 20, 0, 1, 3) + + grid0.addWidget(QtWidgets.QLabel(''), 21, 0, 1, 3) + + # ## Buffer Title + buffer_title_label = QtWidgets.QLabel("%s" % self.bufferName) + grid0.addWidget(buffer_title_label, 22, 0, 1, 3) + + self.buffer_label = QtWidgets.QLabel('%s:' % _("Distance")) + self.buffer_label.setToolTip( + _("A positive value will create the effect of dilation,\n" + "while a negative value will create the effect of erosion.\n" + "Each geometry element of the object will be increased\n" + "or decreased with the 'distance'.") + ) + + self.buffer_entry = FCDoubleSpinner() + self.buffer_entry.set_precision(self.decimals) + self.buffer_entry.setSingleStep(0.1) + self.buffer_entry.setWrapping(True) + self.buffer_entry.set_range(-9999.9999, 9999.9999) + + # self.rotate_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) + + self.buffer_button = FCButton() + self.buffer_button.set_value(_("Buffer")) + self.buffer_button.setToolTip( + _("Create the buffer effect on each geometry,\n" + "element from the selected object.") + ) + self.buffer_button.setMinimumWidth(90) + + grid0.addWidget(self.buffer_label, 23, 0) + grid0.addWidget(self.buffer_entry, 23, 1) + grid0.addWidget(self.buffer_button, 23, 2) + + self.buffer_rounded_cb = FCCheckBox() + self.buffer_rounded_cb.setText('%s' % _("Rounded")) + self.buffer_rounded_cb.setToolTip( + _("If checked then the buffer will surround the buffered shape,\n" + "every corner will be rounded.\n" + "If not checked then the buffer will follow the exact geometry\n" + "of the buffered shape.") + ) + + grid0.addWidget(self.buffer_rounded_cb, 24, 0, 1, 3) + + grid0.addWidget(QtWidgets.QLabel(''), 25, 0, 1, 3) + self.transform_lay.addStretch() # ## Signals @@ -334,14 +383,16 @@ class ToolTransform(FlatCAMTool): self.flipx_button.clicked.connect(self.on_flipx) self.flipy_button.clicked.connect(self.on_flipy) self.flip_ref_button.clicked.connect(self.on_flip_add_coords) + self.buffer_button.clicked.connect(self.on_buffer) - self.rotate_entry.returnPressed.connect(self.on_rotate) - self.skewx_entry.returnPressed.connect(self.on_skewx) - self.skewy_entry.returnPressed.connect(self.on_skewy) - self.scalex_entry.returnPressed.connect(self.on_scalex) - self.scaley_entry.returnPressed.connect(self.on_scaley) - self.offx_entry.returnPressed.connect(self.on_offx) - self.offy_entry.returnPressed.connect(self.on_offy) + # self.rotate_entry.returnPressed.connect(self.on_rotate) + # self.skewx_entry.returnPressed.connect(self.on_skewx) + # self.skewy_entry.returnPressed.connect(self.on_skewy) + # self.scalex_entry.returnPressed.connect(self.on_scalex) + # self.scaley_entry.returnPressed.connect(self.on_scaley) + # self.offx_entry.returnPressed.connect(self.on_offx) + # self.offy_entry.returnPressed.connect(self.on_offy) + # self.buffer_entry.returnPressed.connect(self.on_buffer) def run(self, toggle=True): self.app.report_usage("ToolTransform()") @@ -430,6 +481,16 @@ class ToolTransform(FlatCAMTool): else: self.flip_ref_entry.set_value((0, 0)) + if self.app.defaults["tools_transform_buffer_dis"]: + self.buffer_entry.set_value(self.app.defaults["tools_transform_buffer_dis"]) + else: + self.buffer_entry.set_value(0.0) + + if self.app.defaults["tools_transform_buffer_corner"]: + self.buffer_rounded_cb.set_value(self.app.defaults["tools_transform_buffer_corner"]) + else: + self.buffer_rounded_cb.set_value(True) + def on_rotate(self): value = float(self.rotate_entry.get_value()) if value == 0: @@ -511,8 +572,7 @@ class ToolTransform(FlatCAMTool): def on_offx(self): value = float(self.offx_entry.get_value()) if value == 0: - self.app.inform.emit('[WARNING_NOTCL] %s' % - _("Offset transformation can not be done for a value of 0.")) + self.app.inform.emit('[WARNING_NOTCL] %s' % _("Offset transformation can not be done for a value of 0.")) return axis = 'X' @@ -522,14 +582,20 @@ class ToolTransform(FlatCAMTool): def on_offy(self): value = float(self.offy_entry.get_value()) if value == 0: - self.app.inform.emit('[WARNING_NOTCL] %s' % - _("Offset transformation can not be done for a value of 0.")) + self.app.inform.emit('[WARNING_NOTCL] %s' % _("Offset transformation can not be done for a value of 0.")) return axis = 'Y' self.app.worker_task.emit({'fcn': self.on_offset, 'params': [axis, value]}) return + def on_buffer(self): + value = self.buffer_entry.get_value() + join = 1 if self.buffer_rounded_cb.get_value() else 2 + + self.app.worker_task.emit({'fcn': self.on_buffer_action, 'params': [value, join]}) + return + def on_rotate_action(self, num): obj_list = self.app.collection.get_selected() xminlist = [] @@ -808,4 +874,40 @@ class ToolTransform(FlatCAMTool): (_("Due of"), str(e), _("action was not executed."))) return + def on_buffer_action(self, value, join): + obj_list = self.app.collection.get_selected() + + if not obj_list: + self.app.inform.emit('[WARNING_NOTCL] %s' % _("No object selected. Please Select an object to buffer!")) + return + else: + with self.app.proc_container.new(_("Applying Buffer")): + try: + for sel_obj in obj_list: + if isinstance(sel_obj, FlatCAMCNCjob): + self.app.inform.emit(_("CNCJob objects can't be buffered.")) + elif sel_obj.kind.lower() == 'gerber': + sel_obj.buffer(value, join) + sel_obj.source_file = self.app.export_gerber(obj_name=sel_obj.options['name'], + filename=None, local_use=sel_obj, + use_thread=False) + elif sel_obj.kind.lower() == 'excellon': + sel_obj.buffer(value, join) + sel_obj.source_file = self.app.export_excellon(obj_name=sel_obj.options['name'], + filename=None, local_use=sel_obj, + use_thread=False) + elif sel_obj.kind.lower() == 'geometry': + sel_obj.buffer(value, join) + + self.app.object_changed.emit(sel_obj) + sel_obj.plot() + + self.app.inform.emit('[success] %s...' % _('Buffer done')) + + except Exception as e: + self.app.log.debug("ToolTransform.on_buffer_action() --> %s" % str(e)) + self.app.inform.emit('[ERROR_NOTCL] %s %s, %s.' % + (_("Due of"), str(e), _("action was not executed."))) + return + # end of file From 9e8536ab9fc24243d3911dca7cdbc902b7a6ed86 Mon Sep 17 00:00:00 2001 From: Marius Date: Mon, 23 Dec 2019 23:32:32 +0200 Subject: [PATCH 5/5] =?UTF-8?q?-=20solved=20issue=20#355=20-=20when=20the?= =?UTF-8?q?=20tool=20diameter=20field=20in=20the=20Edit=20=E2=86=92=20Pref?= =?UTF-8?q?erences=20=E2=86=92=20Geometry=20=E2=86=92=20Geometry=20General?= =?UTF-8?q?=20=E2=86=92=20Tools=20=E2=86=92=20Tool=20dia=20is=20only=20one?= =?UTF-8?q?=20the=20app=20failed=20to=20read=20it?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- FlatCAMApp.py | 32 +++++++++++++++++--------------- FlatCAMObj.py | 21 ++++++++++++--------- README.md | 1 + 3 files changed, 30 insertions(+), 24 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 2ba31254..0e3da6c3 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -11531,26 +11531,28 @@ class App(QtCore.QObject): App.log.debug(" **************** Started PROEJCT loading... **************** ") for obj in d['objs']: - def obj_init(obj_inst, app_inst): + try: + def obj_init(obj_inst, app_inst): - obj_inst.from_dict(obj) + obj_inst.from_dict(obj) - App.log.debug("Recreating from opened project an %s object: %s" % - (obj['kind'].capitalize(), obj['options']['name'])) + App.log.debug("Recreating from opened project an %s object: %s" % + (obj['kind'].capitalize(), obj['options']['name'])) - # for some reason, setting ui_title does not work when this method is called from Tcl Shell - # it's because the TclCommand is run in another thread (it inherit TclCommandSignaled) - if cli is None: - self.set_ui_title(name="{} {}: {}".format(_("Loading Project ... restoring"), - obj['kind'].upper(), - obj['options']['name'] - ) - ) + # for some reason, setting ui_title does not work when this method is called from Tcl Shell + # it's because the TclCommand is run in another thread (it inherit TclCommandSignaled) + if cli is None: + self.set_ui_title(name="{} {}: {}".format(_("Loading Project ... restoring"), + obj['kind'].upper(), + obj['options']['name'] + ) + ) - self.new_object(obj['kind'], obj['options']['name'], obj_init, active=False, fit=False, plot=plot) + self.new_object(obj['kind'], obj['options']['name'], obj_init, active=False, fit=False, plot=plot) + except Exception as e: + print('App.open_project() --> ' + str(e)) - self.inform.emit('[success] %s: %s' % - (_("Project loaded from"), filename)) + self.inform.emit('[success] %s: %s' % (_("Project loaded from"), filename)) self.should_we_save = False self.file_opened.emit("project", filename) diff --git a/FlatCAMObj.py b/FlatCAMObj.py index 4ac69b82..4a88160e 100644 --- a/FlatCAMObj.py +++ b/FlatCAMObj.py @@ -3879,15 +3879,18 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): if def_key == opt_key: self.default_data[def_key] = deepcopy(opt_val) - try: - temp_tools = self.options["cnctooldia"].split(",") - tools_list = [ - float(eval(dia)) for dia in temp_tools if dia != '' - ] - except Exception as e: - log.error("At least one tool diameter needed. Verify in Edit -> Preferences -> Geometry General -> " - "Tool dia. %s" % str(e)) - return + if type(self.options["cnctooldia"]) == float: + tools_list = [self.options["cnctooldia"]] + else: + try: + temp_tools = self.options["cnctooldia"].split(",") + tools_list = [ + float(eval(dia)) for dia in temp_tools if dia != '' + ] + except Exception as e: + log.error("FlatCAMGeometry.set_ui() -> At least one tool diameter needed. " + "Verify in Edit -> Preferences -> Geometry General -> Tool dia. %s" % str(e)) + return self.tooluid += 1 diff --git a/README.md b/README.md index b4160bec..3239e6d2 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ CAD program, and create G-Code for Isolation routing. - ~~when the selection is changed in the Project Tree the selection shape on canvas is deleted~~ - if an object is selected on Project Tree and it does not have the selection shape drawn, first click on canvas over it will draw the selection shape - in Tool Transform added a new feature named 'Buffer'. For Geometry and Gerber objects will create (and replace) a geometry at a distance from the original geometry and for Excellon will adjust the Tool diameters +- solved issue #355 - when the tool diameter field in the Edit → Preferences → Geometry → Geometry General → Tools → Tool dia is only one the app failed to read it 22.12.2019