- Follow Tool: added more parameters: simplification and union

- Follow Tool: the resulting LineString geometries are now merged together to minimize the number of lines
This commit is contained in:
Marius Stanciu
2024-01-05 14:04:41 +02:00
parent c3f2f8fdeb
commit 2517f31fa4
4 changed files with 109 additions and 15 deletions

View File

@@ -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()