Files
flatcam-wsl/appEditors/geo_plugins/GeoPaintPlugin.py

349 lines
14 KiB
Python

from PyQt6 import QtWidgets
from appTool import AppToolEditor
from appGUI.GUIElements import VerticalScrollArea, FCLabel, FCFrame, FCButton, GLay, FCDoubleSpinner, FCComboBox, \
FCCheckBox
from camlib import Geometry
from shapely import Polygon
from shapely.ops import unary_union
import gettext
import appTranslation as fcTranslate
import builtins
fcTranslate.apply_language('strings')
if '_' not in builtins.__dict__:
_ = gettext.gettext
class PaintOptionsTool(AppToolEditor):
"""
Inputs to specify how to paint the selected polygons.
"""
def __init__(self, app, fcdraw):
AppToolEditor.__init__(self, app)
self.app = app
self.fcdraw = fcdraw
self.decimals = self.app.decimals
self.ui = PaintEditorUI(layout=self.layout, paint_class=self)
self.connect_signals_at_init()
self.set_tool_ui()
def run(self):
self.app.defaults.report_usage("Geo Editor ToolPaint()")
AppToolEditor.run(self)
# if the splitter us hidden, display it
if self.app.ui.splitter.sizes()[0] == 0:
self.app.ui.splitter.setSizes([1, 1])
# if the Tool Tab is hidden display it, else hide it but only if the objectName is the same
found_idx = None
for idx in range(self.app.ui.notebook.count()):
if self.app.ui.notebook.widget(idx).objectName() == "plugin_tab":
found_idx = idx
break
# show the Tab
if not found_idx:
try:
self.app.ui.notebook.addTab(self.app.ui.plugin_tab, _("Plugin"))
except RuntimeError:
self.app.ui.plugin_tab = QtWidgets.QWidget()
self.app.ui.plugin_tab.setObjectName("plugin_tab")
self.app.ui.plugin_tab_layout = QtWidgets.QVBoxLayout(self.app.ui.plugin_tab)
self.app.ui.plugin_tab_layout.setContentsMargins(2, 2, 2, 2)
self.app.ui.plugin_scroll_area = VerticalScrollArea()
self.app.ui.plugin_tab_layout.addWidget(self.app.ui.plugin_scroll_area)
self.app.ui.notebook.addTab(self.app.ui.plugin_tab, _("Plugin"))
# focus on Tool Tab
self.app.ui.notebook.setCurrentWidget(self.app.ui.plugin_tab)
# self.app.ui.notebook.callback_on_close = self.on_tab_close
self.app.ui.notebook.setTabText(2, _("Paint Tool"))
def connect_signals_at_init(self):
# Signals
self.ui.paint_button.clicked.connect(self.on_paint)
def on_tab_close(self):
self.fcdraw.select_tool("select")
self.app.ui.notebook.callback_on_close = lambda: None
def set_tool_ui(self):
# Init appGUI
if self.app.options["tools_paint_tooldia"]:
self.ui.painttooldia_entry.set_value(self.app.options["tools_paint_tooldia"])
else:
self.ui.painttooldia_entry.set_value(0.0)
if self.app.options["tools_paint_overlap"]:
self.ui.paintoverlap_entry.set_value(self.app.options["tools_paint_overlap"])
else:
self.ui.paintoverlap_entry.set_value(0.0)
if self.app.options["tools_paint_offset"]:
self.ui.paintmargin_entry.set_value(self.app.options["tools_paint_offset"])
else:
self.ui.paintmargin_entry.set_value(0.0)
if self.app.options["tools_paint_method"]:
self.ui.paintmethod_combo.set_value(self.app.options["tools_paint_method"])
else:
self.ui.paintmethod_combo.set_value(_("Seed"))
if self.app.options["tools_paint_connect"]:
self.ui.pathconnect_cb.set_value(self.app.options["tools_paint_connect"])
else:
self.ui.pathconnect_cb.set_value(False)
if self.app.options["tools_paint_contour"]:
self.ui.paintcontour_cb.set_value(self.app.options["tools_paint_contour"])
else:
self.ui.paintcontour_cb.set_value(False)
def on_paint(self):
if not self.fcdraw.selected:
self.app.inform.emit('[WARNING_NOTCL] %s %s' % (_("Cancelled."), _("No shape selected.")))
return
tooldia = self.ui.painttooldia_entry.get_value()
overlap = self.ui.paintoverlap_entry.get_value() / 100.0
margin = self.ui.paintmargin_entry.get_value()
method = self.ui.paintmethod_combo.get_value()
contour = self.ui.paintcontour_cb.get_value()
connect = self.ui.pathconnect_cb.get_value()
self.paint(tooldia, overlap, margin, connect=connect, contour=contour, method=method)
self.fcdraw.select_tool("select")
# self.app.ui.notebook.setTabText(2, _("Tools"))
# self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab)
#
# self.app.ui.splitter.setSizes([0, 1])
def paint(self, tooldia, overlap, margin, connect, contour, method):
def work_task(geo_editor):
with geo_editor.app.proc_container.new(_("Working...")):
if overlap >= 100:
geo_editor.app.inform.emit('[ERROR_NOTCL] %s' %
_("Could not do Paint. Overlap value has to be less than 100%%."))
return
geo_editor.paint_tooldia = tooldia
selected = geo_editor.get_selected()
if len(selected) == 0:
geo_editor.app.inform.emit('[WARNING_NOTCL] %s' % _("Nothing selected."))
return
for param in [tooldia, overlap, margin]:
if not isinstance(param, float):
param_name = [k for k, v in locals().items() if v is param][0]
geo_editor.app.inform.emit('[WARNING] %s: %s' % (_("Invalid value for"), str(param)))
results = []
def recurse(geometry, reset=True):
"""
Creates a list of non-iterable linear geometry objects.
Results are placed in self.flat_geometry
:param geometry: Shapely type, list or list of lists of such.
:param reset: Clears the contents of self.flat_geometry.
"""
if geometry is None:
return
if reset:
self.flat_geo = []
# If iterable, expand recursively.
try:
for geo_el in geometry:
if geo_el is not None and not geo_el.is_emoty:
recurse(geometry=geo_el, reset=False)
# Not iterable, do the actual indexing and add.
except TypeError:
self.flat_geo.append(geometry)
return self.flat_geo
for geo in selected:
local_results = []
for geo_obj in recurse(geo.geo):
try:
if type(geo_obj) == Polygon:
poly_buf = geo_obj.buffer(-margin)
else:
poly_buf = Polygon(geo_obj).buffer(-margin)
if method == _("Seed"):
cp = Geometry.clear_polygon_seed(
geo_editor, polygon_to_clear=poly_buf, tooldia=tooldia,
steps_per_circle=geo_editor.app.options["geometry_circle_steps"],
overlap=overlap, contour=contour, connect=connect)
elif method == _("Lines"):
cp = Geometry.clear_polygon_lines(
geo_editor, polygon=poly_buf, tooldia=tooldia,
steps_per_circle=geo_editor.app.options["geometry_circle_steps"],
overlap=overlap, contour=contour, connect=connect)
else:
cp = Geometry.clear_polygon_shrink(
geo_editor, polygon=poly_buf, tooldia=tooldia,
steps_per_circle=geo_editor.app.options["geometry_circle_steps"],
overlap=overlap, contour=contour, connect=connect)
if cp is not None:
local_results += list(cp.get_objects())
except Exception as e:
geo_editor.app.log.error("Could not Paint the polygons. %s" % str(e))
geo_editor.app.inform.emit(
'[ERROR] %s\n%s' % (_("Could not do Paint. Try a different combination of parameters. "
"Or a different method of Paint"), str(e))
)
return
# add the result to the results list
results.append(unary_union(local_results))
# This is a dirty patch:
for r in results:
geo_editor.add_shape(r)
geo_editor.plot_all()
geo_editor.build_ui_sig.emit()
geo_editor.app.inform.emit('[success] %s' % _("Done."))
self.app.worker_task.emit({'fcn': work_task, 'params': [self.fcdraw]})
class PaintEditorUI:
pluginName = _("Paint")
def __init__(self, layout, paint_class):
self.paint_class = paint_class
self.decimals = self.paint_class.app.decimals
self.layout = layout
# Title
title_label = FCLabel("%s" % self.pluginName, size=16, bold=True)
self.layout.addWidget(title_label)
self.param_label = FCLabel('%s' % _("Parameters"), color='blue', bold=True)
self.layout.addWidget(self.param_label)
# #############################################################################################################
# Tool Params Frame
# #############################################################################################################
tool_par_frame = FCFrame()
self.layout.addWidget(tool_par_frame)
# Grid Layout
param_grid = GLay(v_spacing=5, h_spacing=3)
tool_par_frame.setLayout(param_grid)
# Tool dia
ptdlabel = FCLabel('%s:' % _('Tool Dia'))
ptdlabel.setToolTip(
_("Diameter of the tool to be used in the operation.")
)
param_grid.addWidget(ptdlabel, 0, 0)
self.painttooldia_entry = FCDoubleSpinner()
self.painttooldia_entry.set_range(-10000.0000, 10000.0000)
self.painttooldia_entry.set_precision(self.decimals)
param_grid.addWidget(self.painttooldia_entry, 0, 1)
# Overlap
ovlabel = FCLabel('%s:' % _('Overlap'))
ovlabel.setToolTip(
_("How much (percentage) of the tool width to overlap each tool pass.\n"
"Adjust the value starting with lower values\n"
"and increasing it if areas that should be processed are still \n"
"not processed.\n"
"Lower values = faster processing, faster execution on CNC.\n"
"Higher values = slow processing and slow execution on CNC\n"
"due of too many paths.")
)
self.paintoverlap_entry = FCDoubleSpinner(suffix='%')
self.paintoverlap_entry.set_range(0.0000, 99.9999)
self.paintoverlap_entry.set_precision(self.decimals)
self.paintoverlap_entry.setWrapping(True)
self.paintoverlap_entry.setSingleStep(1)
param_grid.addWidget(ovlabel, 1, 0)
param_grid.addWidget(self.paintoverlap_entry, 1, 1)
# Margin
marginlabel = FCLabel('%s:' % _('Margin'))
marginlabel.setToolTip(
_("Distance by which to avoid\n"
"the edges of the polygon to\n"
"be painted.")
)
self.paintmargin_entry = FCDoubleSpinner()
self.paintmargin_entry.set_range(-10000.0000, 10000.0000)
self.paintmargin_entry.set_precision(self.decimals)
param_grid.addWidget(marginlabel, 2, 0)
param_grid.addWidget(self.paintmargin_entry, 2, 1)
# Method
methodlabel = FCLabel('%s:' % _('Method'))
methodlabel.setToolTip(
_("Algorithm to paint the polygons:\n"
"- Standard: Fixed step inwards.\n"
"- Seed-based: Outwards from seed.\n"
"- Line-based: Parallel lines.")
)
# self.paintmethod_combo = RadioSet([
# {"label": _("Standard"), "value": "standard"},
# {"label": _("Seed-based"), "value": "seed"},
# {"label": _("Straight lines"), "value": "lines"}
# ], orientation='vertical', compact=True)
self.paintmethod_combo = FCComboBox()
self.paintmethod_combo.addItems(
[_("Standard"), _("Seed"), _("Lines")]
)
param_grid.addWidget(methodlabel, 3, 0)
param_grid.addWidget(self.paintmethod_combo, 3, 1)
# Connect lines
pathconnectlabel = FCLabel('%s:' % _("Connect"))
pathconnectlabel.setToolTip(
_("Draw lines between resulting\n"
"segments to minimize tool lifts.")
)
self.pathconnect_cb = FCCheckBox()
param_grid.addWidget(pathconnectlabel, 4, 0)
param_grid.addWidget(self.pathconnect_cb, 4, 1)
contourlabel = FCLabel('%s:' % _("Contour"))
contourlabel.setToolTip(
_("Cut around the perimeter of the polygon\n"
"to trim rough edges.")
)
self.paintcontour_cb = FCCheckBox()
param_grid.addWidget(contourlabel, 5, 0)
param_grid.addWidget(self.paintcontour_cb, 5, 1)
# Buttons
hlay = QtWidgets.QHBoxLayout()
self.layout.addLayout(hlay)
self.paint_button = FCButton(_("Paint"))
hlay.addWidget(self.paint_button)
self.layout.addStretch(1)