From 31eb06d5a928ec0aca8f8031f2fde7cba88702ec Mon Sep 17 00:00:00 2001 From: Marius Stanciu Date: Thu, 10 Mar 2022 14:22:09 +0200 Subject: [PATCH] - fixed multiple issues in the App objects related to wrong usage of self.obj_options attribute instead of self.app.options attribute - remade the Film Plugin such that the `skew` feature is now done in length units as opposed with the previous usage of angles - refactored some big methods from the Film Plugin --- CHANGELOG.md | 5 +- .../preferences/tools/ToolsFilmPrefGroupUI.py | 6 +- appObjects/AppObject.py | 6 +- appObjects/FlatCAMExcellon.py | 4 +- appObjects/FlatCAMGerber.py | 29 +- appPlugins/ToolFilm.py | 876 ++++++++---------- 6 files changed, 431 insertions(+), 495 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a97aebe3..5deff92d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,9 @@ CHANGELOG for FlatCAM beta - fixed an issue where using the 'G' shortcut key in Editors will not toggle the grid snap - fixed an issue in the Excellon Editor where selecting the drills did not highlight them but instead made them invisible (although the selection still worked) - fixed an issue in the Gerber Editor where selecting one shape will auto-select all the shapes made with the same aperture +- fixed multiple issues in the App objects related to wrong usage of self.obj_options attribute instead of self.app.options attribute +- remade the Film Plugin such that the `skew` feature is now done in length units as opposed with the previous usage of angles +- refactored some big methods from the Film Plugin 9.03.2022 @@ -3454,7 +3457,7 @@ RELEASE 8.993 - working on a new tool: Extract Drills Tool who will create a Excellon object out of the apertures of a Gerber object - finished the GUI in the Extract Drills Tool -- fixed issue in Film Tool where some parameters names in calls of method export_positive() were not matching the actual parameters name +- fixed issue in Film Tool where some parameters names in calls of method export_positive_handler() were not matching the actual parameters name - finished the Extract Drills Tool - fixed a small issue in the DoubleSided Tool diff --git a/appGUI/preferences/tools/ToolsFilmPrefGroupUI.py b/appGUI/preferences/tools/ToolsFilmPrefGroupUI.py index c7b6fd63..7aa837ac 100644 --- a/appGUI/preferences/tools/ToolsFilmPrefGroupUI.py +++ b/appGUI/preferences/tools/ToolsFilmPrefGroupUI.py @@ -111,7 +111,7 @@ class ToolsFilmPrefGroupUI(OptionsGroupUI): grid_skew = FCGridLayout(v_spacing=5, h_spacing=3) skew_frame.setLayout(grid_skew) - self.film_skewx_label = FCLabel('%s:' % _("X angle")) + self.film_skewx_label = FCLabel('%s:' % _("X val")) self.film_skewx_entry = FCDoubleSpinner() self.film_skewx_entry.set_range(-999.9999, 999.9999) self.film_skewx_entry.set_precision(self.decimals) @@ -120,7 +120,7 @@ class ToolsFilmPrefGroupUI(OptionsGroupUI): grid_skew.addWidget(self.film_skewx_label, 0, 0) grid_skew.addWidget(self.film_skewx_entry, 0, 1) - self.film_skewy_label = FCLabel('%s:' % _("Y angle")) + self.film_skewy_label = FCLabel('%s:' % _("Y val")) self.film_skewy_entry = FCDoubleSpinner() self.film_skewy_entry.set_range(-999.9999, 999.9999) self.film_skewy_entry.set_precision(self.decimals) @@ -137,7 +137,7 @@ class ToolsFilmPrefGroupUI(OptionsGroupUI): self.film_skew_ref_combo = FCComboBox2() self.film_skew_ref_combo.addItems( - [_('Center'), _('Bottom Left'), _('Top Left'), _('Bottom Right'), _('Top right')]) + [_('Center'), _('Bottom Left')]) grid_skew.addWidget(self.skew_ref_label, 4, 0) grid_skew.addWidget(self.film_skew_ref_combo, 4, 1) diff --git a/appObjects/AppObject.py b/appObjects/AppObject.py index 72e86e6e..ec7844b1 100644 --- a/appObjects/AppObject.py +++ b/appObjects/AppObject.py @@ -446,13 +446,13 @@ class AppObject(QtCore.QObject): for opt_key, opt_val in app.options.items(): if opt_key.find('geometry' + "_") == 0: oname = opt_key[len('geometry') + 1:] - default_data[oname] = self.app.options[opt_key] + default_data[oname] = app.options[opt_key] if opt_key.find('tools_') == 0: - default_data[opt_key] = self.app.options[opt_key] + default_data[opt_key] = app.options[opt_key] new_obj.tools = { 1: { - 'tooldia': float(app.defaults["tools_mill_tooldia"]), + 'tooldia': float(app.options["tools_mill_tooldia"]), 'offset': 'Path', 'offset_value': 0.0, 'type': 'Rough', diff --git a/appObjects/FlatCAMExcellon.py b/appObjects/FlatCAMExcellon.py index 03703511..994cc73d 100644 --- a/appObjects/FlatCAMExcellon.py +++ b/appObjects/FlatCAMExcellon.py @@ -1020,7 +1020,7 @@ class ExcellonObject(FlatCAMObj, Excellon): geo_obj.obj_options['type'] = 'Excellon Geometry' geo_obj.obj_options["tools_mill_tooldia"] = str(tooldia) - geo_obj.obj_options["multidepth"] = app_obj.defaults["tools_mill_multidepth"] + geo_obj.obj_options["multidepth"] = app_obj.options["tools_mill_multidepth"] geo_obj.solid_geometry = [] # in case that the tool used has the same diameter with the hole, and since the maximum resolution @@ -1117,7 +1117,7 @@ class ExcellonObject(FlatCAMObj, Excellon): geo_obj.obj_options['type'] = 'Excellon Geometry' geo_obj.obj_options["tools_mill_tooldia"] = str(tooldia) - geo_obj.obj_options["tools_mill_multidepth"] = app_obj.defaults["tools_mill_multidepth"] + geo_obj.obj_options["tools_mill_multidepth"] = app_obj.options["tools_mill_multidepth"] geo_obj.solid_geometry = [] # in case that the tool used has the same diameter with the hole, and since the maximum resolution diff --git a/appObjects/FlatCAMGerber.py b/appObjects/FlatCAMGerber.py index 72383af1..e21662a1 100644 --- a/appObjects/FlatCAMGerber.py +++ b/appObjects/FlatCAMGerber.py @@ -585,12 +585,12 @@ class GerberObject(FlatCAMObj, Gerber): # store here the default data for Geometry Data default_data = {} - for opt_key, opt_val in app_obj.obj_options.items(): + for opt_key, opt_val in app_obj.options.items(): if opt_key.find('geometry' + "_") == 0: oname = opt_key[len('geometry') + 1:] - default_data[oname] = self.app.options[opt_key] + default_data[oname] = app_obj.options[opt_key] if opt_key.find('tools_mill' + "_") == 0: - default_data[opt_key] = self.app.options[opt_key] + default_data[opt_key] = app_obj.options[opt_key] geo_obj.tools = { 1: { @@ -676,7 +676,7 @@ class GerberObject(FlatCAMObj, Gerber): def iso_init(geo_obj, app_obj): # Propagate options geo_obj.obj_options["tools_mill_tooldia"] = str(dia) - geo_obj.tool_type = self.app.options["tools_iso_tool_shape"] + geo_obj.tool_type = app_obj.options["tools_iso_tool_shape"] geo_obj.multigeo = True # if milling type is climb then the move is counter-clockwise around features @@ -692,12 +692,12 @@ class GerberObject(FlatCAMObj, Gerber): # store here the default data for Geometry Data default_data = {} - for opt_key, opt_val in app_obj.obj_options.items(): + for opt_key, opt_val in app_obj.options.items(): if opt_key.find('geometry' + "_") == 0: oname = opt_key[len('geometry') + 1:] - default_data[oname] = self.app.options[opt_key] + default_data[oname] = app_obj.options[opt_key] if opt_key.find('tools_mill' + "_") == 0: - default_data[opt_key] = self.app.options[opt_key] + default_data[opt_key] = app_obj.options[opt_key] geo_obj.tools = { 1: { @@ -786,7 +786,7 @@ class GerberObject(FlatCAMObj, Gerber): """ if outname is None: - follow_name = self.obj_options["name"] + "_follow" + follow_name = self.options["name"] + "_follow" else: follow_name = outname @@ -808,16 +808,16 @@ class GerberObject(FlatCAMObj, Gerber): # new_obj.obj_options["tools_mill_tooldia"] = str(self.app.options["tools_iso_tooldia"]) new_obj.solid_geometry = deepcopy(self.follow_geometry) - new_obj.obj_options["tools_mill_tooldia"] = app_obj.defaults["tools_mill_tooldia"] + new_obj.obj_options["tools_mill_tooldia"] = app_obj.options["tools_mill_tooldia"] # store here the default data for Geometry Data default_data = {} - for opt_key, opt_val in app_obj.obj_options.items(): + for opt_key, opt_val in app_obj.options.items(): if opt_key.find('geometry' + "_") == 0: oname = opt_key[len('geometry') + 1:] - default_data[oname] = self.app.options[opt_key] + default_data[oname] = app_obj.options[opt_key] if opt_key.find('tools_mill' + "_") == 0: - default_data[opt_key] = self.app.options[opt_key] + default_data[opt_key] = app_obj.options[opt_key] new_obj.tools = { 1: { @@ -1021,10 +1021,7 @@ class GerberObject(FlatCAMObj, Gerber): # return # for marking apertures, line color and fill color are the same - if 'color' in kwargs: - color = kwargs['color'] - else: - color = self.app.options['gerber_plot_fill'] + color = kwargs['color'] if 'color' in kwargs else self.app.options['gerber_plot_fill'] if 'marked_aperture' in kwargs: aperture_to_plot_mark = kwargs['marked_aperture'] diff --git a/appPlugins/ToolFilm.py b/appPlugins/ToolFilm.py index 88a2dd18..a039d8bf 100644 --- a/appPlugins/ToolFilm.py +++ b/appPlugins/ToolFilm.py @@ -4,6 +4,7 @@ # Date: 3/10/2019 # # MIT Licence # # ########################################################## +import math from PyQt6 import QtCore, QtWidgets, QtGui @@ -398,16 +399,16 @@ class Film(AppTool): filename = str(filename) if str(filename) != "": - self.export_positive(name, boxname, filename, - scale_stroke_factor=factor, - scale_factor_x=scale_factor_x, scale_factor_y=scale_factor_y, - scale_reference=scale_reference, - skew_factor_x=skew_factor_x, skew_factor_y=skew_factor_y, - skew_reference=skew_reference, - mirror=mirror, - opacity_val=1.0, - ftype=ftype - ) + self.export_positive_handler(name, boxname, filename, + scale_stroke_factor=factor, + scale_factor_x=scale_factor_x, scale_factor_y=scale_factor_y, + scale_reference=scale_reference, + skew_factor_x=skew_factor_x, skew_factor_y=skew_factor_y, + skew_reference=skew_reference, + mirror=mirror, + opacity_val=1.0, + ftype=ftype + ) return # if we reach here then the filename is null @@ -553,23 +554,23 @@ class Film(AppTool): self.app.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled.")) return else: - self.export_negative(name, boxname, filename, border, - scale_stroke_factor=factor, - scale_factor_x=scale_factor_x, scale_factor_y=scale_factor_y, - scale_reference=scale_reference, - skew_factor_x=skew_factor_x, skew_factor_y=skew_factor_y, - skew_reference=skew_reference, - mirror=mirror, ftype=ftype, - use_convex_hull=use_convex_hull, - rounded_box=rounded_box - ) + self.export_negative_handler(name, boxname, filename, border, + scale_stroke_factor=factor, + scale_factor_x=scale_factor_x, scale_factor_y=scale_factor_y, + scale_reference=scale_reference, + skew_factor_x=skew_factor_x, skew_factor_y=skew_factor_y, + skew_reference=skew_reference, + mirror=mirror, ftype=ftype, + use_convex_hull=use_convex_hull, + rounded_box=rounded_box + ) - def export_negative(self, obj_name, box_name, filename, boundary, - scale_stroke_factor=0.00, - 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_convex_hull=False, rounded_box=False): + def export_negative_handler(self, obj_name, box_name, filename, boundary, + scale_stroke_factor=0.00, + 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_convex_hull=False, rounded_box=False): """ Exports a Geometry Object to an SVG file in negative. @@ -596,7 +597,7 @@ class Film(AppTool): Works only in case the object used as box has multiple geometries :return: """ - self.app.defaults.report_usage("export_negative()") + self.app.defaults.report_usage("export_negative_handler()") if filename is None: filename = self.app.options["global_last_save_folder"] @@ -620,30 +621,13 @@ class Film(AppTool): scale_factor_x = scale_factor_x scale_factor_y = scale_factor_y - def get_complementary(color_param): - # strip the # from the beginning - our_color = color_param[1:] - - # convert the string into hex - our_color = int(our_color, 16) - - # invert the three bytes - # as good as substracting each of RGB component by 255(FF) - comp_color = 0xFFFFFF ^ our_color - - # convert the color back to hex by prefixing a # - comp_color = "#%06X" % comp_color - - # return the result - return comp_color - p_size = self.ui.pagesize_combo.get_value() orientation = self.ui.orientation_radio.get_value() color = obj.obj_options['tools_film_color'] transparency_level = opacity_val def make_negative_film(color, transparency_level, scale_factor_x, scale_factor_y, use_convex_hull, rounded_box): - self.app.log.debug("FilmTool.export_negative().make_negative_film()") + self.app.log.debug("FilmTool.export_negative_handler().make_negative_film()") self.screen_dpi = self.app.qapp.screens()[0].logicalDotsPerInch() @@ -654,247 +638,35 @@ 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: - for tool in box.tools: - flat_geo += box.flatten(box.tools[tool]['solid_geometry']) - box_geo = unary_union(flat_geo) - else: - box_geo = unary_union(box.flatten()) - else: - box_geo = unary_union(box.flatten()) + transformed_box_geo = self.transform_geometry(box, scale_factor_x=scale_factor_x, + scale_factor_y=scale_factor_y, + scale_reference=scale_reference, + skew_factor_x=skew_factor_x, skew_factor_y=skew_factor_y, + skew_reference=skew_reference, + mirror=mirror) - xmin, ymin, xmax, ymax = box_geo.bounds - ref_scale_val = 'center' - if scale_reference == 'topleft': - ref_scale_val = (xmin, ymax) - elif scale_reference == 'bottomleft': - ref_scale_val = (xmin, ymin) - elif scale_reference == 'topright': - ref_scale_val = (xmax, ymax) - elif scale_reference == 'bottomright': - ref_scale_val = (xmax, ymin) + transformed_obj_geo = self.transform_geometry(obj, scale_factor_x=scale_factor_x, + scale_factor_y=scale_factor_y, + scale_reference=scale_reference, + skew_factor_x=skew_factor_x, skew_factor_y=skew_factor_y, + skew_reference=skew_reference, + mirror=mirror) - ref_skew_val = 'center' - if skew_reference == 'topleft': - ref_skew_val = (xmin, ymax) - elif skew_reference == 'bottomleft': - ref_skew_val = (xmin, ymin) - elif skew_reference == 'topright': - ref_skew_val = (xmax, ymax) - elif skew_reference == 'bottomright': - ref_skew_val = (xmax, ymin) - - # Transform the box object geometry - transformed_box_geo = box_geo - - if scale_factor_x and not scale_factor_y: - transformed_box_geo = affinity.scale(transformed_box_geo, scale_factor_x, 1.0, origin=ref_scale_val) - elif not scale_factor_x and scale_factor_y: - transformed_box_geo = affinity.scale(transformed_box_geo, 1.0, scale_factor_y, origin=ref_scale_val) - elif scale_factor_x and scale_factor_y: - transformed_box_geo = affinity.scale(transformed_box_geo, scale_factor_x, scale_factor_y, - origin=ref_scale_val) - - if skew_factor_x and not skew_factor_y: - transformed_box_geo = affinity.skew(transformed_box_geo, skew_factor_x, 0.0, origin=ref_skew_val) - elif not skew_factor_x and skew_factor_y: - transformed_box_geo = affinity.skew(transformed_box_geo, 0.0, skew_factor_y, origin=ref_skew_val) - elif skew_factor_x and skew_factor_y: - transformed_box_geo = affinity.skew(transformed_box_geo, skew_factor_x, skew_factor_y, - origin=ref_skew_val) - - if mirror: - if mirror == 'x': - transformed_box_geo = affinity.scale(transformed_box_geo, 1.0, -1.0, origin='center') - if mirror == 'y': - transformed_box_geo = affinity.scale(transformed_box_geo, -1.0, 1.0, origin='center') - if mirror == 'both': - transformed_box_geo = affinity.scale(transformed_box_geo, -1.0, -1.0, origin='center') + exported_svg = self.create_svg_geometry(transformed_obj_geo, scale_stroke_factor=scale_stroke_factor) + svg_units = obj.units.lower() bounds = transformed_box_geo.bounds - size = bounds[2] - bounds[0], bounds[3] - bounds[1] - exported_svg = obj.export_svg(scale_stroke_factor=scale_stroke_factor, - scale_factor_x=scale_factor_x, scale_factor_y=scale_factor_y, - skew_factor_x=skew_factor_x, skew_factor_y=skew_factor_y, - mirror=mirror, - scale_reference=scale_reference, skew_reference=skew_reference, - mirror_reference='center' - ) + doc_final = self.create_negative_svg(svg_geo=exported_svg, box_bounds=bounds, r_box=rounded_box, + box_geo=transformed_box_geo, c_hull=use_convex_hull, margin=boundary, + color=color, opacity=transparency_level, svg_units=svg_units) - uom = obj.units.lower() + obj_bounds = obj.bounds() + ret = self.write_output_file(content2save=doc_final, filename=filename, file_type=ftype, p_size=p_size, + orientation=orientation, source_bounds=obj_bounds, box_bounds=bounds) - # Convert everything to strings for use in the xml doc - svgwidth = str(size[0] + (2 * boundary)) - svgheight = str(size[1] + (2 * boundary)) - minx = str(bounds[0] - boundary) - miny = str(bounds[1] + boundary + size[1]) - # 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 - # properly within svg apps such as inkscape - svg_header = ' PNG output --> %s" % str(e)) - return 'fail' - else: # PDF - if self.units == 'IN': - unit = inch - else: - unit = mm - - if p_size == 'Bounds': - page_size = None - elif orientation == 'p': - page_size = portrait(self.ui.pagesize[p_size]) - else: - page_size = landscape(self.ui.pagesize[p_size]) - - try: - xmin, ymin, xmax, ymax = obj.bounds() - if page_size: - page_xmax, page_ymax = ( - page_size[0] / mm, - page_size[1] / mm - ) - else: - page_xmax, page_ymax = xmax, ymax - - if xmax < 0 or ymax < 0 or xmin > page_xmax or ymin > page_ymax: - err_msg = '[ERROR_NOTCL] %s %s' % \ - (_("Failed."), - _("The artwork has to be within the selected page size in order to be visible.\n" - "For 'Bounds' page size, it needs to be in the first quadrant.")) - self.app.inform.emit(err_msg) - return 'fail' - except Exception as e: - self.app.log.error("FilmTool.export_negative() --> PDF output 1 --> %s" % str(e)) - return 'fail' - - try: - doc_final = StringIO(doc_final) - drawing = svg2rlg(doc_final) - - if p_size == 'Bounds': - renderPDF.drawToFile(drawing, filename) - else: - my_canvas = canvas.Canvas(filename, pagesize=page_size) - my_canvas.translate(bounds[0] * unit, bounds[1] * unit) - renderPDF.draw(drawing, my_canvas, 0, 0) - my_canvas.save() - except PermissionError: - self.app.inform.emit('[ERROR_NOTCL] %s' % - _("Permission denied, saving not possible.\n" - "Most likely another app is holding the file open and not accessible.")) - return 'fail' - except Exception as e: - self.app.log.error("FilmTool.export_negative() --> PDF output Reportlab section --> %s" % str(e)) - return 'fail' + if ret == 'fail': + return 'fail' if self.app.options["global_open_style"] is False: self.app.file_opened.emit("SVG", filename) @@ -909,7 +681,7 @@ class Film(AppTool): 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: - self.app.log.error("export_negative() process -> %s" % str(e)) + self.app.log.error("export_negative_handler() process -> %s" % str(e)) return self.app.worker_task.emit({'fcn': job_thread_film, 'params': []}) @@ -917,12 +689,106 @@ class Film(AppTool): 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, - 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'): + def create_negative_svg(self, svg_geo, box_bounds, r_box, box_geo, c_hull, margin, color, opacity, svg_units): + # Change the attributes of the exported SVG + # We don't need stroke-width - wrong, we do when we have lines with certain width + # We set opacity to maximum + # We set the color to the inversed color + root = ET.fromstring(svg_geo) + for child in root: + child.set('fill', self.get_complementary(color)) + child.set('opacity', str(opacity)) + child.set('stroke', self.get_complementary(color)) + + uom = svg_units + + # Convert everything to strings for use in the xml doc + size = box_bounds[2] - box_bounds[0], box_bounds[3] - box_bounds[1] + + svgwidth = str(size[0] + (2 * margin)) + svgheight = str(size[1] + (2 * margin)) + minx = str(box_bounds[0] - margin) + miny = str(box_bounds[1] + margin + size[1]) + # 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 + # properly within svg apps such as inkscape + svg_header = ' PNG output --> %s" % str(e)) - return 'fail' - else: # PDF - try: - if self.units == 'IN': - unit = inch - else: - unit = mm - - if p_size == 'Bounds': - page_size = None - elif orientation == 'p': - page_size = portrait(self.ui.pagesize[p_size]) - else: - page_size = landscape(self.ui.pagesize[p_size]) - - xmin, ymin, xmax, ymax = obj.bounds() - if page_size: - page_xmax, page_ymax = ( - page_size[0] / mm, - page_size[1] / mm - ) - else: - page_xmax, page_ymax = xmax, ymax - - if xmax < 0 or ymax < 0 or xmin > page_xmax or ymin > page_ymax: - err_msg = '[ERROR_NOTCL] %s %s' % \ - (_("Failed."), - _("The artwork has to be within the selected page size in order to be visible.\n" - "For 'Bounds' page size, it needs to be in the first quadrant.")) - self.app.inform.emit(err_msg) - return 'fail' - - doc_final = StringIO(doc_final) - drawing = svg2rlg(doc_final) - - if p_size == 'Bounds': - renderPDF.drawToFile(drawing, filename) - else: - my_canvas = canvas.Canvas(filename, pagesize=page_size) - my_canvas.translate(bounds[0] * unit, bounds[1] * unit) - renderPDF.draw(drawing, my_canvas, 0, 0) - my_canvas.save() - except PermissionError: - self.app.inform.emit('[ERROR_NOTCL] %s' % - _("Permission denied, saving not possible.\n" - "Most likely another app is holding the file open and not accessible.")) - return 'fail' - except Exception as e: - self.app.log.error("FilmTool.export_positive() --> PDF output --> %s" % str(e)) - return 'fail' + if ret == 'fail': + return 'fail' if self.app.options["global_open_style"] is False: self.app.file_opened.emit("SVG", filename) @@ -1183,7 +896,7 @@ class Film(AppTool): make_positive_film(color=color, transparency_level=transparency_level, scale_factor_x=scale_factor_x, scale_factor_y=scale_factor_y) except Exception as e: - self.app.log.error("export_positive() process -> %s" % str(e)) + self.app.log.error("export_positive_handler() process -> %s" % str(e)) return self.app.worker_task.emit({'fcn': job_thread_film, 'params': []}) @@ -1191,6 +904,229 @@ class Film(AppTool): make_positive_film(color=color, transparency_level=transparency_level, scale_factor_x=scale_factor_x, scale_factor_y=scale_factor_y) + @staticmethod + def create_positive_svg(svg_geo, box_bounds, margin, color, opacity, svg_units): + # Change the attributes of the exported SVG + # We don't need stroke-width + # We set opacity to maximum + # We set the colour to WHITE + root = ET.fromstring(svg_geo) + for child in root: + child.set('fill', str(color)) + child.set('opacity', str(opacity)) + child.set('stroke', str(color)) + + exported_svg = ET.tostring(root) + + # This contain the measure units + uom = svg_units + + # Convert everything to strings for use in the xml doc + size = box_bounds[2] - box_bounds[0], box_bounds[3] - box_bounds[1] + + svgwidth = str(size[0] + (2 * margin)) + svgheight = str(size[1] + (2 * margin)) + minx = str(box_bounds[0] - margin) + miny = str(box_bounds[1] + margin + size[1]) + + # Add a SVG Header and footer to the svg output from shapely + # The transform flips the Y Axis so that everything renders + # properly within svg apps such as inkscape + svg_header = ' PNG output --> %s" % str(e)) + return 'fail' + else: # PDF + try: + if self.units == 'IN': + unit = inch + else: + unit = mm + + if p_size == 'Bounds': + page_size = None + elif orientation == 'p': + page_size = portrait(self.ui.pagesize[p_size]) + else: + page_size = landscape(self.ui.pagesize[p_size]) + + xmin, ymin, xmax, ymax = source_bounds + if page_size: + page_xmax, page_ymax = ( + page_size[0] / mm, + page_size[1] / mm + ) + else: + page_xmax, page_ymax = xmax, ymax + + if xmax < 0 or ymax < 0 or xmin > page_xmax or ymin > page_ymax: + err_msg = '[ERROR_NOTCL] %s %s' % \ + ( + _("Failed."), + _("The artwork has to be within the selected page size in order to be visible.\n" + "For 'Bounds' page size, it needs to be in the first quadrant.") + ) + self.app.inform.emit(err_msg) + return 'fail' + + doc_final = StringIO(content2save) + drawing = svg2rlg(doc_final) + + if p_size == 'Bounds': + renderPDF.drawToFile(drawing, filename) + else: + my_canvas = canvas.Canvas(filename, pagesize=page_size) + my_canvas.translate(box_bounds[0] * unit, box_bounds[1] * unit) + renderPDF.draw(drawing, my_canvas, 0, 0) + my_canvas.save() + except PermissionError: + self.app.inform.emit(p_msg) + return 'fail' + except Exception as e: + self.app.log.error("FilmTool.write_output_file() --> PDF output --> %s" % str(e)) + return 'fail' + + @staticmethod + def transform_geometry(obj, scale_factor_x=None, scale_factor_y=None, + skew_factor_x=None, skew_factor_y=None, + skew_reference='center', scale_reference='center', mirror=None): + """ + Return a transformed geometry made from a Shapely geometry collection property of the `obj` object + + :return: Shapely geometry transformed + """ + + # Make sure we see a Shapely Geometry class and not a list + if obj.kind.lower() == 'geometry': + flat_geo = [] + if obj.multigeo: + for tool in obj.tools: + flat_geo += obj.flatten(obj.tools[tool]['solid_geometry']) + transformed_geo = unary_union(flat_geo) + else: + transformed_geo = unary_union(obj.flatten()) + else: + transformed_geo = unary_union(obj.flatten()) + + # SCALING + if scale_factor_x or scale_factor_y: + xmin, ymin, xmax, ymax = transformed_geo.bounds + ref_scale_val = 'center' + if scale_reference == 'topleft': + ref_scale_val = (xmin, ymax) + elif scale_reference == 'bottomleft': + ref_scale_val = (xmin, ymin) + elif scale_reference == 'topright': + ref_scale_val = (xmax, ymax) + elif scale_reference == 'bottomright': + ref_scale_val = (xmax, ymin) + + if scale_factor_x and not scale_factor_y: + val_x = scale_factor_x + val_y = 0 + elif not scale_factor_x and scale_factor_y: + val_x = 0 + val_y = scale_factor_y + else: + val_x = scale_factor_x + val_y = scale_factor_y + transformed_geo = affinity.scale(transformed_geo, val_x, val_y, origin=ref_scale_val) + + # SKEWING + if skew_factor_x or skew_factor_y: + xmin, ymin, xmax, ymax = transformed_geo.bounds + if skew_reference == 'bottomleft': + ref_skew_val = (xmin, ymin) + if skew_factor_x and not skew_factor_y: + skew_angle_x = math.degrees(math.atan2(skew_factor_x, (ymax - ymin))) + skew_angle_y = 0.0 + elif not skew_factor_x and skew_factor_y: + skew_angle_x = 0.0 + skew_angle_y = math.degrees(math.atan2(skew_factor_y, (xmax - xmin))) + else: + skew_angle_x = math.degrees(math.atan2(skew_factor_x, (ymax - ymin))) + skew_angle_y = math.degrees(math.atan2(skew_factor_y, (xmax - xmin))) + else: + ref_skew_val = 'center' + if skew_factor_x and not skew_factor_y: + skew_angle_x = math.degrees(math.atan2(skew_factor_x, ((ymax - ymin) * 0.5))) + skew_angle_y = 0.0 + elif not skew_factor_x and skew_factor_y: + skew_angle_x = 0.0 + skew_angle_y = math.degrees(math.atan2(skew_factor_y, ((xmax - xmin) * 0.5))) + else: + skew_angle_x = math.degrees(math.atan2(skew_factor_x, ((ymax - ymin) * 0.5))) + skew_angle_y = math.degrees(math.atan2(skew_factor_y, ((xmax - xmin) * 0.5))) + + transformed_geo = affinity.skew(transformed_geo, skew_angle_x, skew_angle_y, origin=ref_skew_val) + + if mirror: + if mirror == 'x': + transformed_geo = affinity.scale(transformed_geo, 1.0, -1.0, origin='center') + if mirror == 'y': + transformed_geo = affinity.scale(transformed_geo, -1.0, 1.0, origin='center') + if mirror == 'both': + transformed_geo = affinity.scale(transformed_geo, -1.0, -1.0, origin='center') + + return transformed_geo + + @staticmethod + def create_svg_geometry(geom, scale_stroke_factor): + """ + Return SVG geometry made from a Shapely geometry collection property of the `obj` object + + :param geom: Shapely geometry collection + :type geom: + :param scale_stroke_factor: multiplication factor for the SVG stroke-width used within shapely's svg export + If 0 or less which is invalid then default to 0.01 + This value appears to work for zooming, and getting the output svg line width + to match that viewed on screen with FlatCam + MS: I choose a factor of 0.01 so the scale is right for PCB UV film + :type scale_stroke_factor: float + :return: SVG geometry + :rtype: + """ + + if scale_stroke_factor <= 0: + scale_stroke_factor = 0.01 + + # Convert to a SVG + svg_elem = geom.svg(scale_factor=scale_stroke_factor) + return svg_elem + def reset_fields(self): self.ui.tf_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex())) self.ui.tf_box_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex())) @@ -1399,7 +1335,7 @@ class FilmUI: adj_grid.addWidget(self.film_skew_cb, 12, 0, 1, 2) # Skew X - self.film_skewx_label = FCLabel('%s:' % _("X angle")) + self.film_skewx_label = FCLabel('%s:' % _("X val")) self.film_skewx_entry = FCDoubleSpinner(callback=self.confirmation_message) self.film_skewx_entry.set_range(-999.9999, 999.9999) self.film_skewx_entry.set_precision(self.decimals) @@ -1409,7 +1345,7 @@ class FilmUI: adj_grid.addWidget(self.film_skewx_entry, 14, 1) # Skew Y - self.film_skewy_label = FCLabel('%s:' % _("Y angle")) + self.film_skewy_label = FCLabel('%s:' % _("Y val")) self.film_skewy_entry = FCDoubleSpinner(callback=self.confirmation_message) self.film_skewy_entry.set_range(-999.9999, 999.9999) self.film_skewy_entry.set_precision(self.decimals) @@ -1426,7 +1362,7 @@ class FilmUI: self.skew_ref_combo = FCComboBox2() self.skew_ref_combo.addItems( - [_('Center'), _('Bottom Left'), _('Top Left'), _('Bottom Right'), _('Top right')]) + [_('Center'), _('Bottom Left')]) adj_grid.addWidget(self.skew_ref_label, 18, 0) adj_grid.addWidget(self.skew_ref_combo, 18, 1)