From 9fc2ba8ffdc085a73c3cd33c074c4f73ccaf0d62 Mon Sep 17 00:00:00 2001 From: Marius Stanciu Date: Fri, 14 Feb 2020 17:08:06 +0200 Subject: [PATCH] - added a new FlatCAM Tool: Gerber Invert Tool. It will invert the copper features in a Gerber file: where is copper there will be empty and where is empty it will be copper --- FlatCAMApp.py | 21 ++- FlatCAMObj.py | 25 +-- README.md | 1 + flatcamGUI/FlatCAMGUI.py | 2 + flatcamParsers/ParseGerber.py | 9 +- flatcamTools/ToolInvertGerber.py | 274 +++++++++++++++++++++++++++++++ flatcamTools/ToolPaint.py | 1 + flatcamTools/ToolPunchGerber.py | 10 +- flatcamTools/ToolSub.py | 6 +- flatcamTools/__init__.py | 2 + share/invert16.png | Bin 0 -> 245 bytes share/invert32.png | Bin 0 -> 374 bytes 12 files changed, 324 insertions(+), 27 deletions(-) create mode 100644 flatcamTools/ToolInvertGerber.py create mode 100644 share/invert16.png create mode 100644 share/invert32.png diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 7f3596e3..06ffbc72 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -14,7 +14,7 @@ import getopt import random import simplejson as json import lzma -import threading +# import threading import shutil import stat @@ -26,7 +26,7 @@ import ctypes from reportlab.graphics import renderPDF from reportlab.pdfgen import canvas -from reportlab.graphics import renderPM +# from reportlab.graphics import renderPM from reportlab.lib.units import inch, mm from reportlab.lib.pagesizes import landscape, portrait from svglib.svglib import svg2rlg @@ -39,9 +39,9 @@ from xml.dom.minidom import parseString as parse_xml_string from multiprocessing.connection import Listener, Client from multiprocessing import Pool import socket -from array import array +# from array import array -import vispy.scene as scene +# import vispy.scene as scene # ####################################### # # Imports part of FlatCAM ## @@ -1306,7 +1306,7 @@ class App(QtCore.QObject): # Excellon Options "excellon_drillz": self.ui.excellon_defaults_form.excellon_opt_group.cutz_entry, "excellon_multidepth": self.ui.excellon_defaults_form.excellon_opt_group.mpass_cb, - "excellon_depthperpass":self.ui.excellon_defaults_form.excellon_opt_group.maxdepth_entry, + "excellon_depthperpass": self.ui.excellon_defaults_form.excellon_opt_group.maxdepth_entry, "excellon_travelz": self.ui.excellon_defaults_form.excellon_opt_group.travelz_entry, "excellon_endz": self.ui.excellon_defaults_form.excellon_opt_group.endz_entry, "excellon_feedrate": self.ui.excellon_defaults_form.excellon_opt_group.feedrate_z_entry, @@ -2558,6 +2558,7 @@ class App(QtCore.QObject): self.edrills_tool = None self.align_objects_tool = None self.punch_tool = None + self.invert_tool = None # always install tools only after the shell is initialized because the self.inform.emit() depends on shell try: @@ -2724,6 +2725,8 @@ class App(QtCore.QObject): # this holds a widget that is installed in the Plot Area when View Source option is used self.source_editor_tab = None + self.pagesize = dict() + # Storage for shapes, storage that can be used by FlatCAm tools for utility geometry # VisPy visuals if self.is_legacy is False: @@ -3194,6 +3197,9 @@ class App(QtCore.QObject): self.punch_tool = ToolPunchGerber(self) self.punch_tool.install(icon=QtGui.QIcon(self.resource_location + '/punch32.png'), pos=self.ui.menutool) + self.invert_tool = ToolInvertGerber(self) + self.invert_tool.install(icon=QtGui.QIcon(self.resource_location + '/invert32.png'), pos=self.ui.menutool) + self.transform_tool = ToolTransform(self) self.transform_tool.install(icon=QtGui.QIcon(self.resource_location + '/transform.png'), pos=self.ui.menuoptions, separator=True) @@ -3338,6 +3344,7 @@ class App(QtCore.QObject): self.ui.copperfill_btn.triggered.connect(lambda: self.copper_thieving_tool.run(toggle=True)) self.ui.fiducials_btn.triggered.connect(lambda: self.fiducial_tool.run(toggle=True)) self.ui.punch_btn.triggered.connect(lambda: self.punch_tool.run(toggle=True)) + self.ui.invert_btn.triggered.connect(lambda: self.invert_tool.run(toggle=True)) def object2editor(self): """ @@ -8716,14 +8723,14 @@ class App(QtCore.QObject): else: event_pos = (event.xdata, event.ydata) # Matplotlib has the middle and right buttons mapped in reverse compared with VisPy - pan_button = 3 if self.defaults["global_pan_button"] == '2'else 2 + pan_button = 3 if self.defaults["global_pan_button"] == '2' else 2 # So it can receive key presses self.plotcanvas.native.setFocus() self.pos_canvas = self.plotcanvas.translate_coords(event_pos) - if self.grid_status() == True: + if self.grid_status(): self.pos = self.geo_editor.snap(self.pos_canvas[0], self.pos_canvas[1]) else: self.pos = (self.pos_canvas[0], self.pos_canvas[1]) diff --git a/FlatCAMObj.py b/FlatCAMObj.py index e0534d7a..159cd1d5 100644 --- a/FlatCAMObj.py +++ b/FlatCAMObj.py @@ -2164,14 +2164,17 @@ class FlatCAMGerber(FlatCAMObj, Gerber): gerber_code += 'D02*\n' gerber_code += 'G37*\n' gerber_code += '%LPD*%\n' + except Exception as e: + log.debug("FlatCAMObj.FlatCAMGerber.export_gerber() '0' aperture --> %s" % str(e)) - 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']: + 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']: + try: if 'follow' in geo_elem: geo = geo_elem['follow'] if not geo.is_empty: @@ -2212,7 +2215,10 @@ class FlatCAMGerber(FlatCAMObj, Gerber): prev_coord = coord # gerber_code += "D02*\n" + except Exception as e: + log.debug("FlatCAMObj.FlatCAMGerber.export_gerber() 'follow' --> %s" % str(e)) + try: if 'clear' in geo_elem: gerber_code += '%LPC*%\n' @@ -2256,9 +2262,8 @@ class FlatCAMGerber(FlatCAMObj, Gerber): prev_coord = coord # gerber_code += "D02*\n" gerber_code += '%LPD*%\n' - - except Exception as e: - log.debug("FlatCAMObj.FlatCAMGerber.export_gerber() --> %s" % str(e)) + except Exception as e: + log.debug("FlatCAMObj.FlatCAMGerber.export_gerber() 'clear' --> %s" % str(e)) if not self.apertures: log.debug("FlatCAMObj.FlatCAMGerber.export_gerber() --> Gerber Object is empty: no apertures.") diff --git a/README.md b/README.md index 076452e3..6ef6dff6 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ CAD program, and create G-Code for Isolation routing. 14.02.2020 - adjusted the UI for Excellon and Geometry objects +- added a new FlatCAM Tool: Gerber Invert Tool. It will invert the copper features in a Gerber file: where is copper there will be empty and where is empty it will be copper 13.02.2020 diff --git a/flatcamGUI/FlatCAMGUI.py b/flatcamGUI/FlatCAMGUI.py index b8154db7..82d55bcf 100644 --- a/flatcamGUI/FlatCAMGUI.py +++ b/flatcamGUI/FlatCAMGUI.py @@ -928,6 +928,8 @@ class FlatCAMGUI(QtWidgets.QMainWindow): QtGui.QIcon(self.app.resource_location + '/calibrate_32.png'), _("Calibration Tool")) self.punch_btn = self.toolbartools.addAction( QtGui.QIcon(self.app.resource_location + '/punch32.png'), _("Punch Gerber Tool")) + self.invert_btn = self.toolbartools.addAction( + QtGui.QIcon(self.app.resource_location + '/invert32.png'), _("Invert Gerber Tool")) # ######################################################################## # ########################## Excellon Editor Toolbar# #################### diff --git a/flatcamParsers/ParseGerber.py b/flatcamParsers/ParseGerber.py index 8acb3482..235521b4 100644 --- a/flatcamParsers/ParseGerber.py +++ b/flatcamParsers/ParseGerber.py @@ -1414,9 +1414,12 @@ class Gerber(Geometry): self.follow_geometry = follow_buffer # this treats the case when we are storing geometry as solids - - if len(poly_buffer) == 0 and len(self.solid_geometry) == 0: - log.error("Object is not Gerber file or empty. Aborting Object creation.") + try: + if len(poly_buffer) == 0 and len(self.solid_geometry) == 0: + log.error("Object is not Gerber file or empty. Aborting Object creation.") + return 'fail' + except TypeError as e: + log.error("Object is not Gerber file or empty. Aborting Object creation. %s" % str(e)) return 'fail' log.warning("Joining %d polygons." % len(poly_buffer)) diff --git a/flatcamTools/ToolInvertGerber.py b/flatcamTools/ToolInvertGerber.py new file mode 100644 index 00000000..8d96419e --- /dev/null +++ b/flatcamTools/ToolInvertGerber.py @@ -0,0 +1,274 @@ +# ########################################################## +# FlatCAM: 2D Post-processing for Manufacturing # +# File Author: Marius Adrian Stanciu (c) # +# Date: 2/14/2020 # +# MIT Licence # +# ########################################################## + +from PyQt5 import QtWidgets, QtCore + +from FlatCAMTool import FlatCAMTool +from flatcamGUI.GUIElements import FCButton, FCDoubleSpinner + +from shapely.geometry import Polygon, MultiPolygon, MultiLineString, LineString, box +from shapely.ops import cascaded_union + +import traceback +from copy import deepcopy +import time +import logging +import gettext +import FlatCAMTranslation as fcTranslate +import builtins + +fcTranslate.apply_language('strings') +if '_' not in builtins.__dict__: + _ = gettext.gettext + +log = logging.getLogger('base') + + +class ToolInvertGerber(FlatCAMTool): + + toolName = _("Invert Tool") + + def __init__(self, app): + self.app = app + self.decimals = self.app.decimals + + FlatCAMTool.__init__(self, app) + + self.tools_frame = QtWidgets.QFrame() + self.tools_frame.setContentsMargins(0, 0, 0, 0) + self.layout.addWidget(self.tools_frame) + self.tools_box = QtWidgets.QVBoxLayout() + self.tools_box.setContentsMargins(0, 0, 0, 0) + self.tools_frame.setLayout(self.tools_box) + + # Title + title_label = QtWidgets.QLabel("%s" % self.toolName) + title_label.setStyleSheet(""" + QLabel + { + font-size: 16px; + font-weight: bold; + } + """) + self.tools_box.addWidget(title_label) + + # Form Layout + grid0 = QtWidgets.QGridLayout() + grid0.setColumnStretch(0, 0) + grid0.setColumnStretch(1, 1) + self.tools_box.addLayout(grid0) + + grid0.addWidget(QtWidgets.QLabel(''), 0, 0, 1, 2) + + # Target Gerber Object + self.gerber_combo = QtWidgets.QComboBox() + self.gerber_combo.setModel(self.app.collection) + self.gerber_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex())) + self.gerber_combo.setCurrentIndex(1) + + self.gerber_label = QtWidgets.QLabel('%s:' % _("Gerber Object")) + self.gerber_label.setToolTip( + _("Gerber object that will be inverted.") + ) + + grid0.addWidget(self.gerber_label, 1, 0, 1, 2) + grid0.addWidget(self.gerber_combo, 2, 0, 1, 2) + + # Margin + self.margin_label = QtWidgets.QLabel('%s:' % _('Margin')) + self.margin_label.setToolTip( + _("Distance by which to avoid\n" + "the edges of the Gerber object.") + ) + self.margin_entry = FCDoubleSpinner() + self.margin_entry.set_precision(self.decimals) + self.margin_entry.set_range(0.0000, 9999.9999) + self.margin_entry.setObjectName(_("Margin")) + + grid0.addWidget(self.margin_label, 3, 0) + grid0.addWidget(self.margin_entry, 3, 1) + + separator_line = QtWidgets.QFrame() + separator_line.setFrameShape(QtWidgets.QFrame.HLine) + separator_line.setFrameShadow(QtWidgets.QFrame.Sunken) + grid0.addWidget(separator_line, 4, 0, 1, 2) + + self.invert_btn = FCButton(_('Invert Gerber')) + self.invert_btn.setToolTip( + _("Will invert the Gerber object: areas that have copper\n" + "will be emty of copper and previous empty area will be\n" + "filled with copper.") + ) + self.invert_btn.setStyleSheet(""" + QPushButton + { + font-weight: bold; + } + """) + grid0.addWidget(self.invert_btn, 5, 0, 1, 2) + + self.tools_box.addStretch() + + # ## Reset Tool + self.reset_button = QtWidgets.QPushButton(_("Reset Tool")) + self.reset_button.setToolTip( + _("Will reset the tool parameters.") + ) + self.reset_button.setStyleSheet(""" + QPushButton + { + font-weight: bold; + } + """) + self.tools_box.addWidget(self.reset_button) + + self.invert_btn.clicked.connect(self.on_grb_invert) + self.reset_button.clicked.connect(self.set_tool_ui) + + def install(self, icon=None, separator=None, **kwargs): + FlatCAMTool.install(self, icon, separator, shortcut='', **kwargs) + + def run(self, toggle=True): + self.app.report_usage("ToolInvertGerber()") + log.debug("ToolInvertGerber() is running ...") + + if toggle: + # if the splitter is hidden, display it, else hide it but only if the current widget is the same + if self.app.ui.splitter.sizes()[0] == 0: + self.app.ui.splitter.setSizes([1, 1]) + else: + try: + if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName: + # if tab is populated with the tool but it does not have the focus, focus on it + if not self.app.ui.notebook.currentWidget() is self.app.ui.tool_tab: + # focus on Tool Tab + self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab) + else: + self.app.ui.splitter.setSizes([0, 1]) + except AttributeError: + pass + else: + if self.app.ui.splitter.sizes()[0] == 0: + self.app.ui.splitter.setSizes([1, 1]) + + FlatCAMTool.run(self) + self.set_tool_ui() + + self.app.ui.notebook.setTabText(2, _("Invert Tool")) + + def set_tool_ui(self): + self.margin_entry.set_value(0.0) + + def on_grb_invert(self): + margin = self.margin_entry.get_value() + if round(margin, self.decimals) == 0.0: + margin = 1E-10 + + grb_circle_steps = int(self.app.defaults["gerber_circle_steps"]) + obj_name = self.gerber_combo.currentText() + + outname = obj_name + "_inverted" + + # Get source object. + try: + grb_obj = self.app.collection.get_by_name(obj_name) + except Exception as e: + self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), str(obj_name))) + return "Could not retrieve object: %s with error: %s" % (obj_name, str(e)) + + if grb_obj is None: + self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Object not found"), str(obj_name))) + return + + xmin, ymin, xmax, ymax = grb_obj.bounds() + + grb_box = box(xmin, ymin, xmax, ymax).buffer(margin, resolution=grb_circle_steps, join_style=2) + + try: + __ = iter(grb_obj.solid_geometry) + except TypeError: + grb_obj.solid_geometry = list(grb_obj.solid_geometry) + + new_solid_geometry = deepcopy(grb_box) + + for poly in grb_obj.solid_geometry: + new_solid_geometry = new_solid_geometry.difference(poly) + + new_options = dict() + for opt in grb_obj.options: + new_options[opt] = deepcopy(grb_obj.options[opt]) + + new_apertures = dict() + + # for apid, val in grb_obj.apertures.items(): + # new_apertures[apid] = dict() + # for key in val: + # if key == 'geometry': + # new_apertures[apid]['geometry'] = list() + # for elem in val['geometry']: + # geo_elem = dict() + # if 'follow' in elem: + # try: + # geo_elem['clear'] = elem['follow'].buffer(val['size'] / 2.0).exterior + # except AttributeError: + # # TODO should test if width or height is bigger + # geo_elem['clear'] = elem['follow'].buffer(val['width'] / 2.0).exterior + # if 'clear' in elem: + # if isinstance(elem['clear'], Polygon): + # try: + # geo_elem['solid'] = elem['clear'].buffer(val['size'] / 2.0, grb_circle_steps) + # except AttributeError: + # # TODO should test if width or height is bigger + # geo_elem['solid'] = elem['clear'].buffer(val['width'] / 2.0, grb_circle_steps) + # else: + # geo_elem['follow'] = elem['clear'] + # new_apertures[apid]['geometry'].append(deepcopy(geo_elem)) + # else: + # new_apertures[apid][key] = deepcopy(val[key]) + + if '0' not in new_apertures: + new_apertures['0'] = dict() + new_apertures['0']['type'] = 'C' + new_apertures['0']['size'] = 0.0 + new_apertures['0']['geometry'] = list() + + try: + for poly in new_solid_geometry: + new_el = dict() + new_el['solid'] = poly + new_el['follow'] = poly.exterior + new_apertures['0']['geometry'].append(new_el) + except TypeError: + new_el = dict() + new_el['solid'] = new_solid_geometry + new_el['follow'] = new_solid_geometry.exterior + new_apertures['0']['geometry'].append(new_el) + + for td in new_apertures: + print(td, new_apertures[td]) + + def init_func(new_obj, app_obj): + new_obj.options.update(new_options) + new_obj.options['name'] = outname + new_obj.fill_color = deepcopy(grb_obj.fill_color) + new_obj.outline_color = deepcopy(grb_obj.outline_color) + + new_obj.apertures = deepcopy(new_apertures) + + new_obj.solid_geometry = deepcopy(new_solid_geometry) + new_obj.source_file = self.app.export_gerber(obj_name=outname, filename=None, + local_use=new_obj, use_thread=False) + + self.app.new_object('gerber', outname, init_func) + + def reset_fields(self): + self.gerber_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex())) + + @staticmethod + def poly2rings(poly): + return [poly.exterior] + [interior for interior in poly.interiors] +# end of file diff --git a/flatcamTools/ToolPaint.py b/flatcamTools/ToolPaint.py index 5d9780f3..d0a945a1 100644 --- a/flatcamTools/ToolPaint.py +++ b/flatcamTools/ToolPaint.py @@ -358,6 +358,7 @@ class ToolPaint(FlatCAMTool, Gerber): ) self.paintmargin_entry = FCDoubleSpinner() self.paintmargin_entry.set_precision(self.decimals) + self.paintmargin_entry.set_range(-9999.9999, 9999.9999) self.paintmargin_entry.setObjectName(_("Margin")) grid4.addWidget(marginlabel, 2, 0) diff --git a/flatcamTools/ToolPunchGerber.py b/flatcamTools/ToolPunchGerber.py index f7378933..2a5d8ef4 100644 --- a/flatcamTools/ToolPunchGerber.py +++ b/flatcamTools/ToolPunchGerber.py @@ -515,6 +515,8 @@ class ToolPunchGerber(FlatCAMTool): punch_method = self.method_punch.get_value() + new_options = deepcopy(grb_obj.options) + if punch_method == 'exc': # get the Excellon file whose geometry will create the punch holes @@ -574,7 +576,7 @@ class ToolPunchGerber(FlatCAMTool): new_apertures[str(new_apid)] = deepcopy(ap_val) def init_func(new_obj, app_obj): - new_obj.options.update(grb_obj.options) + new_obj.options.update(new_options) new_obj.options['name'] = outname new_obj.fill_color = deepcopy(grb_obj.fill_color) new_obj.outline_color = deepcopy(grb_obj.outline_color) @@ -688,7 +690,7 @@ class ToolPunchGerber(FlatCAMTool): new_apertures[str(new_apid)] = deepcopy(ap_val) def init_func(new_obj, app_obj): - new_obj.options.update(grb_obj.options) + new_obj.options.update(new_options) new_obj.options['name'] = outname new_obj.fill_color = deepcopy(grb_obj.fill_color) new_obj.outline_color = deepcopy(grb_obj.outline_color) @@ -830,7 +832,7 @@ class ToolPunchGerber(FlatCAMTool): new_apertures[str(new_apid)] = deepcopy(ap_val) def init_func(new_obj, app_obj): - new_obj.options.update(grb_obj.options) + new_obj.options.update(new_options) new_obj.options['name'] = outname new_obj.fill_color = deepcopy(grb_obj.fill_color) new_obj.outline_color = deepcopy(grb_obj.outline_color) @@ -969,7 +971,7 @@ class ToolPunchGerber(FlatCAMTool): new_apertures[str(new_apid)] = deepcopy(ap_val) def init_func(new_obj, app_obj): - new_obj.options.update(grb_obj.options) + new_obj.options.update(new_options) new_obj.options['name'] = outname new_obj.fill_color = deepcopy(grb_obj.fill_color) new_obj.outline_color = deepcopy(grb_obj.outline_color) diff --git a/flatcamTools/ToolSub.py b/flatcamTools/ToolSub.py index 37d47ed3..99cc9fa8 100644 --- a/flatcamTools/ToolSub.py +++ b/flatcamTools/ToolSub.py @@ -254,14 +254,14 @@ class ToolSub(FlatCAMTool): FlatCAMTool.run(self) self.set_tool_ui() + self.app.ui.notebook.setTabText(2, _("Sub Tool")) + + def set_tool_ui(self): self.new_apertures.clear() self.new_tools.clear() self.new_solid_geometry = [] self.target_options.clear() - self.app.ui.notebook.setTabText(2, _("Sub Tool")) - - def set_tool_ui(self): self.tools_frame.show() self.close_paths_cb.setChecked(self.app.defaults["tools_sub_close_paths"]) diff --git a/flatcamTools/__init__.py b/flatcamTools/__init__.py index b503ca38..74d1aff5 100644 --- a/flatcamTools/__init__.py +++ b/flatcamTools/__init__.py @@ -39,3 +39,5 @@ from flatcamTools.ToolSub import ToolSub from flatcamTools.ToolTransform import ToolTransform from flatcamTools.ToolPunchGerber import ToolPunchGerber + +from flatcamTools.ToolInvertGerber import ToolInvertGerber diff --git a/share/invert16.png b/share/invert16.png new file mode 100644 index 0000000000000000000000000000000000000000..4246eb5b0a8459c7172e6512f08440d96671b3ae GIT binary patch literal 245 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Y)RhkE)4%caKYZ?lYt_uJY5_^ zEP9VlGUPjCz~d?}>aBOtxrAw@d%{{Pr9#0&HziITImWZR!BBRVhrIT}Ajzzglahbm zzP~Xs_hyIew1m~HzU;L*^(u7@)mx+wFp585d{f+@P{o|pdG&zhE!oyW;UBpN800G4 z#5S|8lPu`ky3OI8aH(@YbX^qpJazoTB<9Xi5 p^7vH0>-n$n&5yZ${-<9?O#d(FW*f>~od9$-gQu&X%Q~loCII{8TloM0 literal 0 HcmV?d00001 diff --git a/share/invert32.png b/share/invert32.png new file mode 100644 index 0000000000000000000000000000000000000000..76e19dff2e2d182b32a97eb7d2b1bb3e041e61b0 GIT binary patch literal 374 zcmV-+0g3*JP)lcCXnS4)^n@9EjD*{o9c0ysSqzyMz%32DTU z*frPATux76oB7oi*Z@2GJpkT-p%b&PBUrv29e@e)?P&p1;7C?>SEUsQ0XP67MScQw z>j0Q2a*d5$#Rm96F1uu&0%x*wQ3;^EhM!V$!8~!k+rU_}uohq+V1Ct0On|cTmzjg| ze`^7Hf#&enBZ4Iz!Y%nKn36d65cZw;DA#bRG*5x|!kSS4t{tw*{2S=h0bs~F+={Hk zQrSNQ^C$qO5Hkekz|xKx`Nk6!z<1KMx)~sbxfj6qC_NGQlJV#jc$PeyS8=087lwGZ U0(5>a?f?J)07*qoM6N<$f)N>><^TWy literal 0 HcmV?d00001