diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 75a77f76..0e3da6c3 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, @@ -8852,12 +8856,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) @@ -8904,20 +8910,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 +8944,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 +8962,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 +8977,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': @@ -11512,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) @@ -12365,7 +12386,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 +12421,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..4a88160e 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'] @@ -596,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" @@ -2196,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, @@ -2214,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" @@ -3542,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" @@ -3865,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 fadfdd62..3239e6d2 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,13 @@ 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 +- 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 +- 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 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/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)) 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