From 8dc4eecbf427aed65e3ad7e0519709bbe6521210 Mon Sep 17 00:00:00 2001 From: Marius Stanciu Date: Thu, 18 Jun 2020 18:50:02 +0300 Subject: [PATCH] - Panelize Tool - add a new option for the panels of type Geometry named Path Optimiztion. If the checkbox is checked then all the LineStrings that are overlapped in the resulting multigeo Geometry panel object will keep only one of the paths thus minimizing the tool cuts. --- CHANGELOG.md | 1 + appGUI/preferences/PreferencesUIManager.py | 1 + .../tools/ToolsPanelizePrefGroupUI.py | 20 +++- appTools/ToolPanelize.py | 95 +++++++++++++++---- defaults.py | 1 + 5 files changed, 92 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 989ea446..e3a739ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ CHANGELOG for FlatCAM beta - Cutout Tool - in manual gap adding there is now an option to automatically turn on the big cursor which could help - Cutout Tool - fixed errors when trying to add a manual gap without having a geometry object selected in the combobox - Cutout Tool - made sure that all the paths generated by this tool are contiguous which means that two lines that meet at one end will become onle line therefore reducing unnecessary Z moves +- Panelize Tool - add a new option for the panels of type Geometry named Path Optimiztion. If the checkbox is checked then all the LineStrings that are overlapped in the resulting multigeo Geometry panel object will keep only one of the paths thus minimizing the tool cuts. 17.06.2020 diff --git a/appGUI/preferences/PreferencesUIManager.py b/appGUI/preferences/PreferencesUIManager.py index 0e8f1a38..7203fd59 100644 --- a/appGUI/preferences/PreferencesUIManager.py +++ b/appGUI/preferences/PreferencesUIManager.py @@ -437,6 +437,7 @@ class PreferencesUIManager: "tools_panelize_spacing_rows": self.ui.tools_defaults_form.tools_panelize_group.pspacing_rows, "tools_panelize_columns": self.ui.tools_defaults_form.tools_panelize_group.pcolumns, "tools_panelize_rows": self.ui.tools_defaults_form.tools_panelize_group.prows, + "tools_panelize_optimization": self.ui.tools_defaults_form.tools_panelize_group.poptimization_cb, "tools_panelize_constrain": self.ui.tools_defaults_form.tools_panelize_group.pconstrain_cb, "tools_panelize_constrainx": self.ui.tools_defaults_form.tools_panelize_group.px_width_entry, "tools_panelize_constrainy": self.ui.tools_defaults_form.tools_panelize_group.py_height_entry, diff --git a/appGUI/preferences/tools/ToolsPanelizePrefGroupUI.py b/appGUI/preferences/tools/ToolsPanelizePrefGroupUI.py index 9189d88d..8dbd7677 100644 --- a/appGUI/preferences/tools/ToolsPanelizePrefGroupUI.py +++ b/appGUI/preferences/tools/ToolsPanelizePrefGroupUI.py @@ -106,6 +106,16 @@ class ToolsPanelizePrefGroupUI(OptionsGroupUI): grid0.addWidget(self.panel_type_label, 4, 0) grid0.addWidget(self.panel_type_radio, 4, 1) + # Path optimization + self.poptimization_cb = FCCheckBox('%s' % _("Path Optimization")) + self.poptimization_cb.setToolTip( + _("Active only for Geometry panel type.\n" + "When checked the application will find\n" + "any two overlapping Line elements in the panel\n" + "and remove the overlapping parts, keeping only one of them.") + ) + grid0.addWidget(self.poptimization_cb, 5, 0, 1, 2) + # ## Constrains self.pconstrain_cb = FCCheckBox('%s:' % _("Constrain within")) self.pconstrain_cb.setToolTip( @@ -115,7 +125,7 @@ class ToolsPanelizePrefGroupUI(OptionsGroupUI): "the final panel will have as many columns and rows as\n" "they fit completely within selected area.") ) - grid0.addWidget(self.pconstrain_cb, 5, 0, 1, 2) + grid0.addWidget(self.pconstrain_cb, 10, 0, 1, 2) self.px_width_entry = FCDoubleSpinner() self.px_width_entry.set_range(0.000001, 9999.9999) @@ -127,8 +137,8 @@ class ToolsPanelizePrefGroupUI(OptionsGroupUI): _("The width (DX) within which the panel must fit.\n" "In current units.") ) - grid0.addWidget(self.x_width_lbl, 6, 0) - grid0.addWidget(self.px_width_entry, 6, 1) + grid0.addWidget(self.x_width_lbl, 12, 0) + grid0.addWidget(self.px_width_entry, 12, 1) self.py_height_entry = FCDoubleSpinner() self.py_height_entry.set_range(0.000001, 9999.9999) @@ -140,7 +150,7 @@ class ToolsPanelizePrefGroupUI(OptionsGroupUI): _("The height (DY)within which the panel must fit.\n" "In current units.") ) - grid0.addWidget(self.y_height_lbl, 7, 0) - grid0.addWidget(self.py_height_entry, 7, 1) + grid0.addWidget(self.y_height_lbl, 17, 0) + grid0.addWidget(self.py_height_entry, 17, 1) self.layout.addStretch() diff --git a/appTools/ToolPanelize.py b/appTools/ToolPanelize.py index 7920b574..9358aa43 100644 --- a/appTools/ToolPanelize.py +++ b/appTools/ToolPanelize.py @@ -15,8 +15,8 @@ from copy import deepcopy import numpy as np import shapely.affinity as affinity -from shapely.ops import unary_union -from shapely.geometry import LineString +from shapely.ops import unary_union, linemerge, snap +from shapely.geometry import LineString, MultiLineString import gettext import appTranslation as fcTranslate @@ -112,6 +112,10 @@ class Panelize(AppTool): self.app.defaults["tools_panelize_columns"] else 0.0 self.ui.columns.set_value(int(cc)) + optimized_path_cb = self.app.defaults["tools_panelize_optimization"] if \ + self.app.defaults["tools_panelize_optimization"] else True + self.ui.optimization_cb.set_value(optimized_path_cb) + c_cb = self.app.defaults["tools_panelize_constrain"] if \ self.app.defaults["tools_panelize_constrain"] else False self.ui.constrain_cb.set_value(c_cb) @@ -128,6 +132,8 @@ class Panelize(AppTool): self.app.defaults["tools_panelize_panel_type"] else 'gerber' self.ui.panel_type_radio.set_value(panel_type) + self.ui.on_panel_type(val=panel_type) + # run once the following so the obj_type attribute is updated in the FCComboBoxes # such that the last loaded object is populated in the combo boxes self.on_type_obj_index_changed() @@ -145,10 +151,12 @@ class Panelize(AppTool): if self.ui.type_obj_combo.currentText() != 'Excellon': self.ui.panel_type_label.setDisabled(False) self.ui.panel_type_radio.setDisabled(False) + self.ui.on_panel_type(val=self.ui.panel_type_radio.get_value()) else: self.ui.panel_type_label.setDisabled(True) self.ui.panel_type_radio.setDisabled(True) self.ui.panel_type_radio.set_value('geometry') + self.ui.optimization_cb.setDisabled(True) def on_type_box_index_changed(self): obj_type = self.ui.type_box_combo.currentIndex() @@ -257,6 +265,8 @@ class Panelize(AppTool): for tt, tt_val in list(panel_source_obj.apertures.items()): copied_apertures[tt] = deepcopy(tt_val) + to_optimize = self.ui.optimization_cb.get_value() + def panelize_worker(): if panel_source_obj is not None: self.app.inform.emit(_("Generating panel ... ")) @@ -370,7 +380,7 @@ class Panelize(AppTool): obj_fin.tools = copied_tools if panel_source_obj.multigeo is True: for tool in panel_source_obj.tools: - obj_fin.tools[tool]['solid_geometry'][:] = [] + obj_fin.tools[tool]['solid_geometry'] = [] elif panel_source_obj.kind == 'gerber': obj_fin.apertures = copied_apertures for ap in obj_fin.apertures: @@ -385,11 +395,6 @@ class Panelize(AppTool): geo_len += len(panel_source_obj.tools[tool]['solid_geometry']) except TypeError: geo_len += 1 - # else: - # try: - # geo_len = len(panel_source_obj.solid_geometry) - # except TypeError: - # geo_len = 1 elif panel_source_obj.kind == 'gerber': for ap in panel_source_obj.apertures: if 'geometry' in panel_source_obj.apertures[ap]: @@ -410,17 +415,20 @@ class Panelize(AppTool): if panel_source_obj.kind == 'geometry': if panel_source_obj.multigeo is True: for tool in panel_source_obj.tools: + # graceful abort requested by the user if app_obj.abort_flag: - # graceful abort requested by the user raise grace # calculate the number of polygons geo_len = len(panel_source_obj.tools[tool]['solid_geometry']) + + # panelization pol_nr = 0 for geo_el in panel_source_obj.tools[tool]['solid_geometry']: trans_geo = translate_recursion(geo_el) obj_fin.tools[tool]['solid_geometry'].append(trans_geo) + # update progress pol_nr += 1 disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 100])) if old_disp_number < disp_number <= 100: @@ -428,15 +436,17 @@ class Panelize(AppTool): ' %s: %d %d%%' % (_("Copy"), int(element), disp_number)) old_disp_number = disp_number else: + # graceful abort requested by the user if app_obj.abort_flag: - # graceful abort requested by the user raise grace + # calculate the number of polygons try: - # calculate the number of polygons geo_len = len(panel_source_obj.solid_geometry) except TypeError: geo_len = 1 + + # panelization pol_nr = 0 try: for geo_el in panel_source_obj.solid_geometry: @@ -447,9 +457,9 @@ class Panelize(AppTool): trans_geo = translate_recursion(geo_el) obj_fin.solid_geometry.append(trans_geo) + # update progress pol_nr += 1 disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 100])) - if old_disp_number < disp_number <= 100: app_obj.proc_container.update_view_text( ' %s: %d %d%%' % (_("Copy"), int(element), disp_number)) @@ -460,14 +470,15 @@ class Panelize(AppTool): obj_fin.solid_geometry.append(trans_geo) # Will panelize a Gerber Object else: + # graceful abort requested by the user if self.app.abort_flag: - # graceful abort requested by the user raise grace + # panelization solid_geometry try: for geo_el in panel_source_obj.solid_geometry: + # graceful abort requested by the user if app_obj.abort_flag: - # graceful abort requested by the user raise grace trans_geo = translate_recursion(geo_el) @@ -477,15 +488,18 @@ class Panelize(AppTool): obj_fin.solid_geometry.append(trans_geo) for apid in panel_source_obj.apertures: + # graceful abort requested by the user if app_obj.abort_flag: - # graceful abort requested by the user raise grace + if 'geometry' in panel_source_obj.apertures[apid]: + # calculate the number of polygons try: - # calculate the number of polygons geo_len = len(panel_source_obj.apertures[apid]['geometry']) except TypeError: geo_len = 1 + + # panelization -> tools pol_nr = 0 for el in panel_source_obj.apertures[apid]['geometry']: if app_obj.abort_flag: @@ -496,20 +510,17 @@ class Panelize(AppTool): if 'solid' in el: geo_aper = translate_recursion(el['solid']) new_el['solid'] = geo_aper - if 'clear' in el: geo_aper = translate_recursion(el['clear']) new_el['clear'] = geo_aper - if 'follow' in el: geo_aper = translate_recursion(el['follow']) new_el['follow'] = geo_aper - obj_fin.apertures[apid]['geometry'].append(deepcopy(new_el)) + # update progress pol_nr += 1 disp_number = int(np.interp(pol_nr, [0, geo_len], [0, 100])) - if old_disp_number < disp_number <= 100: app_obj.proc_container.update_view_text( ' %s: %d %d%%' % (_("Copy"), int(element), disp_number)) @@ -522,17 +533,42 @@ class Panelize(AppTool): # I'm going to do this only here as a fix for panelizing cutouts # I'm going to separate linestrings out of the solid geometry from other # possible type of elements and apply unary_union on them to fuse them + + if to_optimize is True: + app_obj.inform.emit('%s' % _("Optimizing the overlapping paths.")) + for tool in obj_fin.tools: lines = [] other_geo = [] for geo in obj_fin.tools[tool]['solid_geometry']: if isinstance(geo, LineString): lines.append(geo) + elif isinstance(geo, MultiLineString): + for line in geo: + lines.append(line) else: other_geo.append(geo) - fused_lines = list(unary_union(lines)) + + if to_optimize is True: + for idx, line in enumerate(lines): + for idx_s in range(idx+1, len(lines)): + line_mod = lines[idx_s] + dist = line.distance(line_mod) + if dist < 1e-8: + print("Disjoint %d: %d -> %s" % (idx, idx_s, str(dist))) + print("Distance %f" % dist) + res = snap(line_mod, line, tolerance=1e-7) + if res and not res.is_empty: + lines[idx_s] = res + + fused_lines = linemerge(lines) + fused_lines = [unary_union(fused_lines)] + obj_fin.tools[tool]['solid_geometry'] = fused_lines + other_geo + if to_optimize is True: + app_obj.inform.emit('%s' % _("Optimization complete.")) + if panel_type == 'gerber': app_obj.inform.emit('%s' % _("Generating panel ... Adding the Gerber code.")) obj_fin.source_file = self.app.export_gerber(obj_name=self.outname, filename=None, @@ -766,6 +802,16 @@ class PanelizeUI: form_layout.addRow(self.panel_type_label) form_layout.addRow(self.panel_type_radio) + # Path optimization + self.optimization_cb = FCCheckBox('%s' % _("Path Optimization")) + self.optimization_cb.setToolTip( + _("Active only for Geometry panel type.\n" + "When checked the application will find\n" + "any two overlapping Line elements in the panel\n" + "and remove the overlapping parts, keeping only one of them.") + ) + form_layout.addRow(self.optimization_cb) + # Constrains self.constrain_cb = FCCheckBox('%s:' % _("Constrain panel within")) self.constrain_cb.setToolTip( @@ -839,6 +885,13 @@ class PanelizeUI: # #################################### FINSIHED GUI ########################### # ############################################################################# + self.panel_type_radio.activated_custom.connect(self.on_panel_type) + + def on_panel_type(self, val): + if val == 'geometry': + self.optimization_cb.setDisabled(False) + else: + self.optimization_cb.setDisabled(True) def confirmation_message(self, accepted, minval, maxval): if accepted is False: diff --git a/defaults.py b/defaults.py index ba3c8ab5..c896b818 100644 --- a/defaults.py +++ b/defaults.py @@ -506,6 +506,7 @@ class FlatCAMDefaults: "tools_panelize_spacing_rows": 0.0, "tools_panelize_columns": 1, "tools_panelize_rows": 1, + "tools_panelize_optimization": True, "tools_panelize_constrain": False, "tools_panelize_constrainx": 200.0, "tools_panelize_constrainy": 290.0,