# ########################################################## ## # FlatCAM: 2D Post-processing for Manufacturing # # http://flatcam.org # # Author: Juan Pablo Caram (c) # # Date: 2/5/2014 # # MIT Licence # # ########################################################## ## import copy import inspect # TODO: For debugging only. from datetime import datetime from flatcamGUI.ObjectUI import * from FlatCAMCommon import LoudDict from flatcamGUI.PlotCanvasLegacy import ShapeCollectionLegacy from camlib import * import itertools import gettext import FlatCAMTranslation as fcTranslate import builtins fcTranslate.apply_language('strings') if '_' not in builtins.__dict__: _ = gettext.gettext # Interrupts plotting process if FlatCAMObj has been deleted class ObjectDeleted(Exception): pass class ValidationError(Exception): def __init__(self, message, errors): super().__init__(message) self.errors = errors # ####################################### # # FlatCAMObj ## # ####################################### class FlatCAMObj(QtCore.QObject): """ Base type of objects handled in FlatCAM. These become interactive in the GUI, can be plotted, and their options can be modified by the user in their respective forms. """ # Instance of the application to which these are related. # The app should set this value. app = None # signal to plot a single object plot_single_object = pyqtSignal() def __init__(self, name): """ Constructor. :param name: Name of the object given by the user. :return: FlatCAMObj """ QtCore.QObject.__init__(self) # View self.ui = None self.options = LoudDict(name=name) self.options.set_change_callback(self.on_options_change) self.form_fields = {} # store here the default data for Geometry Data self.default_data = {} # 2D mode # Axes must exist and be attached to canvas. self.axes = None self.kind = None # Override with proper name # self.shapes = ShapeCollection(parent=self.app.plotcanvas.view.scene) if self.app.is_legacy is False: self.shapes = self.app.plotcanvas.new_shape_group() else: self.shapes = ShapeCollectionLegacy(obj=self, app=self.app) # self.mark_shapes = self.app.plotcanvas.new_shape_collection(layers=2) self.mark_shapes = {} self.item = None # Link with project view item self.muted_ui = False self.deleted = False try: self._drawing_tolerance = float(self.app.defaults["global_tolerance"]) if \ self.app.defaults["global_tolerance"] else 0.01 except ValueError: self._drawing_tolerance = 0.01 self.isHovering = False self.notHovering = True # self.units = 'IN' self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper() self.plot_single_object.connect(self.single_object_plot) # assert isinstance(self.ui, ObjectUI) # self.ui.name_entry.returnPressed.connect(self.on_name_activate) # self.ui.offset_button.clicked.connect(self.on_offset_button_click) # self.ui.scale_button.clicked.connect(self.on_scale_button_click) def __del__(self): pass def __str__(self): return "".format(self.kind, self.options["name"]) def from_dict(self, d): """ This supersedes ``from_dict`` in derived classes. Derived classes must inherit from FlatCAMObj first, then from derivatives of Geometry. ``self.options`` is only updated, not overwritten. This ensures that options set by the app do not vanish when reading the objects from a project file. :param d: Dictionary with attributes to set. :return: None """ for attr in self.ser_attrs: if attr == 'options': self.options.update(d[attr]) else: try: setattr(self, attr, d[attr]) except KeyError: log.debug("FlatCAMObj.from_dict() --> KeyError: %s. " "Means that we are loading an old project that don't" "have all attributes in the latest FlatCAM." % str(attr)) pass def on_options_change(self, key): # Update form on programmatically options change self.set_form_item(key) # Set object visibility if key == 'plot': self.visible = self.options['plot'] self.optionChanged.emit(key) def set_ui(self, ui): self.ui = ui self.form_fields = {"name": self.ui.name_entry} assert isinstance(self.ui, ObjectUI) self.ui.name_entry.returnPressed.connect(self.on_name_activate) self.ui.offset_button.clicked.connect(self.on_offset_button_click) self.ui.scale_button.clicked.connect(self.on_scale_button_click) self.ui.offsetvector_entry.returnPressed.connect(self.on_offset_button_click) self.ui.scale_entry.returnPressed.connect(self.on_scale_button_click) # self.ui.skew_button.clicked.connect(self.on_skew_button_click) def build_ui(self): """ Sets up the UI/form for this object. Show the UI in the App. :return: None :rtype: None """ self.muted_ui = True FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> FlatCAMObj.build_ui()") # Remove anything else in the box # box_children = self.app.ui.notebook.selected_contents.get_children() # for child in box_children: # self.app.ui.notebook.selected_contents.remove(child) # while self.app.ui.selected_layout.count(): # self.app.ui.selected_layout.takeAt(0) # Put in the UI # box_selected.pack_start(sw, True, True, 0) # self.app.ui.notebook.selected_contents.add(self.ui) # self.app.ui.selected_layout.addWidget(self.ui) try: self.app.ui.selected_scroll_area.takeWidget() except Exception as e: self.app.log.debug("FlatCAMObj.build_ui() --> Nothing to remove: %s" % str(e)) self.app.ui.selected_scroll_area.setWidget(self.ui) self.muted_ui = False def on_name_activate(self, silent=None): old_name = copy(self.options["name"]) new_name = self.ui.name_entry.get_value() if new_name != old_name: # update the SHELL auto-completer model data try: self.app.myKeywords.remove(old_name) self.app.myKeywords.append(new_name) self.app.shell._edit.set_model_data(self.app.myKeywords) self.app.ui.code_editor.set_model_data(self.app.myKeywords) except Exception as e: log.debug("on_name_activate() --> Could not remove the old object name from auto-completer model list") self.options["name"] = self.ui.name_entry.get_value() self.default_data["name"] = self.ui.name_entry.get_value() self.app.collection.update_view() if silent: self.app.inform.emit('[success] %s: %s %s: %s' % ( _("Name changed from"), str(old_name), _("to"), str(new_name) ) ) def on_offset_button_click(self): self.app.report_usage("obj_on_offset_button") self.read_form() vector_val = self.ui.offsetvector_entry.get_value() def worker_task(): with self.app.proc_container.new(_("Offsetting...")): self.offset(vector_val) self.app.proc_container.update_view_text('') with self.app.proc_container.new('%s...' % _("Plotting")): self.plot() self.app.object_changed.emit(self) self.app.worker_task.emit({'fcn': worker_task, 'params': []}) def on_scale_button_click(self): self.app.report_usage("obj_on_scale_button") self.read_form() factor = self.ui.scale_entry.get_value() def worker_task(): with self.app.proc_container.new(_("Scaling...")): self.scale(factor) self.app.proc_container.update_view_text('') with self.app.proc_container.new('%s...' % _("Plotting")): self.plot() self.app.object_changed.emit(self) self.app.worker_task.emit({'fcn': worker_task, 'params': []}) def on_skew_button_click(self): self.app.report_usage("obj_on_skew_button") self.read_form() x_angle = self.ui.xangle_entry.get_value() y_angle = self.ui.yangle_entry.get_value() def worker_task(): with self.app.proc_container.new(_("Skewing...")): self.skew(x_angle, y_angle) self.app.proc_container.update_view_text('') with self.app.proc_container.new('%s...' % _("Plotting")): self.plot() self.app.object_changed.emit(self) self.app.worker_task.emit({'fcn': worker_task, 'params': []}) def to_form(self): """ Copies options to the UI form. :return: None """ FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + " --> FlatCAMObj.to_form()") for option in self.options: try: self.set_form_item(option) except Exception as e: self.app.log.warning("Unexpected error:", sys.exc_info()) def read_form(self): """ Reads form into ``self.options``. :return: None :rtype: None """ FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> FlatCAMObj.read_form()") for option in self.options: try: self.read_form_item(option) except Exception as e: self.app.log.warning("Unexpected error:", sys.exc_info()) def set_form_item(self, option): """ Copies the specified option to the UI form. :param option: Name of the option (Key in ``self.options``). :type option: str :return: None """ try: self.form_fields[option].set_value(self.options[option]) except KeyError: # self.app.log.warn("Tried to set an option or field that does not exist: %s" % option) pass def read_form_item(self, option): """ Reads the specified option from the UI form into ``self.options``. :param option: Name of the option. :type option: str :return: None """ try: self.options[option] = self.form_fields[option].get_value() except KeyError: self.app.log.warning("Failed to read option from field: %s" % option) def plot(self): """ Plot this object (Extend this method to implement the actual plotting). Call this in descendants before doing the plotting. :return: Whether to continue plotting or not depending on the "plot" option. :rtype: bool """ FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + " --> FlatCAMObj.plot()") if self.deleted: return False self.clear() return True def single_object_plot(self): def plot_task(): with self.app.proc_container.new('%s...' % _("Plotting")): self.plot() self.app.object_changed.emit(self) self.app.worker_task.emit({'fcn': plot_task, 'params': []}) def serialize(self): """ Returns a representation of the object as a dictionary so it can be later exported as JSON. Override this method. :return: Dictionary representing the object :rtype: dict """ return def deserialize(self, obj_dict): """ Re-builds an object from its serialized version. :param obj_dict: Dictionary representing a FlatCAMObj :type obj_dict: dict :return: None """ return def add_shape(self, **kwargs): if self.deleted: raise ObjectDeleted() else: key = self.shapes.add(tolerance=self.drawing_tolerance, **kwargs) return key def add_mark_shape(self, apid, **kwargs): if self.deleted: raise ObjectDeleted() else: key = self.mark_shapes[apid].add(tolerance=self.drawing_tolerance, **kwargs) return key @staticmethod def poly2rings(poly): return [poly.exterior] + [interior for interior in poly.interiors] @property def visible(self): return self.shapes.visible @visible.setter def visible(self, value, threaded=True): log.debug("FlatCAMObj.visible()") def worker_task(app_obj): self.shapes.visible = value if self.app.is_legacy is False: # Not all object types has annotations try: self.annotation.visible = value except Exception as e: pass if threaded is False: worker_task(app_obj=self.app) else: self.app.worker_task.emit({'fcn': worker_task, 'params': [self]}) @property def drawing_tolerance(self): self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper() tol = self._drawing_tolerance if self.units == 'MM' or not self.units else self._drawing_tolerance / 25.4 return tol @drawing_tolerance.setter def drawing_tolerance(self, value): self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper() self._drawing_tolerance = value if self.units == 'MM' or not self.units else value / 25.4 def clear(self, update=False): self.shapes.clear(update) # Not all object types has annotations try: self.annotation.clear(update) except AttributeError: pass def delete(self): # Free resources del self.ui del self.options # Set flag self.deleted = True class FlatCAMGerber(FlatCAMObj, Gerber): """ Represents Gerber code. """ optionChanged = QtCore.pyqtSignal(str) replotApertures = QtCore.pyqtSignal() ui_type = GerberObjectUI def merge(self, grb_list, grb_final): """ Merges the geometry of objects in geo_list into the geometry of geo_final. :param grb_list: List of FlatCAMGerber Objects to join. :param grb_final: Destination FlatCAMGeometry object. :return: None """ if grb_final.solid_geometry is None: grb_final.solid_geometry = [] grb_final.follow_geometry = [] if not grb_final.apertures: grb_final.apertures = {} if type(grb_final.solid_geometry) is not list: grb_final.solid_geometry = [grb_final.solid_geometry] grb_final.follow_geometry = [grb_final.follow_geometry] for grb in grb_list: # Expand lists if type(grb) is list: FlatCAMGerber.merge(grb, grb_final) else: # If not list, just append for option in grb.options: if option is not 'name': try: grb_final.options[option] = grb.options[option] except KeyError: log.warning("Failed to copy option.", option) try: for geos in grb.solid_geometry: grb_final.solid_geometry.append(geos) grb_final.follow_geometry.append(geos) except TypeError: grb_final.solid_geometry.append(grb.solid_geometry) grb_final.follow_geometry.append(grb.solid_geometry) for ap in grb.apertures: if ap not in grb_final.apertures: grb_final.apertures[ap] = grb.apertures[ap] else: # create a list of integers out of the grb.apertures keys and find the max of that value # then, the aperture duplicate is assigned an id value incremented with 1, # and finally made string because the apertures dict keys are strings max_ap = str(max([int(k) for k in grb_final.apertures.keys()]) + 1) grb_final.apertures[max_ap] = {} grb_final.apertures[max_ap]['geometry'] = [] for k, v in grb.apertures[ap].items(): grb_final.apertures[max_ap][k] = deepcopy(v) grb_final.solid_geometry = MultiPolygon(grb_final.solid_geometry) grb_final.follow_geometry = MultiPolygon(grb_final.follow_geometry) def __init__(self, name): Gerber.__init__(self, steps_per_circle=int(self.app.defaults["gerber_circle_steps"])) FlatCAMObj.__init__(self, name) self.kind = "gerber" # The 'name' is already in self.options from FlatCAMObj # Automatically updates the UI self.options.update({ "plot": True, "multicolored": False, "solid": False, "isotooldia": 0.016, "isopasses": 1, "isooverlap": 0.15, "milling_type": "cl", "combine_passes": True, "noncoppermargin": 0.0, "noncopperrounded": False, "bboxmargin": 0.0, "bboxrounded": False, "aperture_display": False, "follow": False }) # type of isolation: 0 = exteriors, 1 = interiors, 2 = complete isolation (both interiors and exteriors) self.iso_type = 2 self.multigeo = False self.follow = False self.apertures_row = 0 # store the source file here self.source_file = "" # list of rows with apertures plotted self.marked_rows = [] # Attributes to be included in serialization # Always append to it because it carries contents # from predecessors. self.ser_attrs += ['options', 'kind'] def set_ui(self, ui): """ Maps options with GUI inputs. Connects GUI events to methods. :param ui: GUI object. :type ui: GerberObjectUI :return: None """ FlatCAMObj.set_ui(self, ui) FlatCAMApp.App.log.debug("FlatCAMGerber.set_ui()") self.replotApertures.connect(self.on_mark_cb_click_table) self.form_fields.update({ "plot": self.ui.plot_cb, "multicolored": self.ui.multicolored_cb, "solid": self.ui.solid_cb, "isotooldia": self.ui.iso_tool_dia_entry, "isopasses": self.ui.iso_width_entry, "isooverlap": self.ui.iso_overlap_entry, "milling_type": self.ui.milling_type_radio, "combine_passes": self.ui.combine_passes_cb, "noncoppermargin": self.ui.noncopper_margin_entry, "noncopperrounded": self.ui.noncopper_rounded_cb, "bboxmargin": self.ui.bbmargin_entry, "bboxrounded": self.ui.bbrounded_cb, "aperture_display": self.ui.aperture_table_visibility_cb, "follow": self.ui.follow_cb }) # Fill form fields only on object create self.to_form() assert isinstance(self.ui, GerberObjectUI) self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click) self.ui.solid_cb.stateChanged.connect(self.on_solid_cb_click) self.ui.multicolored_cb.stateChanged.connect(self.on_multicolored_cb_click) self.ui.generate_ext_iso_button.clicked.connect(self.on_ext_iso_button_click) self.ui.generate_int_iso_button.clicked.connect(self.on_int_iso_button_click) self.ui.generate_iso_button.clicked.connect(self.on_iso_button_click) self.ui.generate_ncc_button.clicked.connect(self.app.ncclear_tool.run) self.ui.generate_cutout_button.clicked.connect(self.app.cutout_tool.run) self.ui.generate_bb_button.clicked.connect(self.on_generatebb_button_click) self.ui.generate_noncopper_button.clicked.connect(self.on_generatenoncopper_button_click) self.ui.aperture_table_visibility_cb.stateChanged.connect(self.on_aperture_table_visibility_change) self.ui.follow_cb.stateChanged.connect(self.on_follow_cb_click) # set the model for the Area Exception comboboxes self.ui.obj_combo.setModel(self.app.collection) self.ui.obj_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex())) self.ui.obj_combo.setCurrentIndex(1) self.ui.type_obj_combo.currentIndexChanged.connect(self.on_type_obj_index_changed) # Show/Hide Advanced Options if self.app.defaults["global_app_level"] == 'b': self.ui.level.setText(_( '%s' % _('Basic') )) self.ui.apertures_table_label.hide() self.ui.aperture_table_visibility_cb.hide() self.ui.milling_type_label.hide() self.ui.milling_type_radio.hide() self.ui.generate_ext_iso_button.hide() self.ui.generate_int_iso_button.hide() self.ui.follow_cb.hide() self.ui.except_cb.setChecked(False) self.ui.except_cb.hide() else: self.ui.level.setText(_( '%s' % _('Advanced') )) if self.app.defaults["gerber_buffering"] == 'no': self.ui.create_buffer_button.show() try: self.ui.create_buffer_button.clicked.disconnect(self.on_generate_buffer) except TypeError: pass self.ui.create_buffer_button.clicked.connect(self.on_generate_buffer) else: self.ui.create_buffer_button.hide() # add the shapes storage for marking apertures if self.app.is_legacy is False: for ap_code in self.apertures: self.mark_shapes[ap_code] = self.app.plotcanvas.new_shape_collection(layers=2) else: for ap_code in self.apertures: self.mark_shapes[ap_code] = ShapeCollectionLegacy(obj=self, app=self.app, name=self.options['name'] + str(ap_code)) # set initial state of the aperture table and associated widgets self.on_aperture_table_visibility_change() self.build_ui() def on_type_obj_index_changed(self, index): obj_type = self.ui.type_obj_combo.currentIndex() self.ui.obj_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex())) self.ui.obj_combo.setCurrentIndex(0) def build_ui(self): FlatCAMObj.build_ui(self) try: # if connected, disconnect the signal from the slot on item_changed as it creates issues self.ui.apertures_table.itemChanged.disconnect() except (TypeError, AttributeError): pass self.apertures_row = 0 aper_no = self.apertures_row + 1 sort = [] for k, v in list(self.apertures.items()): sort.append(int(k)) sorted_apertures = sorted(sort) # sort = [] # for k, v in list(self.aperture_macros.items()): # sort.append(k) # sorted_macros = sorted(sort) # n = len(sorted_apertures) + len(sorted_macros) n = len(sorted_apertures) self.ui.apertures_table.setRowCount(n) for ap_code in sorted_apertures: ap_code = str(ap_code) ap_id_item = QtWidgets.QTableWidgetItem('%d' % int(self.apertures_row + 1)) ap_id_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled) self.ui.apertures_table.setItem(self.apertures_row, 0, ap_id_item) # Tool name/id ap_code_item = QtWidgets.QTableWidgetItem(ap_code) ap_code_item.setFlags(QtCore.Qt.ItemIsEnabled) ap_type_item = QtWidgets.QTableWidgetItem(str(self.apertures[ap_code]['type'])) ap_type_item.setFlags(QtCore.Qt.ItemIsEnabled) if str(self.apertures[ap_code]['type']) == 'R' or str(self.apertures[ap_code]['type']) == 'O': ap_dim_item = QtWidgets.QTableWidgetItem( '%.4f, %.4f' % (self.apertures[ap_code]['width'] * self.file_units_factor, self.apertures[ap_code]['height'] * self.file_units_factor ) ) ap_dim_item.setFlags(QtCore.Qt.ItemIsEnabled) elif str(self.apertures[ap_code]['type']) == 'P': ap_dim_item = QtWidgets.QTableWidgetItem( '%.4f, %.4f' % (self.apertures[ap_code]['diam'] * self.file_units_factor, self.apertures[ap_code]['nVertices'] * self.file_units_factor) ) ap_dim_item.setFlags(QtCore.Qt.ItemIsEnabled) else: ap_dim_item = QtWidgets.QTableWidgetItem('') ap_dim_item.setFlags(QtCore.Qt.ItemIsEnabled) try: if self.apertures[ap_code]['size'] is not None: ap_size_item = QtWidgets.QTableWidgetItem('%.4f' % float(self.apertures[ap_code]['size'] * self.file_units_factor)) else: ap_size_item = QtWidgets.QTableWidgetItem('') except KeyError: ap_size_item = QtWidgets.QTableWidgetItem('') ap_size_item.setFlags(QtCore.Qt.ItemIsEnabled) mark_item = FCCheckBox() mark_item.setLayoutDirection(QtCore.Qt.RightToLeft) # if self.ui.aperture_table_visibility_cb.isChecked(): # mark_item.setChecked(True) self.ui.apertures_table.setItem(self.apertures_row, 1, ap_code_item) # Aperture Code self.ui.apertures_table.setItem(self.apertures_row, 2, ap_type_item) # Aperture Type self.ui.apertures_table.setItem(self.apertures_row, 3, ap_size_item) # Aperture Dimensions self.ui.apertures_table.setItem(self.apertures_row, 4, ap_dim_item) # Aperture Dimensions empty_plot_item = QtWidgets.QTableWidgetItem('') empty_plot_item.setFlags(~QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled) self.ui.apertures_table.setItem(self.apertures_row, 5, empty_plot_item) self.ui.apertures_table.setCellWidget(self.apertures_row, 5, mark_item) self.apertures_row += 1 # for ap_code in sorted_macros: # ap_code = str(ap_code) # # ap_id_item = QtWidgets.QTableWidgetItem('%d' % int(self.apertures_row + 1)) # ap_id_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled) # self.ui.apertures_table.setItem(self.apertures_row, 0, ap_id_item) # Tool name/id # # ap_code_item = QtWidgets.QTableWidgetItem(ap_code) # # ap_type_item = QtWidgets.QTableWidgetItem('AM') # ap_type_item.setFlags(QtCore.Qt.ItemIsEnabled) # # mark_item = FCCheckBox() # mark_item.setLayoutDirection(QtCore.Qt.RightToLeft) # # if self.ui.aperture_table_visibility_cb.isChecked(): # # mark_item.setChecked(True) # # self.ui.apertures_table.setItem(self.apertures_row, 1, ap_code_item) # Aperture Code # self.ui.apertures_table.setItem(self.apertures_row, 2, ap_type_item) # Aperture Type # self.ui.apertures_table.setCellWidget(self.apertures_row, 5, mark_item) # # self.apertures_row += 1 self.ui.apertures_table.selectColumn(0) self.ui.apertures_table.resizeColumnsToContents() self.ui.apertures_table.resizeRowsToContents() vertical_header = self.ui.apertures_table.verticalHeader() # vertical_header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents) vertical_header.hide() self.ui.apertures_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) horizontal_header = self.ui.apertures_table.horizontalHeader() horizontal_header.setMinimumSectionSize(10) horizontal_header.setDefaultSectionSize(70) horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed) horizontal_header.resizeSection(0, 27) horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.ResizeToContents) horizontal_header.setSectionResizeMode(2, QtWidgets.QHeaderView.ResizeToContents) horizontal_header.setSectionResizeMode(3, QtWidgets.QHeaderView.ResizeToContents) horizontal_header.setSectionResizeMode(4, QtWidgets.QHeaderView.Stretch) horizontal_header.setSectionResizeMode(5, QtWidgets.QHeaderView.Fixed) horizontal_header.resizeSection(5, 17) self.ui.apertures_table.setColumnWidth(5, 17) self.ui.apertures_table.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) self.ui.apertures_table.setSortingEnabled(False) self.ui.apertures_table.setMinimumHeight(self.ui.apertures_table.getHeight()) self.ui.apertures_table.setMaximumHeight(self.ui.apertures_table.getHeight()) # update the 'mark' checkboxes state according with what is stored in the self.marked_rows list if self.marked_rows: for row in range(self.ui.apertures_table.rowCount()): try: self.ui.apertures_table.cellWidget(row, 5).set_value(self.marked_rows[row]) except IndexError: pass self.ui_connect() def ui_connect(self): for row in range(self.ui.apertures_table.rowCount()): self.ui.apertures_table.cellWidget(row, 5).clicked.connect(self.on_mark_cb_click_table) self.ui.mark_all_cb.clicked.connect(self.on_mark_all_click) def ui_disconnect(self): for row in range(self.ui.apertures_table.rowCount()): try: self.ui.apertures_table.cellWidget(row, 5).clicked.disconnect() except (TypeError, AttributeError): pass try: self.ui.mark_all_cb.clicked.disconnect(self.on_mark_all_click) except (TypeError, AttributeError): pass def on_generate_buffer(self): self.app.inform.emit('[WARNING_NOTCL] %s...' % _("Buffering solid geometry")) def buffer_task(): with self.app.proc_container.new('%s...' % _("Buffering")): if isinstance(self.solid_geometry, list): self.solid_geometry = MultiPolygon(self.solid_geometry) self.solid_geometry = self.solid_geometry.buffer(0.0000001) self.solid_geometry = self.solid_geometry.buffer(-0.0000001) self.app.inform.emit('[success] %s.' % _("Done")) self.plot_single_object.emit() self.app.worker_task.emit({'fcn': buffer_task, 'params': []}) def on_generatenoncopper_button_click(self, *args): self.app.report_usage("gerber_on_generatenoncopper_button") self.read_form() name = self.options["name"] + "_noncopper" def geo_init(geo_obj, app_obj): assert isinstance(geo_obj, FlatCAMGeometry) bounding_box = self.solid_geometry.envelope.buffer(float(self.options["noncoppermargin"])) if not self.options["noncopperrounded"]: bounding_box = bounding_box.envelope non_copper = bounding_box.difference(self.solid_geometry) geo_obj.solid_geometry = non_copper # TODO: Check for None self.app.new_object("geometry", name, geo_init) def on_generatebb_button_click(self, *args): self.app.report_usage("gerber_on_generatebb_button") self.read_form() name = self.options["name"] + "_bbox" def geo_init(geo_obj, app_obj): assert isinstance(geo_obj, FlatCAMGeometry) # Bounding box with rounded corners bounding_box = self.solid_geometry.envelope.buffer(float(self.options["bboxmargin"])) if not self.options["bboxrounded"]: # Remove rounded corners bounding_box = bounding_box.envelope geo_obj.solid_geometry = bounding_box self.app.new_object("geometry", name, geo_init) def on_ext_iso_button_click(self, *args): obj = self.app.collection.get_active() def worker_task(obj, app_obj): with self.app.proc_container.new(_("Isolating...")): if self.ui.follow_cb.get_value() is True: obj.follow_geo() # in the end toggle the visibility of the origin object so we can see the generated Geometry obj.ui.plot_cb.toggle() else: app_obj.report_usage("gerber_on_iso_button") self.read_form() self.isolate(iso_type=0) self.app.worker_task.emit({'fcn': worker_task, 'params': [obj, self.app]}) def on_int_iso_button_click(self, *args): obj = self.app.collection.get_active() def worker_task(obj, app_obj): with self.app.proc_container.new(_("Isolating...")): if self.ui.follow_cb.get_value() is True: obj.follow_geo() # in the end toggle the visibility of the origin object so we can see the generated Geometry obj.ui.plot_cb.toggle() else: app_obj.report_usage("gerber_on_iso_button") self.read_form() self.isolate(iso_type=1) self.app.worker_task.emit({'fcn': worker_task, 'params': [obj, self.app]}) def on_iso_button_click(self, *args): obj = self.app.collection.get_active() def worker_task(obj, app_obj): with self.app.proc_container.new(_("Isolating...")): if self.ui.follow_cb.get_value() is True: obj.follow_geo() # in the end toggle the visibility of the origin object so we can see the generated Geometry obj.ui.plot_cb.toggle() else: app_obj.report_usage("gerber_on_iso_button") self.read_form() self.isolate() self.app.worker_task.emit({'fcn': worker_task, 'params': [obj, self.app]}) def follow_geo(self, outname=None): """ Creates a geometry object "following" the gerber paths. :return: None """ # default_name = self.options["name"] + "_follow" # follow_name = outname or default_name if outname is None: follow_name = self.options["name"] + "_follow" else: follow_name = outname def follow_init(follow_obj, app): # Propagate options follow_obj.options["cnctooldia"] = str(self.options["isotooldia"]) follow_obj.solid_geometry = self.follow_geometry # TODO: Do something if this is None. Offer changing name? try: self.app.new_object("geometry", follow_name, follow_init) except Exception as e: return "Operation failed: %s" % str(e) def isolate(self, iso_type=None, dia=None, passes=None, overlap=None, outname=None, combine=None, milling_type=None, follow=None, plot=True): """ Creates an isolation routing geometry object in the project. :param iso_type: type of isolation to be done: 0 = exteriors, 1 = interiors and 2 = both :param dia: Tool diameter :param passes: Number of tool widths to cut :param overlap: Overlap between passes in fraction of tool diameter :param outname: Base name of the output object :return: None """ if dia is None: dia = float(self.options["isotooldia"]) if passes is None: passes = int(self.options["isopasses"]) if overlap is None: overlap = float(self.options["isooverlap"]) if combine is None: combine = self.options["combine_passes"] else: combine = bool(combine) if milling_type is None: milling_type = self.options["milling_type"] if iso_type is None: self.iso_type = 2 else: self.iso_type = iso_type base_name = self.options["name"] def generate_envelope(offset, invert, envelope_iso_type=2, follow=None, passes=0): # isolation_geometry produces an envelope that is going on the left of the geometry # (the copper features). To leave the least amount of burrs on the features # the tool needs to travel on the right side of the features (this is called conventional milling) # the first pass is the one cutting all of the features, so it needs to be reversed # the other passes overlap preceding ones and cut the left over copper. It is better for them # to cut on the right side of the left over copper i.e on the left side of the features. try: geom = self.isolation_geometry(offset, iso_type=envelope_iso_type, follow=follow, passes=passes) except Exception as e: log.debug('FlatCAMGerber.isolate().generate_envelope() --> %s' % str(e)) return 'fail' if invert: try: pl = [] for p in geom: if p is not None: if isinstance(p, Polygon): pl.append(Polygon(p.exterior.coords[::-1], p.interiors)) elif isinstance(p, LinearRing): pl.append(Polygon(p.coords[::-1])) geom = MultiPolygon(pl) except TypeError: if isinstance(geom, Polygon) and geom is not None: geom = Polygon(geom.exterior.coords[::-1], geom.interiors) elif isinstance(geom, LinearRing) and geom is not None: geom = Polygon(geom.coords[::-1]) else: log.debug("FlatCAMGerber.isolate().generate_envelope() Error --> Unexpected Geometry %s" % type(geom)) except Exception as e: log.debug("FlatCAMGerber.isolate().generate_envelope() Error --> %s" % str(e)) return 'fail' return geom # if invert: # try: # if type(geom) is MultiPolygon: # pl = [] # for p in geom: # if p is not None: # pl.append(Polygon(p.exterior.coords[::-1], p.interiors)) # geom = MultiPolygon(pl) # elif type(geom) is Polygon and geom is not None: # geom = Polygon(geom.exterior.coords[::-1], geom.interiors) # else: # log.debug("FlatCAMGerber.isolate().generate_envelope() Error --> Unexpected Geometry %s" % # type(geom)) # except Exception as e: # log.debug("FlatCAMGerber.isolate().generate_envelope() Error --> %s" % str(e)) # return 'fail' # return geom # if float(self.options["isotooldia"]) < 0: # self.options["isotooldia"] = -self.options["isotooldia"] def area_subtraction(geo): new_geometry = [] name = self.ui.obj_combo.currentText() subtractor_obj = self.app.collection.get_by_name(name) sub_union = cascaded_union(subtractor_obj.solid_geometry) try: for geo_elem in geo: if isinstance(geo_elem, Polygon): for ring in self.poly2rings(geo_elem): new_geo = ring.difference(sub_union) if new_geo and not new_geo.is_empty: new_geometry.append(new_geo) elif isinstance(geo_elem, MultiPolygon): for poly in geo_elem: for ring in self.poly2rings(poly): new_geo = ring.difference(sub_union) if new_geo and not new_geo.is_empty: new_geometry.append(new_geo) elif isinstance(geo_elem, LineString): new_geo = geo_elem.difference(sub_union) if new_geo: if not new_geo.is_empty: new_geometry.append(new_geo) elif isinstance(geo_elem, MultiLineString): for line_elem in geo_elem: new_geo = line_elem.difference(sub_union) if new_geo and not new_geo.is_empty: new_geometry.append(new_geo) except TypeError: if isinstance(geo, Polygon): for ring in self.poly2rings(geo): new_geo = ring.difference(sub_union) if new_geo: if not new_geo.is_empty: new_geometry.append(new_geo) elif isinstance(geo, LineString): new_geo = geo.difference(sub_union) if new_geo and not new_geo.is_empty: new_geometry.append(new_geo) elif isinstance(geo, MultiLineString): for line_elem in geo: new_geo = line_elem.difference(sub_union) if new_geo and not new_geo.is_empty: new_geometry.append(new_geo) return new_geometry if combine: if outname is None: if self.iso_type == 0: iso_name = base_name + "_ext_iso" elif self.iso_type == 1: iso_name = base_name + "_int_iso" else: iso_name = base_name + "_iso" else: iso_name = outname # TODO: This is ugly. Create way to pass data into init function. def iso_init(geo_obj, app_obj): # Propagate options geo_obj.options["cnctooldia"] = str(self.options["isotooldia"]) geo_obj.solid_geometry = [] for i in range(passes): iso_offset = dia * ((2 * i + 1) / 2.0) - (i * overlap * dia) # if milling type is climb then the move is counter-clockwise around features if milling_type == 'cl': # geom = generate_envelope (offset, i == 0) geom = generate_envelope(iso_offset, 1, envelope_iso_type=self.iso_type, follow=follow, passes=i) else: geom = generate_envelope(iso_offset, 0, envelope_iso_type=self.iso_type, follow=follow, passes=i) if geom == 'fail': app_obj.inform.emit('[ERROR_NOTCL] %s' % _("Isolation geometry could not be generated.")) return 'fail' geo_obj.solid_geometry.append(geom) # store here the default data for Geometry Data default_data = {} default_data.update({ "name": iso_name, "plot": self.app.defaults['geometry_plot'], "cutz": self.app.defaults['geometry_cutz'], "vtipdia": self.app.defaults['geometry_vtipdia'], "vtipangle": self.app.defaults['geometry_vtipangle'], "travelz": self.app.defaults['geometry_travelz'], "feedrate": self.app.defaults['geometry_feedrate'], "feedrate_z": self.app.defaults['geometry_feedrate_z'], "feedrate_rapid": self.app.defaults['geometry_feedrate_rapid'], "dwell": self.app.defaults['geometry_dwell'], "dwelltime": self.app.defaults['geometry_dwelltime'], "multidepth": self.app.defaults['geometry_multidepth'], "ppname_g": self.app.defaults['geometry_ppname_g'], "depthperpass": self.app.defaults['geometry_depthperpass'], "extracut": self.app.defaults['geometry_extracut'], "toolchange": self.app.defaults['geometry_toolchange'], "toolchangez": self.app.defaults['geometry_toolchangez'], "endz": self.app.defaults['geometry_endz'], "spindlespeed": self.app.defaults['geometry_spindlespeed'], "toolchangexy": self.app.defaults['geometry_toolchangexy'], "startz": self.app.defaults['geometry_startz'] }) geo_obj.tools = dict() geo_obj.tools['1'] = dict() geo_obj.tools.update({ '1': { 'tooldia': float(self.options["isotooldia"]), 'offset': 'Path', 'offset_value': 0.0, 'type': _('Rough'), 'tool_type': 'C1', 'data': default_data, 'solid_geometry': geo_obj.solid_geometry } }) # detect if solid_geometry is empty and this require list flattening which is "heavy" # or just looking in the lists (they are one level depth) and if any is not empty # proceed with object creation, if there are empty and the number of them is the length # of the list then we have an empty solid_geometry which should raise a Custom Exception empty_cnt = 0 if not isinstance(geo_obj.solid_geometry, list): geo_obj.solid_geometry = [geo_obj.solid_geometry] for g in geo_obj.solid_geometry: if g: break else: empty_cnt += 1 if empty_cnt == len(geo_obj.solid_geometry): raise ValidationError("Empty Geometry", None) else: app_obj.inform.emit('[success] %s" %s' % (_("Isolation geometry created"), geo_obj.options["name"])) # even if combine is checked, one pass is still single-geo geo_obj.multigeo = True if passes > 1 else False # ############################################################ # ########## AREA SUBTRACTION ################################ # ############################################################ if self.ui.except_cb.get_value(): self.app.proc_container.update_view_text(' %s' % _("Subtracting Geo")) geo_obj.solid_geometry = area_subtraction(geo_obj.solid_geometry) # TODO: Do something if this is None. Offer changing name? self.app.new_object("geometry", iso_name, iso_init, plot=plot) else: for i in range(passes): offset = dia * ((2 * i + 1) / 2.0) - (i * overlap * dia) if passes > 1: if outname is None: if self.iso_type == 0: iso_name = base_name + "_ext_iso" + str(i + 1) elif self.iso_type == 1: iso_name = base_name + "_int_iso" + str(i + 1) else: iso_name = base_name + "_iso" + str(i + 1) else: iso_name = outname else: if outname is None: if self.iso_type == 0: iso_name = base_name + "_ext_iso" elif self.iso_type == 1: iso_name = base_name + "_int_iso" else: iso_name = base_name + "_iso" else: iso_name = outname # TODO: This is ugly. Create way to pass data into init function. def iso_init(geo_obj, app_obj): # Propagate options geo_obj.options["cnctooldia"] = str(self.options["isotooldia"]) # if milling type is climb then the move is counter-clockwise around features if milling_type == 'cl': # geo_obj.solid_geometry = generate_envelope(offset, i == 0) geom = generate_envelope(offset, 1, envelope_iso_type=self.iso_type, follow=follow, passes=i) else: geom = generate_envelope(offset, 0, envelope_iso_type=self.iso_type, follow=follow, passes=i) if geom == 'fail': app_obj.inform.emit('[ERROR_NOTCL] %s' % _("Isolation geometry could not be generated.")) return 'fail' geo_obj.solid_geometry = geom # detect if solid_geometry is empty and this require list flattening which is "heavy" # or just looking in the lists (they are one level depth) and if any is not empty # proceed with object creation, if there are empty and the number of them is the length # of the list then we have an empty solid_geometry which should raise a Custom Exception empty_cnt = 0 if not isinstance(geo_obj.solid_geometry, list): geo_obj.solid_geometry = [geo_obj.solid_geometry] for g in geo_obj.solid_geometry: if g: break else: empty_cnt += 1 if empty_cnt == len(geo_obj.solid_geometry): raise ValidationError("Empty Geometry", None) else: app_obj.inform.emit('[success] %s: %s' % (_("Isolation geometry created"), geo_obj.options["name"])) geo_obj.multigeo = False # ############################################################ # ########## AREA SUBTRACTION ################################ # ############################################################ if self.ui.except_cb.get_value(): self.app.proc_container.update_view_text(' %s' % _("Subtracting Geo")) geo_obj.solid_geometry = area_subtraction(geo_obj.solid_geometry) # TODO: Do something if this is None. Offer changing name? self.app.new_object("geometry", iso_name, iso_init, plot=plot) def on_plot_cb_click(self, *args): if self.muted_ui: return self.read_form_item('plot') self.plot() def on_solid_cb_click(self, *args): if self.muted_ui: return self.read_form_item('solid') self.plot() def on_multicolored_cb_click(self, *args): if self.muted_ui: return self.read_form_item('multicolored') self.plot() def on_follow_cb_click(self): if self.muted_ui: return self.plot() def on_aperture_table_visibility_change(self): if self.ui.aperture_table_visibility_cb.isChecked(): self.ui.apertures_table.setVisible(True) for ap in self.mark_shapes: self.mark_shapes[ap].enabled = True self.ui.mark_all_cb.setVisible(True) self.ui.mark_all_cb.setChecked(False) else: self.ui.apertures_table.setVisible(False) self.ui.mark_all_cb.setVisible(False) # on hide disable all mark plots for row in range(self.ui.apertures_table.rowCount()): self.ui.apertures_table.cellWidget(row, 5).set_value(False) self.clear_plot_apertures() for ap in self.mark_shapes: self.mark_shapes[ap].enabled = False def convert_units(self, units): """ Converts the units of the object by scaling dimensions in all geometry and options. :param units: Units to which to convert the object: "IN" or "MM". :type units: str :return: None :rtype: None """ log.debug("FlatCAMObj.FlatCAMGerber.convert_units()") factor = Gerber.convert_units(self, units) self.options['isotooldia'] = float(self.options['isotooldia']) * factor self.options['bboxmargin'] = float(self.options['bboxmargin']) * factor def plot(self, **kwargs): """ :param kwargs: color and face_color :return: """ FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + " --> FlatCAMGerber.plot()") # Does all the required setup and returns False # if the 'ptint' option is set to False. if not FlatCAMObj.plot(self): return if 'color' in kwargs: color = kwargs['color'] else: color = self.app.defaults['global_plot_line'] if 'face_color' in kwargs: face_color = kwargs['face_color'] else: face_color = self.app.defaults['global_plot_fill'] if 'visible' not in kwargs: visible = self.options['plot'] else: visible = kwargs['visible'] # if the Follow Geometry checkbox is checked then plot only the follow geometry if self.ui.follow_cb.get_value(): geometry = self.follow_geometry else: geometry = self.solid_geometry # Make sure geometry is iterable. try: __ = iter(geometry) except TypeError: geometry = [geometry] # if self.app.is_legacy is False: def random_color(): color = np.random.rand(4) color[3] = 1 return color try: if self.options["solid"]: for g in geometry: if type(g) == Polygon or type(g) == LineString: self.add_shape(shape=g, color=color, face_color=random_color() if self.options['multicolored'] else face_color, visible=visible) elif type(g) == Point: pass else: try: for el in g: self.add_shape(shape=el, color=color, face_color=random_color() if self.options['multicolored'] else face_color, visible=visible) except TypeError: self.add_shape(shape=g, color=color, face_color=random_color() if self.options['multicolored'] else face_color, visible=visible) else: for g in geometry: if type(g) == Polygon or type(g) == LineString: self.add_shape(shape=g, color=random_color() if self.options['multicolored'] else 'black', visible=visible) elif type(g) == Point: pass else: for el in g: self.add_shape(shape=el, color=random_color() if self.options['multicolored'] else 'black', visible=visible) self.shapes.redraw() except (ObjectDeleted, AttributeError): self.shapes.clear(update=True) # experimental plot() when the solid_geometry is stored in the self.apertures def plot_aperture(self, **kwargs): """ :param kwargs: color and face_color :return: """ FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + " --> FlatCAMGerber.plot_aperture()") # Does all the required setup and returns False # if the 'ptint' option is set to False. if not FlatCAMObj.plot(self): return # for marking apertures, line color and fill color are the same if 'color' in kwargs: color = kwargs['color'] else: color = self.app.defaults['global_plot_fill'] if 'marked_aperture' not in kwargs: return else: aperture_to_plot_mark = kwargs['marked_aperture'] if aperture_to_plot_mark is None: return if 'visible' not in kwargs: visibility = True else: visibility = kwargs['visible'] with self.app.proc_container.new(_("Plotting Apertures")) as proc: self.app.progress.emit(30) def job_thread(app_obj): self.app.progress.emit(30) try: if aperture_to_plot_mark in self.apertures: for elem in self.apertures[aperture_to_plot_mark]['geometry']: if 'solid' in elem: geo = elem['solid'] if type(geo) == Polygon or type(geo) == LineString: self.add_mark_shape(apid=aperture_to_plot_mark, shape=geo, color=color, face_color=color, visible=visibility) else: for el in geo: self.add_mark_shape(apid=aperture_to_plot_mark, shape=el, color=color, face_color=color, visible=visibility) self.mark_shapes[aperture_to_plot_mark].redraw() self.app.progress.emit(100) except (ObjectDeleted, AttributeError): self.clear_plot_apertures() self.app.worker_task.emit({'fcn': job_thread, 'params': [self]}) def clear_plot_apertures(self, aperture='all'): """ :param aperture: string; aperture for which to clear the mark shapes :return: """ if aperture == 'all': for apid in self.apertures: self.mark_shapes[apid].clear(update=True) else: self.mark_shapes[aperture].clear(update=True) def clear_mark_all(self): self.ui.mark_all_cb.set_value(False) self.marked_rows[:] = [] def on_mark_cb_click_table(self): """ Will mark aperture geometries on canvas or delete the markings depending on the checkbox state :return: """ self.ui_disconnect() cw = self.sender() try: cw_index = self.ui.apertures_table.indexAt(cw.pos()) cw_row = cw_index.row() except AttributeError: cw_row = 0 except TypeError: return self.marked_rows[:] = [] try: aperture = self.ui.apertures_table.item(cw_row, 1).text() except AttributeError: return if self.ui.apertures_table.cellWidget(cw_row, 5).isChecked(): self.marked_rows.append(True) # self.plot_aperture(color='#2d4606bf', marked_aperture=aperture, visible=True) self.plot_aperture(color=self.app.defaults['global_sel_draw_color'], marked_aperture=aperture, visible=True) self.mark_shapes[aperture].redraw() else: self.marked_rows.append(False) self.clear_plot_apertures(aperture=aperture) # make sure that the Mark All is disabled if one of the row mark's are disabled and # if all the row mark's are enabled also enable the Mark All checkbox cb_cnt = 0 total_row = self.ui.apertures_table.rowCount() for row in range(total_row): if self.ui.apertures_table.cellWidget(row, 5).isChecked(): cb_cnt += 1 else: cb_cnt -= 1 if cb_cnt < total_row: self.ui.mark_all_cb.setChecked(False) else: self.ui.mark_all_cb.setChecked(True) self.ui_connect() def on_mark_all_click(self, signal): self.ui_disconnect() mark_all = self.ui.mark_all_cb.isChecked() for row in range(self.ui.apertures_table.rowCount()): # update the mark_rows list if mark_all: self.marked_rows.append(True) else: self.marked_rows[:] = [] mark_cb = self.ui.apertures_table.cellWidget(row, 5) mark_cb.setChecked(mark_all) if mark_all: for aperture in self.apertures: # self.plot_aperture(color='#2d4606bf', marked_aperture=aperture, visible=True) self.plot_aperture(color=self.app.defaults['global_sel_draw_color'], marked_aperture=aperture, visible=True) # HACK: enable/disable the grid for a better look self.app.ui.grid_snap_btn.trigger() self.app.ui.grid_snap_btn.trigger() else: self.clear_plot_apertures() self.ui_connect() def export_gerber(self, whole, fract, g_zeros='L', factor=1): """ Creates a Gerber file content to be exported to a file. :param whole: how many digits in the whole part of coordinates :param fract: how many decimals in coordinates :param g_zeros: type of the zero suppression used: LZ or TZ; string :param factor: factor to be applied onto the Gerber coordinates :return: Gerber_code """ log.debug("FlatCAMGerber.export_gerber() --> Generating the Gerber code from the selected Gerber file") def tz_format(x, y, fac): x_c = x * fac y_c = y * fac x_form = "{:.{dec}f}".format(x_c, dec=fract) y_form = "{:.{dec}f}".format(y_c, dec=fract) # extract whole part and decimal part x_form = x_form.partition('.') y_form = y_form.partition('.') # left padd the 'whole' part with zeros x_whole = x_form[0].rjust(whole, '0') y_whole = y_form[0].rjust(whole, '0') # restore the coordinate padded in the left with 0 and added the decimal part # without the decinal dot x_form = x_whole + x_form[2] y_form = y_whole + y_form[2] return x_form, y_form def lz_format(x, y, fac): x_c = x * fac y_c = y * fac x_form = "{:.{dec}f}".format(x_c, dec=fract).replace('.', '') y_form = "{:.{dec}f}".format(y_c, dec=fract).replace('.', '') # pad with rear zeros x_form.ljust(length, '0') y_form.ljust(length, '0') return x_form, y_form # Gerber code is stored here gerber_code = '' # apertures processing try: length = whole + fract if '0' in self.apertures: if 'geometry' in self.apertures['0']: for geo_elem in self.apertures['0']['geometry']: if 'solid' in geo_elem: geo = geo_elem['solid'] if not geo.is_empty: gerber_code += 'G36*\n' geo_coords = list(geo.exterior.coords) # first command is a move with pen-up D02 at the beginning of the geo if g_zeros == 'T': x_formatted, y_formatted = tz_format(geo_coords[0][0], geo_coords[0][1], factor) gerber_code += "X{xform}Y{yform}D02*\n".format(xform=x_formatted, yform=y_formatted) else: x_formatted, y_formatted = lz_format(geo_coords[0][0], geo_coords[0][1], factor) gerber_code += "X{xform}Y{yform}D02*\n".format(xform=x_formatted, yform=y_formatted) for coord in geo_coords[1:]: if g_zeros == 'T': x_formatted, y_formatted = tz_format(coord[0], coord[1], factor) gerber_code += "X{xform}Y{yform}D01*\n".format(xform=x_formatted, yform=y_formatted) else: x_formatted, y_formatted = lz_format(coord[0], coord[1], factor) gerber_code += "X{xform}Y{yform}D01*\n".format(xform=x_formatted, yform=y_formatted) gerber_code += 'D02*\n' gerber_code += 'G37*\n' clear_list = list(geo.interiors) if clear_list: gerber_code += '%LPC*%\n' for clear_geo in clear_list: gerber_code += 'G36*\n' geo_coords = list(clear_geo.coords) # first command is a move with pen-up D02 at the beginning of the geo if g_zeros == 'T': x_formatted, y_formatted = tz_format( geo_coords[0][0], geo_coords[0][1], factor) gerber_code += "X{xform}Y{yform}D02*\n".format(xform=x_formatted, yform=y_formatted) else: x_formatted, y_formatted = lz_format( geo_coords[0][0], geo_coords[0][1], factor) gerber_code += "X{xform}Y{yform}D02*\n".format(xform=x_formatted, yform=y_formatted) prev_coord = geo_coords[0] for coord in geo_coords[1:]: if coord != prev_coord: if g_zeros == 'T': x_formatted, y_formatted = tz_format(coord[0], coord[1], factor) gerber_code += "X{xform}Y{yform}D01*\n".format(xform=x_formatted, yform=y_formatted) else: x_formatted, y_formatted = lz_format(coord[0], coord[1], factor) gerber_code += "X{xform}Y{yform}D01*\n".format(xform=x_formatted, yform=y_formatted) prev_coord = coord gerber_code += 'D02*\n' gerber_code += 'G37*\n' gerber_code += '%LPD*%\n' if 'clear' in geo_elem: geo = geo_elem['clear'] if not geo.is_empty: gerber_code += '%LPC*%\n' gerber_code += 'G36*\n' geo_coords = list(geo.exterior.coords) # first command is a move with pen-up D02 at the beginning of the geo if g_zeros == 'T': x_formatted, y_formatted = tz_format(geo_coords[0][0], geo_coords[0][1], factor) gerber_code += "X{xform}Y{yform}D02*\n".format(xform=x_formatted, yform=y_formatted) else: x_formatted, y_formatted = lz_format(geo_coords[0][0], geo_coords[0][1], factor) gerber_code += "X{xform}Y{yform}D02*\n".format(xform=x_formatted, yform=y_formatted) prev_coord = geo_coords[0] for coord in geo_coords[1:]: if coord != prev_coord: if g_zeros == 'T': x_formatted, y_formatted = tz_format(coord[0], coord[1], factor) gerber_code += "X{xform}Y{yform}D01*\n".format(xform=x_formatted, yform=y_formatted) else: x_formatted, y_formatted = lz_format(coord[0], coord[1], factor) gerber_code += "X{xform}Y{yform}D01*\n".format(xform=x_formatted, yform=y_formatted) prev_coord = coord gerber_code += 'D02*\n' gerber_code += 'G37*\n' gerber_code += '%LPD*%\n' for apid in self.apertures: if apid == '0': continue else: gerber_code += 'D%s*\n' % str(apid) if 'geometry' in self.apertures[apid]: for geo_elem in self.apertures[apid]['geometry']: if 'follow' in geo_elem: geo = geo_elem['follow'] if not geo.is_empty: if isinstance(geo, Point): if g_zeros == 'T': x_formatted, y_formatted = tz_format(geo.x, geo.y, factor) gerber_code += "X{xform}Y{yform}D03*\n".format(xform=x_formatted, yform=y_formatted) else: x_formatted, y_formatted = lz_format(geo.x, geo.y, factor) gerber_code += "X{xform}Y{yform}D03*\n".format(xform=x_formatted, yform=y_formatted) else: geo_coords = list(geo.coords) # first command is a move with pen-up D02 at the beginning of the geo if g_zeros == 'T': x_formatted, y_formatted = tz_format( geo_coords[0][0], geo_coords[0][1], factor) gerber_code += "X{xform}Y{yform}D02*\n".format(xform=x_formatted, yform=y_formatted) else: x_formatted, y_formatted = lz_format( geo_coords[0][0], geo_coords[0][1], factor) gerber_code += "X{xform}Y{yform}D02*\n".format(xform=x_formatted, yform=y_formatted) prev_coord = geo_coords[0] for coord in geo_coords[1:]: if coord != prev_coord: if g_zeros == 'T': x_formatted, y_formatted = tz_format(coord[0], coord[1], factor) gerber_code += "X{xform}Y{yform}D01*\n".format(xform=x_formatted, yform=y_formatted) else: x_formatted, y_formatted = lz_format(coord[0], coord[1], factor) gerber_code += "X{xform}Y{yform}D01*\n".format(xform=x_formatted, yform=y_formatted) prev_coord = coord # gerber_code += "D02*\n" if 'clear' in geo_elem: gerber_code += '%LPC*%\n' geo = geo_elem['clear'] if not geo.is_empty: if isinstance(geo, Point): if g_zeros == 'T': x_formatted, y_formatted = tz_format(geo.x, geo.y, factor) gerber_code += "X{xform}Y{yform}D03*\n".format(xform=x_formatted, yform=y_formatted) else: x_formatted, y_formatted = lz_format(geo.x, geo.y, factor) gerber_code += "X{xform}Y{yform}D03*\n".format(xform=x_formatted, yform=y_formatted) else: geo_coords = list(geo.coords) # first command is a move with pen-up D02 at the beginning of the geo if g_zeros == 'T': x_formatted, y_formatted = tz_format( geo_coords[0][0], geo_coords[0][1], factor) gerber_code += "X{xform}Y{yform}D02*\n".format(xform=x_formatted, yform=y_formatted) else: x_formatted, y_formatted = lz_format( geo_coords[0][0], geo_coords[0][1], factor) gerber_code += "X{xform}Y{yform}D02*\n".format(xform=x_formatted, yform=y_formatted) prev_coord = geo_coords[0] for coord in geo_coords[1:]: if coord != prev_coord: if g_zeros == 'T': x_formatted, y_formatted = tz_format(coord[0], coord[1], factor) gerber_code += "X{xform}Y{yform}D01*\n".format(xform=x_formatted, yform=y_formatted) else: x_formatted, y_formatted = lz_format(coord[0], coord[1], factor) gerber_code += "X{xform}Y{yform}D01*\n".format(xform=x_formatted, yform=y_formatted) prev_coord = coord # gerber_code += "D02*\n" gerber_code += '%LPD*%\n' except Exception as e: log.debug("FlatCAMObj.FlatCAMGerber.export_gerber() --> %s" % str(e)) if not self.apertures: log.debug("FlatCAMObj.FlatCAMGerber.export_gerber() --> Gerber Object is empty: no apertures.") return 'fail' return gerber_code def mirror(self, axis, point): Gerber.mirror(self, axis=axis, point=point) self.replotApertures.emit() def offset(self, vect): Gerber.offset(self, vect=vect) self.replotApertures.emit() def rotate(self, angle, point): Gerber.rotate(self, angle=angle, point=point) self.replotApertures.emit() def scale(self, xfactor, yfactor=None, point=None): Gerber.scale(self, xfactor=xfactor, yfactor=yfactor, point=point) self.replotApertures.emit() def skew(self, angle_x, angle_y, point): Gerber.skew(self, angle_x=angle_x, angle_y=angle_y, point=point) self.replotApertures.emit() def serialize(self): return { "options": self.options, "kind": self.kind } class FlatCAMExcellon(FlatCAMObj, Excellon): """ Represents Excellon/Drill code. """ ui_type = ExcellonObjectUI optionChanged = QtCore.pyqtSignal(str) def __init__(self, name): Excellon.__init__(self, geo_steps_per_circle=int(self.app.defaults["geometry_circle_steps"])) FlatCAMObj.__init__(self, name) self.kind = "excellon" self.options.update({ "plot": True, "solid": False, "drillz": -0.1, "travelz": 0.1, "feedrate": 5.0, "feedrate_rapid": 5.0, "tooldia": 0.1, "slot_tooldia": 0.1, "toolchange": False, "toolchangez": 1.0, "toolchangexy": "0.0, 0.0", "endz": 2.0, "startz": None, "spindlespeed": None, "dwell": True, "dwelltime": 1000, "ppname_e": 'defaults', "z_pdepth": -0.02, "feedrate_probe": 3.0, "optimization_type": "R", "gcode_type": "drills" }) # TODO: Document this. self.tool_cbs = {} # dict to hold the tool number as key and tool offset as value self.tool_offset = {} # variable to store the total amount of drills per job self.tot_drill_cnt = 0 self.tool_row = 0 # variable to store the total amount of slots per job self.tot_slot_cnt = 0 self.tool_row_slots = 0 # variable to store the distance travelled self.travel_distance = 0.0 # store the source file here self.source_file = "" self.multigeo = False # Attributes to be included in serialization # Always append to it because it carries contents # from predecessors. self.ser_attrs += ['options', 'kind'] def merge(self, exc_list, exc_final): """ Merge Excellon objects found in exc_list parameter into exc_final object. Options are always copied from source . Tools are disregarded, what is taken in consideration is the unique drill diameters found as values in the exc_list tools dict's. In the reconstruction section for each unique tool diameter it will be created a tool_name to be used in the final Excellon object, exc_final. If only one object is in exc_list parameter then this function will copy that object in the exc_final :param exc_list: List or one object of FlatCAMExcellon Objects to join. :param exc_final: Destination FlatCAMExcellon object. :return: None """ # flag to signal that we need to reorder the tools dictionary and drills and slots lists flag_order = False try: flattened_list = list(itertools.chain(*exc_list)) except TypeError: flattened_list = exc_list # this dict will hold the unique tool diameters found in the exc_list objects as the dict keys and the dict # values will be list of Shapely Points; for drills custom_dict_drills = {} # this dict will hold the unique tool diameters found in the exc_list objects as the dict keys and the dict # values will be list of Shapely Points; for slots custom_dict_slots = {} for exc in flattened_list: # copy options of the current excellon obj to the final excellon obj for option in exc.options: if option is not 'name': try: exc_final.options[option] = exc.options[option] except Exception as e: exc.app.log.warning("Failed to copy option.", option) for drill in exc.drills: exc_tool_dia = float('%.4f' % exc.tools[drill['tool']]['C']) if exc_tool_dia not in custom_dict_drills: custom_dict_drills[exc_tool_dia] = [drill['point']] else: custom_dict_drills[exc_tool_dia].append(drill['point']) for slot in exc.slots: exc_tool_dia = float('%.4f' % exc.tools[slot['tool']]['C']) if exc_tool_dia not in custom_dict_slots: custom_dict_slots[exc_tool_dia] = [[slot['start'], slot['stop']]] else: custom_dict_slots[exc_tool_dia].append([slot['start'], slot['stop']]) # add the zeros and units to the exc_final object exc_final.zeros = exc.zeros exc_final.units = exc.units # ########################################## # Here we add data to the exc_final object # # ########################################## # variable to make tool_name for the tools current_tool = 0 # The tools diameter are now the keys in the drill_dia dict and the values are the Shapely Points in case of # drills for tool_dia in custom_dict_drills: # we create a tool name for each key in the drill_dia dict (the key is a unique drill diameter) current_tool += 1 tool_name = str(current_tool) spec = {"C": float(tool_dia)} exc_final.tools[tool_name] = spec # rebuild the drills list of dict's that belong to the exc_final object for point in custom_dict_drills[tool_dia]: exc_final.drills.append( { "point": point, "tool": str(current_tool) } ) # The tools diameter are now the keys in the drill_dia dict and the values are a list ([start, stop]) # of two Shapely Points in case of slots for tool_dia in custom_dict_slots: # we create a tool name for each key in the slot_dia dict (the key is a unique slot diameter) # but only if there are no drills if not exc_final.tools: current_tool += 1 tool_name = str(current_tool) spec = {"C": float(tool_dia)} exc_final.tools[tool_name] = spec else: dia_list = [] for v in exc_final.tools.values(): dia_list.append(float(v["C"])) if tool_dia not in dia_list: flag_order = True current_tool = len(dia_list) + 1 tool_name = str(current_tool) spec = {"C": float(tool_dia)} exc_final.tools[tool_name] = spec else: for k, v in exc_final.tools.items(): if v["C"] == tool_dia: current_tool = int(k) break # rebuild the slots list of dict's that belong to the exc_final object for point in custom_dict_slots[tool_dia]: exc_final.slots.append( { "start": point[0], "stop": point[1], "tool": str(current_tool) } ) # flag_order == True means that there was an slot diameter not in the tools and we also have drills # and the new tool was added to self.tools therefore we need to reorder the tools and drills and slots current_tool = 0 if flag_order is True: dia_list = [] temp_drills = [] temp_slots = [] temp_tools = {} for v in exc_final.tools.values(): dia_list.append(float(v["C"])) dia_list.sort() for ordered_dia in dia_list: current_tool += 1 tool_name_temp = str(current_tool) spec_temp = {"C": float(ordered_dia)} temp_tools[tool_name_temp] = spec_temp for drill in exc_final.drills: exc_tool_dia = float('%.4f' % exc_final.tools[drill['tool']]['C']) if exc_tool_dia == ordered_dia: temp_drills.append( { "point": drill["point"], "tool": str(current_tool) } ) for slot in exc_final.slots: slot_tool_dia = float('%.4f' % exc_final.tools[slot['tool']]['C']) if slot_tool_dia == ordered_dia: temp_slots.append( { "start": slot["start"], "stop": slot["stop"], "tool": str(current_tool) } ) # delete the exc_final tools, drills and slots exc_final.tools = dict() exc_final.drills[:] = [] exc_final.slots[:] = [] # update the exc_final tools, drills and slots with the ordered values exc_final.tools = temp_tools exc_final.drills[:] = temp_drills exc_final.slots[:] = temp_slots # create the geometry for the exc_final object exc_final.create_geometry() def build_ui(self): FlatCAMObj.build_ui(self) self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper() try: # if connected, disconnect the signal from the slot on item_changed as it creates issues self.ui.tools_table.itemChanged.disconnect() except (TypeError, AttributeError): pass n = len(self.tools) # we have (n+2) rows because there are 'n' tools, each a row, plus the last 2 rows for totals. self.ui.tools_table.setRowCount(n + 2) self.tot_drill_cnt = 0 self.tot_slot_cnt = 0 self.tool_row = 0 sort = [] for k, v in list(self.tools.items()): sort.append((k, v.get('C'))) sorted_tools = sorted(sort, key=lambda t1: t1[1]) tools = [i[0] for i in sorted_tools] for tool_no in tools: drill_cnt = 0 # variable to store the nr of drills per tool slot_cnt = 0 # variable to store the nr of slots per tool # Find no of drills for the current tool for drill in self.drills: if drill['tool'] == tool_no: drill_cnt += 1 self.tot_drill_cnt += drill_cnt # Find no of slots for the current tool for slot in self.slots: if slot['tool'] == tool_no: slot_cnt += 1 self.tot_slot_cnt += slot_cnt id = QtWidgets.QTableWidgetItem('%d' % int(tool_no)) id.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled) self.ui.tools_table.setItem(self.tool_row, 0, id) # Tool name/id # Make sure that the drill diameter when in MM is with no more than 2 decimals # There are no drill bits in MM with more than 3 decimals diameter # For INCH the decimals should be no more than 3. There are no drills under 10mils if self.units == 'MM': dia = QtWidgets.QTableWidgetItem('%.2f' % (self.tools[tool_no]['C'])) else: dia = QtWidgets.QTableWidgetItem('%.4f' % (self.tools[tool_no]['C'])) dia.setFlags(QtCore.Qt.ItemIsEnabled) drill_count = QtWidgets.QTableWidgetItem('%d' % drill_cnt) drill_count.setFlags(QtCore.Qt.ItemIsEnabled) # if the slot number is zero is better to not clutter the GUI with zero's so we print a space if slot_cnt > 0: slot_count = QtWidgets.QTableWidgetItem('%d' % slot_cnt) else: slot_count = QtWidgets.QTableWidgetItem('') slot_count.setFlags(QtCore.Qt.ItemIsEnabled) try: if self.units == 'MM': t_offset = self.tool_offset[float('%.2f' % float(self.tools[tool_no]['C']))] else: t_offset = self.tool_offset[float('%.4f' % float(self.tools[tool_no]['C']))] except KeyError: t_offset = self.app.defaults['excellon_offset'] tool_offset_item = QtWidgets.QTableWidgetItem('%s' % str(t_offset)) tool_offset_item.setFlags(QtCore.Qt.ItemIsEnabled) plot_item = FCCheckBox() plot_item.setLayoutDirection(QtCore.Qt.RightToLeft) if self.ui.plot_cb.isChecked(): plot_item.setChecked(True) self.ui.tools_table.setItem(self.tool_row, 1, dia) # Diameter self.ui.tools_table.setItem(self.tool_row, 2, drill_count) # Number of drills per tool self.ui.tools_table.setItem(self.tool_row, 3, slot_count) # Number of drills per tool self.ui.tools_table.setItem(self.tool_row, 4, tool_offset_item) # Tool offset empty_plot_item = QtWidgets.QTableWidgetItem('') empty_plot_item.setFlags(~QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled) self.ui.tools_table.setItem(self.tool_row, 5, empty_plot_item) self.ui.tools_table.setCellWidget(self.tool_row, 5, plot_item) self.tool_row += 1 # add a last row with the Total number of drills empty_1 = QtWidgets.QTableWidgetItem('') empty_1.setFlags(~QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled) empty_1_1 = QtWidgets.QTableWidgetItem('') empty_1_1.setFlags(~QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled) empty_1_2 = QtWidgets.QTableWidgetItem('') empty_1_2.setFlags(~QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled) empty_1_3 = QtWidgets.QTableWidgetItem('') empty_1_3.setFlags(~QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled) label_tot_drill_count = QtWidgets.QTableWidgetItem(_('Total Drills')) tot_drill_count = QtWidgets.QTableWidgetItem('%d' % self.tot_drill_cnt) label_tot_drill_count.setFlags(QtCore.Qt.ItemIsEnabled) tot_drill_count.setFlags(QtCore.Qt.ItemIsEnabled) self.ui.tools_table.setItem(self.tool_row, 0, empty_1) self.ui.tools_table.setItem(self.tool_row, 1, label_tot_drill_count) self.ui.tools_table.setItem(self.tool_row, 2, tot_drill_count) # Total number of drills self.ui.tools_table.setItem(self.tool_row, 3, empty_1_1) self.ui.tools_table.setItem(self.tool_row, 4, empty_1_2) self.ui.tools_table.setItem(self.tool_row, 5, empty_1_3) font = QtGui.QFont() font.setBold(True) font.setWeight(75) for k in [1, 2]: self.ui.tools_table.item(self.tool_row, k).setForeground(QtGui.QColor(127, 0, 255)) self.ui.tools_table.item(self.tool_row, k).setFont(font) self.tool_row += 1 # add a last row with the Total number of slots empty_2 = QtWidgets.QTableWidgetItem('') empty_2.setFlags(~QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled) empty_2_1 = QtWidgets.QTableWidgetItem('') empty_2_1.setFlags(~QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled) empty_2_2 = QtWidgets.QTableWidgetItem('') empty_2_2.setFlags(~QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled) empty_2_3 = QtWidgets.QTableWidgetItem('') empty_2_3.setFlags(~QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled) label_tot_slot_count = QtWidgets.QTableWidgetItem(_('Total Slots')) tot_slot_count = QtWidgets.QTableWidgetItem('%d' % self.tot_slot_cnt) label_tot_slot_count.setFlags(QtCore.Qt.ItemIsEnabled) tot_slot_count.setFlags(QtCore.Qt.ItemIsEnabled) self.ui.tools_table.setItem(self.tool_row, 0, empty_2) self.ui.tools_table.setItem(self.tool_row, 1, label_tot_slot_count) self.ui.tools_table.setItem(self.tool_row, 2, empty_2_1) self.ui.tools_table.setItem(self.tool_row, 3, tot_slot_count) # Total number of slots self.ui.tools_table.setItem(self.tool_row, 4, empty_2_2) self.ui.tools_table.setItem(self.tool_row, 5, empty_2_3) for kl in [1, 2, 3]: self.ui.tools_table.item(self.tool_row, kl).setFont(font) self.ui.tools_table.item(self.tool_row, kl).setForeground(QtGui.QColor(0, 70, 255)) # sort the tool diameter column # self.ui.tools_table.sortItems(1) # all the tools are selected by default self.ui.tools_table.selectColumn(0) # self.ui.tools_table.resizeColumnsToContents() self.ui.tools_table.resizeRowsToContents() vertical_header = self.ui.tools_table.verticalHeader() # vertical_header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents) vertical_header.hide() self.ui.tools_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) horizontal_header = self.ui.tools_table.horizontalHeader() horizontal_header.setMinimumSectionSize(10) horizontal_header.setDefaultSectionSize(70) horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed) horizontal_header.resizeSection(0, 20) horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch) horizontal_header.setSectionResizeMode(2, QtWidgets.QHeaderView.ResizeToContents) horizontal_header.setSectionResizeMode(3, QtWidgets.QHeaderView.ResizeToContents) horizontal_header.setSectionResizeMode(4, QtWidgets.QHeaderView.ResizeToContents) horizontal_header.setSectionResizeMode(5, QtWidgets.QHeaderView.Fixed) horizontal_header.resizeSection(5, 17) self.ui.tools_table.setColumnWidth(5, 17) # horizontal_header.setStretchLastSection(True) # horizontal_header.setColumnWidth(2, QtWidgets.QHeaderView.ResizeToContents) # horizontal_header.setStretchLastSection(True) self.ui.tools_table.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) self.ui.tools_table.setSortingEnabled(False) self.ui.tools_table.setMinimumHeight(self.ui.tools_table.getHeight()) self.ui.tools_table.setMaximumHeight(self.ui.tools_table.getHeight()) if not self.drills: self.ui.tdlabel.hide() self.ui.tooldia_entry.hide() self.ui.generate_milling_button.hide() else: self.ui.tdlabel.show() self.ui.tooldia_entry.show() self.ui.generate_milling_button.show() if not self.slots: self.ui.stdlabel.hide() self.ui.slot_tooldia_entry.hide() self.ui.generate_milling_slots_button.hide() else: self.ui.stdlabel.show() self.ui.slot_tooldia_entry.show() self.ui.generate_milling_slots_button.show() # we reactivate the signals after the after the tool adding as we don't need to see the tool been populated self.ui.tools_table.itemChanged.connect(self.on_tool_offset_edit) self.ui_connect() def set_ui(self, ui): """ Configures the user interface for this object. Connects options to form fields. :param ui: User interface object. :type ui: ExcellonObjectUI :return: None """ FlatCAMObj.set_ui(self, ui) FlatCAMApp.App.log.debug("FlatCAMExcellon.set_ui()") self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper() self.form_fields.update({ "plot": self.ui.plot_cb, "solid": self.ui.solid_cb, "drillz": self.ui.cutz_entry, "travelz": self.ui.travelz_entry, "feedrate": self.ui.feedrate_entry, "feedrate_rapid": self.ui.feedrate_rapid_entry, "tooldia": self.ui.tooldia_entry, "slot_tooldia": self.ui.slot_tooldia_entry, "toolchange": self.ui.toolchange_cb, "toolchangez": self.ui.toolchangez_entry, "spindlespeed": self.ui.spindlespeed_entry, "dwell": self.ui.dwell_cb, "dwelltime": self.ui.dwelltime_entry, "startz": self.ui.estartz_entry, "endz": self.ui.eendz_entry, "ppname_e": self.ui.pp_excellon_name_cb, "z_pdepth": self.ui.pdepth_entry, "feedrate_probe": self.ui.feedrate_probe_entry, "gcode_type": self.ui.excellon_gcode_type_radio }) for name in list(self.app.postprocessors.keys()): # the HPGL postprocessor is only for Geometry not for Excellon job therefore don't add it if name == 'hpgl': continue self.ui.pp_excellon_name_cb.addItem(name) # Fill form fields self.to_form() # update the changes in UI depending on the selected postprocessor in Preferences # after this moment all the changes in the Posprocessor combo will be handled by the activated signal of the # self.ui.pp_excellon_name_cb combobox self.on_pp_changed() # initialize the dict that holds the tools offset t_default_offset = self.app.defaults["excellon_offset"] if not self.tool_offset: for value in self.tools.values(): if self.units == 'MM': dia = float('%.2f' % float(value['C'])) else: dia = float('%.4f' % float(value['C'])) self.tool_offset[dia] = t_default_offset # Show/Hide Advanced Options if self.app.defaults["global_app_level"] == 'b': self.ui.level.setText(_( '%s' % _('Basic') )) self.ui.tools_table.setColumnHidden(4, True) self.ui.estartz_label.hide() self.ui.estartz_entry.hide() self.ui.feedrate_rapid_label.hide() self.ui.feedrate_rapid_entry.hide() self.ui.pdepth_label.hide() self.ui.pdepth_entry.hide() self.ui.feedrate_probe_label.hide() self.ui.feedrate_probe_entry.hide() else: self.ui.level.setText(_( '%s' % _('Advanced') )) assert isinstance(self.ui, ExcellonObjectUI), \ "Expected a ExcellonObjectUI, got %s" % type(self.ui) self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click) self.ui.solid_cb.stateChanged.connect(self.on_solid_cb_click) self.ui.generate_cnc_button.clicked.connect(self.on_create_cncjob_button_click) self.ui.generate_milling_button.clicked.connect(self.on_generate_milling_button_click) self.ui.generate_milling_slots_button.clicked.connect(self.on_generate_milling_slots_button_click) self.ui.pp_excellon_name_cb.activated.connect(self.on_pp_changed) def ui_connect(self): for row in range(self.ui.tools_table.rowCount() - 2): self.ui.tools_table.cellWidget(row, 5).clicked.connect(self.on_plot_cb_click_table) self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click) def ui_disconnect(self): for row in range(self.ui.tools_table.rowCount()): try: self.ui.tools_table.cellWidget(row, 5).clicked.disconnect() except (TypeError, AttributeError): pass try: self.ui.plot_cb.stateChanged.disconnect() except (TypeError, AttributeError): pass def on_tool_offset_edit(self): # if connected, disconnect the signal from the slot on item_changed as it creates issues self.ui.tools_table.itemChanged.disconnect() # self.tools_table_exc.selectionModel().currentChanged.disconnect() self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper() self.is_modified = True row_of_item_changed = self.ui.tools_table.currentRow() if self.units == 'MM': dia = float('%.2f' % float(self.ui.tools_table.item(row_of_item_changed, 1).text())) else: dia = float('%.4f' % float(self.ui.tools_table.item(row_of_item_changed, 1).text())) current_table_offset_edited = None if self.ui.tools_table.currentItem() is not None: try: current_table_offset_edited = float(self.ui.tools_table.currentItem().text()) except ValueError: # try to convert comma to decimal point. if it's still not working error message and return try: current_table_offset_edited = float(self.ui.tools_table.currentItem().text().replace(',', '.')) self.ui.tools_table.currentItem().setText( self.ui.tools_table.currentItem().text().replace(',', '.')) except ValueError: self.app.inform.emit('[ERROR_NOTCL] %s' % _("Wrong value format entered, use a number.")) self.ui.tools_table.currentItem().setText(str(self.tool_offset[dia])) return self.tool_offset[dia] = current_table_offset_edited # we reactivate the signals after the after the tool editing self.ui.tools_table.itemChanged.connect(self.on_tool_offset_edit) def get_selected_tools_list(self): """ Returns the keys to the self.tools dictionary corresponding to the selections on the tool list in the GUI. :return: List of tools. :rtype: list """ return [str(x.text()) for x in self.ui.tools_table.selectedItems()] def get_selected_tools_table_items(self): """ Returns a list of lists, each list in the list is made out of row elements :return: List of table_tools items. :rtype: list """ table_tools_items = [] for x in self.ui.tools_table.selectedItems(): # from the columnCount we subtract a value of 1 which represent the last column (plot column) # which does not have text table_tools_items.append([self.ui.tools_table.item(x.row(), column).text() for column in range(0, self.ui.tools_table.columnCount() - 1)]) for item in table_tools_items: item[0] = str(item[0]) return table_tools_items def export_excellon(self, whole, fract, e_zeros=None, form='dec', factor=1, slot_type='routing'): """ Returns two values, first is a boolean , if 1 then the file has slots and second contain the Excellon code :return: has_slots and Excellon_code """ excellon_code = '' # store here if the file has slots, return 1 if any slots, 0 if only drills has_slots = 0 # drills processing try: if self.drills: length = whole + fract for tool in self.tools: excellon_code += 'T0%s\n' % str(tool) if int(tool) < 10 else 'T%s\n' % str(tool) for drill in self.drills: if form == 'dec' and tool == drill['tool']: drill_x = drill['point'].x * factor drill_y = drill['point'].y * factor excellon_code += "X{:.{dec}f}Y{:.{dec}f}\n".format(drill_x, drill_y, dec=fract) elif e_zeros == 'LZ' and tool == drill['tool']: drill_x = drill['point'].x * factor drill_y = drill['point'].y * factor exc_x_formatted = "{:.{dec}f}".format(drill_x, dec=fract) exc_y_formatted = "{:.{dec}f}".format(drill_y, dec=fract) # extract whole part and decimal part exc_x_formatted = exc_x_formatted.partition('.') exc_y_formatted = exc_y_formatted.partition('.') # left padd the 'whole' part with zeros x_whole = exc_x_formatted[0].rjust(whole, '0') y_whole = exc_y_formatted[0].rjust(whole, '0') # restore the coordinate padded in the left with 0 and added the decimal part # without the decinal dot exc_x_formatted = x_whole + exc_x_formatted[2] exc_y_formatted = y_whole + exc_y_formatted[2] excellon_code += "X{xform}Y{yform}\n".format(xform=exc_x_formatted, yform=exc_y_formatted) elif tool == drill['tool']: drill_x = drill['point'].x * factor drill_y = drill['point'].y * factor exc_x_formatted = "{:.{dec}f}".format(drill_x, dec=fract).replace('.', '') exc_y_formatted = "{:.{dec}f}".format(drill_y, dec=fract).replace('.', '') # pad with rear zeros exc_x_formatted.ljust(length, '0') exc_y_formatted.ljust(length, '0') excellon_code += "X{xform}Y{yform}\n".format(xform=exc_x_formatted, yform=exc_y_formatted) except Exception as e: log.debug(str(e)) # slots processing try: if self.slots: has_slots = 1 for tool in self.tools: excellon_code += 'G05\n' if int(tool) < 10: excellon_code += 'T0' + str(tool) + '\n' else: excellon_code += 'T' + str(tool) + '\n' for slot in self.slots: if form == 'dec' and tool == slot['tool']: start_slot_x = slot['start'].x * factor start_slot_y = slot['start'].y * factor stop_slot_x = slot['stop'].x * factor stop_slot_y = slot['stop'].y * factor if slot_type == 'routing': excellon_code += "G00X{:.{dec}f}Y{:.{dec}f}\nM15\n".format(start_slot_x, start_slot_y, dec=fract) excellon_code += "G01X{:.{dec}f}Y{:.{dec}f}\nM16\n".format(stop_slot_x, stop_slot_y, dec=fract) elif slot_type == 'drilling': excellon_code += "X{:.{dec}f}Y{:.{dec}f}G85X{:.{dec}f}Y{:.{dec}f}\nG05\n".format( start_slot_x, start_slot_y, stop_slot_x, stop_slot_y, dec=fract ) elif e_zeros == 'LZ' and tool == slot['tool']: start_slot_x = slot['start'].x * factor start_slot_y = slot['start'].y * factor stop_slot_x = slot['stop'].x * factor stop_slot_y = slot['stop'].y * factor start_slot_x_formatted = "{:.{dec}f}".format(start_slot_x, dec=fract).replace('.', '') start_slot_y_formatted = "{:.{dec}f}".format(start_slot_y, dec=fract).replace('.', '') stop_slot_x_formatted = "{:.{dec}f}".format(stop_slot_x, dec=fract).replace('.', '') stop_slot_y_formatted = "{:.{dec}f}".format(stop_slot_y, dec=fract).replace('.', '') # extract whole part and decimal part start_slot_x_formatted = start_slot_x_formatted.partition('.') start_slot_y_formatted = start_slot_y_formatted.partition('.') stop_slot_x_formatted = stop_slot_x_formatted.partition('.') stop_slot_y_formatted = stop_slot_y_formatted.partition('.') # left padd the 'whole' part with zeros start_x_whole = start_slot_x_formatted[0].rjust(whole, '0') start_y_whole = start_slot_y_formatted[0].rjust(whole, '0') stop_x_whole = stop_slot_x_formatted[0].rjust(whole, '0') stop_y_whole = stop_slot_y_formatted[0].rjust(whole, '0') # restore the coordinate padded in the left with 0 and added the decimal part # without the decinal dot start_slot_x_formatted = start_x_whole + start_slot_x_formatted[2] start_slot_y_formatted = start_y_whole + start_slot_y_formatted[2] stop_slot_x_formatted = stop_x_whole + stop_slot_x_formatted[2] stop_slot_y_formatted = stop_y_whole + stop_slot_y_formatted[2] if slot_type == 'routing': excellon_code += "G00X{xstart}Y{ystart}\nM15\n".format(xstart=start_slot_x_formatted, ystart=start_slot_y_formatted) excellon_code += "G01X{xstop}Y{ystop}\nM16\n".format(xstop=stop_slot_x_formatted, ystop=stop_slot_y_formatted) elif slot_type == 'drilling': excellon_code += "{xstart}Y{ystart}G85X{xstop}Y{ystop}\nG05\n".format( xstart=start_slot_x_formatted, ystart=start_slot_y_formatted, xstop=stop_slot_x_formatted, ystop=stop_slot_y_formatted ) elif tool == slot['tool']: start_slot_x = slot['start'].x * factor start_slot_y = slot['start'].y * factor stop_slot_x = slot['stop'].x * factor stop_slot_y = slot['stop'].y * factor length = whole + fract start_slot_x_formatted = "{:.{dec}f}".format(start_slot_x, dec=fract).replace('.', '') start_slot_y_formatted = "{:.{dec}f}".format(start_slot_y, dec=fract).replace('.', '') stop_slot_x_formatted = "{:.{dec}f}".format(stop_slot_x, dec=fract).replace('.', '') stop_slot_y_formatted = "{:.{dec}f}".format(stop_slot_y, dec=fract).replace('.', '') # pad with rear zeros start_slot_x_formatted.ljust(length, '0') start_slot_y_formatted.ljust(length, '0') stop_slot_x_formatted.ljust(length, '0') stop_slot_y_formatted.ljust(length, '0') if slot_type == 'routing': excellon_code += "G00X{xstart}Y{ystart}\nM15\n".format(xstart=start_slot_x_formatted, ystart=start_slot_y_formatted) excellon_code += "G01X{xstop}Y{ystop}\nM16\n".format(xstop=stop_slot_x_formatted, ystop=stop_slot_y_formatted) elif slot_type == 'drilling': excellon_code += "{xstart}Y{ystart}G85X{xstop}Y{ystop}\nG05\n".format( xstart=start_slot_x_formatted, ystart=start_slot_y_formatted, xstop=stop_slot_x_formatted, ystop=stop_slot_y_formatted ) except Exception as e: log.debug(str(e)) if not self.drills and not self.slots: log.debug("FlatCAMObj.FlatCAMExcellon.export_excellon() --> Excellon Object is empty: no drills, no slots.") return 'fail' return has_slots, excellon_code def generate_milling_drills(self, tools=None, outname=None, tooldia=None, plot=False, use_thread=False): """ Note: This method is a good template for generic operations as it takes it's options from parameters or otherwise from the object's options and returns a (success, msg) tuple as feedback for shell operations. :return: Success/failure condition tuple (bool, str). :rtype: tuple """ # Get the tools from the list. These are keys # to self.tools if tools is None: tools = self.get_selected_tools_list() if outname is None: outname = self.options["name"] + "_mill" if tooldia is None: tooldia = float(self.options["tooldia"]) # Sort tools by diameter. items() -> [('name', diameter), ...] # sorted_tools = sorted(list(self.tools.items()), key=lambda tl: tl[1]) # no longer works in Python3 sort = [] for k, v in self.tools.items(): sort.append((k, v.get('C'))) sorted_tools = sorted(sort, key=lambda t1: t1[1]) if tools == "all": tools = [i[0] for i in sorted_tools] # List if ordered tool names. log.debug("Tools 'all' and sorted are: %s" % str(tools)) if len(tools) == 0: self.app.inform.emit('[ERROR_NOTCL] %s' % _("Please select one or more tools from the list and try again.")) return False, "Error: No tools." for tool in tools: if tooldia > self.tools[tool]["C"]: self.app.inform.emit('[ERROR_NOTCL] %s %s: %s' % ( _("Milling tool for DRILLS is larger than hole size. Cancelled.", str(tool)) )) return False, "Error: Milling tool is larger than hole." def geo_init(geo_obj, app_obj): assert isinstance(geo_obj, FlatCAMGeometry), \ "Initializer expected a FlatCAMGeometry, got %s" % type(geo_obj) app_obj.progress.emit(20) # ## Add properties to the object # get the tool_table items in a list of row items tool_table_items = self.get_selected_tools_table_items() # insert an information only element in the front tool_table_items.insert(0, [_("Tool_nr"), _("Diameter"), _("Drills_Nr"), _("Slots_Nr")]) geo_obj.options['Tools_in_use'] = tool_table_items geo_obj.options['type'] = 'Excellon Geometry' geo_obj.options["cnctooldia"] = str(tooldia) geo_obj.solid_geometry = [] # in case that the tool used has the same diameter with the hole, and since the maximum resolution # for FlatCAM is 6 decimals, # we add a tenth of the minimum value, meaning 0.0000001, which from our point of view is "almost zero" for hole in self.drills: if hole['tool'] in tools: buffer_value = self.tools[hole['tool']]["C"] / 2 - tooldia / 2 if buffer_value == 0: geo_obj.solid_geometry.append( Point(hole['point']).buffer(0.0000001).exterior) else: geo_obj.solid_geometry.append( Point(hole['point']).buffer(buffer_value).exterior) if use_thread: def geo_thread(app_obj): app_obj.new_object("geometry", outname, geo_init, plot=plot) app_obj.progress.emit(100) # Create a promise with the new name self.app.collection.promise(outname) # Send to worker self.app.worker_task.emit({'fcn': geo_thread, 'params': [self.app]}) else: self.app.new_object("geometry", outname, geo_init, plot=plot) return True, "" def generate_milling_slots(self, tools=None, outname=None, tooldia=None, plot=True, use_thread=False): """ Note: This method is a good template for generic operations as it takes it's options from parameters or otherwise from the object's options and returns a (success, msg) tuple as feedback for shell operations. :return: Success/failure condition tuple (bool, str). :rtype: tuple """ # Get the tools from the list. These are keys # to self.tools if tools is None: tools = self.get_selected_tools_list() if outname is None: outname = self.options["name"] + "_mill" if tooldia is None: tooldia = float(self.options["slot_tooldia"]) # Sort tools by diameter. items() -> [('name', diameter), ...] # sorted_tools = sorted(list(self.tools.items()), key=lambda tl: tl[1]) # no longer works in Python3 sort = [] for k, v in self.tools.items(): sort.append((k, v.get('C'))) sorted_tools = sorted(sort, key=lambda t1: t1[1]) if tools == "all": tools = [i[0] for i in sorted_tools] # List if ordered tool names. log.debug("Tools 'all' and sorted are: %s" % str(tools)) if len(tools) == 0: self.app.inform.emit('[ERROR_NOTCL] %s' % _("Please select one or more tools from the list and try again.")) return False, "Error: No tools." for tool in tools: # I add the 0.0001 value to account for the rounding error in converting from IN to MM and reverse adj_toolstable_tooldia = float('%.4f' % float(tooldia)) adj_file_tooldia = float('%.4f' % float(self.tools[tool]["C"])) if adj_toolstable_tooldia > adj_file_tooldia + 0.0001: self.app.inform.emit('[ERROR_NOTCL] %s' % _("Milling tool for SLOTS is larger than hole size. Cancelled.")) return False, "Error: Milling tool is larger than hole." def geo_init(geo_obj, app_obj): assert isinstance(geo_obj, FlatCAMGeometry), \ "Initializer expected a FlatCAMGeometry, got %s" % type(geo_obj) app_obj.progress.emit(20) # ## Add properties to the object # get the tool_table items in a list of row items tool_table_items = self.get_selected_tools_table_items() # insert an information only element in the front tool_table_items.insert(0, [_("Tool_nr"), _("Diameter"), _("Drills_Nr"), _("Slots_Nr")]) geo_obj.options['Tools_in_use'] = tool_table_items geo_obj.options['type'] = 'Excellon Geometry' geo_obj.options["cnctooldia"] = str(tooldia) geo_obj.solid_geometry = [] # in case that the tool used has the same diameter with the hole, and since the maximum resolution # for FlatCAM is 6 decimals, # we add a tenth of the minimum value, meaning 0.0000001, which from our point of view is "almost zero" for slot in self.slots: if slot['tool'] in tools: toolstable_tool = float('%.4f' % float(tooldia)) file_tool = float('%.4f' % float(self.tools[tool]["C"])) # I add the 0.0001 value to account for the rounding error in converting from IN to MM and reverse # for the file_tool (tooldia actually) buffer_value = float(file_tool / 2) - float(toolstable_tool / 2) + 0.0001 if buffer_value == 0: start = slot['start'] stop = slot['stop'] lines_string = LineString([start, stop]) poly = lines_string.buffer(0.0000001, int(self.geo_steps_per_circle)).exterior geo_obj.solid_geometry.append(poly) else: start = slot['start'] stop = slot['stop'] lines_string = LineString([start, stop]) poly = lines_string.buffer(buffer_value, int(self.geo_steps_per_circle)).exterior geo_obj.solid_geometry.append(poly) if use_thread: def geo_thread(app_obj): app_obj.new_object("geometry", outname + '_slot', geo_init, plot=plot) app_obj.progress.emit(100) # Create a promise with the new name self.app.collection.promise(outname) # Send to worker self.app.worker_task.emit({'fcn': geo_thread, 'params': [self.app]}) else: self.app.new_object("geometry", outname + '_slot', geo_init, plot=plot) return True, "" def on_generate_milling_button_click(self, *args): self.app.report_usage("excellon_on_create_milling_drills button") self.read_form() self.generate_milling_drills(use_thread=False) def on_generate_milling_slots_button_click(self, *args): self.app.report_usage("excellon_on_create_milling_slots_button") self.read_form() self.generate_milling_slots(use_thread=False) def on_pp_changed(self): current_pp = self.ui.pp_excellon_name_cb.get_value() if "toolchange_probe" in current_pp.lower(): self.ui.pdepth_entry.setVisible(True) self.ui.pdepth_label.show() self.ui.feedrate_probe_entry.setVisible(True) self.ui.feedrate_probe_label.show() else: self.ui.pdepth_entry.setVisible(False) self.ui.pdepth_label.hide() self.ui.feedrate_probe_entry.setVisible(False) self.ui.feedrate_probe_label.hide() if 'marlin' in current_pp.lower() or 'custom' in current_pp.lower(): self.ui.feedrate_rapid_label.show() self.ui.feedrate_rapid_entry.show() else: self.ui.feedrate_rapid_label.hide() self.ui.feedrate_rapid_entry.hide() def on_create_cncjob_button_click(self, *args): self.app.report_usage("excellon_on_create_cncjob_button") self.read_form() # Get the tools from the list tools = self.get_selected_tools_list() if len(tools) == 0: # if there is a single tool in the table (remember that the last 2 rows are for totals and do not count in # tool number) it means that there are 3 rows (1 tool and 2 totals). # in this case regardless of the selection status of that tool, use it. if self.ui.tools_table.rowCount() == 3: tools.append(self.ui.tools_table.item(0, 0).text()) else: self.app.inform.emit('[ERROR_NOTCL] %s' % _("Please select one or more tools from the list and try again.")) return xmin = self.options['xmin'] ymin = self.options['ymin'] xmax = self.options['xmax'] ymax = self.options['ymax'] job_name = self.options["name"] + "_cnc" pp_excellon_name = self.options["ppname_e"] # Object initialization function for app.new_object() def job_init(job_obj, app_obj): assert isinstance(job_obj, FlatCAMCNCjob), \ "Initializer expected a FlatCAMCNCjob, got %s" % type(job_obj) # get the tool_table items in a list of row items tool_table_items = self.get_selected_tools_table_items() # insert an information only element in the front tool_table_items.insert(0, [_("Tool_nr"), _("Diameter"), _("Drills_Nr"), _("Slots_Nr")]) # ## Add properties to the object job_obj.origin_kind = 'excellon' job_obj.options['Tools_in_use'] = tool_table_items job_obj.options['type'] = 'Excellon' job_obj.options['ppname_e'] = pp_excellon_name app_obj.progress.emit(20) job_obj.z_cut = float(self.options["drillz"]) job_obj.tool_offset = self.tool_offset job_obj.z_move = float(self.options["travelz"]) job_obj.feedrate = float(self.options["feedrate"]) job_obj.feedrate_rapid = float(self.options["feedrate_rapid"]) job_obj.spindlespeed = float(self.options["spindlespeed"]) if self.options["spindlespeed"] else None job_obj.spindledir = self.app.defaults['excellon_spindledir'] job_obj.dwell = self.options["dwell"] job_obj.dwelltime = float(self.options["dwelltime"]) job_obj.pp_excellon_name = pp_excellon_name job_obj.toolchange_xy_type = "excellon" job_obj.coords_decimals = int(self.app.defaults["cncjob_coords_decimals"]) job_obj.fr_decimals = int(self.app.defaults["cncjob_fr_decimals"]) job_obj.options['xmin'] = xmin job_obj.options['ymin'] = ymin job_obj.options['xmax'] = xmax job_obj.options['ymax'] = ymax try: job_obj.z_pdepth = float(self.options["z_pdepth"]) except ValueError: # try to convert comma to decimal point. if it's still not working error message and return try: job_obj.z_pdepth = float(self.options["z_pdepth"].replace(',', '.')) except ValueError: self.app.inform.emit('[ERROR_NOTCL] %s' % _('Wrong value format for self.defaults["z_pdepth"] ' 'or self.options["z_pdepth"]')) try: job_obj.feedrate_probe = float(self.options["feedrate_probe"]) except ValueError: # try to convert comma to decimal point. if it's still not working error message and return try: job_obj.feedrate_rapid = float(self.options["feedrate_probe"].replace(',', '.')) except ValueError: self.app.inform.emit('[ERROR_NOTCL] %s' % _('Wrong value format for self.defaults["feedrate_probe"] or ' 'self.options["feedrate_probe"]')) # There could be more than one drill size... # job_obj.tooldia = # TODO: duplicate variable! # job_obj.options["tooldia"] = tools_csv = ','.join(tools) ret_val = job_obj.generate_from_excellon_by_tool( self, tools_csv, drillz=float(self.options['drillz']), toolchange=self.options["toolchange"], toolchangexy=self.app.defaults["excellon_toolchangexy"], toolchangez=float(self.options["toolchangez"]), startz=float(self.options["startz"]) if self.options["startz"] else None, endz=float(self.options["endz"]), excellon_optimization_type=self.app.defaults["excellon_optimization_type"]) if ret_val == 'fail': return 'fail' app_obj.progress.emit(50) job_obj.gcode_parse() app_obj.progress.emit(60) job_obj.create_geometry() app_obj.progress.emit(80) # To be run in separate thread def job_thread(app_obj): with self.app.proc_container.new(_("Generating CNC Code")): app_obj.new_object("cncjob", job_name, job_init) app_obj.progress.emit(100) # Create promise for the new name. self.app.collection.promise(job_name) # Send to worker # self.app.worker.add_task(job_thread, [self.app]) self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]}) def convert_units(self, units): log.debug("FlatCAMObj.FlatCAMExcellon.convert_units()") factor = Excellon.convert_units(self, units) self.options['drillz'] = float(self.options['drillz']) * factor self.options['travelz'] = float(self.options['travelz']) * factor self.options['feedrate'] = float(self.options['feedrate']) * factor self.options['feedrate_rapid'] = float(self.options['feedrate_rapid']) * factor self.options['toolchangez'] = float(self.options['toolchangez']) * factor if self.app.defaults["excellon_toolchangexy"] == '': self.options['toolchangexy'] = "0.0, 0.0" else: coords_xy = [float(eval(coord)) for coord in self.app.defaults["excellon_toolchangexy"].split(",")] if len(coords_xy) < 2: self.app.inform.emit('[ERROR] %s' % _("The Toolchange X,Y field in Edit -> Preferences has to be " "in the format (x, y) \n" "but now there is only one value, not two. ")) return 'fail' coords_xy[0] *= factor coords_xy[1] *= factor self.options['toolchangexy'] = "%f, %f" % (coords_xy[0], coords_xy[1]) if self.options['startz'] is not None: self.options['startz'] = float(self.options['startz']) * factor self.options['endz'] = float(self.options['endz']) * factor def on_solid_cb_click(self, *args): if self.muted_ui: return self.read_form_item('solid') self.plot() def on_plot_cb_click(self, *args): if self.muted_ui: return self.plot() self.read_form_item('plot') self.ui_disconnect() cb_flag = self.ui.plot_cb.isChecked() for row in range(self.ui.tools_table.rowCount() - 2): table_cb = self.ui.tools_table.cellWidget(row, 5) if cb_flag: table_cb.setChecked(True) else: table_cb.setChecked(False) self.ui_connect() def on_plot_cb_click_table(self): # self.ui.cnc_tools_table.cellWidget(row, 2).widget().setCheckState(QtCore.Qt.Unchecked) self.ui_disconnect() # cw = self.sender() # cw_index = self.ui.tools_table.indexAt(cw.pos()) # cw_row = cw_index.row() check_row = 0 self.shapes.clear(update=True) for tool_key in self.tools: solid_geometry = self.tools[tool_key]['solid_geometry'] # find the geo_tool_table row associated with the tool_key for row in range(self.ui.tools_table.rowCount()): tool_item = int(self.ui.tools_table.item(row, 0).text()) if tool_item == int(tool_key): check_row = row break if self.ui.tools_table.cellWidget(check_row, 5).isChecked(): self.options['plot'] = True # self.plot_element(element=solid_geometry, visible=True) # Plot excellon (All polygons?) if self.options["solid"]: for geo in solid_geometry: self.add_shape(shape=geo, color='#750000BF', face_color='#C40000BF', visible=self.options['plot'], layer=2) else: for geo in solid_geometry: self.add_shape(shape=geo.exterior, color='red', visible=self.options['plot']) for ints in geo.interiors: self.add_shape(shape=ints, color='green', visible=self.options['plot']) self.shapes.redraw() # make sure that the general plot is disabled if one of the row plot's are disabled and # if all the row plot's are enabled also enable the general plot checkbox cb_cnt = 0 total_row = self.ui.tools_table.rowCount() for row in range(total_row - 2): if self.ui.tools_table.cellWidget(row, 5).isChecked(): cb_cnt += 1 else: cb_cnt -= 1 if cb_cnt < total_row - 2: self.ui.plot_cb.setChecked(False) else: self.ui.plot_cb.setChecked(True) self.ui_connect() # def plot_element(self, element, color='red', visible=None, layer=None): # # visible = visible if visible else self.options['plot'] # # try: # for sub_el in element: # self.plot_element(sub_el) # # except TypeError: # Element is not iterable... # self.add_shape(shape=element, color=color, visible=visible, layer=0) def plot(self, visible=None, kind=None): # Does all the required setup and returns False # if the 'ptint' option is set to False. if not FlatCAMObj.plot(self): return # try: # # Plot Excellon (All polygons?) # if self.options["solid"]: # for tool in self.tools: # for geo in self.tools[tool]['solid_geometry']: # self.add_shape(shape=geo, color='#750000BF', face_color='#C40000BF', # visible=self.options['plot'], # layer=2) # else: # for tool in self.tools: # for geo in self.tools[tool]['solid_geometry']: # self.add_shape(shape=geo.exterior, color='red', visible=self.options['plot']) # for ints in geo.interiors: # self.add_shape(shape=ints, color='orange', visible=self.options['plot']) # # self.shapes.redraw() # return # except (ObjectDeleted, AttributeError, KeyError): # self.shapes.clear(update=True) # this stays for compatibility reasons, in case we try to open old projects try: __ = iter(self.solid_geometry) except TypeError: self.solid_geometry = [self.solid_geometry] visible = visible if visible else self.options['plot'] try: # Plot Excellon (All polygons?) if self.options["solid"]: for geo in self.solid_geometry: self.add_shape(shape=geo, color='#750000BF', face_color='#C40000BF', visible=visible, layer=2) else: for geo in self.solid_geometry: self.add_shape(shape=geo.exterior, color='red', visible=visible) for ints in geo.interiors: self.add_shape(shape=ints, color='orange', visible=visible) self.shapes.redraw() except (ObjectDeleted, AttributeError): self.shapes.clear(update=True) class FlatCAMGeometry(FlatCAMObj, Geometry): """ Geometric object not associated with a specific format. """ optionChanged = QtCore.pyqtSignal(str) ui_type = GeometryObjectUI def merge(self, geo_list, geo_final, multigeo=None): """ Merges the geometry of objects in grb_list into the geometry of geo_final. :param geo_list: List of FlatCAMGerber Objects to join. :param geo_final: Destination FlatCAMGerber object. :return: None """ if geo_final.solid_geometry is None: geo_final.solid_geometry = [] if type(geo_final.solid_geometry) is not list: geo_final.solid_geometry = [geo_final.solid_geometry] for geo in geo_list: for option in geo.options: if option is not 'name': try: geo_final.options[option] = geo.options[option] except Exception as e: log.warning("Failed to copy option %s. Error: %s" % (str(option), str(e))) # Expand lists if type(geo) is list: FlatCAMGeometry.merge(self, geo_list=geo, geo_final=geo_final) # If not list, just append else: # merge solid_geometry, useful for singletool geometry, for multitool each is empty if multigeo is None or multigeo is False: geo_final.multigeo = False try: geo_final.solid_geometry.append(geo.solid_geometry) except Exception as e: log.debug("FlatCAMGeometry.merge() --> %s" % str(e)) else: geo_final.multigeo = True # if multigeo the solid_geometry is empty in the object attributes because it now lives in the # tools object attribute, as a key value geo_final.solid_geometry = [] # find the tool_uid maximum value in the geo_final geo_final_uid_list = [] for key in geo_final.tools: geo_final_uid_list.append(int(key)) try: max_uid = max(geo_final_uid_list, key=int) except ValueError: max_uid = 0 # add and merge tools. If what we try to merge as Geometry is Excellon's and/or Gerber's then don't try # to merge the obj.tools as it is likely there is none to merge. if not isinstance(geo, FlatCAMGerber) and not isinstance(geo, FlatCAMExcellon): for tool_uid in geo.tools: max_uid += 1 geo_final.tools[max_uid] = deepcopy(geo.tools[tool_uid]) @staticmethod def get_pts(o): """ Returns a list of all points in the object, where the object can be a MultiPolygon, Polygon, Not a polygon, or a list of such. Search is done recursively. :param: geometric object :return: List of points :rtype: list """ pts = [] # Iterable: descend into each item. try: for subo in o: pts += FlatCAMGeometry.get_pts(subo) # Non-iterable except TypeError: if o is not None: if type(o) == MultiPolygon: for poly in o: pts += FlatCAMGeometry.get_pts(poly) # ## Descend into .exerior and .interiors elif type(o) == Polygon: pts += FlatCAMGeometry.get_pts(o.exterior) for i in o.interiors: pts += FlatCAMGeometry.get_pts(i) elif type(o) == MultiLineString: for line in o: pts += FlatCAMGeometry.get_pts(line) # ## Has .coords: list them. else: pts += list(o.coords) else: return return pts def __init__(self, name): FlatCAMObj.__init__(self, name) Geometry.__init__(self, geo_steps_per_circle=int(self.app.defaults["geometry_circle_steps"])) self.kind = "geometry" self.options.update({ "plot": True, "cutz": -0.002, "vtipdia": 0.1, "vtipangle": 30, "travelz": 0.1, "feedrate": 5.0, "feedrate_z": 5.0, "feedrate_rapid": 5.0, "spindlespeed": None, "dwell": True, "dwelltime": 1000, "multidepth": False, "depthperpass": 0.002, "extracut": False, "endz": 2.0, "startz": None, "toolchange": False, "toolchangez": 1.0, "toolchangexy": "0.0, 0.0", "ppname_g": 'default', "z_pdepth": -0.02, "feedrate_probe": 3.0, }) if "cnctooldia" not in self.options: self.options["cnctooldia"] = self.app.defaults["geometry_cnctooldia"] self.options["startz"] = self.app.defaults["geometry_startz"] # this will hold the tool unique ID that is useful when having multiple tools with same diameter self.tooluid = 0 ''' self.tools = {} This is a dictionary. Each dict key is associated with a tool used in geo_tools_table. The key is the tool_id of the tools and the value is another dict that will hold the data under the following form: {tooluid: { 'tooldia': 1, 'offset': 'Path', 'offset_value': 0.0 'type': 'Rough', 'tool_type': 'C1', 'data': self.default_tool_data 'solid_geometry': [] } } ''' self.tools = {} # this dict is to store those elements (tools) of self.tools that are selected in the self.geo_tools_table # those elements are the ones used for generating GCode self.sel_tools = {} self.offset_item_options = ["Path", "In", "Out", "Custom"] self.type_item_options = [_("Iso"), _("Rough"), _("Finish")] self.tool_type_item_options = ["C1", "C2", "C3", "C4", "B", "V"] # flag to store if the V-Shape tool is selected in self.ui.geo_tools_table self.v_tool_type = None # flag to store if the Geometry is type 'multi-geometry' meaning that each tool has it's own geometry # the default value is False self.multigeo = False # flag to store if the geometry is part of a special group of geometries that can't be processed by the default # engine of FlatCAM. Most likely are generated by some of tools and are special cases of geometries. self. special_group = None self.old_pp_state = self.app.defaults["geometry_multidepth"] self.old_toolchangeg_state = self.app.defaults["geometry_toolchange"] # Attributes to be included in serialization # Always append to it because it carries contents # from predecessors. self.ser_attrs += ['options', 'kind', 'tools', 'multigeo'] def build_ui(self): self.ui_disconnect() FlatCAMObj.build_ui(self) self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper() offset = 0 tool_idx = 0 n = len(self.tools) self.ui.geo_tools_table.setRowCount(n) for tooluid_key, tooluid_value in self.tools.items(): tool_idx += 1 row_no = tool_idx - 1 id = QtWidgets.QTableWidgetItem('%d' % int(tool_idx)) id.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled) self.ui.geo_tools_table.setItem(row_no, 0, id) # Tool name/id # Make sure that the tool diameter when in MM is with no more than 2 decimals. # There are no tool bits in MM with more than 3 decimals diameter. # For INCH the decimals should be no more than 3. There are no tools under 10mils. if self.units == 'MM': dia_item = QtWidgets.QTableWidgetItem('%.2f' % float(tooluid_value['tooldia'])) else: dia_item = QtWidgets.QTableWidgetItem('%.4f' % float(tooluid_value['tooldia'])) dia_item.setFlags(QtCore.Qt.ItemIsEnabled) offset_item = QtWidgets.QComboBox() for item in self.offset_item_options: offset_item.addItem(item) offset_item.setStyleSheet('background-color: rgb(255,255,255)') idx = offset_item.findText(tooluid_value['offset']) offset_item.setCurrentIndex(idx) type_item = QtWidgets.QComboBox() for item in self.type_item_options: type_item.addItem(item) type_item.setStyleSheet('background-color: rgb(255,255,255)') idx = type_item.findText(tooluid_value['type']) type_item.setCurrentIndex(idx) tool_type_item = QtWidgets.QComboBox() for item in self.tool_type_item_options: tool_type_item.addItem(item) tool_type_item.setStyleSheet('background-color: rgb(255,255,255)') idx = tool_type_item.findText(tooluid_value['tool_type']) tool_type_item.setCurrentIndex(idx) tool_uid_item = QtWidgets.QTableWidgetItem(str(tooluid_key)) plot_item = FCCheckBox() plot_item.setLayoutDirection(QtCore.Qt.RightToLeft) if self.ui.plot_cb.isChecked(): plot_item.setChecked(True) self.ui.geo_tools_table.setItem(row_no, 1, dia_item) # Diameter self.ui.geo_tools_table.setCellWidget(row_no, 2, offset_item) self.ui.geo_tools_table.setCellWidget(row_no, 3, type_item) self.ui.geo_tools_table.setCellWidget(row_no, 4, tool_type_item) # ## REMEMBER: THIS COLUMN IS HIDDEN IN OBJECTUI.PY ### self.ui.geo_tools_table.setItem(row_no, 5, tool_uid_item) # Tool unique ID self.ui.geo_tools_table.setCellWidget(row_no, 6, plot_item) try: self.ui.tool_offset_entry.set_value(tooluid_value['offset_value']) except Exception as e: log.debug("build_ui() --> Could not set the 'offset_value' key in self.tools. Error: %s" % str(e)) # make the diameter column editable for row in range(tool_idx): self.ui.geo_tools_table.item(row, 1).setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsEnabled) # sort the tool diameter column # self.ui.geo_tools_table.sortItems(1) # all the tools are selected by default # self.ui.geo_tools_table.selectColumn(0) self.ui.geo_tools_table.resizeColumnsToContents() self.ui.geo_tools_table.resizeRowsToContents() vertical_header = self.ui.geo_tools_table.verticalHeader() # vertical_header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents) vertical_header.hide() self.ui.geo_tools_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) horizontal_header = self.ui.geo_tools_table.horizontalHeader() horizontal_header.setMinimumSectionSize(10) horizontal_header.setDefaultSectionSize(70) horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed) horizontal_header.resizeSection(0, 20) horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch) # horizontal_header.setColumnWidth(2, QtWidgets.QHeaderView.ResizeToContents) horizontal_header.setSectionResizeMode(3, QtWidgets.QHeaderView.ResizeToContents) horizontal_header.setSectionResizeMode(4, QtWidgets.QHeaderView.Fixed) horizontal_header.resizeSection(4, 40) horizontal_header.setSectionResizeMode(6, QtWidgets.QHeaderView.Fixed) horizontal_header.resizeSection(4, 17) # horizontal_header.setStretchLastSection(True) self.ui.geo_tools_table.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) self.ui.geo_tools_table.setColumnWidth(0, 20) self.ui.geo_tools_table.setColumnWidth(4, 40) self.ui.geo_tools_table.setColumnWidth(6, 17) # self.ui.geo_tools_table.setSortingEnabled(True) self.ui.geo_tools_table.setMinimumHeight(self.ui.geo_tools_table.getHeight()) self.ui.geo_tools_table.setMaximumHeight(self.ui.geo_tools_table.getHeight()) # update UI for all rows - useful after units conversion but only if there is at least one row row_cnt = self.ui.geo_tools_table.rowCount() if row_cnt > 0: for r in range(row_cnt): self.update_ui(r) # select only the first tool / row selected_row = 0 try: self.select_tools_table_row(selected_row, clearsel=True) # update the Geometry UI self.update_ui() except Exception as e: # when the tools table is empty there will be this error but once the table is populated it will go away log.debug(str(e)) # disable the Plot column in Tool Table if the geometry is SingleGeo as it is not needed # and can create some problems if self.multigeo is False: self.ui.geo_tools_table.setColumnHidden(6, True) else: self.ui.geo_tools_table.setColumnHidden(6, False) self.set_tool_offset_visibility(selected_row) self.ui_connect() # HACK: for whatever reasons the name in Selected tab is reverted to the original one after a successful rename # done in the collection view but only for Geometry objects. Perhaps some references remains. Should be fixed. self.ui.name_entry.set_value(self.options['name']) def set_ui(self, ui): FlatCAMObj.set_ui(self, ui) log.debug("FlatCAMGeometry.set_ui()") assert isinstance(self.ui, GeometryObjectUI), \ "Expected a GeometryObjectUI, got %s" % type(self.ui) # populate postprocessor names in the combobox for name in list(self.app.postprocessors.keys()): self.ui.pp_geometry_name_cb.addItem(name) self.form_fields.update({ "plot": self.ui.plot_cb, "cutz": self.ui.cutz_entry, "vtipdia": self.ui.tipdia_entry, "vtipangle": self.ui.tipangle_entry, "travelz": self.ui.travelz_entry, "feedrate": self.ui.cncfeedrate_entry, "feedrate_z": self.ui.cncplunge_entry, "feedrate_rapid": self.ui.cncfeedrate_rapid_entry, "spindlespeed": self.ui.cncspindlespeed_entry, "dwell": self.ui.dwell_cb, "dwelltime": self.ui.dwelltime_entry, "multidepth": self.ui.mpass_cb, "ppname_g": self.ui.pp_geometry_name_cb, "z_pdepth": self.ui.pdepth_entry, "feedrate_probe": self.ui.feedrate_probe_entry, "depthperpass": self.ui.maxdepth_entry, "extracut": self.ui.extracut_cb, "toolchange": self.ui.toolchangeg_cb, "toolchangez": self.ui.toolchangez_entry, "endz": self.ui.gendz_entry, }) # Fill form fields only on object create self.to_form() # update the changes in UI depending on the selected postprocessor in Preferences # after this moment all the changes in the Posprocessor combo will be handled by the activated signal of the # self.ui.pp_geometry_name_cb combobox self.on_pp_changed() self.ui.tipdialabel.hide() self.ui.tipdia_entry.hide() self.ui.tipanglelabel.hide() self.ui.tipangle_entry.hide() self.ui.cutz_entry.setDisabled(False) # store here the default data for Geometry Data self.default_data = {} self.default_data.update({ "name": None, "plot": None, "cutz": None, "vtipdia": None, "vtipangle": None, "travelz": None, "feedrate": None, "feedrate_z": None, "feedrate_rapid": None, "dwell": None, "dwelltime": None, "multidepth": None, "ppname_g": None, "depthperpass": None, "extracut": None, "toolchange": None, "toolchangez": None, "endz": None, "spindlespeed": None, "toolchangexy": None, "startz": None }) # fill in self.default_data values from self.options for def_key in self.default_data: for opt_key, opt_val in self.options.items(): if def_key == opt_key: self.default_data[def_key] = deepcopy(opt_val) try: temp_tools = self.options["cnctooldia"].split(",") tools_list = [ float(eval(dia)) for dia in temp_tools if dia != '' ] except Exception as e: log.error("At least one tool diameter needed. Verify in Edit -> Preferences -> Geometry General -> " "Tool dia. %s" % str(e)) return self.tooluid += 1 if not self.tools: for toold in tools_list: self.tools.update({ self.tooluid: { 'tooldia': float(toold), 'offset': 'Path', 'offset_value': 0.0, 'type': _('Rough'), 'tool_type': 'C1', 'data': self.default_data, 'solid_geometry': self.solid_geometry } }) self.tooluid += 1 else: # if self.tools is not empty then it can safely be assumed that it comes from an opened project. # Because of the serialization the self.tools list on project save, the dict keys (members of self.tools # are each a dict) are turned into strings so we rebuild the self.tools elements so the keys are # again float type; dict's don't like having keys changed when iterated through therefore the need for the # following convoluted way of changing the keys from string to float type temp_tools = {} new_key = 0.0 for tooluid_key in self.tools: val = deepcopy(self.tools[tooluid_key]) new_key = deepcopy(int(tooluid_key)) temp_tools[new_key] = val self.tools.clear() self.tools = deepcopy(temp_tools) self.ui.tool_offset_entry.hide() self.ui.tool_offset_lbl.hide() # used to store the state of the mpass_cb if the selected postproc for geometry is hpgl self.old_pp_state = self.default_data['multidepth'] self.old_toolchangeg_state = self.default_data['toolchange'] if not isinstance(self.ui, GeometryObjectUI): log.debug("Expected a GeometryObjectUI, got %s" % type(self.ui)) return self.ui.geo_tools_table.setupContextMenu() self.ui.geo_tools_table.addContextMenu( _("Copy"), self.on_tool_copy, icon=QtGui.QIcon("share/copy16.png")) self.ui.geo_tools_table.addContextMenu( _("Delete"), lambda: self.on_tool_delete(all=None), icon=QtGui.QIcon("share/delete32.png")) # Show/Hide Advanced Options if self.app.defaults["global_app_level"] == 'b': self.ui.level.setText(_( '%s' % _('Basic') )) self.ui.geo_tools_table.setColumnHidden(2, True) self.ui.geo_tools_table.setColumnHidden(3, True) # self.ui.geo_tools_table.setColumnHidden(4, True) self.ui.addtool_entry_lbl.hide() self.ui.addtool_entry.hide() self.ui.addtool_btn.hide() self.ui.copytool_btn.hide() self.ui.deltool_btn.hide() # self.ui.endzlabel.hide() # self.ui.gendz_entry.hide() self.ui.fr_rapidlabel.hide() self.ui.cncfeedrate_rapid_entry.hide() self.ui.extracut_cb.hide() self.ui.pdepth_label.hide() self.ui.pdepth_entry.hide() self.ui.feedrate_probe_label.hide() self.ui.feedrate_probe_entry.hide() else: self.ui.level.setText(_( '%s' % _('Advanced') )) self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click) self.ui.generate_cnc_button.clicked.connect(self.on_generatecnc_button_click) self.ui.paint_tool_button.clicked.connect(lambda: self.app.paint_tool.run(toggle=False)) self.ui.pp_geometry_name_cb.activated.connect(self.on_pp_changed) self.ui.addtool_entry.returnPressed.connect(lambda: self.on_tool_add()) def set_tool_offset_visibility(self, current_row): if current_row is None: return try: tool_offset = self.ui.geo_tools_table.cellWidget(current_row, 2) if tool_offset is not None: tool_offset_txt = tool_offset.currentText() if tool_offset_txt == 'Custom': self.ui.tool_offset_entry.show() self.ui.tool_offset_lbl.show() else: self.ui.tool_offset_entry.hide() self.ui.tool_offset_lbl.hide() except Exception as e: log.debug("set_tool_offset_visibility() --> " + str(e)) return def on_offset_value_edited(self): """ This will save the offset_value into self.tools storage whenever the offset value is edited :return: """ for current_row in self.ui.geo_tools_table.selectedItems(): # sometime the header get selected and it has row number -1 # we don't want to do anything with the header :) if current_row.row() < 0: continue tool_uid = int(self.ui.geo_tools_table.item(current_row.row(), 5).text()) self.set_tool_offset_visibility(current_row.row()) for tooluid_key, tooluid_value in self.tools.items(): if int(tooluid_key) == tool_uid: try: tooluid_value['offset_value'] = float(self.ui.tool_offset_entry.get_value()) except ValueError: # try to convert comma to decimal point. if it's still not working error message and return try: tooluid_value['offset_value'] = float( self.ui.tool_offset_entry.get_value().replace(',', '.') ) except ValueError: self.app.inform.emit('[ERROR_NOTCL] %s' % _("Wrong value format entered, use a number.")) return def ui_connect(self): # on any change to the widgets that matter it will be called self.gui_form_to_storage which will save the # changes in geometry UI for i in range(self.ui.grid3.count()): current_widget = self.ui.grid3.itemAt(i).widget() if isinstance(current_widget, FCCheckBox): current_widget.stateChanged.connect(self.gui_form_to_storage) elif isinstance(current_widget, FCComboBox): current_widget.currentIndexChanged.connect(self.gui_form_to_storage) elif isinstance(current_widget, FloatEntry) or isinstance(current_widget, LengthEntry) or \ isinstance(current_widget, FCEntry) or isinstance(current_widget, IntEntry): current_widget.editingFinished.connect(self.gui_form_to_storage) for row in range(self.ui.geo_tools_table.rowCount()): for col in [2, 3, 4]: self.ui.geo_tools_table.cellWidget(row, col).currentIndexChanged.connect( self.on_tooltable_cellwidget_change) # I use lambda's because the connected functions have parameters that could be used in certain scenarios self.ui.addtool_btn.clicked.connect(lambda: self.on_tool_add()) self.ui.copytool_btn.clicked.connect(lambda: self.on_tool_copy()) self.ui.deltool_btn.clicked.connect(lambda: self.on_tool_delete()) self.ui.geo_tools_table.currentItemChanged.connect(self.on_row_selection_change) self.ui.geo_tools_table.itemChanged.connect(self.on_tool_edit) self.ui.tool_offset_entry.editingFinished.connect(self.on_offset_value_edited) for row in range(self.ui.geo_tools_table.rowCount()): self.ui.geo_tools_table.cellWidget(row, 6).clicked.connect(self.on_plot_cb_click_table) self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click) def ui_disconnect(self): # on any change to the widgets that matter it will be called self.gui_form_to_storage which will save the # changes in geometry UI for i in range(self.ui.grid3.count()): current_widget = self.ui.grid3.itemAt(i).widget() if isinstance(current_widget, FCCheckBox): try: self.ui.grid3.itemAt(i).widget().stateChanged.disconnect(self.gui_form_to_storage) except (TypeError, AttributeError): pass elif isinstance(current_widget, FCComboBox): try: self.ui.grid3.itemAt(i).widget().currentIndexChanged.disconnect(self.gui_form_to_storage) except (TypeError, AttributeError): pass elif isinstance(current_widget, LengthEntry) or isinstance(current_widget, IntEntry) or \ isinstance(current_widget, FCEntry) or isinstance(current_widget, FloatEntry): try: self.ui.grid3.itemAt(i).widget().editingFinished.disconnect(self.gui_form_to_storage) except (TypeError, AttributeError): pass for row in range(self.ui.geo_tools_table.rowCount()): for col in [2, 3, 4]: try: self.ui.geo_tools_table.cellWidget(row, col).currentIndexChanged.disconnect() except (TypeError, AttributeError): pass try: self.ui.addtool_btn.clicked.disconnect() except (TypeError, AttributeError): pass try: self.ui.copytool_btn.clicked.disconnect() except (TypeError, AttributeError): pass try: self.ui.deltool_btn.clicked.disconnect() except (TypeError, AttributeError): pass try: self.ui.geo_tools_table.currentItemChanged.disconnect() except (TypeError, AttributeError): pass try: self.ui.geo_tools_table.itemChanged.disconnect() except (TypeError, AttributeError): pass try: self.ui.tool_offset_entry.editingFinished.disconnect() except (TypeError, AttributeError): pass for row in range(self.ui.geo_tools_table.rowCount()): try: self.ui.geo_tools_table.cellWidget(row, 6).clicked.disconnect() except (TypeError, AttributeError): pass try: self.ui.plot_cb.stateChanged.disconnect() except (TypeError, AttributeError): pass def on_tool_add(self, dia=None): self.ui_disconnect() self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper() # if a Tool diameter entered is a char instead a number the final message of Tool adding is changed # because the Default value for Tool is used. change_message = False if dia is not None: tooldia = dia else: try: tooldia = float(self.ui.addtool_entry.get_value()) except ValueError: # try to convert comma to decimal point. if it's still not working error message and return try: tooldia = float(self.ui.addtool_entry.get_value().replace(',', '.')) except ValueError: change_message = True tooldia = float(self.options["cnctooldia"][0]) if tooldia is None: self.build_ui() self.app.inform.emit('[ERROR_NOTCL] %s' % _("Please enter the desired tool diameter in Float format.")) return # construct a list of all 'tooluid' in the self.tools tool_uid_list = [] for tooluid_key in self.tools: tool_uid_item = int(tooluid_key) tool_uid_list.append(tool_uid_item) # find maximum from the temp_uid, add 1 and this is the new 'tooluid' if not tool_uid_list: max_uid = 0 else: max_uid = max(tool_uid_list) self.tooluid = max_uid + 1 if self.units == 'IN': tooldia = float('%.4f' % tooldia) else: tooldia = float('%.2f' % tooldia) # here we actually add the new tool; if there is no tool in the tool table we add a tool with default data # otherwise we add a tool with data copied from last tool if not self.tools: self.tools.update({ self.tooluid: { 'tooldia': tooldia, 'offset': 'Path', 'offset_value': 0.0, 'type': _('Rough'), 'tool_type': 'C1', 'data': deepcopy(self.default_data), 'solid_geometry': self.solid_geometry } }) else: last_data = self.tools[max_uid]['data'] last_offset = self.tools[max_uid]['offset'] last_offset_value = self.tools[max_uid]['offset_value'] last_type = self.tools[max_uid]['type'] last_tool_type = self.tools[max_uid]['tool_type'] last_solid_geometry = self.tools[max_uid]['solid_geometry'] # if previous geometry was empty (it may happen for the first tool added) # then copy the object.solid_geometry if not last_solid_geometry: last_solid_geometry = self.solid_geometry self.tools.update({ self.tooluid: { 'tooldia': tooldia, 'offset': last_offset, 'offset_value': last_offset_value, 'type': last_type, 'tool_type': last_tool_type, 'data': deepcopy(last_data), 'solid_geometry': deepcopy(last_solid_geometry) } }) self.tools[self.tooluid]['data']['name'] = self.options['name'] self.ui.tool_offset_entry.hide() self.ui.tool_offset_lbl.hide() # we do this HACK to make sure the tools attribute to be serialized is updated in the self.ser_attrs list try: self.ser_attrs.remove('tools') except TypeError: pass self.ser_attrs.append('tools') if change_message is False: self.app.inform.emit('[success] %s' % _("Tool added in Tool Table.")) else: change_message = False self.app.inform.emit('[WARNING_NOTCL] %s' % _("Default Tool added. Wrong value format entered.")) self.build_ui() # if there is no tool left in the Tools Table, enable the parameters GUI if self.ui.geo_tools_table.rowCount() != 0: self.ui.geo_param_frame.setDisabled(False) def on_tool_copy(self, all=None): self.ui_disconnect() # find the tool_uid maximum value in the self.tools uid_list = [] for key in self.tools: uid_list.append(int(key)) try: max_uid = max(uid_list, key=int) except ValueError: max_uid = 0 if all is None: if self.ui.geo_tools_table.selectedItems(): for current_row in self.ui.geo_tools_table.selectedItems(): # sometime the header get selected and it has row number -1 # we don't want to do anything with the header :) if current_row.row() < 0: continue try: tooluid_copy = int(self.ui.geo_tools_table.item(current_row.row(), 5).text()) self.set_tool_offset_visibility(current_row.row()) max_uid += 1 self.tools[int(max_uid)] = deepcopy(self.tools[tooluid_copy]) except AttributeError: self.app.inform.emit('[WARNING_NOTCL] %s' % _("Failed. Select a tool to copy.")) self.build_ui() return except Exception as e: log.debug("on_tool_copy() --> " + str(e)) # deselect the table # self.ui.geo_tools_table.clearSelection() else: self.app.inform.emit('[WARNING_NOTCL] %s' % _("Failed. Select a tool to copy.")) self.build_ui() return else: # we copy all tools in geo_tools_table try: temp_tools = deepcopy(self.tools) max_uid += 1 for tooluid in temp_tools: self.tools[int(max_uid)] = deepcopy(temp_tools[tooluid]) temp_tools.clear() except Exception as e: log.debug("on_tool_copy() --> " + str(e)) # if there are no more tools in geo tools table then hide the tool offset if not self.tools: self.ui.tool_offset_entry.hide() self.ui.tool_offset_lbl.hide() # we do this HACK to make sure the tools attribute to be serialized is updated in the self.ser_attrs list try: self.ser_attrs.remove('tools') except ValueError: pass self.ser_attrs.append('tools') self.build_ui() self.app.inform.emit('[success] %s' % _("Tool was copied in Tool Table.")) def on_tool_edit(self, current_item): self.ui_disconnect() current_row = current_item.row() try: d = float(self.ui.geo_tools_table.item(current_row, 1).text()) except ValueError: # try to convert comma to decimal point. if it's still not working error message and return try: d = float(self.ui.geo_tools_table.item(current_row, 1).text().replace(',', '.')) except ValueError: self.app.inform.emit('[ERROR_NOTCL] %s' % _("Wrong value format entered, use a number.")) return tool_dia = float('%.4f' % d) tooluid = int(self.ui.geo_tools_table.item(current_row, 5).text()) self.tools[tooluid]['tooldia'] = tool_dia try: self.ser_attrs.remove('tools') self.ser_attrs.append('tools') except (TypeError, ValueError): pass self.app.inform.emit('[success] %s' % _("Tool was edited in Tool Table.")) self.build_ui() def on_tool_delete(self, all=None): self.ui_disconnect() if all is None: if self.ui.geo_tools_table.selectedItems(): for current_row in self.ui.geo_tools_table.selectedItems(): # sometime the header get selected and it has row number -1 # we don't want to do anything with the header :) if current_row.row() < 0: continue try: tooluid_del = int(self.ui.geo_tools_table.item(current_row.row(), 5).text()) self.set_tool_offset_visibility(current_row.row()) temp_tools = deepcopy(self.tools) for tooluid_key in self.tools: if int(tooluid_key) == tooluid_del: # if the self.tools has only one tool and we delete it then we move the solid_geometry # as a property of the object otherwise there will be nothing to hold it if len(self.tools) == 1: self.solid_geometry = deepcopy(self.tools[tooluid_key]['solid_geometry']) temp_tools.pop(tooluid_del, None) self.tools = deepcopy(temp_tools) temp_tools.clear() except AttributeError: self.app.inform.emit('[WARNING_NOTCL] %s' % _("Failed. Select a tool to delete.")) self.build_ui() return except Exception as e: log.debug("on_tool_delete() --> " + str(e)) # deselect the table # self.ui.geo_tools_table.clearSelection() else: self.app.inform.emit('[WARNING_NOTCL] %s' % _("Failed. Select a tool to delete.")) self.build_ui() return else: # we delete all tools in geo_tools_table self.tools.clear() self.app.plot_all() # if there are no more tools in geo tools table then hide the tool offset if not self.tools: self.ui.tool_offset_entry.hide() self.ui.tool_offset_lbl.hide() # we do this HACK to make sure the tools attribute to be serialized is updated in the self.ser_attrs list try: self.ser_attrs.remove('tools') except TypeError: pass self.ser_attrs.append('tools') self.build_ui() self.app.inform.emit('[success] %s' % _("Tool was deleted in Tool Table.")) obj_active = self.app.collection.get_active() # if the object was MultiGeo and now it has no tool at all (therefore no geometry) # we make it back SingleGeo if self.ui.geo_tools_table.rowCount() <= 0: obj_active.multigeo = False obj_active.options['xmin'] = 0 obj_active.options['ymin'] = 0 obj_active.options['xmax'] = 0 obj_active.options['ymax'] = 0 if obj_active.multigeo is True: try: xmin, ymin, xmax, ymax = obj_active.bounds() obj_active.options['xmin'] = xmin obj_active.options['ymin'] = ymin obj_active.options['xmax'] = xmax obj_active.options['ymax'] = ymax except Exception as e: obj_active.options['xmin'] = 0 obj_active.options['ymin'] = 0 obj_active.options['xmax'] = 0 obj_active.options['ymax'] = 0 # if there is no tool left in the Tools Table, disable the parameters GUI if self.ui.geo_tools_table.rowCount() == 0: self.ui.geo_param_frame.setDisabled(True) def on_row_selection_change(self): self.update_ui() def update_ui(self, row=None): self.ui_disconnect() if row is None: try: current_row = self.ui.geo_tools_table.currentRow() except Exception as e: current_row = 0 else: current_row = row if current_row < 0: current_row = 0 self.set_tool_offset_visibility(current_row) # populate the form with the data from the tool associated with the row parameter try: item = self.ui.geo_tools_table.item(current_row, 5) if type(item) is not None: tooluid = int(item.text()) else: return except Exception as e: log.debug("Tool missing. Add a tool in Geo Tool Table. %s" % str(e)) return # update the form with the V-Shape fields if V-Shape selected in the geo_tool_table # also modify the Cut Z form entry to reflect the calculated Cut Z from values got from V-Shape Fields try: item = self.ui.geo_tools_table.cellWidget(current_row, 4) if item is not None: tool_type_txt = item.currentText() self.ui_update_v_shape(tool_type_txt=tool_type_txt) else: return except Exception as e: log.debug("Tool missing in ui_update_v_shape(). Add a tool in Geo Tool Table. %s" % str(e)) return try: # set the form with data from the newly selected tool for tooluid_key, tooluid_value in self.tools.items(): if int(tooluid_key) == tooluid: for key, value in tooluid_value.items(): if key == 'data': form_value_storage = tooluid_value[key] self.update_form(form_value_storage) if key == 'offset_value': # update the offset value in the entry even if the entry is hidden self.ui.tool_offset_entry.set_value(tooluid_value[key]) if key == 'tool_type' and value == 'V': self.update_cutz() except Exception as e: log.debug("FlatCAMObj ---> update_ui() " + str(e)) self.ui_connect() def ui_update_v_shape(self, tool_type_txt): if tool_type_txt == 'V': self.ui.tipdialabel.show() self.ui.tipdia_entry.show() self.ui.tipanglelabel.show() self.ui.tipangle_entry.show() self.ui.cutz_entry.setDisabled(True) self.update_cutz() else: self.ui.tipdialabel.hide() self.ui.tipdia_entry.hide() self.ui.tipanglelabel.hide() self.ui.tipangle_entry.hide() self.ui.cutz_entry.setDisabled(False) def update_cutz(self): try: vdia = float(self.ui.tipdia_entry.get_value()) except ValueError: # try to convert comma to decimal point. if it's still not working error message and return try: vdia = float(self.ui.tipdia_entry.get_value().replace(',', '.')) except ValueError: self.app.inform.emit('[ERROR_NOTCL] %s' % _("Wrong value format entered, use a number.")) return try: half_vangle = float(self.ui.tipangle_entry.get_value()) / 2 except ValueError: # try to convert comma to decimal point. if it's still not working error message and return try: half_vangle = float(self.ui.tipangle_entry.get_value().replace(',', '.')) / 2 except ValueError: self.app.inform.emit('[ERROR_NOTCL] %s' % _("Wrong value format entered, use a number.")) return row = self.ui.geo_tools_table.currentRow() tool_uid = int(self.ui.geo_tools_table.item(row, 5).text()) tooldia = float(self.ui.geo_tools_table.item(row, 1).text()) new_cutz = (tooldia - vdia) / (2 * math.tan(math.radians(half_vangle))) new_cutz = float('%.4f' % -new_cutz) # this value has to be negative self.ui.cutz_entry.set_value(new_cutz) # store the new CutZ value into storage (self.tools) for tooluid_key, tooluid_value in self.tools.items(): if int(tooluid_key) == tool_uid: tooluid_value['data']['cutz'] = new_cutz def on_tooltable_cellwidget_change(self): cw = self.sender() cw_index = self.ui.geo_tools_table.indexAt(cw.pos()) cw_row = cw_index.row() cw_col = cw_index.column() current_uid = int(self.ui.geo_tools_table.item(cw_row, 5).text()) # store the text of the cellWidget that changed it's index in the self.tools for tooluid_key, tooluid_value in self.tools.items(): if int(tooluid_key) == current_uid: cb_txt = cw.currentText() if cw_col == 2: tooluid_value['offset'] = cb_txt if cb_txt == 'Custom': self.ui.tool_offset_entry.show() self.ui.tool_offset_lbl.show() else: self.ui.tool_offset_entry.hide() self.ui.tool_offset_lbl.hide() # reset the offset_value in storage self.tools tooluid_value['offset_value'] = 0.0 elif cw_col == 3: # force toolpath type as 'Iso' if the tool type is V-Shape if self.ui.geo_tools_table.cellWidget(cw_row, 4).currentText() == 'V': tooluid_value['type'] = _('Iso') idx = self.ui.geo_tools_table.cellWidget(cw_row, 3).findText(_('Iso')) self.ui.geo_tools_table.cellWidget(cw_row, 3).setCurrentIndex(idx) else: tooluid_value['type'] = cb_txt elif cw_col == 4: tooluid_value['tool_type'] = cb_txt # if the tool_type selected is V-Shape then autoselect the toolpath type as Iso if cb_txt == 'V': idx = self.ui.geo_tools_table.cellWidget(cw_row, 3).findText(_('Iso')) self.ui.geo_tools_table.cellWidget(cw_row, 3).setCurrentIndex(idx) self.ui_update_v_shape(tool_type_txt=self.ui.geo_tools_table.cellWidget(cw_row, 4).currentText()) def update_form(self, dict_storage): for form_key in self.form_fields: for storage_key in dict_storage: if form_key == storage_key: try: self.form_fields[form_key].set_value(dict_storage[form_key]) except Exception as e: log.debug(str(e)) # this is done here because those buttons control through OptionalInputSelection if some entry's are Enabled # or not. But due of using the ui_disconnect() status is no longer updated and I had to do it here self.ui.ois_dwell_geo.on_cb_change() self.ui.ois_mpass_geo.on_cb_change() self.ui.ois_tcz_geo.on_cb_change() def gui_form_to_storage(self): if self.ui.geo_tools_table.rowCount() == 0: # there is no tool in tool table so we can't save the GUI elements values to storage log.debug("FlatCAMGeometry.gui_form_to_storage() --> no tool in Tools Table, aborting.") return self.ui_disconnect() widget_changed = self.sender() try: widget_idx = self.ui.grid3.indexOf(widget_changed) except Exception as e: return # those are the indexes for the V-Tip Dia and V-Tip Angle, if edited calculate the new Cut Z if widget_idx == 1 or widget_idx == 3: self.update_cutz() # the original connect() function of the OptionalInpuSelection is no longer working because of the # ui_diconnect() so I use this 'hack' if isinstance(widget_changed, FCCheckBox): if widget_changed.text() == 'Multi-Depth:': self.ui.ois_mpass_geo.on_cb_change() if widget_changed.text() == 'Tool change': self.ui.ois_tcz_geo.on_cb_change() if widget_changed.text() == 'Dwell:': self.ui.ois_dwell_geo.on_cb_change() row = self.ui.geo_tools_table.currentRow() if row < 0: row = 0 # store all the data associated with the row parameter to the self.tools storage tooldia_item = float(self.ui.geo_tools_table.item(row, 1).text()) offset_item = self.ui.geo_tools_table.cellWidget(row, 2).currentText() type_item = self.ui.geo_tools_table.cellWidget(row, 3).currentText() tool_type_item = self.ui.geo_tools_table.cellWidget(row, 4).currentText() tooluid_item = int(self.ui.geo_tools_table.item(row, 5).text()) try: offset_value_item = float(self.ui.tool_offset_entry.get_value()) except ValueError: # try to convert comma to decimal point. if it's still not working error message and return try: offset_value_item = float(self.ui.tool_offset_entry.get_value().replace(',', '.')) except ValueError: self.app.inform.emit('[ERROR_NOTCL] %s' % _("Wrong value format entered, use a number.")) return # this new dict will hold the actual useful data, another dict that is the value of key 'data' temp_tools = {} temp_dia = {} temp_data = {} for tooluid_key, tooluid_value in self.tools.items(): if int(tooluid_key) == tooluid_item: for key, value in tooluid_value.items(): if key == 'tooldia': temp_dia[key] = tooldia_item # update the 'offset', 'type' and 'tool_type' sections if key == 'offset': temp_dia[key] = offset_item if key == 'type': temp_dia[key] = type_item if key == 'tool_type': temp_dia[key] = tool_type_item if key == 'offset_value': temp_dia[key] = offset_value_item if key == 'data': # update the 'data' section for data_key in tooluid_value[key].keys(): for form_key, form_value in self.form_fields.items(): if form_key == data_key: temp_data[data_key] = form_value.get_value() # make sure we make a copy of the keys not in the form (we may use 'data' keys that are # updated from self.app.defaults if data_key not in self.form_fields: temp_data[data_key] = value[data_key] temp_dia[key] = deepcopy(temp_data) temp_data.clear() if key == 'solid_geometry': temp_dia[key] = deepcopy(self.tools[tooluid_key]['solid_geometry']) temp_tools[tooluid_key] = deepcopy(temp_dia) else: temp_tools[tooluid_key] = deepcopy(tooluid_value) self.tools.clear() self.tools = deepcopy(temp_tools) temp_tools.clear() self.ui_connect() def select_tools_table_row(self, row, clearsel=None): if clearsel: self.ui.geo_tools_table.clearSelection() if self.ui.geo_tools_table.rowCount() > 0: # self.ui.geo_tools_table.item(row, 0).setSelected(True) self.ui.geo_tools_table.setCurrentItem(self.ui.geo_tools_table.item(row, 0)) def export_dxf(self): units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper() dwg = None try: dwg = ezdxf.new('R2010') msp = dwg.modelspace() def g2dxf(dxf_space, geo): if isinstance(geo, MultiPolygon): for poly in geo: ext_points = list(poly.exterior.coords) dxf_space.add_lwpolyline(ext_points) for interior in poly.interiors: dxf_space.add_lwpolyline(list(interior.coords)) if isinstance(geo, Polygon): ext_points = list(geo.exterior.coords) dxf_space.add_lwpolyline(ext_points) for interior in geo.interiors: dxf_space.add_lwpolyline(list(interior.coords)) if isinstance(geo, MultiLineString): for line in geo: dxf_space.add_lwpolyline(list(line.coords)) if isinstance(geo, LineString) or isinstance(geo, LinearRing): dxf_space.add_lwpolyline(list(geo.coords)) multigeo_solid_geometry = [] if self.multigeo: for tool in self.tools: multigeo_solid_geometry += self.tools[tool]['solid_geometry'] else: multigeo_solid_geometry = self.solid_geometry for geo in multigeo_solid_geometry: if type(geo) == list: for g in geo: g2dxf(msp, g) else: g2dxf(msp, geo) # points = FlatCAMGeometry.get_pts(geo) # msp.add_lwpolyline(points) except Exception as e: log.debug(str(e)) return dwg def get_selected_tools_table_items(self): """ Returns a list of lists, each list in the list is made out of row elements :return: List of table_tools items. :rtype: list """ table_tools_items = [] if self.multigeo: for x in self.ui.geo_tools_table.selectedItems(): table_tools_items.append([self.ui.geo_tools_table.item(x.row(), column).text() for column in range(0, self.ui.geo_tools_table.columnCount())]) else: for x in self.ui.geo_tools_table.selectedItems(): r = [] txt = '' # the last 2 columns for single-geo geometry are irrelevant and create problems reading # so we don't read them for column in range(0, self.ui.geo_tools_table.columnCount() - 2): # the columns have items that have text but also have items that are widgets # for which the text they hold has to be read differently try: txt = self.ui.geo_tools_table.item(x.row(), column).text() except AttributeError: txt = self.ui.geo_tools_table.cellWidget(x.row(), column).currentText() except Exception as e: pass r.append(txt) table_tools_items.append(r) for item in table_tools_items: item[0] = str(item[0]) return table_tools_items def on_pp_changed(self): current_pp = self.ui.pp_geometry_name_cb.get_value() if current_pp == 'hpgl': self.old_pp_state = self.ui.mpass_cb.get_value() self.old_toolchangeg_state = self.ui.toolchangeg_cb.get_value() self.ui.mpass_cb.set_value(False) self.ui.mpass_cb.setDisabled(True) self.ui.toolchangeg_cb.set_value(True) self.ui.toolchangeg_cb.setDisabled(True) else: self.ui.mpass_cb.set_value(self.old_pp_state) self.ui.mpass_cb.setDisabled(False) self.ui.toolchangeg_cb.set_value(self.old_toolchangeg_state) self.ui.toolchangeg_cb.setDisabled(False) if "toolchange_probe" in current_pp.lower(): self.ui.pdepth_entry.setVisible(True) self.ui.pdepth_label.show() self.ui.feedrate_probe_entry.setVisible(True) self.ui.feedrate_probe_label.show() else: self.ui.pdepth_entry.setVisible(False) self.ui.pdepth_label.hide() self.ui.feedrate_probe_entry.setVisible(False) self.ui.feedrate_probe_label.hide() if 'marlin' in current_pp.lower() or 'custom' in current_pp.lower(): self.ui.fr_rapidlabel.show() self.ui.cncfeedrate_rapid_entry.show() else: self.ui.fr_rapidlabel.hide() self.ui.cncfeedrate_rapid_entry.hide() def on_generatecnc_button_click(self, *args): log.debug("Generating CNCJob from Geometry ...") self.app.report_usage("geometry_on_generatecnc_button") # this reads the values in the UI form to the self.options dictionary self.read_form() self.sel_tools = {} try: if self.special_group: self.app.inform.emit('[WARNING_NOTCL] %s %s %s.' % (_("This Geometry can't be processed because it is"), str(self.special_group), _("geometry") ) ) return except AttributeError: pass # test to see if we have tools available in the tool table if self.ui.geo_tools_table.selectedItems(): for x in self.ui.geo_tools_table.selectedItems(): try: tooldia = float(self.ui.geo_tools_table.item(x.row(), 1).text()) except ValueError: # try to convert comma to decimal point. if it's still not working error message and return try: tooldia = float(self.ui.geo_tools_table.item(x.row(), 1).text().replace(',', '.')) except ValueError: self.app.inform.emit('[ERROR_NOTCL] %s' % _("Wrong value format entered, use a number.")) return tooluid = int(self.ui.geo_tools_table.item(x.row(), 5).text()) for tooluid_key, tooluid_value in self.tools.items(): if int(tooluid_key) == tooluid: self.sel_tools.update({ tooluid: deepcopy(tooluid_value) }) self.mtool_gen_cncjob() self.ui.geo_tools_table.clearSelection() elif self.ui.geo_tools_table.rowCount() == 1: tooluid = int(self.ui.geo_tools_table.item(0, 5).text()) for tooluid_key, tooluid_value in self.tools.items(): if int(tooluid_key) == tooluid: self.sel_tools.update({ tooluid: deepcopy(tooluid_value) }) self.mtool_gen_cncjob() self.ui.geo_tools_table.clearSelection() else: self.app.inform.emit('[ERROR_NOTCL] %s' % _("Failed. No tool selected in the tool table ...")) def mtool_gen_cncjob(self, outname=None, tools_dict=None, tools_in_use=None, segx=None, segy=None, plot=True, use_thread=True): """ Creates a multi-tool CNCJob out of this Geometry object. The actual work is done by the target FlatCAMCNCjob object's `generate_from_geometry_2()` method. :param tools_dict: a dictionary that holds the whole data needed to create the Gcode (including the solid_geometry) :param tools_in_use: the tools that are used, needed by some postprocessors :type list of lists, each list in the list is made out of row elements of tools table from GUI :param segx: number of segments on the X axis, for auto-levelling :param segy: number of segments on the Y axis, for auto-levelling :param use_thread: if True use threading :return: None """ # use the name of the first tool selected in self.geo_tools_table which has the diameter passed as tool_dia if outname is None: outname = "%s_%s" % (self.options["name"], 'cnc') else: outname = outname tools_dict = self.sel_tools if tools_dict is None else tools_dict tools_in_use = tools_in_use if tools_in_use is not None else self.get_selected_tools_table_items() segx = segx if segx is not None else float(self.app.defaults['geometry_segx']) segy = segy if segy is not None else float(self.app.defaults['geometry_segy']) try: xmin = self.options['xmin'] ymin = self.options['ymin'] xmax = self.options['xmax'] ymax = self.options['ymax'] except Exception as e: log.debug("FlatCAMObj.FlatCAMGeometry.mtool_gen_cncjob() --> %s\n" % str(e)) msg = '[ERROR] %s' % _("An internal error has occurred. See shell.\n") msg += '%s %s' % (_('FlatCAMObj.FlatCAMGeometry.mtool_gen_cncjob() -->'), str(e)) msg += traceback.format_exc() self.app.inform.emit(msg) return # Object initialization function for app.new_object() # RUNNING ON SEPARATE THREAD! def job_init_single_geometry(job_obj, app_obj): log.debug("Creating a CNCJob out of a single-geometry") assert isinstance(job_obj, FlatCAMCNCjob), \ "Initializer expected a FlatCAMCNCjob, got %s" % type(job_obj) job_obj.options['xmin'] = xmin job_obj.options['ymin'] = ymin job_obj.options['xmax'] = xmax job_obj.options['ymax'] = ymax # count the tools tool_cnt = 0 dia_cnc_dict = {} # this turn on the FlatCAMCNCJob plot for multiple tools job_obj.multitool = True job_obj.multigeo = False job_obj.cnc_tools.clear() job_obj.options['Tools_in_use'] = tools_in_use job_obj.segx = segx if segx else float(self.app.defaults["geometry_segx"]) job_obj.segy = segy if segy else float(self.app.defaults["geometry_segy"]) job_obj.z_pdepth = float(self.app.defaults["geometry_z_pdepth"]) job_obj.feedrate_probe = float(self.app.defaults["geometry_feedrate_probe"]) for tooluid_key in list(tools_dict.keys()): tool_cnt += 1 dia_cnc_dict = deepcopy(tools_dict[tooluid_key]) tooldia_val = float('%.4f' % float(tools_dict[tooluid_key]['tooldia'])) dia_cnc_dict.update({ 'tooldia': tooldia_val }) if dia_cnc_dict['offset'] == 'in': tool_offset = -dia_cnc_dict['tooldia'] / 2 elif dia_cnc_dict['offset'].lower() == 'out': tool_offset = dia_cnc_dict['tooldia'] / 2 elif dia_cnc_dict['offset'].lower() == 'custom': try: offset_value = float(self.ui.tool_offset_entry.get_value()) except ValueError: # try to convert comma to decimal point. if it's still not working error message and return try: offset_value = float(self.ui.tool_offset_entry.get_value().replace(',', '.')) except ValueError: self.app.inform.emit('[ERROR_NOTCL] %s' % _("Wrong value format entered, use a number.")) return if offset_value: tool_offset = float(offset_value) else: self.app.inform.emit('[WARNING] %s' % _("Tool Offset is selected in Tool Table but " "no value is provided.\n" "Add a Tool Offset or change the Offset Type.")) return else: tool_offset = 0.0 dia_cnc_dict.update({ 'offset_value': tool_offset }) z_cut = tools_dict[tooluid_key]['data']["cutz"] z_move = tools_dict[tooluid_key]['data']["travelz"] feedrate = tools_dict[tooluid_key]['data']["feedrate"] feedrate_z = tools_dict[tooluid_key]['data']["feedrate_z"] feedrate_rapid = tools_dict[tooluid_key]['data']["feedrate_rapid"] multidepth = tools_dict[tooluid_key]['data']["multidepth"] extracut = tools_dict[tooluid_key]['data']["extracut"] depthpercut = tools_dict[tooluid_key]['data']["depthperpass"] toolchange = tools_dict[tooluid_key]['data']["toolchange"] toolchangez = tools_dict[tooluid_key]['data']["toolchangez"] toolchangexy = tools_dict[tooluid_key]['data']["toolchangexy"] startz = tools_dict[tooluid_key]['data']["startz"] endz = tools_dict[tooluid_key]['data']["endz"] spindlespeed = tools_dict[tooluid_key]['data']["spindlespeed"] dwell = tools_dict[tooluid_key]['data']["dwell"] dwelltime = tools_dict[tooluid_key]['data']["dwelltime"] pp_geometry_name = tools_dict[tooluid_key]['data']["ppname_g"] spindledir = self.app.defaults['geometry_spindledir'] tool_solid_geometry = self.solid_geometry job_obj.coords_decimals = self.app.defaults["cncjob_coords_decimals"] job_obj.fr_decimals = self.app.defaults["cncjob_fr_decimals"] # Propagate options job_obj.options["tooldia"] = tooldia_val job_obj.options['type'] = 'Geometry' job_obj.options['tool_dia'] = tooldia_val # it seems that the tolerance needs to be a lot lower value than 0.01 and it was hardcoded initially # to a value of 0.0005 which is 20 times less than 0.01 tol = float(self.app.defaults['global_tolerance']) / 20 res = job_obj.generate_from_geometry_2( self, tooldia=tooldia_val, offset=tool_offset, tolerance=tol, z_cut=z_cut, z_move=z_move, feedrate=feedrate, feedrate_z=feedrate_z, feedrate_rapid=feedrate_rapid, spindlespeed=spindlespeed, spindledir=spindledir, dwell=dwell, dwelltime=dwelltime, multidepth=multidepth, depthpercut=depthpercut, extracut=extracut, startz=startz, endz=endz, toolchange=toolchange, toolchangez=toolchangez, toolchangexy=toolchangexy, pp_geometry_name=pp_geometry_name, tool_no=tool_cnt) if res == 'fail': log.debug("FlatCAMGeometry.mtool_gen_cncjob() --> generate_from_geometry2() failed") return 'fail' else: dia_cnc_dict['gcode'] = res app_obj.progress.emit(50) # tell gcode_parse from which point to start drawing the lines depending on what kind of # object is the source of gcode job_obj.toolchange_xy_type = "geometry" self.app.inform.emit('[success] %s' % _("G-Code parsing in progress...")) dia_cnc_dict['gcode_parsed'] = job_obj.gcode_parse() self.app.inform.emit('[success] %s' % _("G-Code parsing finished...")) # TODO this serve for bounding box creation only; should be optimized # commented this; there is no need for the actual GCode geometry - the original one will serve as well # for bounding box values # dia_cnc_dict['solid_geometry'] = cascaded_union([geo['geom'] for geo in dia_cnc_dict['gcode_parsed']]) try: dia_cnc_dict['solid_geometry'] = tool_solid_geometry self.app.inform.emit('[success] %s...' % _("Finished G-Code processing")) except Exception as e: self.app.inform.emit('[ERROR] %s: %s' % (_("G-Code processing failed with error"), str(e))) app_obj.progress.emit(80) job_obj.cnc_tools.update({ tooluid_key: deepcopy(dia_cnc_dict) }) dia_cnc_dict.clear() # Object initialization function for app.new_object() # RUNNING ON SEPARATE THREAD! def job_init_multi_geometry(job_obj, app_obj): log.debug("Creating a CNCJob out of a multi-geometry") assert isinstance(job_obj, FlatCAMCNCjob), \ "Initializer expected a FlatCAMCNCjob, got %s" % type(job_obj) current_uid = int(1) job_obj.options['xmin'] = xmin job_obj.options['ymin'] = ymin job_obj.options['xmax'] = xmax job_obj.options['ymax'] = ymax # count the tools tool_cnt = 0 dia_cnc_dict = {} # this turn on the FlatCAMCNCJob plot for multiple tools job_obj.multitool = True job_obj.multigeo = True job_obj.cnc_tools.clear() job_obj.options['Tools_in_use'] = tools_in_use job_obj.segx = segx if segx else float(self.app.defaults["geometry_segx"]) job_obj.segy = segy if segy else float(self.app.defaults["geometry_segy"]) job_obj.z_pdepth = float(self.app.defaults["geometry_z_pdepth"]) job_obj.feedrate_probe = float(self.app.defaults["geometry_feedrate_probe"]) # make sure that trying to make a CNCJob from an empty file is not creating an app crash if not self.solid_geometry: a = 0 for tooluid_key in self.tools: if self.tools[tooluid_key]['solid_geometry'] is None: a += 1 if a == len(self.tools): self.app.inform.emit('[ERROR_NOTCL] %s...' % _('Cancelled. Empty file, it has no geometry')) return 'fail' for tooluid_key in list(tools_dict.keys()): tool_cnt += 1 dia_cnc_dict = deepcopy(tools_dict[tooluid_key]) tooldia_val = float('%.4f' % float(tools_dict[tooluid_key]['tooldia'])) dia_cnc_dict.update({ 'tooldia': tooldia_val }) # find the tool_dia associated with the tooluid_key # search in the self.tools for the sel_tool_dia and when found see what tooluid has # on the found tooluid in self.tools we also have the solid_geometry that interest us for k, v in self.tools.items(): if float('%.4f' % float(v['tooldia'])) == tooldia_val: current_uid = int(k) break if dia_cnc_dict['offset'] == 'in': tool_offset = -tooldia_val / 2 elif dia_cnc_dict['offset'].lower() == 'out': tool_offset = tooldia_val / 2 elif dia_cnc_dict['offset'].lower() == 'custom': try: offset_value = float(self.ui.tool_offset_entry.get_value()) except ValueError: # try to convert comma to decimal point. if it's still not working error message and return try: offset_value = float(self.ui.tool_offset_entry.get_value().replace(',', '.')) except ValueError: self.app.inform.emit('[ERROR_NOTCL] %s' % _("Wrong value format entered, use a number.")) return if offset_value: tool_offset = float(offset_value) else: self.app.inform.emit('[WARNING] %s' % _("Tool Offset is selected in Tool Table but " "no value is provided.\n" "Add a Tool Offset or change the Offset Type.")) return else: tool_offset = 0.0 dia_cnc_dict.update({ 'offset_value': tool_offset }) z_cut = tools_dict[tooluid_key]['data']["cutz"] z_move = tools_dict[tooluid_key]['data']["travelz"] feedrate = tools_dict[tooluid_key]['data']["feedrate"] feedrate_z = tools_dict[tooluid_key]['data']["feedrate_z"] feedrate_rapid = tools_dict[tooluid_key]['data']["feedrate_rapid"] multidepth = tools_dict[tooluid_key]['data']["multidepth"] extracut = tools_dict[tooluid_key]['data']["extracut"] depthpercut = tools_dict[tooluid_key]['data']["depthperpass"] toolchange = tools_dict[tooluid_key]['data']["toolchange"] toolchangez = tools_dict[tooluid_key]['data']["toolchangez"] toolchangexy = tools_dict[tooluid_key]['data']["toolchangexy"] startz = tools_dict[tooluid_key]['data']["startz"] endz = tools_dict[tooluid_key]['data']["endz"] spindlespeed = tools_dict[tooluid_key]['data']["spindlespeed"] dwell = tools_dict[tooluid_key]['data']["dwell"] dwelltime = tools_dict[tooluid_key]['data']["dwelltime"] pp_geometry_name = tools_dict[tooluid_key]['data']["ppname_g"] spindledir = self.app.defaults['geometry_spindledir'] tool_solid_geometry = self.tools[current_uid]['solid_geometry'] job_obj.coords_decimals = self.app.defaults["cncjob_coords_decimals"] job_obj.fr_decimals = self.app.defaults["cncjob_fr_decimals"] # Propagate options job_obj.options["tooldia"] = tooldia_val job_obj.options['type'] = 'Geometry' job_obj.options['tool_dia'] = tooldia_val # it seems that the tolerance needs to be a lot lower value than 0.01 and it was hardcoded initially # to a value of 0.0005 which is 20 times less than 0.01 tol = float(self.app.defaults['global_tolerance']) / 20 res = job_obj.generate_from_multitool_geometry( tool_solid_geometry, tooldia=tooldia_val, offset=tool_offset, tolerance=tol, z_cut=z_cut, z_move=z_move, feedrate=feedrate, feedrate_z=feedrate_z, feedrate_rapid=feedrate_rapid, spindlespeed=spindlespeed, spindledir=spindledir, dwell=dwell, dwelltime=dwelltime, multidepth=multidepth, depthpercut=depthpercut, extracut=extracut, startz=startz, endz=endz, toolchange=toolchange, toolchangez=toolchangez, toolchangexy=toolchangexy, pp_geometry_name=pp_geometry_name, tool_no=tool_cnt) if res == 'fail': log.debug("FlatCAMGeometry.mtool_gen_cncjob() --> generate_from_geometry2() failed") return 'fail' else: dia_cnc_dict['gcode'] = res self.app.inform.emit('[success] %s' % _("G-Code parsing in progress...")) dia_cnc_dict['gcode_parsed'] = job_obj.gcode_parse() self.app.inform.emit('[success] %s' % _("G-Code parsing finished...")) # TODO this serve for bounding box creation only; should be optimized # commented this; there is no need for the actual GCode geometry - the original one will serve as well # for bounding box values # geo_for_bound_values = cascaded_union([ # geo['geom'] for geo in dia_cnc_dict['gcode_parsed'] if geo['geom'].is_valid is True # ]) try: dia_cnc_dict['solid_geometry'] = deepcopy(tool_solid_geometry) self.app.inform.emit('[success] %s' % _("Finished G-Code processing...")) except Exception as e: self.app.inform.emit('[ERROR] %s: %s' % (_("G-Code processing failed with error"), str(e))) # tell gcode_parse from which point to start drawing the lines depending on what kind of # object is the source of gcode job_obj.toolchange_xy_type = "geometry" app_obj.progress.emit(80) job_obj.cnc_tools.update({ tooluid_key: deepcopy(dia_cnc_dict) }) dia_cnc_dict.clear() if use_thread: # To be run in separate thread # The idea is that if there is a solid_geometry in the file "root" then most likely thare are no # separate solid_geometry in the self.tools dictionary def job_thread(app_obj): if self.solid_geometry: with self.app.proc_container.new(_("Generating CNC Code")): if app_obj.new_object("cncjob", outname, job_init_single_geometry, plot=plot) != 'fail': app_obj.inform.emit('[success] %s: %s' % (_("CNCjob created")), outname) app_obj.progress.emit(100) else: with self.app.proc_container.new(_("Generating CNC Code")): if app_obj.new_object("cncjob", outname, job_init_multi_geometry) != 'fail': app_obj.inform.emit('[success] %s: %s' % (_("CNCjob created")), outname) app_obj.progress.emit(100) # Create a promise with the name self.app.collection.promise(outname) # Send to worker self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]}) else: if self.solid_geometry: self.app.new_object("cncjob", outname, job_init_single_geometry, plot=plot) else: self.app.new_object("cncjob", outname, job_init_multi_geometry, plot=plot) def generatecncjob( self, outname=None, dia=None, offset=None, z_cut=None, z_move=None, feedrate=None, feedrate_z=None, feedrate_rapid=None, spindlespeed=None, dwell=None, dwelltime=None, multidepth=None, depthperpass=None, toolchange=None, toolchangez=None, toolchangexy=None, extracut=None, startz=None, endz=None, pp=None, segx=None, segy=None, use_thread=True, plot=True): """ Only used for TCL Command. Creates a CNCJob out of this Geometry object. The actual work is done by the target FlatCAMCNCjob object's `generate_from_geometry_2()` method. :param z_cut: Cut depth (negative) :param z_move: Hight of the tool when travelling (not cutting) :param feedrate: Feed rate while cutting on X - Y plane :param feedrate_z: Feed rate while cutting on Z plane :param feedrate_rapid: Feed rate while moving with rapids :param dia: Tool diameter :param outname: Name of the new object :param spindlespeed: Spindle speed (RPM) :param pp Name of the postprocessor :return: None """ tooldia = dia if dia else float(self.options["cnctooldia"]) outname = outname if outname is not None else self.options["name"] z_cut = z_cut if z_cut is not None else float(self.options["cutz"]) z_move = z_move if z_move is not None else float(self.options["travelz"]) feedrate = feedrate if feedrate is not None else float(self.options["feedrate"]) feedrate_z = feedrate_z if feedrate_z is not None else float(self.options["feedrate_z"]) feedrate_rapid = feedrate_rapid if feedrate_rapid is not None else float(self.options["feedrate_rapid"]) multidepth = multidepth if multidepth is not None else self.options["multidepth"] depthperpass = depthperpass if depthperpass is not None else float(self.options["depthperpass"]) segx = segx if segx is not None else float(self.app.defaults['geometry_segx']) segy = segy if segy is not None else float(self.app.defaults['geometry_segy']) extracut = extracut if extracut is not None else float(self.options["extracut"]) startz = startz if startz is not None else self.options["startz"] endz = endz if endz is not None else float(self.options["endz"]) toolchangez = toolchangez if toolchangez else float(self.options["toolchangez"]) toolchangexy = toolchangexy if toolchangexy else self.options["toolchangexy"] toolchange = toolchange if toolchange else self.options["toolchange"] offset = offset if offset else 0.0 # int or None. spindlespeed = spindlespeed if spindlespeed else self.options['spindlespeed'] dwell = dwell if dwell else self.options["dwell"] dwelltime = dwelltime if dwelltime else float(self.options["dwelltime"]) ppname_g = pp if pp else self.options["ppname_g"] # Object initialization function for app.new_object() # RUNNING ON SEPARATE THREAD! def job_init(job_obj, app_obj): assert isinstance(job_obj, FlatCAMCNCjob), "Initializer expected a FlatCAMCNCjob, got %s" % type(job_obj) # Propagate options job_obj.options["tooldia"] = tooldia app_obj.progress.emit(20) job_obj.coords_decimals = self.app.defaults["cncjob_coords_decimals"] job_obj.fr_decimals = self.app.defaults["cncjob_fr_decimals"] app_obj.progress.emit(40) job_obj.options['type'] = 'Geometry' job_obj.options['tool_dia'] = tooldia job_obj.segx = segx job_obj.segy = segy try: job_obj.z_pdepth = float(self.options["z_pdepth"]) except ValueError: # try to convert comma to decimal point. if it's still not working error message and return try: job_obj.z_pdepth = float(self.options["z_pdepth"].replace(',', '.')) except ValueError: self.app.inform.emit('[ERROR_NOTCL] %s' % _('Wrong value format for self.defaults["z_pdepth"] or ' 'self.options["z_pdepth"]')) try: job_obj.feedrate_probe = float(self.options["feedrate_probe"]) except ValueError: # try to convert comma to decimal point. if it's still not working error message and return try: job_obj.feedrate_rapid = float(self.options["feedrate_probe"].replace(',', '.')) except ValueError: self.app.inform.emit('[ERROR_NOTCL] %s' % _('Wrong value format for self.defaults["feedrate_probe"] ' 'or self.options["feedrate_probe"]')) job_obj.options['xmin'] = self.options['xmin'] job_obj.options['ymin'] = self.options['ymin'] job_obj.options['xmax'] = self.options['xmax'] job_obj.options['ymax'] = self.options['ymax'] # it seems that the tolerance needs to be a lot lower value than 0.01 and it was hardcoded initially # to a value of 0.0005 which is 20 times less than 0.01 tol = float(self.app.defaults['global_tolerance']) / 20 job_obj.generate_from_geometry_2( self, tooldia=tooldia, offset=offset, tolerance=tol, z_cut=z_cut, z_move=z_move, feedrate=feedrate, feedrate_z=feedrate_z, feedrate_rapid=feedrate_rapid, spindlespeed=spindlespeed, dwell=dwell, dwelltime=dwelltime, multidepth=multidepth, depthpercut=depthperpass, toolchange=toolchange, toolchangez=toolchangez, toolchangexy=toolchangexy, extracut=extracut, startz=startz, endz=endz, pp_geometry_name=ppname_g ) app_obj.progress.emit(50) # tell gcode_parse from which point to start drawing the lines depending on what kind of object is the # source of gcode job_obj.toolchange_xy_type = "geometry" job_obj.gcode_parse() self.app.inform.emit('[success] %s' % _("Finished G-Code processing...")) app_obj.progress.emit(80) if use_thread: # To be run in separate thread def job_thread(app_obj): with self.app.proc_container.new(_("Generating CNC Code")): app_obj.new_object("cncjob", outname, job_init, plot=plot) app_obj.inform.emit('[success] %s: %s' % (_("CNCjob created")), outname) app_obj.progress.emit(100) # Create a promise with the name self.app.collection.promise(outname) # Send to worker self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]}) else: self.app.new_object("cncjob", outname, job_init, plot=plot) # def on_plot_cb_click(self, *args): # TODO: args not needed # if self.muted_ui: # return # self.read_form_item('plot') def scale(self, xfactor, yfactor=None, point=None): """ Scales all geometry by a given factor. :param xfactor: Factor by which to scale the object's geometry/ :type xfactor: float :param yfactor: Factor by which to scale the object's geometry/ :type yfactor: float :return: None :rtype: None """ log.debug("FlatCAMObj.FlatCAMGeometry.scale()") try: xfactor = float(xfactor) except Exception as e: self.app.inform.emit('[ERROR_NOTCL] %s' % _("Scale factor has to be a number: integer or float.")) return if yfactor is None: yfactor = xfactor else: try: yfactor = float(yfactor) except Exception as e: self.app.inform.emit('[ERROR_NOTCL] %s' % _("Scale factor has to be a number: integer or float.")) return if point is None: px = 0 py = 0 else: px, py = point # if type(self.solid_geometry) == list: # geo_list = self.flatten(self.solid_geometry) # self.solid_geometry = [] # # for g in geo_list: # # self.solid_geometry.append(affinity.scale(g, xfactor, yfactor, origin=(px, py))) # self.solid_geometry = [affinity.scale(g, xfactor, yfactor, origin=(px, py)) # for g in geo_list] # else: # self.solid_geometry = affinity.scale(self.solid_geometry, xfactor, yfactor, # origin=(px, py)) # self.app.inform.emit("[success] Geometry Scale done.") self.geo_len = 0 self.old_disp_number = 0 self.el_count = 0 def scale_recursion(geom): if type(geom) is list: geoms = list() for local_geom in geom: geoms.append(scale_recursion(local_geom)) return geoms else: try: self.el_count += 1 disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100])) if self.old_disp_number < disp_number <= 100: self.app.proc_container.update_view_text(' %d%%' % disp_number) self.old_disp_number = disp_number return affinity.scale(geom, xfactor, yfactor, origin=(px, py)) except AttributeError: return geom if self.multigeo is True: for tool in self.tools: # variables to display the percentage of work done self.geo_len = 0 try: for g in self.tools[tool]['solid_geometry']: self.geo_len += 1 except TypeError: self.geo_len = 1 self.old_disp_number = 0 self.el_count = 0 self.tools[tool]['solid_geometry'] = scale_recursion(self.tools[tool]['solid_geometry']) else: try: # variables to display the percentage of work done self.geo_len = 0 try: for g in self.solid_geometry: self.geo_len += 1 except TypeError: self.geo_len = 1 self.old_disp_number = 0 self.el_count = 0 self.solid_geometry = scale_recursion(self.solid_geometry) except AttributeError: self.solid_geometry = [] return self.app.proc_container.new_text = '' self.app.inform.emit('[success] %s' % _("Geometry Scale done.")) def offset(self, vect): """ Offsets all geometry by a given vector/ :param vect: (x, y) vector by which to offset the object's geometry. :type vect: tuple :return: None :rtype: None """ log.debug("FlatCAMObj.FlatCAMGeometry.offset()") try: dx, dy = vect except TypeError: self.app.inform.emit('[ERROR_NOTCL] %s' % _("An (x,y) pair of values are needed. " "Probable you entered only one value in the Offset field." )) return self.geo_len = 0 self.old_disp_number = 0 self.el_count = 0 def translate_recursion(geom): if type(geom) is list: geoms = list() for local_geom in geom: geoms.append(translate_recursion(local_geom)) return geoms else: try: self.el_count += 1 disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100])) if self.old_disp_number < disp_number <= 100: self.app.proc_container.update_view_text(' %d%%' % disp_number) self.old_disp_number = disp_number return affinity.translate(geom, xoff=dx, yoff=dy) except AttributeError: return geom if self.multigeo is True: for tool in self.tools: # variables to display the percentage of work done self.geo_len = 0 try: for g in self.tools[tool]['solid_geometry']: self.geo_len += 1 except TypeError: self.geo_len = 1 self.old_disp_number = 0 self.el_count = 0 self.tools[tool]['solid_geometry'] = translate_recursion(self.tools[tool]['solid_geometry']) else: # variables to display the percentage of work done self.geo_len = 0 try: for g in self.solid_geometry: self.geo_len += 1 except TypeError: self.geo_len = 1 self.old_disp_number = 0 self.el_count = 0 self.solid_geometry = translate_recursion(self.solid_geometry) self.app.proc_container.new_text = '' self.app.inform.emit('[success] %s' % _("Geometry Offset done.")) def convert_units(self, units): log.debug("FlatCAMObj.FlatCAMGeometry.convert_units()") self.ui_disconnect() factor = Geometry.convert_units(self, units) self.options['cutz'] = float(self.options['cutz']) * factor self.options['depthperpass'] = float(self.options['depthperpass']) * factor self.options['travelz'] = float(self.options['travelz']) * factor self.options['feedrate'] = float(self.options['feedrate']) * factor self.options['feedrate_z'] = float(self.options['feedrate_z']) * factor self.options['feedrate_rapid'] = float(self.options['feedrate_rapid']) * factor self.options['endz'] = float(self.options['endz']) * factor # self.options['cnctooldia'] *= factor # self.options['painttooldia'] *= factor # self.options['paintmargin'] *= factor # self.options['paintoverlap'] *= factor self.options["toolchangez"] = float(self.options["toolchangez"]) * factor if self.app.defaults["geometry_toolchangexy"] == '': self.options['toolchangexy'] = "0.0, 0.0" else: coords_xy = [float(eval(coord)) for coord in self.app.defaults["geometry_toolchangexy"].split(",")] if len(coords_xy) < 2: self.app.inform.emit('[ERROR] %s' % _("The Toolchange X,Y field in Edit -> Preferences " "has to be in the format (x, y)\n" "but now there is only one value, not two." )) return 'fail' coords_xy[0] *= factor coords_xy[1] *= factor self.options['toolchangexy'] = "%f, %f" % (coords_xy[0], coords_xy[1]) if self.options['startz'] is not None: self.options['startz'] = float(self.options['startz']) * factor param_list = ['cutz', 'depthperpass', 'travelz', 'feedrate', 'feedrate_z', 'feedrate_rapid', 'endz', 'toolchangez'] if isinstance(self, FlatCAMGeometry): temp_tools_dict = {} tool_dia_copy = {} data_copy = {} for tooluid_key, tooluid_value in self.tools.items(): for dia_key, dia_value in tooluid_value.items(): if dia_key == 'tooldia': dia_value *= factor dia_value = float('%.4f' % dia_value) tool_dia_copy[dia_key] = dia_value if dia_key == 'offset': tool_dia_copy[dia_key] = dia_value if dia_key == 'offset_value': dia_value *= factor tool_dia_copy[dia_key] = dia_value # convert the value in the Custom Tool Offset entry in UI custom_offset = None try: custom_offset = float(self.ui.tool_offset_entry.get_value()) except ValueError: # try to convert comma to decimal point. if it's still not working error message and return try: custom_offset = float(self.ui.tool_offset_entry.get_value().replace(',', '.')) except ValueError: self.app.inform.emit('[ERROR_NOTCL] %s' % _("Wrong value format entered, use a number.")) return except TypeError: pass if custom_offset: custom_offset *= factor self.ui.tool_offset_entry.set_value(custom_offset) if dia_key == 'type': tool_dia_copy[dia_key] = dia_value if dia_key == 'tool_type': tool_dia_copy[dia_key] = dia_value if dia_key == 'data': for data_key, data_value in dia_value.items(): # convert the form fields that are convertible for param in param_list: if data_key == param and data_value is not None: data_copy[data_key] = data_value * factor # copy the other dict entries that are not convertible if data_key not in param_list: data_copy[data_key] = data_value tool_dia_copy[dia_key] = deepcopy(data_copy) data_copy.clear() temp_tools_dict.update({ tooluid_key: deepcopy(tool_dia_copy) }) tool_dia_copy.clear() self.tools.clear() self.tools = deepcopy(temp_tools_dict) # if there is a value in the new tool field then convert that one too tooldia = self.ui.addtool_entry.get_value() if tooldia: tooldia *= factor # limit the decimals to 2 for METRIC and 3 for INCH if units.lower() == 'in': tooldia = float('%.4f' % tooldia) else: tooldia = float('%.2f' % tooldia) self.ui.addtool_entry.set_value(tooldia) return factor def plot_element(self, element, color='red', visible=None): visible = visible if visible else self.options['plot'] try: for sub_el in element: self.plot_element(sub_el) except TypeError: # Element is not iterable... # if self.app.is_legacy is False: self.add_shape(shape=element, color=color, visible=visible, layer=0) # else: # if type(element) == Polygon: # x, y = element.exterior.coords.xy # self.axes.plot(x, y, 'r-') # for ints in element.interiors: # x, y = ints.coords.xy # self.axes.plot(x, y, 'r-') # return # # if type(element) == LineString or type(element) == LinearRing: # x, y = element.coords.xy # self.axes.plot(x, y, 'r-') # return def plot(self, visible=None, kind=None): """ Plot the object. :param visible: Controls if the added shape is visible of not :param kind: added so there is no error when a project is loaded and it has both geometry and CNCJob, because CNCJob require the 'kind' parameter. Perhaps the FlatCAMObj.plot() has to be rewrited :return: """ # Does all the required setup and returns False # if the 'ptint' option is set to False. if not FlatCAMObj.plot(self): return try: # plot solid geometries found as members of self.tools attribute dict # for MultiGeo if self.multigeo is True: # geo multi tool usage for tooluid_key in self.tools: solid_geometry = self.tools[tooluid_key]['solid_geometry'] self.plot_element(solid_geometry, visible=visible) else: # plot solid geometry that may be an direct attribute of the geometry object # for SingleGeo if self.solid_geometry: self.plot_element(self.solid_geometry, visible=visible) # self.plot_element(self.solid_geometry, visible=self.options['plot']) self.shapes.redraw() except (ObjectDeleted, AttributeError): self.shapes.clear(update=True) def on_plot_cb_click(self, *args): if self.muted_ui: return self.read_form_item('plot') self.plot() self.ui_disconnect() cb_flag = self.ui.plot_cb.isChecked() for row in range(self.ui.geo_tools_table.rowCount()): table_cb = self.ui.geo_tools_table.cellWidget(row, 6) if cb_flag: table_cb.setChecked(True) else: table_cb.setChecked(False) self.ui_connect() def on_plot_cb_click_table(self): # self.ui.cnc_tools_table.cellWidget(row, 2).widget().setCheckState(QtCore.Qt.Unchecked) self.ui_disconnect() # cw = self.sender() # cw_index = self.ui.geo_tools_table.indexAt(cw.pos()) # cw_row = cw_index.row() check_row = 0 self.shapes.clear(update=True) for tooluid_key in self.tools: solid_geometry = self.tools[tooluid_key]['solid_geometry'] # find the geo_tool_table row associated with the tooluid_key for row in range(self.ui.geo_tools_table.rowCount()): tooluid_item = int(self.ui.geo_tools_table.item(row, 5).text()) if tooluid_item == int(tooluid_key): check_row = row break if self.ui.geo_tools_table.cellWidget(check_row, 6).isChecked(): self.plot_element(element=solid_geometry, visible=True) self.shapes.redraw() # make sure that the general plot is disabled if one of the row plot's are disabled and # if all the row plot's are enabled also enable the general plot checkbox cb_cnt = 0 total_row = self.ui.geo_tools_table.rowCount() for row in range(total_row): if self.ui.geo_tools_table.cellWidget(row, 6).isChecked(): cb_cnt += 1 else: cb_cnt -= 1 if cb_cnt < total_row: self.ui.plot_cb.setChecked(False) else: self.ui.plot_cb.setChecked(True) self.ui_connect() class FlatCAMCNCjob(FlatCAMObj, CNCjob): """ Represents G-Code. """ optionChanged = QtCore.pyqtSignal(str) ui_type = CNCObjectUI def __init__(self, name, units="in", kind="generic", z_move=0.1, feedrate=3.0, feedrate_rapid=3.0, z_cut=-0.002, tooldia=0.0, spindlespeed=None): FlatCAMApp.App.log.debug("Creating CNCJob object...") CNCjob.__init__(self, units=units, kind=kind, z_move=z_move, feedrate=feedrate, feedrate_rapid=feedrate_rapid, z_cut=z_cut, tooldia=tooldia, spindlespeed=spindlespeed, steps_per_circle=int(self.app.defaults["cncjob_steps_per_circle"])) FlatCAMObj.__init__(self, name) self.kind = "cncjob" self.options.update({ "plot": True, "tooldia": 0.03937, # 0.4mm in inches "append": "", "prepend": "", "dwell": False, "dwelltime": 1, "type": 'Geometry', "toolchange_macro": '', "toolchange_macro_enable": False }) ''' This is a dict of dictionaries. Each dict is associated with a tool present in the file. The key is the diameter of the tools and the value is another dict that will hold the data under the following form: {tooldia: { 'tooluid': 1, 'offset': 'Path', 'type_item': 'Rough', 'tool_type': 'C1', 'data': {} # a dict to hold the parameters 'gcode': "" # a string with the actual GCODE 'gcode_parsed': {} # dictionary holding the CNCJob geometry and type of geometry (cut or move) 'solid_geometry': [] }, ... } It is populated in the FlatCAMGeometry.mtool_gen_cncjob() BEWARE: I rely on the ordered nature of the Python 3.7 dictionary. Things might change ... ''' self.cnc_tools = {} ''' This is a dict of dictionaries. Each dict is associated with a tool present in the file. The key is the diameter of the tools and the value is another dict that will hold the data under the following form: {tooldia: { 'tool': int, 'nr_drills': int, 'nr_slots': int, 'offset': float, 'data': {} # a dict to hold the parameters 'gcode': "" # a string with the actual GCODE 'gcode_parsed': {} # dictionary holding the CNCJob geometry and type of geometry (cut or move) 'solid_geometry': [] }, ... } It is populated in the FlatCAMExcellon.on_create_cncjob_click() but actually it's done in camlib.Excellon.generate_from_excellon_by_tool() BEWARE: I rely on the ordered nature of the Python 3.7 dictionary. Things might change ... ''' self.exc_cnc_tools = {} # flag to store if the CNCJob is part of a special group of CNCJob objects that can't be processed by the # default engine of FlatCAM. They generated by some of tools and are special cases of CNCJob objects. self.special_group = None # for now it show if the plot will be done for multi-tool CNCJob (True) or for single tool # (like the one in the TCL Command), False self.multitool = False # used for parsing the GCode lines to adjust the GCode when the GCode is offseted or scaled gcodex_re_string = r'(?=.*(X[-\+]?\d*\.\d*))' self.g_x_re = re.compile(gcodex_re_string) gcodey_re_string = r'(?=.*(Y[-\+]?\d*\.\d*))' self.g_y_re = re.compile(gcodey_re_string) gcodez_re_string = r'(?=.*(Z[-\+]?\d*\.\d*))' self.g_z_re = re.compile(gcodez_re_string) gcodef_re_string = r'(?=.*(F[-\+]?\d*\.\d*))' self.g_f_re = re.compile(gcodef_re_string) gcodet_re_string = r'(?=.*(\=\s*[-\+]?\d*\.\d*))' self.g_t_re = re.compile(gcodet_re_string) gcodenr_re_string = r'([+-]?\d*\.\d+)' self.g_nr_re = re.compile(gcodenr_re_string) # Attributes to be included in serialization # Always append to it because it carries contents # from predecessors. self.ser_attrs += ['options', 'kind', 'cnc_tools', 'multitool'] if self.app.is_legacy is False: self.text_col = self.app.plotcanvas.new_text_collection() self.text_col.enabled = True self.annotation = self.app.plotcanvas.new_text_group(collection=self.text_col) def build_ui(self): self.ui_disconnect() FlatCAMObj.build_ui(self) # if the FlatCAM object is Excellon don't build the CNC Tools Table but hide it if self.cnc_tools: self.ui.cnc_tools_table.show() else: self.ui.cnc_tools_table.hide() self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper() offset = 0 tool_idx = 0 n = len(self.cnc_tools) self.ui.cnc_tools_table.setRowCount(n) for dia_key, dia_value in self.cnc_tools.items(): tool_idx += 1 row_no = tool_idx - 1 id = QtWidgets.QTableWidgetItem('%d' % int(tool_idx)) # id.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled) self.ui.cnc_tools_table.setItem(row_no, 0, id) # Tool name/id # Make sure that the tool diameter when in MM is with no more than 2 decimals. # There are no tool bits in MM with more than 2 decimals diameter. # For INCH the decimals should be no more than 4. There are no tools under 10mils. if self.units == 'MM': dia_item = QtWidgets.QTableWidgetItem('%.2f' % float(dia_value['tooldia'])) else: dia_item = QtWidgets.QTableWidgetItem('%.4f' % float(dia_value['tooldia'])) offset_txt = list(str(dia_value['offset'])) offset_txt[0] = offset_txt[0].upper() offset_item = QtWidgets.QTableWidgetItem(''.join(offset_txt)) type_item = QtWidgets.QTableWidgetItem(str(dia_value['type'])) tool_type_item = QtWidgets.QTableWidgetItem(str(dia_value['tool_type'])) id.setFlags(QtCore.Qt.ItemIsEnabled) dia_item.setFlags(QtCore.Qt.ItemIsEnabled) offset_item.setFlags(QtCore.Qt.ItemIsEnabled) type_item.setFlags(QtCore.Qt.ItemIsEnabled) tool_type_item.setFlags(QtCore.Qt.ItemIsEnabled) # hack so the checkbox stay centered in the table cell # used this: # https://stackoverflow.com/questions/32458111/pyqt-allign-checkbox-and-put-it-in-every-row # plot_item = QtWidgets.QWidget() # checkbox = FCCheckBox() # checkbox.setCheckState(QtCore.Qt.Checked) # qhboxlayout = QtWidgets.QHBoxLayout(plot_item) # qhboxlayout.addWidget(checkbox) # qhboxlayout.setAlignment(QtCore.Qt.AlignCenter) # qhboxlayout.setContentsMargins(0, 0, 0, 0) plot_item = FCCheckBox() plot_item.setLayoutDirection(QtCore.Qt.RightToLeft) tool_uid_item = QtWidgets.QTableWidgetItem(str(dia_key)) if self.ui.plot_cb.isChecked(): plot_item.setChecked(True) self.ui.cnc_tools_table.setItem(row_no, 1, dia_item) # Diameter self.ui.cnc_tools_table.setItem(row_no, 2, offset_item) # Offset self.ui.cnc_tools_table.setItem(row_no, 3, type_item) # Toolpath Type self.ui.cnc_tools_table.setItem(row_no, 4, tool_type_item) # Tool Type # ## REMEMBER: THIS COLUMN IS HIDDEN IN OBJECTUI.PY # ## self.ui.cnc_tools_table.setItem(row_no, 5, tool_uid_item) # Tool unique ID) self.ui.cnc_tools_table.setCellWidget(row_no, 6, plot_item) # make the diameter column editable # for row in range(tool_idx): # self.ui.cnc_tools_table.item(row, 1).setFlags(QtCore.Qt.ItemIsSelectable | # QtCore.Qt.ItemIsEnabled) for row in range(tool_idx): self.ui.cnc_tools_table.item(row, 0).setFlags( self.ui.cnc_tools_table.item(row, 0).flags() ^ QtCore.Qt.ItemIsSelectable) self.ui.cnc_tools_table.resizeColumnsToContents() self.ui.cnc_tools_table.resizeRowsToContents() vertical_header = self.ui.cnc_tools_table.verticalHeader() # vertical_header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents) vertical_header.hide() self.ui.cnc_tools_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) horizontal_header = self.ui.cnc_tools_table.horizontalHeader() horizontal_header.setMinimumSectionSize(10) horizontal_header.setDefaultSectionSize(70) horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed) horizontal_header.resizeSection(0, 20) horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch) horizontal_header.setSectionResizeMode(3, QtWidgets.QHeaderView.ResizeToContents) horizontal_header.setSectionResizeMode(4, QtWidgets.QHeaderView.Fixed) horizontal_header.resizeSection(4, 40) horizontal_header.setSectionResizeMode(6, QtWidgets.QHeaderView.Fixed) horizontal_header.resizeSection(4, 17) # horizontal_header.setStretchLastSection(True) self.ui.cnc_tools_table.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) self.ui.cnc_tools_table.setColumnWidth(0, 20) self.ui.cnc_tools_table.setColumnWidth(4, 40) self.ui.cnc_tools_table.setColumnWidth(6, 17) # self.ui.geo_tools_table.setSortingEnabled(True) self.ui.cnc_tools_table.setMinimumHeight(self.ui.cnc_tools_table.getHeight()) self.ui.cnc_tools_table.setMaximumHeight(self.ui.cnc_tools_table.getHeight()) self.ui_connect() def set_ui(self, ui): FlatCAMObj.set_ui(self, ui) FlatCAMApp.App.log.debug("FlatCAMCNCJob.set_ui()") assert isinstance(self.ui, CNCObjectUI), \ "Expected a CNCObjectUI, got %s" % type(self.ui) # this signal has to be connected to it's slot before the defaults are populated # the decision done in the slot has to override the default value set bellow self.ui.toolchange_cb.toggled.connect(self.on_toolchange_custom_clicked) self.form_fields.update({ "plot": self.ui.plot_cb, # "tooldia": self.ui.tooldia_entry, "append": self.ui.append_text, "prepend": self.ui.prepend_text, "toolchange_macro": self.ui.toolchange_text, "toolchange_macro_enable": self.ui.toolchange_cb }) # Fill form fields only on object create self.to_form() # this means that the object that created this CNCJob was an Excellon or Geometry try: if self.travel_distance: self.ui.t_distance_label.show() self.ui.t_distance_entry.setVisible(True) self.ui.t_distance_entry.setDisabled(True) self.ui.t_distance_entry.set_value('%.4f' % float(self.travel_distance)) self.ui.units_label.setText(str(self.units).lower()) self.ui.units_label.setDisabled(True) self.ui.t_time_label.show() self.ui.t_time_entry.setVisible(True) self.ui.t_time_entry.setDisabled(True) # if time is more than 1 then we have minutes, else we have seconds if self.routing_time > 1: self.ui.t_time_entry.set_value('%.4f' % math.ceil(float(self.routing_time))) self.ui.units_time_label.setText('min') else: time_r = self.routing_time * 60 self.ui.t_time_entry.set_value('%.4f' % math.ceil(float(time_r))) self.ui.units_time_label.setText('sec') self.ui.units_time_label.setDisabled(True) except AttributeError: pass # set the kind of geometries are plotted by default with plot2() from camlib.CNCJob self.ui.cncplot_method_combo.set_value(self.app.defaults["cncjob_plot_kind"]) try: self.ui.annotation_cb.stateChanged.disconnect(self.on_annotation_change) except (TypeError, AttributeError): pass self.ui.annotation_cb.stateChanged.connect(self.on_annotation_change) if self.app.is_legacy is False: # set if to display text annotations self.ui.annotation_cb.set_value(self.app.defaults["cncjob_annotation"]) # Show/Hide Advanced Options if self.app.defaults["global_app_level"] == 'b': self.ui.level.setText(_( 'Basic' )) self.ui.cnc_frame.hide() else: self.ui.level.setText(_( 'Advanced' )) self.ui.cnc_frame.show() self.ui.updateplot_button.clicked.connect(self.on_updateplot_button_click) self.ui.export_gcode_button.clicked.connect(self.on_exportgcode_button_click) self.ui.modify_gcode_button.clicked.connect(self.on_edit_code_click) self.ui.tc_variable_combo.currentIndexChanged[str].connect(self.on_cnc_custom_parameters) self.ui.cncplot_method_combo.activated_custom.connect(self.on_plot_kind_change) def on_cnc_custom_parameters(self, signal_text): if signal_text == 'Parameters': return else: self.ui.toolchange_text.insertPlainText('%%%s%%' % signal_text) def ui_connect(self): for row in range(self.ui.cnc_tools_table.rowCount()): self.ui.cnc_tools_table.cellWidget(row, 6).clicked.connect(self.on_plot_cb_click_table) self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click) def ui_disconnect(self): for row in range(self.ui.cnc_tools_table.rowCount()): self.ui.cnc_tools_table.cellWidget(row, 6).clicked.disconnect(self.on_plot_cb_click_table) try: self.ui.plot_cb.stateChanged.disconnect(self.on_plot_cb_click) except (TypeError, AttributeError): pass def on_updateplot_button_click(self, *args): """ Callback for the "Updata Plot" button. Reads the form for updates and plots the object. """ self.read_form() self.plot() def on_plot_kind_change(self): kind = self.ui.cncplot_method_combo.get_value() def worker_task(): with self.app.proc_container.new(_("Plotting...")): self.plot(kind=kind) self.app.worker_task.emit({'fcn': worker_task, 'params': []}) def on_exportgcode_button_click(self, *args): self.app.report_usage("cncjob_on_exportgcode_button") self.read_form() name = self.app.collection.get_active().options['name'] if 'Roland' in self.pp_excellon_name or 'Roland' in self.pp_geometry_name: _filter_ = "RML1 Files (*.rol);;" \ "All Files (*.*)" elif 'hpgl' in self.pp_geometry_name: _filter_ = "HPGL Files (*.plt);;" \ "All Files (*.*)" else: _filter_ = "G-Code Files (*.nc);;G-Code Files (*.txt);;G-Code Files (*.tap);;G-Code Files (*.cnc);;" \ "G-Code Files (*.g-code);;All Files (*.*)" try: dir_file_to_save = self.app.get_last_save_folder() + '/' + str(name) filename, _f = QtWidgets.QFileDialog.getSaveFileName( caption=_("Export Machine Code ..."), directory=dir_file_to_save, filter=_filter_ ) except TypeError: filename, _f = QtWidgets.QFileDialog.getSaveFileName(caption=_("Export Machine Code ..."), filter=_filter_) filename = str(filename) if filename == '': self.app.inform.emit('[WARNING_NOTCL] %s' % _("Export Machine Code cancelled ...")) return new_name = os.path.split(str(filename))[1].rpartition('.')[0] self.ui.name_entry.set_value(new_name) self.on_name_activate(silent=True) preamble = str(self.ui.prepend_text.get_value()) postamble = str(self.ui.append_text.get_value()) gc = self.export_gcode(filename, preamble=preamble, postamble=postamble) if gc == 'fail': return if self.app.defaults["global_open_style"] is False: self.app.file_opened.emit("gcode", filename) self.app.file_saved.emit("gcode", filename) self.app.inform.emit('[success] %s: %s' % (_("Machine Code file saved to"), filename)) def on_edit_code_click(self, *args): preamble = str(self.ui.prepend_text.get_value()) postamble = str(self.ui.append_text.get_value()) gco = self.export_gcode(preamble=preamble, postamble=postamble, to_file=True) if gco == 'fail': return else: self.app.gcode_edited = gco self.app.init_code_editor(name=_("Code Editor")) self.app.ui.buttonOpen.clicked.connect(self.app.handleOpen) self.app.ui.buttonSave.clicked.connect(self.app.handleSaveGCode) # then append the text from GCode to the text editor try: for line in self.app.gcode_edited: proc_line = str(line).strip('\n') self.app.ui.code_editor.append(proc_line) except Exception as e: log.debug('FlatCAMCNNJob.on_edit_code_click() -->%s' % str(e)) self.app.inform.emit('[ERROR] %s %s' % (_('FlatCAMCNNJob.on_edit_code_click() -->'), str(e))) return self.app.ui.code_editor.moveCursor(QtGui.QTextCursor.Start) self.app.handleTextChanged() self.app.ui.show() self.app.inform.emit('[success] %s...' % _('Loaded Machine Code into Code Editor')) def gcode_header(self): log.debug("FlatCAMCNCJob.gcode_header()") time_str = "{:%A, %d %B %Y at %H:%M}".format(datetime.now()) marlin = False hpgl = False probe_pp = False try: for key in self.cnc_tools: ppg = self.cnc_tools[key]['data']['ppname_g'] if ppg == 'marlin' or ppg == 'Repetier': marlin = True break if ppg == 'hpgl': hpgl = True break if "toolchange_probe" in ppg.lower(): probe_pp = True break except Exception as e: log.debug("FlatCAMCNCJob.gcode_header() error: --> %s" % str(e)) try: if self.options['ppname_e'] == 'marlin' or self.options['ppname_e'] == 'Repetier': marlin = True except Exception as e: log.debug("FlatCAMCNCJob.gcode_header(): --> There is no such self.option: %s" % str(e)) try: if "toolchange_probe" in self.options['ppname_e'].lower(): probe_pp = True except Exception as e: log.debug("FlatCAMCNCJob.gcode_header(): --> There is no such self.option: %s" % str(e)) if marlin is True: gcode = ';Marlin(Repetier) G-CODE GENERATED BY FLATCAM v%s - www.flatcam.org - Version Date: %s\n' % \ (str(self.app.version), str(self.app.version_date)) + '\n' gcode += ';Name: ' + str(self.options['name']) + '\n' gcode += ';Type: ' + "G-code from " + str(self.options['type']) + '\n' # if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry': # gcode += '(Tools in use: ' + str(p['options']['Tools_in_use']) + ')\n' gcode += ';Units: ' + self.units.upper() + '\n' + "\n" gcode += ';Created on ' + time_str + '\n' + '\n' elif hpgl is True: gcode = 'CO "HPGL CODE GENERATED BY FLATCAM v%s - www.flatcam.org - Version Date: %s' % \ (str(self.app.version), str(self.app.version_date)) + '";\n' gcode += 'CO "Name: ' + str(self.options['name']) + '";\n' gcode += 'CO "Type: ' + "HPGL code from " + str(self.options['type']) + '";\n' # if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry': # gcode += '(Tools in use: ' + str(p['options']['Tools_in_use']) + ')\n' gcode += 'CO "Units: ' + self.units.upper() + '";\n' gcode += 'CO "Created on ' + time_str + '";\n' elif probe_pp is True: gcode = '(G-CODE GENERATED BY FLATCAM v%s - www.flatcam.org - Version Date: %s)\n' % \ (str(self.app.version), str(self.app.version_date)) + '\n' gcode += '(This GCode tool change is done by using a Probe.)\n' \ '(Make sure that before you start the job you first do a rough zero for Z axis.)\n' \ '(This means that you need to zero the CNC axis and then jog to the toolchange X, Y location,)\n' \ '(mount the probe and adjust the Z so more or less the probe tip touch the plate. ' \ 'Then zero the Z axis.)\n' + '\n' gcode += '(Name: ' + str(self.options['name']) + ')\n' gcode += '(Type: ' + "G-code from " + str(self.options['type']) + ')\n' # if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry': # gcode += '(Tools in use: ' + str(p['options']['Tools_in_use']) + ')\n' gcode += '(Units: ' + self.units.upper() + ')\n' + "\n" gcode += '(Created on ' + time_str + ')\n' + '\n' else: gcode = '(G-CODE GENERATED BY FLATCAM v%s - www.flatcam.org - Version Date: %s)\n' % \ (str(self.app.version), str(self.app.version_date)) + '\n' gcode += '(Name: ' + str(self.options['name']) + ')\n' gcode += '(Type: ' + "G-code from " + str(self.options['type']) + ')\n' # if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry': # gcode += '(Tools in use: ' + str(p['options']['Tools_in_use']) + ')\n' gcode += '(Units: ' + self.units.upper() + ')\n' + "\n" gcode += '(Created on ' + time_str + ')\n' + '\n' return gcode def gcode_footer(self, end_command=None): """ :param end_command: 'M02' or 'M30' - String :return: """ if end_command: return end_command else: return 'M02' def export_gcode(self, filename=None, preamble='', postamble='', to_file=False): gcode = '' roland = False hpgl = False try: if self.special_group: self.app.inform.emit('[WARNING_NOTCL] %s %s %s.' % (_("This CNCJob object can't be processed because it is a"), str(self.special_group), _("CNCJob object"))) return 'fail' except AttributeError: pass # detect if using Roland postprocessor try: for key in self.cnc_tools: if self.cnc_tools[key]['data']['ppname_g'] == 'Roland_MDX_20': roland = True break if self.cnc_tools[key]['data']['ppname_g'] == 'hpgl': hpgl = True break except Exception as e: try: for key in self.cnc_tools: if self.cnc_tools[key]['data']['ppname_e'] == 'Roland_MDX_20': roland = True break except Exception as e: pass # do not add gcode_header when using the Roland postprocessor, add it for every other postprocessor if roland is False and hpgl is False: gcode = self.gcode_header() # detect if using multi-tool and make the Gcode summation correctly for each case if self.multitool is True: for tooluid_key in self.cnc_tools: for key, value in self.cnc_tools[tooluid_key].items(): if key == 'gcode': gcode += value break else: gcode += self.gcode if roland is True: g = preamble + gcode + postamble elif hpgl is True: g = self.gcode_header() + preamble + gcode + postamble else: # fix so the preamble gets inserted in between the comments header and the actual start of GCODE g_idx = gcode.rfind('G20') # if it did not find 'G20' then search for 'G21' if g_idx == -1: g_idx = gcode.rfind('G21') # if it did not find 'G20' and it did not find 'G21' then there is an error and return if g_idx == -1: self.app.inform.emit('[ERROR_NOTCL] %s' % _("G-code does not have a units code: either G20 or G21")) return g = gcode[:g_idx] + preamble + '\n' + gcode[g_idx:] + postamble + self.gcode_footer() # if toolchange custom is used, replace M6 code with the code from the Toolchange Custom Text box if self.ui.toolchange_cb.get_value() is True: # match = self.re_toolchange.search(g) if 'M6' in g: m6_code = self.parse_custom_toolchange_code(self.ui.toolchange_text.get_value()) if m6_code is None or m6_code == '': self.app.inform.emit('[ERROR_NOTCL] %s' % _("Cancelled. The Toolchange Custom code is enabled but it's empty." )) return 'fail' g = g.replace('M6', m6_code) self.app.inform.emit('[success] %s' % _("Toolchange G-code was replaced by a custom code.")) # lines = StringIO(self.gcode) lines = StringIO(g) # Write if filename is not None: try: with open(filename, 'w') as f: for line in lines: f.write(line) except FileNotFoundError: self.app.inform.emit('[WARNING_NOTCL] %s' % _("No such file or directory")) return except PermissionError: self.app.inform.emit('[WARNING] %s' % _("Permission denied, saving not possible.\n" "Most likely another app is holding the file open and not accessible.")) return 'fail' elif to_file is False: # Just for adding it to the recent files list. if self.app.defaults["global_open_style"] is False: self.app.file_opened.emit("cncjob", filename) self.app.file_saved.emit("cncjob", filename) self.app.inform.emit('[success] %s: %s' % (_("Saved to"), filename)) else: return lines def on_toolchange_custom_clicked(self, signal): try: if 'toolchange_custom' not in str(self.options['ppname_e']).lower(): if self.ui.toolchange_cb.get_value(): self.ui.toolchange_cb.set_value(False) self.app.inform.emit('[WARNING_NOTCL] %s' % _("The used postprocessor file has to have in it's name: 'toolchange_custom'")) except KeyError: try: for key in self.cnc_tools: ppg = self.cnc_tools[key]['data']['ppname_g'] if 'toolchange_custom' not in str(ppg).lower(): print(ppg) if self.ui.toolchange_cb.get_value(): self.ui.toolchange_cb.set_value(False) self.app.inform.emit('[WARNING_NOTCL] %s' % _("The used postprocessor file has to have in it's name: " "'toolchange_custom'")) except KeyError: self.app.inform.emit('[ERROR] %s' % _("There is no postprocessor file.")) def get_gcode(self, preamble='', postamble=''): # we need this to be able get_gcode separatelly for shell command export_gcode return preamble + '\n' + self.gcode + "\n" + postamble def get_svg(self): # we need this to be able get_svg separately for shell command export_svg pass def on_plot_cb_click(self, *args): if self.muted_ui: return kind = self.ui.cncplot_method_combo.get_value() self.plot(kind=kind) self.read_form_item('plot') self.ui_disconnect() cb_flag = self.ui.plot_cb.isChecked() for row in range(self.ui.cnc_tools_table.rowCount()): table_cb = self.ui.cnc_tools_table.cellWidget(row, 6) if cb_flag: table_cb.setChecked(True) else: table_cb.setChecked(False) self.ui_connect() def on_plot_cb_click_table(self): # self.ui.cnc_tools_table.cellWidget(row, 2).widget().setCheckState(QtCore.Qt.Unchecked) self.ui_disconnect() # cw = self.sender() # cw_index = self.ui.cnc_tools_table.indexAt(cw.pos()) # cw_row = cw_index.row() kind = self.ui.cncplot_method_combo.get_value() self.shapes.clear(update=True) for tooluid_key in self.cnc_tools: tooldia = float('%.4f' % float(self.cnc_tools[tooluid_key]['tooldia'])) gcode_parsed = self.cnc_tools[tooluid_key]['gcode_parsed'] # tool_uid = int(self.ui.cnc_tools_table.item(cw_row, 3).text()) for r in range(self.ui.cnc_tools_table.rowCount()): if int(self.ui.cnc_tools_table.item(r, 5).text()) == int(tooluid_key): if self.ui.cnc_tools_table.cellWidget(r, 6).isChecked(): self.plot2(tooldia=tooldia, obj=self, visible=True, gcode_parsed=gcode_parsed, kind=kind) self.shapes.redraw() # make sure that the general plot is disabled if one of the row plot's are disabled and # if all the row plot's are enabled also enable the general plot checkbox cb_cnt = 0 total_row = self.ui.cnc_tools_table.rowCount() for row in range(total_row): if self.ui.cnc_tools_table.cellWidget(row, 6).isChecked(): cb_cnt += 1 else: cb_cnt -= 1 if cb_cnt < total_row: self.ui.plot_cb.setChecked(False) else: self.ui.plot_cb.setChecked(True) self.ui_connect() def plot(self, visible=None, kind='all'): # Does all the required setup and returns False # if the 'ptint' option is set to False. if not FlatCAMObj.plot(self): return visible = visible if visible else self.options['plot'] if self.app.is_legacy is False: if self.ui.annotation_cb.get_value() and self.ui.plot_cb.get_value(): self.text_col.enabled = True else: self.text_col.enabled = False self.annotation.redraw() try: if self.multitool is False: # single tool usage try: dia_plot = float(self.options["tooldia"]) except ValueError: # we may have a tuple with only one element and a comma dia_plot = [float(el) for el in self.options["tooldia"].split(',') if el != ''][0] self.plot2(dia_plot, obj=self, visible=visible, kind=kind) else: # multiple tools usage for tooluid_key in self.cnc_tools: tooldia = float('%.4f' % float(self.cnc_tools[tooluid_key]['tooldia'])) gcode_parsed = self.cnc_tools[tooluid_key]['gcode_parsed'] self.plot2(tooldia=tooldia, obj=self, visible=visible, gcode_parsed=gcode_parsed, kind=kind) self.shapes.redraw() except (ObjectDeleted, AttributeError): self.shapes.clear(update=True) if self.app.is_legacy is False: self.annotation.clear(update=True) def on_annotation_change(self): if self.app.is_legacy is False: if self.ui.annotation_cb.get_value(): self.text_col.enabled = True else: self.text_col.enabled = False # kind = self.ui.cncplot_method_combo.get_value() # self.plot(kind=kind) self.annotation.redraw() else: self.inform.emit(_("Not available with the current Graphic Engine Legacy(2D).")) def convert_units(self, units): log.debug("FlatCAMObj.FlatCAMECNCjob.convert_units()") factor = CNCjob.convert_units(self, units) self.options["tooldia"] = float(self.options["tooldia"]) * factor param_list = ['cutz', 'depthperpass', 'travelz', 'feedrate', 'feedrate_z', 'feedrate_rapid', 'endz', 'toolchangez'] temp_tools_dict = {} tool_dia_copy = {} data_copy = {} for tooluid_key, tooluid_value in self.cnc_tools.items(): for dia_key, dia_value in tooluid_value.items(): if dia_key == 'tooldia': dia_value *= factor dia_value = float('%.4f' % dia_value) tool_dia_copy[dia_key] = dia_value if dia_key == 'offset': tool_dia_copy[dia_key] = dia_value if dia_key == 'offset_value': dia_value *= factor tool_dia_copy[dia_key] = dia_value if dia_key == 'type': tool_dia_copy[dia_key] = dia_value if dia_key == 'tool_type': tool_dia_copy[dia_key] = dia_value if dia_key == 'data': for data_key, data_value in dia_value.items(): # convert the form fields that are convertible for param in param_list: if data_key == param and data_value is not None: data_copy[data_key] = data_value * factor # copy the other dict entries that are not convertible if data_key not in param_list: data_copy[data_key] = data_value tool_dia_copy[dia_key] = deepcopy(data_copy) data_copy.clear() if dia_key == 'gcode': tool_dia_copy[dia_key] = dia_value if dia_key == 'gcode_parsed': tool_dia_copy[dia_key] = dia_value if dia_key == 'solid_geometry': tool_dia_copy[dia_key] = dia_value # if dia_key == 'solid_geometry': # tool_dia_copy[dia_key] = affinity.scale(dia_value, xfact=factor, origin=(0, 0)) # if dia_key == 'gcode_parsed': # for g in dia_value: # g['geom'] = affinity.scale(g['geom'], factor, factor, origin=(0, 0)) # # tool_dia_copy['gcode_parsed'] = deepcopy(dia_value) # tool_dia_copy['solid_geometry'] = cascaded_union([geo['geom'] for geo in dia_value]) temp_tools_dict.update({ tooluid_key: deepcopy(tool_dia_copy) }) tool_dia_copy.clear() self.cnc_tools.clear() self.cnc_tools = deepcopy(temp_tools_dict) # end of file