349 lines
14 KiB
Python
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)
|