From 74746097766ef2afcc94c97b931d0078eee7f516 Mon Sep 17 00:00:00 2001 From: Juan Pablo Caram Date: Mon, 19 Sep 2016 16:44:29 -0400 Subject: [PATCH] Added feature: Select all polygons for painting and shell support with "paint" command. --- FlatCAMApp.py | 41 ++++++------ FlatCAMGUI.py | 16 ++++- FlatCAMObj.py | 111 ++++++++++++++++++++++++++++----- ObjectUI.py | 14 +++++ camlib.py | 10 +++ tclCommands/TclCommandPaint.py | 84 +++++++++++++++++++++++++ tclCommands/__init__.py | 1 + 7 files changed, 242 insertions(+), 35 deletions(-) create mode 100644 tclCommands/TclCommandPaint.py diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 3e93c6a8..a9fcf029 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -259,6 +259,7 @@ class App(QtCore.QObject): "geometry_spindlespeed": self.defaults_form.geometry_group.cncspindlespeed_entry, "geometry_paintoverlap": self.defaults_form.geometry_group.paintoverlap_entry, "geometry_paintmargin": self.defaults_form.geometry_group.paintmargin_entry, + "geometry_selectmethod": self.defaults_form.geometry_group.selectmethod_combo, "cncjob_plot": self.defaults_form.cncjob_group.plot_cb, "cncjob_tooldia": self.defaults_form.cncjob_group.tooldia_entry, "cncjob_prepend": self.defaults_form.cncjob_group.prepend_text, @@ -304,6 +305,7 @@ class App(QtCore.QObject): "geometry_painttooldia": 0.07, "geometry_paintoverlap": 0.15, "geometry_paintmargin": 0.0, + "geometry_selectmethod": "single", "cncjob_plot": True, "cncjob_tooldia": 0.016, "cncjob_prepend": "", @@ -397,6 +399,7 @@ class App(QtCore.QObject): "geometry_painttooldia": self.options_form.geometry_group.painttooldia_entry, "geometry_paintoverlap": self.options_form.geometry_group.paintoverlap_entry, "geometry_paintmargin": self.options_form.geometry_group.paintmargin_entry, + "geometry_selectmethod": self.options_form.geometry_group.selectmethod_combo, "cncjob_plot": self.options_form.cncjob_group.plot_cb, "cncjob_tooldia": self.options_form.cncjob_group.tooldia_entry, "cncjob_prepend": self.options_form.cncjob_group.prepend_text, @@ -439,12 +442,15 @@ class App(QtCore.QObject): "geometry_painttooldia": 0.07, "geometry_paintoverlap": 0.15, "geometry_paintmargin": 0.0, + "geometry_selectmethod": "single", "cncjob_plot": True, "cncjob_tooldia": 0.016, "cncjob_prepend": "", "cncjob_append": "", - "background_timeout": 300000, #default value is 5 minutes - "verbose_error_level": 0, # shell verbosity 0 = default(python trace only for unknown errors), 1 = show trace(show trace allways), 2 = (For the future). + "background_timeout": 300000, # Default value is 5 minutes + "verbose_error_level": 0, # Shell verbosity: + # 0 = default(python trace only for unknown errors), + # 1 = show trace(show trace allways), 2 = (For the future). }) self.options.update(self.defaults) # Copy app defaults to project options #self.options_write_form() @@ -877,13 +883,13 @@ class App(QtCore.QObject): #self.shell.append_error("?\n") self.shell.append_error(str(e) + "\n") - def info(self, msg): + def info(self, msg, toshell=True): """ Informs the user. Normally on the status bar, optionally also on the shell. :param msg: Text to write. - :param toshell: Forward the + :param toshell: Forward the meesage to the shell. :return: None """ @@ -894,12 +900,15 @@ class App(QtCore.QObject): msg_ = match.group(2) self.ui.fcinfo.set_status(QtCore.QString(msg_), level=level) - error = level == "error" or level == "warning" - self.shell_message(msg, error=error, show=True) + if toshell: + error = level == "error" or level == "warning" + self.shell_message(msg, error=error, show=True) else: self.ui.fcinfo.set_status(QtCore.QString(msg), level="info") - self.shell_message(msg) + + if toshell: + self.shell_message(msg) def load_defaults(self): """ @@ -981,6 +990,11 @@ class App(QtCore.QObject): this is, updates the GUI accordingly, any other records and plots it. This method is thread-safe. + Notes: + * If the name is in use, the self.collection will modify it + when appending it to the collection. There is no need to handle + name conflicts here. + :param kind: The kind of object to create. One of 'gerber', 'excellon', 'cncjob' and 'geometry'. :type kind: str @@ -998,19 +1012,6 @@ class App(QtCore.QObject): t0 = time.time() # Debug - ### Check for existing name - # while name in self.collection.get_names(): - # ## Create a new name - # # Ends with number? - # App.log.debug("new_object(): Object name (%s) exists, changing." % name) - # match = re.search(r'(.*[^\d])?(\d+)$', name) - # if match: # Yes: Increment the number! - # base = match.group(1) or '' - # num = int(match.group(2)) - # name = base + str(num + 1) - # else: # No: add a number! - # name += "_1" - ## Create object classdict = { "gerber": FlatCAMGerber, diff --git a/FlatCAMGUI.py b/FlatCAMGUI.py index 908152e6..10a2820e 100644 --- a/FlatCAMGUI.py +++ b/FlatCAMGUI.py @@ -746,7 +746,21 @@ class GeometryOptionsGroupUI(OptionsGroupUI): ) grid2.addWidget(marginlabel, 2, 0) self.paintmargin_entry = LengthEntry() - grid2.addWidget(self.paintmargin_entry) + grid2.addWidget(self.paintmargin_entry, 2, 1) + + # Polygon selection + selectlabel = QtGui.QLabel('Selection:') + selectlabel.setToolTip( + "How to select the polygons to paint." + ) + grid2.addWidget(selectlabel, 3, 0) + # grid3 = QtGui.QGridLayout() + self.selectmethod_combo = RadioSet([ + {"label": "Single", "value": "single"}, + {"label": "All", "value": "all"}, + # {"label": "Rectangle", "value": "rectangle"} + ]) + grid2.addWidget(self.selectmethod_combo, 3, 1) class CNCJobOptionsGroupUI(OptionsGroupUI): diff --git a/FlatCAMObj.py b/FlatCAMObj.py index 0b1152d7..76f018ca 100644 --- a/FlatCAMObj.py +++ b/FlatCAMObj.py @@ -1203,7 +1203,8 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): "paintmargin": 0.01, "paintmethod": "standard", "multidepth": False, - "depthperpass": 0.002 + "depthperpass": 0.002, + "selectmethod": "single" }) # Attributes to be included in serialization @@ -1234,7 +1235,8 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): "paintmargin": self.ui.paintmargin_entry, "paintmethod": self.ui.paintmethod_combo, "multidepth": self.ui.mpass_cb, - "depthperpass": self.ui.maxdepth_entry + "depthperpass": self.ui.maxdepth_entry, + "selectmethod": self.ui.selectmethod_combo }) self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click) @@ -1244,24 +1246,38 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): def on_paint_button_click(self, *args): self.app.report_usage("geometry_on_paint_button") - self.app.info("Click inside the desired polygon.") self.read_form() tooldia = self.options["painttooldia"] overlap = self.options["paintoverlap"] - # Connection ID for the click event - subscription = None + if self.options["selectmethod"] == "all": + self.paint_poly_all(tooldia, overlap) + return - # To be called after clicking on the plot. - def doit(event): - self.app.info("Painting polygon...") - self.app.plotcanvas.mpl_disconnect(subscription) - point = [event.xdata, event.ydata] - self.paint_poly(point, tooldia, overlap) + if self.options["selectmethod"] == "single": + self.app.info("Click inside the desired polygon.") - subscription = self.app.plotcanvas.mpl_connect('button_press_event', doit) + # To be called after clicking on the plot. + def doit(event): + self.app.info("Painting polygon...") + self.app.plotcanvas.mpl_disconnect(subscription) + point = [event.xdata, event.ydata] + self.paint_poly_single_click(point, tooldia, overlap) - def paint_poly(self, inside_pt, tooldia, overlap): + subscription = self.app.plotcanvas.mpl_connect('button_press_event', doit) + + def paint_poly_single_click(self, inside_pt, tooldia, overlap, outname=None): + """ + Paints a polygon selected by clicking on its interior. + + Note: + * The margin is taken directly from the form. + + :param inside_pt: [x, y] + :param tooldia: Diameter of the painting tool + :param overlap: Overlap of the tool between passes. + :return: None + """ # Which polygon. #poly = find_polygon(self.solid_geometry, inside_pt) @@ -1275,7 +1291,7 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): proc = self.app.proc_container.new("Painting polygon.") - name = self.options["name"] + "_paint" + name = outname or self.options["name"] + "_paint" # Initializes the new geometry object def gen_paintarea(geo_obj, app_obj): @@ -1293,6 +1309,73 @@ class FlatCAMGeometry(FlatCAMObj, Geometry): geo_obj.solid_geometry = list(cp.get_objects()) geo_obj.options["cnctooldia"] = tooldia + + # Experimental... + print "Indexing...", + geo_obj.make_index() + print "Done" + + self.app.inform.emit("Done.") + + def job_thread(app_obj): + try: + app_obj.new_object("geometry", name, gen_paintarea) + except Exception as e: + proc.done() + raise e + proc.done() + + self.app.inform.emit("Polygon Paint started ...") + + # Promise object with the new name + self.app.collection.promise(name) + + # Background + self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]}) + + def paint_poly_all(self, tooldia, overlap, outname=None): + + proc = self.app.proc_container.new("Painting polygon.") + + name = outname or self.options["name"] + "_paint" + + def recurse(geo): + try: + for subg in geo: + for subsubg in recurse(subg): + yield subsubg + except TypeError: + if isinstance(geo, Polygon): + yield geo + + raise StopIteration + + # Initializes the new geometry object + def gen_paintarea(geo_obj, app_obj): + assert isinstance(geo_obj, FlatCAMGeometry), \ + "Initializer expected a FlatCAMGeometry, got %s" % type(geo_obj) + + geo_obj.solid_geometry = [] + + for poly in recurse(self.solid_geometry): + + if self.options["paintmethod"] == "seed": + cp = self.clear_polygon2(poly.buffer(-self.options["paintmargin"]), + tooldia, overlap=overlap) + + else: + cp = self.clear_polygon(poly.buffer(-self.options["paintmargin"]), + tooldia, overlap=overlap) + + geo_obj.solid_geometry += list(cp.get_objects()) + + geo_obj.options["cnctooldia"] = tooldia + + # Experimental... + print "Indexing...", + geo_obj.make_index() + print "Done" + self.app.inform.emit("Done.") def job_thread(app_obj): diff --git a/ObjectUI.py b/ObjectUI.py index 61b4b009..7e3d341d 100644 --- a/ObjectUI.py +++ b/ObjectUI.py @@ -397,6 +397,20 @@ class GeometryObjectUI(ObjectUI): ]) grid2.addWidget(self.paintmethod_combo, 3, 1) + # Polygon selection + selectlabel = QtGui.QLabel('Selection:') + selectlabel.setToolTip( + "How to select the polygons to paint." + ) + grid2.addWidget(selectlabel, 4, 0) + #grid3 = QtGui.QGridLayout() + self.selectmethod_combo = RadioSet([ + {"label": "Single", "value": "single"}, + {"label": "All", "value": "all"}, + #{"label": "Rectangle", "value": "rectangle"} + ]) + grid2.addWidget(self.selectmethod_combo, 4, 1) + # GO Button self.generate_paint_button = QtGui.QPushButton('Generate') self.generate_paint_button.setToolTip( diff --git a/camlib.py b/camlib.py index 6e66d48d..e65ef0fc 100644 --- a/camlib.py +++ b/camlib.py @@ -92,6 +92,16 @@ class Geometry(object): # Flattened geometry (list of paths only) self.flat_geometry = [] + # Index + self.index = None + + def make_index(self): + self.flatten() + self.index = FlatCAMRTree() + + for i, g in enumerate(self.flat_geometry): + self.index.insert(i, g) + def add_circle(self, origin, radius): """ Adds a circle to the object. diff --git a/tclCommands/TclCommandPaint.py b/tclCommands/TclCommandPaint.py new file mode 100644 index 00000000..41a226a5 --- /dev/null +++ b/tclCommands/TclCommandPaint.py @@ -0,0 +1,84 @@ +from ObjectCollection import * +import TclCommand + + +class TclCommandPaint(TclCommand.TclCommandSignaled): + """ + Paint the interior of polygons + """ + + # Array of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon) + aliases = ['paint'] + + # dictionary of types from Tcl command, needs to be ordered + arg_names = collections.OrderedDict([ + ('name', str), + ('tooldia', float), + ('overlap', float) + ]) + + # dictionary of types from Tcl command, needs to be ordered , this is for options like -optionname value + option_types = collections.OrderedDict([ + ('outname', str), + ('all', bool), + ('x', float), + ('y', float) + ]) + + # array of mandatory options for current Tcl command: required = {'name','outname'} + required = ['name', 'tooldia', 'overlap'] + + # structured help for current command, args needs to be ordered + help = { + 'main': "Paint polygons", + 'args': collections.OrderedDict([ + ('name', 'Name of the source Geometry object.'), + ('tooldia', 'Diameter of the tool to be used.'), + ('overlap', 'Fraction of the tool diameter to overlap cuts.'), + ('outname', 'Name of the resulting Geometry object.'), + ('all', 'Paint all polygons in the object.'), + ('x', 'X value of coordinate for the selection of a single polygon.'), + ('y', 'Y value of coordinate for the selection of a single polygon.') + ]), + 'examples': [] + } + + def execute(self, args, unnamed_args): + """ + execute current TCL shell command + + :param args: array of known named arguments and options + :param unnamed_args: array of other values which were passed into command + without -somename and we do not have them in known arg_names + :return: None or exception + """ + + name = args['name'] + tooldia = args['tooldia'] + overlap = args['overlap'] + + if 'outname' in args: + outname = args['outname'] + else: + outname = name + "_paint" + + obj = self.app.collection.get_by_name(name) + if obj is None: + self.raise_tcl_error("Object not found: %s" % name) + + if not isinstance(obj, Geometry): + self.raise_tcl_error('Expected Geometry, got %s %s.' % (name, type(obj))) + + if 'all' in args and args['all']: + obj.paint_poly_all(tooldia, overlap, outname) + return + + if 'x' not in args or 'y' not in args: + self.raise_tcl_error('Expected -all 1 or -x and -y .') + + x = args['x'] + y = args['y'] + + obj.paint_poly_single_click([x, y], tooldia, overlap, outname) + + diff --git a/tclCommands/__init__.py b/tclCommands/__init__.py index 47e65b42..858fc3c9 100644 --- a/tclCommands/__init__.py +++ b/tclCommands/__init__.py @@ -13,6 +13,7 @@ import tclCommands.TclCommandInteriors import tclCommands.TclCommandIsolate import tclCommands.TclCommandNew import tclCommands.TclCommandOpenGerber +import tclCommands.TclCommandPaint __all__ = []