From d5a9e0bb5a2d8719e56a3ec4237949a0b577ef4e Mon Sep 17 00:00:00 2001 From: Marius Stanciu Date: Mon, 25 Nov 2019 16:52:37 +0200 Subject: [PATCH] - In Gerber isolation changed the UI - in Gerber isolation added the option to selectively isolate only certain polygons --- FlatCAMApp.py | 15 ++- FlatCAMObj.py | 222 +++++++++++++++++++++--------------- README.md | 5 + camlib.py | 109 ++++++++---------- flatcamGUI/ObjectUI.py | 132 ++++++++++++--------- flatcamGUI/PreferencesUI.py | 64 ++++++++--- 6 files changed, 325 insertions(+), 222 deletions(-) diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 77a89668..438dd48f 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -523,6 +523,7 @@ class App(QtCore.QObject): "gerber_isooverlap": 0.00393701, "gerber_milling_type": "cl", "gerber_combine_passes": False, + "gerber_iso_scope": 'all', "gerber_noncoppermargin": 0.00393701, "gerber_noncopperrounded": False, "gerber_bboxmargin": 0.00393701, @@ -537,6 +538,7 @@ class App(QtCore.QObject): "gerber_vtipdia": 0.1, "gerber_vtipangle": 30, "gerber_vcutz": -0.05, + "gerber_iso_type": "full", "gerber_buffering": "full", "gerber_simplification": False, "gerber_simp_tolerance": 0.0005, @@ -1077,6 +1079,7 @@ class App(QtCore.QObject): "gerber_isopasses": self.ui.gerber_defaults_form.gerber_opt_group.iso_width_entry, "gerber_isooverlap": self.ui.gerber_defaults_form.gerber_opt_group.iso_overlap_entry, "gerber_combine_passes": self.ui.gerber_defaults_form.gerber_opt_group.combine_passes_cb, + "gerber_iso_scope": self.ui.gerber_defaults_form.gerber_opt_group.iso_scope_radio, "gerber_milling_type": self.ui.gerber_defaults_form.gerber_opt_group.milling_type_radio, "gerber_noncoppermargin": self.ui.gerber_defaults_form.gerber_opt_group.noncopper_margin_entry, "gerber_noncopperrounded": self.ui.gerber_defaults_form.gerber_opt_group.noncopper_rounded_cb, @@ -1092,6 +1095,7 @@ class App(QtCore.QObject): "gerber_vtipdia": self.ui.gerber_defaults_form.gerber_adv_opt_group.tipdia_spinner, "gerber_vtipangle": self.ui.gerber_defaults_form.gerber_adv_opt_group.tipangle_spinner, "gerber_vcutz": self.ui.gerber_defaults_form.gerber_adv_opt_group.cutz_spinner, + "gerber_iso_type": self.ui.gerber_defaults_form.gerber_adv_opt_group.iso_type_radio, "gerber_buffering": self.ui.gerber_defaults_form.gerber_adv_opt_group.buffering_radio, "gerber_simplification": self.ui.gerber_defaults_form.gerber_adv_opt_group.simplify_cb, @@ -2424,6 +2428,9 @@ class App(QtCore.QObject): # decide if we have a double click or single click self.doubleclick = False + # store here the is_dragging value + self.event_is_dragging = False + # variable to store if a command is active (then the var is not None) and which one it is self.command_active = None # variable to store the status of moving selection action @@ -8337,7 +8344,7 @@ class App(QtCore.QObject): pan_button = 2 else: pan_button = 3 - event_is_dragging = event.is_dragging + self.event_is_dragging = event.is_dragging else: event_pos = (event.xdata, event.ydata) # Matplotlib has the middle and right buttons mapped in reverse compared with VisPy @@ -8345,7 +8352,7 @@ class App(QtCore.QObject): pan_button = 3 else: pan_button = 2 - event_is_dragging = self.plotcanvas.is_dragging + self.event_is_dragging = self.plotcanvas.is_dragging # So it can receive key presses self.plotcanvas.native.setFocus() @@ -8355,7 +8362,7 @@ class App(QtCore.QObject): if not origin_click: # if the RMB is clicked and mouse is moving over plot then 'panning_action' is True - if event.button == pan_button and event_is_dragging == 1: + if event.button == pan_button and self.event_is_dragging == 1: self.ui.popMenu.mouse_is_panning = True return @@ -8383,7 +8390,7 @@ class App(QtCore.QObject): self.mouse = [pos[0], pos[1]] # if the mouse is moved and the LMB is clicked then the action is a selection - if event_is_dragging == 1 and event.button == 1: + if self.event_is_dragging == 1 and event.button == 1: self.delete_selection_shape() if dx < 0: self.draw_moving_selection_shape(self.pos, pos, color=self.defaults['global_alt_sel_line'], diff --git a/FlatCAMObj.py b/FlatCAMObj.py index 8f20afd1..c1775dad 100644 --- a/FlatCAMObj.py +++ b/FlatCAMObj.py @@ -597,7 +597,9 @@ class FlatCAMGerber(FlatCAMObj, Gerber): "bboxmargin": 0.0, "bboxrounded": False, "aperture_display": False, - "follow": False + "follow": False, + "iso_scope": 'all', + "iso_type": 'full' }) # type of isolation: 0 = exteriors, 1 = interiors, 2 = complete isolation (both interiors and exteriors) @@ -618,6 +620,12 @@ class FlatCAMGerber(FlatCAMObj, Gerber): # Number of decimals to be used by tools in this class self.decimals = 4 + # Mouse events + self.mr = self.app.mr + + # list to store the polygons selected for isolation + self.poly_list = list() + # Attributes to be included in serialization # Always append to it because it carries contents # from predecessors. @@ -662,7 +670,9 @@ class FlatCAMGerber(FlatCAMObj, Gerber): "bboxmargin": self.ui.bbmargin_entry, "bboxrounded": self.ui.bbrounded_cb, "aperture_display": self.ui.aperture_table_visibility_cb, - "follow": self.ui.follow_cb + "follow": self.ui.follow_cb, + "iso_scope": self.ui.iso_scope_radio, + "iso_type": self.ui.iso_type_radio }) # Fill form fields only on object create @@ -672,8 +682,6 @@ class FlatCAMGerber(FlatCAMObj, Gerber): self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click) self.ui.solid_cb.stateChanged.connect(self.on_solid_cb_click) self.ui.multicolored_cb.stateChanged.connect(self.on_multicolored_cb_click) - self.ui.generate_ext_iso_button.clicked.connect(self.on_ext_iso_button_click) - self.ui.generate_int_iso_button.clicked.connect(self.on_int_iso_button_click) self.ui.generate_iso_button.clicked.connect(self.on_iso_button_click) self.ui.generate_ncc_button.clicked.connect(self.app.ncclear_tool.run) self.ui.generate_cutout_button.clicked.connect(self.app.cutout_tool.run) @@ -710,8 +718,8 @@ class FlatCAMGerber(FlatCAMObj, Gerber): self.ui.aperture_table_visibility_cb.hide() self.ui.milling_type_label.hide() self.ui.milling_type_radio.hide() - self.ui.generate_ext_iso_button.hide() - self.ui.generate_int_iso_button.hide() + self.ui.iso_type_radio.hide() + self.ui.follow_cb.hide() self.ui.except_cb.setChecked(False) self.ui.except_cb.hide() @@ -978,42 +986,16 @@ class FlatCAMGerber(FlatCAMObj, Gerber): self.app.new_object("geometry", name, geo_init) - def on_ext_iso_button_click(self, *args): - obj = self.app.collection.get_active() - - def worker_task(obj, app_obj): - with self.app.proc_container.new(_("Isolating...")): - if self.ui.follow_cb.get_value() is True: - obj.follow_geo() - # in the end toggle the visibility of the origin object so we can see the generated Geometry - obj.ui.plot_cb.toggle() - else: - app_obj.report_usage("gerber_on_iso_button") - self.read_form() - self.isolate(iso_type=0) - - self.app.worker_task.emit({'fcn': worker_task, 'params': [obj, self.app]}) - - def on_int_iso_button_click(self, *args): - obj = self.app.collection.get_active() - - def worker_task(obj, app_obj): - with self.app.proc_container.new(_("Isolating...")): - if self.ui.follow_cb.get_value() is True: - obj.follow_geo() - # in the end toggle the visibility of the origin object so we can see the generated Geometry - obj.ui.plot_cb.toggle() - else: - app_obj.report_usage("gerber_on_iso_button") - self.read_form() - self.isolate(iso_type=1) - - self.app.worker_task.emit({'fcn': worker_task, 'params': [obj, self.app]}) - def on_iso_button_click(self, *args): obj = self.app.collection.get_active() + self.iso_type = 2 + if self.ui.iso_type_radio.get_value() == 'ext': + self.iso_type = 0 + if self.ui.iso_type_radio.get_value() == 'int': + self.iso_type = 1 + def worker_task(obj, app_obj): with self.app.proc_container.new(_("Isolating...")): if self.ui.follow_cb.get_value() is True: @@ -1023,7 +1005,9 @@ class FlatCAMGerber(FlatCAMObj, Gerber): else: app_obj.report_usage("gerber_on_iso_button") self.read_form() - self.isolate() + + iso_scope = 'all' if self.ui.iso_scope_radio == 'all' else 'single' + self.isolate_handler(iso_type=self.iso_type, iso_scope=iso_scope) self.app.worker_task.emit({'fcn': worker_task, 'params': [obj, self.app]}) @@ -1053,18 +1037,96 @@ class FlatCAMGerber(FlatCAMObj, Gerber): except Exception as e: return "Operation failed: %s" % str(e) - def isolate(self, iso_type=None, dia=None, passes=None, overlap=None, outname=None, combine=None, + def isolate_handler(self, iso_type, iso_scope): + + if iso_scope == 'all': + self.isolate(iso_type=iso_type) + else: + # disengage the grid snapping since it will be hard to find the drills on grid + if self.app.ui.grid_snap_btn.isChecked(): + self.grid_status_memory = True + self.app.ui.grid_snap_btn.trigger() + else: + self.grid_status_memory = False + + self.mr = self.app.plotcanvas.graph_event_connect('mouse_release', self.on_mouse_click_release) + + if self.app.is_legacy is False: + self.app.plotcanvas.graph_event_disconnect('mouse_release', self.app.on_mouse_click_release_over_plot) + else: + self.app.plotcanvas.graph_event_disconnect(self.app.mr) + + self.app.inform.emit('[WARNING_NOTCL] %s' % _("Click on polygon to isolate it.")) + + def on_mouse_click_release(self, event): + if self.app.is_legacy is False: + event_pos = event.pos + event_is_dragging = event.is_dragging + right_button = 2 + else: + event_pos = (event.xdata, event.ydata) + event_is_dragging = self.app.plotcanvas.is_dragging + right_button = 3 + + try: + x = float(event_pos[0]) + y = float(event_pos[1]) + except TypeError: + return + + event_pos = (x, y) + curr_pos = self.app.plotcanvas.translate_coords(event_pos) + + if event.button == 1: + clicked_poly = self.find_polygon(point=(curr_pos[0], curr_pos[1])) + + if clicked_poly: + self.poly_list.append(clicked_poly) + self.app.inform.emit( + '%s: %d. %s' % (_("Added polygon"), + int(len(self.poly_list)), + _("Click to start adding next polygon or right click to start isolation.")) + ) + else: + self.app.inform.emit(_("No polygon detected under click position. Try again.")) + + elif event.button == right_button and self.app.event_is_dragging is False: + # restore the Grid snapping if it was active before + if self.grid_status_memory is True: + self.app.ui.grid_snap_btn.trigger() + + if self.app.is_legacy is False: + self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_click_release) + else: + self.app.plotcanvas.graph_event_disconnect(self.mr) + + self.app.mr = self.app.plotcanvas.graph_event_connect('mouse_release', + self.app.on_mouse_click_release_over_plot) + + self.isolate(iso_type=self.iso_type, geometry=self.poly_list) + + def isolate(self, iso_type=None, geometry=None, dia=None, passes=None, overlap=None, outname=None, combine=None, milling_type=None, follow=None, plot=True): """ Creates an isolation routing geometry object in the project. :param iso_type: type of isolation to be done: 0 = exteriors, 1 = interiors and 2 = both + :param iso_scope: whether to isolate all polygons or single polygpns: 'all' = all, 'single' = one by one, single :param dia: Tool diameter :param passes: Number of tool widths to cut :param overlap: Overlap between passes in fraction of tool diameter :param outname: Base name of the output object :return: None """ + + if geometry is None: + if follow: + work_geo = self.follow_geometry + else: + work_geo = self.solid_geometry + else: + work_geo = geometry + if dia is None: dia = float(self.options["isotooldia"]) if passes is None: @@ -1075,28 +1137,33 @@ class FlatCAMGerber(FlatCAMObj, Gerber): combine = self.options["combine_passes"] else: combine = bool(combine) + if milling_type is None: milling_type = self.options["milling_type"] if iso_type is None: - self.iso_type = 2 + iso_t = 2 else: - self.iso_type = iso_type + iso_t = iso_type base_name = self.options["name"] - def generate_envelope(offset, invert, envelope_iso_type=2, follow=None, passes=0): + def generate_envelope(offset, invert, geometry=None, env_iso_type=2, follow=None, nr_passes=0): # isolation_geometry produces an envelope that is going on the left of the geometry # (the copper features). To leave the least amount of burrs on the features # the tool needs to travel on the right side of the features (this is called conventional milling) # the first pass is the one cutting all of the features, so it needs to be reversed # the other passes overlap preceding ones and cut the left over copper. It is better for them # to cut on the right side of the left over copper i.e on the left side of the features. - try: - geom = self.isolation_geometry(offset, iso_type=envelope_iso_type, follow=follow, passes=passes) - except Exception as e: - log.debug('FlatCAMGerber.isolate().generate_envelope() --> %s' % str(e)) - return 'fail' + + if follow: + geom = self.isolation_geometry(offset, geometry=geometry, follow=follow) + else: + try: + geom = self.isolation_geometry(offset, geometry=geometry, iso_type=env_iso_type, passes=nr_passes) + except Exception as e: + log.debug('FlatCAMGerber.isolate().generate_envelope() --> %s' % str(e)) + return 'fail' if invert: try: @@ -1121,24 +1188,6 @@ class FlatCAMGerber(FlatCAMObj, Gerber): return 'fail' return geom - # if invert: - # try: - # if type(geom) is MultiPolygon: - # pl = [] - # for p in geom: - # if p is not None: - # pl.append(Polygon(p.exterior.coords[::-1], p.interiors)) - # geom = MultiPolygon(pl) - # elif type(geom) is Polygon and geom is not None: - # geom = Polygon(geom.exterior.coords[::-1], geom.interiors) - # else: - # log.debug("FlatCAMGerber.isolate().generate_envelope() Error --> Unexpected Geometry %s" % - # type(geom)) - # except Exception as e: - # log.debug("FlatCAMGerber.isolate().generate_envelope() Error --> %s" % str(e)) - # return 'fail' - # return geom - # if float(self.options["isotooldia"]) < 0: # self.options["isotooldia"] = -self.options["isotooldia"] @@ -1210,30 +1259,26 @@ class FlatCAMGerber(FlatCAMObj, Gerber): iso_offset = dia * ((2 * i + 1) / 2.0) - (i * overlap * dia) # if milling type is climb then the move is counter-clockwise around features - if milling_type == 'cl': - # geom = generate_envelope (offset, i == 0) - geom = generate_envelope(iso_offset, 1, envelope_iso_type=self.iso_type, follow=follow, - passes=i) - else: - geom = generate_envelope(iso_offset, 0, envelope_iso_type=self.iso_type, follow=follow, - passes=i) + mill_t = 1 if milling_type == 'cl' else 0 + geom = generate_envelope(iso_offset, mill_t, geometry=work_geo, env_iso_type=iso_t, follow=follow, + nr_passes=i) + if geom == 'fail': - app_obj.inform.emit('[ERROR_NOTCL] %s' % - _("Isolation geometry could not be generated.")) + app_obj.inform.emit('[ERROR_NOTCL] %s' % _("Isolation geometry could not be generated.")) return 'fail' geo_obj.solid_geometry.append(geom) # transfer the Cut Z and Vtip and VAngle values in case that we use the V-Shape tool in Gerber UI - if self.ui.tool_type_radio.get_value() == 'circular': - new_cutz = self.app.defaults['geometry_cutz'] - new_vtipdia = self.app.defaults['geometry_vtipdia'] - new_vtipangle = self.app.defaults['geometry_vtipangle'] - tool_type = 'C1' - else: + if self.ui.tool_type_radio.get_value() == 'v': new_cutz = self.ui.cutz_spinner.get_value() new_vtipdia = self.ui.tipdia_spinner.get_value() new_vtipangle = self.ui.tipangle_spinner.get_value() tool_type = 'V' + else: + new_cutz = self.app.defaults['geometry_cutz'] + new_vtipdia = self.app.defaults['geometry_vtipdia'] + new_vtipangle = self.app.defaults['geometry_vtipangle'] + tool_type = 'C1' # store here the default data for Geometry Data default_data = {} @@ -1280,7 +1325,8 @@ class FlatCAMGerber(FlatCAMObj, Gerber): # proceed with object creation, if there are empty and the number of them is the length # of the list then we have an empty solid_geometry which should raise a Custom Exception empty_cnt = 0 - if not isinstance(geo_obj.solid_geometry, list): + if not isinstance(geo_obj.solid_geometry, list) and \ + not isinstance(geo_obj.solid_geometry, MultiPolygon): geo_obj.solid_geometry = [geo_obj.solid_geometry] for g in geo_obj.solid_geometry: @@ -1338,13 +1384,11 @@ class FlatCAMGerber(FlatCAMObj, Gerber): geo_obj.options["cnctooldia"] = str(self.options["isotooldia"]) # if milling type is climb then the move is counter-clockwise around features - if milling_type == 'cl': - # geo_obj.solid_geometry = generate_envelope(offset, i == 0) - geom = generate_envelope(offset, 1, envelope_iso_type=self.iso_type, follow=follow, - passes=i) - else: - geom = generate_envelope(offset, 0, envelope_iso_type=self.iso_type, follow=follow, - passes=i) + mill_t = 1 if milling_type == 'cl' else 0 + mill_t = 1 if milling_type == 'cl' else 0 + geom = generate_envelope(offset, mill_t, geometry=work_geo, env_iso_type=iso_t, follow=follow, + nr_passes=i) + if geom == 'fail': app_obj.inform.emit('[ERROR_NOTCL] %s' % _("Isolation geometry could not be generated.")) diff --git a/README.md b/README.md index ff787596..586f47db 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,11 @@ CAD program, and create G-Code for Isolation routing. ================================================= +25.11.2019 + +- In Gerber isolation changed the UI +- in Gerber isolation added the option to selectively isolate only certain polygons + 23.11.2019 - in Tool Fiducials added a new fiducial type: chess pattern diff --git a/camlib.py b/camlib.py index 9212d5cd..6d2501ce 100644 --- a/camlib.py +++ b/camlib.py @@ -897,7 +897,7 @@ class Geometry(object): # # return self.flat_geometry, self.flat_geometry_rtree - def isolation_geometry(self, offset, iso_type=2, corner=None, follow=None, passes=0): + def isolation_geometry(self, offset, geometry=None, iso_type=2, corner=None, follow=None, passes=0): """ Creates contours around geometry at a given offset distance. @@ -916,73 +916,58 @@ class Geometry(object): # graceful abort requested by the user raise FlatCAMApp.GracefulException - geo_iso = [] - if offset == 0: - if follow: - geo_iso = self.follow_geometry - else: - geo_iso = self.solid_geometry + geo_iso = list() + + if follow: + return geometry + + if geometry: + working_geo = geometry else: - if follow: - geo_iso = self.follow_geometry + working_geo = self.solid_geometry + + try: + geo_len = len(working_geo) + except TypeError: + geo_len = 1 + + old_disp_number = 0 + pol_nr = 0 + # yet, it can be done by issuing an unary_union in the end, thus getting rid of the overlapping geo + try: + for pol in working_geo: + if self.app.abort_flag: + # graceful abort requested by the user + raise FlatCAMApp.GracefulException + if offset == 0: + geo_iso.append(pol) + else: + corner_type = 1 if corner is None else corner + geo_iso.append(pol.buffer(offset, int(int(self.geo_steps_per_circle) / 4), join_style=corner_type)) + pol_nr += 1 + disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 100])) + + if old_disp_number < disp_number <= 100: + self.app.proc_container.update_view_text(' %s %d: %d%%' % + (_("Pass"), int(passes + 1), int(disp_number))) + old_disp_number = disp_number + except TypeError: + # taking care of the case when the self.solid_geometry is just a single Polygon, not a list or a + # MultiPolygon (not an iterable) + if offset == 0: + geo_iso.append(working_geo) else: - # if isinstance(self.solid_geometry, list): - # temp_geo = cascaded_union(self.solid_geometry) - # else: - # temp_geo = self.solid_geometry + corner_type = 1 if corner is None else corner + geo_iso.append(working_geo.buffer(offset, int(int(self.geo_steps_per_circle) / 4), + join_style=corner_type)) - # Remember: do not make a buffer for each element in the solid_geometry because it will cut into - # other copper features - # if corner is None: - # geo_iso = temp_geo.buffer(offset, int(int(self.geo_steps_per_circle) / 4)) - # else: - # geo_iso = temp_geo.buffer(offset, int(int(self.geo_steps_per_circle) / 4), - # join_style=corner) - - # variables to display the percentage of work done - geo_len = 0 - try: - for pol in self.solid_geometry: - geo_len += 1 - except TypeError: - geo_len = 1 - disp_number = 0 - old_disp_number = 0 - pol_nr = 0 - # yet, it can be done by issuing an unary_union in the end, thus getting rid of the overlapping geo - try: - for pol in self.solid_geometry: - if self.app.abort_flag: - # graceful abort requested by the user - raise FlatCAMApp.GracefulException - if corner is None: - geo_iso.append(pol.buffer(offset, int(int(self.geo_steps_per_circle) / 4))) - else: - geo_iso.append(pol.buffer(offset, int(int(self.geo_steps_per_circle) / 4)), - join_style=corner) - pol_nr += 1 - disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 100])) - - if old_disp_number < disp_number <= 100: - self.app.proc_container.update_view_text(' %s %d: %d%%' % - (_("Pass"), int(passes + 1), int(disp_number))) - old_disp_number = disp_number - except TypeError: - # taking care of the case when the self.solid_geometry is just a single Polygon, not a list or a - # MultiPolygon (not an iterable) - if corner is None: - geo_iso.append(self.solid_geometry.buffer(offset, int(int(self.geo_steps_per_circle) / 4))) - else: - geo_iso.append(self.solid_geometry.buffer(offset, int(int(self.geo_steps_per_circle) / 4)), - join_style=corner) - self.app.proc_container.update_view_text(' %s' % _("Buffering")) - geo_iso = unary_union(geo_iso) + self.app.proc_container.update_view_text(' %s' % _("Buffering")) + geo_iso = unary_union(geo_iso) self.app.proc_container.update_view_text('') # end of replaced block - if follow: - return geo_iso - elif iso_type == 2: + + if iso_type == 2: return geo_iso elif iso_type == 0: self.app.proc_container.update_view_text(' %s' % _("Get Exteriors")) diff --git a/flatcamGUI/ObjectUI.py b/flatcamGUI/ObjectUI.py index 4db16297..23e62676 100644 --- a/flatcamGUI/ObjectUI.py +++ b/flatcamGUI/ObjectUI.py @@ -281,6 +281,7 @@ class GerberObjectUI(ObjectUI): self.custom_box.addLayout(grid1) grid1.setColumnStretch(0, 0) grid1.setColumnStretch(1, 1) + grid1.setColumnStretch(2, 1) # Tool Type self.tool_type_label = QtWidgets.QLabel('%s:' % _('Tool Type')) @@ -290,8 +291,8 @@ class GerberObjectUI(ObjectUI): "When the 'V-shape' is selected then the tool\n" "diameter will depend on the chosen cut depth.") ) - self.tool_type_radio = RadioSet([{'label': 'Circular', 'value': 'circular'}, - {'label': 'V-Shape', 'value': 'v'}]) + self.tool_type_radio = RadioSet([{'label': _('Circular'), 'value': 'circular'}, + {'label': _('V-Shape'), 'value': 'v'}]) grid1.addWidget(self.tool_type_label, 0, 0) grid1.addWidget(self.tool_type_radio, 0, 1, 1, 2) @@ -396,7 +397,7 @@ class GerberObjectUI(ObjectUI): grid1.addWidget(self.milling_type_radio, 7, 1, 1, 2) # combine all passes CB - self.combine_passes_cb = FCCheckBox(label=_('Combine Passes')) + self.combine_passes_cb = FCCheckBox(label=_('Combine')) self.combine_passes_cb.setToolTip( _("Combine all passes into one object") ) @@ -406,15 +407,15 @@ class GerberObjectUI(ObjectUI): self.follow_cb.setToolTip(_("Generate a 'Follow' geometry.\n" "This means that it will cut through\n" "the middle of the trace.")) + grid1.addWidget(self.combine_passes_cb, 8, 0) # avoid an area from isolation self.except_cb = FCCheckBox(label=_('Except')) + grid1.addWidget(self.follow_cb, 8, 1) + self.except_cb.setToolTip(_("When the isolation geometry is generated,\n" "by checking this, the area of the object bellow\n" "will be subtracted from the isolation geometry.")) - - grid1.addWidget(self.combine_passes_cb, 8, 0) - grid1.addWidget(self.follow_cb, 8, 1) grid1.addWidget(self.except_cb, 8, 2) # ## Form Layout @@ -454,8 +455,50 @@ class GerberObjectUI(ObjectUI): form_layout.addRow(self.obj_label, self.obj_combo) - self.gen_iso_label = QtWidgets.QLabel("%s" % _("Generate Isolation Geometry")) - self.gen_iso_label.setToolTip( + # ---------------------------------------------- # + # --------- Isolation scope -------------------- # + # ---------------------------------------------- # + self.iso_scope_label = QtWidgets.QLabel('%s:' % _('Scope')) + self.iso_scope_label.setToolTip( + _("Isolation scope. Choose what to isolate:\n" + "- 'All' -> Isolate all the polygons in the object\n" + "- 'Single' -> Isolate a single polygon.") + ) + self.iso_scope_radio = RadioSet([{'label': _('All'), 'value': 'all'}, + {'label': _('Single'), 'value': 'single'}]) + + grid1.addWidget(self.iso_scope_label, 10, 0) + grid1.addWidget(self.iso_scope_radio, 10, 1, 1, 2) + + # ---------------------------------------------- # + # --------- Isolation type -------------------- # + # ---------------------------------------------- # + self.iso_type_label = QtWidgets.QLabel('%s:' % _('Isolation Type')) + self.iso_type_label.setToolTip( + _("Choose how the isolation will be executed:\n" + "- 'Full' -> complete isolation of polygons\n" + "- 'Ext' -> will isolate only on the outside\n" + "- 'Int' -> will isolate only on the inside\n" + "'Exterior' isolation is almost always possible\n" + "(with the right tool) but 'Interior'\n" + "isolation can be done only when there is an opening\n" + "inside of the polygon (e.g polygon is a 'doughnut' shape).") + ) + self.iso_type_radio = RadioSet([{'label': _('Full'), 'value': 'full'}, + {'label': _('Ext'), 'value': 'ext'}, + {'label': _('Int'), 'value': 'int'}]) + + grid1.addWidget(self.iso_type_label, 11, 0) + grid1.addWidget(self.iso_type_radio, 11, 1, 1, 2) + + self.generate_iso_button = QtWidgets.QPushButton("%s" % _("Generate Isolation Geometry")) + self.generate_iso_button.setStyleSheet(""" + QPushButton + { + font-weight: bold; + } + """) + self.generate_iso_button.setToolTip( _("Create a Geometry object with toolpaths to cut \n" "isolation outside, inside or on both sides of the\n" "object. For a Gerber object outside means outside\n" @@ -466,7 +509,7 @@ class GerberObjectUI(ObjectUI): "inside the actual Gerber feature, use a negative tool\n" "diameter above.") ) - grid1.addWidget(self.gen_iso_label, 10, 0, 1, 3) + grid1.addWidget(self.generate_iso_button, 12, 0, 1, 3) self.create_buffer_button = QtWidgets.QPushButton(_('Buffer Solid Geometry')) self.create_buffer_button.setToolTip( @@ -475,48 +518,15 @@ class GerberObjectUI(ObjectUI): "Clicking this will create the buffered geometry\n" "required for isolation.") ) - grid1.addWidget(self.create_buffer_button, 11, 0, 1, 3) - - self.generate_iso_button = QtWidgets.QPushButton(_('FULL Geo')) - self.generate_iso_button.setToolTip( - _("Create the Geometry Object\n" - "for isolation routing. It contains both\n" - "the interiors and exteriors geometry.") - ) - grid1.addWidget(self.generate_iso_button, 12, 0) - - hlay_1 = QtWidgets.QHBoxLayout() - grid1.addLayout(hlay_1, 12, 1, 1, 2) - - self.generate_ext_iso_button = QtWidgets.QPushButton(_('Ext Geo')) - self.generate_ext_iso_button.setToolTip( - _("Create the Geometry Object\n" - "for isolation routing containing\n" - "only the exteriors geometry.") - ) - # self.generate_ext_iso_button.setMinimumWidth(100) - hlay_1.addWidget(self.generate_ext_iso_button) - - self.generate_int_iso_button = QtWidgets.QPushButton(_('Int Geo')) - self.generate_int_iso_button.setToolTip( - _("Create the Geometry Object\n" - "for isolation routing containing\n" - "only the interiors geometry.") - ) - # self.generate_ext_iso_button.setMinimumWidth(90) - hlay_1.addWidget(self.generate_int_iso_button) + grid1.addWidget(self.create_buffer_button, 13, 0, 1, 2) self.ohis_iso = OptionalHideInputSection( self.except_cb, [self.type_obj_combo, self.type_obj_combo_label, self.obj_combo, self.obj_label], logic=True ) - # when the follow checkbox is checked then the exteriors and interiors isolation generation buttons - # are disabled as is doesn't make sense to have them enabled due of the nature of "follow" - self.ois_iso = OptionalInputSection(self.follow_cb, - [self.generate_int_iso_button, self.generate_ext_iso_button], logic=False) - grid1.addWidget(QtWidgets.QLabel(''), 13, 0) + grid1.addWidget(QtWidgets.QLabel(''), 14, 0) # ########################################### # ########## NEW GRID ####################### @@ -562,6 +572,11 @@ class GerberObjectUI(ObjectUI): grid2.addWidget(self.board_cutout_label, 2, 0) grid2.addWidget(self.generate_cutout_button, 2, 1) + separator_line = QtWidgets.QFrame() + separator_line.setFrameShape(QtWidgets.QFrame.HLine) + separator_line.setFrameShadow(QtWidgets.QFrame.Sunken) + grid2.addWidget(separator_line, 3, 0, 1, 2) + # ## Non-copper regions self.noncopper_label = QtWidgets.QLabel("%s" % _("Non-copper regions")) self.noncopper_label.setToolTip( @@ -572,7 +587,7 @@ class GerberObjectUI(ObjectUI): "copper from a specified region.") ) - grid2.addWidget(self.noncopper_label, 3, 0, 1, 2) + grid2.addWidget(self.noncopper_label, 4, 0, 1, 2) # Margin bmlabel = QtWidgets.QLabel('%s:' % _('Boundary Margin')) @@ -588,8 +603,8 @@ class GerberObjectUI(ObjectUI): self.noncopper_margin_entry.set_precision(self.decimals) self.noncopper_margin_entry.setSingleStep(0.1) - grid2.addWidget(bmlabel, 4, 0) - grid2.addWidget(self.noncopper_margin_entry, 4, 1) + grid2.addWidget(bmlabel, 5, 0) + grid2.addWidget(self.noncopper_margin_entry, 5, 1) # Rounded corners self.noncopper_rounded_cb = FCCheckBox(label=_("Rounded Geo")) @@ -599,8 +614,13 @@ class GerberObjectUI(ObjectUI): self.noncopper_rounded_cb.setMinimumWidth(90) self.generate_noncopper_button = QtWidgets.QPushButton(_('Generate Geo')) - grid2.addWidget(self.noncopper_rounded_cb, 5, 0) - grid2.addWidget(self.generate_noncopper_button, 5, 1) + grid2.addWidget(self.noncopper_rounded_cb, 6, 0) + grid2.addWidget(self.generate_noncopper_button, 6, 1) + + separator_line1 = QtWidgets.QFrame() + separator_line1.setFrameShape(QtWidgets.QFrame.HLine) + separator_line1.setFrameShadow(QtWidgets.QFrame.Sunken) + grid2.addWidget(separator_line1, 7, 0, 1, 2) # ## Bounding box self.boundingbox_label = QtWidgets.QLabel('%s' % _('Bounding Box')) @@ -609,7 +629,7 @@ class GerberObjectUI(ObjectUI): "Square shape.") ) - grid2.addWidget(self.boundingbox_label, 6, 0, 1, 2) + grid2.addWidget(self.boundingbox_label, 8, 0, 1, 2) bbmargin = QtWidgets.QLabel('%s:' % _('Boundary Margin')) bbmargin.setToolTip( @@ -622,8 +642,8 @@ class GerberObjectUI(ObjectUI): self.bbmargin_entry.set_precision(self.decimals) self.bbmargin_entry.setSingleStep(0.1) - grid2.addWidget(bbmargin, 7, 0) - grid2.addWidget(self.bbmargin_entry, 7, 1) + grid2.addWidget(bbmargin, 9, 0) + grid2.addWidget(self.bbmargin_entry, 9, 1) self.bbrounded_cb = FCCheckBox(label=_("Rounded Geo")) self.bbrounded_cb.setToolTip( @@ -638,9 +658,13 @@ class GerberObjectUI(ObjectUI): self.generate_bb_button.setToolTip( _("Generate the Geometry object.") ) - grid2.addWidget(self.bbrounded_cb, 8, 0) - grid2.addWidget(self.generate_bb_button, 8, 1) + grid2.addWidget(self.bbrounded_cb, 10, 0) + grid2.addWidget(self.generate_bb_button, 10, 1) + separator_line2 = QtWidgets.QFrame() + separator_line2.setFrameShape(QtWidgets.QFrame.HLine) + separator_line2.setFrameShadow(QtWidgets.QFrame.Sunken) + grid2.addWidget(separator_line2, 11, 0, 1, 2) class ExcellonObjectUI(ObjectUI): """ diff --git a/flatcamGUI/PreferencesUI.py b/flatcamGUI/PreferencesUI.py index 2c704b80..defa40fc 100644 --- a/flatcamGUI/PreferencesUI.py +++ b/flatcamGUI/PreferencesUI.py @@ -1344,9 +1344,10 @@ class GerberOptPrefGroupUI(OptionsGroupUI): _("Width of the isolation gap in\n" "number (integer) of tool widths.") ) - grid0.addWidget(passlabel, 1, 0) self.iso_width_entry = FCSpinner() self.iso_width_entry.setRange(1, 999) + + grid0.addWidget(passlabel, 1, 0) grid0.addWidget(self.iso_width_entry, 1, 1) # Pass overlap @@ -1356,14 +1357,28 @@ class GerberOptPrefGroupUI(OptionsGroupUI): "Example:\n" "A value here of 0.25 means an overlap of 25%% from the tool diameter found above.") ) - grid0.addWidget(overlabel, 2, 0) self.iso_overlap_entry = FCDoubleSpinner() self.iso_overlap_entry.set_precision(3) self.iso_overlap_entry.setWrapping(True) self.iso_overlap_entry.setRange(0.000, 0.999) self.iso_overlap_entry.setSingleStep(0.1) + + grid0.addWidget(overlabel, 2, 0) grid0.addWidget(self.iso_overlap_entry, 2, 1) + # Isolation Scope + self.iso_scope_label = QtWidgets.QLabel('%s:' % _('Scope')) + self.iso_scope_label.setToolTip( + _("Isolation scope. Choose what to isolate:\n" + "- 'All' -> Isolate all the polygons in the object\n" + "- 'Single' -> Isolate a single polygon.") + ) + self.iso_scope_radio = RadioSet([{'label': _('All'), 'value': 'all'}, + {'label': _('Single'), 'value': 'single'}]) + + grid0.addWidget(self.iso_scope_label, 3, 0) + grid0.addWidget(self.iso_scope_radio, 3, 1, 1, 2) + # Milling Type milling_type_label = QtWidgets.QLabel('%s:' % _('Milling Type')) milling_type_label.setToolTip( @@ -1371,17 +1386,17 @@ class GerberOptPrefGroupUI(OptionsGroupUI): "- climb / best for precision milling and to reduce tool usage\n" "- conventional / useful when there is no backlash compensation") ) - grid0.addWidget(milling_type_label, 3, 0) + grid0.addWidget(milling_type_label, 4, 0) self.milling_type_radio = RadioSet([{'label': _('Climb'), 'value': 'cl'}, {'label': _('Conv.'), 'value': 'cv'}]) - grid0.addWidget(self.milling_type_radio, 3, 1) + grid0.addWidget(self.milling_type_radio, 4, 1) # Combine passes self.combine_passes_cb = FCCheckBox(label=_('Combine Passes')) self.combine_passes_cb.setToolTip( _("Combine all passes into one object") ) - grid0.addWidget(self.combine_passes_cb, 4, 0, 1, 2) + grid0.addWidget(self.combine_passes_cb, 5, 0, 1, 2) # ## Clear non-copper regions self.clearcopper_label = QtWidgets.QLabel("%s:" % _("Non-copper regions")) @@ -1537,9 +1552,29 @@ class GerberAdvOptPrefGroupUI(OptionsGroupUI): self.cutz_spinner.set_range(-99.9999, -0.0001) self.cutz_spinner.setSingleStep(0.1) self.cutz_spinner.setWrapping(True) + grid0.addWidget(self.cutzlabel, 5, 0) grid0.addWidget(self.cutz_spinner, 5, 1, 1, 2) + # Isolation Type + self.iso_type_label = QtWidgets.QLabel('%s:' % _('Isolation Type')) + self.iso_type_label.setToolTip( + _("Choose how the isolation will be executed:\n" + "- 'Full' -> complete isolation of polygons\n" + "- 'Ext' -> will isolate only on the outside\n" + "- 'Int' -> will isolate only on the inside\n" + "'Exterior' isolation is almost always possible\n" + "(with the right tool) but 'Interior'\n" + "isolation can be done only when there is an opening\n" + "inside of the polygon (e.g polygon is a 'doughnut' shape).") + ) + self.iso_type_radio = RadioSet([{'label': _('Full'), 'value': 'full'}, + {'label': _('Exterior'), 'value': 'ext'}, + {'label': _('Interior'), 'value': 'int'}]) + + grid0.addWidget(self.iso_type_label, 6, 0,) + grid0.addWidget(self.iso_type_radio, 6, 1, 1, 2) + # Buffering Type buffering_label = QtWidgets.QLabel('%s:' % _('Buffering')) buffering_label.setToolTip( @@ -1550,8 +1585,8 @@ class GerberAdvOptPrefGroupUI(OptionsGroupUI): ) self.buffering_radio = RadioSet([{'label': _('None'), 'value': 'no'}, {'label': _('Full'), 'value': 'full'}]) - grid0.addWidget(buffering_label, 6, 0) - grid0.addWidget(self.buffering_radio, 6, 1) + grid0.addWidget(buffering_label, 7, 0) + grid0.addWidget(self.buffering_radio, 7, 1) # Simplification self.simplify_cb = FCCheckBox(label=_('Simplify')) @@ -1560,7 +1595,7 @@ class GerberAdvOptPrefGroupUI(OptionsGroupUI): "loaded with simplification having a set tolerance.\n" "<>: Don't change this unless you know what you are doing !!!") ) - grid0.addWidget(self.simplify_cb, 7, 0, 1, 2) + grid0.addWidget(self.simplify_cb, 8, 0, 1, 2) # Simplification tolerance self.simplification_tol_label = QtWidgets.QLabel(_('Tolerance')) @@ -1572,11 +1607,14 @@ class GerberAdvOptPrefGroupUI(OptionsGroupUI): self.simplification_tol_spinner.setRange(0.00000, 0.01000) self.simplification_tol_spinner.setSingleStep(0.0001) - grid0.addWidget(self.simplification_tol_label, 8, 0) - grid0.addWidget(self.simplification_tol_spinner, 8, 1) - self.ois_simplif = OptionalInputSection(self.simplify_cb, - [self.simplification_tol_label, self.simplification_tol_spinner], - logic=True) + grid0.addWidget(self.simplification_tol_label, 9, 0) + grid0.addWidget(self.simplification_tol_spinner, 9, 1) + self.ois_simplif = OptionalInputSection( + self.simplify_cb, + [ + self.simplification_tol_label, self.simplification_tol_spinner + ], + logic=True) self.layout.addStretch()