From 67b0a81f179dda913c157eabf2cd90c196e29913 Mon Sep 17 00:00:00 2001 From: Marius Stanciu Date: Fri, 4 Oct 2019 02:59:11 +0300 Subject: [PATCH] - updated the Film Tool and added the ability to generate Punched Positive films (holes in the pads) when a Gerber file is the film's source. The punch holes source can be either an Excellon file or the pads center --- README.md | 4 + flatcamTools/ToolFilm.py | 315 +++++++++++++++++++++++++++++++-------- 2 files changed, 257 insertions(+), 62 deletions(-) diff --git a/README.md b/README.md index 82a0abec..526fe484 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,10 @@ CAD program, and create G-Code for Isolation routing. ================================================= +4.10.2019 + +- updated the Film Tool and added the ability to generate Punched Positive films (holes in the pads) when a Gerber file is the film's source. The punch holes source can be either an Excellon file or the pads center + 3.10.2019 - previously I've added the initial layout for the FlatCAMDocument object diff --git a/flatcamTools/ToolFilm.py b/flatcamTools/ToolFilm.py index dbd39bca..0fe96c19 100644 --- a/flatcamTools/ToolFilm.py +++ b/flatcamTools/ToolFilm.py @@ -7,10 +7,14 @@ # ########################################################## ## from FlatCAMTool import FlatCAMTool +from FlatCAMObj import * -from flatcamGUI.GUIElements import RadioSet, FCEntry +from flatcamGUI.GUIElements import RadioSet, FCDoubleSpinner, FCCheckBox, \ + OptionalHideInputSection, OptionalInputSection from PyQt5 import QtGui, QtCore, QtWidgets +from copy import deepcopy + import gettext import FlatCAMTranslation as fcTranslate import builtins @@ -39,8 +43,11 @@ class Film(FlatCAMTool): self.layout.addWidget(title_label) # Form Layout - tf_form_layout = QtWidgets.QFormLayout() - self.layout.addLayout(tf_form_layout) + grid0 = QtWidgets.QGridLayout() + self.layout.addLayout(grid0) + + grid0.setColumnStretch(0, 0) + grid0.setColumnStretch(1, 1) # Type of object for which to create the film self.tf_type_obj_combo = QtWidgets.QComboBox() @@ -60,7 +67,8 @@ class Film(FlatCAMTool): "The selection here decide the type of objects that will be\n" "in the Film Object combobox.") ) - tf_form_layout.addRow(self.tf_type_obj_combo_label, self.tf_type_obj_combo) + grid0.addWidget(self.tf_type_obj_combo_label, 0, 0) + grid0.addWidget(self.tf_type_obj_combo, 0, 1) # List of objects for which we can create the film self.tf_object_combo = QtWidgets.QComboBox() @@ -72,7 +80,8 @@ class Film(FlatCAMTool): self.tf_object_label.setToolTip( _("Object for which to create the film.") ) - tf_form_layout.addRow(self.tf_object_label, self.tf_object_combo) + grid0.addWidget(self.tf_object_label, 1, 0) + grid0.addWidget(self.tf_object_combo, 1, 1) # Type of Box Object to be used as an envelope for film creation # Within this we can create negative @@ -93,7 +102,8 @@ class Film(FlatCAMTool): "The selection here decide the type of objects that will be\n" "in the Box Object combobox.") ) - tf_form_layout.addRow(self.tf_type_box_combo_label, self.tf_type_box_combo) + grid0.addWidget(self.tf_type_box_combo_label, 2, 0) + grid0.addWidget(self.tf_type_box_combo, 2, 1) # Box self.tf_box_combo = QtWidgets.QComboBox() @@ -108,11 +118,28 @@ class Film(FlatCAMTool): "Usually it is the PCB outline but it can be also the\n" "same object for which the film is created.") ) - tf_form_layout.addRow(self.tf_box_combo_label, self.tf_box_combo) + grid0.addWidget(self.tf_box_combo_label, 3, 0) + grid0.addWidget(self.tf_box_combo, 3, 1) + + # Scale Stroke size + self.film_scale_entry = FCDoubleSpinner() + self.film_scale_entry.set_range(-999.9999, 999.9999) + + self.film_scale_label = QtWidgets.QLabel('%s:' % _("Scale Stroke")) + self.film_scale_label.setToolTip( + _("Scale the line stroke thickness of each feature in the SVG file.\n" + "It means that the line that envelope each SVG feature will be thicker or thinner,\n" + "therefore the fine features may be more affected by this parameter.") + ) + grid0.addWidget(self.film_scale_label, 4, 0) + grid0.addWidget(self.film_scale_entry, 4, 1) + + grid0.addWidget(QtWidgets.QLabel(''), 5, 0) # Film Type self.film_type = RadioSet([{'label': _('Positive'), 'value': 'pos'}, - {'label': _('Negative'), 'value': 'neg'}]) + {'label': _('Negative'), 'value': 'neg'}], + stretch=False) self.film_type_label = QtWidgets.QLabel(_("Film Type:")) self.film_type_label.setToolTip( _("Generate a Positive black film or a Negative film.\n" @@ -122,11 +149,12 @@ class Film(FlatCAMTool): "with white on a black canvas.\n" "The Film format is SVG.") ) - tf_form_layout.addRow(self.film_type_label, self.film_type) + grid0.addWidget(self.film_type_label, 6, 0) + grid0.addWidget(self.film_type, 6, 1) # Boundary for negative film generation - - self.boundary_entry = FCEntry() + self.boundary_entry = FCDoubleSpinner() + self.boundary_entry.set_range(-999.9999, 999.9999) self.boundary_label = QtWidgets.QLabel('%s:' % _("Border")) self.boundary_label.setToolTip( _("Specify a border around the object.\n" @@ -138,21 +166,72 @@ class Film(FlatCAMTool): "white color like the rest and which may confound with the\n" "surroundings if not for this border.") ) - tf_form_layout.addRow(self.boundary_label, self.boundary_entry) + grid0.addWidget(self.boundary_label, 7, 0) + grid0.addWidget(self.boundary_entry, 7, 1) - self.film_scale_entry = FCEntry() - self.film_scale_label = QtWidgets.QLabel('%s:' % _("Scale Stroke")) - self.film_scale_label.setToolTip( - _("Scale the line stroke thickness of each feature in the SVG file.\n" - "It means that the line that envelope each SVG feature will be thicker or thinner,\n" - "therefore the fine features may be more affected by this parameter.") + self.boundary_label.hide() + self.boundary_entry.hide() + + # Punch Drill holes + self.punch_cb = FCCheckBox(_("Punch drill holes")) + self.punch_cb.setToolTip(_("When checked the generated film will have holes in pads when\n" + "the generated film is positive. This is done to help drilling,\n" + "when done manually.")) + grid0.addWidget(self.punch_cb, 8, 0, 1, 2) + + # this way I can hide/show the frame + self.punch_frame = QtWidgets.QFrame() + self.punch_frame.setContentsMargins(0, 0, 0, 0) + self.layout.addWidget(self.punch_frame) + punch_grid = QtWidgets.QGridLayout() + punch_grid.setContentsMargins(0, 0, 0, 0) + self.punch_frame.setLayout(punch_grid) + + punch_grid.setColumnStretch(0, 0) + punch_grid.setColumnStretch(1, 1) + + self.ois_p = OptionalHideInputSection(self.punch_cb, [self.punch_frame]) + + self.source_label = QtWidgets.QLabel('%s:' % _("Source")) + self.source_label.setToolTip( + _("The punch hole source can be:\n" + "- Excellon -> an Excellon holes center will serve as reference.\n" + "- Pad Center -> will try to use the pads center as reference.") ) - tf_form_layout.addRow(self.film_scale_label, self.film_scale_entry) + self.source_punch = RadioSet([{'label': _('Excellon'), 'value': 'exc'}, + {'label': _('Pad center'), 'value': 'pad'}], + stretch=False) + punch_grid.addWidget(self.source_label, 0, 0) + punch_grid.addWidget(self.source_punch, 0, 1) + + self.exc_label = QtWidgets.QLabel('%s:' % _("Excellon Obj")) + self.exc_label.setToolTip( + _("Remove the geometry of Excellon from the Film to create tge holes in pads.") + ) + self.exc_combo = QtWidgets.QComboBox() + self.exc_combo.setModel(self.app.collection) + self.exc_combo.setRootModelIndex(self.app.collection.index(1, 0, QtCore.QModelIndex())) + self.exc_combo.setCurrentIndex(1) + punch_grid.addWidget(self.exc_label, 1, 0) + punch_grid.addWidget(self.exc_combo, 1, 1) + + self.exc_label.hide() + self.exc_combo.hide() + + self.punch_size_label = QtWidgets.QLabel('%s:' % _("Punch Size")) + self.punch_size_label.setToolTip(_("The value here will control how big is the punch hole in the pads.")) + self.punch_size_spinner = FCDoubleSpinner() + self.punch_size_spinner.set_range(0, 999.9999) + + punch_grid.addWidget(self.punch_size_label, 2, 0) + punch_grid.addWidget(self.punch_size_spinner, 2, 1) + + self.punch_size_label.hide() + self.punch_size_spinner.hide() # Buttons hlay = QtWidgets.QHBoxLayout() self.layout.addLayout(hlay) - hlay.addStretch() self.film_object_button = QtWidgets.QPushButton(_("Save Film")) self.film_object_button.setToolTip( @@ -170,6 +249,9 @@ class Film(FlatCAMTool): self.tf_type_obj_combo.currentIndexChanged.connect(self.on_type_obj_index_changed) self.tf_type_box_combo.currentIndexChanged.connect(self.on_type_box_index_changed) + self.film_type.activated_custom.connect(self.on_film_type) + self.source_punch.activated_custom.connect(self.on_punch_source) + def on_type_obj_index_changed(self, index): obj_type = self.tf_type_obj_combo.currentIndex() self.tf_object_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex())) @@ -216,6 +298,7 @@ class Film(FlatCAMTool): f_type = self.app.defaults["tools_film_type"] if self.app.defaults["tools_film_type"] else 'neg' self.film_type.set_value(str(f_type)) + self.on_film_type(val=f_type) b_entry = self.app.defaults["tools_film_boundary"] if self.app.defaults["tools_film_boundary"] else 0.0 self.boundary_entry.set_value(float(b_entry)) @@ -223,7 +306,41 @@ class Film(FlatCAMTool): scale_stroke_width = self.app.defaults["tools_film_scale"] if self.app.defaults["tools_film_scale"] else 0.0 self.film_scale_entry.set_value(int(scale_stroke_width)) + self.punch_cb.set_value(False) + self.source_punch.set_value('exc') + + def on_film_type(self, val): + type_of_film = val + + if type_of_film == 'neg': + self.boundary_label.show() + self.boundary_entry.show() + self.punch_cb.hide() + else: + self.boundary_label.hide() + self.boundary_entry.hide() + self.punch_cb.show() + + def on_punch_source(self, val): + if val == 'pad' and self.punch_cb.get_value(): + self.punch_size_label.show() + self.punch_size_spinner.show() + self.exc_label.hide() + self.exc_combo.hide() + else: + self.punch_size_label.hide() + self.punch_size_spinner.hide() + self.exc_label.show() + self.exc_combo.show() + + if val == 'pad' and self.tf_type_obj_combo.currentText() == 'Geometry': + self.source_punch.set_value('exc') + self.app.inform.emit('[WARNING_NOTCL] %s' % _("Using the Pad center does not work on Geometry objects. " + "Only a Gerber object has pads.")) + def on_film_creation(self): + log.debug("ToolFilm.Film.on_film_creation() started ...") + try: name = self.tf_object_combo.currentText() except Exception as e: @@ -238,59 +355,133 @@ class Film(FlatCAMTool): _("No FlatCAM object selected. Load an object for Box and retry.")) return - try: - border = float(self.boundary_entry.get_value()) - except ValueError: - # try to convert comma to decimal point. if it's still not working error message and return - try: - border = float(self.boundary_entry.get_value().replace(',', '.')) - except ValueError: - self.app.inform.emit('[ERROR_NOTCL] %s' % _("Wrong value format entered, use a number.")) - return + scale_stroke_width = float(self.film_scale_entry.get_value()) - try: - scale_stroke_width = int(self.film_scale_entry.get_value()) - except ValueError: - self.app.inform.emit('[ERROR_NOTCL] %s' % _("Wrong value format entered, use a number.")) - return + source = self.source_punch.get_value() - if border is None: - border = 0 + # ################################################################# + # ################ STARTING THE JOB ############################### + # ################################################################# self.app.inform.emit(_("Generating Film ...")) if self.film_type.get_value() == "pos": - try: - filename, _f = QtWidgets.QFileDialog.getSaveFileName( - caption=_("Export SVG positive"), - directory=self.app.get_last_save_folder() + '/' + name, - filter="*.svg") - except TypeError: - filename, _f = QtWidgets.QFileDialog.getSaveFileName(caption=_("Export SVG positive")) - filename = str(filename) - - if str(filename) == "": - self.app.inform.emit('[WARNING_NOTCL] %s' % _("Export SVG positive cancelled.")) - return + if self.punch_cb.get_value() is False: + self.generate_positive_normal_film(name, boxname, factor=scale_stroke_width) else: - self.app.export_svg_positive(name, boxname, filename, scale_factor=scale_stroke_width) + self.generate_positive_punched_film(name, boxname, source, factor=scale_stroke_width) else: + self.generate_negative_film(name, boxname, factor=scale_stroke_width) + + def generate_positive_normal_film(self, name, boxname, factor): + log.debug("ToolFilm.Film.generate_positive_normal_film() started ...") + try: + filename, _f = QtWidgets.QFileDialog.getSaveFileName( + caption=_("Export SVG positive"), + directory=self.app.get_last_save_folder() + '/' + name, + filter="*.svg") + except TypeError: + filename, _f = QtWidgets.QFileDialog.getSaveFileName(caption=_("Export SVG positive")) + + filename = str(filename) + + if str(filename) == "": + self.app.inform.emit('[WARNING_NOTCL] %s' % _("Export SVG positive cancelled.")) + return + else: + self.app.export_svg_positive(name, boxname, filename, scale_factor=factor) + + def generate_positive_punched_film(self, name, boxname, source, factor): + + film_obj = self.app.collection.get_by_name(name) + + if source == 'exc': + log.debug("ToolFilm.Film.generate_positive_punched_film() with Excellon source started ...") + try: - filename, _f = QtWidgets.QFileDialog.getSaveFileName( - caption=_("Export SVG negative"), - directory=self.app.get_last_save_folder() + '/' + name, - filter="*.svg") - except TypeError: - filename, _f = QtWidgets.QFileDialog.getSaveFileName(caption=_("Export SVG negative")) - - filename = str(filename) - - if str(filename) == "": - self.app.inform.emit('[WARNING_NOTCL] %s' % _("Export SVG negative cancelled.")) + exc_name = self.exc_combo.currentText() + except Exception as e: + self.app.inform.emit('[ERROR_NOTCL] %s' % + _("No Excellon object selected. Load an object for punching reference and retry.")) return - else: - self.app.export_svg_negative(name, boxname, filename, border, scale_factor=scale_stroke_width) + + exc_obj = self.app.collection.get_by_name(exc_name) + exc_solid_geometry = MultiPolygon(exc_obj.solid_geometry) + punched_solid_geometry = MultiPolygon(film_obj.solid_geometry).difference(exc_solid_geometry) + + def init_func(new_obj, app_obj): + new_obj.solid_geometry = deepcopy(punched_solid_geometry) + + outname = name + "_punched" + self.app.new_object('gerber', outname, init_func) + + self.generate_positive_normal_film(outname, boxname, factor=factor) + else: + log.debug("ToolFilm.Film.generate_positive_punched_film() with Pad center source started ...") + + punch_size = float(self.punch_size_spinner.get_value()) + + punching_geo = list() + for apid in film_obj.apertures: + if film_obj.apertures[apid]['type'] == 'C': + if punch_size >= float(film_obj.apertures[apid]['size']): + self.app.inform.emit('[ERROR_NOTCL] %s' % + _(" Could not generate punched hole film because the punch hole size" + "is bigger than some of the apertures in the Gerber object.")) + return 'fail' + else: + for elem in film_obj.apertures[apid]['geometry']: + if 'follow' in elem: + if isinstance(elem['follow'], Point): + punching_geo.append(elem['follow'].buffer(punch_size / 2)) + else: + if punch_size >= float(film_obj.apertures[apid]['width']) or \ + punch_size >= float(film_obj.apertures[apid]['height']): + self.app.inform.emit('[ERROR_NOTCL] %s' % + _(" Could not generate punched hole film because the punch hole size" + "is bigger than some of the apertures in the Gerber object.")) + return 'fail' + else: + for elem in film_obj.apertures[apid]['geometry']: + if 'follow' in elem: + if isinstance(elem['follow'], Point): + punching_geo.append(elem['follow'].buffer(punch_size / 2)) + + punching_geo = MultiPolygon(punching_geo) + punched_solid_geometry = MultiPolygon(film_obj.solid_geometry).difference(punching_geo) + + def init_func(new_obj, app_obj): + new_obj.solid_geometry = deepcopy(punched_solid_geometry) + + outname = name + "_punched" + self.app.new_object('gerber', outname, init_func) + + self.generate_positive_normal_film(outname, boxname, factor=factor) + + def generate_negative_film(self, name, boxname, factor): + log.debug("ToolFilm.Film.generate_negative_film() started ...") + + border = float(self.boundary_entry.get_value()) + + if border is None: + border = 0 + + try: + filename, _f = QtWidgets.QFileDialog.getSaveFileName( + caption=_("Export SVG negative"), + directory=self.app.get_last_save_folder() + '/' + name, + filter="*.svg") + except TypeError: + filename, _f = QtWidgets.QFileDialog.getSaveFileName(caption=_("Export SVG negative")) + + filename = str(filename) + + if str(filename) == "": + self.app.inform.emit('[WARNING_NOTCL] %s' % _("Export SVG negative cancelled.")) + return + else: + self.app.export_svg_negative(name, boxname, filename, border, scale_factor=factor) def reset_fields(self): self.tf_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))