diff --git a/CHANGELOG.md b/CHANGELOG.md index f99cf076..a353146a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ CHANGELOG for FlatCAM beta 24.09.2021 - in Extract Plugin some minor UI changes +- in Film Plugin added new parameters and improvements: now the negative film can have a box that is convex and it is no longer limited to square shapes. Also, if the box object has only one geometric element (an outline) then that one will be the final shape of the negative 21.09.2021 diff --git a/appGUI/preferences/PreferencesUIManager.py b/appGUI/preferences/PreferencesUIManager.py index fb2ea956..76801c57 100644 --- a/appGUI/preferences/PreferencesUIManager.py +++ b/appGUI/preferences/PreferencesUIManager.py @@ -473,6 +473,8 @@ class PreferencesUIManager: "tools_2sided_allign_axis": self.ui.plugin_eng_pref_form.tools_2sided_group.align_axis_radio, # Film Tool + "tools_film_shape": self.ui.plugin_pref_form.tools_film_group.convex_box_cb, + "tools_film_rounded": self.ui.plugin_pref_form.tools_film_group.rounded_cb, "tools_film_polarity": self.ui.plugin_pref_form.tools_film_group.film_type_radio, "tools_film_boundary": self.ui.plugin_pref_form.tools_film_group.film_boundary_entry, "tools_film_scale_stroke": self.ui.plugin_pref_form.tools_film_group.film_scale_stroke_entry, diff --git a/appGUI/preferences/tools/ToolsFilmPrefGroupUI.py b/appGUI/preferences/tools/ToolsFilmPrefGroupUI.py index c8efce21..2347c10a 100644 --- a/appGUI/preferences/tools/ToolsFilmPrefGroupUI.py +++ b/appGUI/preferences/tools/ToolsFilmPrefGroupUI.py @@ -184,14 +184,38 @@ class ToolsFilmPrefGroupUI(OptionsGroupUI): grid_par = FCGridLayout() par_frame.setLayout(grid_par) + # Convex Shape + # Surrounding convex box shape + self.convex_box_label = FCLabel('%s:' % _("Convex Shape")) + self.convex_box_label.setToolTip( + _("Create a convex shape surrounding the entire PCB.\n" + "If not checked the shape is rectangular.") + ) + self.convex_box_cb = FCCheckBox() + + grid_par.addWidget(self.convex_box_label, 0, 0) + grid_par.addWidget(self.convex_box_cb, 0, 1) + + # Rounded corners + self.rounded_label = FCLabel('%s:' % _("Rounded")) + self.rounded_label.setToolTip( + _("Resulting geometry will have rounded corners.") + ) + + self.rounded_cb = FCCheckBox() + + grid_par.addWidget(self.rounded_label, 2, 0) + grid_par.addWidget(self.rounded_cb, 2, 1) + + # Polarity self.film_type_radio = RadioSet([{'label': 'Pos', 'value': 'pos'}, {'label': 'Neg', 'value': 'neg'}]) ftypelbl = FCLabel('%s:' % _('Polarity')) ftypelbl.setToolTip( _("Generate a Positive black film or a Negative film.") ) - grid_par.addWidget(ftypelbl, 0, 0) - grid_par.addWidget(self.film_type_radio, 0, 1) + grid_par.addWidget(ftypelbl, 4, 0) + grid_par.addWidget(self.film_type_radio, 4, 1) # Film Color self.film_color_label = FCLabel('%s:' % _('Film Color')) @@ -200,8 +224,8 @@ class ToolsFilmPrefGroupUI(OptionsGroupUI): ) self.film_color_entry = FCColorEntry() - grid_par.addWidget(self.film_color_label, 2, 0) - grid_par.addWidget(self.film_color_entry, 2, 1) + grid_par.addWidget(self.film_color_label, 6, 0) + grid_par.addWidget(self.film_color_entry, 6, 1) # Film Border self.film_boundary_entry = FCDoubleSpinner() @@ -220,9 +244,10 @@ class ToolsFilmPrefGroupUI(OptionsGroupUI): "white color like the rest and which may confound with the\n" "surroundings if not for this border.") ) - grid_par.addWidget(self.film_boundary_label, 4, 0) - grid_par.addWidget(self.film_boundary_entry, 4, 1) + grid_par.addWidget(self.film_boundary_label, 8, 0) + grid_par.addWidget(self.film_boundary_entry, 8, 1) + # Scale Stroke self.film_scale_stroke_entry = FCDoubleSpinner() self.film_scale_stroke_entry.set_precision(self.decimals) self.film_scale_stroke_entry.set_range(0, 10000.0000) @@ -234,8 +259,8 @@ class ToolsFilmPrefGroupUI(OptionsGroupUI): "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.") ) - grid_par.addWidget(self.film_scale_stroke_label, 6, 0) - grid_par.addWidget(self.film_scale_stroke_entry, 6, 1) + grid_par.addWidget(self.film_scale_stroke_label, 10, 0) + grid_par.addWidget(self.film_scale_stroke_entry, 10, 1) self.file_type_radio = RadioSet([{'label': _('SVG'), 'value': 'svg'}, {'label': _('PNG'), 'value': 'png'}, @@ -249,8 +274,8 @@ class ToolsFilmPrefGroupUI(OptionsGroupUI): "- 'PNG' -> raster image\n" "- 'PDF' -> portable document format") ) - grid_par.addWidget(self.file_type_label, 8, 0) - grid_par.addWidget(self.file_type_radio, 8, 1) + grid_par.addWidget(self.file_type_label, 12, 0) + grid_par.addWidget(self.file_type_radio, 12, 1) # Page orientation self.orientation_label = FCLabel('%s:' % _("Page Orientation")) @@ -262,8 +287,8 @@ class ToolsFilmPrefGroupUI(OptionsGroupUI): {'label': _('Landscape'), 'value': 'l'}, ], stretch=False) - grid_par.addWidget(self.orientation_label, 10, 0) - grid_par.addWidget(self.orientation_radio, 10, 1) + grid_par.addWidget(self.orientation_label, 14, 0) + grid_par.addWidget(self.orientation_radio, 14, 1) # Page Size self.pagesize_label = FCLabel('%s:' % _("Page Size")) @@ -328,8 +353,8 @@ class ToolsFilmPrefGroupUI(OptionsGroupUI): page_size_list = list(self.pagesize.keys()) self.pagesize_combo.addItems(page_size_list) - grid_par.addWidget(self.pagesize_label, 12, 0) - grid_par.addWidget(self.pagesize_combo, 12, 1) + grid_par.addWidget(self.pagesize_label, 16, 0) + grid_par.addWidget(self.pagesize_combo, 16, 1) # PNG DPI self.png_dpi_label = FCLabel('%s:' % "PNG DPI") @@ -339,8 +364,8 @@ class ToolsFilmPrefGroupUI(OptionsGroupUI): self.png_dpi_spinner = FCSpinner() self.png_dpi_spinner.set_range(0, 100000) - grid_par.addWidget(self.png_dpi_label, 14, 0) - grid_par.addWidget(self.png_dpi_spinner, 14, 1) + grid_par.addWidget(self.png_dpi_label, 18, 0) + grid_par.addWidget(self.png_dpi_spinner, 18, 1) self.layout.addStretch(1) diff --git a/appPlugins/ToolFilm.py b/appPlugins/ToolFilm.py index abbb558d..5e233e27 100644 --- a/appPlugins/ToolFilm.py +++ b/appPlugins/ToolFilm.py @@ -14,7 +14,7 @@ from appGUI.GUIElements import RadioSet, FCDoubleSpinner, FCCheckBox, \ from copy import deepcopy import logging -from shapely.geometry import Polygon, MultiPolygon, Point +from shapely.geometry import Polygon, MultiPolygon, Point, LineString, LinearRing import shapely.affinity as affinity from shapely.ops import unary_union @@ -211,6 +211,9 @@ class Film(AppTool): self.ui.png_dpi_spinner.set_value(self.app.defaults["tools_film_png_dpi"]) + self.ui.convex_box_cb.set_value(self.app.defaults["tools_film_shape"]) + self.ui.rounded_cb.set_value(self.app.defaults["tools_film_rounded"]) + obj = self.app.collection.get_active() if obj: obj_name = obj.options['name'] @@ -483,6 +486,9 @@ class Film(AppTool): def generate_negative_film(self, name, boxname, factor, ftype='svg'): log.debug("ToolFilm.Film.generate_negative_film() started ...") + use_convex_hull = self.ui.convex_box_cb.get_value() + rounded_box = self.ui.rounded_cb.get_value() + scale_factor_x = 1 scale_factor_y = 1 skew_factor_x = None @@ -546,7 +552,9 @@ class Film(AppTool): scale_reference=scale_reference, skew_factor_x=skew_factor_x, skew_factor_y=skew_factor_y, skew_reference=skew_reference, - mirror=mirror, ftype=ftype + mirror=mirror, ftype=ftype, + use_convex_hull=use_convex_hull, + rounded_box=rounded_box ) def export_negative(self, obj_name, box_name, filename, boundary, @@ -554,7 +562,7 @@ class Film(AppTool): scale_factor_x=1, scale_factor_y=1, scale_reference='center', skew_factor_x=None, skew_factor_y=None, skew_reference='center', mirror=None, opacity_val=1.0, - use_thread=True, ftype='svg'): + use_thread=True, ftype='svg', use_convex_hull=False, rounded_box=False): """ Exports a Geometry Object to an SVG file in negative. @@ -576,6 +584,9 @@ class Film(AppTool): :param opacity_val: :param use_thread: if to be run in a separate thread; boolean :param ftype: the type of file for saving the film: 'svg', 'png' or 'pdf' + :param use_convex_hull: Bool; if True it will make the negative box to minimize the black coverage + :param rounded_box: Bool; if True the negative bounded box will have rounded corners + Works only in case the object used as box has multiple geometries :return: """ self.app.defaults.report_usage("export_negative()") @@ -602,16 +613,16 @@ class Film(AppTool): scale_factor_x = scale_factor_x scale_factor_y = scale_factor_y - def get_complementary(color): + def get_complementary(color_param): # strip the # from the beginning - color = color[1:] + our_color = color_param[1:] # convert the string into hex - color = int(color, 16) + our_color = int(our_color, 16) # invert the three bytes # as good as substracting each of RGB component by 255(FF) - comp_color = 0xFFFFFF ^ color + comp_color = 0xFFFFFF ^ our_color # convert the color back to hex by prefixing a # comp_color = "#%06X" % comp_color @@ -624,7 +635,7 @@ class Film(AppTool): color = obj.options['tools_film_color'] transparency_level = opacity_val - def make_negative_film(color, transparency_level, scale_factor_x, scale_factor_y): + def make_negative_film(color, transparency_level, scale_factor_x, scale_factor_y, use_convex_hull, rounded_box): log.debug("FilmTool.export_negative().make_negative_film()") self.screen_dpi = self.app.qapp.screens()[0].logicalDotsPerInch() @@ -636,6 +647,8 @@ class Film(AppTool): scale_factor_x += dpi_rate scale_factor_y += dpi_rate + # ######################################################################################################## + # the case when the BOX object is a Geometry Object if box.kind.lower() == 'geometry': flat_geo = [] if box.multigeo: @@ -713,7 +726,7 @@ class Film(AppTool): svgheight = str(size[1] + (2 * boundary)) minx = str(bounds[0] - boundary) miny = str(bounds[1] + boundary + size[1]) - miny_rect = str(bounds[1] - boundary) + # miny_rect = str(bounds[1] - boundary) # Add a SVG Header and footer to the svg output from shapely # The transform flips the Y Axis so that everything renders @@ -741,12 +754,46 @@ class Film(AppTool): # first_svg_elem += 'width="' + svgwidth + '" ' + 'height="' + svgheight + '" ' # first_svg_elem += 'fill="#000000" opacity="1.0" stroke-width="0.0"' - first_svg_elem_tag = 'rect' + # first_svg_elem_tag = 'rect' + # first_svg_elem_attribs = { + # 'x': minx, + # 'y': miny_rect, + # 'width': svgwidth, + # 'height': svgheight, + # 'id': 'neg_rect', + # 'style': 'fill:%s;opacity:1.0;stroke-width:0.0' % str(color) + # } + + # decide if to round the bounding box for the negative + join_s = 1 if rounded_box else 2 + + if isinstance(transformed_box_geo, (LineString, LinearRing)): + b_geo = Polygon(transformed_box_geo).buffer(boundary, join_style=join_s) + coords_list = list(b_geo.exterior.coords) + elif isinstance(transformed_box_geo, list) and len(transformed_box_geo) == 1 and \ + isinstance(transformed_box_geo[0], (LineString, LinearRing)): + b_geo = Polygon(transformed_box_geo[0]).buffer(boundary, join_style=join_s) + coords_list = list(b_geo.exterior.coords) + elif isinstance(transformed_box_geo, Polygon): + coords_list = list(transformed_box_geo.exterior.coords) + elif isinstance(transformed_box_geo, list) and len(transformed_box_geo) == 1 and \ + isinstance(transformed_box_geo[0], Polygon): + coords_list = list(transformed_box_geo[0].exterior.coords) + else: + if use_convex_hull: + buff_box = transformed_box_geo.convex_hull.buffer(boundary, join_style=join_s) + else: + buff_box = transformed_box_geo.envelope.buffer(boundary, join_style=join_s) + box_buff_outline = buff_box.exterior + coords_list = list(box_buff_outline.coords) + + points_container = '' + for coord_tuple in coords_list: + points_container += '%s, %s ' % (str(coord_tuple[0]), str(coord_tuple[1])) + + first_svg_elem_tag = 'polygon' first_svg_elem_attribs = { - 'x': minx, - 'y': miny_rect, - 'width': svgwidth, - 'height': svgheight, + 'points': points_container, 'id': 'neg_rect', 'style': 'fill:%s;opacity:1.0;stroke-width:0.0' % str(color) } @@ -852,14 +899,16 @@ class Film(AppTool): with self.app.proc_container.new(_("Working...")): try: make_negative_film(color=color, transparency_level=transparency_level, - scale_factor_x=scale_factor_x, scale_factor_y=scale_factor_y) + scale_factor_x=scale_factor_x, scale_factor_y=scale_factor_y, + use_convex_hull=use_convex_hull, rounded_box=rounded_box) except Exception as e: log.error("export_negative() process -> %s" % str(e)) return self.app.worker_task.emit({'fcn': job_thread_film, 'params': []}) else: - make_negative_film(scale_factor_x=scale_factor_x, scale_factor_y=scale_factor_y) + make_negative_film(scale_factor_x=scale_factor_x, scale_factor_y=scale_factor_y, + use_convex_hull=use_convex_hull, rounded_box=rounded_box) def export_positive(self, obj_name, box_name, filename, scale_stroke_factor=0.00, @@ -1435,6 +1484,29 @@ class FilmUI: grid_par = FCGridLayout(v_spacing=5, h_spacing=3) par_frame.setLayout(grid_par) + # Convex Shape + # Surrounding convex box shape + self.convex_box_label = FCLabel('%s:' % _("Convex Shape")) + self.convex_box_label.setToolTip( + _("Create a convex shape surrounding the entire PCB.\n" + "If not checked the shape is rectangular.") + ) + self.convex_box_cb = FCCheckBox() + + grid_par.addWidget(self.convex_box_label, 0, 0) + grid_par.addWidget(self.convex_box_cb, 0, 1) + + # Rounded corners + self.rounded_label = FCLabel('%s:' % _("Rounded")) + self.rounded_label.setToolTip( + _("Resulting geometry will have rounded corners.") + ) + + self.rounded_cb = FCCheckBox() + + grid_par.addWidget(self.rounded_label, 2, 0) + grid_par.addWidget(self.rounded_cb, 2, 1) + # Scale Stroke size self.film_scale_stroke_entry = FCDoubleSpinner(callback=self.confirmation_message) self.film_scale_stroke_entry.set_range(-999.9999, 999.9999) @@ -1447,10 +1519,10 @@ class FilmUI: "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.") ) - grid_par.addWidget(self.film_scale_stroke_label, 0, 0) - grid_par.addWidget(self.film_scale_stroke_entry, 0, 1) + grid_par.addWidget(self.film_scale_stroke_label, 4, 0) + grid_par.addWidget(self.film_scale_stroke_entry, 4, 1) - # Film Type + # Polarity self.film_type = RadioSet([{'label': _('Positive'), 'value': 'pos'}, {'label': _('Negative'), 'value': 'neg'}], stretch=False) @@ -1458,10 +1530,10 @@ class FilmUI: self.film_type_label.setToolTip( _("Generate a Positive black film or a Negative film.") ) - grid_par.addWidget(self.film_type_label, 2, 0) - grid_par.addWidget(self.film_type, 2, 1) + grid_par.addWidget(self.film_type_label, 6, 0) + grid_par.addWidget(self.film_type, 6, 1) - # Boundary for negative film generation + # Border for negative film generation self.boundary_entry = FCDoubleSpinner(callback=self.confirmation_message) self.boundary_entry.set_range(-999.9999, 999.9999) self.boundary_entry.setSingleStep(0.01) @@ -1478,8 +1550,8 @@ class FilmUI: "white color like the rest and which may confound with the\n" "surroundings if not for this border.") ) - grid_par.addWidget(self.boundary_label, 4, 0) - grid_par.addWidget(self.boundary_entry, 4, 1) + grid_par.addWidget(self.boundary_label, 8, 0) + grid_par.addWidget(self.boundary_entry, 8, 1) self.boundary_label.hide() self.boundary_entry.hide() @@ -1489,12 +1561,12 @@ class FilmUI: 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.")) - grid_par.addWidget(self.punch_cb, 6, 0, 1, 2) + grid_par.addWidget(self.punch_cb, 10, 0, 1, 2) # this way I can hide/show the frame self.punch_frame = QtWidgets.QFrame() self.punch_frame.setContentsMargins(0, 0, 0, 0) - grid_par.addWidget(self.punch_frame, 8, 0, 1, 2) + grid_par.addWidget(self.punch_frame, 12, 0, 1, 2) punch_grid = FCGridLayout(v_spacing=5, h_spacing=3) punch_grid.setContentsMargins(0, 0, 0, 0) diff --git a/defaults.py b/defaults.py index 814d6072..2908a648 100644 --- a/defaults.py +++ b/defaults.py @@ -542,6 +542,8 @@ class FlatCAMDefaults: "tools_2sided_allign_axis": "X", # Film Tool + "tools_film_shape": False, + "tools_film_rounded": False, "tools_film_polarity": 'neg', "tools_film_boundary": 1.0, "tools_film_scale_stroke": 0,