From 2517f31fa46d82f9bdd0db47ff5a9b4a3db1fd1b Mon Sep 17 00:00:00 2001 From: Marius Stanciu Date: Fri, 5 Jan 2024 14:04:41 +0200 Subject: [PATCH] - Follow Tool: added more parameters: simplification and union - Follow Tool: the resulting LineString geometries are now merged together to minimize the number of lines --- CHANGELOG.md | 5 ++ appPlugins/ToolFollow.py | 110 ++++++++++++++++++++++++++++++++++----- camlib.py | 4 +- defaults.py | 5 ++ 4 files changed, 109 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b74eebb..3bcde9ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ CHANGELOG for FlatCAM Evo beta ================================================= +5.01.2024 + +- Follow Tool: added more parameters: simplification and union +- Follow Tool: the resulting LineString geometries are now merged together to minimize the number of lines + 3.01.2024 - Geo Editor -> Simplification Sub-tool: fixed an issue when calculating vertexes number diff --git a/appPlugins/ToolFollow.py b/appPlugins/ToolFollow.py index a6a15378..58432c08 100644 --- a/appPlugins/ToolFollow.py +++ b/appPlugins/ToolFollow.py @@ -7,13 +7,14 @@ from PyQt6 import QtWidgets, QtCore, QtGui from appTool import AppTool -from appGUI.GUIElements import VerticalScrollArea, FCLabel, FCButton, FCFrame, GLay, FCComboBox, RadioSet +from appGUI.GUIElements import (VerticalScrollArea, FCLabel, FCButton, FCFrame, GLay, FCComboBox, RadioSet, + FCDoubleSpinner, FCCheckBox, OptionalInputSection) import logging from copy import deepcopy import numpy as np -from shapely import Polygon +from shapely import Polygon, line_merge, MultiLineString, Point, simplify from shapely.ops import unary_union import gettext @@ -129,7 +130,7 @@ class ToolFollow(AppTool, Gerber): def connect_signals_at_init(self): self.ui.level.toggled.connect(self.on_level_changed) - self.ui.selectmethod_radio.activated_custom.connect(self.ui.on_selection) + self.ui.select_method_radio.activated_custom.connect(self.ui.on_selection) self.ui.generate_geometry_button.clicked.connect(self.on_generate_geometry_click) def set_tool_ui(self): @@ -140,7 +141,7 @@ class ToolFollow(AppTool, Gerber): self.pluginName = self.ui.pluginName self.connect_signals_at_init() - self.ui.selectmethod_radio.set_value('all') # _("All") + self.ui.select_method_radio.set_value('all') # _("All") self.ui.area_shape_radio.set_value('square') self.sel_rect[:] = [] @@ -154,10 +155,29 @@ class ToolFollow(AppTool, Gerber): obj_name = obj.obj_options['name'] self.ui.object_combo.set_value(obj_name) + # Set UI + self.ui.simplify_cb.set_value(self.app.options["tools_follow_simplification"]) + self.ui.tol_entry.set_value(self.app.options["tools_follow_tolerance"]) + self.ui.union_cb.set_value(self.app.options["tools_follow_union"]) + # Show/Hide Advanced Options app_mode = self.app.options["global_app_level"] self.change_level(app_mode) + # SIGNALS + self.ui.simplify_cb.stateChanged.connect(self.on_simplify_changed) + self.ui.tol_entry.valueChanged.connect(self.on_tolerance_changed) + self.ui.union_cb.stateChanged.connect(self.on_union_changed) + + def on_simplify_changed(self, checked): + self.app.options["tools_follow_simplification"] = checked + + def on_tolerance_changed(self, value): + self.app.options["tools_follow_tolerance"] = value + + def on_union_changed(self, checked): + self.app.options["tools_follow_union"] = checked + def change_level(self, level): """ @@ -217,7 +237,7 @@ class ToolFollow(AppTool, Gerber): formatted_name = obj_name outname = '%s_follow' % formatted_name - select_method = self.ui.selectmethod_radio.get_value() + select_method = self.ui.select_method_radio.get_value() if select_method == 'all': # _("All") self.follow_all(obj, outname) else: @@ -259,7 +279,7 @@ class ToolFollow(AppTool, Gerber): return "Could not retrieve object: %s with error: %s" % (obj_name, str(e)) if obj is None: - self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Object not found"), str(self.obj_name))) + self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Object not found"), str(obj_name))) return formatted_name = obj_name.rpartition('.')[0] @@ -283,6 +303,10 @@ class ToolFollow(AppTool, Gerber): :return: None """ + should_union = self.ui.union_cb.get_value() + should_simplify = self.ui.simplify_cb.get_value() + simplify_tol = self.ui.tol_entry.get_value() + def follow_init(new_obj, app_obj): if type(app_obj.defaults["tools_mill_tooldia"]) == float: tools_list = [app_obj.defaults["tools_mill_tooldia"]] @@ -306,12 +330,28 @@ class ToolFollow(AppTool, Gerber): if opt_key.find('tools_') == 0: new_data[opt_key] = app_obj.options[opt_key] - followed_obj.follow_geometry = flatten_shapely_geometry(followed_obj.follow_geometry) + flattened_follow_geometry = flatten_shapely_geometry(followed_obj.follow_geometry) + cleaned_flat_follow_geometry = [ + f for f in flattened_follow_geometry if not isinstance(f, Point) and not f.is_empty + ] + + merged_geo = line_merge(MultiLineString(cleaned_flat_follow_geometry)) + if merged_geo and not merged_geo.is_empty: + flattened_follow_geometry = flatten_shapely_geometry(merged_geo) + + followed_obj.follow_geometry = flattened_follow_geometry + + # Filter out empty geometries follow_geo = [ g for g in followed_obj.follow_geometry if g and not g.is_empty and g.is_valid and g.geom_type != 'Point' ] + if should_simplify and simplify_tol > 0.0: + follow_geo = [simplify(f, tolerance=simplify_tol) for f in follow_geo] + if should_union: + follow_geo = unary_union(follow_geo) + if not follow_geo: self.app.log.warning("ToolFollow.follow_geo() -> Empty Follow Geometry") return 'fail' @@ -345,6 +385,9 @@ class ToolFollow(AppTool, Gerber): :type outname: str :return: None """ + should_union = self.ui.union_cb.get_value() + should_simplify = self.ui.simplify_cb.get_value() + simplify_tol = self.ui.tol_entry.get_value() def follow_init(new_obj, app_obj): new_obj.multigeo = True @@ -381,8 +424,21 @@ class ToolFollow(AppTool, Gerber): self.points = [] area_follow = flatten_shapely_geometry(area_follow) + cleaned_flat_follow_area = [ + f for f in area_follow if not isinstance(f, Point) and not f.is_empty + ] + + merged_geo = line_merge(MultiLineString(cleaned_flat_follow_area)) + if merged_geo and not merged_geo.is_empty: + area_follow = flatten_shapely_geometry(merged_geo) + cleaned_area_follow = [g for g in area_follow if not g.is_empty and g.is_valid and g.geom_type != 'Point'] + if should_simplify and simplify_tol > 0.0: + cleaned_area_follow = [simplify(f, tolerance=simplify_tol) for f in cleaned_area_follow] + if should_union: + cleaned_area_follow = unary_union(cleaned_area_follow) + new_obj.multigeo = True new_obj.solid_geometry = deepcopy(cleaned_area_follow) new_obj.tools = { @@ -730,6 +786,34 @@ class FollowUI: grid0 = GLay(v_spacing=5, h_spacing=3) self.gp_frame.setLayout(grid0) + # Simplification + self.simplify_cb = FCCheckBox(_("Simplify")) + self.simplify_cb.setToolTip( + _("If checked, the toolpaths will be simplified with the given tolerance.") + ) + self.tol_label = FCLabel('%s:' % _('Tolerance')) + self.tol_label.setToolTip( + _("The tolerance of the simplification.") + ) + self.tol_entry = FCDoubleSpinner() + self.tol_entry.set_range(0.0, 10000.0) + self.tol_entry.set_precision(self.decimals) + self.tol_entry.setSingleStep(0.01) + + self.simp_optional = OptionalInputSection(self.simplify_cb, [self.tol_label, self.tol_entry]) + + grid0.addWidget(self.simplify_cb, 0, 0, 1, 2) + grid0.addWidget(self.tol_label, 2, 0) + grid0.addWidget(self.tol_entry, 2, 1) + + # UNION + self.union_cb = FCCheckBox(_("Union")) + self.union_cb.setToolTip( + _("If checked, the toolpaths will be joined into a Union.") + ) + + grid0.addWidget(self.union_cb, 4, 0, 1, 2) + # Polygon selection self.select_label = FCLabel('%s:' % _('Selection')) self.select_label.setToolTip( @@ -738,11 +822,11 @@ class FollowUI: "- 'Area Selection' - left mouse click to start selection of the area to be processed.") ) - self.selectmethod_radio = RadioSet([{'label': _("All"), 'value': 'all'}, - {'label': _("Area Selection"), 'value': 'area'}]) + self.select_method_radio = RadioSet([{'label': _("All"), 'value': 'all'}, + {'label': _("Area Selection"), 'value': 'area'}]) - grid0.addWidget(self.select_label, 0, 0) - grid0.addWidget(self.selectmethod_radio, 0, 1) + grid0.addWidget(self.select_label, 6, 0) + grid0.addWidget(self.select_method_radio, 6, 1) # Area Selection shape self.area_shape_label = FCLabel('%s:' % _("Shape")) @@ -753,8 +837,8 @@ class FollowUI: self.area_shape_radio = RadioSet([{'label': _("Square"), 'value': 'square'}, {'label': _("Polygon"), 'value': 'polygon'}]) - grid0.addWidget(self.area_shape_label, 2, 0) - grid0.addWidget(self.area_shape_radio, 2, 1) + grid0.addWidget(self.area_shape_label, 8, 0) + grid0.addWidget(self.area_shape_radio, 8, 1) self.area_shape_label.hide() self.area_shape_radio.hide() diff --git a/camlib.py b/camlib.py index ad4be074..7f1a4360 100644 --- a/camlib.py +++ b/camlib.py @@ -7793,7 +7793,7 @@ class CNCjob(Geometry): self.app.proc_container.new_text = '' -def flatten_shapely_geometry(geometry, simplify_tolerance=0.0): +def flatten_shapely_geometry(geometry, simplify_tolerance: float = 0.0) -> list: """ :param geometry: @@ -7818,7 +7818,7 @@ def flatten_shapely_geometry(geometry, simplify_tolerance=0.0): return flat_list -def get_bounds(geometry_list): +def get_bounds(geometry_list: list) -> list: """ Will return limit values for a list of geometries diff --git a/defaults.py b/defaults.py index aa6ac3db..c71b3807 100644 --- a/defaults.py +++ b/defaults.py @@ -367,6 +367,11 @@ class AppDefaults: "cncjob_prepend": "", "cncjob_append": "", + # Follow Tool + "tools_follow_simplification": False, + "tools_follow_tolerance": 0.01, + "tools_follow_union": False, + # Isolation Routing Plugin "tools_iso_tooldia": "0.1", "tools_iso_order": 2, # Reverse